Documentation
¶
Overview ¶
adapter.go —— codex 的 executor.Adapter 实现:五动作与事件翻译。
职责:
- Start/Events/Send/RespondPermission/Stop 五动作
- 把 codex 的 ServerNotification / ServerRequest 翻译成 AdapterEvent 四类事件
- 回合边界判定与收尾分类(复用 internal/executor/turn 的 trailer 与 git 取证)
- 把事件流渲染进 render.log,供 handoff attach 的第二窗口实况显示
边界:
- 不写 store、不做审批判断、不做状态机迁移(executor 契约的硬边界)
- 不碰 codex 的配置文件:安全档位全部协议级下发且每回合重钉
appserver.go —— codex app-server 的 WebSocket JSON-RPC 2.0 双向客户端。
职责:
- 维护一条到 `codex app-server --listen ws://…` 的 WS 连接
- 我方请求(initialize / thread.* / turn.*)按 id 匹配响应;turn/start 用 CallAsync 拿异步通道(它虽然立即返回,但仍不能阻塞 Start 的启动路径)
- 对方通知经 OnNotify 分发;对方请求经 OnServerRequest 上抛,应答可延迟任意久 后经 Reply 回发——协调者可能过夜才裁决
边界:
- 不认识 codex 的业务语义(不知道什么是回合、什么是权限),只做协议管道; 语义翻译在 adapter.go
- 不重连:重连属 adapter 的生命周期决策,本层只在连接死亡时 OnClosed 通知
铁律:**每一条带 id 的入站消息都必须有应答**。Handler 认不出的方法由本层统一 回 -32601——静默丢弃有 id 的请求等于让 codex 侧永久等待,回合从此挂死。
items.go —— codex ThreadItem 的结构定义与 itemId → item 的有界索引。
职责:
- 解析 item/started 与 item/completed 通知里的 item 本体
- 维护 itemId → 最近一次 item 的有界索引
- 把 item 渲染成 render.log 的一行人读文本
边界:
- 不产 handoff 事件、不做权限判据:判据在 perm.go,事件在 adapter.go
为什么需要索引:`item/fileChange/requestApproval` 的报文**没有路径**(schema 的 必填字段只有 itemId/threadId/turnId/startedAtMs,spec §5.4),路径只在同 itemId 的 item 通知的 changes[].path 里。没有这张索引,写文件类权限门交出的 PermRequest 就没有路径,B27 的路径判据直接失效。
为什么有界:item 数量由 codex 侧决定,长任务可产出上万条。权限请求总是紧跟在 对应 item 之后到达,512 条窗口足够宽;无界会让内存随 item 数线性增长。
perm.go —— codex 权限请求的判据、挂起表与裁决映射。
职责:
- 把 item/*/requestApproval 的报文翻译成 executor.PermRequest(安全判据的输入)
- 维护 itemId → 待裁决请求 的挂起表,供 RespondPermission 回发
- 记录本回合被拒清单,回合收尾时一并交代给协调者
边界:
- **不做审批判断**:批不批由 manager 依协调者应答决定(executor 契约的硬边界)
- 不写 store、不发事件
裁决映射只有两个出口(spec §5.4,依据官方 schema):
- accept —— 放行这一次
- decline —— 拒这一次,**回合继续**
绝不使用 cancel(会立刻掐掉整个回合,等于协调者点一次「拒绝」就杀掉任务, 与另三个 adapter 行为不对等),也绝不使用 acceptForSession / acceptWithExecpolicyAmendment / applyNetworkPolicyAmendment(都是「以后同类 不再问」,正是 B23 明确否掉的语义)。
preflight.go —— agentd 以 codex 为缺省执行者启动时的环境预检。
职责:
- 硬前提(codex 在 PATH、已登录)不满足时给出可行动的错误,早失败早止损
- 软污染源(AGENTS.md / hooks.json / mcp_servers)存在时 WARN 并提示清理
边界:
- 不改任何文件:清理是人的决定,agentd 不替用户动他的 ~/.codex
- 不检查配置里的 model / sandbox_mode / approvals_reviewer 等项——它们全部 被 handoff 协议级压过(spec §1.1 实证),检查它们只会制造噪音
为什么区分 error 与 WARN:硬前提不满足时任务必然失败,且失败点在回合中途、 诊断成本高;软污染源只改变 executor 的干活方式,不影响安全边界(安全档位由 代码钉死,spec §1.3),值得提醒但不值得挡住启动。
probe.go —— 只读存活探测。
职责:
- Probe:读 proc.json,走 Proc.Alive 的既有判据(存活锁 + TCP 可连),如实返回结论
边界:
- **绝不写**:不回收执行者进程、不碰 store、不发事件
- 判据弱于 grok 的 HTTP 探活:端口活着不等于协议层活着(见 proc.go 文件头), 所以 Note 里如实写「端口可连」,不夸大成「executor 正常」
proc.go —— codex app-server 的进程生命周期:prochost 托管、探活、恢复凭据落盘。
职责:
- StartServe:选空闲端口、经 prochost 拉起 app-server、探活等就绪、落 proc.json
- Alive/Kill/LogTail:存活探测、回收、诊断尾部
- ReadServeInfo:从 proc.json 重建 Proc,供 agentd 重启后 Resume(B18)
边界:
- 不说协议、不解析事件:协议在 appserver.go,语义在 adapter.go
- 不做重试决策:探活失败只如实返回,重试与判死节奏归 adapter 的看门狗
为什么没有 Secret 字段(与 grok 不同):`codex app-server --listen ws://` 不带 鉴权 secret,proc.json 里没有凭据,LogTail 也不需要脱敏。仍写 0600——任务目录 里的文件一律 0600,不为个案开口子。
为什么存活判据是「存活锁 + TCP 连通」而不是只有 HTTP GET(与 grok 不同): 锁证明 shim 还在;`--listen ws://` 起的是纯 WebSocket 服务端,没有 HTTP 面可探, TCP 连通是弱于 HTTP 的判据——端口 listen 住但协议层已死时会误判为活。**真正的 健康信号是 WS 连接自身的死亡**(Handler.OnClosed),Alive 只用于「起没起来」 和看门狗的粗判,不要把它当强判据用。
question.go —— codex 原生提问通道 item/tool/requestUserInput 的翻译。
职责:
- 解析提问报文,渲染成交给协调者的问题全文
- 构造必须立即回发的应答体
边界:
- 不决定「回合要不要结束」:那是 adapter 回合收尾的事
- **不代传机密**:isSecret 的问题正文不进事件库
为什么必须立即应答而不是等协调者:回调跑在读循环 goroutine 上,等协调者会卡死 整条连接;而不应答会让 codex 侧的回合永久挂起。grok 那边这条通道翻过两次车 (应答形态错被判工具失败、兜底重复上报导致一次提问两张工单),此处逐条对症。
reap.go —— 运行态丢失时的兜底回收(B20)。
职责:按 proc.json 拿 prochost.Handle 并 Kill,不留孤儿进程。 边界:不删任务目录、不碰 worktree(那是归档与 B15 的职责);
**不删 ~/.codex/sessions**——那是 codex 自己的会话历史,删了会破坏 用户本人的 `codex resume`(spec §5.5)。
resume.go —— agentd 重启后的运行态恢复与连接看门狗。
职责:
- Resume:按四级阶梯尝试恢复(不可恢复 / reattach / cold / fresh)
- watchdog:探活 app-server,判死后走统一的失败处置
边界:
- 不重建 worktree:任务工作区可能已随归档清理,重建是 Dispatch 的职责, 越界重建会让归档过的任务诈尸
- 不改任务状态:Resume 只如实返回结论,状态迁移归 manager
codex 的两个结构性优势(相对 grok):
- rollout 落在用户级 ~/.codex/sessions/**,agentd 重启、甚至 app-server 进程重启后 thread 都还在盘上,冷恢复不依赖任务目录里的会话数据
- 没有凭据软链要修(复用用户级 home,凭据零副本),冷恢复路径短一截
taskenv.go —— codex 任务的启动物料:包内文件名常量。
职责:
- 统一约定任务目录内的文件名(serve.log / render.log)
边界:
- 不起进程(proc.go 的 prochost 负责)、不碰协议(appserver.go)
- **刻意不生成任何 codex 配置文件**:本设计的安全档位全部协议级下发 (spec §2「配置下发:全部协议级,不碰任何 config 文件」),写配置文件会 让「代码钉死安全边界」这条保证多出一个可被绕过的入口
- env 注入不再经启动脚本:改由 proc.go 的 Spec.Env 直传(B19 覆盖语义不变, droppedEnvKeys 的丢弃逻辑随脚本一并移入 StartServe 的 env 处理)
Index ¶
- func Preflight(home string, log *slog.Logger) error
- type Adapter
- func (a *Adapter) Events(taskID string) <-chan executor.AdapterEvent
- func (a *Adapter) PermissionsVolatile() bool
- func (a *Adapter) Probe(req executor.ProbeReq) (executor.ProbeOutcome, error)
- func (a *Adapter) ProcHandle(taskID, taskDir string) (prochost.Handle, error)
- func (a *Adapter) Reap(taskID, taskDir string) error
- func (a *Adapter) RespondPermission(ctx context.Context, taskID, permID, decision string) error
- func (a *Adapter) Resume(req executor.ResumeReq) (executor.ResumeOutcome, error)
- func (a *Adapter) Send(ctx context.Context, taskID, text string) error
- func (a *Adapter) Start(ctx context.Context, req executor.StartReq) (err error)
- func (a *Adapter) Stop(taskID string) error
- type Client
- func (c *Client) Call(ctx context.Context, method string, params any) (json.RawMessage, error)
- func (c *Client) CallAsync(method string, params any) (<-chan Result, error)
- func (c *Client) Close() error
- func (c *Client) Notify(method string, params any) error
- func (c *Client) Reply(reqID json.RawMessage, result any) error
- func (c *Client) ReplyError(reqID json.RawMessage, code int, message string) error
- type Handler
- type Proc
- type Result
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Adapter ¶
type Adapter struct {
// contains filtered or unexported fields
}
Adapter 是 codex 的 executor.Adapter 实现。
并发安全:runs 表由 mu 保护;每个任务的运行态只被该任务自己的回调路径访问。
func (*Adapter) Events ¶
func (a *Adapter) Events(taskID string) <-chan executor.AdapterEvent
Events 返回该任务的事件流通道(Start 后可用)。通道关闭表示执行终结。
func (*Adapter) PermissionsVolatile ¶
PermissionsVolatile 表明本 adapter 的权限请求随连接消亡。
manager 据此在 agentd 重启后拒绝恢复「尚有未决权限工单」的任务:按最保守路径 假设 thread/resume 不会重发未决授权请求(spec §8)。
func (*Adapter) Probe ¶
Probe 只读探测 codex app-server 是否仍存活(manager 的 prober 可选接口)。
返回:
- err != nil:探不出结论(proc.json 缺失/损坏),调用方按 unknown 处理
func (*Adapter) ProcHandle ¶
ProcHandle 交出该任务的进程句柄(来自任务目录的 proc.json)。
参数:
- taskID: 任务 ID,仅用于日志定位
- taskDir: 任务目录(凭据所在)
返回:
- 进程句柄;proc.json 不存在或不可解析时返回错误
注意:本方法**只读**,不探活、不发信号——存活判定与回收分别是 prochost.Alive 与 prochost.Sweep 的职责。agentd 以可选接口消费它 (不实现该方法的 adapter 一律按「无凭据」降级,与 reaper/prober 同款路数)。
func (*Adapter) Reap ¶
Reap 回收一个任务残留的执行者进程。
参数:
- taskID: 任务 ID(用于日志)
- taskDir: 任务目录(用于读 proc.json)
为什么不再有「确定性命名兜底」:旧实现在 proc.json 缺失时退到 tmux 会话名 handoff-<id8>,因为会话名可由 taskID 推导。锁+pid 无法从 taskID 推导, proc.json 缺失就是真的无据可查——如实报错交协调者,不猜。
返回:回收失败的错误;进程本就不在时返回 nil(回收是幂等的)
func (*Adapter) RespondPermission ¶
RespondPermission 应答 codex 的权限请求。
参数:
- taskID: 目标任务
- permID: 权限请求 id(即 codex 的 itemId,裸值不带命名空间前缀)
- decision: "once"(批准本次)或 "reject"(拒绝)
返回:
- 任务不在运行中、或挂起表查不到该 permID 时,包装 executor.ErrTaskNotRunning ——两者都意味着「executor 侧那次请求已经不在了」,调用方据此转失败交协调者, 而不是当作可重试的瞬时错误
func (*Adapter) Resume ¶
Resume 尝试恢复一个 agentd 重启前已在执行的任务。
参数:
- req: 恢复请求(TaskDir 是 serve.json 所在;RepoPath 是 thread/resume 的 cwd; SessionID 是落库的 threadId;Cold 决定是否允许重起进程)
返回:
- Alive=true:进程存活或已重起、WS 已重连、thread 已载入、事件流已重建
- Alive=false:判不可恢复,调用方据此转 failed 交协调者。**这不是错误**, err 恒为 nil 的路径很多,调用方不要靠 err 判别
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client 是一条 app-server 连接。并发安全:nextID/pending 由 mu 保护; 写连接由 writeMu 串行化(websocket 不允许并发写)。
func Dial ¶
Dial 连接 app-server 端点并启动读循环。
参数:
- ctx: 仅控制握手阶段;连接生命周期延续到 Close
- wsURL: 形如 ws://127.0.0.1:<port>
- h: 回调面(不得为 nil)
- log: 日志入口(nil 退回 slog.Default())
返回:已就绪的连接;握手失败时返回错误
func (*Client) Reply ¶
func (c *Client) Reply(reqID json.RawMessage, result any) error
Reply 应答对方请求。reqID 必须是 OnServerRequest 收到的原值。
func (*Client) ReplyError ¶
ReplyError 以 JSON-RPC 错误应答对方请求(用于不实现的方法,如令牌刷新)。
type Handler ¶
type Handler interface {
// OnNotify 收到对方通知(无 id 的消息)。
OnNotify(method string, params json.RawMessage)
// OnServerRequest 收到对方请求(有 id,必须应答)。
//
// 返回 false 表示本端不认识该方法,传输层随即代为回 -32601;返回 true 表示
// 本端接管,实现方**必须**在此后某个时刻调用 Reply 或 ReplyError。
OnServerRequest(reqID json.RawMessage, method string, params json.RawMessage) bool
// OnClosed 连接终止(err 为终止原因,主动 Close 时为 nil)。
OnClosed(err error)
}
Handler 是 adapter 侧的回调面。实现方必须假定回调在读循环 goroutine 上触发: **不得在回调里做阻塞操作**,否则会卡住整条连接的消息消费。
type Proc ¶
type Proc struct {
Handle prochost.Handle `json:"handle"`
TaskDir string `json:"task_dir"` // 任务目录
Port int `json:"port"`
}
Proc 是一个 codex app-server 实例的句柄与恢复凭据。
func ReadServeInfo ¶
ReadServeInfo 从任务目录读回 Proc,供 agentd 重启后 Resume(B18)。
func StartServe ¶
func StartServe(ctx context.Context, repoPath, taskID, taskDir string, env []string, log *slog.Logger) (*Proc, error)
StartServe 经 prochost 拉起一个任务专属的 codex app-server 并等其就绪。
参数:
- ctx: 控制启动阶段的超时/取消
- repoPath: 任务工作目录(serve 的 cwd)
- taskID: 任务 ID(日志与 proc.json 定位)
- taskDir: 任务物料目录
- env: 注入到 app-server 进程的环境变量(B19)
- log: 日志入口(nil 退回 slog.Default())
返回:就绪的 Proc;任一步失败返回错误(错误携带 serve.log 尾部)
注意:**没有 model 参数**(与 grok 不同)——codex 的模型选择是协议级的 (thread/start 的 model 字段),不经启动描述。
func (*Proc) Alive ¶
Alive 检查 codex app-server 是否仍然存活:存活锁被持有 且 WS 端口可连。
为什么第一条是锁:本地文件操作、微秒级;端口探测要走网络栈且失败要等超时。 锁判死就不必再探端口。