fix(sandbox): use materialized skill paths in startup prompts (#91791)

* fix(sandbox): use materialized skill paths in command prompts

* fix(sandbox): resolve backend prompt workdirs

* fix(sandbox): preserve custom backend prompt fallback

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
brokemac79
2026-06-10 15:35:34 +01:00
committed by GitHub
parent 4ecec2f9e2
commit b71d8e1c32
13 changed files with 397 additions and 63 deletions

View File

@@ -24,6 +24,7 @@ export default definePluginEntry({
manager: createOpenShellSandboxBackendManager({
pluginConfig,
}),
resolveWorkdir: () => pluginConfig.remoteWorkspaceDir,
});
},
});

View File

@@ -96,7 +96,7 @@ describe("resolveSandboxSkillRuntimeInputs", () => {
});
it("rebuilds sandbox prompts from materialized skill paths", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sandbox-skills-"));
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sandbox-skills-"));
try {
const effectiveWorkspace = path.join(root, "workspace");
const materializedWorkspace = path.join(root, "state", "sandbox-skills");
@@ -160,7 +160,9 @@ describe("resolveSandboxSkillRuntimeInputs", () => {
});
expect(prompt).toContain("/workspace/.openclaw/sandbox-skills/skills/demo/SKILL.md");
expect(prompt.replaceAll("\\", "/")).not.toContain(materializedWorkspace.replaceAll("\\", "/"));
expect(prompt.replaceAll("\\", "/")).not.toContain(
materializedWorkspace.replaceAll("\\", "/"),
);
expect(prompt).not.toContain(hostSkillPath);
expect(prompt).not.toContain("plugin-skills");
expect(prompt.replaceAll("\\", "/")).not.toContain("/skills/canvas/SKILL.md");

View File

@@ -201,22 +201,25 @@ describe("resolveSandboxContext", () => {
}, 15_000);
it("resolves a registered non-docker backend", async () => {
const restore = registerSandboxBackend("test-backend", async () => ({
id: "test-backend",
runtimeId: "test-runtime",
runtimeLabel: "Test Runtime",
workdir: "/workspace",
buildExecSpec: async () => ({
argv: ["test-backend", "exec"],
env: process.env,
stdinMode: "pipe-closed",
const restore = registerSandboxBackend("test-backend", {
factory: async () => ({
id: "test-backend",
runtimeId: "test-runtime",
runtimeLabel: "Test Runtime",
workdir: "/runtime/workspace",
buildExecSpec: async () => ({
argv: ["test-backend", "exec"],
env: process.env,
stdinMode: "pipe-closed",
}),
runShellCommand: async () => ({
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
code: 0,
}),
}),
runShellCommand: async () => ({
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
code: 0,
}),
}));
resolveWorkdir: () => "/runtime/workspace",
});
try {
const cfg: OpenClawConfig = {
agents: {
@@ -242,6 +245,13 @@ describe("resolveSandboxContext", () => {
expect(result?.runtimeId).toBe("test-runtime");
expect(result?.containerName).toBe("test-runtime");
expect(result?.backend?.id).toBe("test-backend");
const workspace = await ensureSandboxWorkspaceForSession({
config: cfg,
sessionKey: "agent:worker:task",
workspaceDir: "/tmp/openclaw-test",
});
expect(workspace?.containerWorkdir).toBe("/runtime/workspace");
} finally {
restore();
}
@@ -409,11 +419,50 @@ describe("resolveSandboxContext", () => {
expect(syncOptions?.config).toBe(cfg);
expect(syncOptions?.agentId).toBe("main");
expect(syncOptions?.eligibility).toEqual({ remote: { note: "test-remote" } });
expect(result?.skillsWorkspaceDir).toBe(syncOptions?.targetWorkspaceDir);
expect(result?.workspaceAccess).toBe("rw");
expect(result?.skillsEligibility).toEqual({ remote: { note: "test-remote" } });
await expect(
fs.readFile(path.join(userOwnedSandboxSkillsDir, "SKILL.md"), "utf8"),
).resolves.toBe("# User owned\n");
}, 15_000);
it("uses the SSH backend remote workspace for sandbox workspace info", async () => {
syncSkillsToWorkspaceMock.mockClear();
const workspaceDir = await createSandboxFixtureDir("ssh-workspace");
const cfg: OpenClawConfig = {
agents: {
defaults: {
sandbox: {
mode: "all",
backend: "ssh",
scope: "session",
workspaceAccess: "rw",
ssh: {
target: "ssh.example.test",
workspaceRoot: "/remote/openclaw",
},
},
},
},
};
const result = await ensureSandboxWorkspaceForSession({
config: cfg,
sessionKey: "agent:main:main",
workspaceDir,
});
expect(result?.workspaceDir).toBe(workspaceDir);
expect(result?.containerWorkdir).toMatch(
/^\/remote\/openclaw\/openclaw-ssh-agent-main-main-[a-f0-9]{8}\/workspace$/,
);
expect(result?.containerWorkdir).not.toBe("/workspace");
expect(result?.skillsWorkspaceDir).toContain(
path.join(".openclaw", "sandbox", "skills-workspaces"),
);
}, 15_000);
it("materializes skills for shared writable sandboxes even when roots match", async () => {
syncSkillsToWorkspaceMock.mockClear();
const workspaceDir = await createSandboxFixtureDir("shared-workspace");

View File

@@ -20,6 +20,7 @@ export { ensureSandboxWorkspaceForSession, resolveSandboxContext } from "./sandb
export {
getSandboxBackendFactory,
getSandboxBackendManager,
getSandboxBackendWorkdirResolver,
registerSandboxBackend,
requireSandboxBackendFactory,
} from "./sandbox/backend.js";
@@ -69,6 +70,7 @@ export type {
SandboxBackendManager,
SandboxBackendRegistration,
SandboxBackendRuntimeInfo,
SandboxBackendWorkdirResolver,
} from "./sandbox/backend.js";
export type { RemoteShellSandboxHandle } from "./sandbox/remote-fs-bridge.js";
export type {

View File

@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import {
getSandboxBackendFactory,
getSandboxBackendManager,
getSandboxBackendWorkdirResolver,
registerSandboxBackend,
} from "./backend.js";
@@ -40,4 +41,18 @@ describe("sandbox backend registry", () => {
restore();
expect(getSandboxBackendManager("test-managed")).toBeNull();
});
it("registers backend workdir resolvers alongside factories", () => {
const factory = async () => {
throw new Error("not used");
};
const resolveWorkdir = () => "/runtime/workspace";
const restore = registerSandboxBackend("test-workdir", {
factory,
resolveWorkdir,
});
expect(getSandboxBackendWorkdirResolver("test-workdir")).toBe(resolveWorkdir);
restore();
expect(getSandboxBackendWorkdirResolver("test-workdir")).toBeNull();
});
});

View File

@@ -10,6 +10,7 @@ import type {
SandboxBackendId,
SandboxBackendManager,
SandboxBackendRegistration,
SandboxBackendWorkdirResolver,
} from "./backend.types.js";
export type {
@@ -19,6 +20,7 @@ export type {
SandboxBackendManager,
SandboxBackendRegistration,
SandboxBackendRuntimeInfo,
SandboxBackendWorkdirResolver,
} from "./backend.types.js";
export type {
SandboxBackendCommandParams,
@@ -77,6 +79,11 @@ export function getSandboxBackendManager(id: string): SandboxBackendManager | nu
return getSandboxBackendFactories().get(normalizeSandboxBackendId(id))?.manager ?? null;
}
/** Look up optional backend workdir resolution that does not start the runtime. */
export function getSandboxBackendWorkdirResolver(id: string): SandboxBackendWorkdirResolver | null {
return getSandboxBackendFactories().get(normalizeSandboxBackendId(id))?.resolveWorkdir ?? null;
}
/** Resolve a backend factory or throw the user-facing configuration error. */
export function requireSandboxBackendFactory(id: string): SandboxBackendFactory {
const factory = getSandboxBackendFactory(id);
@@ -92,14 +99,21 @@ export function requireSandboxBackendFactory(id: string): SandboxBackendFactory
}
import { createDockerSandboxBackend, dockerSandboxBackendManager } from "./docker-backend.js";
import { createSshSandboxBackend, sshSandboxBackendManager } from "./ssh-backend.js";
import {
createSshSandboxBackend,
resolveSshRuntimePaths,
sshSandboxBackendManager,
} from "./ssh-backend.js";
registerSandboxBackend("docker", {
factory: createDockerSandboxBackend,
manager: dockerSandboxBackendManager,
resolveWorkdir: ({ cfg }) => cfg.docker.workdir,
});
registerSandboxBackend("ssh", {
factory: createSshSandboxBackend,
manager: sshSandboxBackendManager,
resolveWorkdir: ({ cfg, scopeKey }) =>
resolveSshRuntimePaths(cfg.ssh.workspaceRoot, scopeKey).remoteWorkspaceDir,
});

View File

@@ -44,18 +44,23 @@ export type SandboxBackendFactory = (
params: CreateSandboxBackendParams,
) => Promise<SandboxBackendHandle>;
/** Resolve the runtime workdir without creating or starting the backend. */
export type SandboxBackendWorkdirResolver = (params: CreateSandboxBackendParams) => string;
/** Registry input accepted for sandbox backend registration. */
export type SandboxBackendRegistration =
| SandboxBackendFactory
| {
factory: SandboxBackendFactory;
manager?: SandboxBackendManager;
resolveWorkdir?: SandboxBackendWorkdirResolver;
};
/** Normalized backend registration stored in the sandbox backend registry. */
export type RegisteredSandboxBackend = {
factory: SandboxBackendFactory;
manager?: SandboxBackendManager;
resolveWorkdir?: SandboxBackendWorkdirResolver;
};
export type { SandboxBackendHandle, SandboxBackendId } from "./backend-handle.types.js";

View File

@@ -18,7 +18,7 @@ import { defaultRuntime } from "../../runtime.js";
import type { SkillEligibilityContext } from "../../skills/types.js";
import { resolveUserPath } from "../../utils.js";
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js";
import { requireSandboxBackendFactory } from "./backend.js";
import { getSandboxBackendWorkdirResolver, requireSandboxBackendFactory } from "./backend.js";
import { ensureSandboxBrowser } from "./browser.js";
import { resolveSandboxConfigForAgent } from "./config.js";
import { SANDBOX_STATE_DIR } from "./constants.js";
@@ -178,6 +178,24 @@ function resolveSandboxSession(params: { config?: OpenClawConfig; sessionKey?: s
return { rawSessionKey, runtime, cfg };
}
function resolveSandboxWorkspaceInfoWorkdir(params: {
cfg: ReturnType<typeof resolveSandboxConfigForAgent>;
rawSessionKey: string;
scopeKey: string;
workspaceDir: string;
agentWorkspaceDir: string;
skillsWorkspaceDir: string;
}): string | undefined {
return getSandboxBackendWorkdirResolver(params.cfg.backend)?.({
sessionKey: params.rawSessionKey,
scopeKey: params.scopeKey,
workspaceDir: params.workspaceDir,
agentWorkspaceDir: params.agentWorkspaceDir,
skillsWorkspaceDir: params.skillsWorkspaceDir,
cfg: params.cfg,
});
}
export async function resolveSandboxContext(params: {
config?: OpenClawConfig;
sessionKey?: string;
@@ -308,16 +326,28 @@ export async function ensureSandboxWorkspaceForSession(params: {
}
const { rawSessionKey, cfg, runtime } = resolved;
const { workspaceDir } = await ensureSandboxWorkspaceLayout({
cfg,
agentId: runtime.agentId,
rawSessionKey,
config: params.config,
workspaceDir: params.workspaceDir,
});
const { agentWorkspaceDir, scopeKey, skillsEligibility, skillsWorkspaceDir, workspaceDir } =
await ensureSandboxWorkspaceLayout({
cfg,
agentId: runtime.agentId,
rawSessionKey,
config: params.config,
workspaceDir: params.workspaceDir,
});
const containerWorkdir = resolveSandboxWorkspaceInfoWorkdir({
cfg,
rawSessionKey,
scopeKey,
workspaceDir,
agentWorkspaceDir,
skillsWorkspaceDir,
});
return {
workspaceDir,
containerWorkdir: cfg.docker.workdir,
...(containerWorkdir ? { containerWorkdir } : {}),
skillsWorkspaceDir,
...(skillsEligibility ? { skillsEligibility } : {}),
workspaceAccess: cfg.workspaceAccess,
};
}

View File

@@ -333,7 +333,10 @@ async function isExistingDirectory(dir: string): Promise<boolean> {
}
}
function resolveSshRuntimePaths(workspaceRoot: string, scopeKey: string): ResolvedSshRuntimePaths {
export function resolveSshRuntimePaths(
workspaceRoot: string,
scopeKey: string,
): ResolvedSshRuntimePaths {
const runtimeId = buildSshSandboxRuntimeId(scopeKey);
const runtimeRootDir = path.posix.join(workspaceRoot, runtimeId);
return {

View File

@@ -1,3 +1,4 @@
import type { SkillEligibilityContext } from "../../skills/types.js";
/**
* Sandbox runtime configuration and context types.
*
@@ -6,7 +7,6 @@
import type { SandboxBackendHandle, SandboxBackendId } from "./backend-handle.types.js";
import type { SandboxFsBridge } from "./fs-bridge.types.js";
import type { SandboxDockerConfig } from "./types.docker.js";
import type { SkillEligibilityContext } from "../../skills/types.js";
export type { SandboxDockerConfig } from "./types.docker.js";
@@ -115,5 +115,8 @@ export type SandboxContext = {
export type SandboxWorkspaceInfo = {
workspaceDir: string;
containerWorkdir: string;
containerWorkdir?: string;
skillsWorkspaceDir?: string;
skillsEligibility?: SkillEligibilityContext;
workspaceAccess?: SandboxWorkspaceAccess;
};

View File

@@ -1,10 +1,17 @@
// Tests system prompt command output and bundled prompt section selection.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
import { createOpenClawCodingTools } from "../../agents/agent-tools.js";
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
import {
ensureSandboxWorkspaceForSession,
resolveSandboxRuntimeStatus,
} from "../../agents/sandbox.js";
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
import { resolveReusableWorkspaceSkillSnapshot } from "../../skills/runtime/session-snapshot.js";
import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js";
import type { HandleCommandsParams } from "./commands-types.js";
@@ -20,6 +27,7 @@ vi.mock("../../agents/bootstrap-files.js", () => ({
}));
vi.mock("../../agents/sandbox.js", () => ({
ensureSandboxWorkspaceForSession: vi.fn(async () => null),
resolveSandboxRuntimeStatus: vi.fn(() => ({ sandboxed: false, mode: "off" })),
}));
@@ -130,6 +138,12 @@ describe("resolveCommandsSystemPromptBundle", () => {
vi.clearAllMocks();
createOpenClawCodingToolsMock.mockClear();
createOpenClawCodingToolsMock.mockReturnValue([]);
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue(null);
vi.mocked(resolveReusableWorkspaceSkillSnapshot).mockReturnValue({
snapshot: { prompt: "", skills: [], resolvedSkills: [] },
shouldRefresh: false,
snapshotVersion: "test-snapshot",
} as never);
});
it("opts command tool builds into gateway subagent binding", async () => {
@@ -254,6 +268,87 @@ describe("resolveCommandsSystemPromptBundle", () => {
expect(sandboxInfo?.elevated?.fullAccessBlockedReason).toBe("host-policy");
});
it("uses materialized sandbox skill paths for sandbox command prompts", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-command-sandbox-skills-"));
try {
const workspaceDir = path.join(root, "workspace");
const skillsWorkspaceDir = path.join(root, "state", "sandbox-skills");
const skillDir = path.join(skillsWorkspaceDir, "skills", "gog");
await fs.mkdir(skillDir, { recursive: true });
await fs.writeFile(
path.join(skillDir, "SKILL.md"),
["---", "name: gog", "description: Gog skill", "---", "# Gog", ""].join("\n"),
"utf8",
);
const params = makeParams();
params.workspaceDir = workspaceDir;
vi.mocked(resolveSandboxRuntimeStatus).mockReturnValue({
sandboxed: true,
mode: "workspace-write",
} as never);
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir,
containerWorkdir: "/workspace",
skillsWorkspaceDir,
skillsEligibility: {
remote: {
platforms: ["linux"],
hasBin: () => true,
hasAnyBin: () => true,
note: "sandbox",
},
},
workspaceAccess: "rw",
} as never);
vi.mocked(resolveReusableWorkspaceSkillSnapshot).mockReturnValue({
snapshot: {
prompt:
"<available_skills>~/.npm-global/lib/node_modules/openclaw/skills/gog/SKILL.md</available_skills>",
skills: [],
resolvedSkills: [],
},
shouldRefresh: false,
snapshotVersion: "host-snapshot",
} as never);
const result = await resolveCommandsSystemPromptBundle(params);
expect(result.skillsPrompt).toContain(
"/workspace/.openclaw/sandbox-skills/skills/gog/SKILL.md",
);
expect(result.skillsPrompt).not.toContain("~/.npm-global");
expect(vi.mocked(resolveReusableWorkspaceSkillSnapshot)).not.toHaveBeenCalled();
const promptParams = requireFirstArg(
vi.mocked(buildAgentSystemPrompt),
"buildAgentSystemPrompt",
);
expect(promptParams.skillsPrompt).toContain(
"/workspace/.openclaw/sandbox-skills/skills/gog/SKILL.md",
);
expect(String(promptParams.skillsPrompt)).not.toContain("~/.npm-global");
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
it("preserves host skill snapshots for custom backends without a declared workdir", async () => {
const params = makeParams();
vi.mocked(resolveSandboxRuntimeStatus).mockReturnValue({
sandboxed: true,
mode: "workspace-write",
} as never);
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: params.workspaceDir,
skillsWorkspaceDir: "/tmp/sandbox-skills",
workspaceAccess: "rw",
});
const result = await resolveCommandsSystemPromptBundle(params);
expect(result.skillsPrompt).toBe("");
expect(vi.mocked(resolveReusableWorkspaceSkillSnapshot)).toHaveBeenCalledOnce();
});
it("uses config-backed prompt settings for the target agent", async () => {
vi.mocked(resolveSandboxRuntimeStatus).mockReturnValue({
sandboxed: false,

View File

@@ -5,17 +5,27 @@ import { createOpenClawCodingTools } from "../../agents/agent-tools.js";
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
import type { EmbeddedContextFile } from "../../agents/embedded-agent-helpers.js";
import { resolveEmbeddedFullAccessState } from "../../agents/embedded-agent-runner/sandbox-info.js";
import {
mapSandboxSkillEntriesForPrompt,
resolveSandboxSkillRuntimeInputs,
} from "../../agents/embedded-agent-runner/sandbox-skills.js";
import { canExecRequestNode } from "../../agents/exec-defaults.js";
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
import { resolveAgentPromptSurfaceForSessionKey } from "../../agents/prompt-surface.js";
import type { AgentTool } from "../../agents/runtime/index.js";
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
import {
ensureSandboxWorkspaceForSession,
resolveSandboxRuntimeStatus,
} from "../../agents/sandbox.js";
import { buildConfiguredAgentSystemPrompt } from "../../agents/system-prompt-config.js";
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
import type { WorkspaceBootstrapFile } from "../../agents/workspace.js";
import { listRegisteredPluginAgentPromptGuidance } from "../../plugins/command-registry-state.js";
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
import { getRemoteSkillEligibility } from "../../skills/runtime/remote.js";
import { resolveReusableWorkspaceSkillSnapshot } from "../../skills/runtime/session-snapshot.js";
import type { SkillEligibilityContext } from "../../skills/types.js";
import type { HandleCommandsParams } from "./commands-types.js";
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
@@ -28,6 +38,122 @@ export type CommandsSystemPromptBundle = {
sandboxRuntime: ReturnType<typeof resolveSandboxRuntimeStatus>;
};
function resolveCommandSkillsEligibility(params: {
agentId: string;
config: HandleCommandsParams["cfg"];
sessionEntry: HandleCommandsParams["sessionEntry"] | undefined;
sessionKey: string | undefined;
}): SkillEligibilityContext {
try {
return {
remote: getRemoteSkillEligibility({
advertiseExecNode: canExecRequestNode({
cfg: params.config,
sessionEntry: params.sessionEntry,
sessionKey: params.sessionKey,
agentId: params.agentId,
}),
}),
};
} catch {
try {
return {
remote: getRemoteSkillEligibility({
advertiseExecNode: false,
}),
};
} catch {
return {};
}
}
}
async function resolveCommandSkillsPrompt(params: {
agentId: string;
config: HandleCommandsParams["cfg"];
eligibility: SkillEligibilityContext;
sandboxed: boolean;
sessionKey: string | undefined;
workspaceDir: string;
}): Promise<string> {
if (params.sandboxed) {
try {
// Sandboxed prompt inspection must not fall back to host skill snapshots:
// those paths can be unreadable inside the container.
const sandboxWorkspace = await ensureSandboxWorkspaceForSession({
config: params.config,
sessionKey: params.sessionKey,
workspaceDir: params.workspaceDir,
});
if (!sandboxWorkspace) {
return "";
}
if (sandboxWorkspace.containerWorkdir) {
const {
skillsEligibility,
skillsPromptWorkspaceDir,
skillsSnapshot: skillsSnapshotForRun,
skillsWorkspaceDir,
workspaceOnly,
} = resolveSandboxSkillRuntimeInputs({
sandbox: {
enabled: true,
containerWorkdir: sandboxWorkspace.containerWorkdir,
...(sandboxWorkspace.skillsEligibility
? { skillsEligibility: sandboxWorkspace.skillsEligibility }
: {}),
...(sandboxWorkspace.skillsWorkspaceDir
? { skillsWorkspaceDir: sandboxWorkspace.skillsWorkspaceDir }
: {}),
...(sandboxWorkspace.workspaceAccess
? { workspaceAccess: sandboxWorkspace.workspaceAccess }
: {}),
},
effectiveWorkspace: sandboxWorkspace.workspaceDir,
});
const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries({
workspaceDir: skillsWorkspaceDir,
config: params.config,
agentId: params.agentId,
eligibility: skillsEligibility,
skillsSnapshot: skillsSnapshotForRun,
workspaceOnly,
});
const promptSkillEntries = mapSandboxSkillEntriesForPrompt({
entries: shouldLoadSkillEntries ? skillEntries : undefined,
skillsWorkspaceDir,
skillsPromptWorkspaceDir,
});
return resolveSkillsPromptForRun({
skillsSnapshot: skillsSnapshotForRun,
entries: promptSkillEntries,
config: params.config,
workspaceDir: skillsPromptWorkspaceDir,
agentId: params.agentId,
eligibility: skillsEligibility,
});
}
// Existing third-party backends may not expose the optional workdir
// resolver yet. Preserve their previous host-snapshot inspection path.
} catch {
return "";
}
}
try {
const skillsSnapshot = resolveReusableWorkspaceSkillSnapshot({
workspaceDir: params.workspaceDir,
config: params.config,
agentId: params.agentId,
eligibility: params.eligibility,
watch: false,
});
return skillsSnapshot.snapshot.prompt ?? "";
} catch {
return "";
}
}
export async function resolveCommandsSystemPromptBundle(
params: HandleCommandsParams,
): Promise<CommandsSystemPromptBundle> {
@@ -45,42 +171,29 @@ export async function resolveCommandsSystemPromptBundle(
sessionId: targetSessionEntry?.sessionId,
agentId: sessionAgentId,
});
const sandboxRuntime = resolveSandboxRuntimeStatus({
cfg: params.cfg,
sessionKey: resolveRuntimePolicySessionKey({
cfg: params.cfg,
ctx: params.ctx,
sessionKey: params.sessionKey ?? params.ctx.SessionKey,
}),
});
const toolPolicySessionKey = resolveRuntimePolicySessionKey({
cfg: params.cfg,
ctx: params.ctx,
sessionKey: params.sessionKey,
});
const skillsSnapshot = (() => {
try {
return resolveReusableWorkspaceSkillSnapshot({
workspaceDir,
config: params.cfg,
agentId: sessionAgentId,
eligibility: {
remote: getRemoteSkillEligibility({
advertiseExecNode: canExecRequestNode({
cfg: params.cfg,
sessionEntry: targetSessionEntry,
sessionKey: params.sessionKey,
agentId: sessionAgentId,
}),
}),
},
watch: false,
});
} catch {
return { snapshot: { prompt: "", skills: [], resolvedSkills: [] } };
}
})();
const skillsPrompt = skillsSnapshot.snapshot.prompt ?? "";
const sandboxRuntime = resolveSandboxRuntimeStatus({
cfg: params.cfg,
sessionKey: toolPolicySessionKey,
});
const skillsEligibility = resolveCommandSkillsEligibility({
agentId: sessionAgentId,
config: params.cfg,
sessionEntry: targetSessionEntry,
sessionKey: params.sessionKey,
});
const skillsPrompt = await resolveCommandSkillsPrompt({
agentId: sessionAgentId,
config: params.cfg,
eligibility: skillsEligibility,
sandboxed: sandboxRuntime.sandboxed,
sessionKey: toolPolicySessionKey,
workspaceDir,
});
const tools = (() => {
try {
return createOpenClawCodingTools({

View File

@@ -16,6 +16,7 @@ export type {
SandboxBackendManager,
SandboxBackendRegistration,
SandboxBackendRuntimeInfo,
SandboxBackendWorkdirResolver,
SandboxContext,
SandboxResolvedPath,
SandboxSshConfig,
@@ -36,6 +37,7 @@ export {
disposeSshSandboxSession,
getSandboxBackendFactory,
getSandboxBackendManager,
getSandboxBackendWorkdirResolver,
isToolAllowed,
registerSandboxBackend,
requireSandboxBackendFactory,