feat(sandbox): add workspace access mode
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
- Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests.
|
- Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests.
|
||||||
- Pairing: lock + atomically write pairing stores with 0600 perms and stop logging pairing codes in provider logs.
|
- Pairing: lock + atomically write pairing stores with 0600 perms and stop logging pairing codes in provider logs.
|
||||||
- Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first).
|
- Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first).
|
||||||
|
- Sandbox: add `agent.sandbox.workspaceAccess` (`none`/`ro`/`rw`) to control agent workspace visibility inside the container; `ro` hard-disables `write`/`edit`.
|
||||||
- Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353.
|
- Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353.
|
||||||
- Tools: unify reaction removal semantics across Discord/Slack/Telegram/WhatsApp and allow WhatsApp reaction routing across accounts.
|
- Tools: unify reaction removal semantics across Discord/Slack/Telegram/WhatsApp and allow WhatsApp reaction routing across accounts.
|
||||||
- Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect.
|
- Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect.
|
||||||
|
|||||||
@@ -867,7 +867,10 @@ sessions so they cannot access your host system.
|
|||||||
Defaults (if enabled):
|
Defaults (if enabled):
|
||||||
- scope: `"agent"` (one container + workspace per agent)
|
- scope: `"agent"` (one container + workspace per agent)
|
||||||
- Debian bookworm-slim based image
|
- Debian bookworm-slim based image
|
||||||
- workspace per agent under `~/.clawdbot/sandboxes`
|
- agent workspace access: `workspaceAccess: "none"` (default)
|
||||||
|
- `"none"`: use a per-scope sandbox workspace under `~/.clawdbot/sandboxes`
|
||||||
|
- `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
||||||
|
- `"rw"`: mount the agent workspace read/write at `/workspace`
|
||||||
- auto-prune: idle > 24h OR age > 7d
|
- auto-prune: idle > 24h OR age > 7d
|
||||||
- tools: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn` (deny wins)
|
- tools: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn` (deny wins)
|
||||||
- optional sandboxed browser (Chromium + CDP, noVNC observer)
|
- optional sandboxed browser (Chromium + CDP, noVNC observer)
|
||||||
@@ -885,6 +888,7 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
|||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "non-main", // off | non-main | all
|
mode: "non-main", // off | non-main | all
|
||||||
scope: "agent", // session | agent | shared (agent is default)
|
scope: "agent", // session | agent | shared (agent is default)
|
||||||
|
workspaceAccess: "none", // none | ro | rw
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes",
|
workspaceRoot: "~/.clawdbot/sandboxes",
|
||||||
docker: {
|
docker: {
|
||||||
image: "clawdbot-sandbox:bookworm-slim",
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
@@ -941,6 +945,8 @@ scripts/sandbox-setup.sh
|
|||||||
Note: sandbox containers default to `network: "none"`; set `agent.sandbox.docker.network`
|
Note: sandbox containers default to `network: "none"`; set `agent.sandbox.docker.network`
|
||||||
to `"bridge"` (or your custom network) if the agent needs outbound access.
|
to `"bridge"` (or your custom network) if the agent needs outbound access.
|
||||||
|
|
||||||
|
Note: inbound attachments are staged into the active workspace at `media/inbound/*`. With `workspaceAccess: "rw"`, that means files are written into the agent workspace.
|
||||||
|
|
||||||
Build the optional browser image with:
|
Build the optional browser image with:
|
||||||
```bash
|
```bash
|
||||||
scripts/sandbox-browser-setup.sh
|
scripts/sandbox-browser-setup.sh
|
||||||
|
|||||||
@@ -146,6 +146,11 @@ Note: to prevent cross-agent access, keep `sandbox.scope` at `"agent"` (default)
|
|||||||
or `"session"` for stricter per-session isolation. `scope: "shared"` uses a
|
or `"session"` for stricter per-session isolation. `scope: "shared"` uses a
|
||||||
single container/workspace.
|
single container/workspace.
|
||||||
|
|
||||||
|
Also consider agent workspace access inside the sandbox:
|
||||||
|
- `agent.sandbox.workspaceAccess: "none"` (default) keeps the agent workspace off-limits; tools run against a sandbox workspace under `~/.clawdbot/sandboxes`
|
||||||
|
- `workspaceAccess: "ro"` mounts the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
||||||
|
- `workspaceAccess: "rw"` mounts the agent workspace read/write at `/workspace`
|
||||||
|
|
||||||
Important: `agent.elevated` is an explicit escape hatch that runs bash on the host. Keep `agent.elevated.allowFrom` tight and don’t enable it for strangers.
|
Important: `agent.elevated` is an explicit escape hatch that runs bash on the host. Keep `agent.elevated.allowFrom` tight and don’t enable it for strangers.
|
||||||
|
|
||||||
## What to Tell Your AI
|
## What to Tell Your AI
|
||||||
|
|||||||
@@ -79,8 +79,9 @@ container. The gateway stays on your host, but the tool execution is isolated:
|
|||||||
- scope: `"agent"` by default (one container + workspace per agent)
|
- scope: `"agent"` by default (one container + workspace per agent)
|
||||||
- scope: `"session"` for per-session isolation
|
- scope: `"session"` for per-session isolation
|
||||||
- per-scope workspace folder mounted at `/workspace`
|
- per-scope workspace folder mounted at `/workspace`
|
||||||
|
- optional agent workspace access (`agent.sandbox.workspaceAccess`)
|
||||||
- allow/deny tool policy (deny wins)
|
- allow/deny tool policy (deny wins)
|
||||||
- inbound media is copied into the sandbox workspace (`media/inbound/*`) so tools can read it
|
- inbound media is copied into the active sandbox workspace (`media/inbound/*`) so tools can read it (with `workspaceAccess: "rw"`, this lands in the agent workspace)
|
||||||
|
|
||||||
Warning: `scope: "shared"` disables cross-session isolation. All sessions share
|
Warning: `scope: "shared"` disables cross-session isolation. All sessions share
|
||||||
one container and one workspace.
|
one container and one workspace.
|
||||||
@@ -89,7 +90,9 @@ one container and one workspace.
|
|||||||
|
|
||||||
- Image: `clawdbot-sandbox:bookworm-slim`
|
- Image: `clawdbot-sandbox:bookworm-slim`
|
||||||
- One container per agent
|
- One container per agent
|
||||||
- Workspace per agent under `~/.clawdbot/sandboxes`
|
- Agent workspace access: `workspaceAccess: "none"` (default) uses `~/.clawdbot/sandboxes`
|
||||||
|
- `"ro"` keeps the sandbox workspace at `/workspace` and mounts the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
||||||
|
- `"rw"` mounts the agent workspace read/write at `/workspace`
|
||||||
- Auto-prune: idle > 24h OR age > 7d
|
- Auto-prune: idle > 24h OR age > 7d
|
||||||
- Network: `none` by default (explicitly opt-in if you need egress)
|
- Network: `none` by default (explicitly opt-in if you need egress)
|
||||||
- Default allow: `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`
|
- Default allow: `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`
|
||||||
@@ -103,6 +106,7 @@ one container and one workspace.
|
|||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "non-main", // off | non-main | all
|
mode: "non-main", // off | non-main | all
|
||||||
scope: "agent", // session | agent | shared (agent is default)
|
scope: "agent", // session | agent | shared (agent is default)
|
||||||
|
workspaceAccess: "none", // none | ro | rw
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes",
|
workspaceRoot: "~/.clawdbot/sandboxes",
|
||||||
docker: {
|
docker: {
|
||||||
image: "clawdbot-sandbox:bookworm-slim",
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
|
|||||||
@@ -132,6 +132,20 @@ describe("bash tool backgrounding", () => {
|
|||||||
).rejects.toThrow("elevated is not available right now.");
|
).rejects.toThrow("elevated is not available right now.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not default to elevated when not allowed", async () => {
|
||||||
|
const customBash = createBashTool({
|
||||||
|
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
|
||||||
|
backgroundMs: 1000,
|
||||||
|
timeoutSec: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await customBash.execute("call1", {
|
||||||
|
command: "echo hi",
|
||||||
|
});
|
||||||
|
const text = result.content.find((c) => c.type === "text")?.text ?? "";
|
||||||
|
expect(text).toContain("hi");
|
||||||
|
});
|
||||||
|
|
||||||
it("logs line-based slices and defaults to last lines", async () => {
|
it("logs line-based slices and defaults to last lines", async () => {
|
||||||
const result = await bashTool.execute("call1", {
|
const result = await bashTool.execute("call1", {
|
||||||
command:
|
command:
|
||||||
|
|||||||
@@ -165,10 +165,14 @@ export function createBashTool(
|
|||||||
const sessionId = randomUUID();
|
const sessionId = randomUUID();
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const elevatedDefaults = defaults?.elevated;
|
const elevatedDefaults = defaults?.elevated;
|
||||||
|
const elevatedDefaultOn =
|
||||||
|
elevatedDefaults?.defaultLevel === "on" &&
|
||||||
|
elevatedDefaults.enabled &&
|
||||||
|
elevatedDefaults.allowed;
|
||||||
const elevatedRequested =
|
const elevatedRequested =
|
||||||
typeof params.elevated === "boolean"
|
typeof params.elevated === "boolean"
|
||||||
? params.elevated
|
? params.elevated
|
||||||
: elevatedDefaults?.defaultLevel === "on";
|
: elevatedDefaultOn;
|
||||||
if (elevatedRequested) {
|
if (elevatedRequested) {
|
||||||
if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) {
|
if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) {
|
||||||
throw new Error("elevated is not available right now.");
|
throw new Error("elevated is not available right now.");
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ describe("buildEmbeddedSandboxInfo", () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sessionKey: "session:test",
|
sessionKey: "session:test",
|
||||||
workspaceDir: "/tmp/clawdbot-sandbox",
|
workspaceDir: "/tmp/clawdbot-sandbox",
|
||||||
|
agentWorkspaceDir: "/tmp/clawdbot-workspace",
|
||||||
|
workspaceAccess: "none",
|
||||||
containerName: "clawdbot-sbx-test",
|
containerName: "clawdbot-sbx-test",
|
||||||
containerWorkdir: "/workspace",
|
containerWorkdir: "/workspace",
|
||||||
docker: {
|
docker: {
|
||||||
@@ -44,6 +46,8 @@ describe("buildEmbeddedSandboxInfo", () => {
|
|||||||
expect(buildEmbeddedSandboxInfo(sandbox)).toEqual({
|
expect(buildEmbeddedSandboxInfo(sandbox)).toEqual({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
workspaceDir: "/tmp/clawdbot-sandbox",
|
workspaceDir: "/tmp/clawdbot-sandbox",
|
||||||
|
workspaceAccess: "none",
|
||||||
|
agentWorkspaceMount: undefined,
|
||||||
browserControlUrl: "http://localhost:9222",
|
browserControlUrl: "http://localhost:9222",
|
||||||
browserNoVncUrl: "http://localhost:6080",
|
browserNoVncUrl: "http://localhost:6080",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ const isAbortError = (err: unknown): boolean => {
|
|||||||
type EmbeddedSandboxInfo = {
|
type EmbeddedSandboxInfo = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
|
workspaceAccess?: "none" | "ro" | "rw";
|
||||||
|
agentWorkspaceMount?: string;
|
||||||
browserControlUrl?: string;
|
browserControlUrl?: string;
|
||||||
browserNoVncUrl?: string;
|
browserNoVncUrl?: string;
|
||||||
};
|
};
|
||||||
@@ -249,6 +251,9 @@ export function buildEmbeddedSandboxInfo(
|
|||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
workspaceDir: sandbox.workspaceDir,
|
workspaceDir: sandbox.workspaceDir,
|
||||||
|
workspaceAccess: sandbox.workspaceAccess,
|
||||||
|
agentWorkspaceMount:
|
||||||
|
sandbox.workspaceAccess === "ro" ? "/agent" : undefined,
|
||||||
browserControlUrl: sandbox.browser?.controlUrl,
|
browserControlUrl: sandbox.browser?.controlUrl,
|
||||||
browserNoVncUrl: sandbox.browser?.noVncUrl,
|
browserNoVncUrl: sandbox.browser?.noVncUrl,
|
||||||
};
|
};
|
||||||
@@ -466,32 +471,38 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||||
|
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
||||||
|
const sandbox = await resolveSandboxContext({
|
||||||
|
config: params.config,
|
||||||
|
sessionKey: sandboxSessionKey,
|
||||||
|
workspaceDir: resolvedWorkspace,
|
||||||
|
});
|
||||||
|
const effectiveWorkspace = sandbox?.enabled
|
||||||
|
? sandbox.workspaceAccess === "rw"
|
||||||
|
? resolvedWorkspace
|
||||||
|
: sandbox.workspaceDir
|
||||||
|
: resolvedWorkspace;
|
||||||
|
await fs.mkdir(effectiveWorkspace, { recursive: true });
|
||||||
await ensureSessionHeader({
|
await ensureSessionHeader({
|
||||||
sessionFile: params.sessionFile,
|
sessionFile: params.sessionFile,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
cwd: resolvedWorkspace,
|
cwd: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
let restoreSkillEnv: (() => void) | undefined;
|
let restoreSkillEnv: (() => void) | undefined;
|
||||||
process.chdir(resolvedWorkspace);
|
process.chdir(effectiveWorkspace);
|
||||||
try {
|
try {
|
||||||
const shouldLoadSkillEntries =
|
const shouldLoadSkillEntries =
|
||||||
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
||||||
const skillEntries = shouldLoadSkillEntries
|
const skillEntries = shouldLoadSkillEntries
|
||||||
? loadWorkspaceSkillEntries(resolvedWorkspace)
|
? loadWorkspaceSkillEntries(effectiveWorkspace)
|
||||||
: [];
|
: [];
|
||||||
const skillsSnapshot =
|
const skillsSnapshot =
|
||||||
params.skillsSnapshot ??
|
params.skillsSnapshot ??
|
||||||
buildWorkspaceSkillSnapshot(resolvedWorkspace, {
|
buildWorkspaceSkillSnapshot(effectiveWorkspace, {
|
||||||
config: params.config,
|
config: params.config,
|
||||||
entries: skillEntries,
|
entries: skillEntries,
|
||||||
});
|
});
|
||||||
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
|
||||||
const sandbox = await resolveSandboxContext({
|
|
||||||
config: params.config,
|
|
||||||
sessionKey: sandboxSessionKey,
|
|
||||||
workspaceDir: resolvedWorkspace,
|
|
||||||
});
|
|
||||||
restoreSkillEnv = params.skillsSnapshot
|
restoreSkillEnv = params.skillsSnapshot
|
||||||
? applySkillEnvOverridesFromSnapshot({
|
? applySkillEnvOverridesFromSnapshot({
|
||||||
snapshot: params.skillsSnapshot,
|
snapshot: params.skillsSnapshot,
|
||||||
@@ -503,7 +514,7 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const bootstrapFiles =
|
const bootstrapFiles =
|
||||||
await loadWorkspaceBootstrapFiles(resolvedWorkspace);
|
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
@@ -533,7 +544,7 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
const systemPrompt = buildSystemPrompt({
|
const systemPrompt = buildSystemPrompt({
|
||||||
appendPrompt: buildAgentSystemPromptAppend({
|
appendPrompt: buildAgentSystemPromptAppend({
|
||||||
workspaceDir: resolvedWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
defaultThinkLevel: params.thinkLevel,
|
defaultThinkLevel: params.thinkLevel,
|
||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
ownerNumbers: params.ownerNumbers,
|
ownerNumbers: params.ownerNumbers,
|
||||||
@@ -550,13 +561,13 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
}),
|
}),
|
||||||
contextFiles,
|
contextFiles,
|
||||||
skills: promptSkills,
|
skills: promptSkills,
|
||||||
cwd: resolvedWorkspace,
|
cwd: effectiveWorkspace,
|
||||||
tools,
|
tools,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionManager = SessionManager.open(params.sessionFile);
|
const sessionManager = SessionManager.open(params.sessionFile);
|
||||||
const settingsManager = SettingsManager.create(
|
const settingsManager = SettingsManager.create(
|
||||||
resolvedWorkspace,
|
effectiveWorkspace,
|
||||||
agentDir,
|
agentDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -760,33 +771,38 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||||
|
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
||||||
|
const sandbox = await resolveSandboxContext({
|
||||||
|
config: params.config,
|
||||||
|
sessionKey: sandboxSessionKey,
|
||||||
|
workspaceDir: resolvedWorkspace,
|
||||||
|
});
|
||||||
|
const effectiveWorkspace = sandbox?.enabled
|
||||||
|
? sandbox.workspaceAccess === "rw"
|
||||||
|
? resolvedWorkspace
|
||||||
|
: sandbox.workspaceDir
|
||||||
|
: resolvedWorkspace;
|
||||||
|
await fs.mkdir(effectiveWorkspace, { recursive: true });
|
||||||
await ensureSessionHeader({
|
await ensureSessionHeader({
|
||||||
sessionFile: params.sessionFile,
|
sessionFile: params.sessionFile,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
cwd: resolvedWorkspace,
|
cwd: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
let restoreSkillEnv: (() => void) | undefined;
|
let restoreSkillEnv: (() => void) | undefined;
|
||||||
process.chdir(resolvedWorkspace);
|
process.chdir(effectiveWorkspace);
|
||||||
try {
|
try {
|
||||||
const shouldLoadSkillEntries =
|
const shouldLoadSkillEntries =
|
||||||
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
||||||
const skillEntries = shouldLoadSkillEntries
|
const skillEntries = shouldLoadSkillEntries
|
||||||
? loadWorkspaceSkillEntries(resolvedWorkspace)
|
? loadWorkspaceSkillEntries(effectiveWorkspace)
|
||||||
: [];
|
: [];
|
||||||
const skillsSnapshot =
|
const skillsSnapshot =
|
||||||
params.skillsSnapshot ??
|
params.skillsSnapshot ??
|
||||||
buildWorkspaceSkillSnapshot(resolvedWorkspace, {
|
buildWorkspaceSkillSnapshot(effectiveWorkspace, {
|
||||||
config: params.config,
|
config: params.config,
|
||||||
entries: skillEntries,
|
entries: skillEntries,
|
||||||
});
|
});
|
||||||
const sandboxSessionKey =
|
|
||||||
params.sessionKey?.trim() || params.sessionId;
|
|
||||||
const sandbox = await resolveSandboxContext({
|
|
||||||
config: params.config,
|
|
||||||
sessionKey: sandboxSessionKey,
|
|
||||||
workspaceDir: resolvedWorkspace,
|
|
||||||
});
|
|
||||||
restoreSkillEnv = params.skillsSnapshot
|
restoreSkillEnv = params.skillsSnapshot
|
||||||
? applySkillEnvOverridesFromSnapshot({
|
? applySkillEnvOverridesFromSnapshot({
|
||||||
snapshot: params.skillsSnapshot,
|
snapshot: params.skillsSnapshot,
|
||||||
@@ -798,7 +814,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const bootstrapFiles =
|
const bootstrapFiles =
|
||||||
await loadWorkspaceBootstrapFiles(resolvedWorkspace);
|
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const promptSkills = resolvePromptSkills(
|
const promptSkills = resolvePromptSkills(
|
||||||
skillsSnapshot,
|
skillsSnapshot,
|
||||||
@@ -833,7 +849,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
const systemPrompt = buildSystemPrompt({
|
const systemPrompt = buildSystemPrompt({
|
||||||
appendPrompt: buildAgentSystemPromptAppend({
|
appendPrompt: buildAgentSystemPromptAppend({
|
||||||
workspaceDir: resolvedWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
defaultThinkLevel: thinkLevel,
|
defaultThinkLevel: thinkLevel,
|
||||||
extraSystemPrompt: params.extraSystemPrompt,
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
ownerNumbers: params.ownerNumbers,
|
ownerNumbers: params.ownerNumbers,
|
||||||
@@ -850,13 +866,13 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
}),
|
}),
|
||||||
contextFiles,
|
contextFiles,
|
||||||
skills: promptSkills,
|
skills: promptSkills,
|
||||||
cwd: resolvedWorkspace,
|
cwd: effectiveWorkspace,
|
||||||
tools,
|
tools,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionManager = SessionManager.open(params.sessionFile);
|
const sessionManager = SessionManager.open(params.sessionFile);
|
||||||
const settingsManager = SettingsManager.create(
|
const settingsManager = SettingsManager.create(
|
||||||
resolvedWorkspace,
|
effectiveWorkspace,
|
||||||
agentDir,
|
agentDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -240,6 +240,8 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sessionKey: "sandbox:test",
|
sessionKey: "sandbox:test",
|
||||||
workspaceDir: path.join(os.tmpdir(), "clawdbot-sandbox"),
|
workspaceDir: path.join(os.tmpdir(), "clawdbot-sandbox"),
|
||||||
|
agentWorkspaceDir: path.join(os.tmpdir(), "clawdbot-workspace"),
|
||||||
|
workspaceAccess: "none",
|
||||||
containerName: "clawdbot-sbx-test",
|
containerName: "clawdbot-sbx-test",
|
||||||
containerWorkdir: "/workspace",
|
containerWorkdir: "/workspace",
|
||||||
docker: {
|
docker: {
|
||||||
@@ -264,6 +266,37 @@ describe("createClawdbotCodingTools", () => {
|
|||||||
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hard-disables write/edit when sandbox workspaceAccess is ro", () => {
|
||||||
|
const sandbox = {
|
||||||
|
enabled: true,
|
||||||
|
sessionKey: "sandbox:test",
|
||||||
|
workspaceDir: path.join(os.tmpdir(), "clawdbot-sandbox"),
|
||||||
|
agentWorkspaceDir: path.join(os.tmpdir(), "clawdbot-workspace"),
|
||||||
|
workspaceAccess: "ro",
|
||||||
|
containerName: "clawdbot-sbx-test",
|
||||||
|
containerWorkdir: "/workspace",
|
||||||
|
docker: {
|
||||||
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
|
containerPrefix: "clawdbot-sbx-",
|
||||||
|
workdir: "/workspace",
|
||||||
|
readOnlyRoot: true,
|
||||||
|
tmpfs: [],
|
||||||
|
network: "none",
|
||||||
|
user: "1000:1000",
|
||||||
|
capDrop: ["ALL"],
|
||||||
|
env: { LANG: "C.UTF-8" },
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
allow: ["read", "write", "edit"],
|
||||||
|
deny: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const tools = createClawdbotCodingTools({ sandbox });
|
||||||
|
expect(tools.some((tool) => tool.name === "read")).toBe(true);
|
||||||
|
expect(tools.some((tool) => tool.name === "write")).toBe(false);
|
||||||
|
expect(tools.some((tool) => tool.name === "edit")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("filters tools by agent tool policy even without sandbox", () => {
|
it("filters tools by agent tool policy even without sandbox", () => {
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
config: { agent: { tools: { deny: ["browser"] } } },
|
config: { agent: { tools: { deny: ["browser"] } } },
|
||||||
|
|||||||
@@ -526,6 +526,7 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
const bashToolName = "bash";
|
const bashToolName = "bash";
|
||||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||||
const sandboxRoot = sandbox?.workspaceDir;
|
const sandboxRoot = sandbox?.workspaceDir;
|
||||||
|
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
|
||||||
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
||||||
if (tool.name === readTool.name) {
|
if (tool.name === readTool.name) {
|
||||||
return sandboxRoot
|
return sandboxRoot
|
||||||
@@ -555,10 +556,12 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
const tools: AnyAgentTool[] = [
|
const tools: AnyAgentTool[] = [
|
||||||
...base,
|
...base,
|
||||||
...(sandboxRoot
|
...(sandboxRoot
|
||||||
? [
|
? allowWorkspaceWrites
|
||||||
createSandboxedEditTool(sandboxRoot),
|
? [
|
||||||
createSandboxedWriteTool(sandboxRoot),
|
createSandboxedEditTool(sandboxRoot),
|
||||||
]
|
createSandboxedWriteTool(sandboxRoot),
|
||||||
|
]
|
||||||
|
: []
|
||||||
: []),
|
: []),
|
||||||
bashTool as unknown as AnyAgentTool,
|
bashTool as unknown as AnyAgentTool,
|
||||||
processTool as unknown as AnyAgentTool,
|
processTool as unknown as AnyAgentTool,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
DEFAULT_AGENTS_FILENAME,
|
DEFAULT_AGENTS_FILENAME,
|
||||||
DEFAULT_BOOTSTRAP_FILENAME,
|
DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
|
DEFAULT_HEARTBEAT_FILENAME,
|
||||||
DEFAULT_IDENTITY_FILENAME,
|
DEFAULT_IDENTITY_FILENAME,
|
||||||
DEFAULT_SOUL_FILENAME,
|
DEFAULT_SOUL_FILENAME,
|
||||||
DEFAULT_TOOLS_FILENAME,
|
DEFAULT_TOOLS_FILENAME,
|
||||||
@@ -35,6 +36,8 @@ export type SandboxToolPolicy = {
|
|||||||
deny?: string[];
|
deny?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SandboxWorkspaceAccess = "none" | "ro" | "rw";
|
||||||
|
|
||||||
export type SandboxBrowserConfig = {
|
export type SandboxBrowserConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
image: string;
|
image: string;
|
||||||
@@ -78,6 +81,7 @@ export type SandboxScope = "session" | "agent" | "shared";
|
|||||||
export type SandboxConfig = {
|
export type SandboxConfig = {
|
||||||
mode: "off" | "non-main" | "all";
|
mode: "off" | "non-main" | "all";
|
||||||
scope: SandboxScope;
|
scope: SandboxScope;
|
||||||
|
workspaceAccess: SandboxWorkspaceAccess;
|
||||||
workspaceRoot: string;
|
workspaceRoot: string;
|
||||||
docker: SandboxDockerConfig;
|
docker: SandboxDockerConfig;
|
||||||
browser: SandboxBrowserConfig;
|
browser: SandboxBrowserConfig;
|
||||||
@@ -95,6 +99,8 @@ export type SandboxContext = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
|
agentWorkspaceDir: string;
|
||||||
|
workspaceAccess: SandboxWorkspaceAccess;
|
||||||
containerName: string;
|
containerName: string;
|
||||||
containerWorkdir: string;
|
containerWorkdir: string;
|
||||||
docker: SandboxDockerConfig;
|
docker: SandboxDockerConfig;
|
||||||
@@ -144,6 +150,7 @@ const DEFAULT_SANDBOX_BROWSER_PREFIX = "clawdbot-sbx-browser-";
|
|||||||
const DEFAULT_SANDBOX_BROWSER_CDP_PORT = 9222;
|
const DEFAULT_SANDBOX_BROWSER_CDP_PORT = 9222;
|
||||||
const DEFAULT_SANDBOX_BROWSER_VNC_PORT = 5900;
|
const DEFAULT_SANDBOX_BROWSER_VNC_PORT = 5900;
|
||||||
const DEFAULT_SANDBOX_BROWSER_NOVNC_PORT = 6080;
|
const DEFAULT_SANDBOX_BROWSER_NOVNC_PORT = 6080;
|
||||||
|
const SANDBOX_AGENT_WORKSPACE_MOUNT = "/agent";
|
||||||
|
|
||||||
const SANDBOX_STATE_DIR = path.join(STATE_DIR_CLAWDBOT, "sandbox");
|
const SANDBOX_STATE_DIR = path.join(STATE_DIR_CLAWDBOT, "sandbox");
|
||||||
const SANDBOX_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "containers.json");
|
const SANDBOX_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "containers.json");
|
||||||
@@ -227,6 +234,7 @@ function defaultSandboxConfig(cfg?: ClawdbotConfig): SandboxConfig {
|
|||||||
scope: agent?.scope,
|
scope: agent?.scope,
|
||||||
perSession: agent?.perSession,
|
perSession: agent?.perSession,
|
||||||
}),
|
}),
|
||||||
|
workspaceAccess: agent?.workspaceAccess ?? "none",
|
||||||
workspaceRoot: agent?.workspaceRoot ?? DEFAULT_SANDBOX_WORKSPACE_ROOT,
|
workspaceRoot: agent?.workspaceRoot ?? DEFAULT_SANDBOX_WORKSPACE_ROOT,
|
||||||
docker: {
|
docker: {
|
||||||
image: agent?.docker?.image ?? DEFAULT_SANDBOX_IMAGE,
|
image: agent?.docker?.image ?? DEFAULT_SANDBOX_IMAGE,
|
||||||
@@ -474,6 +482,7 @@ async function ensureSandboxWorkspace(
|
|||||||
DEFAULT_IDENTITY_FILENAME,
|
DEFAULT_IDENTITY_FILENAME,
|
||||||
DEFAULT_USER_FILENAME,
|
DEFAULT_USER_FILENAME,
|
||||||
DEFAULT_BOOTSTRAP_FILENAME,
|
DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
|
DEFAULT_HEARTBEAT_FILENAME,
|
||||||
];
|
];
|
||||||
for (const name of files) {
|
for (const name of files) {
|
||||||
const src = path.join(seed, name);
|
const src = path.join(seed, name);
|
||||||
@@ -582,6 +591,8 @@ async function createSandboxContainer(params: {
|
|||||||
name: string;
|
name: string;
|
||||||
cfg: SandboxDockerConfig;
|
cfg: SandboxDockerConfig;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
|
workspaceAccess: SandboxWorkspaceAccess;
|
||||||
|
agentWorkspaceDir: string;
|
||||||
scopeKey: string;
|
scopeKey: string;
|
||||||
}) {
|
}) {
|
||||||
const { name, cfg, workspaceDir, scopeKey } = params;
|
const { name, cfg, workspaceDir, scopeKey } = params;
|
||||||
@@ -593,7 +604,21 @@ async function createSandboxContainer(params: {
|
|||||||
scopeKey,
|
scopeKey,
|
||||||
});
|
});
|
||||||
args.push("--workdir", cfg.workdir);
|
args.push("--workdir", cfg.workdir);
|
||||||
args.push("-v", `${workspaceDir}:${cfg.workdir}`);
|
const mainMountSuffix =
|
||||||
|
params.workspaceAccess === "ro" && workspaceDir === params.agentWorkspaceDir
|
||||||
|
? ":ro"
|
||||||
|
: "";
|
||||||
|
args.push("-v", `${workspaceDir}:${cfg.workdir}${mainMountSuffix}`);
|
||||||
|
if (
|
||||||
|
params.workspaceAccess !== "none" &&
|
||||||
|
workspaceDir !== params.agentWorkspaceDir
|
||||||
|
) {
|
||||||
|
const agentMountSuffix = params.workspaceAccess === "ro" ? ":ro" : "";
|
||||||
|
args.push(
|
||||||
|
"-v",
|
||||||
|
`${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
args.push(cfg.image, "sleep", "infinity");
|
args.push(cfg.image, "sleep", "infinity");
|
||||||
|
|
||||||
await execDocker(args);
|
await execDocker(args);
|
||||||
@@ -607,6 +632,7 @@ async function createSandboxContainer(params: {
|
|||||||
async function ensureSandboxContainer(params: {
|
async function ensureSandboxContainer(params: {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
|
agentWorkspaceDir: string;
|
||||||
cfg: SandboxConfig;
|
cfg: SandboxConfig;
|
||||||
}) {
|
}) {
|
||||||
const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey);
|
const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey);
|
||||||
@@ -620,6 +646,8 @@ async function ensureSandboxContainer(params: {
|
|||||||
name: containerName,
|
name: containerName,
|
||||||
cfg: params.cfg.docker,
|
cfg: params.cfg.docker,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
|
workspaceAccess: params.cfg.workspaceAccess,
|
||||||
|
agentWorkspaceDir: params.agentWorkspaceDir,
|
||||||
scopeKey,
|
scopeKey,
|
||||||
});
|
});
|
||||||
} else if (!state.running) {
|
} else if (!state.running) {
|
||||||
@@ -675,6 +703,7 @@ function buildSandboxBrowserResolvedConfig(params: {
|
|||||||
async function ensureSandboxBrowser(params: {
|
async function ensureSandboxBrowser(params: {
|
||||||
scopeKey: string;
|
scopeKey: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
|
agentWorkspaceDir: string;
|
||||||
cfg: SandboxConfig;
|
cfg: SandboxConfig;
|
||||||
}): Promise<SandboxBrowserContext | null> {
|
}): Promise<SandboxBrowserContext | null> {
|
||||||
if (!params.cfg.browser.enabled) return null;
|
if (!params.cfg.browser.enabled) return null;
|
||||||
@@ -695,7 +724,25 @@ async function ensureSandboxBrowser(params: {
|
|||||||
scopeKey: params.scopeKey,
|
scopeKey: params.scopeKey,
|
||||||
labels: { "clawdbot.sandboxBrowser": "1" },
|
labels: { "clawdbot.sandboxBrowser": "1" },
|
||||||
});
|
});
|
||||||
args.push("-v", `${params.workspaceDir}:${params.cfg.docker.workdir}`);
|
const mainMountSuffix =
|
||||||
|
params.cfg.workspaceAccess === "ro" &&
|
||||||
|
params.workspaceDir === params.agentWorkspaceDir
|
||||||
|
? ":ro"
|
||||||
|
: "";
|
||||||
|
args.push(
|
||||||
|
"-v",
|
||||||
|
`${params.workspaceDir}:${params.cfg.docker.workdir}${mainMountSuffix}`,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
params.cfg.workspaceAccess !== "none" &&
|
||||||
|
params.workspaceDir !== params.agentWorkspaceDir
|
||||||
|
) {
|
||||||
|
const agentMountSuffix = params.cfg.workspaceAccess === "ro" ? ":ro" : "";
|
||||||
|
args.push(
|
||||||
|
"-v",
|
||||||
|
`${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`);
|
args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`);
|
||||||
if (params.cfg.browser.enableNoVnc && !params.cfg.browser.headless) {
|
if (params.cfg.browser.enableNoVnc && !params.cfg.browser.headless) {
|
||||||
args.push("-p", `127.0.0.1::${params.cfg.browser.noVncPort}`);
|
args.push("-p", `127.0.0.1::${params.cfg.browser.noVncPort}`);
|
||||||
@@ -883,29 +930,38 @@ export async function resolveSandboxContext(params: {
|
|||||||
|
|
||||||
await maybePruneSandboxes(cfg);
|
await maybePruneSandboxes(cfg);
|
||||||
|
|
||||||
|
const agentWorkspaceDir = resolveUserPath(
|
||||||
|
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
|
);
|
||||||
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
||||||
const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey);
|
const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey);
|
||||||
const workspaceDir =
|
const sandboxWorkspaceDir =
|
||||||
cfg.scope === "shared"
|
cfg.scope === "shared"
|
||||||
? workspaceRoot
|
? workspaceRoot
|
||||||
: resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
|
: resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
|
||||||
const seedWorkspace =
|
const workspaceDir =
|
||||||
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR;
|
cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir;
|
||||||
await ensureSandboxWorkspace(
|
if (workspaceDir === sandboxWorkspaceDir) {
|
||||||
workspaceDir,
|
await ensureSandboxWorkspace(
|
||||||
seedWorkspace,
|
sandboxWorkspaceDir,
|
||||||
params.config?.agent?.skipBootstrap,
|
agentWorkspaceDir,
|
||||||
);
|
params.config?.agent?.skipBootstrap,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const containerName = await ensureSandboxContainer({
|
const containerName = await ensureSandboxContainer({
|
||||||
sessionKey: rawSessionKey,
|
sessionKey: rawSessionKey,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
agentWorkspaceDir,
|
||||||
cfg,
|
cfg,
|
||||||
});
|
});
|
||||||
|
|
||||||
const browser = await ensureSandboxBrowser({
|
const browser = await ensureSandboxBrowser({
|
||||||
scopeKey,
|
scopeKey,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
agentWorkspaceDir,
|
||||||
cfg,
|
cfg,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -913,6 +969,8 @@ export async function resolveSandboxContext(params: {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sessionKey: rawSessionKey,
|
sessionKey: rawSessionKey,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
agentWorkspaceDir,
|
||||||
|
workspaceAccess: cfg.workspaceAccess,
|
||||||
containerName,
|
containerName,
|
||||||
containerWorkdir: cfg.docker.workdir,
|
containerWorkdir: cfg.docker.workdir,
|
||||||
docker: cfg.docker,
|
docker: cfg.docker,
|
||||||
@@ -932,19 +990,26 @@ export async function ensureSandboxWorkspaceForSession(params: {
|
|||||||
const mainKey = params.config?.session?.mainKey?.trim() || "main";
|
const mainKey = params.config?.session?.mainKey?.trim() || "main";
|
||||||
if (!shouldSandboxSession(cfg, rawSessionKey, mainKey)) return null;
|
if (!shouldSandboxSession(cfg, rawSessionKey, mainKey)) return null;
|
||||||
|
|
||||||
|
const agentWorkspaceDir = resolveUserPath(
|
||||||
|
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
|
);
|
||||||
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
||||||
const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey);
|
const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey);
|
||||||
const workspaceDir =
|
const sandboxWorkspaceDir =
|
||||||
cfg.scope === "shared"
|
cfg.scope === "shared"
|
||||||
? workspaceRoot
|
? workspaceRoot
|
||||||
: resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
|
: resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
|
||||||
const seedWorkspace =
|
const workspaceDir =
|
||||||
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR;
|
cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir;
|
||||||
await ensureSandboxWorkspace(
|
if (workspaceDir === sandboxWorkspaceDir) {
|
||||||
workspaceDir,
|
await ensureSandboxWorkspace(
|
||||||
seedWorkspace,
|
sandboxWorkspaceDir,
|
||||||
params.config?.agent?.skipBootstrap,
|
agentWorkspaceDir,
|
||||||
);
|
params.config?.agent?.skipBootstrap,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
sandboxInfo?: {
|
sandboxInfo?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
|
workspaceAccess?: "none" | "ro" | "rw";
|
||||||
|
agentWorkspaceMount?: string;
|
||||||
browserControlUrl?: string;
|
browserControlUrl?: string;
|
||||||
browserNoVncUrl?: string;
|
browserNoVncUrl?: string;
|
||||||
};
|
};
|
||||||
@@ -185,6 +187,13 @@ export function buildAgentSystemPromptAppend(params: {
|
|||||||
params.sandboxInfo.workspaceDir
|
params.sandboxInfo.workspaceDir
|
||||||
? `Sandbox workspace: ${params.sandboxInfo.workspaceDir}`
|
? `Sandbox workspace: ${params.sandboxInfo.workspaceDir}`
|
||||||
: "",
|
: "",
|
||||||
|
params.sandboxInfo.workspaceAccess
|
||||||
|
? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${
|
||||||
|
params.sandboxInfo.agentWorkspaceMount
|
||||||
|
? ` (mounted at ${params.sandboxInfo.agentWorkspaceMount})`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
: "",
|
||||||
params.sandboxInfo.browserControlUrl
|
params.sandboxInfo.browserControlUrl
|
||||||
? `Sandbox browser control URL: ${params.sandboxInfo.browserControlUrl}`
|
? `Sandbox browser control URL: ${params.sandboxInfo.browserControlUrl}`
|
||||||
: "",
|
: "",
|
||||||
|
|||||||
@@ -924,6 +924,13 @@ export type ClawdbotConfig = {
|
|||||||
sandbox?: {
|
sandbox?: {
|
||||||
/** Enable sandboxing for sessions. */
|
/** Enable sandboxing for sessions. */
|
||||||
mode?: "off" | "non-main" | "all";
|
mode?: "off" | "non-main" | "all";
|
||||||
|
/**
|
||||||
|
* Agent workspace access inside the sandbox.
|
||||||
|
* - "none": do not mount the agent workspace into the container; use a sandbox workspace under workspaceRoot
|
||||||
|
* - "ro": mount the agent workspace read-only; disables write/edit tools
|
||||||
|
* - "rw": mount the agent workspace read/write; enables write/edit tools
|
||||||
|
*/
|
||||||
|
workspaceAccess?: "none" | "ro" | "rw";
|
||||||
/**
|
/**
|
||||||
* Session tools visibility for sandboxed sessions.
|
* Session tools visibility for sandboxed sessions.
|
||||||
* - "spawned": only allow session tools to target sessions spawned from this session (default)
|
* - "spawned": only allow session tools to target sessions spawned from this session (default)
|
||||||
|
|||||||
@@ -579,6 +579,9 @@ export const ClawdbotSchema = z.object({
|
|||||||
mode: z
|
mode: z
|
||||||
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
workspaceAccess: z
|
||||||
|
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
||||||
|
.optional(),
|
||||||
sessionToolsVisibility: z
|
sessionToolsVisibility: z
|
||||||
.union([z.literal("spawned"), z.literal("all")])
|
.union([z.literal("spawned"), z.literal("all")])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user