Skip to content

OpenClaw 源码解读(十四)ACP 控制平面

一、导读

ACP(Agent Client Protocol)是 OpenClaw 为 IDE 集成 设计的标准协议层。它让 Cursor、VS Code、Claude Desktop 等外部 IDE 客户端,通过统一的 JSON-RPC 流式接口与 OpenClaw Gateway 通信——就像一个"翻译官"站在 IDE 和网关之间,把 ACP 标准协议翻译成 OpenClaw 的内部 Gateway RPC。

本文将沿着一条从外到内的主线来拆解这套系统:

IDE 客户端 ←(ACP协议/ndJSON)→ ACP Server ←(WebSocket)→ Gateway

                            Control Plane(管理器/运行时缓存/Actor队列)

涉及的核心源码全部位于 src/acp/ 目录,约 30 个文件,~4000 行代码。

![ACP 控制平面系统全景架构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/01-infographic-acp-system-overview-1775150800967.png)


二、ACP 协议两端:Client 与 Server

2.1 Server 端 —— server.ts

serveAcpGateway() 是 ACP 服务器的入口函数。它做了三件事:

  1. 建立 Gateway WebSocket 连接:加载配置、解析凭证、启动 GatewayClient,并等待 onHelloOk 确认握手完成。
  2. 桥接 stdio 到 ndJSON 流:将 process.stdin/process.stdout 转为 Web Streams,通过 ndJsonStream() 创建双向 JSON 传输通道。
  3. 创建 ACP Agent:实例化 AgentSideConnection(来自 @agentclientprotocol/sdk),把翻译器 AcpGatewayAgent 注入其中。

关键设计:ACP Server 是一个 stdio 进程(不是 HTTP 服务器),IDE 通过 spawn 子进程与它通信。这与 MCP 的 stdio 模式完全一致,意味着 IDE 只需 spawn openclaw acp 就能获得一个 ACP Agent。

IDE → stdin → ndJsonStream → AcpGatewayAgent → GatewayClient → WebSocket → Gateway
IDE ← stdout ← ndJsonStream ← AcpGatewayAgent ← GatewayClient ← WebSocket ← Gateway

2.2 Client 端 —— client.ts

Client 端是 OpenClaw 自己的 ACP 消费者实现(交互式客户端),核心在 createAcpClient()

  1. Spawn ACP Server 子进程:解析命令行参数、确定 openclaw acp 的路径,调用 node:child_process.spawn() 启动。
  2. 建立 ClientSideConnection:SDK 提供的 ACP 客户端连接,桥接子进程的 stdin/stdout。
  3. 权限控制系统:定义了工具调用的权限审批逻辑 resolvePermissionRequest()

权限审批机制值得特别关注:

typescript
// 安全自动批准的工具 ID
const SAFE_AUTO_APPROVE_TOOL_IDS = new Set(["read", "search", "web_search", "memory_search"]);

// 对 read 工具做路径范围检查——只有在 cwd 内的读取才自动批准
function isReadToolCallScopedToCwd(...) {
  const absolutePath = resolveAbsoluteScopedPath(rawPath, cwd);
  return isPathWithinRoot(absolutePath, path.resolve(cwd));
}

这套机制确保了:

  • readsearch 等只读工具自动批准(前提是 read 路径在工作目录内)
  • DANGEROUS_ACP_TOOLS 中的工具强制交互式确认
  • 非 TTY 环境下所有工具一律拒绝(30 秒超时)

![ACP Client-Server 通信桥梁与权限控制](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/02-infographic-client-server-bridge-1775150801910.png)


三、翻译器:AcpGatewayAgent

translator.ts 中的 AcpGatewayAgent 是整个 ACP 系统的核心。它实现了 @agentclientprotocol/sdkAgent 接口,充当 ACP 协议和 Gateway 之间的双向翻译器。

3.1 Session 生命周期

IDE: newSession()  →  AcpGatewayAgent  →  生成 UUID sessionId
                                        →  解析 meta(sessionKey/label/reset)
                                        →  通过 Gateway 解析或创建 session
                                        →  存入 SessionStore
                                        →  发送 availableCommands 到 IDE
                                        →  返回 { sessionId }

