# domainCheck Ops Agent Protocol ## 1. 目标 这份文档用于冻结 `Node Agent <-> Overseas Control Plane` 的正式 contract。 适用范围: - `POST /api/v1/ops/agent/tokens` - `POST /api/v1/ops/agent/bootstrap-plan` - `GET /api/v1/ops/nodes/{node_code}/handover` - `POST /api/v1/ops/nodes/{node_code}/handover/bootstrap-plan` - `POST /api/v1/ops/agent/register` - `POST /api/v1/ops/agent/heartbeat` - `POST /api/v1/ops/agent/pull` - `POST /api/v1/ops/agent/jobs/{job_id}/start` - `POST /api/v1/ops/agent/jobs/{job_id}/complete` - `POST /api/v1/ops/agent/jobs/{job_id}/events` 设计原则: - Agent 只执行声明过的结构化动作 - 控制面负责调度、门禁、编排和审计 - 所有回执都必须可重放、可去重、可死信化 --- ## 2. 公共响应包裹 所有接口统一返回: ```json { "code": 0, "message": "ok", "detail_code": "", "data": {} } ``` 约束: - `code=0` 表示成功 - `code=1` 表示业务失败 - `detail_code` 用于结构化错误语义 - `message` 只承担可读说明,不承担机器判断主逻辑 常见 `detail_code`: - `agent_token_invalid` - `agent_token_expired` - `agent_token_node_mismatch` - `ops_job_not_owned_by_agent` - `ops_job_invalid_status_transition` - `release_checksum_mismatch` - `release_health_check_failed` --- ## 3. 认证与请求头 Agent 相关接口统一使用: - `Content-Type: application/json` - `X-Domaincheck-Agent-Token: ` 其中: - `tokens` - `bootstrap-plan` 不要求 Agent Token。 其余接口都要求 Token。 --- ## 4. 公共节点载荷 `register` 与 `heartbeat` 当前共享 `_base_payload()`。 最小字段: ```json { "node_code": "mainland-worker-02", "region": "mainland", "role": "worker", "title": "mainland-worker-02", "hostname": "host-a", "ip": "10.0.0.12", "agent_version": "0.1.0", "capabilities": [ "service.start", "service.stop", "service.restart", "service.status", "health.snapshot", "logs.collect", "diagnostics.collect", "deploy.release" ], "labels": {}, "metadata": { "service_names": { "api": "domaincheck-api", "worker": "domaincheck-worker", "sync_agent": "domaincheck-sync-agent", "node_agent": "domaincheck-node-agent" }, "delivery_queue": { "state": "healthy", "label": "正常", "reason": "当前没有待重试回执,也没有死信记录。", "pending_count": 0, "dead_letter_count": 0, "last_flush_at": "", "oldest_pending_at": "", "oldest_pending_request_id": "", "oldest_pending_kind": "", "oldest_dead_letter_at": "", "oldest_dead_letter_request_id": "", "oldest_dead_letter_kind": "" } } } ``` 说明: - `service_names` 是控制面识别节点服务拓扑的最小入口 - `delivery_queue` 是控制面识别 Agent 回执现场的最小入口 --- ## 5. Bootstrap Contract ### 5.1 Issue Token `POST /api/v1/ops/agent/tokens` 请求体最小字段: ```json { "node_code": "mainland-worker-02", "issued_by": "web-ui", "expires_in_hours": 72, "metadata": { "source": "ops-center" } } ``` 返回体关键字段: - `token` - `token_preview` - `record_id` - `node_code` - `expires_at` - `created_at` ### 5.2 Bootstrap Plan `POST /api/v1/ops/agent/bootstrap-plan` 请求体最小字段: ```json { "node_code": "mainland-worker-02", "node_region": "mainland", "node_role": "worker", "issued_by": "web-ui", "expires_in_hours": 72, "control_plane_base_url": "https://ops.example.com", "root_dir": "/opt/domaincheck", "metadata": { "source": "ops-center" } } ``` 返回体关键字段: - Token 相关字段 - `control_plane_base_url` - `bootstrap_plan.node_code` - `bootstrap_plan.node_region` - `bootstrap_plan.node_role` - `bootstrap_plan.root_dir` - `bootstrap_plan.env_file` - `bootstrap_plan.service_name` - `bootstrap_plan.service_file` - `bootstrap_plan.install_script_path` - `bootstrap_plan.env_content` - `bootstrap_plan.command_lines` - `bootstrap_plan.command_block` - `bootstrap_plan.health_checks` - `bootstrap_plan.health_check_block` - `bootstrap_plan.bootstrap_script_name` - `bootstrap_plan.bootstrap_script_path` - `bootstrap_plan.bootstrap_script_content` - `bootstrap_plan.bootstrap_run_script_block` 约束: - `bootstrap_plan` 是标准接入方案对象 - 页面、Codex、CLI 必须复用这一个对象 - 不允许各端再次手写 env 或 shell 逻辑 --- ## 6. Runtime Contract ### 6.1 Register `POST /api/v1/ops/agent/register` 语义: - 校验 Token 与 `node_code` - Upsert Agent 运行态 - 将节点推入托管目录候选态 最小成功返回字段: - `node_code` - `expires_at` - `capabilities` ### 6.2 Heartbeat `POST /api/v1/ops/agent/heartbeat` 语义: - 刷新 `last_seen_at` - 刷新节点身份、服务名和队列快照 - 维持控制面中的 Agent 在线态 最小成功返回字段: - `node_code` - `server_time` - `expires_at` ### 6.3 Pull `POST /api/v1/ops/agent/pull?limit=1` 请求体最小字段: ```json { "node_code": "mainland-worker-02" } ``` 返回体关键字段: ```json { "jobs": [ { "id": 123, "job_code": "ops-xxx", "action": "health.snapshot", "target_node_code": "mainland-worker-02", "status": "dispatching", "execution_mode": "remote-agent", "payload": {}, "metadata": {}, "policy": {}, "steps": [] } ], "count": 1 } ``` 约束: - Agent 只能拿到属于自己的任务 - Agent 不负责选择任务 - 调度权始终在控制面 ### 6.3.1 Agent Job Envelope 为了避免 Agent 再从自然语言动作名里“猜执行上下文”,`pull.data.jobs[]` 当前已经按统一任务包裹返回。 当前 `pull.data` 顶层也会额外带: - `protocol_version` - `envelope_type` - `node_code` - `limit` - `count` 推荐最小结构: ```json { "protocol_version": "ops-agent/v1", "envelope_type": "agent_job", "id": 123, "job_id": 123, "job_code": "ops-20260418-001", "job_type": "ops_action", "action": "logs.collect", "target_node_code": "mainland-worker-02", "status": "dispatching", "execution_mode": "remote-agent", "step_key": "worker_logs", "step_title": "收集 Worker 日志", "job_ref": { "job_id": 123, "job_code": "ops-20260418-001", "action": "logs.collect", "target_node_code": "mainland-worker-02" }, "step_ref": { "step_id": 456, "step_key": "worker_logs", "step_title": "收集 Worker 日志" }, "payload": { "service_name": "domaincheck-worker", "lines": 120, "include_agent_logs": false }, "policy": { "timeout_seconds": 120, "stop_on_failure": true, "auto_approve": true }, "metadata": { "requested_by": "playbook:inspection.standard", "source": "ops-center" }, "release_context": { "release_id": 0, "release_version": "", "rollout_id": 0, "rollout_code": "" }, "focus_ref": { "kind": "ops_job", "job_id": 123, "job_code": "ops-20260418-001", "action": "logs.collect", "target_node_code": "mainland-worker-02" } } ``` 正式要求: - Agent 必须优先消费: - `action` - `payload` - `policy` - Agent 不应通过: - `step_title` - `summary` - `notes` 去反推真实执行参数 - 与发布相关的任务应通过: - `release_context` 传递 release / rollout 关联,而不是让 Agent 自己查询“当前版本” 这层的意义是: - `ops job` - 仍是正式执行颗粒度 - `Agent Job Envelope` - 是 Node Agent 拿到的稳定执行视图 这样同一套 Agent 才能同时承接: - 标准巡检 - 日志收集 - 诊断采样 - Release 部署 - Rollout 回滚 ### 6.4 Start `POST /api/v1/ops/agent/jobs/{job_id}/start` 请求体最小字段: ```json { "node_code": "mainland-worker-02" } ``` 语义: - `dispatching` 表示已派发 - `running` 表示节点已真实开工 ### 6.5 Complete `POST /api/v1/ops/agent/jobs/{job_id}/complete` 请求体最小字段: ```json { "node_code": "mainland-worker-02", "client_request_id": "complete-ops-123-1", "status": "success", "stdout": "", "stderr": "", "result": { "summary": "ok" }, "error_message": "" } ``` 允许状态: - `success` - `failed` - `partially_succeeded` 幂等约束: - 使用 `client_request_id` - 控制面写入 `ops_jobs.last_agent_complete_request_id` - 同一完成回执可安全重放,不应再次制造副作用 推荐补充字段: - `duration_ms` - `result.summary_text` - `result.focus_ref` 当前成功返回里也会额外带: - `completion_summary.job_id` - `completion_summary.job_code` - `completion_summary.status` - `completion_summary.status_label` - `completion_summary.result_summary_text` - `completion_summary.focus_ref` - `completion_summary.deduplicated` 这样控制面在不展开 stdout/stderr 的情况下,也能直接把: - 任务收口摘要 - 后续跳转落点 并入 activity / inspection / rollout 观察面。 ### 6.6 Events `POST /api/v1/ops/agent/jobs/{job_id}/events` 请求体最小字段: ```json { "node_code": "mainland-worker-02", "client_event_id": "event-ops-123-checksum-1", "step_id": 0, "event_type": "deploy_checksum_verified", "level": "info", "message": "checksum ok", "payload": { "checksum": "sha256:..." } } ``` 去重约束: - 使用 `client_event_id` - 控制面通过唯一索引去重 - 同一事件可安全重放,不应重复落库 推荐补充字段: - `occurred_at` - `summary_text` - `focus_ref` 当前成功返回里也会额外带: - `event` - 即标准化后的事件对象 - 内含 `event_key / summary_text / occurred_at / focus_ref` 这样事件流就不再只是“原始日志点”,而能直接成为: - playbook run events - rollout events - activity-stream 共同消费的现场片段。 --- ## 7. Agent 本地补发队列 当前主链路已经正式具备: - `pending` 队列 - `dead-letter` 队列 - 定时 `_flush_delivery_queue()` - 永久性错误转死信 当前重点覆盖: - `jobs/{id}/complete` - `jobs/{id}/events` 队列状态: - `healthy` - `retrying` - `dead_letter` 语义: - `healthy`:无积压,无死信 - `retrying`:存在待补发回执,Agent 会继续自动重放 - `dead_letter`:存在永久失败记录,需人工介入 控制面消费方式: - 节点维度: - `delivery_queue_state` - `delivery_queue_label` - `delivery_queue_reason` - `delivery_queue_pending_count` - `delivery_queue_dead_letter_count` - 汇总维度: - `queue_retrying_nodes` - `queue_dead_letter_nodes` - `queue_pending_records` - `queue_dead_letter_records` ### 7.1 当前控制面已可见的最小现场 当前至少已经可以通过: - `GET /api/v1/ops/nodes` 看到每台托管节点的队列现场: ```json { "node_code": "mainland-worker-02", "delivery_queue_state": "dead_letter", "delivery_queue_label": "死信 2", "delivery_queue_reason": "当前存在 2 条死信记录,建议优先查看节点日志或诊断编排。", "delivery_queue_pending_count": 0, "delivery_queue_dead_letter_count": 2, "delivery_queue_last_flush_at": "2026-04-18 06:10:00", "delivery_queue_oldest_pending_at": "", "delivery_queue_oldest_pending_request_id": "", "delivery_queue_oldest_pending_kind": "", "delivery_queue_oldest_dead_letter_at": "2026-04-18 05:59:00", "delivery_queue_oldest_dead_letter_request_id": "complete-ops-123-1", "delivery_queue_oldest_dead_letter_kind": "job_complete" } ``` 这已经足够让: - 页面显示“节点存在死信” - `overview / driver-feed / codex-brief` 产出驾驶建议 - CLI 快速判断现场是不是要先走日志或诊断 但这还不够支撑正式运维,因为它只能“看见死信”,还不能“处理死信”。 ### 7.2 Delivery Queue Record 正式对象 后续控制面不应再把死信理解成一个纯计数器,而要把每条待补发 / 死信记录升级成正式对象。 推荐最小结构: ```json { "record_id": "dq-mainland-worker-02-20260418-001", "node_code": "mainland-worker-02", "state": "dead_letter", "request_kind": "job_complete", "client_request_id": "complete-ops-123-1", "job_id": 123, "job_code": "ops-20260418-001", "target_api": "/api/v1/ops/agent/jobs/123/complete", "detail_code": "ops_job_invalid_status_transition", "error_message": "当前任务状态不允许 complete", "attempt_count": 6, "first_queued_at": "2026-04-18 05:58:00", "last_attempt_at": "2026-04-18 05:59:00", "next_retry_at": "", "payload_preview": { "status": "success" }, "response_preview": { "code": 1, "detail_code": "ops_job_invalid_status_transition" } } ``` 正式约束: - `record_id` - 是控制面侧唯一主键 - `client_request_id` - 是 Agent 幂等键 - `detail_code` - 是死信聚类主键,不能只靠 `message` - `payload_preview / response_preview` - 用于页面和 Codex 快速判断,不必默认展开完整原始报文 推荐状态: - `pending` - `retrying` - `dead_letter` - `replayed` - `discarded` ### 7.3 死信操作面正式入口 为了让“看见死信”之后不用回节点本地处理,推荐把操作面固定成下面这组接口。 当前已落地的第一阶段能力: - `GET /api/v1/ops/nodes/{node_code}/delivery-queue` - `GET /api/v1/ops/nodes/{node_code}/delivery-queue/records` 当前控制面返回的记录可见性为: - `record_visibility=head_only` 也就是: - 当前已经能稳定查看节点级队列总览 - 也能看到 `pending / dead_letter` 的头部记录摘要 - 但还没有把节点本地全量 `pending/*.json / dead-letter/*.json` 正式同步到控制面 所以这两条 GET 入口现在属于“正式只读观察面”,而不是完整操作面。 #### a. 单节点队列总览 - `GET /api/v1/ops/nodes/{node_code}/delivery-queue` 最小返回体: ```json { "node_code": "mainland-worker-02", "summary": { "state": "dead_letter", "pending_count": 0, "dead_letter_count": 2, "last_flush_at": "2026-04-18 06:10:00" }, "oldest_pending_record": {}, "oldest_dead_letter_record": {} } ``` #### b. 队列记录列表 - `GET /api/v1/ops/nodes/{node_code}/delivery-queue/records` 推荐筛选参数: - `state=pending|retrying|dead_letter|replayed|discarded` - `request_kind=job_complete|job_event` - `detail_code=...` - `group_by=detail_code|request_kind|target_api` - `limit=50` 当带 `group_by` 时,返回值应优先给出聚类摘要,而不是只返回平铺明细。 #### c. 单条死信重放 - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/records/{record_id}/replay` 当前已落地,但要明确当前阶段约束: - 控制面仍然是 `record_visibility=head_only` - 所以单条动作目前只允许针对“当前可见头部记录” - 真正的执行不是控制面直接改远端文件,而是创建正式 `ops job` - 由目标节点 `Node Agent` 执行 `delivery.queue.replay` 最小请求体: ```json { "requested_by": "web-ui", "reason": "确认任务状态已修复,重放这条 complete 回执" } ``` #### d. 批量重放 - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/replay` 当前已落地为正式操作面。 推荐请求体: ```json { "requested_by": "codex", "selector": { "state": "dead_letter", "detail_code": "ops_job_invalid_status_transition" }, "limit": 20 } ``` #### e. 丢弃死信 - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/records/{record_id}/discard` 当前已落地,但与单条重放一样,仍然只允许针对当前可见头部死信记录发起单条动作。 最小请求体: ```json { "discarded_by": "web-ui", "reason": "确认这条回执不再需要补发" } ``` #### f. 主动冲刷队列 - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/flush` 当前已落地为正式操作面,执行方式同样是: - 控制面创建 `ops job` - 目标节点 `Node Agent` 执行 `delivery.queue.flush` 语义: - 不是“强行成功” - 而是立即触发一次 Agent 补发冲刷 - 结果仍然回到 `pending / retrying / dead_letter / replayed` ### 7.4 为什么必须做成正式操作面 这层不是为了让页面多一个按钮,而是为了保证: - 页面可以处理死信 - CLI 可以处理死信 - 海外 Codex 驾驶员可以处理死信 三者都不需要绕回: - 手工 SSH 登录节点 - 手工删本地文件 - 手工重放某条 HTTP 请求 正式规则应该固定成: - “死信查看” 走控制面对象 - “死信重放 / 丢弃 / 冲刷” 走控制面对象 - Agent 只负责执行,不负责决定如何处理死信 --- ## 8. Job Status Enum 当前 Agent 主链路约束: - `queued` - `dispatching` - `running` - `success` - `failed` - `partially_succeeded` 语义: - `queued`:任务已创建,尚未派发 - `dispatching`:控制面已派发,但节点尚未确认开工 - `running`:节点已真实执行 - `success`:成功完成 - `failed`:失败完成 - `partially_succeeded`:部分成功,需关注 --- ## 9. 实现边界 Agent 只执行已声明动作: - `service.start` - `service.stop` - `service.restart` - `service.status` - `health.snapshot` - `logs.collect` - `diagnostics.collect` - `deploy.release` 不允许控制面长期依赖任意 shell 下发。 `shell executor` 只能作为受限兜底能力存在,并必须挂审批与审计。