Files
Agentswarm/docs/integration/runtime-contract.md
T
gongzhiyong a117c02e3f docs(#56): 更正模型 key 库名 heicode-kv → heicode-vault(HM 实测口径)
agent_swarm#56 评论:模型 key 的真实 Key Vault 库名是 `heicode-vault`
(`https://heicode-vault.vault.azure.net`),早期契约文档误写为 `heicode-kv`;
且生产 `SECRET_RESOLVER` 须指向 `heicode-vault`。

- runtime-contract.md §3.3.1 A.3:库名更正 + 标注 SECRET_RESOLVER 指向 heicode-vault
  + 明确 Swarm 需提供 Pod 身份的 clientId+objectId 给 HM 授权(只读、限 swarm-model-key-*)。
- security-boundary.md:secret_ref 示例 host 同步更正。
- test-key-injection-contract.py:模型 key fixture host 同步更正(resolver 仅取末段名,
  功能不变;测试仍全绿)。

纯文档/fixture 更名,无事件 schema/契约字段改动。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:12:27 +08:00

170 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Manager ↔ Swarm Runtime Contract(Swarm 侧拥有)
> 状态:**FROZEN v1(契约冻结)** —— 回应 `agent_swarm#14`(来自 HM #28/#45 客户端「任务驾驶舱」查询面:stop 端点 + ID 映射 + 状态机)。对齐对象:Heicode Manager Runtime Team。
>
> 冻结结论(供 HM #45 接 stop 真实调用):
> - **stop 端点**:`POST /api/agent/swarm/deployments/{id}/stop`(§3),已实现,幂等键 `X-Idempotency-Key`。
> - **ID 映射**:`deployment_id ↔ swarm_id ↔ manager_deployment_id`(§3.2),`get_run_by_identifier` 支持任一 id 查询。
> - **状态机**:运行时真实状态见 §4;与客户端 #28 期望(含 `preparing/degraded/verifying`)的**映射表见 §4.1**(运行时不新增臆造状态,规则 #9)。
>
> 依据:
> - HM 侧裁定 `heicode-mananger/docs/integration/heicode-swarm-deferred.md`:**HM 当前不实现 swarm runtime**;多 Agent 蜂群归 `agent_swarm` / `HeiCode-Swarm`(本仓)。
> - 既有运行时集成范式 `heicode-mananger/docs/integration/heicode-am-contract.md`(单 Agent 模板 Agent,经 AM 启动)。本契约沿用其鉴权、回调、env、路径覆盖等约定。
> - 事件 envelope/类型见 [`event-schema.md`](./event-schema.md)(同批冻结,`agent_swarm#15`)。
## 1. 角色与边界
- **HM(Heicode Manager)**:控制面。负责用户输入、资源/权限/计费/审计、模型网关(`/v1/*`)。HM **不**承载蜂群编排。
- **Swarm Runtime(本仓)**:执行面。接收 HM 下发的编排请求,分解→派发→执行→评审→汇总,并通过**带签名回调**回报生命周期事件。
- HM 不在对话/执行回路;Swarm 通过回调把状态推回 HM,HM 也可主动拉取(见 §4)。
## 2. 鉴权(沿用 AM 约定)
- HM → Swarm:`Authorization: Bearer <service_token>`。Swarm 侧校验 `AGENT_RUNTIME_SERVICE_TOKEN`(兼容 `AGNET_RUNTIME_SERVICE_TOKEN`)。未配置时为**非安全开发模式**(仅本地)。
- 统一响应信封:成功 `{ "success": true, "data": {...} }`;失败 `{ "success": false, "error": { "code", "message", "request_id" } }`。
- 常用请求头:`X-Correlation-ID`、`X-Idempotency-Key`、`Authorization`。
## 3. 生命周期接口(已实现 / 待对齐)
Swarm 暴露以下接口(三组别名等价,便于 HM 路径覆盖):
`/api/swarms`、`/api/agent/swarm/deployments`、`/api/agnet/deployments`。
| 动作 | 方法 + 路径 | 状态 | 说明 |
|---|---|---|---|
| **create** | `POST /api/agent/swarm/deployments` | ✅ 已实现 | 创建蜂群部署;幂等键 `X-Idempotency-Key` |
| **status** | `GET …/{deployment_id}` | ✅ 已实现 | 返回部署概要与状态 |
| task graph | `GET …/{deployment_id}/tasks` | ✅ 已实现 | 任务 DAG |
| logs / events | `GET …/{deployment_id}/logs`、`/events` | ✅ 已实现 | 事件流(分页 `cursor`/`limit`) |
| metrics / workflow / diagnostics | `GET …/{deployment_id}/metrics`、`/workflow`、`/diagnostics` | ✅ 已实现 | 指标与编排视图 |
| **input(追加输入)** | `POST …/{deployment_id}/input` | ✅ 已实现 | 接收用户后续 prompt,注入为 `source=user_append` 任务(终态 run 自动 reopen 为 running;`stopped` 拒绝)。指令文本仅作任务描述下发给 agent,**不回显进事件流**(task.created 仅类别 message)。见 #40 |
| **result(结果)** | `GET …/{deployment_id}/result` | ✅ 已实现 | 返回用户面结果:`{summary, deliverable, artifacts[], termination_reason, status}`;产物内容在各 artifact 的 `uri`(git/runtime),非内联 |
| **cancel** | `POST …/{deployment_id}/stop` | ✅ 已实现 | 停止并取消非终态任务,向 Agent 下发 `cancel_task` |
| **approve** | `POST …/{deployment_id}/approvals/{approval_id}` | ✅ 已实现 | 接收 Manager 审批决定(approved/rejected) |
| **resume** | — | 🟡 待对齐 | 当前仅「审批通过」隐式恢复(approvals);无独立 resume 端点 |
| **retry** | — | 🟡 待对齐 | 任务级重试 / 评审重做为内部机制;无外部 retry 端点 |
### 3.1 create 请求(必填校验)
必填:`orchestration_plan.objective`、`callback.url`、`metadata.manager_deployment_id`。
约束:`mode` 必须为 `swarm`;`billing_context.secret_ref`(若有)必须 `azkv://` 前缀;`resource_grants`/`metadata`/`callback` 不得含明文密钥。
create 响应 `data`:`deployment_id`、`runtime_deployment_id`、`manager_deployment_id`、`swarm_id`、`mode`、`status`、`runtime_execution_status`、`created`。
### 3.2 ID 语义(回应 HM 标记的 `deployment_id ↔ swarm_id` 缺口)
- `deployment_id` / `runtime_deployment_id`:Swarm 侧运行时部署 ID(`runtime-dep-…`)。
- `swarm_id`:工作流 ID(`swarm-…`),作为 `workflow_id`;事件按 `swarm_id` 持久化与查询。
- `manager_deployment_id`:HM 侧部署 ID(请求 `metadata.manager_deployment_id` 透传)。
- `correlation_id`:贯穿一次交付的追踪 ID(`X-Correlation-ID`),作为 `trace_id`。
- 三者映射在 Swarm 持久化中维护:`deployment_id ↔ swarm_id ↔ manager_deployment_id`,`get_run_by_identifier` 支持三者任一查询。
> HM 侧 `heicode-swarm-deferred.md` 记录的「Swarm 仅暴露 `/tasks`、缺 `deployment_id↔swarm_id`」为旧状态;本仓 v5/v6 已实现上述映射与 `/api/agent/swarm/*` 接口,需 HM 复核更新该锚点。
### 3.3 蜂群专家 agent 拉起环境契约(agent launch env,回应 agent_swarm#16)
> **状态(团队决议)**:**Swarm 运行时负责拉起专家 agent 并执行每用户限额**(不再由 AM 拉起)。下表 env 字段 + 拉起方/限额口径**已定**。实现:`orchestrator/agent_launcher.py` + `main.launch_swarm_agents`,测试 `scripts/test-agent-launcher.py`。
**拓扑**:专家 agent 仍是**独立进程**,主动出站连编排器 WS(`/ws/{agent_id}`,见 §5/security-boundary §6)。**拉起方 = Swarm 运行时**:编排器在 create 播种后由 `agent_launcher` 按 provisioning 策略拉起一个能力多样的 agent 池。去中心化下**无「按 run 分解算出 agent 数/角色」这一步**(种子任务无 required caps、任意 agent 可认领,子任务按能力自路由),故 agent 池**按用户**拉起、为固定能力集。§1 的「派发」= 把任务指派给**已连入**的 agent;**拉起 agent 进程**是本节定义的 Swarm 新职责。
**拉起后端**(`AGENT_LAUNCH_BACKEND`,fail-soft——拉起失败不影响 create):
- `none`(默认):不自动拉起,agent 由外部供给(保留 CI/e2e 与「外部拉起」部署);
- `subprocess`:本地起 `python -m agent.main`(**仅 dev**——进程在编排器 pod 内,无隔离/限额);
- `command`:执行部署注入的模板 `AGENT_LAUNCH_CMD`(env 经进程环境传入,**密钥不上 argv**);
- **`kubernetes`(生产)**:**每 agent 一个 Pod**,带资源 requests/limits + 标签(`heicode-swarm-id`/`heicode-user-id`,供 GC/teardown)。模型 key 经**每-swarm k8s Secret**(`build_secret_manifest`,via stdin 应用)由 Pod `secretKeyRef` 引用——**绝不**内联进 PodSpec env(否则进 etcd/`kubectl get -o yaml`)。停止时按标签 `kubectl delete pod,secret`。**前置**:编排器镜像含 `kubectl` + 一个对 `AGENT_POD_NAMESPACE` 有 pod/secret RBAC 的 ServiceAccount;`ORCHESTRATOR_URL` = **集群内 Service DNS**(如 `ws://swarm-orchestrator.<ns>.svc.cluster.local:8000`);NetworkPolicy 放行出网到 HM `/v1` + git。**硬化替代**:用 azkv CSI SecretProviderClass 让 Pod 直接挂载密钥(编排器全程不碰明文),见 security-boundary §6。config:`AGENT_POD_IMAGE`/`AGENT_POD_NAMESPACE`/`AGENT_POD_SERVICE_ACCOUNT`/`AGENT_POD_CPU|MEM_REQUEST|LIMIT`。
**限额(Swarm 执行)**:池大小 `AGENT_LAUNCH_POOL_SIZE`(默认 3);**实际拉起数 = min(池大小, `MAX_AGENTS_PER_USER` − 该用户已连接数)**——Swarm 在**拉起时**限额,并在 **agent 注册时**兜底硬拒(security-boundary §6 / PR#32)。
Swarm 拉起每个专家 agent 时注入以下进程环境变量(`agent/main.py` 实读):
| env | 含义 | 来源 |
|---|---|---|
| `OPENAI_API_KEY` | 模型调用凭据 = 该用户 per-user `sk-`(NewAPI token),扣发起用户 `user.Quota` | **Swarm 从 `billing_context.secret_ref` 解析**(HM 现签写入 KV) |
| `OPENAI_API_BASE` | HM 模型网关 `/v1`(OpenAI 兼容) | 部署(`AGENT_OPENAI_API_BASE`/`OPENAI_API_BASE`) |
| `OPENAI_MODEL` | 所选模型 id | `billing_context.default_model_id`/计划 |
| `ORCHESTRATOR_URL` | 编排器 WS 基址;agent 据此回连、自选任务 | 部署(`ORCHESTRATOR_PUBLIC_URL`/`AGENT_RUNTIME_WS_URL`) |
| `AGENT_ID` | agent 实例 id(唯一) | **Swarm launcher** |
| `AGENT_CAPABILITIES` | 能力集合(逗号分隔) | **Swarm launcher**(`AGENT_LAUNCH_CAPABILITIES` 池策略) |
| `HEICODE_USER_ID` | 发起用户;用于每用户并发上限 `MAX_AGENTS_PER_USER`(注册时强制) | **Swarm 从 create `x-user-id` 透传** |
| `WORKSPACE_DIR` / `GIT_REPO_URL` | 可选:工作区 / 代码仓 | 资源授权 |
**约束**:
- 模型 key 由 **Swarm 服务端从 `billing_context.secret_ref`(`azkv://`)解析**(`resolve_model_key`:override → azkv(部署 SecretResolver / dev `HEICODE_SECRET_<name>`)→ 编排器 `OPENAI_API_KEY` 兜底)后注入被拉起 agent 的 env。key **不入** create 请求体 / 回调 / 日志 / argv(满足 §3.1);解析不到则 agent keyless 启动并明确报错(不伪造)。`billing_context` 的 `newapi_user_ref`/`quota_ref` 仍仅为归因元数据。见 [usage-billing-schema.md §2](./usage-billing-schema.md)。
- 同一用户的专家 agent 注入**同一把** `sk-` + `OPENAI_API_BASE=HM/v1`,计费归一到发起用户;`task_executor` 随模型请求带 `X-Agent-*` 归因头供 HM/NewAPI 关联。token 吊销走 §3.3.1 A.5 的 `swarm.pool_terminated` 握手(per-user,全部 run stop 后吊销)。
- **运行时事件前置**:只有 (i) HM 真把 create 派发到编排器(`SWARM_RUNTIME_ENABLED=true`,非 manager-local 适配器)且 (ii) Swarm 已按上表拉起 agent 并连入,编排器才回推 `task.*`/`swarm.*` 运行时事件(否则 events feed 仅有 HM 控制面 `deployment.status_changed`,见 #39)。
#### 3.3.1 模型 key 注入对接参数(HM #60,Swarm 侧已定死)
HM 实现 #60(计费 key 注入)前需 Swarm 定死的参数,逐条口径如下(实现:`agent_launcher.resolve_model_key` / `_resolve_secret_ref`,`swarm_runtime` 吊销握手;测试 `scripts/test-key-injection-contract.py`):
| # | 参数 | Swarm 口径(已定) |
|---|---|---|
| A.1 | `sk-` 粒度 + mint 时机 | **每用户一把**(首次蜂群开通 mint、跨该用户所有 run 复用);同一用户的专家 agent 注入同一把 `sk-`、计费归一到发起用户。 |
| A.1 | KV secret 命名约定 | `swarm-model-key-<user_id>`(Swarm 只取 `secret_ref` 末段做名,不强约束路径;命名供 HM 定位)。 |
| A.2 | KV secret **value 格式** | **JSON `{"openai_api_key":"sk-..."}`**(对齐 callback 签名密钥的 `{"callback_signing_secret":"..."}` 约定,可扩展)。Swarm 解析字段名 = `openai_api_key`;裸 `sk-` 字符串亦兼容(`_extract_model_key`);解析不到/字段缺失 → 不伪造、agent keyless 明确报错。 |
| A.3 | Swarm 读 KV 身份 / RBAC | **⚠ 联调阻塞前置(分两步)**:① **Swarm 侧先 provision Pod 的 Workload Identity**(建 UAMI → federate 到 `orchestrator-sa` SA → 用 `client-id` 注解 SA)——当前**尚未建**(仓内无 `azure.workload.identity/client-id` 注解、无 IaC);② **HM/运维** 在其库 **`heicode-vault`**(`https://heicode-vault.vault.azure.net`)给该身份授 `Key Vault Secrets User`(只读、限 `swarm-model-key-*`)。`heicode-vault` 是 HM 侧库,故授权由 HM/运维做;Swarm 只提供 Pod 身份 id(`clientId` + `objectId`)+ 收窄作用域。不通则 `secret_ref` 解不出。生产 KV 适配器(`SECRET_RESOLVER`)在仓外经部署接线,**须指向 `heicode-vault`**;dev/CI 用 `HEICODE_SECRET_<name>` 环境映射。(库名更正自 agent_swarm#56:实库为 `heicode-vault`,非早期文档的 `heicode-kv`。) |
| A.4 | `OPENAI_API_BASE` | **Swarm 部署常量**(`AGENT_OPENAI_API_BASE`/`OPENAI_API_BASE`,设为 HM 网关 `https://code.xinghanlab.com/v1`),**不**经 create 的 `billing_context` 下发。 |
| A.5 | 吊销信号 | **事件驱动(方案 A)**:`sk-` per-user 长存;`stop` 是唯一**终态**(`completed`/`failed` 可经 `POST …/input` 重开,故仍保留 key)。当某用户**所有 run 均被 stop**(retained 集清空)时,运行时发**恰好一次** `swarm.pool_terminated{user_id, secret_ref}`,HM 收到即吊销 `sk-` + 清 KV。单 run 的 `swarm.stopped` **不**触发吊销(key per-user 复用);账户停用 / HM 主动 delete 走同一 stop 路径。 |
> `swarm.pool_terminated` 是 **HM 控制面生命周期事件**(非客户端 cockpit 事件,**不**入 `FROZEN_CLIENT_EVENT_TYPES`);HM 需在 `subscribed_events` + `agent_callback` 登记以收吊销信号。payload `secret_ref` 为 `azkv://` 引用(非明文 key)。
## 4. 状态机
部署状态:`waiting_approval` → `running` →(`blocked` ⇄ `running`)→ 终态 `completed` / `failed` / `stopped`。
| 状态 | 含义 | 进入方式 |
|---|---|---|
| `waiting_approval` | 高危/需审批,等待 Manager 审批 | create 时命中审批条件 |
| `running` | 任务派发与执行中 | 审批通过 / 有在途任务 |
| `blocked` | 任务因移交/依赖阻塞 | 子任务 `blocked_on_handoff` |
| `completed` | 所有任务终态且评审通过 | 全部完成(评审循环可选) |
| `failed` | 存在失败且不可恢复 | 任务终态含 failed |
| `stopped` | Manager 主动停止 | `…/stop` |
每次状态变更通过回调 `deployment.status_changed` 推送(见 §5);**终态另发** `swarm.completed`/`swarm.failed`/`swarm.stopped`(客户端驾驶舱据此切终态横幅,见 event-schema §4)。
### 4.1 与客户端期望状态的映射(#14.3 决议:映射,不新增运行时状态)
客户端 #28 期望 `created/preparing/running/waiting_approval/degraded/verifying/completed/failed/stopped`。本运行时**不臆造** `preparing/degraded/verifying` 等中间态(规则 #9:无真实信号不造态),由 HM/客户端按下表映射现有真实状态:
| 客户端期望态 | 运行时真实来源 | 映射口径 |
|---|---|---|
| `created` | create 成功、尚无在途任务 | 初始 `running` 前的瞬态;HM 可在落 `runtime_swarm_id` 后、首个 `task.*` 前显示 |
| `preparing` | 同上(播种中) | 映射到 `running`(种子已注入、Agent 尚未自选);无独立状态 |
| `running` | `running` | 直通 |
| `waiting_approval` | `waiting_approval` | 直通 |
| `degraded` | `blocked` | `blocked`(移交/依赖/审批驳回阻塞)映射为 `degraded`;P-guard `run.metadata["health"]` 不健康亦可佐证 |
| `verifying` | `running` + 交叉评审进行中 | 评审期仍是 `running`;如需细分,由 `timeline.updated`(`Review cycle N`) / `run.metadata["cross_review"]` 提示 |
| `completed`/`failed`/`stopped` | 同名终态 | 直通;并有 `swarm.{completed,failed,stopped}` 终态事件 |
> 即:运行时真实状态集 = `waiting_approval/running/blocked/completed/failed/stopped`(§4 表);`created/preparing/degraded/verifying` 是**展示层别名**,不改变运行时语义,也不进入回调 `status` 字段。如客户端坚持要真实细分态,需另立工单评估状态机扩展(非本次冻结范围)。
## 5. 回调(已与 HM 处理器对齐)
Swarm → HM:`POST {callback.url}`(create 时下发,默认 HM 的 `/api/agent/callbacks/runtime-events`)。
**鉴权与签名(与 HM `agent_callback.go` 一致,已实现):**
- 头:`X-Agent-Service-Token`(或 `Authorization: Bearer`)、`X-Agent-Timestamp`(Unix 毫秒)、`X-Agent-Signature`、`X-Agent-Event-Id`、`X-Correlation-ID`(均含 `X-Agnet-` 兼容别名)。
- 签名:`signature = "sha256=" + hex(HMAC_SHA256(secret, f"{timestamp}.{event_id}.{raw_body}"))`。
- secret:`AGENT_CALLBACK_SIGNING_SECRET`(兼容 `AGNET_…`);HM 侧容差默认 300s。
- 幂等:HM 按 `X-Agent-Event-Id` → body `event_id` → `idempotency_key` 去重。
事件 envelope 字段与 event_type 取值见 [`event-schema.md`](./event-schema.md)(与 HM 注册表一致)。
## 6. 路径与超时覆盖(沿用 AM 约定)
HM 侧可用 env 覆盖:`AGENT_RUNTIME_BASE_URL`、`AGENT_RUNTIME_SERVICE_TOKEN`、`AGENT_RUNTIME_AGENT_START_PATH`、`AGENT_RUNTIME_AGENT_PATH`、`AGENT_RUNTIME_AGENT_STOP_PATH`、`AGENT_RUNTIME_START_TIMEOUT_SECONDS`、`AGENT_RUNTIME_CALLBACK_URL`。
## 7. 安全
- `env`/请求体不得含明文密钥;凭据经 `secret_ref`(`azkv://`)注入。详见 [`security-boundary.md`](./security-boundary.md)。
- Swarm 启动接口必须 HTTPS / 私网;回调走 HTTPS。
## 8. 冻结状态与剩余对齐项
本次冻结(agent_swarm#14)已定稿:stop 端点(§3)、ID 映射(§3.2)、状态机 + 客户端映射表(§4/§4.1)、终态 swarm.* 事件。HM #45 可据此接 stop 真实调用。
剩余非阻塞项(不影响 #45 Phase1 接入):
- [ ] resume / retry 是否需要独立外部端点,还是沿用 approvals + 内部重做(当前:无独立端点)。
- [ ] HM 是否以 AM 同款 `/agents` 生命周期(而非 `/api/agent/swarm/*`)调用 Swarm;若是,需路径映射。
- [ ] HM 注册表(`agent_callback.go`)登记本次新增的 `swarm.completed/failed/stopped`、`approval.approved/rejected`、`handoff.created`(见 event-schema §4 ⭐)。
- [ ] 更新 HM 侧 `heicode-swarm-deferred.md` 锚点,登记本契约。
- [ ] #46 Phase2(SSE 长连 + 追加输入写语义):待本契约 + event-schema 冻结后由 HM 把轮询升级 SSE(见 frontend-event-api.md §4)。