feat(swarm): GET /swarms/:id 详情补 resource_grants/budget/audit_logs (#66 里程碑07/08)

HM 自有数据,不依赖 swarm 运行时,仅 detail 带、list 不带(免膨胀):
- resource_grants:plan_json 提取,脱敏(grant_id/resource_id/resource_type/binding_scope/
  target_role/permission_scope/status + has_secret);绝不下发 secret_ref 值。
- budget:max_tokens/max_cost_usd/max_duration_sec(plan.Budget 上限)。
- audit_logs:HM 控制面 deployment 级审计(脱敏 + 截断 100);run 级 trace 待 swarm /audit 代理(PR#41)。

usage/used_*/cost_by_phase 待 #60 + swarm metrics,本批不臆造。
测试:脱敏(secret_ref 值不外泄)/has_secret/budget 上限/空坏JSON。build+vet+controller 测试全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:24:09 +08:00
co-authored by Claude Opus 4.8
parent 219bbf1836
commit a5419f9a15
2 changed files with 115 additions and 1 deletions
+84 -1
View File
@@ -216,7 +216,90 @@ func HeicodeGetSwarmStatus(c *gin.Context) {
if !ok { if !ok {
return return
} }
common.ApiSuccess(c, swarmDeploymentView(dep)) // 详情视图在状态摘要上补 HM 自有数据(#66 里程碑07/08):resource_grants(脱敏)/budget 上限/
// 控制面审计。这些不依赖 swarm 运行时,故 list(轻量)不带、仅 detail 带。
// usage/used_*/cost_by_phase 待 #60(计费 sk-)+ swarm metrics,本批不臆造。
detail := swarmDeploymentView(dep)
detail["resource_grants"] = swarmResourceGrantsView(dep.PlanJSON)
detail["budget"] = swarmBudgetView(dep.PlanJSON)
detail["audit_logs"] = swarmAuditView(dep.DeploymentID)
common.ApiSuccess(c, detail)
}
// swarmPlanForView 解析 plan_json 的 budget + resource_grants(详情视图用,复用既有类型)。
func swarmPlanForView(planJSON string) (agentBudget, []agentResourceGrant) {
if strings.TrimSpace(planJSON) == "" {
return agentBudget{}, nil
}
var plan struct {
Budget agentBudget `json:"budget"`
ResourceGrants []agentResourceGrant `json:"resource_grants"`
}
if err := common.UnmarshalJsonStr(planJSON, &plan); err != nil {
return agentBudget{}, nil
}
return plan.Budget, plan.ResourceGrants
}
// swarmResourceGrantsView:resource_grants 脱敏视图(#66 里程碑07,只读)。
// **安全红线**:绝不下发 secret_ref 值,只用 has_secret 标有无。
func swarmResourceGrantsView(planJSON string) []gin.H {
_, grants := swarmPlanForView(planJSON)
out := make([]gin.H, 0, len(grants))
for _, g := range grants {
out = append(out, gin.H{
"grant_id": g.GrantID,
"resource_id": g.ResourceID,
"resource_type": g.ResourceType,
"binding_scope": g.BindingScope,
"target_role": g.TargetRole,
"permission_scope": g.PermissionScope,
"status": g.Status,
"has_secret": strings.TrimSpace(g.SecretRef) != "",
})
}
return out
}
// swarmBudgetView:预算上限(#66 里程碑08)。used_* 待 #60 计费 + swarm metrics,本批不返(不臆造)。
func swarmBudgetView(planJSON string) gin.H {
b, _ := swarmPlanForView(planJSON)
return gin.H{
"max_tokens": b.MaxTokens,
"max_cost_usd": b.MaxCostUSD,
"max_duration_sec": b.MaxDurationSec,
}
}
// swarmAuditView:HM 控制面部署审计(deployment 级,脱敏 + 截断)。
// run 级 trace(prompt/model/tool/approval)是另一 scope,待 swarm `/audit` 代理(PR#41)。
func swarmAuditView(deploymentID string) []gin.H {
if model.DB == nil {
return []gin.H{}
}
rows, err := model.ListAgentAuditEventsByDeployment(deploymentID)
if err != nil || len(rows) == 0 {
return []gin.H{}
}
const maxRows = 100
if len(rows) > maxRows {
rows = rows[:maxRows]
}
out := make([]gin.H, 0, len(rows))
for _, r := range rows {
item := gin.H{
"event": r.Event,
"actor": r.Actor,
"result": r.Result,
"correlation_id": r.CorrelationID,
"occurred_at": r.OccurredAt,
}
if strings.TrimSpace(r.DetailsJSON) != "" {
item["details"] = sanitizeSwarmPayload(r.DetailsJSON) // 递归剔敏 + RedactText
}
out = append(out, item)
}
return out
} }
// HeicodeListSwarmEvents: GET /api/heicode/swarms/:id/events?after=&limit= — 事件增量拉取。 // HeicodeListSwarmEvents: GET /api/heicode/swarms/:id/events?after=&limit= — 事件增量拉取。
@@ -198,3 +198,34 @@ func TestSwarmGoalSummary(t *testing.T) {
require.LessOrEqual(t, len([]rune(out)), 201) // 200 + … require.LessOrEqual(t, len([]rune(out)), 201) // 200 + …
require.True(t, strings.HasSuffix(out, "…")) require.True(t, strings.HasSuffix(out, "…"))
} }
// #66 里程碑07:resource_grants 脱敏视图 —— 绝不下发 secret_ref 值,只标 has_secret。
func TestSwarmResourceGrantsView(t *testing.T) {
plan := `{"resource_grants":[
{"grant_id":"g1","resource_id":"r1","resource_type":"git","binding_scope":"git:repo","target_role":"impl","permission_scope":["read","write"],"status":"active","secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/git-pat"},
{"grant_id":"g2","resource_id":"r2","resource_type":"sk","status":"active"}
]}`
out := swarmResourceGrantsView(plan)
require.Len(t, out, 2)
require.Equal(t, "git", out[0]["resource_type"])
require.Equal(t, true, out[0]["has_secret"])
require.Equal(t, false, out[1]["has_secret"])
b, _ := json.Marshal(out)
require.NotContains(t, string(b), "secret_ref")
require.NotContains(t, string(b), "azkv://")
require.NotContains(t, string(b), "git-pat")
require.Empty(t, swarmResourceGrantsView(""))
require.Empty(t, swarmResourceGrantsView("bad-json"))
}
// #66 里程碑08:budget 上限;used_* 本批不返。
func TestSwarmBudgetView(t *testing.T) {
b := swarmBudgetView(`{"budget":{"max_tokens":100000,"max_cost_usd":5.5,"max_duration_sec":3600}}`)
require.EqualValues(t, 100000, b["max_tokens"])
require.EqualValues(t, 5.5, b["max_cost_usd"])
require.EqualValues(t, 3600, b["max_duration_sec"])
_, hasUsed := b["used_model_cost"]
require.False(t, hasUsed, "used_* 本批不应臆造")
empty := swarmBudgetView("")
require.EqualValues(t, 0, empty["max_tokens"])
}