← 返回首页

LR2:把执行做成协议——execd 与 14 态 Cell

··8 分钟·
#AI Agent#Orcana#Linux#协议

为什么把执行做成协议

run_process("sleep 5") 这样的工具函数,在单机演示里够用,在真实环境里暴露三个问题:

  1. 认证:任何能调 API 的进程都能启动任意命令。agent 的工具边界 = 进程的信任边界,这不是设计,是侥幸。
  2. 状态:进程跑起来了,谁负责知道它活着?谁负责在 agent 崩溃后清理?进程表和 agent 内存之间没有权威的事实源。
  3. 恢复:agent 挂了再起来,之前启动的进程是继续、是杀掉、还是等它自己了结?没有记录就没有答案。

LR2(Linux Runtime 2.0)的回答是:把执行从"函数调用"升级成协议。LR2-0 先做 ExecutionGateway 统一入口(Intent / Context / SpecBuilder),把 run_process / run_shell_script 全部迁移过去,shadow parity 评测保证行为不变——旧路径和新路径并行跑同一批用例,输出逐项对比。然后 LR2-1 做真正的主角:execd。

execd:常驻执行 daemon

帧协议:4 字节 big-endian 长度前缀 + UTF-8 JSON,单帧上限 16 MiB。frame.ts 做编解码和流式解析器(跨帧缓冲),events.ts 定义 ServerEvent——eventSequence 单调递增 + 类型 + payload。12 种方法消息。编解码有 golden 测试:长度边界、截断、大帧、未知方法拒绝、版本不匹配拒绝。

认证:peercred.ts 用 bun:ffi + libc getsockopt(SOL_SOCKET, SO_PEERCRED) 拿对端真实 pid/uid/gid——身份由内核背书,不需要 token、不需要握手密钥。降级策略也写死了:bun 版本升级导致 ffi 失效时,降级为 socket 文件权限 + 目录权限(0600/0700 已限同用户),认证强度降级必须显式记录——不允许静默变弱。

状态:SQLite 13 表,append-only cell_events 事件日志 + 物化状态。WAL 模式,崩溃安全。SubmitCell 用 requestId 幂等——idempotency_keys 表里同 requestId 直接返回已提交的响应,网络重试不会重复执行。

事件流:WatchCell 订阅断线期间的事件增量由 SQLite cell_events 兜底,重连客户端从最后确认的 eventSequence 断点续读——事件流不会因为连接抖动而丢。

Cell 状态机(14 态,ADR-002):一个 Cell 就是一个受管的执行单元。从 pending → submitting → running → completed / failed / cancelled,中间有 cancelling、cleaning、killing 这些过渡态,还有两个容易被忽略的终态:SIDE_EFFECT_UNKNOWN(外部副作用不盲跑,等 reconcile)和 LOST。取消不是一条命令,是一段受管的状态推进:cancel → 真实终止进程树 → 记录清理结果。

LeaseManager:Cell 有租约。Acquire / Renew / Release,过期即失效、幂等 sweep。客户端崩了,租约到期,Cell 进入恢复流程——不会因为客户端失联而永远挂着。

Recovery:启动时扫描所有非终态 Cell,收敛到终态。Recovery Receipt 只记录已知事实,不编造原因。外部副作用(比如 Cell 已经启动了一个没法安全杀掉的进程)显式标记为 SIDE_EFFECT_UNKNOWN,作为终态等待 reconcile,而不是假装能恢复。

端到端的样子

验收场景是这么写的:客户端崩溃,Cell 不丢;重启后恢复扫描收敛;租约过期失效;进程树真实终止(kill 组而不是 kill 单进程)。

systemd user service 部署,Delegate=NoNewPrivileges 限制提权面。CLI 和 daemon 分离——CLI 是瘦客户端,daemon 是唯一执行者。客户端只是协议的对话方。

一个细节的启发

恢复扫描"不触碰 SIDE_EFFECT_UNKNOWN"这个决定,当时是作为 bug 修掉的(一开始它被当作可恢复状态继续扫)。细想其实是个设计原则:**不知道后果的操作,宁可停下,不可重试。**外部进程可能已经做了一半的副作用——杀掉它可能更糟,重跑它可能重复。终态 + 人工 reconcile 是最诚实的答案。这条原则以后写进每一份执行协议里。

进度

LR2-1 按 L1-A 到 L1-I 的阶段划分推进:

阶段内容状态
L1-AExecProtocol 契约(帧编解码 + 12 方法 + eventSequence + SO_PEERCRED)完成
L1-BStateStore(13 表 + append-only 事件 + WAL 崩溃安全)完成
L1-CServer(socket + 认证 + 帧路由 + WatchCell 断点续读)完成
L1-DCellManager(幂等 Submit + 14 态状态机 + 真实进程终止)完成
L1-ELeaseManager(租约 + 过期失效)完成
L1-FRecovery(非终态扫描 + Receipt 只记事实 + 外部副作用不盲跑)完成
L1-G组装 + daemon 入口 + CLI + systemd 服务完成
L1-HGate 验收(7 项 Gate 各一条显式断言,门禁 318/318 全绿)完成

每一步都是"代码 + 测试 + 接线"三件套验收。下一步是收尾阶段(L1-I)。