package gateway import ( "context" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict" ) // 本文件是 design/11 §4「敏感操作代审」的对外侧实现(裁决 13.4.3): // loop 的工具准入钩子判定出「该敏感操作已获(或未获)代审」后,组合根 // 把记录交给这里——① 落审计链 audit_logs(who/what/when/result), // ② 向 owner/admin 连接广播小字帧。判定与代审本身在 // internal/capability/restrict(Gate);本文件只管「记录 + 呈现」。 // wsApprovalNotice 是主代理代审小字的 WS 事件类型(design/11 §4)。 // 它携带内容(角色名、动作、文案),因此 design/11 §7.1 的帧门默认 // fail-closed:只投递给 owner + admin 连接,viewer 收不到,也不进那 7 个 // 状态机信号白名单。 const wsApprovalNotice = "approval_notice" // 代审在审计链里的 who/what/result 口径(design/11 §8): // who = 想动手的角色名,what = "approval.<工具名>",result = granted|denied。 // 代审动作批准/拒绝**各落一行**,因此前缀固定、结果二分。 const ( approvalAuditPrefix = "approval." approvalResultGrant = "granted" approvalResultDeny = "denied" ) // RecordApproval 记录一次主代理代审(design/11 §4 §8;裁决 13.4.3)。 // // ① 审计:把 who(角色名)/ what(approval.<工具名>)/ when / result // (granted / denied)追加进 audit_logs —— 代审动作批准与拒绝各落一行, // 供管理面板「可观测性 → 审计」与通知 feed 查询。 // ② 前端小字:广播 approval_notice 帧,携带 design/11 §4 的固定文案 // 「xxx(角色名) 想要干什么,我审查后批准了」(explain),以及工具名、 // 操作类别与动作摘要(detail,客户端可作副文本或忽略)。 // // 审计写入用 context.WithoutCancel:代审发生在一次 Run 内部,而 Run 的 ctx // 可能已被取消(用户中断)——已经发生的代审仍然必须留痕。审计存储坏掉时 // gateway.audit 只记一条日志,不影响代审已经成立的结论。 // // 方法对 nil 接收者安全(组合根在网关构建完成前调用它时不会 panic)。 func (s *Server) RecordApproval(ctx context.Context, ev restrict.ApprovalEvent) { if s == nil { return } if ctx == nil { ctx = context.Background() } result := approvalResultDeny if ev.Allowed { result = approvalResultGrant } s.audit(context.WithoutCancel(ctx), ev.Actor, approvalAuditPrefix+ev.Tool, result) s.hub.broadcast(wsEvent{ Type: wsApprovalNotice, SessionID: ev.SessionID, Data: map[string]any{ "actor": ev.Actor, "tool": ev.Tool, "op": string(ev.Op), "detail": ev.Detail, "explain": ev.Explain, "allowed": ev.Allowed, "at": ev.At.UTC().Format(time.RFC3339Nano), }, }) }