Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Sandbox 系统架构

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