feat: add sandbox scope default
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
|
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
|
||||||
- To keep old “open to everyone” behavior: set `dmPolicy="open"` and include `"*"` in the relevant `allowFrom` (Discord/Slack: `discord.dm.allowFrom` / `slack.dm.allowFrom`).
|
- To keep old “open to everyone” behavior: set `dmPolicy="open"` and include `"*"` in the relevant `allowFrom` (Discord/Slack: `discord.dm.allowFrom` / `slack.dm.allowFrom`).
|
||||||
- Approve requests via `clawdbot pairing list --provider <provider>` + `clawdbot pairing approve --provider <provider> <code>` (Telegram also supports `clawdbot telegram pairing ...`).
|
- Approve requests via `clawdbot pairing list --provider <provider>` + `clawdbot pairing approve --provider <provider> <code>` (Telegram also supports `clawdbot telegram pairing ...`).
|
||||||
|
- Sandbox: default `agent.sandbox.scope` to `"agent"` (one container/workspace per agent). Use `"session"` for per-session isolation; `"shared"` disables cross-session isolation.
|
||||||
- Timestamps in agent envelopes are now UTC (compact `YYYY-MM-DDTHH:mmZ`); removed `messages.timestampPrefix`. Add `agent.userTimezone` to tell the model the user’s local time (system prompt only).
|
- Timestamps in agent envelopes are now UTC (compact `YYYY-MM-DDTHH:mmZ`); removed `messages.timestampPrefix`. Add `agent.userTimezone` to tell the model the user’s local time (system prompt only).
|
||||||
- Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup.
|
- Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup.
|
||||||
- Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context.
|
- Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context.
|
||||||
|
|||||||
@@ -629,7 +629,7 @@ Default: `~/clawd`.
|
|||||||
```
|
```
|
||||||
|
|
||||||
If `agent.sandbox` is enabled, non-main sessions can override this with their
|
If `agent.sandbox` is enabled, non-main sessions can override this with their
|
||||||
own per-session workspaces under `agent.sandbox.workspaceRoot`.
|
own per-scope workspaces under `agent.sandbox.workspaceRoot`.
|
||||||
|
|
||||||
### `agent.skipBootstrap`
|
### `agent.skipBootstrap`
|
||||||
|
|
||||||
@@ -847,27 +847,30 @@ per session key at a time). Default: 1.
|
|||||||
|
|
||||||
### `agent.sandbox`
|
### `agent.sandbox`
|
||||||
|
|
||||||
Optional per-session **Docker sandboxing** for the embedded agent. Intended for
|
Optional **Docker sandboxing** for the embedded agent. Intended for non-main
|
||||||
non-main sessions so they cannot access your host system.
|
sessions so they cannot access your host system.
|
||||||
|
|
||||||
Defaults (if enabled):
|
Defaults (if enabled):
|
||||||
- one container per session
|
- scope: `"agent"` (one container + workspace per agent)
|
||||||
- Debian bookworm-slim based image
|
- Debian bookworm-slim based image
|
||||||
- workspace per session under `~/.clawdbot/sandboxes`
|
- workspace per agent under `~/.clawdbot/sandboxes`
|
||||||
- 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)
|
||||||
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
|
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
|
||||||
|
|
||||||
Warning: `perSession: false` means a shared container and shared workspace. No
|
Warning: `scope: "shared"` means a shared container and shared workspace. No
|
||||||
cross-session isolation.
|
cross-session isolation. Use `scope: "session"` for per-session isolation.
|
||||||
|
|
||||||
|
Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
||||||
|
`false` → `scope: "shared"`).
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
agent: {
|
agent: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "non-main", // off | non-main | all
|
mode: "non-main", // off | non-main | all
|
||||||
perSession: true, // recommended for isolation (false = shared container/workspace)
|
scope: "agent", // session | agent | shared (agent is default)
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes",
|
workspaceRoot: "~/.clawdbot/sandboxes",
|
||||||
docker: {
|
docker: {
|
||||||
image: "clawdbot-sandbox:bookworm-slim",
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
|
|||||||
@@ -140,10 +140,11 @@ We're considering a `readOnlyMode` flag that prevents the AI from:
|
|||||||
Two complementary approaches:
|
Two complementary approaches:
|
||||||
|
|
||||||
- **Run the full Gateway in Docker** (container boundary): [Docker](/install/docker)
|
- **Run the full Gateway in Docker** (container boundary): [Docker](/install/docker)
|
||||||
- **Per-session tool sandbox** (`agent.sandbox`, host gateway + Docker-isolated tools): [Configuration](/gateway/configuration)
|
- **Tool sandbox** (`agent.sandbox`, host gateway + Docker-isolated tools): [Configuration](/gateway/configuration)
|
||||||
|
|
||||||
Note: to prevent cross-agent access, keep `perSession: true` so each session gets
|
Note: to prevent cross-agent access, keep `sandbox.scope` at `"agent"` (default)
|
||||||
its own container + workspace. `perSession: false` shares a single container.
|
or `"session"` for stricter per-session isolation. `scope: "shared"` uses a
|
||||||
|
single container/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.
|
||||||
|
|
||||||
|
|||||||
@@ -70,25 +70,26 @@ pnpm test:docker:qr
|
|||||||
- Gateway bind defaults to `lan` for container use.
|
- Gateway bind defaults to `lan` for container use.
|
||||||
- The gateway container is the source of truth for sessions (`~/.clawdbot/agents/<agentId>/sessions/`).
|
- The gateway container is the source of truth for sessions (`~/.clawdbot/agents/<agentId>/sessions/`).
|
||||||
|
|
||||||
## Per-session Agent Sandbox (host gateway + Docker tools)
|
## Agent Sandbox (host gateway + Docker tools)
|
||||||
|
|
||||||
### What it does
|
### What it does
|
||||||
|
|
||||||
When `agent.sandbox` is enabled, **non-main sessions** run tools inside a Docker
|
When `agent.sandbox` is enabled, **non-main sessions** run tools inside a Docker
|
||||||
container. The gateway stays on your host, but the tool execution is isolated:
|
container. The gateway stays on your host, but the tool execution is isolated:
|
||||||
- one container per session (hard wall)
|
- scope: `"agent"` by default (one container + workspace per agent)
|
||||||
- per-session workspace folder mounted at `/workspace`
|
- scope: `"session"` for per-session isolation
|
||||||
|
- per-scope workspace folder mounted at `/workspace`
|
||||||
- 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 sandbox workspace (`media/inbound/*`) so tools can read it
|
||||||
|
|
||||||
Warning: setting `perSession: false` disables per-session isolation. All sessions
|
Warning: `scope: "shared"` disables cross-session isolation. All sessions share
|
||||||
share one container and one workspace, so there is no cross-session isolation.
|
one container and one workspace.
|
||||||
|
|
||||||
### Default behavior
|
### Default behavior
|
||||||
|
|
||||||
- Image: `clawdbot-sandbox:bookworm-slim`
|
- Image: `clawdbot-sandbox:bookworm-slim`
|
||||||
- One container per session
|
- One container per agent
|
||||||
- Workspace per session under `~/.clawdbot/sandboxes`
|
- Workspace per agent under `~/.clawdbot/sandboxes`
|
||||||
- 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`
|
||||||
@@ -101,7 +102,7 @@ share one container and one workspace, so there is no cross-session isolation.
|
|||||||
agent: {
|
agent: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "non-main", // off | non-main | all
|
mode: "non-main", // off | non-main | all
|
||||||
perSession: true,
|
scope: "agent", // session | agent | shared (agent is default)
|
||||||
workspaceRoot: "~/.clawdbot/sandboxes",
|
workspaceRoot: "~/.clawdbot/sandboxes",
|
||||||
docker: {
|
docker: {
|
||||||
image: "clawdbot-sandbox:bookworm-slim",
|
image: "clawdbot-sandbox:bookworm-slim",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe("buildSandboxCreateArgs", () => {
|
|||||||
const args = buildSandboxCreateArgs({
|
const args = buildSandboxCreateArgs({
|
||||||
name: "clawdbot-sbx-test",
|
name: "clawdbot-sbx-test",
|
||||||
cfg,
|
cfg,
|
||||||
sessionKey: "main",
|
scopeKey: "main",
|
||||||
createdAtMs: 1700000000000,
|
createdAtMs: 1700000000000,
|
||||||
labels: { "clawdbot.sandboxBrowser": "1" },
|
labels: { "clawdbot.sandboxBrowser": "1" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type { ClawdbotConfig } from "../config/config.js";
|
|||||||
import { STATE_DIR_CLAWDBOT } from "../config/config.js";
|
import { STATE_DIR_CLAWDBOT } from "../config/config.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
|
import { resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
DEFAULT_AGENTS_FILENAME,
|
DEFAULT_AGENTS_FILENAME,
|
||||||
@@ -72,9 +73,11 @@ export type SandboxPruneConfig = {
|
|||||||
maxAgeDays: number;
|
maxAgeDays: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SandboxScope = "session" | "agent" | "shared";
|
||||||
|
|
||||||
export type SandboxConfig = {
|
export type SandboxConfig = {
|
||||||
mode: "off" | "non-main" | "all";
|
mode: "off" | "non-main" | "all";
|
||||||
perSession: boolean;
|
scope: SandboxScope;
|
||||||
workspaceRoot: string;
|
workspaceRoot: string;
|
||||||
docker: SandboxDockerConfig;
|
docker: SandboxDockerConfig;
|
||||||
browser: SandboxBrowserConfig;
|
browser: SandboxBrowserConfig;
|
||||||
@@ -197,11 +200,33 @@ function isToolAllowed(policy: SandboxToolPolicy, name: string) {
|
|||||||
return allow.includes(name.toLowerCase());
|
return allow.includes(name.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSandboxScope(params: {
|
||||||
|
scope?: SandboxScope;
|
||||||
|
perSession?: boolean;
|
||||||
|
}): SandboxScope {
|
||||||
|
if (params.scope) return params.scope;
|
||||||
|
if (typeof params.perSession === "boolean") {
|
||||||
|
return params.perSession ? "session" : "shared";
|
||||||
|
}
|
||||||
|
return "agent";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSandboxScopeKey(scope: SandboxScope, sessionKey: string) {
|
||||||
|
const trimmed = sessionKey.trim() || "main";
|
||||||
|
if (scope === "shared") return "shared";
|
||||||
|
if (scope === "session") return trimmed;
|
||||||
|
const agentId = resolveAgentIdFromSessionKey(trimmed);
|
||||||
|
return `agent:${agentId}`;
|
||||||
|
}
|
||||||
|
|
||||||
function defaultSandboxConfig(cfg?: ClawdbotConfig): SandboxConfig {
|
function defaultSandboxConfig(cfg?: ClawdbotConfig): SandboxConfig {
|
||||||
const agent = cfg?.agent?.sandbox;
|
const agent = cfg?.agent?.sandbox;
|
||||||
return {
|
return {
|
||||||
mode: agent?.mode ?? "off",
|
mode: agent?.mode ?? "off",
|
||||||
perSession: agent?.perSession ?? true,
|
scope: resolveSandboxScope({
|
||||||
|
scope: agent?.scope,
|
||||||
|
perSession: agent?.perSession,
|
||||||
|
}),
|
||||||
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,
|
||||||
@@ -502,14 +527,14 @@ function formatUlimitValue(
|
|||||||
export function buildSandboxCreateArgs(params: {
|
export function buildSandboxCreateArgs(params: {
|
||||||
name: string;
|
name: string;
|
||||||
cfg: SandboxDockerConfig;
|
cfg: SandboxDockerConfig;
|
||||||
sessionKey: string;
|
scopeKey: string;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
labels?: Record<string, string>;
|
labels?: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const createdAtMs = params.createdAtMs ?? Date.now();
|
const createdAtMs = params.createdAtMs ?? Date.now();
|
||||||
const args = ["create", "--name", params.name];
|
const args = ["create", "--name", params.name];
|
||||||
args.push("--label", "clawdbot.sandbox=1");
|
args.push("--label", "clawdbot.sandbox=1");
|
||||||
args.push("--label", `clawdbot.sessionKey=${params.sessionKey}`);
|
args.push("--label", `clawdbot.sessionKey=${params.scopeKey}`);
|
||||||
args.push("--label", `clawdbot.createdAtMs=${createdAtMs}`);
|
args.push("--label", `clawdbot.createdAtMs=${createdAtMs}`);
|
||||||
for (const [key, value] of Object.entries(params.labels ?? {})) {
|
for (const [key, value] of Object.entries(params.labels ?? {})) {
|
||||||
if (key && value) args.push("--label", `${key}=${value}`);
|
if (key && value) args.push("--label", `${key}=${value}`);
|
||||||
@@ -557,15 +582,15 @@ async function createSandboxContainer(params: {
|
|||||||
name: string;
|
name: string;
|
||||||
cfg: SandboxDockerConfig;
|
cfg: SandboxDockerConfig;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
sessionKey: string;
|
scopeKey: string;
|
||||||
}) {
|
}) {
|
||||||
const { name, cfg, workspaceDir, sessionKey } = params;
|
const { name, cfg, workspaceDir, scopeKey } = params;
|
||||||
await ensureDockerImage(cfg.image);
|
await ensureDockerImage(cfg.image);
|
||||||
|
|
||||||
const args = buildSandboxCreateArgs({
|
const args = buildSandboxCreateArgs({
|
||||||
name,
|
name,
|
||||||
cfg,
|
cfg,
|
||||||
sessionKey,
|
scopeKey,
|
||||||
});
|
});
|
||||||
args.push("--workdir", cfg.workdir);
|
args.push("--workdir", cfg.workdir);
|
||||||
args.push("-v", `${workspaceDir}:${cfg.workdir}`);
|
args.push("-v", `${workspaceDir}:${cfg.workdir}`);
|
||||||
@@ -584,9 +609,9 @@ async function ensureSandboxContainer(params: {
|
|||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
cfg: SandboxConfig;
|
cfg: SandboxConfig;
|
||||||
}) {
|
}) {
|
||||||
const slug = params.cfg.perSession
|
const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey);
|
||||||
? slugifySessionKey(params.sessionKey)
|
const slug =
|
||||||
: "shared";
|
params.cfg.scope === "shared" ? "shared" : slugifySessionKey(scopeKey);
|
||||||
const name = `${params.cfg.docker.containerPrefix}${slug}`;
|
const name = `${params.cfg.docker.containerPrefix}${slug}`;
|
||||||
const containerName = name.slice(0, 63);
|
const containerName = name.slice(0, 63);
|
||||||
const state = await dockerContainerState(containerName);
|
const state = await dockerContainerState(containerName);
|
||||||
@@ -595,7 +620,7 @@ async function ensureSandboxContainer(params: {
|
|||||||
name: containerName,
|
name: containerName,
|
||||||
cfg: params.cfg.docker,
|
cfg: params.cfg.docker,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
sessionKey: params.sessionKey,
|
scopeKey,
|
||||||
});
|
});
|
||||||
} else if (!state.running) {
|
} else if (!state.running) {
|
||||||
await execDocker(["start", containerName]);
|
await execDocker(["start", containerName]);
|
||||||
@@ -603,7 +628,7 @@ async function ensureSandboxContainer(params: {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await updateRegistry({
|
await updateRegistry({
|
||||||
containerName,
|
containerName,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: scopeKey,
|
||||||
createdAtMs: now,
|
createdAtMs: now,
|
||||||
lastUsedAtMs: now,
|
lastUsedAtMs: now,
|
||||||
image: params.cfg.docker.image,
|
image: params.cfg.docker.image,
|
||||||
@@ -648,16 +673,15 @@ function buildSandboxBrowserResolvedConfig(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSandboxBrowser(params: {
|
async function ensureSandboxBrowser(params: {
|
||||||
sessionKey: string;
|
scopeKey: string;
|
||||||
workspaceDir: string;
|
workspaceDir: 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;
|
||||||
if (!isToolAllowed(params.cfg.tools, "browser")) return null;
|
if (!isToolAllowed(params.cfg.tools, "browser")) return null;
|
||||||
|
|
||||||
const slug = params.cfg.perSession
|
const slug =
|
||||||
? slugifySessionKey(params.sessionKey)
|
params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey);
|
||||||
: "shared";
|
|
||||||
const name = `${params.cfg.browser.containerPrefix}${slug}`;
|
const name = `${params.cfg.browser.containerPrefix}${slug}`;
|
||||||
const containerName = name.slice(0, 63);
|
const containerName = name.slice(0, 63);
|
||||||
const state = await dockerContainerState(containerName);
|
const state = await dockerContainerState(containerName);
|
||||||
@@ -666,7 +690,7 @@ async function ensureSandboxBrowser(params: {
|
|||||||
const args = buildSandboxCreateArgs({
|
const args = buildSandboxCreateArgs({
|
||||||
name: containerName,
|
name: containerName,
|
||||||
cfg: params.cfg.docker,
|
cfg: params.cfg.docker,
|
||||||
sessionKey: params.sessionKey,
|
scopeKey: params.scopeKey,
|
||||||
labels: { "clawdbot.sandboxBrowser": "1" },
|
labels: { "clawdbot.sandboxBrowser": "1" },
|
||||||
});
|
});
|
||||||
args.push("-v", `${params.workspaceDir}:${params.cfg.docker.workdir}`);
|
args.push("-v", `${params.workspaceDir}:${params.cfg.docker.workdir}`);
|
||||||
@@ -710,7 +734,7 @@ async function ensureSandboxBrowser(params: {
|
|||||||
? await readDockerPort(containerName, params.cfg.browser.noVncPort)
|
? await readDockerPort(containerName, params.cfg.browser.noVncPort)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const existing = BROWSER_BRIDGES.get(params.sessionKey);
|
const existing = BROWSER_BRIDGES.get(params.scopeKey);
|
||||||
const existingProfile = existing
|
const existingProfile = existing
|
||||||
? resolveProfile(existing.bridge.state.resolved, "clawd")
|
? resolveProfile(existing.bridge.state.resolved, "clawd")
|
||||||
: null;
|
: null;
|
||||||
@@ -722,7 +746,7 @@ async function ensureSandboxBrowser(params: {
|
|||||||
await stopBrowserBridgeServer(existing.bridge.server).catch(
|
await stopBrowserBridgeServer(existing.bridge.server).catch(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
);
|
);
|
||||||
BROWSER_BRIDGES.delete(params.sessionKey);
|
BROWSER_BRIDGES.delete(params.scopeKey);
|
||||||
}
|
}
|
||||||
let bridge: BrowserBridge;
|
let bridge: BrowserBridge;
|
||||||
if (shouldReuse && existing) {
|
if (shouldReuse && existing) {
|
||||||
@@ -737,13 +761,13 @@ async function ensureSandboxBrowser(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!shouldReuse) {
|
if (!shouldReuse) {
|
||||||
BROWSER_BRIDGES.set(params.sessionKey, { bridge, containerName });
|
BROWSER_BRIDGES.set(params.scopeKey, { bridge, containerName });
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
await updateBrowserRegistry({
|
await updateBrowserRegistry({
|
||||||
containerName,
|
containerName,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.scopeKey,
|
||||||
createdAtMs: now,
|
createdAtMs: now,
|
||||||
lastUsedAtMs: now,
|
lastUsedAtMs: now,
|
||||||
image: params.cfg.browser.image,
|
image: params.cfg.browser.image,
|
||||||
@@ -858,9 +882,11 @@ export async function resolveSandboxContext(params: {
|
|||||||
await maybePruneSandboxes(cfg);
|
await maybePruneSandboxes(cfg);
|
||||||
|
|
||||||
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
||||||
const workspaceDir = cfg.perSession
|
const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey);
|
||||||
? resolveSandboxWorkspaceDir(workspaceRoot, rawSessionKey)
|
const workspaceDir =
|
||||||
: workspaceRoot;
|
cfg.scope === "shared"
|
||||||
|
? workspaceRoot
|
||||||
|
: resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
|
||||||
const seedWorkspace =
|
const seedWorkspace =
|
||||||
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR;
|
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR;
|
||||||
await ensureSandboxWorkspace(
|
await ensureSandboxWorkspace(
|
||||||
@@ -876,7 +902,7 @@ export async function resolveSandboxContext(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const browser = await ensureSandboxBrowser({
|
const browser = await ensureSandboxBrowser({
|
||||||
sessionKey: rawSessionKey,
|
scopeKey,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
cfg,
|
cfg,
|
||||||
});
|
});
|
||||||
@@ -905,9 +931,11 @@ export async function ensureSandboxWorkspaceForSession(params: {
|
|||||||
if (!shouldSandboxSession(cfg, rawSessionKey, mainKey)) return null;
|
if (!shouldSandboxSession(cfg, rawSessionKey, mainKey)) return null;
|
||||||
|
|
||||||
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
const workspaceRoot = resolveUserPath(cfg.workspaceRoot);
|
||||||
const workspaceDir = cfg.perSession
|
const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey);
|
||||||
? resolveSandboxWorkspaceDir(workspaceRoot, rawSessionKey)
|
const workspaceDir =
|
||||||
: workspaceRoot;
|
cfg.scope === "shared"
|
||||||
|
? workspaceRoot
|
||||||
|
: resolveSandboxWorkspaceDir(workspaceRoot, scopeKey);
|
||||||
const seedWorkspace =
|
const seedWorkspace =
|
||||||
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR;
|
params.workspaceDir?.trim() || DEFAULT_AGENT_WORKSPACE_DIR;
|
||||||
await ensureSandboxWorkspace(
|
await ensureSandboxWorkspace(
|
||||||
|
|||||||
@@ -532,6 +532,9 @@ export type RoutingConfig = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
sandbox?: {
|
sandbox?: {
|
||||||
mode?: "off" | "non-main" | "all";
|
mode?: "off" | "non-main" | "all";
|
||||||
|
/** Container/workspace scope for sandbox isolation. */
|
||||||
|
scope?: "session" | "agent" | "shared";
|
||||||
|
/** Legacy alias for scope ("session" when true, "shared" when false). */
|
||||||
perSession?: boolean;
|
perSession?: boolean;
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
};
|
};
|
||||||
@@ -912,7 +915,9 @@ export type ClawdbotConfig = {
|
|||||||
* - "all": allow session tools to target any session
|
* - "all": allow session tools to target any session
|
||||||
*/
|
*/
|
||||||
sessionToolsVisibility?: "spawned" | "all";
|
sessionToolsVisibility?: "spawned" | "all";
|
||||||
/** Use one container per session (recommended for hard isolation). */
|
/** Container/workspace scope for sandbox isolation. */
|
||||||
|
scope?: "session" | "agent" | "shared";
|
||||||
|
/** Legacy alias for scope ("session" when true, "shared" when false). */
|
||||||
perSession?: boolean;
|
perSession?: boolean;
|
||||||
/** Root directory for sandbox workspaces. */
|
/** Root directory for sandbox workspaces. */
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
|
|||||||
@@ -235,6 +235,13 @@ const RoutingSchema = z
|
|||||||
z.literal("all"),
|
z.literal("all"),
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
scope: z
|
||||||
|
.union([
|
||||||
|
z.literal("session"),
|
||||||
|
z.literal("agent"),
|
||||||
|
z.literal("shared"),
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
perSession: z.boolean().optional(),
|
perSession: z.boolean().optional(),
|
||||||
workspaceRoot: z.string().optional(),
|
workspaceRoot: z.string().optional(),
|
||||||
})
|
})
|
||||||
@@ -573,6 +580,13 @@ export const ClawdbotSchema = z.object({
|
|||||||
sessionToolsVisibility: z
|
sessionToolsVisibility: z
|
||||||
.union([z.literal("spawned"), z.literal("all")])
|
.union([z.literal("spawned"), z.literal("all")])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
scope: z
|
||||||
|
.union([
|
||||||
|
z.literal("session"),
|
||||||
|
z.literal("agent"),
|
||||||
|
z.literal("shared"),
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
perSession: z.boolean().optional(),
|
perSession: z.boolean().optional(),
|
||||||
workspaceRoot: z.string().optional(),
|
workspaceRoot: z.string().optional(),
|
||||||
docker: z
|
docker: z
|
||||||
|
|||||||
Reference in New Issue
Block a user