Sandbox 系统架构
Moltbot 的 Docker 沙箱隔离机制
概述
Sandbox 系统为非主会话提供 Docker 容器隔离,允许 Agent 在受限环境中执行命令和访问文件。支持工作空间映射、工具策略、容器生命周期管理。
设计目标
- 安全隔离: 非主会话在独立容器中运行
- 工作空间映射: 灵活的宿主/容器文件映射策略
- 工具限制: 沙箱中可用工具受限
- 自动管理: 容器自动创建、启动、清理
- 配置驱动: 通过配置控制沙箱行为
Sandbox 模式
模式类型
文件: src/agents/sandbox/types.ts
export type SandboxMode = "off" | "all" | "non-main";
| 模式 | 行为 | 适用场景 |
|---|---|---|
off | 不启用沙箱 | 开发环境,完全信任 |
all | 所有会话都启用沙箱 | 最大安全隔离 |
non-main | 仅非主会话启用沙箱 | 推荐:主会话有完全访问,其他受限 |
配置示例
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // ✅ 推荐
workspaceAccess: "ro", // 只读访问宿主工作区
pruneMinutes: 60, // 60 分钟后清理
pruneCron: "0 * * * *" // 每小时运行清理
}
}
}
}
工作空间访问
访问模式
文件: src/agents/sandbox/types.ts
export type SandboxWorkspaceAccess = "none" | "ro" | "rw";
| 模式 | 挂载点 | 说明 |
|---|---|---|
none | /workspace | 完全隔离,无宿主访问 |
ro | /agent:ro + /workspace | 只读宿主工作区 |
rw | /agent:rw + /workspace | 读写宿主工作区 |
工作空间映射
// 宿主路径
host: /home/user/workspace
agentWorkspaceDir: /home/user/.clawdbot/agents/main/workspace
sandboxWorkspaceDir: /home/user/.clawdbot/sandboxes/session-xyz/workspace
// 映射策略
Mode: none
- sandboxWorkspaceDir -> container: /workspace (rw)
- 无宿主访问
Mode: ro
- host -> container: /agent:ro (ro)
- sandboxWorkspaceDir -> container: /workspace (rw)
- 可读宿主工作区,不能修改
Mode: rw
- host -> container: /workspace (rw)
- 完全读写访问
容器生命周期
容器确保
文件: src/agents/sandbox/docker.ts
export async function ensureSandboxContainer(params: {
sessionKey: string;
workspaceDir: string;
agentWorkspaceDir: string;
cfg: MoltbotConfig;
}): Promise<SandboxContainerInfo> {
// 1. 检查容器是否存在
const existing = await docker.listContainers();
const containerName = resolveContainerName(params.sessionKey);
const found = existing.find(c => c.Names.includes(containerName));
// 2. 配置哈希验证
const configHash = computeDockerConfigHash(params.cfg);
if (found) {
const containerConfigHash = found.Labels['moltbot.config-hash'];
if (containerConfigHash === configHash) {
// 配置一致,检查是否运行
if (found.State === 'running') {
return { containerName, status: 'running' };
}
// 容器存在但未运行起来
await startContainer(found.Id);
return { containerName, status: 'started' };
}
// 配置不匹配,删除并重建
await removeContainer(found.Id);
}
// 3. 创建新容器
await createContainer({
name: containerName,
image: 'node:22-alpine',
workdir: '/workspace',
volumes: buildVolumeMounts(params),
labels: {
'moltbot.session-key': params.sessionKey,
'moltbot.config-hash': configHash,
'moltbot.created-at': Date.now().toString(),
},
env: buildContainerEnv(params),
});
return { containerName, status: 'created' };
}
配置哈希
function computeDockerConfigHash(cfg: MoltbotConfig): string {
// 计算沙箱相关配置的哈希
const config = {
mode: cfg.agents?.defaults?.sandbox?.mode,
workspaceAccess: cfg.agents?.defaults?.sandbox?.workspaceAccess,
image: cfg.agents?.defaults?.sandbox?.image,
// ... 其他相关配置
};
return cryptoHash(JSON.stringify(config));
}
容器清理
文件: src/agents/sandbox/prune.ts
export async function pruneSandboxContainers(params: {
cfg: MoltbotConfig;
ageMinutes?: number;
}): Promise<PruneResult> {
// 1. 获取所有 moltbot 容器
const containers = await docker.listContainers({
filters: { label: ['moltbot.session-key'] }
});
// 2. 检查每个容器
const toRemove: DockerContainer[] = [];
for (const container of containers) {
const createdAt = parseInt(container.Labels['moltbot.created-at']);
const age = Date.now() - createdAt;
// 检查年龄
if (age > (ageMinutes || 60) * 60_000) {
toRemove.push(container);
}
}
// 3. 删除过期容器
for (const container of toRemove) {
await removeContainer(container.Id);
}
return {
removed: toRemove.length,
total: containers.length
};
}
工具策略
策略配置
文件: src/agents/sandbox/tool-policy.ts
export function resolveSandboxToolPolicyForAgent(
cfg: MoltbotConfig,
agentId: string
): ToolPolicy {
// 1. Agent 级配置
const agentCfg = findAgentConfig(cfg, agentId);
if (agentCfg?.tools?.sandbox?.tools) {
return agentCfg.tools.sandbox.tools;
}
// 2. 全局沙箱配置
if (cfg.tools?.sandbox?.tools) {
return cfg.tools.sandbox.tools;
}
// 3. 默认策略
return {
allow: ['bash', 'process', 'read', 'write', 'edit', 'sessions_list', 'sessions_history', 'sessions_send'],
deny: ['browser', 'canvas', 'nodes', 'cron', 'discord', 'gateway']
};
}
工具匹配
export function isToolAllowed(
policy: ToolPolicy,
toolName: string
): boolean {
// 1. 先检查 deny 列表
if (policy.deny) {
for (const pattern of policy.deny) {
if (matchGlob(pattern, toolName)) {
return false; // 在 deny 中,不允许
}
}
}
// 2. 检查 allow 列表
if (policy.allow) {
for (const pattern of policy.allow) {
if (matchGlob(pattern, toolName)) {
return true; // 在 allow 中,允许
}
}
// 有 allow 但不匹配,不允许
return policy.allow.length === 0;
}
// 3. 无配置,默认允许
return true;
}
配置示例
{
agents: {
defaults: {
tools: {
sandbox: {
tools: {
allow: ["bash", "read", "write", "edit", "sessions_list"],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway", "process"]
}
}
}
},
list: {
work: {
tools: {
sandbox: {
tools: {
allow: ["read", "write", "edit"], // 更严格
deny: ["*", "!read", "!write", "!edit"]
}
}
}
}
}
}
}
工具执行
沙箱工具包装器
文件: src/agents/sandbox/context.ts
function wrapSandboxTool(
tool: AnyAgentTool,
ctx: SandboxContext
): AnyAgentTool {
return {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
handler: async (args, signal) => {
if (!ctx.enabled) {
throw new Error("Sandbox not enabled for this session");
}
// 构建执行请求
const request = {
toolName: tool.name,
args,
sessionId: ctx.sessionKey,
};
// 发送到沙箱容器
const result = await executeInSandbox({
containerName: ctx.containerName,
request,
signal,
});
return {
content: result.output,
is_error: result.isError,
data: result.data,
};
},
};
}
容器内执行
async function executeInSandbox(params: {
containerName: string;
request: SandboxRequest;
signal: AbortSignal;
}): Promise<SandboxResponse> {
// 1. 获取容器
const container = await docker.getContainer(params.containerName);
// 2. 准备执行脚本
const script = `
const handler = ${toolHandlerName};
const result = await handler(${JSON.stringify(params.request.args)});
console.log(JSON.stringify(result));
`;
// 3. 执行命令
const exec = await container.exec({
Cmd: ['node', '-e', script],
AttachStdout: true,
AttachStderr: true,
});
// 4. 等待完成
const stream = await exec.start({ Detach: false });
const output = await collectStream(stream, params.signal);
// 5. 解析结果
return JSON.parse(output);
}
上下文解析
沙箱状态解析
文件: src/agents/sandbox/runtime-status.ts
export function resolveSandboxRuntimeStatus(params: {
cfg: MoltbotConfig;
sessionKey: string;
mainSessionKey: string;
}): SandboxRuntimeStatus {
const sandboxConfig = params.cfg.agents?.defaults?.sandbox;
const mode = sandboxConfig?.mode ?? "off";
// 判断是否应该启用沙箱
const enabled = shouldSandboxSession({
cfg: params.cfg,
sessionKey: params.sessionKey,
mainSessionKey: params.mainSessionKey,
});
if (!enabled) {
return {
enabled: false,
reason: "sandbox disabled by config"
};
}
return {
enabled: true,
mode,
workspaceAccess: sandboxConfig?.workspaceAccess ?? "none",
image: sandboxConfig?.image ?? "node:22-alpine",
pruneMinutes: sandboxConfig?.pruneMinutes ?? 60,
};
}
会话判定
function shouldSandboxSession(params: {
cfg: MoltbotConfig;
sessionKey: string;
mainSessionKey: string;
}): boolean {
const mode = params.cfg.agents?.defaults?.sandbox?.mode ?? "off";
switch (mode) {
case "off":
return false;
case "all":
return true;
case "non-main":
// 非主会话启用沙箱
return params.sessionKey !== params.mainSessionKey;
default:
return false;
}
}
镜像管理
自定义镜像
{
agents: {
defaults: {
sandbox: {
image: "node:22-alpine", // 默认
// 或使用自定义镜像
image: "custom/sandbox:latest"
}
}
}
}
镜像拉取
async function ensureSandboxImage(imageName: string): Promise<void> {
try {
await docker.pull(imageName);
} catch (err) {
if (err.message.includes('no such image')) {
// 镜像不存在,尝试拉取
await docker.pull(imageName);
} else {
throw err;
}
}
}
调试和监控
容器日志
# 查看所有沙箱容器
docker ps --filter "label=moltbot.session-key"
# 查看特定会话的容器日志
docker logs <container-name>
# 进入容器调试
docker exec -it <container-name> sh
状态查询
WebSocket 方法:
// 查询沙箱状态
await gatewayCall({
method: "agents.list",
params: {}
});
// 响应包含每个 Agent 的沙箱状态
{
agents: [
{
id: "main",
sandbox: {
enabled: false
}
},
{
id: "work",
sandbox: {
enabled: true,
mode: "non-main",
containerName: "moltbot-sandbox-work",
status: "running"
}
}
]
}
最佳实践
1. 使用 non-main 模式
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // ✅ 主会话全访问,其他受限
workspaceAccess: "ro" // ✅ 只读宿主工作区
}
}
}
}
2. 限制危险工具
{
agents: {
defaults: {
tools: {
sandbox: {
tools: {
deny: ["browser", "canvas", "nodes", "discord", "gateway"]
}
}
}
}
}
}
3. 定期清理
{
agents: {
defaults: {
sandbox: {
pruneMinutes: 60, // 60 分钟后清理
pruneCron: "0 * * * *" // 每小时运行清理
}
}
}
}
4. 使用只读工作空间
{
agents: {
defaults: {
sandbox: {
workspaceAccess: "ro" // ✅ 防止意外修改
}
}
}
}
代码路径引用
| 功能 | 文件路径 |
|---|---|
| 运行时状态 | src/agents/sandbox/runtime-status.ts |
| Docker 管理 | src/agents/sandbox/docker.ts |
| 工具策略 | src/agents/sandbox/tool-policy.ts |
| 上下文 | src/agents/sandbox/context.ts |
| 清理 | src/agents/sandbox/prune.ts |
| 配置类型 | src/agents/sandbox/types.ts |
| 工作空间 | src/agents/sandbox/workspace.ts |