三种进入方式:

  • newSession:创建全新 ACP 会话,生成 UUID 作为 sessionId
  • loadSession:加载已有会话(支持 IDE 重启后恢复)
  • unstable_listSessions:列出所有 Gateway 会话(通过 sessions.list RPC)

3.2 Prompt 处理流程

prompt() 方法是最复杂的翻译环节:

IDE prompt → 提取文本 → 附加 cwd 前缀 → chat.send RPC → 等待事件流

关键安全措施:

  1. 2MB 大小限制:逐块计算字节数,在内存分配前拒绝超大 prompt(CWE-400 防护)
  2. Abort 控制:每个 prompt 创建 AbortController,支持 IDE 端的取消操作
  3. 幂等键:使用 runId 作为 idempotencyKey,确保重复请求不会产生多次执行
  4. 速率限制:session 创建受固定窗口限流保护(默认 120 次/10 秒)

3.3 事件流翻译

Gateway 返回的事件被翻译成 ACP 标准的 sessionUpdate 通知:

Gateway 事件ACP sessionUpdate说明
chat.deltaagent_message_chunk增量文本(只发送新增部分)
chat.finalstopReason: end_turn正常完成
chat.abortedstopReason: cancelled被取消
chat.errorstopReason: refusal出错
agent.tool.starttool_call工具调用开始
agent.tool.resulttool_call_update工具调用结束

增量文本的处理特别精巧——Gateway 返回的是 累积全文,但 ACP 需要 增量 chunk

typescript
const sentSoFar = pending.sentTextLength ?? 0;
if (fullText.length <= sentSoFar) return;
const newText = fullText.slice(sentSoFar);
pending.sentTextLength = fullText.length;

![AcpGatewayAgent 核心翻译器与事件映射](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/03-infographic-translator-core-1775150802717.png)


四、事件映射与工具推断 —— event-mapper.ts

事件映射器处理三类转换:

4.1 Prompt 内容提取

extractTextFromPrompt() 支持三种 ACP ContentBlock 类型:

  • text:直接提取文本
  • resource:提取内嵌资源文本
  • resource_link:转换为 [Resource link (title)] uri 格式

对控制字符做了完整的转义处理(\0\r\n 等),防止注入。

4.2 附件提取

extractAttachmentsFromPrompt() 从 prompt 中提取 image 类型的 ContentBlock,转换为 Gateway 的附件格式(base64 data + mimeType)。

4.3 工具类型推断

inferToolKind() 通过工具名称的关键字匹配推断工具类型:

typescript
if (normalized.includes("read")) return "read";
if (normalized.includes("write") || normalized.includes("edit")) return "edit";
if (normalized.includes("delete") || normalized.includes("remove")) return "delete";
if (normalized.includes("search") || normalized.includes("find")) return "search";
if (normalized.includes("exec") || normalized.includes("run")) return "execute";
if (normalized.includes("fetch") || normalized.includes("http")) return "fetch";

这个推断结果会作为 ToolKind 发送给 IDE,帮助 IDE 做差异化的 UI 展示。

![事件映射器三类转换与工具推断逻辑](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/04-infographic-event-mapper-1775150803507.png)


五、Session 映射 —— session-mapper.ts

Session 映射器解决的核心问题是:ACP sessionId 如何映射到 Gateway sessionKey?

5.1 Meta 解析

parseSessionMeta() 从 ACP 请求的 _meta 字段中提取会话参数,支持多种别名:

typescript
sessionKey: readString(record, ["sessionKey", "session", "key"]),
sessionLabel: readString(record, ["sessionLabel", "label"]),
resetSession: readBool(record, ["resetSession", "reset"]),
requireExisting: readBool(record, ["requireExistingSession", "requireExisting"]),

这种多别名设计让不同 IDE 客户端可以用自己习惯的字段名。

5.2 Session Key 解析策略

resolveSessionKey() 按优先级解析:

