Files
heicode-mananger/docs/integration/heicode-am-contract.md
T
chenchenandClaude Opus 4.8 c05b27de6a revert: drop outbound env-key logging; root cause was AM stale prod image
Token IS transmitted by HM (confirmed); the agent didn't enforce it because AM
hadn't deployed the image containing the §5 check to production. So the debug
log is unnecessary — removed. Contract §0.1 updated: token-check is "code-ready,
pending AM prod image", not a HM gap. UI access-token/direct-URL display kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 00:28:53 +08:00

198 lines
14 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.
# 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 <service_token>` 调 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<Agent_Prompt>...</Agent_Prompt>",
"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":"<HEICODE_AGENT_ID>","access_token":"<X-Agent-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 补进客户端文档。