fix: enable encrypted swarm requests
This commit is contained in:
@@ -24,6 +24,45 @@ Agent Manager / Swarm Runtime 需要负责执行层:
|
||||
- 接收审批结果后继续或停止。
|
||||
- 回传日志、指标、用量和最终结果。
|
||||
|
||||
### 1.1 2026-05-28 生产联调实测结论
|
||||
|
||||
使用测试用户 `zsbgnw@gmail.com`(Manager 用户 `chenchen`,`user_id=22`)在生产 Manager 上实测蜂群入口。
|
||||
|
||||
当前生产 Manager Runtime 配置实际为:
|
||||
|
||||
```text
|
||||
AGNET_RUNTIME_BASE_URL=http://20.212.121.126
|
||||
AGNET_RUNTIME_CREATE_PATH=/api/agnet/deployments
|
||||
AGNET_RUNTIME_STOP_PATH=/api/agnet/deployments/{deployment_id}/stop
|
||||
```
|
||||
|
||||
实测结果:
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| `GET /api/agnet/runtime/health` | 通过,远端 Agent Manager IP healthy |
|
||||
| `POST /api/swarms` 使用 `agent_runtime.platform=agnet-swarm` | 失败,Manager 返回 `agent_runtime.platform must be agnet when runtime context is present` |
|
||||
| `POST /api/swarms` 使用 `agent_runtime.platform=agnet` | 通过 |
|
||||
| Manager deployment | `dep_72cdc138b9c8` |
|
||||
| Manager 返回 swarm_id | `dep_72cdc138b9c8`,当前是 Manager 本地 deployment id |
|
||||
| Runtime deployment | `dep_77370f27186f` |
|
||||
| Manager events | `deployment.accepted`、`runtime.sync.started`、`runtime.sync.accepted` |
|
||||
| Manager stop | 通过 |
|
||||
| Runtime stop | 通过,Agent Manager 侧 `dep_77370f27186f` 状态为 `stopped` |
|
||||
| Runtime callback | 未观察到,生产 Manager callback 表 0 条对应记录 |
|
||||
| artifact / timeline callback | 未产出 |
|
||||
| SK / usage callback | 未产出 |
|
||||
| Runtime logs | 仅 `Pod ... has no logs yet` |
|
||||
| Runtime metrics | 返回固定 CPU/内存/网络汇总,看起来仍是占位值 |
|
||||
|
||||
结论:
|
||||
|
||||
1. Heicode Manager 的 `/api/swarms` adapter 能接收用户态蜂群请求,并能通过当前 Runtime bridge 调 Agent Manager IP。
|
||||
2. 当前生产链路实际走的是 Agent Manager `/api/agnet/deployments`,不是 Agent Manager `/api/swarms`。
|
||||
3. Manager stop 可以传递到 Agent Manager deployment stop。
|
||||
4. 当前还没有真实蜂群 Runtime callback、task graph、artifact、SK、usage。
|
||||
5. Agent Manager 如果要满足正式蜂群模式,仍需实现本文后续定义的 `/api/swarms`、`/api/swarms/{swarm_id}`、`/api/swarms/{swarm_id}/stop`、callback 和 usage 约束。
|
||||
|
||||
## 2. 总体调用链
|
||||
|
||||
```text
|
||||
@@ -40,6 +79,21 @@ Heicode Desktop Client
|
||||
POST /api/swarms/{swarm_id}/approvals/{approval_id}
|
||||
```
|
||||
|
||||
客户端到 Heicode Manager 的 `POST /api/swarms` 与普通 sub 用户态接口一样,必须支持 V2 body 加密:
|
||||
|
||||
```http
|
||||
Content-Encoding: heicode-aead-v1
|
||||
X-Heicode-Device-Id: <device_id>
|
||||
X-Heicode-Timestamp: <unix_ms>
|
||||
X-Heicode-Nonce: <nonce_hex>
|
||||
X-Heicode-Fingerprint: <device_fingerprint>
|
||||
X-Heicode-Eph-Pubkey: <base64_x25519_ephemeral_pubkey>
|
||||
X-Heicode-Signature: <base64_ed25519_signature>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
未加密 Web 控制台请求仍按 Manager session cookie + `New-Api-User` 兼容处理。Manager 调 Agent Manager / Swarm Runtime 的服务间请求不使用该客户端 body 加密协议,而使用 `Authorization: Bearer <service_token>` 和公共追踪 Header。
|
||||
|
||||
## 3. Agent Manager 必须提供的接口
|
||||
|
||||
| 优先级 | 方法 | 路径 | 必须 | 用途 |
|
||||
@@ -621,4 +675,3 @@ Runtime 可通过 `budget.alert` 或 `timeline.updated` 回传用量摘要。
|
||||
9. 最终交付物回到客户端。
|
||||
|
||||
Manager 本地模拟事件、测试桩、空态页面和 schema 校验只能证明 Manager 接收和展示能力,不能证明蜂群生产闭环完成。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Heicode 桌面客户端 sub 敏捷流程 API 对接文档
|
||||
|
||||
更新时间:2026-05-27
|
||||
更新时间:2026-05-28
|
||||
适用范围:Heicode Desktop / 本地服务对接 Heicode Manager,跑通普通 sub 模式敏捷开发流程。
|
||||
Manager 生产地址:`https://code.xinghanlab.com`
|
||||
|
||||
@@ -20,17 +20,19 @@ Manager 负责辅助控制面:任务草稿桥接、资源/权限、Agnet deplo
|
||||
|---|---|---|
|
||||
| `/api/heicode-auth/api/user/tasks/*` | HeicodeTask 任务编排代理,创建任务、追问、查询任务 | V2 加密 body + `Authorization: Bearer <heicode_access_token>` |
|
||||
| `/api/agnet/user/*` | Manager 用户态 Agnet 控制面,deployment、timeline、artifact、审批 | V2 加密 body;未加密 Web 控制台请求继续使用 Manager session + `New-Api-User` |
|
||||
| `/api/swarms` | 蜂群模式创建入口 / Runtime adapter 入口 | V2 加密 body;未加密 Web 控制台请求继续使用 Manager session + `New-Api-User` |
|
||||
| `/api/user/self` | 查询当前 Manager 用户 | Manager 登录 session cookie |
|
||||
|
||||
### 2.2 桌面端请求 body 加密
|
||||
|
||||
桌面客户端调用 Manager 的 sub 流程接口时,应使用与模型调用一致的 V2 加密请求协议。生产 Manager 自 `1.4.7` 起已支持该能力。
|
||||
桌面客户端调用 Manager 的 sub / 蜂群入口时,应使用与模型调用一致的 V2 加密请求协议。普通 sub 用户态接口在生产 Manager `1.4.7+` 已支持;`POST /api/swarms` 蜂群入口在本次 Manager 代码中补齐同一套 V2 body 加密鉴权,需随下一次生产部署生效。
|
||||
|
||||
适用接口:
|
||||
|
||||
| API 前缀 | V2 加密 body | 说明 |
|
||||
|---|---|---|
|
||||
| `/api/agnet/user/*` | 支持 | Manager 解密并校验设备签名后,按当前设备对应用户执行 |
|
||||
| `/api/swarms` | 支持 | Manager 解密并校验设备签名后,按当前设备对应用户创建蜂群/Runtime adapter deployment |
|
||||
| `/api/heicode-auth/*` | 支持 | Manager 解密并校验设备签名后,把明文 body 代理给上游 HeicodeTask 服务;仍需携带 `heicode_access_token` |
|
||||
| 浏览器后台普通页面请求 | 兼容未加密 JSON | 不影响现有 Manager Web 控制台 |
|
||||
|
||||
@@ -84,7 +86,7 @@ Authorization: Bearer <heicode_access_token>
|
||||
|
||||
### 2.3 Manager 用户态 Header
|
||||
|
||||
未加密 Web 控制台请求调用 `/api/agnet/user/*` 时必须带:
|
||||
未加密 Web 控制台请求调用 `/api/agnet/user/*` 或 `/api/swarms` 时必须带:
|
||||
|
||||
```http
|
||||
Cookie: session=<manager-session>
|
||||
@@ -95,9 +97,21 @@ Accept: application/json
|
||||
|
||||
`New-Api-User` 必须等于当前登录用户 ID,否则会返回未授权。
|
||||
|
||||
使用 V2 加密 body 时,`/api/agnet/user/*` 不依赖浏览器 session cookie,也不需要 `New-Api-User`;Manager 会从设备绑定 token 中解析用户身份。为兼容当前 Web 控制台,未加密请求仍按 session cookie + `New-Api-User` 处理。
|
||||
使用 V2 加密 body 时,`/api/agnet/user/*` 和 `/api/swarms` 不依赖浏览器 session cookie,也不需要 `New-Api-User`;Manager 会从设备绑定 token 中解析用户身份。为兼容当前 Web 控制台,未加密请求仍按 session cookie + `New-Api-User` 处理。
|
||||
|
||||
### 2.4 统一响应 Envelope
|
||||
### 2.4 蜂群入口加密边界
|
||||
|
||||
蜂群模式的业务流程与普通 sub 敏捷流程分开对接,但客户端到 Manager 的请求加密规则一致。
|
||||
|
||||
| 接口 | 所属模式 | 加密要求 |
|
||||
|---|---|---|
|
||||
| `POST /api/agnet/user/tasks/{task_id}/deployment-draft` | 普通 sub 敏捷 | V2 body 加密 |
|
||||
| `POST /api/agnet/user/deployments` | 普通 sub 敏捷 | V2 body 加密 |
|
||||
| `POST /api/swarms` | 蜂群模式 | V2 body 加密 |
|
||||
|
||||
注意:本文后续章节仍只描述普通 sub 敏捷主流程;蜂群 task graph、claim、heartbeat、handoff、approval decision 等字段以单独蜂群对接文档为准。
|
||||
|
||||
### 2.5 统一响应 Envelope
|
||||
|
||||
成功:
|
||||
|
||||
@@ -1221,7 +1235,7 @@ setInterval(async () => {
|
||||
|
||||
## 15. 当前生产注意事项
|
||||
|
||||
1. `https://code.xinghanlab.com` 的 Manager 用户态接口已上线,当前生产版本为 `1.4.9`。
|
||||
1. `https://code.xinghanlab.com` 的 Manager 用户态接口已上线;本次蜂群入口 V2 加密修复随 Manager `1.4.10` 发布。
|
||||
2. Manager 本地控制面可创建 `sub_mode=agile/waterfall` deployment。
|
||||
3. 生产 Manager 已配置 Agent Manager Runtime,当前直接走 `http://20.212.121.126`;域名和 HTTPS 后续单独处理,不作为客户端当前接入阻塞项。
|
||||
4. V2 加密 `deployment-draft` 已在生产验证通过:真实构造 `Content-Encoding: heicode-aead-v1` 请求返回 200,`sub_mode=agile`,`user_id=22`。
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.4.9
|
||||
1.4.10
|
||||
|
||||
@@ -128,6 +128,64 @@ func TestUserOrV2DeviceAuthDecryptsValidEncryptedRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserOrV2DeviceAuthDecryptsEncryptedSwarmRequest(t *testing.T) {
|
||||
setupV2UserAuthTestDB(t)
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
deviceID := "device-v2-swarm"
|
||||
fingerprint := strings.Repeat("d", 64)
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate ed25519 key: %v", err)
|
||||
}
|
||||
publicKeyB64 := base64.StdEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))
|
||||
deviceIDCopy := deviceID
|
||||
token := model.Token{
|
||||
UserId: 42,
|
||||
Key: "sk-v2-swarm-test",
|
||||
Status: common.TokenStatusEnabled,
|
||||
Name: "v2 swarm test",
|
||||
ExpiredTime: -1,
|
||||
UnlimitedQuota: true,
|
||||
DeviceId: &deviceIDCopy,
|
||||
DevicePubkey: &publicKeyB64,
|
||||
DeviceFingerprint: fingerprint,
|
||||
RequireDeviceBinding: true,
|
||||
}
|
||||
if err := token.Insert(); err != nil {
|
||||
t.Fatalf("insert token: %v", err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/swarms", UserOrV2DeviceAuth(), func(c *gin.Context) {
|
||||
if got := c.GetInt("id"); got != 42 {
|
||||
t.Fatalf("id context = %d, want 42", got)
|
||||
}
|
||||
var payload struct {
|
||||
OrchestrationPlan struct {
|
||||
Objective string `json:"objective"`
|
||||
} `json:"orchestration_plan"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
t.Fatalf("bind decrypted body: %v", err)
|
||||
}
|
||||
if payload.OrchestrationPlan.Objective != "encrypted swarm task" {
|
||||
t.Fatalf("objective = %q", payload.OrchestrationPlan.Objective)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
})
|
||||
|
||||
body := []byte(`{"orchestration_plan":{"objective":"encrypted swarm task"}}`)
|
||||
req := newEncryptedV2Request(t, http.MethodPost, "/api/swarms", body, deviceID, fingerprint, privateKey)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func setupV2UserAuthTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
common.UsingSQLite = true
|
||||
|
||||
@@ -61,7 +61,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
|
||||
apiRouter.GET("/agnet/callbacks/swarm-events/schema", controller.AgnetGetSwarmEventCallbackSchema)
|
||||
apiRouter.POST("/agnet/callbacks/swarm-events", controller.AgnetReceiveSwarmEventCallback)
|
||||
apiRouter.POST("/swarms", middleware.UserAuth(), controller.AgnetCreateUserSwarm)
|
||||
apiRouter.POST("/swarms", middleware.UserOrV2DeviceAuth(), controller.AgnetCreateUserSwarm)
|
||||
//apiRouter.POST("/waffo-pancake/webhook", controller.WaffoPancakeWebhook)
|
||||
|
||||
// Universal secure verification routes
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/heicode/manager/middleware"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -13,3 +16,19 @@ func TestSetApiRouterDoesNotRegisterDuplicateRoutes(t *testing.T) {
|
||||
SetApiRouter(gin.New())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSwarmRouteUsesV2DeviceAuth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
SetApiRouter(engine)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/swarms", nil)
|
||||
req.Header.Set("Content-Encoding", middleware.V2ContentEncoding)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
require.Equal(t, middleware.V2AuthErrorEphPubkeyMissing, rec.Header().Get(middleware.HeaderAuthError))
|
||||
require.NotEmpty(t, rec.Header().Get(middleware.HeaderServerTime))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user