1. meta.sessionLabel   → Gateway sessions.resolve(label)
2. meta.sessionKey     → 直接返回 / Gateway sessions.resolve(key)
3. opts.defaultLabel   → Gateway sessions.resolve(label)
4. opts.defaultKey     → 直接返回 / Gateway sessions.resolve(key)
5. fallbackKey         → 最终兜底

requireExisting 为 true 时,所有 key 都必须通过 Gateway 验证确实存在。

![Session Key 五级优先级解析策略](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/05-infographic-session-mapper-1775150804211.png)


六、内存 Session Store —— session.ts

ACP 服务端用纯内存 Map 管理活跃会话,支持以下特性:

特性实现
最大会话数默认 5000,超限时驱逐最旧的空闲会话
空闲过期默认 24 小时 TTL,定期 reap
Run 追踪双向映射 sessionId ↔ runId
取消控制cancelActiveRun() 触发 AbortController

驱逐策略的设计很有意思——它遵循"活跃运行不可驱逐"原则:

typescript
const evictOldestIdleSession = () => {
  for (const [sessionId, session] of sessions.entries()) {
    if (session.activeRunId || session.abortController) continue;  // 跳过正在运行的
    if (session.lastTouchedAt >= oldestLastTouchedAt) continue;
    oldestLastTouchedAt = session.lastTouchedAt;
    oldestSessionId = sessionId;
  }
  return removeSession(oldestSessionId);
};

![内存 Session Store 管理与驱逐机制](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/06-infographic-session-store-1775150804956.png)


七、策略控制 —— policy.ts

策略层提供细粒度的 ACP 功能开关:

7.1 三级开关

acp.enabled = false         → 整个 ACP 关闭
acp.dispatch.enabled = false → 仅关闭 dispatch(分发)
acp.allowedAgents = [...]   → 白名单过滤

7.2 Agent 白名单

typescript
export function isAcpAgentAllowedByPolicy(cfg, agentId): boolean {
  const allowed = (cfg.acp?.allowedAgents ?? [])
    .map(entry => normalizeAgentId(entry))
    .filter(Boolean);
  if (allowed.length === 0) return true;  // 空白名单 = 允许全部
  return allowed.includes(normalizeAgentId(agentId));
}

白名单为空时默认放行所有 agent——这是一种"默认开放"的设计,降低了配置门槛。


八、ACP 控制平面核心 —— AcpSessionManager

control-plane/manager.core.ts 中的 AcpSessionManager 是整个 ACP 系统最复杂的组件。它管理 ACP 会话的完整生命周期:初始化、运行、取消、关闭、驱逐。

8.1 核心数据结构

typescript
class AcpSessionManager {
  private readonly actorQueue = new SessionActorQueue();      // Actor 并发控制
  private readonly runtimeCache = new RuntimeCache();          // 运行时句柄缓存
  private readonly activeTurnBySession = new Map<string, ActiveTurnState>();  // 活跃 Turn
  private readonly turnLatencyStats: TurnLatencyStats;         // 延迟统计
  private readonly errorCountsByCode = new Map<string, number>(); // 错误计数
}

四大支柱:Actor 队列、运行时缓存、Turn 追踪、可观测性统计。

8.2 Session 解析三态

resolveSession() 返回三种状态:

typescript
type AcpSessionResolution =
  | { kind: "none"; sessionKey: string }       // 非 ACP 会话
  | { kind: "stale"; sessionKey: string; error: AcpRuntimeError }  // 有 ACP key 但缺 meta
  | { kind: "ready"; sessionKey: string; meta: SessionAcpMeta }    // 就绪

stale 状态发生在会话的 ACP 元数据丢失(可能是手动删除或迁移导致),此时会提示用户重新创建。

8.3 初始化流程

initializeSession() 是创建 ACP 会话的完整流程:

1. 规范化 sessionKey 和 agentId
2. 驱逐空闲运行时句柄
3. 进入 Actor 队列(串行化)
4. 获取运行时后端(requireRuntimeBackend)
5. 验证并发会话限制
6. 调用 runtime.ensureSession()
7. 构造并持久化 SessionAcpMeta
8. 缓存运行时句柄

失败回滚特别严谨——如果元数据写入失败,会自动 close 已创建的运行时:

