# domainCheck Ops Job Contract ## 1. 目标 这份文档用于冻结 `ops job` 的正式 contract。 它是整套海外单脑控制面里最核心的执行对象。 正式定义: > `ops job` 是一条可审计、可编排、可派发、可回执、可取消的标准运维任务记录。 它不是: - 一条 shell 命令 - 一个页面按钮点击结果 - 一个仅供 agent 消费的内部对象 它应该同时服务于: - 页面按钮 - CLI - 海外 Codex 驾驶员 - Node Agent - Release / Rollout - Playbook Run 也就是说: - `playbook run` - 是编排聚合对象 - `rollout` - 是发布推进对象 - `ops job` - 是真正的执行颗粒度对象 --- ## 2. 入口接口 当前已落地的主入口: - `GET /api/v1/ops/jobs` - `GET /api/v1/ops/jobs/{job_id}` - `GET /api/v1/ops/jobs/{job_id}/events` - `POST /api/v1/ops/jobs` - `POST /api/v1/ops/jobs/batch` - `POST /api/v1/ops/policy/preview` - `POST /api/v1/ops/jobs/{job_id}/approve` - `POST /api/v1/ops/jobs/{job_id}/cancel` - `POST /api/v1/ops/jobs/{job_id}/dispatch` 配套对象来源: - `ops_jobs` - `ops_job_steps` - `ops_job_events` 正式约束: - 页面、CLI、Codex 不能各自定义第 2 套任务模型 - `playbook run` 只能聚合 `ops job` - `rollout` 只能批量生成 `ops job` - `Node Agent` 只能拉取和回执 `ops job` --- ## 3. 当前实现里的 Job Object 来源: - `GET /api/v1/ops/jobs` - `GET /api/v1/ops/jobs/{job_id}` - 创建 / 审批 / 取消 / 派发后的返回体中的 `job` 最小字段: ```json { "id": 101, "job_code": "ops-20260418064500-a1b2c3", "action": "logs.collect", "target_type": "node", "target_node_code": "mainland-worker-01", "target_node_codes": [ "mainland-worker-01" ], "status": "queued", "status_label": "排队中", "execution_mode": "remote-agent", "execution_mode_label": "Node Agent", "requested_by": "web-ui/ops-center", "payload": {}, "metadata": {}, "result": {}, "summary": "目标节点 mainland-worker-01 / 发起 web-ui/ops-center / 方式 Node Agent", "summary_text": "目标节点 mainland-worker-01 / 发起 web-ui/ops-center / 方式 Node Agent", "error_message": "", "created_at": "2026-04-18 06:45:00", "started_at": "", "finished_at": "", "updated_at": "2026-04-18 06:45:00", "risk_level": "medium", "approval_required": false, "approval_status": "approved", "approval_status_label": "已审批", "approved_by": "", "approved_at": "", "blocked_reason": "", "cancellation_reason": "", "dispatched_at": "", "target_selector": {}, "policy": {}, "rollout_id": 0, "steps_total": 0, "steps_running": 0, "steps_success": 0, "steps_failed": 0, "steps_terminal": 0, "steps_loaded": false, "focus_ref": { "kind": "ops_job", "job_id": 101, "job_code": "ops-20260418064500-a1b2c3", "action": "logs.collect", "target_node_code": "mainland-worker-01" }, "steps": [] } ``` 正式要求: - `job_code` - 是跨页面、CLI、事件流的稳定任务标识 - `action` - 是执行语义主键,不应用 `title` 或自然语言代替 - `target_type` - 定义任务作用范围,不应只靠是否传了 `target_node_code` 来猜 - `execution_mode` - 是正式执行方式,不是附属备注 - `policy` - 是创建时冻结的策略快照,不应由页面二次推导 - `status_label / approval_status_label` - 是给页面、CLI、Codex 直接消费的人类可读标签 - `summary_text` - 是任务摘要 contract,不要求调用方再用状态和节点自己拼一句中文 - `steps_total / steps_running / steps_success / steps_failed / steps_terminal` - 是任务步骤聚合统计,避免前端和 CLI 再自行遍历 steps 统计 - `focus_ref` - 是统一定位对象,后续页面、CLI、Codex 应优先消费它,而不是自己重组跳转参数 --- ## 4. Job 状态机 ### 4.1 当前实现中已使用的状态 当前后端已实际使用: - `queued` - `awaiting_approval` - `blocked` - `running` - `success` - `failed` - `cancelled` 这些状态已经是正式 contract 的最小闭环。 ### 4.2 推荐终局扩展状态 终局可继续扩展为: - `draft` - `approved` - `queued` - `dispatching` - `running` - `verifying` - `partially_succeeded` - `rollback_running` - `rolled_back` - `success` - `failed` - `cancelled` - `timed_out` 但要明确区分: - 当前实际 API 已稳定承诺的,是最小状态集 - 终局扩展状态不能破坏当前状态语义 --- ## 5. Execution Mode Contract 当前实现里正式出现的执行方式: - `remote-agent` - `local-runtime` - `control-plane` - `ssh` 语义必须固定: ### 5.1 `remote-agent` - 默认正式执行方式 - 由目标节点 Node Agent 拉取并执行 ### 5.2 `local-runtime` - 仅用于当前节点本机即时执行 - 主要用于本机 runtime 动作 ### 5.3 `control-plane` - 由海外控制面直接执行 - 适用于控制面内生动作 ### 5.4 `ssh` - 只作为救援和过渡执行器 - 不能成为大规模正式发布的默认方式 正式约束: - 发布默认优先 `remote-agent` - `ssh` 只应是兜底链路 - 页面、CLI、Codex 不应再自行创造第 5 种 execution mode --- ## 6. Target Type Contract 当前 contract 至少应允许: - `node` - `batch` - `nodes` - `rollout` 语义建议固定为: ### 6.1 `node` - 单节点任务 ### 6.2 `batch` / `nodes` - 多节点批量任务 - 当前通常通过 `/ops/jobs/batch` 创建 ### 6.3 `rollout` - 该任务属于某轮 release rollout 推进的一部分 正式要求: - 如果任务来自 rollout,必须能通过 `rollout_id` 回溯 - 如果任务来自 playbook,应通过 `metadata` 标出 run 归属 --- ## 7. Job Step Contract 来源: - `GET /api/v1/ops/jobs/{job_id}` 最小字段: ```json { "id": 201, "step_key": "dispatch", "title": "调度动作", "node_code": "mainland-worker-01", "status": "queued", "stdout": "", "stderr": "", "result": {}, "created_at": "2026-04-18 06:45:00", "started_at": "", "finished_at": "", "updated_at": "2026-04-18 06:45:00" } ``` 当前实现说明: - 现阶段每条 `ops job` 至少会先生成一个 `dispatch` step - 以后如果引入真正 DAG / 多步骤执行,也应继续沿用 `ops_job_steps` 正式约束: - `step_key` - 是步骤主键,不应用标题代替 - `status` - 必须与 job 状态联动,但不要求完全相同 - `stdout / stderr` - 是步骤级输出,不应与整条 job 的 `result` 混成一层 --- ## 8. Job Event Contract 来源: - `GET /api/v1/ops/jobs/{job_id}/events` 最小字段建议: ```json { "id": 301, "event_key": "job-event:301", "job_id": 101, "step_id": 201, "node_code": "mainland-worker-01", "client_event_id": "evt-001", "event_type": "job_dispatch_requested", "level": "info", "level_label": "信息", "message": "任务等待 node-agent 拉取: 101", "summary": "任务等待 node-agent 拉取: 101", "payload": {}, "has_payload": false, "occurred_at": "2026-04-18 06:45:03", "created_at": "2026-04-18 06:45:03" } ``` 当前代码里事件来源包括: - job created - job blocked - job awaiting approval - job approved - job cancelled - job dispatch requested - job executed locally / over ssh / on control plane - agent start / complete / custom events 正式要求: - `events` 是任务时间线,不是调试附属品 - 页面、CLI、Codex 都应从这里读“任务做到哪一步了” - 不应重新从 stdout/stderr 推断关键状态 补充约束: - `event_key` - 是事件行的稳定前端 / CLI 标识 - `summary` - 是给页面、CLI、Codex 直接消费的短摘要,不要求调用方再拼接 `message` - `level_label` - 是面向人看的等级标签 - `occurred_at` - 是统一观察面时间字段,允许与 `created_at` 同值 ### 8.1 Event List Summary Contract 当前 `GET /api/v1/ops/jobs/{job_id}/events` 建议同时返回: ```json { "summary": { "job_id": 101, "returned_total": 12, "level_counts": { "info": 8, "warning": 3, "error": 1 }, "event_type_counts": { "job_created": 1, "job_dispatch_requested": 1, "executor_received": 1 }, "node_counts": { "mainland-worker-01": 12 }, "latest_at": "2026-04-18 06:45:08", "filters": { "limit": 50 } } } ``` 正式要求: - 事件接口不只给一串数组 - 至少要给: - 当前返回了多少条 - 等级分布 - 事件类型分布 - 最近时间 - 这样页面、CLI、Codex 才能先判断“有没有异常事件”再决定是否展开明细 --- ## 9. List Contract 与 Compact 模式 来源: - `GET /api/v1/ops/jobs` 当前实现特点: - 默认 `list_ops_jobs(..., compact=True)` - 返回 compact job - `steps` 被置空 - `payload / metadata / result` 会被压缩 因此建议把列表 contract 明确成: ```json { "jobs": [ { "id": 101, "job_code": "ops-20260418064500-a1b2c3", "action": "logs.collect", "status": "queued", "execution_mode": "remote-agent", "requested_by": "web-ui/ops-center", "payload": { "tail_lines": 120 }, "metadata": {}, "result": {}, "result_summary": {}, "is_compact": true, "steps": [] } ] } ``` 正式要求: - 列表页默认只承诺 compact 视图 - 详情页再承诺 full job + steps - 页面不应假设列表结果天然带完整 steps --- ## 10. Create Contract 来源: - `POST /api/v1/ops/jobs` 最小请求体: ```json { "action": "logs.collect", "target_type": "node", "target_node_code": "mainland-worker-01", "execution_mode": "remote-agent", "requested_by": "web-ui/ops-center", "payload": { "tail_lines": 120 }, "metadata": { "source": "ops-center" }, "auto_approve": false, "run_now": true } ``` 当前实现还支持: - `target_selector` - `rollout_id` 最小返回体: ```json { "job": {}, "executed_immediately": false } ``` 关键语义: - `executed_immediately=true` - 说明任务已被控制面即时执行,而不是停留在队列 - `executed_immediately=false` - 说明任务进入了: - `queued` - `awaiting_approval` - `blocked` 正式要求: - 创建接口不能只回答“建成功了” - 必须明确回答: - 当前状态是什么 - 是否立即执行 - 如果没执行,是在等审批、等 agent、还是被阻断 --- ## 11. Batch Create Contract 来源: - `POST /api/v1/ops/jobs/batch` 最小请求体建议: ```json { "template_key": "diagnostics.collect", "target_node_codes": [ "mainland-worker-01", "mainland-worker-02" ], "execution_mode": "remote-agent", "requested_by": "web-ui/ops-center", "payload": {}, "metadata": {}, "auto_approve": true } ``` 最小返回体建议: ```json { "jobs": [], "results": [] } ``` 正式要求: - batch 只是创建方式,不是新的执行对象 - batch 最终仍要展开成多条 `ops job` - 单条失败不能让结果完全丢失,必须保留逐项结果 --- ## 12. Policy Preview Contract 来源: - `POST /api/v1/ops/policy/preview` - `ops_jobs.policy` 这层是创建前的正式门禁对象。 当前创建任务时,后端已先做: - 风险等级判断 - 阻断判断 - 审批要求判断 建议最小返回字段: ```json { "blocked": false, "blocking_reasons": [], "approval_required": true, "approval_reasons": [ "当前目标包含 control 节点" ], "warnings": [], "recommendations": [], "risk_level": "high", "target_summary": {}, "batch_plan": {}, "cluster_guardrails": {}, "operational_readiness": {} } ``` 正式要求: - 页面、CLI、Codex 都应复用同一份 policy preview - 不能各自再写一套“能不能执行”的判断逻辑 --- ## 13. Approval / Cancel / Dispatch Contract ### 13.1 Approve 来源: - `POST /api/v1/ops/jobs/{job_id}/approve` 最小请求体: ```json { "approved_by": "admin" } ``` 语义: - 仅 `awaiting_approval` 可审批 - 审批后回到 `queued` ### 13.2 Cancel 来源: - `POST /api/v1/ops/jobs/{job_id}/cancel` 最小请求体: ```json { "cancelled_by": "admin", "reason": "本轮灰度暂停" } ``` 语义: - `success / failed / cancelled` 不可取消 - 取消后 job 进入 `cancelled` - steps 同步进入 `cancelled` 或保持终态 ### 13.3 Dispatch 来源: - `POST /api/v1/ops/jobs/{job_id}/dispatch` 语义: - `queued` 才可派发 - `control-plane` - 立即由控制面执行 - `local-runtime` - 当前节点本机即时执行 - `ssh` - 由 SSH 执行器执行 - `remote-agent` - 保持 `queued`,等待对应 agent 拉取 这点必须明确: > dispatch 不等于一定会立刻变成 running。 对于 `remote-agent`,它可能只是: - 写入事件 - 保持排队 - 等节点稍后 pull --- ## 14. 与 Playbook / Rollout / Agent 的边界 ### 14.1 与 Playbook - `playbook run` - 负责展开、聚合、重跑、取消整轮编排 - `ops job` - 负责单条执行记录 ### 14.2 与 Rollout - `rollout` - 负责发布批次推进 - `ops job` - 负责每个节点每步的实际任务 ### 14.3 与 Node Agent - 控制面创建 `ops job` - agent 拉取 `ops job` - agent 回写 `ops_job_events` - agent 推进 job / step 状态 ### 14.4 与 Observability - `ops_observability_contract` - 看现场事实 - `ops_job_contract` - 看执行动作本身 这两层不能混: - 观察面告诉你“谁在跑、谁异常” - 任务面告诉你“这一条任务如何被创建、审批、派发、执行、取消” --- ## 15. 终局约束 后续不应再回退到下面几种模式: ### 1. 页面点按钮直接跑 shell 错误。 正确方式: - 页面创建 `ops job` ### 2. Codex 直接 SSH 改现场 错误。 正确方式: - Codex 创建 `ops job` / `playbook run` ### 3. 发布系统不经过任务层 错误。 正确方式: - rollout 批量生成 `ops job` ### 4. 死信治理不留痕 错误。 正确方式: - `flush / replay / discard` 都生成正式 `ops job` 所以可以把这份文档看成整套系统的“执行订单母本”。 后续只要这份 contract 稳住,页面、CLI、Codex、Agent、Release、Playbook 就都能围绕同一个中心对象协作。