乐于分享
好东西不私藏

OpenClaw Sandbox 与 Session 管理深度解析

OpenClaw Sandbox 与 Session 管理深度解析

目录

一.概述

OpenClaw 使用 Docker 容器作为 sandbox 来隔离 agent 执行环境。理解 sandbox 和 session 的管理机制对于有效使用 OpenClaw 至关重要。

核心问题:

  • 不同的配置会生成不同的 sandbox 和容器

  • SessionID 和 SessionKey 的区别和联系

  • Scope 配置如何影响容器复用

二.核心概念

1.SessionKey vs SessionID

属性
SessionKey
SessionID
用途
路由、存储、sandbox
追踪特定会话实例
格式
agent:main:main
UUID(如 custom-123
生成时机
根据配置自动生成
首次创建会话时生成
持久性
多次运行可复用
每次运行可能不同
决定因素
Agent ID + Scope + MainKey
随机或用户指定
存储位置
嵌入在路径中
存储在 session entry 中

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(08);  // 取前8位const safe = trimmed    .toLowerCase()    .replace(/[^a-z0-9._-]+/g"-")     // 替换非法字符    .replace(/^-+|-+$/g"");            // 去除首尾连字符const base = safe.slice(032) || "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(063);// 4. 检查容器是否存在const state = await dockerContainerState(containerName);// 5. 创建或启动容器if (!state.exists) {await createSandboxContainer({ name: containerName, ... });  } elseif (!state.running) {await execDocker(["start", containerName]);  }return containerName;}

5.实际示例

SessionKey
Scope
ScopeKey
Slug
容器名
agent:main:mainsessionagent:main:mainagent-main-main-6d9217feopenclaw-sbx-agent-main-main-6d9217fe
agent:main:mainagentagent:mainagent-main-f331f052openclaw-sbx-agent-main-f331f052
agent:main:mainsharedsharedsharedopenclaw-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 类型对比

Scope
容器数量
隔离粒度
SessionKey → ScopeKey
资源效率
安全性
"session"
每个 sessionKey 一个
会话级
agent:main:main
 → agent:main:main
"agent"
每个 agentId 一个
Agent 级
agent:main:main
 → agent: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(08);// 步骤 3: 清理字符串(只保留安全字符)const safe = trimmed    .toLowerCase()                  // 转小写    .replace(/[^a-z0-9._-]+/g"-"// 非法字符替换为 -    .replace(/^-+|-+$/g"");       // 去除首尾 -// 步骤 4: 限制长度(最多32字符)const base = safe.slice(032) || "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(063);// 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

使用场景
推荐 Scope
理由
开发环境
session
完全隔离,避免干扰
生产环境
agent
节省资源,同一 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-f331f052

2.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(063);

十.总结

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源码交流群。