diff --git a/docs/integration/heicode-desktop-client-api.md b/docs/integration/heicode-desktop-client-api.md index d1d2ac55..a1071c64 100644 --- a/docs/integration/heicode-desktop-client-api.md +++ b/docs/integration/heicode-desktop-client-api.md @@ -241,7 +241,12 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) ) "status": "Pending", // Pending | running | failed | stopped … "runtime_id": "dep-4bb07dc1e376", // AM 侧运行时 id "created_at": "2026-06-04T09:04:15Z", - "updated_at": "2026-06-04T09:04:15Z" + "updated_at": "2026-06-04T09:04:15Z", + "security": { // ★ #55 A2A 直连安全元数据 + "scheme": "http", // http | https + "security_profile": "none", // none | tls | mtls + "secure": false // = security_profile != none + } } ], "total": 1 @@ -327,6 +332,8 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) ) > ⚠️ **连之前先确认 agent 就绪**:新建后 `status=Pending`(还在拉起)。等 `GET /api/heicode/agents/{id}/status` 变 `running`、或 `GET {subdomain}/health` 返 200 再连。**2026-06-04 复测:数秒即 `running`,`/health` 200、`/message/send` 带令牌任务 `completed`,直连已通。** > > ⚠️ 当前 AM 侧两点(待加固,不影响调通):① **令牌校验尚未真正生效**——无 `X-Agent-Access-Token` 也被放行;客户端仍应规范地每请求都带,等 AM 开启校验即自动生效。② 子域名目前 `http://` 明文,令牌/`api_key` 会明文传输,等 AM 上 HTTPS。 +> +> 🔒 **传输安全门(#55)**:agent 对象回传 `security`(`scheme` http/https、`security_profile` none/tls/mtls、`secure` 布尔)。客户端据此在生产强制 HTTPS(`HEICODE_AGENT_REQUIRE_SECURE=1`):`secure=false`(当前明文)→ 拒绝直连并提示。AM 上线 HTTPS/mTLS listener 后 `security_profile` 自动变 tls,客户端无需改包。HM 保守口径:无法确证 TLS 即标 `none`。 - **同步**:`POST {subdomain}/message/send` - **流式**:`POST {subdomain}/message/stream`(返回 `text/event-stream`) diff --git a/heicode/controller/agent_endpoint_security_test.go b/heicode/controller/agent_endpoint_security_test.go new file mode 100644 index 00000000..712220b1 --- /dev/null +++ b/heicode/controller/agent_endpoint_security_test.go @@ -0,0 +1,24 @@ +package controller + +import "testing" + +// #55: agent endpoint 安全级别推断 —— 明文/裸主机 = none(客户端生产可拒),显式 https = tls。 +func TestAgentEndpointSecurity(t *testing.T) { + cases := []struct { + in string + scheme, prof string + secure bool + }{ + {"https://dep-x.agents.example", "https", "tls", true}, + {"http://dep-x.agents.example", "http", "none", false}, + {"dep-x.taijiagnet.com", "http", "none", false}, // 裸主机:AM 当前明文 + {" HTTPS://Dep.Example ", "https", "tls", true}, + {"", "http", "none", false}, + } + for _, c := range cases { + sc, pr, se := agentEndpointSecurity(c.in) + if sc != c.scheme || pr != c.prof || se != c.secure { + t.Errorf("agentEndpointSecurity(%q) = (%s,%s,%v), want (%s,%s,%v)", c.in, sc, pr, se, c.scheme, c.prof, c.secure) + } + } +} diff --git a/heicode/controller/agent_template_handlers.go b/heicode/controller/agent_template_handlers.go index ec2f4390..21e010b5 100644 --- a/heicode/controller/agent_template_handlers.go +++ b/heicode/controller/agent_template_handlers.go @@ -111,6 +111,7 @@ func templateAgentResponse(row model.AgentDeployment) gin.H { if strings.TrimSpace(row.BindingIDsJSON) != "" { _ = common.UnmarshalJsonStr(row.BindingIDsJSON, &bindingIDs) } + scheme, profile, secure := agentEndpointSecurity(row.Subdomain) return gin.H{ "agent_id": row.DeploymentID, "template_id": row.TemplateID, @@ -121,6 +122,30 @@ func templateAgentResponse(row model.AgentDeployment) gin.H { "runtime_id": row.RuntimeDeploymentID, "created_at": row.CreatedAtText, "updated_at": row.UpdatedAtText, + // #55: A2A 直连安全元数据。客户端据此在生产强制 HTTPS(HEICODE_AGENT_REQUIRE_SECURE): + // scheme=http/https,security_profile=none/tls,secure=profile!=none。AM 启用 HTTPS/ + // mTLS listener 是 AM(azgy)的事;HM 只如实回传当前子域安全级别,明文 http → + // security_profile=none,客户端可拒绝并提示(元数据不含任何 secret_ref)。 + "security": gin.H{ + "scheme": scheme, + "security_profile": profile, + "secure": secure, + }, + } +} + +// agentEndpointSecurity 从子域字符串推断 A2A 直连的安全级别(#55)。纯函数,可单测。 +// 显式 https:// → (https, tls, true);显式 http:// 或裸主机(AM 当前默认明文) → (http, none, false)。 +// 保守口径:无法确证 TLS 即视为 none,宁可让客户端在生产拒绝,也不回传"看似安全"的明文端点。 +func agentEndpointSecurity(subdomain string) (scheme, profile string, secure bool) { + s := strings.TrimSpace(strings.ToLower(subdomain)) + switch { + case strings.HasPrefix(s, "https://"): + return "https", "tls", true + case strings.HasPrefix(s, "http://"): + return "http", "none", false + default: + return "http", "none", false // 裸主机:AM 当前明文 HTTP,保守标 none } }