Codex On-Call
Codex On-Call 流水线把结构化告警——来自网关自身的告警引擎,或任何遵循下文事件契约的其他生产者——变成只读的 Codex 调查。一个常驻的小型中继服务(relay)接收告警,立即把原始消息发到 Slack,然后把分析工作交给两个后端之一(CODEX_ONCALL_DISPATCH_BACKEND):
github——一个 GitHub Actions workflow 检出当前的dev分支,针对 relay 所配置的 Responses API 端点(即网关)运行codex exec,并自己在同一条 Slack thread 里回复。cloud-agent——relay 在某个 cloud agent 控制面上创建一个 job(该控制面可以是任何实现了apps/backend/serving/oncall/agent_backend.py中/v1/agent/service/oncall/*契约的服务);该平台自己的 runner 主机在沙箱里执行 Codex,并带一份按次签发的推理 grant,而 relay 轮询这个 job、校验结果,并自己把它发进 thread。不消耗 GitHub 托管的分钟数——Actions 故障不会连带把分析链路一起拖下水——Actions secret 里没有长期有效的模型密钥,花费记在api_logs.agent_job_id上,检出则钉在创建 job 时CODEX_ONCALL_AGENT_BASE_REF这个 ref 所解析到的那个 commit 上(这个 ref 是分支名,默认为dev,不是 sha)。
off-host alert producer ──── restricted HTTPS ─┐
│
Docker Compose network ▼
gateway backend ───────────────────────► codex-oncall relay ──► Slack alert
│ ▲
backend = github │ backend = cloud-agent
┌───────────────────────────┴─────────────────┐ │
│ repository_dispatch │ │ thread reply
▼ ▼ │ (relay posts)
GitHub Actions: codex-oncall ──► thread reply cloud agent control plane
checkout dev → codex exec job create + poll
│ Responses API │ claim + grant
▼ ▼
https://your-gateway.example/v1 runner host: codex sandbox
│ │ Responses API
▼ ▼
<model-id> (CODEX_ONCALL_CODEX_MODEL) gateway /v1/responses
github 后端上的职责划分:
Relay(Docker,常驻):认证生产者,立即发出原始告警,在 SQLite 里对事件去重,为每次告警触发排入一次持久化的交接,并在 GitHub 不可达时大声回退。它不运行 Codex,不持有模型凭据,也不包含任何仓库快照。
Workflow(GitHub Actions,每次告警一轮):临时 runner 检出实时的
dev分支(不存在镜像陈旧问题),安装钉死版本的 Codex CLI,跑只读调查,并把结构化分析(或它自己的失败通知)发进原始的 Slack thread。runner 虚拟机在每次运行后销毁。
分析在构造上就是只读的:workflow job 以 permissions: contents: read 运行,所以分析本身做的任何事都无法创建 issue、分支、pull request 或合并。relay 自己那把 GitHub token 的说法要弱一些。它的用途限定在发送 repository_dispatch,但 GitHub 是通过 Contents: read & write 来授予这项能力的(见下文「配置 GitHub」第 3 步),而带 Contents:write 的 token 同样可以推 commit。请把它当作这个仓库的写凭据看待,并把 PAT 的范围限制在本仓库之内。结构化结果里带有 issue_recommendation 和 draft_pr_recommendation 两个字段(apps/backend/serving/oncall/models.py),交由人来处理。让流水线拥有写操作意味着另一套凭据和明确的策略闸门,这是刻意不做进本功能的。
运行时边界
告警投递从不等待 Codex。Slack 接受原始告警、持久化的 SQLite job 入队之后,relay 就返回
202。生产者保留其原有的 incoming webhook 作为回退。因此 relay 超时或返回非 2xx 不会让这次呼叫丢失。
分发载荷只包含经过脱敏和长度限制的告警(密钥已抹除、
slack_text已剥离),外加 Slack thread 坐标和模型配置。载荷中的值在 workflow 里一律按不可信数据对待:它们通过环境变量间接落盘,绝不会被插值进 shell 脚本。workflow job 以只读的
GITHUB_TOKEN(permissions: contents: read)运行,secret 按步骤划分作用域:网关 API key 只对 Codex 那一步可见,Slack bot token 只对发消息的步骤可见。codex exec运行时禁用了用户配置、rules、hooks、apps、subagents 和联网搜索,shell 环境被限制为PATH/HOME/LANG/LC_ALL。一次沙箱自检会在 runner 支持时选择--sandbox read-only,否则回退——并附带一条可见的 workflow 警告——改以临时虚拟机作为隔离边界。同一个事件指纹同一时间只跑一次 workflow(Actions 并发组);重复的会排队,而不是互相竞争。
relay 只重试交接本身。分发成功之后,结果就归 workflow 负责:分析失败时它会自己发出失败通知(带上 run URL);如果分发本身最终失败,则由 relay 发出失败通知。
指纹用于对传输层的重试去重。恢复事件会关闭当前事件,并在其原始 Slack thread 里回复。
配置 GitHub
确认
.github/workflows/codex-oncall.yml存在于默认分支上,这样repository_dispatch才能触发它。在仓库里创建两个 Actions secret:
密钥
用途
CODEX_ONCALL_MODEL_API_KEYworkflow 用来访问网关 Responses API 的 HybridInference
hyi-...key。它必须能看到CODEX_ONCALL_CODEX_MODEL;如果该 deployment 对这个模型做了角色门控(apps/backend/serving/config/model_visibility.py),就要用一个能看到它的角色来签发这把 key。这不是上游 provider 的 key——Codex 无法直接调用 provider(见下文)。CODEX_ONCALL_SLACK_BOT_TOKENrelay 使用的同一把 Slack bot token(需要
chat:write,并且已被邀请进频道)。为 relay 创建一个细粒度 PAT,只对本仓库授予 Contents: read & write——这正是
repository_dispatch所需的权限。它写进下文的.env.oncall,不要放进 Actions secret。
配置 relay
创建 relay 专用的环境文件。不要把这些密钥放进共享的 backend .env,因为 backend 容器会加载整个文件。
cp .env.oncall.example .env.oncall
chmod 0600 .env.oncall
openssl rand -hex 32
填写 .env.oncall:
CODEX_ONCALL_RELAY_TOKEN=<random shared bearer token>
CODEX_ONCALL_SLACK_BOT_TOKEN=xoxb-...
CODEX_ONCALL_SLACK_CHANNEL_ID=C0123456789
CODEX_ONCALL_GITHUB_TOKEN=github_pat_...
CODEX_ONCALL_GITHUB_REPOSITORY=<owner>/<repo>
CODEX_ONCALL_CODEX_MODEL=llama-3.3-70b
CODEX_ONCALL_MODEL_BASE_URL=https://your-gateway.example/v1
CODEX_ONCALL_CODEX_MODEL 和 CODEX_ONCALL_MODEL_BASE_URL 会随每次分发载荷一起转发,所以模型策略集中在一处控制——换模型只需改 .env.oncall 再重启 relay,不用改代码或 workflow(API key 是唯一的例外:它放在 CODEX_ONCALL_MODEL_API_KEY 这个 Actions secret 里)。base URL 必须能从 GitHub 托管的 runner 访问到——要用公网网关地址,不能用 Compose 内部主机名。
为什么 base URL 必须指向网关:Codex 只讲 OpenAI Responses API——上游已经移除了 chat 协议支持(openai/codex#7782)——而大多数 OpenAI 兼容的 provider 端点只提供 Chat Completions,没有 /v1/responses。正是网关的北向 Responses 路由(apps/backend/serving/servers/routers/responses.py)才让这些模型对 Codex 可达;把 workflow 直接指向某个 provider 会在加载配置时失败。如果要通过某个特定 provider 来支付分析所耗的 token,就把该 provider 接进网关加载的模型注册表,而 workflow 仍然指向网关。这个注册表就是 MODELS_CONFIG_PATH 所指的文件;没有它则取生效中的 distribution manifest(DISTRIBUTION_CONFIG_PATH 配合 DISTRIBUTION_CONFIG_MODE=active)里的 paths.models 条目;再没有则用随仓库发布的参考注册表 config/examples/models.openrouter.yaml。
模型选择:网关提供的、能扛住一轮 agentic 循环的任何模型都可以。有两条实际约束:模型必须经得起多轮工具调用(一次分析会驱动 codex exec 走很多步 shell),并且每次运行会发出数万个 prompt token,所以单 token 价格在这里比延迟更重要。.env.oncall.example 里写的是 llama-3.3-70b,也就是参考注册表登记的那个模型;有自己模型目录的 deployment 则填自己的。
Slack app 需要 chat:write,并且必须被加进目标频道。relay 使用 chat.postMessage,这样 workflow 才能在原始告警 thread 里回复。
后端:cloud agent
cloud-agent 后端把上面整个 GitHub 那一半都替换掉了——不需要 Actions secret,不需要 repository_dispatch 用的 PAT,也不需要 workflow。取而代之的是:
在网关上创建(或复用)一个用于 on-call 分析的服务账号。它的角色决定 job 的 grant 能带哪些模型,所以它必须能看到
CODEX_ONCALL_CODEX_MODEL;它的每日配额就是这些分析能花的额度。在 cloud agent 控制面上,按该平台自己的文档来配置。relay 期望它接受一个 on-call 分发 token、把 job 归属到上述服务账号的网关用户 id,并允许分析所要检出的那个仓库。该平台持有的 GitHub App 必须安装在那个仓库上——runner 用只读的 installation token 克隆。
在
.env.oncall中:CODEX_ONCALL_DISPATCH_BACKEND=cloud-agent CODEX_ONCALL_AGENT_BASE_URL=https://your-agent-control-plane.example CODEX_ONCALL_AGENT_DISPATCH_TOKEN=<same token as the control plane> CODEX_ONCALL_AGENT_BASE_REF=dev CODEX_ONCALL_AGENT_CONSOLE_URL=https://your-agent-console.example/agents/jobs/{job_id}CODEX_ONCALL_CODEX_MODEL的含义保持不变;模型必须能被该服务账号解析到。CODEX_ONCALL_GITHUB_*和CODEX_ONCALL_MODEL_BASE_URL在这个后端上不使用——平台会把自己的网关地址注入沙箱。CODEX_ONCALL_AGENT_CONSOLE_URL只是一个链接模板:relay 会替换其中的{job_id}再把结果发到 Slack,所以路径必须与 agent 控制台的 job 页面一致。不设置它,Slack 里就只带一个裸的 job id。
运行时行为:relay 的 worker 会把每个已分发的分析停在 await_result 阶段(SQLite,重启安全),每 CODEX_ONCALL_AGENT_POLL_SECONDS 轮询一次平台,并取消存活超过 CODEX_ONCALL_AGENT_TIMEOUT_SECONDS 的 job。只有当归一化后的事件日志里至少有一次成功的命令执行、并且最后一条消息恰好解析成一份符合 schema 的分析时,结果才会被发出——这与 workflow 发消息那一步强制执行的落地性规则相同。同一事件指纹同一时间只有一份分析在跑;期间的重复触发只更新事件,不会再排入新的 job。
回滚只需改一处:设置 CODEX_ONCALL_DISPATCH_BACKEND=github(GitHub 那几个值仍然保留),然后重启 relay。此刻停在 await_result 的 job 会失败关闭,并在各自的 thread 里留下通知——GitHub 后端无法轮询它们。
启用 Compose profile
在共享的 backend .env 里设置生产者相关的值。relay token 必须与 .env.oncall 一致。
COMPOSE_PROFILES=oncall
CODEX_ONCALL_RELAY_URL=http://codex-oncall:8091
CODEX_ONCALL_RELAY_TOKEN=<same shared token>
SLACK_ALERTS_WEBHOOK_URL=<existing fallback webhook>
启用网关基于规则的告警引擎时,还要设置 ALERTS_ENABLED=true。只要 URL 和 token 都在,网关现有的其他告警生产者会自动改用 relay。
构建并启动现有的 Compose 栈——make build 就是 docker compose up -d --build,两件事一起做:
make build
curl -fsS http://127.0.0.1:8091/healthz
健康检查响应中必须包含 "ready":true。relay 镜像里只有 FastAPI 服务——Codex CLI 的版本钉死在 .github/workflows/codex-oncall.yml 里(CODEX_CLI_VERSION),而每次 workflow 运行分析的都是一份新检出的 dev,所以没有需要保持同步的镜像快照。
Compose 网络之外的生产者
网关后端通过 Compose 网络访问 relay,所以对它来说 http://codex-oncall:8091 就够了。跑在别处的生产者——外部监控、另一台主机上的定时任务——解析不了这个名字,而且 Compose 服务只把端口 8091 发布在宿主机的 loopback 上(deploy/docker/docker-compose.yml)。要在那个 loopback 端口前面放一条受限的 TLS 路由,把公网 URL 连同同一个 CODEX_ONCALL_RELAY_TOKEN 交给外部生产者;relay 会用常数时间比较对每个 POST /v1/alerts 做认证,不通过则回 401。
在配置 relay URL 的同时,保留生产者原有的 Slack webhook。relay 只是一个尽力而为的加速器:relay 不可达时,仍然靠 webhook 把人呼起来。
冒烟测试
在 Compose 宿主机上,用 .env.oncall 里的 token 发一个合成事件:
curl -i http://127.0.0.1:8091/v1/alerts \
-H "Authorization: Bearer $CODEX_ONCALL_RELAY_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"version":"1",
"alert_id":"smoke-1",
"fingerprint":"smoke:staging:provider",
"source":"manual-smoke-test",
"status":"firing",
"severity":"warn",
"title":"Synthetic provider warning",
"environment":"staging",
"occurred_at":"2026-07-10T00:00:00Z",
"summary":"Synthetic event; no production impact",
"context":{"provider":"example"},
"slack_text":"Synthetic Codex on-call smoke test"
}'
请求成功会返回 202,body 为 {"accepted": ..., "duplicate": ..., "fingerprint": ..., "slack_thread_ts": ...},并立即把这条合成告警发到 Slack。在 github 后端上,它接着会在仓库的 Actions 标签页下启动一次 Codex On-Call 运行;在 cloud-agent 后端上,它会在控制面上创建一个 job。两种情况下,运行结束后分析回复都会落进该告警的 Slack thread。
在去重窗口内(载荷中的 dedupe_window_seconds,默认 300)重复使用同一个指纹,会返回 duplicate: true,且不再发一条新的顶层消息。第一次在 github 后端上真跑时,检查 workflow 日志里的沙箱自检结果——它会说明 Codex 是拿到了自己的只读沙箱,还是回退到了临时 runner 虚拟机。