Skip to content

Commit fe162cf

Browse files
committed
fix: report effective sandbox explain workspace
1 parent c730d8f commit fe162cf

4 files changed

Lines changed: 148 additions & 25 deletions

File tree

src/agents/sandbox/context.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* Prepares workspace layout, backend handle, filesystem bridge, browser bridge, and registry state for one run.
55
*/
66
import fs from "node:fs/promises";
7-
import path from "node:path";
87
import type { OpenClawConfig } from "../../config/types.openclaw.js";
98
import {
109
ensureBrowserControlAuth,
@@ -16,18 +15,14 @@ import {
1615
} from "../../plugin-sdk/browser-profiles.js";
1716
import { defaultRuntime } from "../../runtime.js";
1817
import type { SkillEligibilityContext } from "../../skills/types.js";
19-
import { resolveUserPath } from "../../utils.js";
20-
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js";
2118
import { getSandboxBackendWorkdirResolver, requireSandboxBackendFactory } from "./backend.js";
2219
import { ensureSandboxBrowser } from "./browser.js";
2320
import { resolveSandboxConfigForAgent } from "./config.js";
24-
import { SANDBOX_STATE_DIR } from "./constants.js";
2521
import { createSandboxFsBridge } from "./fs-bridge.js";
2622
import { updateRegistry } from "./registry.js";
2723
import { resolveSandboxRuntimeStatus } from "./runtime-status.js";
28-
import { resolveSandboxScopeKey, resolveSandboxWorkspaceDir } from "./shared.js";
24+
import { resolveSandboxWorkspaceLayoutPaths } from "./shared.js";
2925
import type { SandboxContext, SandboxDockerConfig, SandboxWorkspaceInfo } from "./types.js";
30-
import { resolveMaterializedSandboxSkillsWorkspaceDir } from "./workspace-mounts.js";
3126
import { ensureSandboxWorkspace } from "./workspace.js";
3227

3328
async function syncSandboxSkillsToWorkspace(params: {
@@ -83,23 +78,12 @@ async function ensureSandboxWorkspaceLayout(params: {
8378
workspaceDir: string;
8479
}> {
8580
const { cfg, rawSessionKey } = params;
86-
87-
const agentWorkspaceDir = resolveUserPath(
88-
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR,
89-
);
90-
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
91-
const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey);
92-
const sandboxWorkspaceDir =
93-
cfg.scope === "shared" ? workspaceRoot : resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
94-
const workspaceDir = cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir;
95-
const materializedSkillsRoot = resolveSandboxWorkspaceDir(
96-
path.join(SANDBOX_STATE_DIR, "skills-workspaces"),
97-
scopeKey,
98-
);
99-
const skillsWorkspaceDir =
100-
cfg.workspaceAccess === "rw"
101-
? resolveMaterializedSandboxSkillsWorkspaceDir(materializedSkillsRoot)
102-
: sandboxWorkspaceDir;
81+
const { agentWorkspaceDir, sandboxWorkspaceDir, scopeKey, skillsWorkspaceDir, workspaceDir } =
82+
resolveSandboxWorkspaceLayoutPaths({
83+
cfg,
84+
rawSessionKey,
85+
workspaceDir: params.workspaceDir,
86+
});
10387

10488
let skillsEligibility: SkillEligibilityContext | undefined;
10589
if (cfg.workspaceAccess !== "rw") {

src/agents/sandbox/shared.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
88
import { normalizeAgentId } from "../../routing/session-key.js";
99
import { resolveUserPath } from "../../utils.js";
1010
import { resolveAgentIdFromSessionKey } from "../agent-scope.js";
11+
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js";
12+
import { SANDBOX_STATE_DIR } from "./constants.js";
1113
import { hashTextSha256 } from "./hash.js";
14+
import type { SandboxConfig } from "./types.js";
15+
import { resolveMaterializedSandboxSkillsWorkspaceDir } from "./workspace-mounts.js";
1216

1317
/** Converts an arbitrary session key into a bounded filesystem/container-safe slug. */
1418
export function slugifySessionKey(value: string) {
@@ -53,3 +57,38 @@ export function resolveSandboxAgentId(scopeKey: string): string | undefined {
5357
}
5458
return resolveAgentIdFromSessionKey(trimmed);
5559
}
60+
61+
export function resolveSandboxWorkspaceLayoutPaths(params: {
62+
cfg: Pick<SandboxConfig, "scope" | "workspaceAccess" | "workspaceRoot">;
63+
rawSessionKey: string;
64+
workspaceDir?: string;
65+
}) {
66+
const agentWorkspaceDir = resolveUserPath(
67+
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR,
68+
);
69+
const workspaceRoot = resolveUserPath(params.cfg.workspaceRoot);
70+
const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.rawSessionKey);
71+
const sandboxWorkspaceDir =
72+
params.cfg.scope === "shared"
73+
? workspaceRoot
74+
: resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
75+
const workspaceDir =
76+
params.cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir;
77+
const materializedSkillsRoot = resolveSandboxWorkspaceDir(
78+
path.join(SANDBOX_STATE_DIR, "skills-workspaces"),
79+
scopeKey,
80+
);
81+
const skillsWorkspaceDir =
82+
params.cfg.workspaceAccess === "rw"
83+
? resolveMaterializedSandboxSkillsWorkspaceDir(materializedSkillsRoot)
84+
: sandboxWorkspaceDir;
85+
86+
return {
87+
agentWorkspaceDir,
88+
scopeKey,
89+
sandboxWorkspaceDir,
90+
skillsWorkspaceDir,
91+
workspaceDir,
92+
workspaceSource: params.cfg.workspaceAccess === "rw" ? "agent" : "sandbox",
93+
} as const;
94+
}

src/commands/sandbox-explain.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Sandbox explain tests cover command output for sandbox browser and container diagnostics.
2+
import path from "node:path";
23
import { describe, expect, it, vi } from "vitest";
34
import { sandboxExplainCommand } from "./sandbox-explain.js";
45

@@ -99,4 +100,50 @@ describe("sandbox explain command", () => {
99100
key: "agents.list[].tools.sandbox.tools.alsoAllow",
100101
});
101102
});
103+
104+
it("reports the rw agent workspace as the effective workspace mount", async () => {
105+
mockCfg = {
106+
agents: {
107+
defaults: {
108+
sandbox: {
109+
mode: "all",
110+
scope: "agent",
111+
workspaceAccess: "rw",
112+
workspaceRoot: "/tmp/openclaw-sandboxes",
113+
},
114+
},
115+
list: [
116+
{
117+
id: "builder",
118+
workspace: "/tmp/openclaw-agent-workspace",
119+
},
120+
],
121+
},
122+
session: { store: "/tmp/openclaw-test-sessions-{agentId}.json" },
123+
};
124+
125+
const logs: string[] = [];
126+
await sandboxExplainCommand({ json: true, agent: "builder" }, {
127+
log: (msg: string) => logs.push(msg),
128+
error: (msg: string) => logs.push(msg),
129+
exit: (_code: number) => {},
130+
} as unknown as Parameters<typeof sandboxExplainCommand>[1]);
131+
132+
const parsed = JSON.parse(logs.join(""));
133+
const agentWorkspace = path.resolve("/tmp/openclaw-agent-workspace");
134+
expect(parsed.sandbox.workspaceAccess).toBe("rw");
135+
expect(parsed.sandbox.workspaceRoot).toBe(agentWorkspace);
136+
expect(parsed.sandbox.configuredWorkspaceRoot).toBe("/tmp/openclaw-sandboxes");
137+
expect(parsed.sandbox.agentWorkspaceRoot).toBe(agentWorkspace);
138+
expect(parsed.sandbox.workspaceSource).toBe("agent");
139+
expect(parsed.sandbox.containerWorkdir).toBe("/workspace");
140+
expect(parsed.sandbox.workspaceMounts).toEqual([
141+
{
142+
hostRoot: agentWorkspace,
143+
containerRoot: "/workspace",
144+
writable: true,
145+
source: "workspace",
146+
},
147+
]);
148+
});
102149
});

