mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 13:28:10 +00:00
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:
@@ -24,6 +24,7 @@ export default definePluginEntry({
|
||||
manager: createOpenShellSandboxBackendManager({
|
||||
pluginConfig,
|
||||
}),
|
||||
resolveWorkdir: () => pluginConfig.remoteWorkspaceDir,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user