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).
|
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
|
## Tool policy: which tools exist/are callable
|
||||||
|
|
||||||
Two layers matter:
|
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.
|
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
|
## Images + setup
|
||||||
Default image: `clawdbot-sandbox:bookworm-slim`
|
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).
|
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?
|
### How does memory work?
|
||||||
|
|
||||||
Clawdbot memory is just Markdown files in the agent workspace:
|
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 type { ProviderId } from "../providers/plugins/types.js";
|
||||||
|
import { normalizeProviderId } from "../providers/registry.js";
|
||||||
|
import type { NativeCommandsSetting } from "./types.js";
|
||||||
|
|
||||||
function resolveAutoDefault(providerId?: ProviderId): boolean {
|
function resolveAutoDefault(providerId?: ProviderId): boolean {
|
||||||
const id = normalizeProviderId(providerId);
|
const id = normalizeProviderId(providerId);
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import {
|
|||||||
type User,
|
type User,
|
||||||
} from "@buape/carbon";
|
} from "@buape/carbon";
|
||||||
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
||||||
import type { APIAttachment } from "discord-api-types/v10";
|
import {
|
||||||
import { ApplicationCommandOptionType, Routes } from "discord-api-types/v10";
|
type APIAttachment,
|
||||||
|
ApplicationCommandOptionType,
|
||||||
|
Routes,
|
||||||
|
} from "discord-api-types/v10";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resolveAckReaction,
|
resolveAckReaction,
|
||||||
@@ -49,12 +52,12 @@ import {
|
|||||||
} from "../auto-reply/reply/reply-dispatcher.js";
|
} from "../auto-reply/reply/reply-dispatcher.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import type { ClawdbotConfig, ReplyToMode } from "../config/config.js";
|
|
||||||
import { loadConfig } from "../config/config.js";
|
|
||||||
import {
|
import {
|
||||||
isNativeCommandsExplicitlyDisabled,
|
isNativeCommandsExplicitlyDisabled,
|
||||||
resolveNativeCommandsEnabled,
|
resolveNativeCommandsEnabled,
|
||||||
} from "../config/commands.js";
|
} 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 { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||||
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
||||||
import { formatDurationSeconds } from "../infra/format-duration.js";
|
import { formatDurationSeconds } from "../infra/format-duration.js";
|
||||||
|
|||||||
@@ -109,7 +109,17 @@ describe("voice-call plugin", () => {
|
|||||||
|
|
||||||
it("tool get_status returns json payload", async () => {
|
it("tool get_status returns json payload", async () => {
|
||||||
const { tools } = setup({ provider: "mock" });
|
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", {
|
const result = await tool.execute("id", {
|
||||||
action: "get_status",
|
action: "get_status",
|
||||||
callId: "call-1",
|
callId: "call-1",
|
||||||
@@ -119,7 +129,17 @@ describe("voice-call plugin", () => {
|
|||||||
|
|
||||||
it("legacy tool status without sid returns error payload", async () => {
|
it("legacy tool status without sid returns error payload", async () => {
|
||||||
const { tools } = setup({ provider: "mock" });
|
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" });
|
const result = await tool.execute("id", { mode: "status" });
|
||||||
expect(String(result.details.error)).toContain("sid required");
|
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 { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
|
import { resolveNativeCommandsEnabled } from "../config/commands.js";
|
||||||
import type {
|
import type {
|
||||||
ClawdbotConfig,
|
ClawdbotConfig,
|
||||||
SlackReactionNotificationMode,
|
SlackReactionNotificationMode,
|
||||||
SlackSlashCommandConfig,
|
SlackSlashCommandConfig,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { resolveNativeCommandsEnabled } from "../config/commands.js";
|
|
||||||
import {
|
import {
|
||||||
resolveSessionKey,
|
resolveSessionKey,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ import {
|
|||||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import type { ClawdbotConfig, ReplyToMode } from "../config/config.js";
|
|
||||||
import { loadConfig } from "../config/config.js";
|
|
||||||
import {
|
import {
|
||||||
isNativeCommandsExplicitlyDisabled,
|
isNativeCommandsExplicitlyDisabled,
|
||||||
resolveNativeCommandsEnabled,
|
resolveNativeCommandsEnabled,
|
||||||
} from "../config/commands.js";
|
} from "../config/commands.js";
|
||||||
|
import type { ClawdbotConfig, ReplyToMode } from "../config/config.js";
|
||||||
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveProviderGroupPolicy,
|
resolveProviderGroupPolicy,
|
||||||
resolveProviderGroupRequireMention,
|
resolveProviderGroupRequireMention,
|
||||||
|
|||||||
Reference in New Issue
Block a user