src/commands/sandbox-explain.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { formatDocsLink } from "../../packages/terminal-core/src/links.js";
1212
import { colorize, isRich, theme } from "../../packages/terminal-core/src/theme.js";
1313
import { resolveAgentConfig } from "../agents/agent-scope.js";
1414
import { resolveSandboxConfigForAgent } from "../agents/sandbox.js";
15+
import { getSandboxBackendWorkdirResolver } from "../agents/sandbox/backend.js";
16+
import { buildSandboxFsMounts } from "../agents/sandbox/fs-paths.js";
17+
import { resolveSandboxWorkspaceLayoutPaths } from "../agents/sandbox/shared.js";
1518
import { resolveSandboxToolPolicyForAgent } from "../agents/sandbox/tool-policy.js";
1619
import { normalizeAnyChannelId } from "../channels/registry.js";
1720
import { getRuntimeConfig } from "../config/config.js";
@@ -171,6 +174,33 @@ export async function sandboxExplainCommand(
171174
});
172175

173176
const sandboxCfg = resolveSandboxConfigForAgent(cfg, resolvedAgentId);
177+
const agentConfig = resolveAgentConfig(cfg, resolvedAgentId);
178+
const workspaceLayout = resolveSandboxWorkspaceLayoutPaths({
179+
cfg: sandboxCfg,
180+
rawSessionKey: sessionKey,
181+
workspaceDir: agentConfig?.workspace,
182+
});
183+
const containerWorkdir =
184+
getSandboxBackendWorkdirResolver(sandboxCfg.backend)?.({
185+
sessionKey,
186+
scopeKey: workspaceLayout.scopeKey,
187+
workspaceDir: workspaceLayout.workspaceDir,
188+
agentWorkspaceDir: workspaceLayout.agentWorkspaceDir,
189+
skillsWorkspaceDir: workspaceLayout.skillsWorkspaceDir,
190+
cfg: sandboxCfg,
191+
}) ?? sandboxCfg.docker.workdir;
192+
const workspaceMounts =
193+
sandboxCfg.backend === "docker"
194+
? buildSandboxFsMounts({
195+
workspaceDir: workspaceLayout.workspaceDir,
196+
agentWorkspaceDir: workspaceLayout.agentWorkspaceDir,
197+
skillsWorkspaceDir: workspaceLayout.skillsWorkspaceDir,
198+
workspaceAccess: sandboxCfg.workspaceAccess,
199+
containerName: "",
200+
containerWorkdir,
201+
docker: sandboxCfg.docker,
202+
})
203+
: [];
174204
const toolPolicy = resolveSandboxToolPolicyForAgent(cfg, resolvedAgentId);
175205
const mainSessionKey = resolveAgentMainSessionKey({
176206
cfg,
@@ -189,7 +219,6 @@ export async function sandboxExplainCommand(
189219
sessionKey,
190220
});
191221

192-
const agentConfig = resolveAgentConfig(cfg, resolvedAgentId);
193222
const elevatedGlobal = cfg.tools?.elevated;
194223
const elevatedAgent = agentConfig?.tools?.elevated;
195224
const elevatedGlobalEnabled = elevatedGlobal?.enabled !== false;
@@ -264,7 +293,13 @@ export async function sandboxExplainCommand(
264293
mode: sandboxCfg.mode,
265294
scope: sandboxCfg.scope,
266295
workspaceAccess: sandboxCfg.workspaceAccess,
267-
workspaceRoot: sandboxCfg.workspaceRoot,
296+
workspaceRoot: workspaceLayout.workspaceDir,
297+
configuredWorkspaceRoot: sandboxCfg.workspaceRoot,
298+
sandboxWorkspaceRoot: workspaceLayout.sandboxWorkspaceDir,
299+
agentWorkspaceRoot: workspaceLayout.agentWorkspaceDir,
300+
containerWorkdir,
301+
workspaceMounts,
302+
workspaceSource: workspaceLayout.workspaceSource,
268303
sessionIsSandboxed,
269304
tools: {
270305
allow: toolPolicy.allow,
@@ -318,6 +353,24 @@ export async function sandboxExplainCommand(
318353
payload.sandbox.workspaceAccess,
319354
)} ${key("workspaceRoot:")} ${value(payload.sandbox.workspaceRoot)}`,
320355
);
356+
lines.push(
357+
` ${key("configuredWorkspaceRoot:")} ${value(payload.sandbox.configuredWorkspaceRoot)}`,
358+
);
359+
lines.push(
360+
` ${key("containerWorkdir:")} ${value(
361+
payload.sandbox.containerWorkdir,
362+
)} ${key("workspaceSource:")} ${value(payload.sandbox.workspaceSource)}`,
363+
);
364+
if (payload.sandbox.workspaceMounts.length > 0) {
365+
lines.push(` ${key("workspaceMounts:")}`);
366+
for (const mount of payload.sandbox.workspaceMounts) {
367+
lines.push(
368+
` - ${value(mount.hostRoot)} -> ${value(mount.containerRoot)} ${key(
369+
mount.writable ? "rw" : "ro",
370+
)} ${key(`(${mount.source})`)}`,
371+
);
372+
}
373+
}
321374
lines.push("");
322375
lines.push(heading("Sandbox tool policy:"));
323376
lines.push(

0 commit comments

Comments
 (0)