typescript
try {
  await this.writeSessionMeta({ ... });
} catch (error) {
  await runtime.close({ handle, reason: "init-meta-failed" }).catch(...);
  throw error;
}

8.4 Turn 执行引擎

runTurn() 是核心的"一轮对话"执行逻辑:

1. 解析 session → 确保运行时句柄
2. 应用运行时控制(mode/model/配置)
3. 设置状态为 "running"
4. 创建 AbortController + 注册 Turn
5. 遍历 runtime.runTurn() 的 AsyncIterable 事件流
6. 成功 → 记录延迟统计 → 状态改为 "idle"
7. 失败 → 记录错误 → 状态改为 "error"
8. finally:
   - 非 oneshot → 协调运行时身份标识
   - oneshot → 关闭运行时并清除缓存

AbortSignal 的处理使用了双层设计:

typescript
const internalAbortController = new AbortController();
const onCallerAbort = () => internalAbortController.abort();
input.signal?.addEventListener("abort", onCallerAbort, { once: true });

// 组合信号
const combinedSignal = AbortSignal.any([input.signal, internalAbortController.signal]);

外层 input.signal 来自调用方(比如用户取消),内层 internalAbortController 来自管理器自身(比如 cancelSession)。使用 AbortSignal.any() 合并两个信号源。

![AcpSessionManager 四大支柱与 Turn 执行引擎](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/07-infographic-manager-turn-engine-1775150805968.png)


九、Session Actor Queue —— 并发控制

session-actor-queue.ts 实现了 per-session 串行化 的 Actor 模型:

typescript
export class SessionActorQueue {
  private readonly queue = new KeyedAsyncQueue();

  async run<T>(actorKey: string, op: () => Promise<T>): Promise<T> {
    return this.queue.enqueue(actorKey, op, {
      onEnqueue: () => { /* 增加 pending 计数 */ },
      onSettle: () => { /* 减少 pending 计数 */ },
    });
  }
}

基于 KeyedAsyncQueue(来自 plugin-sdk),每个 sessionKey 被归一化为 actorKey,同一个 actor 的操作严格串行,不同 actor 完全并行。

这个设计解决了 ACP 场景下的经典问题:同一个 IDE 会话的多个操作(比如用户快速连发两条消息)必须按序执行,但不同会话之间不应互相阻塞。

Manager 暴露了 getTotalPendingCount() 供可观测性使用——在 /acp status 中可以看到全局队列深度。

![Session Actor Queue 并发控制模型](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/08-infographic-actor-queue-1775150806720.png)


十、Runtime Cache —— 运行时句柄缓存

runtime-cache.ts 管理 ACP 运行时句柄的内存缓存:

typescript
type CachedRuntimeState = {
  runtime: AcpRuntime;               // 运行时实例
  handle: AcpRuntimeHandle;          // 会话句柄
  backend: string;                   // 后端标识
  agent: string;                     // Agent ID
  mode: AcpRuntimeSessionMode;       // persistent | oneshot
  cwd?: string;                      // 工作目录
  appliedControlSignature?: string;  // 已应用的控制配置签名
};

10.1 缓存命中判断

ensureRuntimeHandle() 的缓存命中需要四项全部匹配:

typescript
const backendMatches = !configuredBackend || cached.backend === configuredBackend;
const agentMatches = cached.agent === agent;
const modeMatches = cached.mode === mode;
const cwdMatches = (cached.cwd ?? "") === (cwd ?? "");

任一不匹配就清除缓存并重新创建。

10.2 空闲驱逐

Manager 的 evictIdleRuntimeHandles() 使用 collectIdleCandidates() 找出超过 TTL 的缓存条目:

typescript
const candidates = this.runtimeCache.collectIdleCandidates({
  maxIdleMs: idleTtlMs,
  now,
});
for (const candidate of candidates) {
  await this.actorQueue.run(candidate.actorKey, async () => {
    if (this.activeTurnBySession.has(candidate.actorKey)) return; // 正在运行的不驱逐
    // ... close runtime
    this.evictedRuntimeCount += 1;
  });
}

