170 lines
6.8 KiB
Go
170 lines
6.8 KiB
Go
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()
|
||
}
|