@@ -20,6 +20,7 @@ import { hashCliSessionText } from "../cli-session.js";
2020import { resetContextWindowCacheForTest } from "../context.js" ;
2121import { buildActiveImageGenerationTaskPromptContextForSession } from "../image-generation-task-status.js" ;
2222import { buildActiveMusicGenerationTaskPromptContextForSession } from "../music-generation-task-status.js" ;
23+ import type { SandboxWorkspaceInfo } from "../sandbox/types.js" ;
2324import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../system-prompt-cache-boundary.js" ;
2425import { buildActiveVideoGenerationTaskPromptContextForSession } from "../video-generation-task-status.js" ;
2526import {
@@ -29,12 +30,19 @@ import {
2930} from "./prepare.js" ;
3031
3132const getRuntimeConfigMock = vi . hoisted ( ( ) => vi . fn ( ( ) => ( { } ) ) ) ;
33+ const ensureSandboxWorkspaceForSessionMock = vi . hoisted ( ( ) =>
34+ vi . fn < ( ) => Promise < SandboxWorkspaceInfo | null > > ( async ( ) => null ) ,
35+ ) ;
3236let sessionFileEnvSnapshot : ReturnType < typeof captureEnv > | undefined ;
3337
3438vi . mock ( "../../config/config.js" , ( ) => ( {
3539 getRuntimeConfig : getRuntimeConfigMock ,
3640} ) ) ;
3741
42+ vi . mock ( "../sandbox.js" , ( ) => ( {
43+ ensureSandboxWorkspaceForSession : ensureSandboxWorkspaceForSessionMock ,
44+ } ) ) ;
45+
3846vi . mock ( "../../plugins/hook-runner-global.js" , ( ) => ( {
3947 getGlobalHookRunner : vi . fn ( ( ) => null ) ,
4048} ) ) ;
@@ -254,6 +262,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
254262 mockBuildActiveImageGenerationTaskPromptContextForSession . mockReturnValue ( undefined ) ;
255263 mockBuildActiveVideoGenerationTaskPromptContextForSession . mockReturnValue ( undefined ) ;
256264 mockBuildActiveMusicGenerationTaskPromptContextForSession . mockReturnValue ( undefined ) ;
265+ ensureSandboxWorkspaceForSessionMock . mockReset ( ) ;
266+ ensureSandboxWorkspaceForSessionMock . mockResolvedValue ( null ) ;
257267 } ) ;
258268
259269 afterEach ( ( ) => {
@@ -263,6 +273,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
263273 mockBuildActiveImageGenerationTaskPromptContextForSession . mockReset ( ) ;
264274 mockBuildActiveVideoGenerationTaskPromptContextForSession . mockReset ( ) ;
265275 mockBuildActiveMusicGenerationTaskPromptContextForSession . mockReset ( ) ;
276+ ensureSandboxWorkspaceForSessionMock . mockReset ( ) ;
266277 resetContextWindowCacheForTest ( ) ;
267278 clearMemoryPluginState ( ) ;
268279 vi . unstubAllEnvs ( ) ;
@@ -1745,6 +1756,95 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
17451756 }
17461757 } ) ;
17471758
1759+ it ( "renders CLI skills from sandbox-readable paths instead of persisted host snapshots" , async ( ) => {
1760+ const { dir, sessionFile } = createSessionFile ( ) ;
1761+ const hostSkillDir = "/home/tzdai/.npm-global/lib/node_modules/openclaw/skills/gog" ;
1762+ const hostSkillPath = `${ hostSkillDir } /SKILL.md` ;
1763+ const materializedWorkspace = path . join ( dir , "state" , "sandbox-skills" ) ;
1764+ const materializedSkillDir = path . join ( materializedWorkspace , "skills" , "gog" ) ;
1765+ const materializedSkillPath = path . join ( materializedSkillDir , "SKILL.md" ) ;
1766+ fs . mkdirSync ( materializedSkillDir , { recursive : true } ) ;
1767+ fs . writeFileSync (
1768+ materializedSkillPath ,
1769+ [
1770+ "---" ,
1771+ "name: gog" ,
1772+ "description: Read Gmail safely." ,
1773+ "---" ,
1774+ "" ,
1775+ "Use the Gmail tools before answering mail questions." ,
1776+ ] . join ( "\n" ) ,
1777+ "utf-8" ,
1778+ ) ;
1779+ ensureSandboxWorkspaceForSessionMock . mockResolvedValue ( {
1780+ workspaceDir : dir ,
1781+ containerWorkdir : "/workspace" ,
1782+ skillsWorkspaceDir : materializedWorkspace ,
1783+ workspaceAccess : "rw" ,
1784+ } ) ;
1785+
1786+ try {
1787+ const context = await prepareCliRunContext ( {
1788+ sessionId : "session-test" ,
1789+ sessionKey : "agent:main:sandboxed-user" ,
1790+ agentId : "main" ,
1791+ sessionFile,
1792+ workspaceDir : dir ,
1793+ prompt : "are there any unread emails" ,
1794+ provider : "test-cli" ,
1795+ model : "test-model" ,
1796+ timeoutMs : 1_000 ,
1797+ runId : "run-sandbox-cli-skill-prompt" ,
1798+ config : createCliBackendConfig ( ) ,
1799+ skillsSnapshot : {
1800+ prompt : [
1801+ "<available_skills>" ,
1802+ " <skill>" ,
1803+ " <name>gog</name>" ,
1804+ " <description>Read Gmail safely.</description>" ,
1805+ ` <location>${ hostSkillPath } </location>` ,
1806+ " </skill>" ,
1807+ "</available_skills>" ,
1808+ ] . join ( "\n" ) ,
1809+ skills : [ { name : "gog" } ] ,
1810+ resolvedSkills : [
1811+ {
1812+ name : "gog" ,
1813+ description : "Read Gmail safely." ,
1814+ filePath : hostSkillPath ,
1815+ baseDir : hostSkillDir ,
1816+ source : "openclaw-bundled" ,
1817+ sourceInfo : {
1818+ path : hostSkillPath ,
1819+ source : "openclaw-bundled" ,
1820+ scope : "project" ,
1821+ origin : "top-level" ,
1822+ baseDir : hostSkillDir ,
1823+ } ,
1824+ disableModelInvocation : false ,
1825+ } ,
1826+ ] ,
1827+ } ,
1828+ } ) ;
1829+
1830+ expect ( ensureSandboxWorkspaceForSessionMock ) . toHaveBeenCalledWith ( {
1831+ config : createCliBackendConfig ( ) ,
1832+ sessionKey : "agent:main:sandboxed-user" ,
1833+ workspaceDir : dir ,
1834+ } ) ;
1835+ expect ( context . systemPrompt ) . toContain (
1836+ "/workspace/.openclaw/sandbox-skills/skills/gog/SKILL.md" ,
1837+ ) ;
1838+ expect ( context . systemPrompt ) . not . toContain ( hostSkillPath ) ;
1839+ expect ( context . systemPromptReport . skills . promptChars ) . toBeGreaterThan ( 0 ) ;
1840+ expect ( context . systemPromptReport . skills . entries ) . toEqual ( [
1841+ { name : "gog" , blockChars : expect . any ( Number ) } ,
1842+ ] ) ;
1843+ } finally {
1844+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
1845+ }
1846+ } ) ;
1847+
17481848 it ( "omits Claude CLI prompt skills when the native skills plugin can carry them" , async ( ) => {
17491849 const { dir, sessionFile } = createSessionFile ( ) ;
17501850 const skillDir = path . join ( dir , "skills" , "weather" ) ;
0 commit comments