驱逐操作也通过 Actor Queue 串行化,避免驱逐和 Turn 执行的竞态。

10.3 控制配置签名

appliedControlSignature 是一个 JSON 字符串指纹,记录上次成功推送到运行时的配置:

typescript
function buildRuntimeControlSignature(options): string {
  return JSON.stringify({
    runtimeMode: normalized.runtimeMode ?? null,
    model: normalized.model ?? null,
    permissionProfile: normalized.permissionProfile ?? null,
    timeoutSeconds: normalized.timeoutSeconds ?? null,
    backendExtras: entries,
  });
}

每次 runTurn() 前,如果签名未变则跳过控制推送,避免重复 RPC 调用。

![Runtime Cache 缓存策略与控制签名](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/09-infographic-runtime-cache-1775150807487.png)


十一、Runtime 抽象层

11.1 AcpRuntime 接口

runtime/types.ts 定义了运行时后端必须实现的接口:

typescript
interface AcpRuntime {
  ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
  runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
  cancel(input: { handle; reason? }): Promise<void>;
  close(input: { handle; reason }): Promise<void>;

  // 可选能力
  getCapabilities?(input): Promise<AcpRuntimeCapabilities> | AcpRuntimeCapabilities;
  getStatus?(input): Promise<AcpRuntimeStatus>;
  setMode?(input): Promise<void>;
  setConfigOption?(input): Promise<void>;
  doctor?(): Promise<AcpRuntimeDoctorReport>;
}

必选方法 4 个(ensure/runTurn/cancel/close),可选方法 5 个(getCapabilities/getStatus/setMode/setConfigOption/doctor)。

11.2 事件流类型

AcpRuntimeEvent 是一个判别联合类型,覆盖 5 种事件:

type说明附加字段
text_delta文本增量text, stream(output/thought)
status状态更新text, used/size
tool_call工具调用toolCallId, status, title
done完成stopReason
error错误message, code, retryable

11.3 Session 模式

typescript
type AcpRuntimeSessionMode = "persistent" | "oneshot";
  • persistent:会话保持,Turn 结束后运行时不关闭(标准模式)
  • oneshot:一次性,Turn 结束后自动 close 运行时并清除缓存

11.4 后端注册中心

runtime/registry.ts 使用 Symbol 全局注册表管理后端:

typescript
const ACP_RUNTIME_REGISTRY_STATE_KEY = Symbol.for("openclaw.acpRuntimeRegistryState");

后端选择逻辑:

  1. 指定 ID → 精确匹配
  2. 未指定 → 遍历所有后端,优先选健康的
  3. 找不到 → 抛出 ACP_BACKEND_MISSING 错误

健康检查通过可选的 healthy() 方法实现:

typescript
function isBackendHealthy(backend: AcpRuntimeBackend): boolean {
  if (!backend.healthy) return true;  // 没有健康检查 = 默认健康
  try { return backend.healthy(); }
  catch { return false; }
}

![Runtime 抽象层接口设计](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/10-infographic-runtime-abstraction-1775150808583.png)


十二、错误体系

runtime/errors.ts 定义了 7 种 ACP 错误码:

错误码含义
ACP_BACKEND_MISSING后端未安装/未注册
ACP_BACKEND_UNAVAILABLE后端暂时不可用
ACP_BACKEND_UNSUPPORTED_CONTROL后端不支持请求的控制操作
ACP_DISPATCH_DISABLED策略禁止了 ACP dispatch
ACP_INVALID_RUNTIME_OPTION运行时选项验证失败
ACP_SESSION_INIT_FAILED会话初始化失败
ACP_TURN_FAILEDTurn 执行失败

withAcpRuntimeErrorBoundary() 是统一的错误边界包装器——任何异常都会被规范化为 AcpRuntimeError

typescript
async function withAcpRuntimeErrorBoundary<T>(params: {
  run: () => Promise<T>;
  fallbackCode: AcpRuntimeErrorCode;
  fallbackMessage: string;
}): Promise<T> {
  try { return await params.run(); }
  catch (error) { throw toAcpRuntimeError({ error, fallbackCode, fallbackMessage }); }
}

