# HM ↔ AM 接口契约(模板 Agent 模型)— 给 agent_management 团队 > 更新时间:2026-06-04 > 角色:**HM** = Heicode Manager(网关/控制面/模型网关)。**AM** = agent_management(云端 agent 运行时,你们)。 > 现状(2026-06-04 真实联调):**创建 agent 已打通**(HM→`POST /agents`→AM 返回 subdomain);**剩 3 条阻断在 AM 侧**(删除 500 bug / 缺 stop 端点 / agent 不就绪),见 §0.1。HM 调 AM 的代码全部隔离在 `heicode/controller/agent_template_runtime.go`,字段/路径可按本文对齐;路径都支持环境变量覆盖(见末尾)。 --- ## 0. 模型一句话 用户在 HM 网页台选「资源 + agent 模板」部署 → **HM 把 agent 定义(.md)+ 解好的资源 env 交给 AM 启动** → AM 起一个**常驻 agent**、分配**唯一子域名** 返回 → 桌面客户端从 HM 拿到地址后**直连 agent(A2A)对话**。HM 不在对话回路;agent 用模型走 HM `/v1/*`。 --- ## 0.1 联调结果(2026-06-04 二次真实复测,账号在生产实跑) > AM 已发新版 runtime 文档并部署。**之前三条阻断全部修复,端到端跑通**;复测中只剩 1 条新发现 + 1 条加固项(见末尾)。 | 接口 | 结果 | |---|---| | **创建** `POST /agents` | ✅ **通**:16~22s 返回 `runtime_id`(带横杠)+ `subdomain`,HM 现签的 `access_token` 已下发(非空 UUID),agent 记录建立 | | **取状态** `GET /agents/{agent_name}` | ✅ 通:数秒内从 `Pending` → `running` | | **停止** `POST /agents/{agent_name}/stop` | ✅ **修复**:返回 `status:stopped`(旧版 404 已解决) | | **删除** `DELETE /agents/{agent_name}` | ✅ **修复**:`runtime_cleanup:"ok"`(旧版 `UnboundLocalError` 500 已解决) | | **agent 直连(A2A)** | ✅ **可达**:`GET /health` 200 healthy、`/.well-known/agent.json` 拿到 agent card(旧版连不上已解决) | | **对令牌调用** `POST /message/send` (带正确 `X-Agent-Access-Token`) | ✅ 通:task `completed`、模型真实跑通 | | **模型调用** | ✅ HM 现签 `OPENAI_API_KEY` 打 `/v1`(gpt-5.4)正常 | | **令牌校验(① 本地比对)** | 🟡 **代码已写、生产镜像待更新**:复测时无令牌仍 200(应 401),根因为 AM 含校验逻辑的新镜像未发到生产;更新后即生效 | | **直连通道加密** | ⚠️ **HTTP 明文**:`access_info.domain_url` 为 `http://…`,`X-Agent-Access-Token` 与 `api_key` 明文传输 | > **AM 侧剩余待办(2 条,均不阻断功能,但阻断安全目标)**: > 1. 🟡 **令牌校验:代码已写、待生产镜像更新**。已查明:HM 确实在 `POST /agents` 的 `env` 里下发了 `AGENT_ACCESS_TOKEN`(= 返回客户端的 `access_token`,同一把);复测当时无令牌仍 200、`auth_required=None`,**根因是 AM 含 §5 校验逻辑的新镜像未发布到生产**(跑的是旧镜像)。AM 把新镜像更到生产后即生效。届时请复测:无令牌→401、错令牌→403、对令牌→放行。**在生效前 = 任一有效 `api_key` 可驱动任意用户的 agent、读走其挂载资源**。 > 2. ⚠️ **子域名上 HTTPS/TLS**。当前 `http://` 明文,令牌与 `api_key` 可被嗅探;通道加密是这套方案的前提(HM 不中转,靠 TLS)。 > > 其余(创建/状态/停止/删除/直连/模型)HM 已全部生产验证通过。 --- ## 1. AM 需要提供的 4 个接口 HM 用 `Authorization: Bearer ` 调 AM。统一响应信封建议 `{ "success": true, "data": {...} }`(HM 会从 `data` 取值;失败给 `{ "success": false, "message": "..." }`)。 ### 1.1 启动 agent(核心,已对齐你们的 `POST /agents`) `POST {AM}/agents` **请求体(HM 实际发送的,已按你们 CODING_A2A §2 对齐):** ```json { "name": "dep-b5fab27e9255", // 由 HM 部署 id 派生(DNS 安全) "template": "coding_a2a_agent", "framework": "A2A", "config": { "user_id": "22", "manager_deployment_id": "dep_b5fab27e9255", "callback_url": "https://code.xinghanlab.com/api/agent/callbacks/runtime-events" }, "env": { "AGENT_ROLE_NAME": "architect", "AGENT_INSTRUCTION_TEXT": "---\nname: architect\n---\n...", "OPENAI_BASE_URL": "https://code.xinghanlab.com/v1", "OPENAI_API_KEY": "sk-xxxx", "MODEL_NAME": "gpt-5.4", "AGENT_ACCESS_TOKEN": "550e8400-e29b-...", // 客户端↔agent 访问令牌(见 §3.1) "HEICODE_AGENT_ID": "dep_b5fab27e9255", // 回 HM verify 时带这个 "GIT_PROVIDER": "github", "GIT_REPO_URL": "...", "GIT_TOKEN": "…", "MYSQL_HOST": "...", "MYSQL_PASSWORD": "…" } } ``` **响应(HM 解析你们返回的字段):** - `runtime_id` ← `runtime_id`/`agent_id`/`id`/`name`/`namespace`(取到的实例标识,用于 status/stop/delete)。 - `subdomain` ← `subdomain` 或 `access_info.domain` / `access_info.external_ip`(agent 直连地址)。 - `access_token`:**HM 自己现签并下发**(不取你们返回值),即注入 env 的 `AGENT_ACCESS_TOKEN`,客户端用它做访问鉴权(见 §3.1)。 - `status` ← `status`/`runtime_status`(取不到默认 `running`)。 **AM 启动时做的(你们已实现):**用 `env.AGENT_INSTRUCTION_TEXT`+`AGENT_ROLE_NAME` 设角色;注入 env;agent 用模型走 `OPENAI_BASE_URL`(=HM `/v1`)。 > ⚠️ provisioning 同步:HM 超时默认 60s(`AGENT_RUNTIME_START_TIMEOUT_SECONDS`)。 > ⚠️ **下面 1.2–1.4 的路径是 HM 的推测**(你们文档只写了创建 + agent 自身 /health + A2A 调用,没写 agent-manager 的生命周期接口)。**实测:创建 `POST /agents` ✅ 通,但 stop/delete 用 `/api/agent/agents/{id}/...` 返回 404。** HM 已把默认改成与创建同命名空间的 `/agents/{id}`。**请 AM 确认真实的 状态/停止/删除 端点(方法 + 路径 + 用哪个 id),不一致我们改 env 覆盖即可。** ### 1.2 取 agent 状态 `GET {AM}/agents/{runtime_id}` → `data: { "status": "running" }`(HM 接受 `status`/`runtime_status`/`state`)。 ### 1.3 停止 agent `POST {AM}/agents/{runtime_id}/stop` → 2xx 即可。 ### 1.4 删除 agent `DELETE {AM}/agents/{runtime_id}` → 2xx 即可。 > `runtime_id` = HM 从创建响应取到的实例标识(优先 `runtime_id`/`agent_id`/`id`/`name`/`namespace`)。实测创建时 HM 发的 `name="dep-xxxx"`,若你们生命周期用别的 id,请在响应里明确返回 `runtime_id`。 --- ## 2. env 命名约定(HM↔AM 必须一致,agent 模板按此读) HM 把用户绑定的资源按**资源类型 + provider**解成固定 env 名(代码 `controller/agent_template_env.go`)。**AM 的 agent 模板按这些名字读 env**;用户在 HM 只填业务字段、看不到 env 名。 > **2026-06-04 已对齐 AM 的 `coding_a2a_agent`**:env 名按你们文档实现。当前**只支持 AM 已支持的 4 类**(git / mysql / postgres / azure blob);vm / redis / mongo / 对象桶等 HM 暂不下发,待 AM 支持再加。 | 资源类型 | provider | 注入的 env(非密 + 密钥) | |---|---|---| | git | github / gitea / gitlab | `GIT_PROVIDER` `GIT_REPO_URL` `GIT_DEFAULT_BRANCH` · `GIT_TOKEN`(密) | | database | mysql | `MYSQL_HOST` `MYSQL_PORT` `MYSQL_DATABASE` `MYSQL_USER` · `MYSQL_PASSWORD`(密) | | database | postgres | `POSTGRES_HOST` `POSTGRES_PORT` `POSTGRES_DATABASE` `POSTGRES_USER` · `POSTGRES_PASSWORD`(密) | | storage | azure blob | `AZURE_BLOB_ACCOUNT_NAME` `AZURE_BLOB_CONTAINER` · `AZURE_BLOB_ACCOUNT_KEY`(密) | 另外随启动注入(模型网关,3 个都传): - `OPENAI_BASE_URL` = HM `/v1`(`https://code.xinghanlab.com/v1`)。 - `MODEL_NAME` = **默认 `gpt-5.4`**(可用 env `AGENT_RUNTIME_DEFAULT_MODEL` 覆盖;模板 frontmatter 里的 `opus/sonnet` 只是角色提示,**不会**当作网关模型传)。 - `OPENAI_API_KEY` = **HM 为该用户现签的一把 new-api sk(`sk-…`)**,计费到该用户账上;**删 agent 时自动吊销**。已实测:用它打 `/v1/chat/completions`(gpt-5.4)能真实返回、计入用户用量。客户端在 A2A 请求里也可带自己的 `api_key`(你们文档 §7);请求级会覆盖启动级。 - 缺失的可选字段不会出现在 env 里(agent 自行容错)。 - 同类型资源 HM 限制只挂一个(避免 env 名冲突)。 - 角色/指令注入:HM 把模板 .md 放进 `env.AGENT_INSTRUCTION_TEXT`、模板 key 放进 `env.AGENT_ROLE_NAME`(对齐你们 §4)。 --- ## 3. 客户端 ↔ agent 直连(A2A,已对齐你们文档) 客户端从 HM 拿到 `subdomain` 后**直连 agent**,走你们的 **A2A 协议**(`POST {subdomain}/message/send` 同步、`POST {subdomain}/message/stream` 流式、`GET {subdomain}/.well-known/agent.json`、`GET {subdomain}/health`)。HM 不在回路。HM 已据此写好客户端文档 `heicode-desktop-client-api.md` §6。 ### 3.1 客户端↔agent 访问鉴权(HM 已提供;接入方式 = ① 本地校验) > 解决「agent 公网可达、谁有有效 sk 都能驱动别人的 agent、读走其挂载的 git token / db 密码 / blob key」的按用户隔离问题。**HM 侧已落地;双方约定 AM 用 ① 本地校验接入(见下,一行字符串比较)。** - 部署 agent 时,**HM 给每个 agent 现签一把随机访问令牌**(per-agent access token,UUID),并: 1. **注入进 agent 的 env**:`AGENT_ACCESS_TOKEN`(令牌本身)、`HEICODE_AGENT_ID`(该 agent 的 `agent_id`,即 `dep-xxx`)。 2. **返回给客户端**:HM 的 agent 列表/详情里 `access_token` 字段就是这把令牌(只有部署它的那个用户拿得到)。 - 客户端直连 agent 时,把这把令牌放进**请求头 `X-Agent-Access-Token`**。 - **agent 鉴权方式 = ① 本地校验(已选定)**: - 比较 `请求头 X-Agent-Access-Token == 自己 env 里的 AGENT_ACCESS_TOKEN`(常量时间比较,建议 `hmac.compare`/`secrets.compare_digest`)。两者是 HM 启动时注入的同一把密钥,**相等即放行,不等返回 401/403**。 - **不用回 HM、零额外依赖、零网络往返**。这是双方约定的正式接入方式。 - 缺失或为空(老 agent / 未注入)时如何处理由 AM 决定(建议:env 里有 `AGENT_ACCESS_TOKEN` 就强制校验,没有则放行以兼容)。 - **可选兜底:HM 权威校验端点**(AM 不需要、默认不用;仅当 AM 想让 HM 当权威、或想顺带拿 `user_id` 时)。公开端点、常量时间比较、生产实测可用: - 请求:`POST {HM}/api/heicode/agent-access/verify`,`Content-Type: application/json`,body `{"agent_id":"","access_token":""}` - 命中:`{"success":true,"data":{"valid":true,"agent_id":"dep_xxx","user_id":"22","template_id":"architect"}}`(`user_id` 为字符串) - 不命中(令牌错 / agent 不存在 / 空令牌):`{"success":true,"data":{"valid":false}}`(不泄露任何用户信息) - **只有部署该 agent 的用户拿到令牌 ⇒ 只有他能调** = 「这个 agent 这个用户能调」。 - **通道加密 = TLS**:agent 子域名请走 **HTTPS**,令牌与对话内容由 TLS 加密;HM 不在回路,无需 HM 中转解密。 **AM 侧伪代码(本地校验):** ```python # coding_a2a_agent 收到 A2A 请求时 expected = os.environ.get("AGENT_ACCESS_TOKEN", "") got = request.headers.get("X-Agent-Access-Token", "") if expected and not secrets.compare_digest(expected, got): raise HTTPException(401, "agent access denied") # 通过 → 正常处理(api_key 仍用于打 HM /v1 计费) ``` 模型鉴权(打 HM `/v1/*`)仍是 A2A 请求里的 `api_key`(模型 sk-),与上面的访问令牌**职责分离**:`api_key` 管「用谁的额度跑模型」,`AGENT_ACCESS_TOKEN` 管「谁有权调这个 agent」。 > 若 AM 完全不接:退化为旧状态——任一有效 sk 都能驱动任一 agent,存在跨用户读资源风险。HM 已把机制备好,接入成本极低(本地校验仅一行字符串比较)。 --- ## 4. 安全要求 - `env` 里**含明文密钥**(vm/db 密码、blob key、git token)。**AM 的启动接口必须 HTTPS 或私网**;HM 检测到明文密钥发往非 HTTPS AM 会打告警。 - **agent 绝不能把 `.env`/密钥回显**到对话、日志、或提交进 git——由 AM 保证。 - 生产密钥**只到 agent 运行环境**,不要再外传。 --- ## 5. 回调(可选,lifecycle) HM 提供 `POST /api/agent/callbacks/runtime-events`(HMAC 签名,`callback_url` 已随启动下发)。AM 可上报 agent 生命周期(started/stopped/unhealthy)。**当前 HM 主要用主动拉取(§1.2)判状态,回调非必须**;若 AM 走回调推送,与我们对齐事件格式即可。 --- ## 6. 路径覆盖(联调期改路径无需改 HM 代码) HM 这些路径都可用环境变量覆盖(默认值见 §1),AM 若用别的路径,运维改 env 即可: | env | 默认 | 用途 | |---|---|---| | `AGENT_RUNTIME_BASE_URL` | — | AM 基址(必须配,HTTPS/私网) | | `AGENT_RUNTIME_SERVICE_TOKEN` | — | HM 调 AM 的 Bearer | | `AGENT_RUNTIME_AGENT_START_PATH` | `/agents` | 启动(你们的 create) | | `AGENT_RUNTIME_AGENT_PATH` | `/agents/{agent_id}` | 取状态(GET)/删除(DELETE) | | `AGENT_RUNTIME_AGENT_STOP_PATH` | `/agents/{agent_id}/stop` | 停止 | | `AGENT_RUNTIME_START_TIMEOUT_SECONDS` | `60` | 启动超时 | > 字段名若需调整(如 `subdomain`→别的),只改 `heicode/controller/agent_template_runtime.go` 中 `amStartTemplateAgent`/`amGetAgentStatus` 的解析键一处即可,其余 HM 代码不动。 --- ## 7. 联调自检清单 - [ ] `POST /agents` 收到 `config`+`env`(含 `AGENT_INSTRUCTION_TEXT`/`AGENT_ROLE_NAME`/`AGENT_ACCESS_TOKEN`),起 agent、注入 env、返回 `{namespace 或 runtime_id, access_info.domain 或 subdomain, status}`。 - [ ] agent 用模型打到 HM `/v1/*`(带用户身份),计费正常。 - [ ] agent 子域名按 §3.1 ① **本地比对** `X-Agent-Access-Token == env AGENT_ACCESS_TOKEN`(常量时间);不符返回 401/403,非该 agent 所有者的调用被拒。 - [ ] `GET /agents/{id}` 返回真实 status;`stop`/`delete` 生效。 - [ ] agent 不泄露 env/密钥;启动接口 HTTPS/私网。 - [ ] 把 §3 的 SSE 直连契约回给 HM 补进客户端文档。