目录
[概述](#概述)
[核心概念](#核心概念)
[Sandbox 命名规则](#sandbox-命名规则)
[Session 原理与实现](#session-原理与实现)
[Scope 配置详解](#scope-配置详解)
[源码实现分析](#源码实现分析)
[实战示例](#实战示例)
[最佳实践](#最佳实践)
一.概述
OpenClaw 使用 Docker 容器作为 sandbox 来隔离 agent 执行环境。理解 sandbox 和 session 的管理机制对于有效使用 OpenClaw 至关重要。
核心问题:
不同的配置会生成不同的 sandbox 和容器
SessionID 和 SessionKey 的区别和联系
Scope 配置如何影响容器复用
二.核心概念
1.SessionKey vs SessionID
agent:main:main | custom-123) | |
2.Sandbox 工作流
命令执行 ↓解析 SessionKey ↓根据 Scope 确定 ScopeKey ↓生成 Slug (SessionKey → hash) ↓创建/复用 Docker 容器 ↓挂载工作目录三.Sandbox 命名规则
1.命名公式
目录名:~/.openclaw/sandboxes/{slug}
容器名:openclaw-sbx-{slug}
Slug 生成:{清理后的sessionKey前32位}-{8位SHA256哈希}
2.源码实现
文件:src/agents/sandbox/shared.ts
exportfunctionslugifySessionKey(value: string) {const trimmed = value.trim() || "session";const hash = hashTextSha256(trimmed).slice(0, 8); // 取前8位const safe = trimmed .toLowerCase() .replace(/[^a-z0-9._-]+/g, "-") // 替换非法字符 .replace(/^-+|-+$/g, ""); // 去除首尾连字符const base = safe.slice(0, 32) || "session"; // 最多32字符return`${base}-${hash}`;}3.ScopeKey 解析
文件:src/agents/sandbox/shared.ts
exportfunctionresolveSandboxScopeKey( scope: "session" | "agent" | "shared", sessionKey: string) {const trimmed = sessionKey.trim() || "main";if (scope === "shared") {return"shared"; }if (scope === "session") {return trimmed; // 返回完整 sessionKey }// scope === "agent"const agentId = resolveAgentIdFromSessionKey(trimmed);return`agent:${agentId}`; // 只返回 agent 部分}4.容器创建
文件:src/agents/sandbox/docker.ts
exportasyncfunctionensureSandboxContainer(params: { sessionKey: string; workspaceDir: string; agentWorkspaceDir: string; cfg: SandboxConfig;}) {// 1. 解析 scopeKeyconst scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey);// 2. 生成 slugconst slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(scopeKey);// 3. 构建容器名(限制63字符)const name = `${params.cfg.docker.containerPrefix}${slug}`;const containerName = name.slice(0, 63);// 4. 检查容器是否存在const state = await dockerContainerState(containerName);// 5. 创建或启动容器if (!state.exists) {await createSandboxContainer({ name: containerName, ... }); } elseif (!state.running) {await execDocker(["start", containerName]); }return containerName;}5.实际示例
agent:main:main | session | agent:main:main | agent-main-main-6d9217fe | openclaw-sbx-agent-main-main-6d9217fe |
agent:main:main | agent | agent:main | agent-main-f331f052 | openclaw-sbx-agent-main-f331f052 |
agent:main:main | shared | shared | shared | openclaw-sbx-shared |
四.Session 原理与实现
1.--session-id 参数
作用: 恢复已存在会话的唯一标识符
工作流程:
// 文件: src/commands/agent/session.tsexportfunctionresolveSession(opts: { cfg: OpenClawConfig; to?: string; sessionId?: string; sessionKey?: string; agentId?: string;}): SessionResolution{const { sessionKey, sessionStore, storePath } = resolveSessionKeyForRequest({ cfg: opts.cfg, to: opts.to, sessionId: opts.sessionId, // "custom-123" sessionKey: opts.sessionKey, agentId: opts.agentId, });// 查找 session store 中是否存在该 sessionIdconst sessionEntry = sessionKey ? sessionStore[sessionKey] : undefined;// 确定最终使用的 sessionIdconst sessionId = opts.sessionId?.trim() || // 用户指定的 (fresh ? sessionEntry?.sessionId : undefined) || // 已存在的 crypto.randomUUID(); // 新生成的return { sessionId, sessionKey, sessionEntry, sessionStore, storePath, isNewSession: !fresh && !opts.sessionId, };}2.SessionID 查找逻辑
文件:src/commands/agent/session.ts
exportfunctionresolveSessionKeyForRequest(opts: { cfg: OpenClawConfig; to?: string; sessionId?: string; sessionKey?: string; agentId?: string;}): SessionKeyResolution{// 1. 获取 agent 的默认 sessionKeyconst explicitSessionKey = opts.sessionKey?.trim() || resolveExplicitAgentSessionKey({ cfg: opts.cfg, agentId: opts.agentId, // "main" });// 2. 如果提供了 sessionId,查找对应的 sessionKeyif (opts.sessionId && !explicitSessionKey) {// 在主存储中查找const foundKey = Object.keys(sessionStore).find((key) => sessionStore[key]?.sessionId === opts.sessionId, );if (foundKey) { sessionKey = foundKey; } }// 3. 如果在主存储中没找到,搜索所有 agent 的存储if (opts.sessionId && !explicitSessionKey && !sessionKey) {const allAgentIds = listAgentIds(opts.cfg);for (const agentId of allAgentIds) {const altStore = loadSessionStore(altStorePath);const foundKey = Object.keys(altStore).find((key) => altStore[key]?.sessionId === opts.sessionId, );if (foundKey) {return { sessionKey: foundKey, sessionStore: altStore, ... }; } } }return { sessionKey, sessionStore, storePath };}3.SessionKey 生成
文件:src/config/sessions/main-session.ts
exportfunctionresolveAgentMainSessionKey(params: { cfg?: { session?: { mainKey?: string } }; agentId: string;}): string{const mainKey = normalizeMainKey(params.cfg?.session?.mainKey);return buildAgentMainSessionKey({ agentId: params.agentId, mainKey });}// 文件: src/routing/session-key.tsexportfunctionbuildAgentMainSessionKey(params: { agentId: string; mainKey?: string;}): string{const agentId = normalizeAgentId(params.agentId);const mainKey = normalizeMainKey(params.mainKey);return`agent:${agentId}:${mainKey}`;}4.使用场景
# 场景 1: 首次运行 - 创建新会话pnpm openclaw agent --agent main --message "你好"# → 生成新的 sessionId (UUID)# → 使用 sessionKey: agent:main:main# → sandbox: agent-main-main-6d9217fe# 场景 2: 继续会话 - 使用已有的 sessionIdpnpm openclaw agent --agent main --session-id abc-123-def --message "再见"# → 如果 abc-123-def 存在于 store# → 恢复该会话的历史和上下文# → 复用相同的 sandbox# 场景 3: 使用不存在的 sessionIdpnpm openclaw agent --agent main --session-id custom-123 --message "测试"# → custom-123 不存在于 store,被忽略# → 使用默认 sessionKey: agent:main:main# → 如果 agent:main:main 有历史记录,恢复该会话 # → 如果 agent:main:main 无历史记录,创建新会话(sessionId 仍为 custom-123)五.Scope 配置详解
1.Scope 类型对比
"session" | agent:main:mainagent:main:main | ||||
"agent" | agent:main:mainagent:main | ||||
"shared" | shared |
2.配置示例
文件:~/.openclaw/config.yml
agents:defaults:sandbox:mode:"all"# 启用 sandboxscope:"session"# 每个 session 独立容器# 或# scope: "agent" # 每个 agent 共享容器# 或# scope: "shared" # 全局共享容器3.实际影响
场景:使用 --agent main 运行多个会话
# 会话 1pnpm openclaw agent --agent main --message "任务1"# 会话 2pnpm openclaw agent --agent main --message "任务2"scope="session" 时:
~/.openclaw/sandboxes/├── agent-main-main-6d9217fe # agent:main:main 的 sandbox两个会话共享同一个容器(因为 sessionKey 相同)。
scope="agent" 时:
~/.openclaw/sandboxes/├── agent-main-f331f052 # agent:main 的 sandbox所有 main agent 的会话共享这个容器。
如果使用不同的 --to 参数(产生不同 sessionKey):
# 会话 1: --to +15555550123# sessionKey: agent:main:telegram:direct:+15555550123# → sandbox: agent-main-telegram-direct-15555550123-{hash}# 会话 2: --to +15555550999# sessionKey: agent:main:telegram:direct:+15555550999# → sandbox: agent-main-telegram-direct-15555550999-{hash}scope="session" 会为每个 sessionKey 创建独立容器。
scope="agent" 会将它们合并到 agent-main 容器。
六.源码实现分析
1.关键文件结构
src/├── agents/│ ├── sandbox/│ │ ├── docker.ts # Docker 容器管理│ │ ├── shared.ts # SessionKey 解析和 slug 生成│ │ ├── constants.ts # 常量定义│ │ └── types.ts # 类型定义│ └── agent-scope.ts # Agent ID 解析├── commands/│ └── agent/│ └── session.ts # Session 解析逻辑├── config/│ └── sessions/│ ├── session-key.ts # SessionKey 生成│ └── main-session.ts # Main session key 处理└── routing/ └── session-key.ts # SessionKey 工具函数2.Slug 生成算法详解
// 输入: "agent:main:main"// 输出: "agent-main-main-6d9217fe"functionslugifySessionKey(value: string) {// 步骤 1: 清理空白const trimmed = value.trim() || "session";// 步骤 2: 计算 SHA256 哈希(取前8位)const hash = hashTextSha256(trimmed).slice(0, 8);// 步骤 3: 清理字符串(只保留安全字符)const safe = trimmed .toLowerCase() // 转小写 .replace(/[^a-z0-9._-]+/g, "-") // 非法字符替换为 - .replace(/^-+|-+$/g, ""); // 去除首尾 -// 步骤 4: 限制长度(最多32字符)const base = safe.slice(0, 32) || "session";// 步骤 5: 组合return`${base}-${hash}`;}3.容器管理流程
// 1. 确保容器存在asyncfunctionensureSandboxContainer(params) {const scopeKey = resolveSandboxScopeKey( params.cfg.scope, // "session" | "agent" | "shared" params.sessionKey // "agent:main:main" );// scope="session" → scopeKey = "agent:main:main"// scope="agent" → scopeKey = "agent:main"// scope="shared" → scopeKey = "shared"const slug = slugifySessionKey(scopeKey);const containerName = `openclaw-sbx-${slug}`.slice(0, 63);// 2. 检查容器状态const state = await dockerContainerState(containerName);// 3. 创建或复用if (!state.exists) {await createSandboxContainer(containerName); } elseif (!state.running) {await dockerStart(containerName); }return containerName;}4.配置哈希检测
// 检测 sandbox 配置是否变更asyncfunctionensureSandboxContainer(params) {const expectedHash = computeSandboxConfigHash({ docker: params.cfg.docker, workspaceAccess: params.cfg.workspaceAccess, workspaceDir: params.workspaceDir, });const currentHash = await readContainerConfigHash(containerName);const hashMismatch = currentHash !== expectedHash;if (hashMismatch) {// 配置变更,需要重建容器await dockerRm(containerName);await createSandboxContainer(containerName); }}七.实战示例
1.场景 1: 从 scope="agent" 迁移到 scope="session"
初始状态:
# config.ymlagents:defaults:sandbox:mode:"all"scope:"agent"运行命令:
pnpm openclaw agent --agent main --message "测试"生成的容器:
~/.openclaw/sandboxes/agent-main-f331f052openclaw-sbx-agent-main-f331f052修改配置:
agents:defaults:sandbox:mode:"all"scope:"session"# 修改为 session再次运行相同命令:
pnpm openclaw agent --agent main --message "测试"生成的新容器:
~/.openclaw/sandboxes/agent-main-main-6d9217feopenclaw-sbx-agent-main-main-6d9217fe结果: 两个容器并存,互不影响。
2.场景 2: 使用 --session-id 恢复会话
首次运行:
pnpm openclaw agent --agent main --message "开始项目"系统返回:
Session ID: abc-123-def-456Session Key: agent:main:main继续该会话:
pnpm openclaw agent --agent main --session-id abc-123-def-456 --message "继续工作"✅ 恢复历史消息
✅ 继续上下文
✅ 复用相同 sandbox
3.场景 3: 不同 sessionKey 的隔离
# 会话 1: main agent 的主会话pnpm openclaw agent --agent main --message "主会话"# → sessionKey: agent:main:main# → sandbox: agent-main-main-6d9217fe# 会话 2: Telegram 私聊pnpm openclaw agent --agent main --to +15555550123 --message "私聊消息"# → sessionKey: agent:main:telegram:direct:+15555550123# → sandbox: agent-main-telegram-direct-15555550123-{hash}# 会话 3: Telegram 群组pnpm openclaw agent --agent main --to chat-abc123 --message "群组消息"# → sessionKey: agent:main:telegram:group:chat-abc123# → sandbox: agent-main-telegram-group-chat-abc123-{hash}scope="session" 时,这三个会话使用完全独立的容器。
八.最佳实践
1.选择合适的 Scope
session | ||
agent | ||
shared | ||
session | ||
agent |
2.容器清理
# 查看所有 OpenClaw 容器docker ps -a --filter "name=openclaw-sbx"# 停止所有 sandbox 容器docker ps --filter "name=openclaw-sbx" -q | xargs docker stop# 删除所有 sandbox 容器docker ps -a --filter "name=openclaw-sbx" -q | xargs docker rm# 清理 sandbox 目录rm -rf ~/.openclaw/sandboxes/*3.调试技巧
# 查看 session storecat ~/.openclaw/sessions/main/sessions.json | jq .# 查看特定 session 的信息cat ~/.openclaw/sessions/main/sessions.json | jq '.["agent:main:main"]'# 验证 hash 计算echo -n "agent:main:main" | sha256sum | cut -c1-8# 查看容器标签docker inspect openclaw-sbx-agent-main-main-6d9217fe | jq '.[0].Config.Labels'4.配置检查
# 查看当前 sandbox 配置pnpm openclaw config get agents.defaults.sandbox# 查看所有 sandbox 目录ls -la ~/.openclaw/sandboxes/# 查看 sandbox 容器状态docker stats --no-stream --filter "name=openclaw-sbx"九.常见问题
1.Q: 修改 scope 后旧容器会自动删除吗?
A: 不会。旧容器会保留,需要手动清理:
# 停止并删除旧容器docker rm -f openclaw-sbx-agent-main-f331f052# 删除对应目录rm -rf ~/.openclaw/sandboxes/agent-main-f331f0522.Q: --session-id 找不到会怎样?
A: 系统会使用 agent 的默认 sessionKey(如 agent:main:main),并创建或恢复对应的会话。
注解:
# → 比如,--session-id custom-123 不存在于 store,被忽略# → 使用默认 sessionKey: agent:main:main# → 如果 agent:main:main 有历史记录,恢复该会话 # → 如果 agent:main:main 无历史记录,创建新会话(sessionId 仍为 custom-123)3.Q: 如何查看当前会话的 sessionKey?
A: 查看 session store 文件:
cat ~/.openclaw/sessions/main/sessions.json | jq 'to_entries[] | select(.value.sessionId == "你的session-id") | .key'4.Q: 两个不同的 sessionId 可能对应同一个 sessionKey 吗?
A: 不可能。SessionKey 到 SessionEntry 是一对一映射,每个 sessionKey 只能有一个 entry。
5.Q: 容器名超过 63 字符怎么办?
A: 代码自动截断:
const containerName = name.slice(0, 63);十.总结
1.关键要点
SessionKey 决定 Sandbox
完整的 agent:main:main → agent-main-main-{hash}
简化的 agent:main → agent-main-{hash}
Scope 控制复用策略
session: 每个 sessionKey 独立agent: 每个 agentId 共享shared: 全局共享--session-id 用于恢复会话
必须存在于 session store
不存在则使用默认 sessionKey
命名规则统一
目录:
{slug}容器:
openclaw-sbx-{slug}Slug:
{清理后的key前32位}-{8位hash}SessionKey 格式规范
## 通用模式agent:{agentId}:{segment1}[:{segment2}[:{segment3}...]]其中 segment 数量和含义取决于:- scope 配置 (session | agent | global)- mainKey 配置 - dmScope 配置 (main | per-peer | per-channel-peer | per-account-channel-peer)- peerKind (direct | group | channel) - 是否有 thread ## 常见格式agent:{agentId}:{mainKey} # 主会话agent:{agentId}:{channel}:{peerKind}:{peerId} # 群组/频道/直连agent:{agentId}:direct:{peerId} # 直连(per-peer)agent:{agentId}:{channel}:direct:{peerId} # 直连(per-channel-peer)agent:{agentId}:{channel}:{accountId}:direct:{peerId} # 直连(per-account-channel-peer) {sessionKey}:thread:{threadId} # Thread 会话agent:{agentId}:global# 全局会话 ## 实际示例# 基础格式agent:main:main # main agent 的主会话agent:ops:main # ops agent 的主会话# Peer 会话(根据 dmScope 配置不同)agent:main:telegram:direct:+15555550123# per-channel-peeragent:main:direct:+15555550123# per-peeragent:main:telegram:account1:direct:+15555550123# per-account-channel-peer # 群组/频道会话 agent:main:telegram:group:chat-abc123 # Telegram 群组agent:main:discord:channel:123456789# Discord 频道# Thread 会话agent:main:telegram:group:chat-abc123:thread:12345# 全局会话(scope="global")agent:main:global# 全局会话2.相关源码文件
src/agents/sandbox/docker.ts- 容器管理src/agents/sandbox/shared.ts- SessionKey 解析src/commands/agent/session.ts- Session 解析src/config/sessions/session-key.ts- SessionKey 生成src/routing/session-key.ts- SessionKey 工具函数
知识星球:Dify源码剖析及答疑,OpenClaw源码剖析及答疑。加微信buxingtianxia21进NLP工程化资料群,Dify源码交流群,OpenClaw源码交流群。
夜雨聆风