Clawd-on-desk Agent Runtime 架构深度解析:从 Hook 事件到桌宠状态机的完整数据链路

原创2026-10-09 00:26:281,959 阅读
文章标签:桌面应用交互助手

Clawd-on-desk Agent Runtime 架构深度解析:从 Hook 事件到桌宠状态机的完整数据链路

Clawd-on-desk 是一款把 Claude Code、Codex、Cursor 等 AI 编程 Agent 的运行状态实时映射到桌面像素宠物的开源应用。本文以 docs/project/agent-runtime-architecture.md 为骨架,结合仓库源码,系统拆解其 Agent Runtime 架构:本地 HTTP 服务如何承接来自二十余种 Agent 的 hook/plugin 事件,state.js 状态机如何做多会话追踪与优先级仲裁,权限决策、会话标题、Recap 本地投影与进程链元数据如何协同。读完本文,你将掌握 Clawd-on-desk 从「Agent 触发事件」到「桌宠呈现状态」的完整链路,以及每种集成形态(command hook、HTTP hook、in-process plugin、JSONL 轮询)的取舍与边界。

一、整体架构:一条统一的本地状态总线

Clawd-on-desk 的运行时核心是一个监听本机回环地址的 HTTP 服务(默认 127.0.0.1:23333),由 src/server.js 负责监听、端口与组合,src/server-route-state.js 与 src/server-route-permission.js 分别处理 /state 与 /permission 两条路由。所有 Agent 集成最终都汇聚到同一条数据流:

Agent 触发事件
  → hook / plugin 脚本(解析 stdin JSON / BusEvent)
  → HTTP POST 127.0.0.1:23333/state { state, session_id, event, ... }
  → src/server.js HTTP 壳 → src/server-route-state.js → src/agent-runtime-main.js → src/state.js 状态机
  → IPC state-change 事件
  → src/renderer.js(<object> SVG 预加载 + 淡入切换 + 眼球追踪)

从源码结构看,src/agent-runtime-main.js 是官方 hook / 本地 monitor 仲裁的 owner:它负责 official hook 与 JSONL monitor 的事件级 suppression(避免重复状态/重复气泡),并持有 Codex turn fence、Codex official activity、Qoder 与 WorkBuddy 会话标题 tracker 等运行时子系统。而 src/state.js 是真正的状态机实现——多会话追踪、优先级排序、最小显示时长与睡眠序列都在这里完成。

1.1 每种 Agent 的集成形态

仓库把 Agent 集成划分为四种形态,各有明确的传输与阻塞语义:

形态 代表 Agent 关键特征
command hook(非阻塞) Claude Code、Copilot、Cursor、Gemini、Antigravity、Kiro、CodeBuddy、Grok、WorkBuddy、QwenWork、TraeCode、MiniMax、Kimi、ZCode 脚本从 stdin 读 JSON,fire-and-forget POST /state,stdout 输出 {} 不接管权限
HTTP hook(阻塞) Claude Code / CodeBuddy 的 PermissionRequest、Codex official PermissionRequest、ZCode Phase 2 脚本挂起等待 /permission,拿到人工决定后写 stdout 返回
in-process plugin opencode、MiMo Code、Pi、OpenClaw、Hermes、DeepSeek Harness 插件跑在宿主进程内,~0ms 延迟,经事件总线转发
JSONL 轮询(fallback) Codex CLI official hook 未覆盖的事件、hook 禁用/不可用、历史兼容时启用

1.2 Claude Code:主数据链路与 PostToolBatch 仲裁

Claude Code 是主链路,对应 hooks/clawd-hook.js(零依赖 Node 脚本,stdin 读 JSON 取 session_id + source_pid)与 agents/claude-code.js(事件映射表)。事件映射定义了桌宠的 11 种显示状态:

// agents/claude-code.js(节选)
eventMap: {
  SessionStart: "idle",
  UserPromptSubmit: "thinking",
  PreToolUse: "working",
  PostToolUse: "working",
  PostToolUseFailure: "error",
  Stop: "attention",
  SubagentStart: "juggling",
  PreCompact: "sweeping",
  Notification: "notification",
  WorktreeCreate: "carrying",
}

文档特别强调 Claude 的 live tool/model phase 额外使用 PostToolBatch(保守基线 2.1.280+)。这条规则的实际实现位于 src/claude-tool-phase.js 与 hooks/claude-tool-batch.js:hook 只发送有界的 tool IDs 和 prompt_id,省略 inputs、responses 与进程探针;claude-tool-phase.js 维护一个有界的内存主会话台账,/state 在权限清理前观察它,并把内部决策传给 state.js,同一事件不会被消费两次。关键仲裁语义包括:

  • 只有 batch 提示被整体拒绝;未退役的 prompt id 落在普通 tool hook 上、且当前无 turn 打开时,可以在没有 Submit 的情况下建立一个排队 turn;
  • 一个未见过的 id 在 turn 打开时只禁用 batch 推断、不退役当前 prompt,其自身的 Stop 仍正常完成;
  • 已退役的 hook 与已定局的成功 tail 只能注释既有标题/模型/上下文元数据,不能改变 phase 或存活状态;
  • batch 永远不能替代待决的审批或存活中的 subagent 提示,其 recovery/history 分类刻意留空,防止迟到的 phase 提示重新打开持久记录;
  • AskUserQuestion 的 transcript 完成探针在批处理被接受后仍可存活,可在缺少 Stop 时收束 thinking phase。

