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:
@@ -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"])
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user