feat(agent): per-agent client↔agent access token for per-user authorization
HM now mints a random per-agent access token at deploy, injects it into the
agent env (AGENT_ACCESS_TOKEN + HEICODE_AGENT_ID) and returns it to the
deploying client (agent list access_token). Only the owning user receives it,
so only they can drive the agent — closing the gap where any valid sk- could
drive any agent and exfiltrate its mounted resources.
AM authorizes the caller either locally (compare to its env token) or via the
new public POST /api/heicode/agent-access/verify {agent_id, access_token} ->
{valid, user_id} (constant-time compare, no info leak on miss). AM may opt out.
Docs: AM contract §3.1 + client API §6 updated; access_token no longer empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,8 @@ HM 用 `Authorization: Bearer <service_token>` 调 AM。统一响应信封建议
|
||||
"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": "…"
|
||||
}
|
||||
@@ -60,7 +62,7 @@ HM 用 `Authorization: Bearer <service_token>` 调 AM。统一响应信封建议
|
||||
**响应(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` ← `access_token`/`token`(你们当前不返回 → HM 留空,客户端用 A2A `api_key`,见 §3)。
|
||||
- `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`)。
|
||||
@@ -110,8 +112,23 @@ HM 把用户绑定的资源按**资源类型 + provider**解成固定 env 名(
|
||||
|
||||
客户端从 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。
|
||||
|
||||
- **鉴权 = A2A 请求里的 `api_key`**(模型 sk-)。你们当前**不返回专属 access_token**,所以 HM 列表里 `access_token` 为空,客户端用 `api_key` 连。
|
||||
- ⚠️ **安全缺口(请你们确认/补)**:agent 端 env 里挂着用户的 git token / db 密码 / blob key,工具能 `run_database_query`/`read_blob_text`。若子域名公网可达、且只要任一有效 sk 就能驱动它,**别的用户可经此读走该用户的资源**。需要**按用户隔离**(per-agent 专属令牌,或网络隔离 + 校验 api_key 属于该 agent 的所有者)。HM 侧规划后续提供 V2 解密接口(客户端加密、agent 解密拿 key)配合。
|
||||
### 3.1 客户端↔agent 访问鉴权(HM 已提供,AM 可选接)
|
||||
|
||||
> 解决「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`,或 A2A `params` 里约定字段)。
|
||||
- **agent 鉴权该令牌,二选一:**
|
||||
- ① **本地校验(零额外依赖,推荐)**:比较 `调用方令牌 == 自己 env 里的 AGENT_ACCESS_TOKEN`。两者是 HM 启动时注入的同一把密钥,相等即放行。**不用回 HM。**
|
||||
- ② **HM 权威校验**:`POST {HM}/api/heicode/agent-access/verify`,body `{"agent_id":"<HEICODE_AGENT_ID>","access_token":"<调用方令牌>"}` → 返回 `{"success":true,"data":{"valid":true,"user_id":22,"template_id":"..."}}`。令牌不符只回 `{"valid":false}`(不泄露任何用户信息)。这样 AM 还能拿到「该 agent 属于哪个 HM 用户」。常量时间比较、公开端点(agent 无 HM 会话,令牌本身即凭据)。
|
||||
- **只有部署该 agent 的用户拿到令牌 ⇒ 只有他能调** = 「这个 agent 这个用户能调」。
|
||||
- **通道加密 = TLS**:agent 子域名请走 **HTTPS**,令牌与对话内容由 TLS 加密;HM 不在回路,无需 HM 中转解密。
|
||||
|
||||
模型鉴权(打 HM `/v1/*`)仍是 A2A 请求里的 `api_key`(模型 sk-),与上面的访问令牌**职责分离**:`api_key` 管「用谁的额度跑模型」,`AGENT_ACCESS_TOKEN` 管「谁有权调这个 agent」。
|
||||
|
||||
> 若 AM 完全不接:退化为旧状态——任一有效 sk 都能驱动任一 agent,存在跨用户读资源风险。HM 已把机制备好,接入成本极低(本地校验仅一行字符串比较)。
|
||||
|
||||
---
|
||||
|
||||
@@ -150,7 +167,7 @@ HM 这些路径都可用环境变量覆盖(默认值见 §1),AM 若用别的路
|
||||
|
||||
- [ ] `POST /agents/start` 收到 `agent_definition`+`env`+`callback_url`,起 agent、注入 env、返回 `{runtime_id, subdomain, access_token, status}`。
|
||||
- [ ] agent 用模型打到 HM `/v1/*`(带用户身份),计费正常。
|
||||
- [ ] agent 子域名校验 access_token;非法连接被拒。
|
||||
- [ ] agent 子域名按 §3.1 校验 `AGENT_ACCESS_TOKEN`(本地比较或回 HM `/api/heicode/agent-access/verify`);非该 agent 所有者的调用被拒。
|
||||
- [ ] `GET /agents/{id}` 返回真实 status;`stop`/`delete` 生效。
|
||||
- [ ] agent 不泄露 env/密钥;启动接口 HTTPS/私网。
|
||||
- [ ] 把 §3 的 SSE 直连契约回给 HM 补进客户端文档。
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
**总原则**
|
||||
- 客户端调 HM 用 **V2 设备签名**(与模型调用同一套,见 §1)。
|
||||
- 列表里的 `access_token` 是连 agent 用的,**由 agent 自己校验**;HM 只负责发给你。
|
||||
- 列表里的 `access_token` 是连 agent 用的**专属访问令牌**(HM 给每个 agent 现签,只有部署它的你拿得到)。直连时带上它(`X-Agent-Access-Token` 头);**agent 校验「是不是该 agent 的所有者」**。它与模型 `api_key` 职责分离:`access_token` 管「谁能调这个 agent」,`api_key` 管「用谁的额度跑模型」。
|
||||
- 模型永远走 HM `/v1/*`,不要直连上游。
|
||||
|
||||
---
|
||||
@@ -150,7 +150,7 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
|
||||
|
||||
> **2026-06-04 生产实测**:部署 / 列表 / 详情 / 状态 / 删除均真实可用。
|
||||
> - `subdomain` 由 AM 分配(如 `dep-xxxx.taijiagnet.com`);新建后 `status` 为 `Pending`,需等 agent 起来变 `running`(直连前先确认,见 §6)。
|
||||
> - **`access_token` 当前恒为空字符串**——AM 的 coding_a2a_agent **不发专属令牌**,直连用 A2A 的 `api_key`(见 §6)。
|
||||
> - **`access_token` 是 HM 现签的 per-agent 专属访问令牌**(非空,UUID)——直连 agent 时带上(`X-Agent-Access-Token` 头),agent 据此判定「是不是本 agent 的所有者」(见 §6)。
|
||||
> - `stop`/`delete` 调 AM:目前 AM 的 `stop` 端点缺失、`delete` 有已知 bug,所以 `delete` 会**先清掉 HM 本地记录**并返回 `runtime_cleanup:"failed"`(AM 侧可能残留);`stop` 暂时会失败。
|
||||
|
||||
| 方法 | 路径 | 说明 | 状态 |
|
||||
@@ -172,7 +172,7 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
|
||||
"agent_id": "dep_4bb07dc1e376", // HM 侧 agent 记录 id(部署/停删都用它)
|
||||
"template_id": "architect", // 用了哪个模板
|
||||
"subdomain": "dep-4bb07dc1e376.taijiagnet.com", // ★ 直连地址(AM 分配,主机名)
|
||||
"access_token": "", // 当前恒空(AM 不发;直连用 A2A api_key)
|
||||
"access_token": "550e8400-e29b-41d4-a716-446655440000", // ★ per-agent 访问令牌(直连带上)
|
||||
"binding_ids": [], // 挂了哪些资源绑定(网页台绑的)
|
||||
"status": "Pending", // Pending | running | failed | stopped …
|
||||
"runtime_id": "dep-4bb07dc1e376", // AM 侧运行时 id
|
||||
@@ -208,6 +208,8 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
|
||||
- **流式**:`POST {subdomain}/message/stream`(返回 `text/event-stream`)
|
||||
- **发现/健康**:`GET {subdomain}/.well-known/agent.json`、`GET {subdomain}/health`
|
||||
|
||||
请求头:`X-Agent-Access-Token: <§5 列表里的 access_token>` —— **访问鉴权**(证明你是该 agent 的所有者)。
|
||||
|
||||
请求体(JSON-RPC,A2A):
|
||||
```json
|
||||
{ "jsonrpc":"2.0", "id":"task-1", "method":"message/send",
|
||||
@@ -220,11 +222,13 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
|
||||
}
|
||||
```
|
||||
|
||||
- **鉴权 = `params.api_key`**(模型 sk-,客户端自带的网关 key);agent 凭它调 HM `/v1/*` 计费。HM 当前列表里的 `access_token` 字段对该 agent**为空**(AM 不发专属 token);连 agent 用上面的 `api_key`。
|
||||
**两层鉴权,职责分离:**
|
||||
- **访问鉴权 = `access_token`**(`X-Agent-Access-Token` 头,§5 列表里那把 per-agent 令牌):证明「你是这个 agent 的所有者,有权调它」。agent 本地比对自己 env 里的 `AGENT_ACCESS_TOKEN`,或回 HM `POST /api/heicode/agent-access/verify` 校验。
|
||||
- **模型鉴权 = `params.api_key`**(模型 sk-,客户端自带的网关 key):agent 凭它调 HM `/v1/*`,计费到你账上。
|
||||
- agent 用 `.env` 里注入的资源(git/mysql/postgres/azure-blob)自行干活;工具:`read_file/write_file/edit_file/run_command/git_*/run_database_query/list_blob_objects` 等。缺配置的资源工具调用会返回 `resource not configured`,不阻塞。
|
||||
- 可在 `params.configuration.resources` 里按需覆盖资源(请求级覆盖启动级)。
|
||||
|
||||
> ⚠️ 安全:当前 agent 端鉴权仅为"持有效 api_key",非 per-agent 专属令牌。HM 侧规划**用 V2 解密接口**做按用户隔离(客户端加密请求→agent 解密拿 key),或由 AM 自行加门。上线公网前需确认该端点不被他人用任意 key 驱动(见 AM 契约 §3/§4)。
|
||||
> ℹ️ 安全:HM 已为「客户端↔agent」提供按用户隔离机制 —— per-agent 访问令牌(`access_token`,部署时现签、注入 agent env、随列表下发),只有部署该 agent 的你拿得到 ⇒ 只有你能调。**AM 是否启用由 AM 决定**(可本地比对或回 HM verify,也可自行另搞一套);若 AM 不启用,则退化为「任一有效 api_key 可驱动任一 agent」。通道加密走 agent 子域名的 HTTPS/TLS。详见 AM 契约 §3.1。
|
||||
|
||||
---
|
||||
|
||||
@@ -267,7 +271,9 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
|
||||
- 想看有哪些角色: GET /api/heicode/agent-templates (中文名/简介)
|
||||
- 部署(也可在网页台做): POST /api/heicode/agents {template_id, binding_ids}
|
||||
4. 选一个 status=running 的 agent → 取 subdomain + access_token
|
||||
5. 直连 subdomain (SSE, 带 token) → 与 agent 对话/干活 ← 不经 HM
|
||||
5. 直连 subdomain (SSE) → 与 agent 对话/干活 ← 不经 HM
|
||||
- 头带 X-Agent-Access-Token: <access_token> (访问鉴权,证明是该 agent 所有者)
|
||||
- body params.api_key: <模型 sk> (模型鉴权,计费到你账上)
|
||||
6. 期间用模型 → HM /v1/* (客户端与 agent 两端都是)
|
||||
7. 管理: GET /agents/{id}/status | POST /agents/{id}/stop | DELETE /agents/{id}
|
||||
```
|
||||
@@ -285,6 +291,7 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
|
||||
| `POST /api/heicode/agents` | 会话/设备 | 🟢(调 AM,AM 接口 🔴) |
|
||||
| `POST /api/heicode/agents/{id}/stop`、`DELETE /{id}` | 会话/设备 | 🟢(调 AM 🔴) |
|
||||
| 模型 `/v1/*` | 同模型调用 | 🟢 |
|
||||
| 直连 agent SSE(subdomain+token) | agent 自校验 | 🔴(待 AM agent 端) |
|
||||
| `POST /api/heicode/agent-access/verify` | 公开(令牌即凭据) | 🟢(供 agent 服务端校验,见 §6) |
|
||||
| 直连 agent SSE(subdomain + X-Agent-Access-Token) | agent 校验(本地比对/回 HM verify) | 🔴(待 AM agent 端) |
|
||||
|
||||
> 🔴 项不阻塞客户端集成:HM 侧字段/接口已定型并生产验证,等 AM 实现 agent 启动/状态/直连即全线打通。AM 契约见 `heicode-hm-template-agent-model.md` §7 与代码 `controller/agent_template_runtime.go`(隔离层)。
|
||||
|
||||
Reference in New Issue
Block a user