这种「有界证据 + 精确匹配」的仲裁设计,保证了多来源事件(hook 与 JSONL)不会导致状态重复或错误回退。

1.3 Codex CLI:official hooks 为主 + JSONL fallback 的双通道

Codex 是集成复杂度最高的 Agent 之一,核心注册表配置见 agents/codex.js,hook 实现见 hooks/codex-hook.js:

  • official hooks 为主通道:SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / Stop 经 stdin JSON 进入,session_id 优先与 transcript_path 的 rollout UUID 对齐(防御性提取);
  • JSONL 轮询为 fallback:agents/codex-log-monitor.js 以 1500ms 间隔增量轮询 ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl,覆盖 hook 未覆盖的事件(如 response_item:web_search_call——official hooks 不覆盖 WebSearch,JSONL 是其唯一 lifecycle/tool 边界)。本地 JSONL 路径不经过 HTTP server;
  • 事件级 suppression:agent-runtime-main.js 对 hook-active session 做事件级 suppression,CODEX_OFFICIAL_LOG_SUPPRESS_TTL_MS = 10 分钟,避免重复状态与重复气泡。

Codex 压缩完成的兼容处理见 hooks/codex-log-event.js:同时兼容旧 event_msg:context_compacted 与新 event_msg:item_completed(payload.item.type === "ContextCompaction"),归一化到旧事件键后沿用 sweeping 映射与 timestamp/backfill 保护;它不是 turn completion,也不清理待回答问题。

会话标题通道:本机 Codex 会话标题由 JSONL monitor 每轮为已观察到生命周期的会话合并读取一次 session_index.jsonl(512 KiB tail 上限),新标题/改名以 session_index:title 送入 updateSessionMetadata(expectedAgentId: "codex"),即使 rollout 未增长也刷新 HUD/Dashboard;不创建会话、不改变状态/活跃时间/完成提醒/小结,不清空已有标题。尚未生成原生标题时使用既有文件夹 fallback,用户别名始终优先。

