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>
170 lines
18 KiB
Markdown
170 lines
18 KiB
Markdown
# 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)。
|