docs: clarify sandbox bind mounts (#790)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string, unknown> }>
|
||||
| {
|
||||
details: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
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<string, unknown> }>
|
||||
| {
|
||||
details: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
const tool = tools[0] as VoiceTool;
|
||||
const result = await tool.execute("id", { mode: "status" });
|
||||
expect(String(result.details.error)).toContain("sid required");
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user