本地归档生命周期(#655):src/codex-archive-tracker.js 是独立于 JSONL 内容解析的归档证据 tracker,与本地 Codex runtime 同启同停。它只在本地 CODEX_HOME 的 archived_sessions 里寻找 regular rollout-*.jsonl,用文件名推导 canonical UUID 并与文件头部有界 session_meta(payload.id / payload.session_id 必须一致且等于文件名 id)校验,再对同一 path 做读后快照复核。关键设计:

  • 归档文件消失是 unarchive 证据;截断/损坏/冲突或 id 不匹配的元数据从不构成归档证据,不会据此退役;
  • 目录不可列、stat/read 的 EACCES/EPERM/EIO 等 I/O 错误与被中断的扫描只算 UNKNOWN:保留既有 suppression,绝不据此退役;
  • 每轮只做一次异步 readdir,只对当前 live 候选读 metadata(每轮至多一个 batch),不为无关历史归档预先索引;
  • evidence 与失败指纹缓存都有 LRU 上限,未变化的坏 live 指纹按指数退避跳过读取;多个 live 超过 batch 时用游标跨轮公平推进;
  • 轮询基准是 5s,但 batch 积压或文件 I/O 都会增加延迟,因此不宣称普遍 ≤5s;CODEX_HOME 在 tracker 实例生命周期内按启动时解析,运行时改动需重启生效。

1.4 Windows 固定入口与安全演进(#986)

本机 Codex 注册使用每个 CODEX_HOME 下固定的分平台入口。Windows 的固定 commandWindows 使用 PowerShell call-operator 直连:& "node" "codex-hook.js" --clawd-windows-stable。hook 进程启动时自读 UTF-8/Base64 clawd-hooks/codex-hook.js.windows.run 数据 sidecar 注入 env——2026-09-04 起由内联 PowerShell dispatcher 改为直连,原因是原 dispatcher 的「解码并执行」命令行被 Windows Defender ML 判为 Trojan:Win32/Commando.A!ml(见 clawd-on-desk#986),且不再落地或二次启动 .ps1。hooks/codex-hook.js 中的 applyWindowsStableSidecarEnv() 实现了这一逻辑:校验签名行、Base64 往返解码、target binding(防止无关/过期 sidecar 给另一份 hook 供 env),并限定仅在 platform === "win32"、argv 含 --clawd-windows-stable、不含 WSL interop 参数且非 remote 时生效。POSIX 使用 clawd-hooks/codex-hook.js.sh 与对应 manifest,只原子更新受管 wrapper,不改 hooks.json 的命令字符串。Doctor 按 Codex 官方归一化 handler 的 SHA-256 精确核对 trusted_hash。

二、全 Agent 数据流速查

文档的 Data Flow 章节覆盖了所有已接入的 Agent,按集成形态分组整理如下(均可溯源到对应 hook/agent 模块):

2.1 Command Hook 家族

Agent Hook 脚本 → 映射模块 事件/状态特征
Copilot CLI hooks/copilot-hook.js → agents/copilot-cli.js camelCase 事件名;PermissionRequest 阻塞 POST /permission
Cursor Agent hooks/cursor-hook.js → agents/cursor-agent.js hook_event_name → PascalCase + POST;beforeSubmitPrompt stdout 为 {continue:true},其余为 {},不接管权限
Gemini CLI hooks/gemini-hook.js → agents/gemini-cli.js hook-only,stdin JSON + stdout JSON
Antigravity CLI (agy) hooks/antigravity-hook.js → agents/antigravity-cli.js 注册到 ~/.gemini/config/hooks.json 的 clawd hook group,仅状态事件;PreToolUse 故意不注册,权限交给 agy 自带 5 选项 native menu
Kiro CLI hooks/kiro-hook.js → agents/kiro-cli.js Kiro 无 global hooks,hook 注入到 ~/.kiro/agents/ 下每个 custom agent 配置;需 kiro-cli --agent clawd 或 /agent swap clawd 启用
CodeBuddy hooks/codebuddy-hook.js → agents/codebuddy.js PascalCase,Claude Code 兼容格式;阻塞式审批走 PermissionRequest HTTP hook
Grok Build hooks/grok-hook.js → agents/grok-build.js 注册到 <GROK_HOME 或 ~/.grok>/hooks/clawd-on-desk.json;经 src/grok-turn-fence.js 有界内存 turn fence 仲裁;state + Notification only
WorkBuddy hooks/workbuddy-hook.js → agents/workbuddy.js PascalCase,Claude Code 兼容;state + Notification only,审批留在原生沙箱与 GUI
QwenWork hooks/qwenwork-hook.js → agents/qwenwork.js 注册到 ~/.QwenWorkCN/settings.json;PermissionRequest/PermissionDenied 仅观察映射成 working(每任务 40+ 次),只发送 tool_input 的 sha1 fingerprint
TraeCode hooks/traecode-hook.js → agents/traecode.js 注册到 ~/.trae-cn/hooks.json;Windows 用无引号外壳 PowerShell -EncodedCommand;无 SessionEnd,靠 traecode-desktop-idle-timeout 退役
MiniMax Code hooks/minimax-hook.js → agents/minimax.js 本地插件目录 <MINIMAX_DATA_DIR 或 MAVIS_DATA_DIR 或 ~/.minimax>/plugins/clawd-state/;handler 固定 exec-form,timeout=2 秒(MiniMax 只接受 1–10 整数秒),阻塞式人工审批物理不可行
Kimi Code CLI hooks/kimi-hook.js → agents/kimi-cli.js 注册到 ~/.kimi/config.toml 的 [[hooks]] 条目;Clawd 启动时自动同步
ZCode hooks/zcode-hook.js → agents/zcode.js 注册到 ~/.zcode/cli/config.json 的 hooks.events.*(7 个事件全注册);Phase 2 起 PermissionRequest 长阻塞 POST /permission(等待 590s,installer 注册 per-hook timeoutMs 600000),有决定时 stdout 返回最小 hookSpecificOutput,无决定/超时/断连输出 {} 并 exit 0 回退原生流程

一个值得注意的通用安全模式:launchedByGrok() 守卫(见 hooks/cursor-hook.js 顶部)——Grok 默认扫描 ~/.claude/settings.json,因此 clawd-hook.js / cursor-hook.js / auto-start.js 只在非空 GROK_HOOK_EVENT 下直接退出(输出 {}),避免产生假会话。

2.2 In-process Plugin 家族

Agent 插件入口 特征
opencode hooks/opencode-plugin/index.mjs CLI/TUI 跑在 Bun,Desktop sidecar 跑在 Electron utilityProcess/Node;session.created 的 parentID 记录为 child→parent 映射,child 状态上报带 headless: true
MiMo Code hooks/mimocode-plugin/index.mjs 与 opencode 同源事件词汇;跑在 mimo.exe 进程内,共享 @mimo-ai/plugin SDK
Pi hooks/pi-extension.ts + hooks/pi-extension-core.js global extension,目录 ~/.pi/agent/extensions/clawd-on-desk;state-only,不等待 /permission
OpenClaw hooks/openclaw-plugin/index.js plain ESM default object,OpenClaw plugin loader 直接识别;Phase 1 只上报状态
Hermes hooks/hermes-plugin/init.py Python plugin,跑在 Hermes worker 进程内;同步 POST(避免短命 hermes -z 进程退出前丢事件),Clawd 未启动时有短 cooldown
DeepSeek Harness hooks/dsh-clawd-bridge/ Node ESM plugin,每个 session 独立 FIFO POST 动态发现的 127.0.0.1:23333-23337/state;src/dsh-state-sequence.js 用持久 event.seq / 独占 session.seq watermark 拒绝 stale、duplicate 与 dispose 后 late event

2.3 远程与 WSL

  • 远程 SSH(反向端口转发):远程服务器上的 Claude Code / Codex CLI 的 secure hooks 只 POST 到 profile pin 住的远端转发端口,SSH 隧道落到该 profile 的临时本地 ingress;ingress 校验 routing nonce 并写入 profileId canonical namespace,带 CLAWD_REMOTE=1 + CLAWD_SSH_REMOTE=1,跳过远端 PID 聚焦。secure identity 缺失/损坏时 fail closed,不回退 23333-23337 扫描。
  • WSL(本机 loopback,但 PID 属于 Linux VM):WSL 里的 hook 用 Linux ps 解析进程字段,又经 127.0.0.1 发到 Windows Clawd;Windows 打开进程时忽略 PID 低两位。服务端按请求自身标记(wsl_distro 非空或 host: "wsl:<distro>")剥离与 Remote SSH 完全相同的进程字段(sourcePid / wtHwnd / agentPid / pidChain / editor / tmuxSocket / tmuxClient),保留 orcaPaneKey / cwd / host / wsl_distro。因此 WSL 会话没有按进程退出的清理,只按空闲超时清除。

三、会话标题:多级来源与优先级契约

文档用一个清晰的优先级链定义会话标题来源(以 Claude Code 为例):

hook 输入的 session_title(手动改名)→ transcript 里的手动标题(custom-title / agent-name,取最后一条有效者,不按会话过滤)→ AI 标题(ai-title,取本会话最新一条有效者)→ 仅 UserPromptSubmit 且以上都没有时用消息首行兜底。

消息首行兜底会沿 body 上报 session_title_from_prompt: true;服务端不让它覆盖已有的正式标题(手动改名、AI 标题、metadata-only 写入的、重启恢复的),metadata-only 请求里的标题一律算正式标题并忽略该标记;消息首行派生的标题不写入会话历史和恢复记录。首行提取规则实现在 hooks/clawd-hook.js 的 extractPromptTitle():取第一条非空行、命中密钥正则(PROMPT_TITLE_SECRET_RE,覆盖 api_key/authorization/bearer/password/secret/token、sk-、ghp_、AKIA 等形态)不给标题、最多 40 字。

围绕标题派生,还有三类专门实现:

  • Qoder:src/qoder-session-title.js 对 SessionStart / UserPromptSubmit / Stop 做异步增量读取(工具、权限、通知事件不触发扫描,显式标题也不触发 I/O);仅处理 enabled、本机且非 WSL 的会话;每个会话串行读取,FileHandle 始终在 finally 中关闭,读取中断时逐 chunk 保持 offset 与 partial 一致。
  • WorkBuddy:hooks/workbuddy-hook.js 转发 transcript_path 并标记 prompt 首行 fallback;src/agent-runtime-main.js 持有 workbuddy-session-title observer(见 createWorkBuddySessionTitleTracker),读取匹配的 sessions 行,优先 custom_title → title;根目录依次为绝对 WORKBUDDY_CONFIG_DIR、~/.workbuddy-ai、~/.workbuddy,拥有 transcript 的 home 优先;cwd 不匹配与超过 4 KiB 的标题被拒绝。数据库只读打开、读后关闭,绝不写库内容或 settings,缺失的 home 从不创建。SQLite 不可用时回退 src/jsonl-session-title.js 增量读取(每扫描最多 1 MiB 新字节、保留有界 partial 行);只有 SQLite 能报告 archive/delete 生命周期。
  • Cursor:hooks/cursor-session-title.js 只读标准 Cursor desktop profile 的 User/globalStorage/state.vscdb,按 conversation ID 查 composerHeaders、旧 ItemTable['composer.composerHeaders'] 或 cursorDiskKV['composerData:<id>'] 中的 name;单条 JSON record 最多 1 MiB,数据库缺失/损坏/锁定/未知 schema 均不阻塞状态 hook;node:sqlite 自 Node 22.13 / 23.4 起无需 flag。

此外,TraeCode 与 MiniMax Code 没有原生会话标题字段,采用「从首次 prompt 首行派生并保持首个标题」的 server 端 first-wins 策略。

四、本地会话历史与恢复

~/.clawd/session-history-v1/ 是独立的本机会话索引,与 session-recovery-lease.js 的进程存活条件严格分离:

  • lease 用于恢复仍在运行的状态(hooks/session-recovery-lease.js),history 用于在进程退出/重启后找到可手动继续的旧会话(hooks/session-history.js);历史行本身不是 live session,不进状态机、HUD、recap 或权限自动化;
  • Claude command hook 在 POST 前 best-effort 写入历史(Clawd 离线也能记录),只覆盖本机交互式 Claude Code;保存 session ID、cwd、显式标题、状态和时间,不保存 prompt 派生标题、回复或工具内容——它是索引,不是 transcript 备份;目录/文件权限为 0700 / 0600(POSIX);
  • 有效记录按 30 天 / 200 条清理;Dashboard 主列表最多展示 25 条已确认(transcript 在且 cwd 现存)的行,未确认可恢复的行进默认收起的折叠组;每条记录复用 lease 的跨进程锁,锁内读取、合并并原子替换;同毫秒 terminal 事件优先;
  • src/session-history-loader.js 除生成显示标题外不读取内容:没有显式标题时,读取探测定位到的 transcript 的第一条用户输入,用 live prompt 标题的同一套规则生成标题,只用于显示、不写入历史;提取结果按 transcript 路径 + mtime + size 缓存;
  • src/session-history-runtime.js 是唯一手动恢复 owner:只接受受信任 Dashboard 发来的 agent / session ID,在 main 重读已保存 cwd,并重新检查已安装、已启用和本机 live 状态;remote / WSL / 其他 agent 的同名 ID 不应误挡本机恢复;同一 session 的并发恢复合并为一次请求;main 保留 30 秒确认窗口,超时只允许用户检查终端后手动重试,不自动重试,也不伪造 live 状态。

五、本地权限 HTTP 边界(Local Permission HTTP Boundary)

POST /permission 是原生 hook/plugin 接口,src/server.js 在读取 body 或记录 hook 事件之前执行严格请求校验:

  • 拒绝任何 Origin 头(包括空或 null);
  • HTTP Host 必须是显式 127.0.0.1、localhost 或 [::1](可带合法端口);
  • 媒体类型必须是 application/json(允许 charset=utf-8 等参数);
  • 重复的 Host 或 Content-Type 字段被拒绝;Forwarded 头不授予访问权;OPTIONS 不启用跨域 preflight。

这些是浏览器请求防护:仅靠 loopback 绑定与 CORS 响应限制并不能阻止简单的跨源 POST 创建审批 UI。被拒绝的请求收到空 400/403/415 响应并关闭连接,没有 Clawd 成功标记或 agent 审批/拒绝;原生 hook 保留自己的 no-decision fallback。文档明确说明该机制不认证不受限的同用户进程(它们可以构造合法头);/state 路由不在这个 permission 专属防护范围内。回归证据使用真实 HTTP 路由器与权限所有权模块,见 test/server-permission-ingress.test.js。

六、Recap 本地投影:隐私边界内的活动归因

Recap 是已接受运行时活动的本地投影,而不是 HTTP 或 updateSession() 入口处的第二个观察者。在 agent gates、Codex source/replay 仲裁、权限来源处理、subagent 过滤与完成仲裁全部落定后,src/state.js 把接受的边界通过 src/recap-metrics.js 映射,并向 src/recap-runtime.js 发送 allowlist 的 canonical 事件。

  • src/recap-journal.js 冻结桌面民用时间,并在追加 14 天 ticket 前把稳定的 scope/session/dedupe 身份替换为安装本地 HMAC;
  • 同一规范化记录更新 src/recap-aggregate.js;src/recap-coverage.js 独立记录 Clawd 何时能接收信号;
  • 每日聚合与 coverage 在 ~/.clawd/recap-v1/ 下限制到 400 个本地日;
  • 查询 IPC 只返回宽泛的 local / wsl / remote scope 类别,绝不返回 HMAC 值、profile ID 或分布名;
  • 启动时在有界 event-loop 批次中重建 14 天聚合;不支持的 pre-release aggregate/coverage schema 被隔离而不是迁移;
  • DND 仍是交互/视觉 gate,不停止 recap 或 coverage;Suspend、进程关闭与 recapEnabled=false 会关闭 coverage。

完整指标、隐私与 DST 契约见 docs/guides/recap.md。

七、运行时所有权边界(Runtime Ownership Boundaries)

src/main.js 是 composition root,不再是各子系统的实现 owner。新增或修改行为时应先进入对应 owner,避免把逻辑重新堆回 main.js:

边界 Owner
HTTP /state / /permission src/server-route-state.js / src/server-route-permission.js;src/server.js 负责监听、端口与组合
official hook / local monitor 仲裁 src/agent-runtime-main.js,配合 src/codex-turn-fence.js / src/codex-official-activity.js
双窗口与浮层 src/pet-window-runtime.js 创建/定位 render + hit window;src/floating-window-runtime.js / src/topmost-runtime.js 管浮层重排与 z-order
Settings 写入与副作用 settings-controller 是唯一写入者;settings-actions* 是 pre-commit gates;settings-effect-router 是 post-commit runtime effects
Settings UI settings-ui-core 持有 shared UI state,settings-renderer 是侧栏/tab shell,业务页在 settings-tab-*
Quota reminders src/quota-alerts-runtime.js 读取按来源分离的账户快照;quota-alerts 拥有有界哈希去重历史;quota-notifications 确认原生投递
Theme src/theme-loader.js 是 stateless loader;src/theme-runtime.js 是唯一 active-theme owner

两个跨模块契约值得注意:其一,state.js 的 session snapshot 是共享 schema——Dashboard、Session HUD(含 Orbit quota ring)以及可选 Telegram completion、Discord presence、LAN PWA 等 consumer 都读取它,新增/重命名/删除字段必须检查全部 consumer;其二,Quota reminders 保持默认禁用,使用既有 quota collection,启动后要求每个窗口 fresh confirmation,且从不发起账户请求——完整阈值、恢复与保留见 docs/guides/quota-reminders.md。

八、多 Agent 注册表与能力声明

agents/registry.js 把 26 个 Agent 配置模块聚合成注册表,提供按 ID 查找(getAgent)与按平台收集进程名(getAllProcessNames / getStartupRecoveryProcessNames,Windows 优先 win,Linux 回退 mac)。每个 agent 模块导出事件映射、进程名与能力声明,例如:

  • agents/claude-code.js:capabilities: { httpHook, permissionApproval, notificationHook, sessionEnd, subagent }
  • agents/codex.js:eventSource: "hook+log-poll",sessionEnd: false(无 SessionEnd 事件,task_complete 标记 turn 结束、进程退出清理会话),logConfig: { sessionDir: "~/.codex/sessions", filePattern: "rollout-*.jsonl", pollIntervalMs: 1500 }
  • agents/codex-log-monitor.js:JSONL fallback 增量轮询器(文件监视 + 增量读取 + 状态/metadata fallback,不再做审批猜测)
  • agents/gemini-log-monitor.js:legacy Gemini session JSON 轮询器,当前 hook-only 路径不启动

运行时的安装意图/启停/权限气泡开关经 src/agent-gate.js 读取 prefs.agents[id].integrationInstalled / .enabled / .permissionsEnabled。语义上:enabled 只表示是否处理该 agent 的事件(关闭会让 state.js / server.js 停止处理、清理 session/bubble);integrationInstalled 才表示本机 hook/plugin/extension 是否由 Clawd 维护。snapshot 缺字段时 gate 保守默认 true 以兼容旧版;新安装 schema 显式把 Claude Code / Codex 设为已安装且启用,其余 agent 未安装且未启用。Claude Code 额外有 .subagentPermissionsEnabled 子开关(#451),控制 Task 子 agent 的 PermissionRequest 是否弹泡泡。

动态 custom HTTP Agent 是上述安装模型的明确例外:customApplications 是注册真相,v1 为 state-only,/permission 恒不返回 Allow/Deny,也不创建权限 bubble;已注册 custom 的权限请求返回 204 no-decision,删除/伪造的 custom- ID 直接拒绝,不能降级成 Claude Code subagent。server-hook-events.js 的 recent-event ring 按已解析 agent ID 分桶,非法 custom- identity 写入固定 rejected-custom 桶(原始 ID 最多保留 80 字符),ring 不写 prefs、重启即清空。

九、Hook 与 Plugin 同步

启动链路只自动补齐 integrationInstalled=true 且 enabled=true 的缺失集成;若 prefs 文件不可读(locked && recovered),内存 snapshot 只是非权威 defaults fallback,整条 prefs-backed agent runtime gate fail closed——本次进程不自动同步集成、不启动 monitor、不接受 state/permission ingress、不恢复旧 session。server.js 启动后异步同步已安装且已启用的 Claude / Codex / Copilot / Gemini / Antigravity / Cursor / CodeBuddy / WorkBuddy / Kiro / Kimi / Qwen / ZCode / CodeWhale / Qoder / QoderWork / QwenWork / Reasonix hooks,以及 opencode / MiMo Code / OpenClaw / Hermes / DeepSeek Harness plugins 和 Pi / OMP extension。Settings Agent 页的 Install 执行对应 sync 并一起提交 integrationInstalled=true, enabled=true;Uninstall 调用 marker-scoped 卸载器并提交 false, false。

9.1 Claude hook 健康巡检与自愈(#657)

src/claude-settings-watcher.js 在原有目录 watcher(盯 ~/.claude/、debounce 1 秒)之外,跑一个自调度的低频只读健康巡检(默认周期 5 分钟,不依赖任何 settings.json fs 事件——hook 脚本在其他目录被删除也能发现):

  • 判断逻辑收敛在 src/claude-hook-health.js 的 inspectClaudeHookHealth():解析 command、校验 nodeBin/scriptPath、比对 hooks/install.js 的 resolveClaudeHookPaths() 给出的 command target 与 CLAUDE_CORE_HOOK_EVENTS,复用 Doctor 的 agent-node-bin-parser.js 解析器;resolver 是 total 的只读函数,任何 I/O/plan/环境错误都返回结构化 {ok:false, reason, message},绝不 throw;
  • 可自动修复的问题经 src/claude-hook-operations.js 的实例级队列串行 repair,repair 后重新读盘用同一 inspector 复验;同一 repair signature 连续 3 次修复+复验失败后进入 manual-fix-required,停止自动 mutation,只保留 5 分钟只读复查;
  • source 与 target 是两个真相:source 是当前安装包/repo 的 asarUnpackedPath() 脚本,target 是 settings command 应指向的路径;source 入口或依赖闭包缺失是不可自动修复的 source-script-missing;target generation 缺失/损坏(逐字节校验)产生可自动修复的 target-generation-missing;
  • 本机 Linux AppImage 下,三个入口(clawd-hook.js / auto-start.js / claude-statusline.js)及其完整相对 require 闭包会被 materialize 到 ~/.clawd/appimage-hooks/<generation>/ 并写 0600 的 .clawd-appimage-path marker,要求 APPDIR 绝对路径且拥有全部三个 source entry;共享实现是叶子模块 hooks/appimage-hook-materializer.js,登记在 src/remote-ssh-deploy.js 的 HOOK_FILES。注意:Codex 的本机 AppImage 门禁目前仍是 APPIMAGE-only,没有 Claude 的 APPDIR ownership 校验,两边语义未统一;
  • 所有 mutation 入口(启动 reconcile、watcher 自动恢复、周期自愈、Settings Agent Install/Enable、Doctor Fix、autoStartWithClaude 开关、Uninstall、About 清理)都经过 server.js 持有的同一个 claude-hook-operations.js 队列实例,串行执行、互不覆盖;
  • 巡检严格受 manageClaudeHooksAutomatically、claude-code.integrationInstalled、claude-code.enabled 三个 gate 保护;
  • server.getClaudeHookHealthStatus() 暴露只读状态(healthy / repairing / degraded / manual-fix-required / guarded / stopped),供 Doctor 使用。

十、权限决策流与 Permission Bubble

权限决策分两种阻塞模型:

Claude Code / CodeBuddy(HTTP hook,阻塞):

Claude Code PermissionRequest
  → HTTP POST 127.0.0.1:23333/permission { tool_name, tool_input, session_id, permission_suggestions }
  → main.js 创建 bubble 窗口(bubble.html)显示权限卡片
  → 用户点击 Allow / Deny / suggestion → HTTP 响应 { behavior }
  → Claude Code 执行对应行为

子 agent(Task)内触发的请求带 agent_id(实例 uuid)/ agent_type;server-agent-id.js 归一化为 claude-code 并标记 subagent 来源;当 agents["claude-code"].subagentPermissionsEnabled=false(#451)时直接断开连接让 CC 回落终端提示(ExitPlanMode / AskUserQuestion 豁免)。

Codex(official PermissionRequest command hook,阻塞):hook 脚本挂起等待 /permission,再把 sanitized allow/deny JSON 写到 stdout。默认 intercept 模式创建普通 Allow/Deny bubble;显式 native 模式记录 notification 并立即返回 no-decision,交给 Codex AutoReview / 原生审批;DND / disabled / bubble hidden / Clawd unavailable 时 stdout {},Codex 回到原生审批提示。POST /permission 的 Codex body 额外带 turn_id、tool_input_description、tool_input_fingerprint。

opencode / MiMo Code(event hook + 反向 bridge,非阻塞):plugin POST /permission(带 bridge_url + bridge_token)→ Clawd 立即 200 ACK(不挂连接)→ 创建 bubble → 用户决定 → Clawd POST plugin 的反向 bridge → bridge 用 ctx.client._client.post() 调宿主内置 Hono 路由 /permission/:id/reply。用户在原生 UI 回答时走 permission.replied lifecycle(使用独立的 lifecycle_bridge_url/token,对旧 Clawd fail-safe),Clawd exact-match 删除该 request 的本地 pending、bubble、timer 与 notification,不复制 reply、不产生第二次宿主决定。

DeepSeek Harness(approval waterfall,阻塞):bridge prepend listener 挂起 POST /permission,独立 adapter 创建仅 Allow Once / Deny 的 bubble;204、断连、DND、disabled 或所有审批通道无决定时调 next() 交还 DSH 原生审批流程;ask_user_question 不进入 Clawd。

关键决策边界(源码与文档一致):

  • agents/registry.js 的 capability 声明是 agent 是否进入权限、interactive bubble、subagent 等路径的权威来源;automation 的 agent/family eligibility 另有显式白名单,故意不能从 permissionApproval 自动推导;
  • WorkBuddy 不进入 /permission;QwenWork 不进入 /permission(PermissionRequest/PermissionDenied 只被观察并映射成 working);TraeCode、MiniMax 同样不注册 /permission、不进 permission automation eligibility;
  • DND 只负责「不弹 bubble」,不替用户决定:opencode 与 MiMo Code 分支 silent drop 让 TUI 内置提示接管,Claude Code 分支 res.destroy() 让 CC 回内置确认,Codex 分支返回 no-decision {},DSH 分支返回带 server identity 的 204;
  • permission.js 是 permission presentation 的唯一 owner:用目标 workArea、text scale、HUD avoid rect 和每张卡实测宽高先尝试原逐窗栈,不安全时按 agent + session 选 FIFO 代表并预留队列入口;overflow 队列使用独立 permission-queue.html,只暴露 open/close/select/ACK 四类导航 IPC,没有任何决定 IPC;
  • 每个权限请求创建独立 BrowserWindow,普通卡片默认约 340 CSS px 三行摘要,详情态约 500 CSS px(偏好 min(60% workArea, 620 CSS px)),详情正文独立滚动;桌面同时最多一个详情 owner;
  • 涉及 Claude Code 权限 payload 的改动必须用真实 Claude Code 验证——curl 自编请求历史上掩盖过字段结构 bug。

十一、Plugin 集成专题

文档的 Plugin Notes 提供了若干跨插件通用约束与重点边界:

  • 进程树 walk 从 process.pid 起步,不是 ppid;不要用 process.ppid 做轻量替代——Claude Code / hook 进程链里它通常只是临时 shell PID;
  • task 工具会直接新建 session;只有 session.created 明确带 event.properties.info.parentID 的 session 才被视为 child;opencode child session 作为 root 拥有的后台 headless 工作处理,不参与 HUD / focus / 多会话 fanout;
  • opencode 2.x(#1039):core.mjs 内 createOpencodeFamilyPluginV2 产出零 import 的 {id, setup} 定义(v2 loader 拒绝函数 default export);installer 仅在宿主探测确认为 v2 时注册进 plugins 键(v1 的 plugin 键不动);事件词汇完全换代(session.step/reasoning/text/tool/execution.*、session.renamed),cwd 取事件信封 location.directory;权限用 ctx.permission.hook("evaluate") 阻塞 POST /permission,决定是响应体 {decision: allow|always|deny},204/超时/错误不改 effect 回原生 ask;v2 上无 reverse bridge;
  • 打包后需要把 app.asar/ 重写为 app.asar.unpacked/;plugin 内发出的 POST 必须 fire-and-forget,避免拖慢 TUI(Hermes 是唯一同步 POST 的例外);
  • DSH 版本族门禁:按小版本族(major.minor)放行,维护唯一写出具体版本/npm artifact/integrity 的已验证 artifact 清单(0.2.0-rc.2 优先,0.1.5-rc.3、0.1.5-rc.1、0.1.1-rc.2、0.1.0-rc.6 保留);未命中任何族或低于下限的版本一律禁止 mutation;安装器按 canonical DSH_HOME 哈希命名空间把 bridge 复制成 immutable hash generation,用官方 dsh plugin --profile web|desktop add/remove mutation;
  • Pi 专属约束(Pi Notes):extension 运行目录不在 Clawd repo 内,不能依赖 hooks/shared-process.js;只在 ctx.hasUI === true 或交互式 TTY 模式上报状态;tool_call handler 必须顶层 catch 并返回 undefined(Pi 的 emitToolCall() 不 catch extension 异常);tool_result 按 isError 拆成 PostToolUse / PostToolUseFailure;agents.pi.permissionsEnabled 默认 false,v4 migration 会把旧 true 重置为 false;
  • OMP 专属约束(OMP Notes):extension 目录按 active agent directory 解析(默认 ~/.omp/agent,PI_CONFIG_DIR 改 config root,PI_CODING_AGENT_DIR 改默认值),hooks/omp-install.js 的 resolveOmpAgentDir() 是唯一解析入口;完成事件两阶段确认(session_stop 只记候选,等 agent_end 确认 willContinue !== true 才上报 Stop);session_switch / session_branch 对被离开的 session 补发合成 SessionEnd;社区 bridge clawd-on-desk-omp.ts 与内置 extension 互斥(发现即 fail closed);
  • OpenClaw 专属约束(OpenClaw Notes):Phase 1 只支持状态动画;manifest 必须包含 activation.onStartup 和空对象 configSchema;model_call_ended 成功后 1500ms debounce 发 Stop;session_end 只在 idle|daily|deleted|unknown 时映射 SessionEnd/sleeping;POST body 是 allowlist(agent_id / session_id / state / event / cwd / agent_pid / tool_name / tool_use_id / hook_source / openclaw_* / error_present 等),禁止透传 params / result / error 字符串 / messages。

十二、终端聚焦与远程传输协调

  • CJS hook 脚本通过 hooks/shared-process.js 的 createPidResolver() 与 lifecycle context 遍历进程树定位终端应用 PID(Windows Terminal、VS Code、iTerm2 等);source_pid 跟随状态更新送到 main.js,右键 Sessions 子菜单点击后 focusTerminalWindow() 用 PowerShell(Windows)或 osascript(macOS)聚焦终端;
  • Windows 的 Cursor / VS Code 父进程窗口优先按项目标题唯一匹配;标题不匹配或缺少 cwd 时,仅在该进程可见候选窗口唯一时兜底唤起;多窗口歧义或无可见候选时不以 MainWindowHandle 猜选;
  • 远程场景只通过 Settings Remote SSH controller 部署:runtimeKey → layout 解析、installId/profileId/nonce 身份、原子 lease/fencing、持久部署事务和 profile 专属 ingress 共同把远端 hook 事件回送到本地 Clawd;scripts/remote-deploy.sh 已 fail-fast 停用;远程部署不设置本机 integrationInstalled,也不自动重启 gateway(托管模块被替换时只报告 restart-required);
  • Remote SSH transport coordination:remote-ssh-transport.js 通过 ssh -G 展开本机 SSH 配置并分类 effective transport——ordinary SSH 保持原 parallel tunnel + health-probe 行为,Codespaces gh cs ssh --stdio 与显式 serialized override 进入 single-session 路径;serialized ownership 以有效 transport key 为作用域而非 profile id;正常暂停通过 tunnel stdin EOF 请求远端 readiness 进程退出,未验证 drain / watchdog timeout / 仍有 live child 时 slot 进入 quarantine,禁止新 child、mutation、resume 和 interactive terminal。用户流程见 docs/guides/guide-remote-ssh.md,真机矩阵与精确清理流程见 scripts/manual/README.md。

十三、Windows B1a 进程链元数据能力(#694)

Codex、Cursor Agent、Kiro CLI、CodeBuddy 和 Reasonix 的本地 Windows hook 支持版本化的 server-side process-chain capability(src/server-windows-process-metadata.js)。Clawd runtime owner 把以下数据写入 ~/.clawd/runtime.json:随机 instanceGeneration,以及每个 agent 的 legacy | shadow | b1a-authoritative mode。默认始终是 legacy;shadow 和 b1a-authoritative 仅用于显式开发/验证,resolver 初始化或 ABI 校验失败时在写 runtime 前降级回 legacy。

本地 Windows、非 remote/WSL 的 hook 可以把当前 hook Node PID 和 runtime generation 放入 X-Clawd-Hook-Pid / X-Clawd-Process-Instance 头;headless/official/subagent 分类在 server 收到请求后完成,只有通过 effective eligibility 的请求才消费这些 header。header 只发往同一次 immutable runtime observation 指定的端口;PID/generation 是 capability routing metadata,不是认证凭据。b1a-authoritative 下五个 hook 的 eligible 路径不再启动 legacy snapshot PowerShell,以 server 结果 replace/clear sourcePid、agentPid、pidChain 与 walk-derived editor。CodeBuddy direct HTTP PermissionRequest 不经过 command hook,因此没有可信 hook PID,B1a 当前只覆盖其 command state。

十四、其余运行时注意点

  • Context Menu Owner Window:contextMenuOwner 必须保留 parent: win;退出路径依赖 requestAppQuit() 先置 isQuitting = true,再让 window-all-closed 真正走到退出分支,不要绕开这套守卫;
  • Updating:Git 模式(非打包,macOS/Linux 源码运行)git fetch 比较 HEAD,有更新则 git pull + 必要时 npm install,然后 app.relaunch();Windows NSIS 与 macOS DMG 打包模式走 electron-updater,均保持 autoDownload=false,用户确认后才下载;macOS Release 同时发布 x64 / arm64 的 DMG 与 ZIP,latest-mac.yml 必须同时列出两架构且 top-level path 指向 x64 ZIP;托盘菜单里的「Check for Updates」可手动触发;
  • i18n:支持 en / zh / zh-TW / ko / ja / pt-BR / es 七种语言,文案集中在 src/i18n.js,语言偏好持久化到 clawd-prefs.json,启动时通过 hydrate() 灌入 controller。

结语

Clawd-on-desk 的 Agent Runtime 本质上是一套「本地优先、事件驱动、多源仲裁」的运行时:所有 Agent 集成被规范化为非阻塞状态上报与阻塞权限决策两条通道,agent-runtime-main.js 负责跨来源去重与生命周期仲裁,state.js 状态机把事件收敛为桌宠可消费的会话快照,而 main.js 退居 composition root 通过明确的 owner 边界防止逻辑回流。无论你是在扩展新 Agent 集成、调试会话标题异常,还是排查权限气泡行为,都可以从本文的数据流表格与源码路径出发,快速定位到对应的 owner 模块与测试证据。

登录后查看全文
clawd-on-desk