Merge pull request #56 from xmindlab-heicode/feat/agent-endpoint-security-scheme
feat(agent): return A2A endpoint security scheme/profile (#55)
This commit is contained in:
@@ -260,7 +260,12 @@ signature = base64( ed25519_sign( device_priv, sha256(canonical) ) )
|
|||||||
"status": "Pending", // Pending | running | failed | stopped …
|
"status": "Pending", // Pending | running | failed | stopped …
|
||||||
"runtime_id": "dep-4bb07dc1e376", // AM 侧运行时 id
|
"runtime_id": "dep-4bb07dc1e376", // AM 侧运行时 id
|
||||||
"created_at": "2026-06-04T09:04:15Z",
|
"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
|
"total": 1
|
||||||
@@ -346,6 +351,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`,直连已通。**
|
> ⚠️ **连之前先确认 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。
|
> ⚠️ 当前 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/send`
|
||||||
- **流式**:`POST {subdomain}/message/stream`(返回 `text/event-stream`)
|
- **流式**:`POST {subdomain}/message/stream`(返回 `text/event-stream`)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,6 +111,7 @@ func templateAgentResponse(row model.AgentDeployment) gin.H {
|
|||||||
if strings.TrimSpace(row.BindingIDsJSON) != "" {
|
if strings.TrimSpace(row.BindingIDsJSON) != "" {
|
||||||
_ = common.UnmarshalJsonStr(row.BindingIDsJSON, &bindingIDs)
|
_ = common.UnmarshalJsonStr(row.BindingIDsJSON, &bindingIDs)
|
||||||
}
|
}
|
||||||
|
scheme, profile, secure := agentEndpointSecurity(row.Subdomain)
|
||||||
return gin.H{
|
return gin.H{
|
||||||
"agent_id": row.DeploymentID,
|
"agent_id": row.DeploymentID,
|
||||||
"template_id": row.TemplateID,
|
"template_id": row.TemplateID,
|
||||||
@@ -121,6 +122,30 @@ func templateAgentResponse(row model.AgentDeployment) gin.H {
|
|||||||
"runtime_id": row.RuntimeDeploymentID,
|
"runtime_id": row.RuntimeDeploymentID,
|
||||||
"created_at": row.CreatedAtText,
|
"created_at": row.CreatedAtText,
|
||||||
"updated_at": row.UpdatedAtText,
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user