回应 Fasthei 复审(PR #53 CHANGES_REQUESTED): 1. 事件 payload 脱敏:swarmEventView 经 sanitizeSwarmPayload —— 递归剔除 secret_ref/credentials/token/api_key/private_key/access_key/password 及 plan/payload/ permission_manifest/env 大字段,再跑 RedactText 兜底。绝不下发 azkv:// secret_ref 或 sk-/Bearer(approval.requested 等 envelope 携带的凭据引用)。加 TestSanitizeSwarmPayload_*。 2. user 作用域:model.ListSwarmCallbackEventsAfter 增加 userID 参数 + WHERE user_id, controller 传入当前用户;防 runtime_swarm_id/deployment_id 碰撞或误写导致跨用户事件泄漏。 测试补 user 隔离用例。 3. stop 不伪造成功:移除「开关打开返回 accepted:true」路径;未启用→POLICY_REJECTED, 启用也→NOT_IMPLEMENTED(未转发运行时),直到 agent_swarm#2 冻结接上真实 stop。 文档 §5.2 同步(脱敏 / user 作用域 / stop 语义)。go build/vet 干净,controller+model 全回归通过。 Affects: Manager only(只读查询脱敏 + 写端点安全语义)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
3.9 KiB
Go
118 lines
3.9 KiB
Go
package model
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
type AgentCallbackEvent struct {
|
|
Id int `gorm:"primaryKey" json:"id"`
|
|
EventID string `gorm:"type:varchar(128);uniqueIndex" json:"event_id"`
|
|
IdempotencyKey string `gorm:"type:varchar(128);index" json:"idempotency_key"`
|
|
CallbackType string `gorm:"type:varchar(64);index" json:"callback_type"`
|
|
EventType string `gorm:"type:varchar(64);index" json:"event_type"`
|
|
DeploymentID string `gorm:"type:varchar(64);index" json:"deployment_id"`
|
|
SwarmID string `gorm:"type:varchar(128);index" json:"swarm_id"`
|
|
AgentInstanceID string `gorm:"type:varchar(128);index" json:"agent_instance_id"`
|
|
TaskID string `gorm:"type:varchar(128);index" json:"task_id"`
|
|
UserID string `gorm:"type:varchar(64);index" json:"user_id"`
|
|
BindingScope string `gorm:"type:varchar(512);index" json:"binding_scope"`
|
|
CorrelationID string `gorm:"type:varchar(64);index" json:"correlation_id"`
|
|
Source string `gorm:"type:varchar(64)" json:"source"`
|
|
Result string `gorm:"type:varchar(32)" json:"result"`
|
|
PayloadJSON string `gorm:"type:text" json:"payload_json"`
|
|
OccurredAt string `gorm:"type:varchar(32)" json:"occurred_at"`
|
|
CreatedAtMs int64 `gorm:"bigint;index" json:"created_at_ms"`
|
|
}
|
|
|
|
func (AgentCallbackEvent) TableName() string {
|
|
return "agent_callback_events"
|
|
}
|
|
|
|
type ListAgentCallbackEventsFilter struct {
|
|
DeploymentID string
|
|
TaskID string
|
|
CorrelationID string
|
|
Limit int
|
|
}
|
|
|
|
func InsertAgentCallbackEvent(row *AgentCallbackEvent) (bool, error) {
|
|
if DB == nil || row == nil {
|
|
return false, nil
|
|
}
|
|
if row.EventID == "" {
|
|
return false, errors.New("event_id is required")
|
|
}
|
|
var existing AgentCallbackEvent
|
|
if err := DB.Where("event_id = ?", row.EventID).First(&existing).Error; err == nil {
|
|
return false, nil
|
|
}
|
|
if row.IdempotencyKey != "" {
|
|
if err := DB.Where("idempotency_key = ?", row.IdempotencyKey).First(&existing).Error; err == nil {
|
|
return false, nil
|
|
}
|
|
}
|
|
if err := DB.Create(row).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// ListSwarmCallbackEventsAfter returns persisted swarm runtime events for one run
|
|
// (matched by deployment_id OR swarm_id) with an id-based `after` cursor for
|
|
// incremental polling (#45 events?after). Ordered oldest-first so the client can
|
|
// append; the caller uses the last returned Id as the next `after`. Reading from
|
|
// HM-persisted callback rows means this needs no live Swarm call.
|
|
func ListSwarmCallbackEventsAfter(userID, deploymentID, swarmID string, afterID, limit int) ([]AgentCallbackEvent, error) {
|
|
if DB == nil {
|
|
return nil, nil
|
|
}
|
|
q := DB.Model(&AgentCallbackEvent{})
|
|
switch {
|
|
case deploymentID != "" && swarmID != "":
|
|
q = q.Where("deployment_id = ? OR swarm_id = ?", deploymentID, swarmID)
|
|
case deploymentID != "":
|
|
q = q.Where("deployment_id = ?", deploymentID)
|
|
case swarmID != "":
|
|
q = q.Where("swarm_id = ?", swarmID)
|
|
default:
|
|
return nil, nil
|
|
}
|
|
// 防跨用户泄漏:即使 runtime_swarm_id/deployment_id 碰撞或误写,也按 user_id 收口(#45 复审 #3)。
|
|
if strings.TrimSpace(userID) != "" {
|
|
q = q.Where("user_id = ?", userID)
|
|
}
|
|
if afterID > 0 {
|
|
q = q.Where("id > ?", afterID)
|
|
}
|
|
if limit <= 0 || limit > 1000 {
|
|
limit = 200
|
|
}
|
|
var items []AgentCallbackEvent
|
|
err := q.Order("id asc").Limit(limit).Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func ListAgentCallbackEvents(f ListAgentCallbackEventsFilter) ([]AgentCallbackEvent, error) {
|
|
if DB == nil {
|
|
return nil, nil
|
|
}
|
|
q := DB.Model(&AgentCallbackEvent{})
|
|
if f.DeploymentID != "" {
|
|
q = q.Where("deployment_id = ?", f.DeploymentID)
|
|
}
|
|
if f.TaskID != "" {
|
|
q = q.Where("task_id = ?", f.TaskID)
|
|
}
|
|
if f.CorrelationID != "" {
|
|
q = q.Where("correlation_id = ?", f.CorrelationID)
|
|
}
|
|
limit := f.Limit
|
|
if limit <= 0 || limit > 1000 {
|
|
limit = 200
|
|
}
|
|
var items []AgentCallbackEvent
|
|
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
|
|
return items, err
|
|
}
|