# 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 `。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`。 > > **架构师裁定(2026-06-15,取代此前逃生门口径)**:**所有蜂群一律走去中心化主路——Swarm 永远播种单一目标任务(`build_seed_task_specs`),并按用户拉起 agent 池。** `orchestration_plan.agents`(HM 现会发 `[{role:general}]`)**不控制任务创建,也不控制 agent 拉起**:既不会让运行时跳过播种、改按 breakdown 建任务,也不会让运行时认为「caller 自行拉起 agent」而不拉池。此前「Manager 显式 agent breakdown 由 caller 拉起 / honored as-is」的逃生门(`_manager_provided_agents`)已**彻底退役删除**,本节其余口径(拉起后端、限额、模型 key 注入 §3.3.1)不变。这与 CLAUDE.md「去中心化蜂群是唯一行为(cutover 已完成)」一致。 **拓扑**:专家 agent 仍是**独立进程**,主动出站连编排器 WS(`/ws/{agent_id}`,见 §5/security-boundary §6)。**拉起方 = Swarm 运行时**:编排器在 create **无条件播种**单一目标任务后,由 `agent_launcher` 按 provisioning 策略拉起一个能力多样的 agent 池。去中心化下**无「按 run 分解算出 agent 数/角色」这一步**,且**无「按 caller 提供的 agent breakdown 拉起」这一步**(种子任务无 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..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` | 可选:工作区根 | 部署(`WORKSPACE_DIR`) | | `GIT_REPO_URL` | 代码仓克隆地址;agent 有此值才 clone | **Swarm 从 git `resource_grant.metadata.repo_url` 解析** | | `GIT_USERNAME` / `GIT_PASSWORD` | git HTTPS 凭据(注入 clone URL) | **Swarm 从 git `resource_grant.secret_ref`(`azkv://`)解析** | | `GIT_BASE_BRANCH` | 可选:基线分支(默认 `main`) | git grant `metadata.base_branch` | **约束**: - 模型 key 由 **Swarm 服务端从 `billing_context.secret_ref`(`azkv://`)解析**(`resolve_model_key`:override → azkv(部署 SecretResolver / dev `HEICODE_SECRET_`)→ 编排器 `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)。 - **git 仓库绑定**(agent_swarm#63 / HM #92)由 **Swarm 服务端从 `resource_grants` 里 `resource_type=git` 的 grant 解析**(`resolve_git_grant`):仓库地址 = `grant.metadata.repo_url`(非密文,inline 注入 `GIT_REPO_URL`);git 凭据 = `grant.secret_ref`(`azkv://`,同模型 key 路径解析 → 注入 `GIT_USERNAME`/`GIT_PASSWORD`)。git 凭据**不入** create 请求体 / 回调 / 日志 / argv;k8s 后端 `GIT_PASSWORD` 经 per-swarm Secret 的 `secretKeyRef` 注入(绝不内联 PodSpec)。grant 有 repo 无凭据时仍注入 `GIT_REPO_URL`(公有仓可 clone;私有仓 clone 报错,不伪造)。git KV secret 值约定为 JSON `{"git_username","git_password"}`(接受 `git_token`/`token` 形式 + 裸 token),**待 HM #92 对齐**。同一用户 agent 池注入同一 git grant。 - 同一用户的专家 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-`(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_` 环境映射。(库名更正自 agent_swarm#56:实库为 `heicode-vault`,非早期文档的 `heicode-kv`。) | | A.4 | `OPENAI_API_BASE` | **Swarm 部署常量**(`AGENT_OPENAI_API_BASE`/`OPENAI_API_BASE`,设为 HM 网关 **`https://code.heicode.cc/v1`**),**不**经 create 的 `billing_context` 下发。(域名更正自 agent_swarm#56:`code.heicode.cc` 为正式地址,`code.xinghanlab.com` 为前期过渡域名。) | | 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)。