十三、Session Identity 系统

runtime/session-identity.ts 解决了一个复杂的问题:ACP 会话在不同后端之间的身份标识协调

13.1 身份状态机

typescript
type SessionAcpIdentity = {
  state: "pending" | "resolved";
  source: "ensure" | "status" | "event";
  acpxRecordId?: string;      // 后端记录 ID
  acpxSessionId?: string;     // 后端会话 ID
  agentSessionId?: string;    // 上游 Agent 会话 ID
  lastUpdatedAt: number;
};

身份从 pending 开始,当获取到 acpxSessionIdagentSessionId 后转为 resolved

13.2 身份合并策略

mergeSessionIdentity() 实现了"只升级不降级"的合并逻辑:

current=pending  + incoming=resolved → 采用 incoming(升级)
current=resolved + incoming=pending  → 保留 current(不降级)
current=resolved + incoming=resolved → 采用 incoming(覆盖)

13.3 三种身份来源

  • ensure:调用 runtime.ensureSession() 时获得的初始身份
  • status:调用 runtime.getStatus() 时获得的实时身份
  • event:从运行时事件流中获得的身份更新

Manager 在 Turn 结束后会调用 reconcileRuntimeSessionIdentifiers() 来协调身份,确保持久化的 meta 和缓存中的 handle 都保持最新。


十四、运行时选项系统

control-plane/runtime-options.ts 管理 6 个可配置的运行时选项:

选项类型限制
runtimeModestring≤ 64 字符
modelstring≤ 200 字符
cwdstring≤ 4096 字符,必须绝对路径
permissionProfilestring≤ 80 字符
timeoutSecondsnumber1 ~ 86400
backendExtrasRecord<string, string>≤ 32 个键值对

每个选项都有严格的验证:长度限制、控制字符检查、键名正则校验(/^[a-z0-9][a-z0-9._:-]*$/i)。

inferRuntimeOptionPatchFromConfigOption() 支持从 key-value 配置推断运行时选项:

"model" → { model: value }
"approval_policy" / "permissions" → { permissionProfile: value }
"timeout" → { timeoutSeconds: parseInt(value) }
"cwd" → { cwd: value }
其他 → { backendExtras: { key: value } }

十五、运行时控制推送

manager.runtime-controls.ts 负责在 Turn 执行前将配置推送到运行时后端:

1. 计算当前选项的签名
2. 比较缓存中的 appliedControlSignature
3. 如果一致 → 跳过
4. 如果不一致 → 解析运行时能力 → 推送 setMode/setConfigOption

推送的配置项通过 buildRuntimeConfigOptionPairs() 转换为键值对:

model → "model"
permissionProfile → "approval_policy"
timeoutSeconds → "timeout" (转字符串)
backendExtras 中的其他键 → 直接传递

如果后端不支持请求的控制操作,会抛出 ACP_BACKEND_UNSUPPORTED_CONTROL


十六、Spawn 与清理

control-plane/spawn.ts 处理 ACP 会话创建失败后的清理:

typescript
async function cleanupFailedAcpSpawn(params) {
  // 1. 关闭运行时
  await runtimeCloseHandle.runtime.close({ handle, reason: "spawn-failed" });
  // 2. 通过 Manager 关闭会话
  await acpManager.closeSession({ sessionKey, reason: "spawn-failed" });
  // 3. 解绑 session binding
  await getSessionBindingService().unbind({ targetSessionKey, reason: "spawn-failed" });
  // 4. 删除 Gateway 会话
  await callGateway({ method: "sessions.delete", ... });
}

四层清理,每层都用 .catch() 吞掉错误——best-effort 原则,确保清理不会因某一步失败而中断。


十七、Commands 系统

commands.ts 定义了 ACP 会话中可用的斜杠命令列表:

typescript
export function getAvailableCommands(): AvailableCommand[] {
  return [
    { name: "help", description: "Show help and common commands." },
    { name: "status", description: "Show current status." },
    { name: "think", description: "Set thinking level (off|minimal|low|medium|high|xhigh)." },
    { name: "model", description: "Select a model (list|status|<name>)." },
    { name: "reset", description: "Reset the session (/new)." },
    // ... 共 26 个命令
  ];
}

