codex

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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):

  1. rollout 落在用户级 ~/.codex/sessions/**,agentd 重启、甚至 app-server 进程重启后 thread 都还在盘上,冷恢复不依赖任务目录里的会话数据
  2. 没有凭据软链要修(复用用户级 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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Preflight

func Preflight(home string, log *slog.Logger) error

Preflight 检查 executor 机的 codex 环境。

参数:

  • home: codex home 目录;空串时取 $HOME/.codex
  • log: 日志入口(nil 退回 slog.Default())

返回:

  • 硬前提不满足时返回带可行动指引的错误;软污染源只打 WARN,返回 nil

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter 是 codex 的 executor.Adapter 实现。

并发安全:runs 表由 mu 保护;每个任务的运行态只被该任务自己的回调路径访问。

func New

func New(log *slog.Logger) *Adapter

New 创建 codex adapter。

参数:

  • log: 本模块日志入口(nil 时退回 slog.Default())

func (*Adapter) Events

func (a *Adapter) Events(taskID string) <-chan executor.AdapterEvent

Events 返回该任务的事件流通道(Start 后可用)。通道关闭表示执行终结。

func (*Adapter) PermissionsVolatile

func (a *Adapter) PermissionsVolatile() bool

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

func (a *Adapter) ProcHandle(taskID, taskDir string) (prochost.Handle, error)

ProcHandle 交出该任务的进程句柄(来自任务目录的 proc.json)。

参数:

  • taskID: 任务 ID,仅用于日志定位
  • taskDir: 任务目录(凭据所在)

返回:

  • 进程句柄;proc.json 不存在或不可解析时返回错误

注意:本方法**只读**,不探活、不发信号——存活判定与回收分别是 prochost.Alive 与 prochost.Sweep 的职责。agentd 以可选接口消费它 (不实现该方法的 adapter 一律按「无凭据」降级,与 reaper/prober 同款路数)。

func (*Adapter) Reap

func (a *Adapter) Reap(taskID, taskDir string) error

Reap 回收一个任务残留的执行者进程。

参数:

  • taskID: 任务 ID(用于日志)
  • taskDir: 任务目录(用于读 proc.json)

为什么不再有「确定性命名兜底」:旧实现在 proc.json 缺失时退到 tmux 会话名 handoff-<id8>,因为会话名可由 taskID 推导。锁+pid 无法从 taskID 推导, proc.json 缺失就是真的无据可查——如实报错交协调者,不猜。

返回:回收失败的错误;进程本就不在时返回 nil(回收是幂等的)

func (*Adapter) RespondPermission

func (a *Adapter) RespondPermission(ctx context.Context, taskID, permID, decision string) error

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 判别

func (*Adapter) Send

func (a *Adapter) Send(ctx context.Context, taskID, text string) error

Send 回答提问 / 回发修改指令,对同一会话续接执行。text 原样透传不加工。

func (*Adapter) Start

func (a *Adapter) Start(ctx context.Context, req executor.StartReq) (err error)

Start 异步启动执行并立即返回。

步骤:StartServe → Dial → initialize + initialized → thread/start → emit progress{SessionID}(会话就绪信号)→ turn/start(不等待)。

注意:turn/start 是异步的(立即返回 inProgress),回合终态在 turn/completed 通知里,因此回合边界由通知驱动而非响应驱动。

func (*Adapter) Stop

func (a *Adapter) Stop(taskID string) error

Stop 终止执行并回收资源:置 stopping → turn/interrupt → 关连接 → kill 执行者进程组 → 关事件通道。

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client 是一条 app-server 连接。并发安全:nextID/pending 由 mu 保护; 写连接由 writeMu 串行化(websocket 不允许并发写)。

func Dial

func Dial(ctx context.Context, wsURL string, h Handler, log *slog.Logger) (*Client, error)

Dial 连接 app-server 端点并启动读循环。

参数:

  • ctx: 仅控制握手阶段;连接生命周期延续到 Close
  • wsURL: 形如 ws://127.0.0.1:<port>
  • h: 回调面(不得为 nil)
  • log: 日志入口(nil 退回 slog.Default())

返回:已就绪的连接;握手失败时返回错误

func (*Client) Call

func (c *Client) Call(ctx context.Context, method string, params any) (json.RawMessage, error)

Call 发起请求并阻塞等待响应。

func (*Client) CallAsync

func (c *Client) CallAsync(method string, params any) (<-chan Result, error)

CallAsync 发起请求并立即返回结果通道。

func (*Client) Close

func (c *Client) Close() error

Close 关闭连接,所有挂起的请求以错误终结。

func (*Client) Notify

func (c *Client) Notify(method string, params any) error

Notify 发送通知(无需应答,用于 initialized)。

func (*Client) Reply

func (c *Client) Reply(reqID json.RawMessage, result any) error

Reply 应答对方请求。reqID 必须是 OnServerRequest 收到的原值。

func (*Client) ReplyError

func (c *Client) ReplyError(reqID json.RawMessage, code int, message string) error

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

func ReadServeInfo(taskDir string) (*Proc, error)

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

func (p *Proc) Alive() bool

Alive 检查 codex app-server 是否仍然存活:存活锁被持有 且 WS 端口可连。

为什么第一条是锁:本地文件操作、微秒级;端口探测要走网络栈且失败要等超时。 锁判死就不必再探端口。

func (*Proc) Kill

func (p *Proc) Kill() error

Kill 终止 codex app-server 及其后代(按进程组),幂等。

func (*Proc) LogTail

func (p *Proc) LogTail() string

LogTail 返回 serve.log 尾部,供启动超时与死亡诊断(B16:失败要给可行动真因)。

func (*Proc) WSURL

func (p *Proc) WSURL() string

WSURL 返回 app-server 的 WebSocket 端点。

注意:形态由 Task 1 的 V-5 探针实测确认;若实测形态带路径,改这里一处即可。

type Result

type Result struct {
	Result json.RawMessage
	Err    error
}

Result 是一次异步调用的终局(二选一)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL