package restrict import ( "context" "log/slog" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/audit" ) // 本文件是 design/11 §4「敏感操作代审」的准入侧实现(裁决 13.4.3): // loop 的工具钩子(ToolLoopHooks.CallAdmit / BatchAdmit)调用 Gate.Admit, // 敏感调用经主代理代审、非敏感调用自主放行。落库与前端小字广播不在本包 // ——Gate 只产出记录(Decision.Approval),由组合根交给 gateway 落 audit_logs // 并广播 approval_notice 帧。 // Reviewer 执行一次主代理代审(design/11 §4)。 // // 生产实现适配 *audit.Approver(组合根注入,见 cmd/cyrene/wire_runtime.go); // 用窄接口是为了本包能用桩测试,不必起编排层。 type Reviewer interface { // ReviewAndApprove 审核归属 actor 的角色想做的 action,返回是否放行。 // describe 是给审查者看的动作说明。空白 actor/action/describe 或 ctx 取消 // 由实现返回错误;返回错误时门按 fail-closed 拦截。 ReviewAndApprove(ctx context.Context, actor, action, describe string) (allowed bool, err error) } // Resolver 从一次 Run 的上下文解析代审归属(design/11 §4 小字里的「xxx」): // sessionID = 该 Run 的会话(前端小字的投递目标,可为空表示不定位会话), // actor = 想动手的角色名。组合根用 loop.SessionFromContext + 该会话的角色解析。 type Resolver func(ctx context.Context) (sessionID, actor string) // ApprovalEvent 是一次代审的完整记录(design/11 §4 §8:who / what / when / // result + 前端小字)。组合根把它落进 audit_logs 并广播给前端。 type ApprovalEvent struct { // SessionID 是该次 Run 的会话 id(可为空)。 SessionID string // Actor 是想要动手的角色名(小字里的 xxx)。 Actor string // Tool 是敏感工具名(如 terminal)。 Tool string // Op 是敏感操作类别(design/11 §4 的 7 类:执行命令 / 修改文件 / 写数据 / // 对外发消息 / 访问密钥与隐私 / 改配置与权限 / 网络访问)。 Op SensitiveOp // Detail 是动作摘要(工具 + 关键参数,已截断)。 Detail string // Explain 是前端小字文案(design/11 §4 的固定句式)。 Explain string // Allowed 报告代审是否放行。 Allowed bool // At 是代审发生时间。 At time.Time } // Decision 是一次工具调用准入的结论。 type Decision struct { // Allow 报告该调用是否放行。 Allow bool // Reason 是拦截时回灌给模型的软提示(放行时为空),体例与既有安全门的 // "not_allowed: ..." 一致,使模型能重新评估而不是硬崩。 Reason string // Approval 是本次代审的记录;非敏感调用为 nil。调用方负责记录它 // (审计落库 + 前端小字),放行与拦截都要记(design/11 §8)。 Approval *ApprovalEvent } // Gate 是 loop 的工具准入门(design/11 §4;裁决 13.4.3):敏感操作走主代理 // 代审,其余自主放行。它对并发使用安全(无可变状态)。 type Gate struct { reviewer Reviewer resolve Resolver log *slog.Logger } // GateOption 配置一个 Gate。 type GateOption func(*Gate) // WithGateLogger 让门把每次代审结论与拦截原因记到 l(来源标 // capability/restrict)。为 nil 时不记日志。 func WithGateLogger(l *slog.Logger) GateOption { return func(g *Gate) { g.log = logger.WithSource(l, "capability/restrict") } } // NewGate 构建代审门。reviewer 为 nil 时门是 fail-closed 的:所有敏感调用 // 一律拦截并留下一条「未获代审」的拒绝记录——绝不因为没有代审者就静默放行。 // resolve 为 nil 时归属解析为空(actor 为空会让小字退化,但判定不受影响)。 func NewGate(reviewer Reviewer, resolve Resolver, opts ...GateOption) *Gate { g := &Gate{reviewer: reviewer, resolve: resolve} for _, opt := range opts { opt(g) } return g } // Admit 判定一次工具调用:非敏感立即放行(不产生代审记录),敏感调用先做 // 主代理代审再决定放行或拦截。返回的 Decision.Approval 非 nil 时必须由调用方 // 记录(design/11 §8:代审动作批准/拒绝各落一行)。 func (g *Gate) Admit(ctx context.Context, name string, args map[string]any) Decision { op, detail, sensitive := SensitiveCall(name, args) if !sensitive { return Decision{Allow: true} } sessionID, actor := g.resolveCtx(ctx) action := string(op) if detail != "" { action += ":" + detail } ev := &ApprovalEvent{ SessionID: sessionID, Actor: actor, Tool: name, Op: op, Detail: detail, At: time.Now(), } if g == nil || g.reviewer == nil { ev.Explain = approvalExplain(actor, false) g.logf("敏感操作无代审者,按 fail-closed 拦截", name, actor, nil) return Decision{Allow: false, Reason: approvalDeniedReason(name), Approval: ev} } allowed, err := g.reviewer.ReviewAndApprove(ctx, actor, action, detail) if err != nil { ev.Explain = approvalExplain(actor, false) g.logf("代审失败,按 fail-closed 拦截", name, actor, err) return Decision{Allow: false, Reason: approvalDeniedReason(name), Approval: ev} } ev.Allowed = allowed ev.Explain = approvalExplain(actor, allowed) g.logf("敏感操作代审", name, actor, nil) if !allowed { return Decision{Allow: false, Reason: approvalDeniedReason(name), Approval: ev} } return Decision{Allow: true, Approval: ev} } // resolveCtx 解析归属;Resolver 为 nil 时返回两个空串(判定不依赖它们)。 func (g *Gate) resolveCtx(ctx context.Context) (string, string) { if g == nil || g.resolve == nil { return "", "" } return g.resolve(ctx) } // logf 记一条带来源的代审日志:只记工具名与角色名,**不记动作摘要** // (参数里可能有秘密;摘要只进本包的 ApprovalEvent 与审计 who/what/result 链)。 func (g *Gate) logf(msg, tool, actor string, err error) { if g == nil || g.log == nil { return } args := []any{"tool", tool, "actor", actor} if err != nil { args = append(args, "error", err) } g.log.Info(msg, args...) } // approvalDeniedReason 是敏感操作未获代审放行时回灌给模型的软提示, // 与 loop 安全门文案(not_allowed: ...)同一体例。 func approvalDeniedReason(name string) string { return "not_approved: sensitive tool " + name + " requires the main agent's approval" } // approvalExplain 产出前端小字文案。它复用 orchestration/audit 的固定句式 // (design/11 §4:「xxx 想要干什么,我审查后批准了」),使代审成功与 // 「无代审者/代审报错」两条路径的小字完全一致,不出现两份文案。 func approvalExplain(actor string, allowed bool) string { return audit.Approval{Actor: actor, Allowed: allowed}.Explain() }