diff --git a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md index f5fbf7420..c0e5a1e68 100644 --- a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md +++ b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @@ -39,6 +39,14 @@ Sandboxing is controlled by `agents.defaults.sandbox.mode`: See [Sandboxing](/gateway/sandboxing) for the full matrix (scope, workspace mounts, images). +### Bind mounts (security quick check) + +- `docker.binds` *pierces* the sandbox filesystem: whatever you mount is visible inside the container with the mode you set (`:ro` or `:rw`). +- Default is read-write if you omit the mode; prefer `:ro` for source/secrets. +- `scope: "shared"` ignores per-agent binds (only global binds apply). +- Binding `/var/run/docker.sock` effectively hands host control to the sandbox; only do this intentionally. +- Workspace access (`workspaceAccess: "ro"`/`"rw"`) is independent of bind modes. + ## Tool policy: which tools exist/are callable Two layers matter: diff --git a/docs/gateway/sandboxing.md b/docs/gateway/sandboxing.md index 4411300ee..266210fda 100644 --- a/docs/gateway/sandboxing.md +++ b/docs/gateway/sandboxing.md @@ -62,6 +62,41 @@ Format: `host:container:mode` (e.g., `"/home/user/source:/source:rw"`). Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"`, per-agent binds are ignored. +Example (read-only source + docker socket): + +```json5 +{ + agents: { + defaults: { + sandbox: { + docker: { + binds: [ + "/home/user/source:/source:ro", + "/var/run/docker.sock:/var/run/docker.sock" + ] + } + } + }, + list: [ + { + id: "build", + sandbox: { + docker: { + binds: ["/mnt/cache:/cache:rw"] + } + } + } + ] + } +} +``` + +Security notes: +- Binds bypass the sandbox filesystem: they expose host paths with whatever mode you set (`:ro` or `:rw`). +- Sensitive mounts (e.g., `docker.sock`, secrets, SSH keys) should be `:ro` unless absolutely required. +- Combine with `workspaceAccess: "ro"` if you only need read access to the workspace; bind modes stay independent. +- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for how binds interact with tool policy and elevated exec. + ## Images + setup Default image: `clawdbot-sandbox:bookworm-slim` diff --git a/docs/start/faq.md b/docs/start/faq.md index b7e814f54..8845b284b 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -209,6 +209,10 @@ ClawdHub installs into `./skills` under your current directory; Clawdbot treats Yes. See [Sandboxing](/gateway/sandboxing). For Docker-specific setup (full gateway in Docker or sandbox images), see [Docker](/install/docker). +### How do I bind a host folder into the sandbox? + +Set `agents.defaults.sandbox.docker.binds` to `["host:path:mode"]` (e.g., `"/home/user/src:/src:ro"`). Global + per-agent binds merge; per-agent binds are ignored when `scope: "shared"`. Use `:ro` for anything sensitive and remember binds bypass the sandbox filesystem walls. See [Sandboxing](/gateway/sandboxing#custom-bind-mounts) and [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated#bind-mounts-security-quick-check) for examples and safety notes. + ### How does memory work? Clawdbot memory is just Markdown files in the agent workspace: diff --git a/src/config/commands.ts b/src/config/commands.ts index a2d5d86fe..b09916fe2 100644 --- a/src/config/commands.ts +++ b/src/config/commands.ts @@ -1,6 +1,6 @@ -import type { NativeCommandsSetting } from "./types.js"; -import { normalizeProviderId } from "../providers/registry.js"; import type { ProviderId } from "../providers/plugins/types.js"; +import { normalizeProviderId } from "../providers/registry.js"; +import type { NativeCommandsSetting } from "./types.js"; function resolveAutoDefault(providerId?: ProviderId): boolean { const id = normalizeProviderId(providerId); diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 4c475b1ec..2d856ad4b 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -14,8 +14,11 @@ import { type User, } from "@buape/carbon"; import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway"; -import type { APIAttachment } from "discord-api-types/v10"; -import { ApplicationCommandOptionType, Routes } from "discord-api-types/v10"; +import { + type APIAttachment, + ApplicationCommandOptionType, + Routes, +} from "discord-api-types/v10"; import { resolveAckReaction, @@ -49,12 +52,12 @@ import { } from "../auto-reply/reply/reply-dispatcher.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; import type { ReplyPayload } from "../auto-reply/types.js"; -import type { ClawdbotConfig, ReplyToMode } from "../config/config.js"; -import { loadConfig } from "../config/config.js"; import { isNativeCommandsExplicitlyDisabled, resolveNativeCommandsEnabled, } from "../config/commands.js"; +import type { ClawdbotConfig, ReplyToMode } from "../config/config.js"; +import { loadConfig } from "../config/config.js"; import { resolveStorePath, updateLastRoute } from "../config/sessions.js"; import { danger, logVerbose, shouldLogVerbose } from "../globals.js"; import { formatDurationSeconds } from "../infra/format-duration.js"; diff --git a/src/plugins/voice-call.plugin.test.ts b/src/plugins/voice-call.plugin.test.ts index 623288d25..51d0a2807 100644 --- a/src/plugins/voice-call.plugin.test.ts +++ b/src/plugins/voice-call.plugin.test.ts @@ -109,7 +109,17 @@ describe("voice-call plugin", () => { it("tool get_status returns json payload", async () => { const { tools } = setup({ provider: "mock" }); - const tool = tools[0] as { execute: (id: string, params: unknown) => any }; + type VoiceTool = { + execute: ( + id: string, + params: unknown, + ) => + | Promise<{ details: Record }> + | { + details: Record; + }; + }; + const tool = tools[0] as VoiceTool; const result = await tool.execute("id", { action: "get_status", callId: "call-1", @@ -119,7 +129,17 @@ describe("voice-call plugin", () => { it("legacy tool status without sid returns error payload", async () => { const { tools } = setup({ provider: "mock" }); - const tool = tools[0] as { execute: (id: string, params: unknown) => any }; + type VoiceTool = { + execute: ( + id: string, + params: unknown, + ) => + | Promise<{ details: Record }> + | { + details: Record; + }; + }; + const tool = tools[0] as VoiceTool; const result = await tool.execute("id", { mode: "status" }); expect(String(result.details.error)).toContain("sid required"); }); diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index 250d2ecdb..69e11b0bb 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -38,13 +38,13 @@ import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispa import { getReplyFromConfig } from "../auto-reply/reply.js"; import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import type { ReplyPayload } from "../auto-reply/types.js"; +import { resolveNativeCommandsEnabled } from "../config/commands.js"; import type { ClawdbotConfig, SlackReactionNotificationMode, SlackSlashCommandConfig, } from "../config/config.js"; import { loadConfig } from "../config/config.js"; -import { resolveNativeCommandsEnabled } from "../config/commands.js"; import { resolveSessionKey, resolveStorePath, diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index fa1ec03b8..f92ad8682 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -33,12 +33,12 @@ import { import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; import { getReplyFromConfig } from "../auto-reply/reply.js"; import type { ReplyPayload } from "../auto-reply/types.js"; -import type { ClawdbotConfig, ReplyToMode } from "../config/config.js"; -import { loadConfig } from "../config/config.js"; import { isNativeCommandsExplicitlyDisabled, resolveNativeCommandsEnabled, } from "../config/commands.js"; +import type { ClawdbotConfig, ReplyToMode } from "../config/config.js"; +import { loadConfig } from "../config/config.js"; import { resolveProviderGroupPolicy, resolveProviderGroupRequireMention,