diff --git a/docs/integration/AgentManager蜂群Runtime接口实现要求.md b/docs/integration/AgentManager蜂群Runtime接口实现要求.md index 9b4a8296..b2a6d8e6 100644 --- a/docs/integration/AgentManager蜂群Runtime接口实现要求.md +++ b/docs/integration/AgentManager蜂群Runtime接口实现要求.md @@ -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: +X-Heicode-Timestamp: +X-Heicode-Nonce: +X-Heicode-Fingerprint: +X-Heicode-Eph-Pubkey: +X-Heicode-Signature: +Content-Type: application/json +``` + +未加密 Web 控制台请求仍按 Manager session cookie + `New-Api-User` 兼容处理。Manager 调 Agent Manager / Swarm Runtime 的服务间请求不使用该客户端 body 加密协议,而使用 `Authorization: Bearer ` 和公共追踪 Header。 + ## 3. Agent Manager 必须提供的接口 | 优先级 | 方法 | 路径 | 必须 | 用途 | @@ -621,4 +675,3 @@ Runtime 可通过 `budget.alert` 或 `timeline.updated` 回传用量摘要。 9. 最终交付物回到客户端。 Manager 本地模拟事件、测试桩、空态页面和 schema 校验只能证明 Manager 接收和展示能力,不能证明蜂群生产闭环完成。 - diff --git a/docs/integration/heicode-desktop-sub-agile-api.md b/docs/integration/heicode-desktop-sub-agile-api.md index 1d434e4c..d139887a 100644 --- a/docs/integration/heicode-desktop-sub-agile-api.md +++ b/docs/integration/heicode-desktop-sub-agile-api.md @@ -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 ` | | `/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 ### 2.3 Manager 用户态 Header -未加密 Web 控制台请求调用 `/api/agnet/user/*` 时必须带: +未加密 Web 控制台请求调用 `/api/agnet/user/*` 或 `/api/swarms` 时必须带: ```http Cookie: 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`。 diff --git a/heicode/VERSION b/heicode/VERSION index 4ea2b1f4..ac9f79ca 100644 --- a/heicode/VERSION +++ b/heicode/VERSION @@ -1 +1 @@ -1.4.9 +1.4.10 diff --git a/heicode/middleware/auth_v2_user_test.go b/heicode/middleware/auth_v2_user_test.go index 7dd8d4d4..8fa5733b 100644 --- a/heicode/middleware/auth_v2_user_test.go +++ b/heicode/middleware/auth_v2_user_test.go @@ -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 diff --git a/heicode/router/api-router.go b/heicode/router/api-router.go index 65ced402..e528f4b7 100644 --- a/heicode/router/api-router.go +++ b/heicode/router/api-router.go @@ -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 diff --git a/heicode/router/api_router_test.go b/heicode/router/api_router_test.go index 88e37264..fa3d1559 100644 --- a/heicode/router/api_router_test.go +++ b/heicode/router/api_router_test.go @@ -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)) +}