这些命令在 session 创建/加载时通过 available_commands_update 推送给 IDE,IDE 可以据此构建命令面板。


十八、可观测性

Manager 暴露 getObservabilitySnapshot() 提供全局状态快照:

typescript
type AcpManagerObservabilitySnapshot = {
  runtimeCache: {
    activeSessions: number;
    idleTtlMs: number;
    evictedTotal: number;
    lastEvictedAt?: number;
  };
  turns: {
    active: number;
    queueDepth: number;
    completed: number;
    failed: number;
    averageLatencyMs: number;
    maxLatencyMs: number;
  };
  errorsByCode: Record<string, number>;
};

这为运维提供了关键指标:活跃会话数、队列深度、Turn 延迟统计、错误码分布。


十九、安全设计总结

ACP 系统的安全设计贯穿各层:

层级安全措施
协议层2MB prompt 大小限制(CWE-400 防护)
认证层Gateway token/password,支持文件读取避免命令行泄漏
策略层三级开关 + Agent 白名单
权限层工具调用分级审批(自动/交互/拒绝)
会话层速率限制(120次/10秒)+ 并发限制
运行时层选项验证(长度/字符/路径)、统一错误边界
清理层四层 best-effort 清理

![ACP 七层安全防护体系](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/11-infographic-security-layers-1775150809593.png)


二十、设计模式总结

模式应用位置效果
Actor ModelSessionActorQueueper-session 串行化,跨 session 并行
TranslatorAcpGatewayAgentACP 协议 ↔ Gateway RPC 双向翻译
Cache-AsideRuntimeCache运行时句柄缓存 + 空闲驱逐
Factory + RegistryAcpRuntimeBackend插件化运行时后端
Error BoundarywithAcpRuntimeErrorBoundary统一错误规范化
Rate LimiterFixedWindowRateLimiter会话创建限流
Idempotency KeyrunId防止重复 prompt 执行
Signature-based SkipappliedControlSignature避免重复配置推送

![ACP 八大设计模式总结](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十四)ACP 控制平面/12-infographic-design-patterns-1775150810748.png)


二十一、推荐阅读顺序

  1. src/acp/types.ts — 基础类型定义
  2. src/acp/policy.ts — 策略控制
  3. src/acp/session.ts — 内存 Session Store
  4. src/acp/session-mapper.ts — Session Key 解析
  5. src/acp/event-mapper.ts — 事件翻译
  6. src/acp/translator.ts — 核心翻译器(AcpGatewayAgent)
  7. src/acp/server.ts — 服务端入口
  8. src/acp/client.ts — 客户端 + 权限系统
  9. src/acp/runtime/types.ts — 运行时抽象接口
  10. src/acp/runtime/errors.ts — 错误体系
  11. src/acp/runtime/registry.ts — 后端注册中心
  12. src/acp/runtime/session-identity.ts — 身份协调
  13. src/acp/control-plane/session-actor-queue.ts — Actor 队列
  14. src/acp/control-plane/runtime-cache.ts — 运行时缓存
  15. src/acp/control-plane/runtime-options.ts — 选项系统
  16. src/acp/control-plane/manager.core.ts — Manager 核心

二十二、思考题

  1. 为什么 ACP Server 用 stdio 而不是 HTTP? 这和 MCP 的 stdio 传输有什么共同的设计考量?

  2. Actor Queue 的 per-session 串行化会成为性能瓶颈吗? 什么场景下可能需要放松这个约束?

  3. Session Identity 的"只升级不降级"策略在什么边界情况下可能出问题? 比如后端更换了会话 ID 但保持了 pending 状态。

  4. 运行时控制的签名比较(appliedControlSignature)使用 JSON.stringify 做序列化——这种方式的局限性是什么? 有没有更可靠的签名方案?

  5. 如果你要为 ACP 添加一种新的运行时后端(比如集成 GitHub Copilot),需要实现哪些接口?最小可行实现需要几个方法?

读文档、看源码、写代码,理解 AI Agent 本质 🤖