package loop import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool" ) // auditToolRejectNotAllowed 是群聊/陌生人环境中被 // design/11 §3 安全门拒绝的工具调用的审计 "what"。 const auditToolRejectNotAllowed = "tool.reject.not_allowed" // AuditEvent 向审计 sink 上报被安全门拦截的工具拒绝 // (design/11 §3 §8)。循环在受限(群聊/陌生人)环境中拒绝 // 工具调用时发出一个,使该拒绝可审计,且 // 前端可以渲染「xx 工具在群聊被拒」的小字。它经 // Options.OnAudit 接到 gateway 审计轨迹。 type AuditEvent struct { // Who 是实施行为的 actor 环境标签("group_member"/"stranger")。 Who string // What 是操作,如 "tool.reject.not_allowed"。 What string // Result 是结果详情 —— 指名被拒工具的 not_allowed 提示。 Result string } // restrictSchemas 仅返回 ctx 上会话环境可调用的工具 schema, // 依据 design/11 §3 安全门:群聊/陌生人会话只能 // 调用其可编辑标记允许该环境的工具(默认 // 关闭),而 owner 环境保留全集。它是对 capacity // 允许集视图的最终裁决 —— 是安全红线而非便利 // 提示 —— 因此它最后运行,且只能收缩暴露面。 func (l *Loop) restrictSchemas(ctx context.Context, d *tool.Dispatcher, schemas []model.ToolSpec) []model.ToolSpec { policy := d.PolicyEngine(sessionEnvFrom(ctx).ActorEnv) kept := make([]model.ToolSpec, 0, len(schemas)) for _, s := range schemas { if policy.Allow(s.Name) { kept = append(kept, s) } } return kept } // applyToolSecurityGate 拦截所有工具在 ctx 会话环境中 // 不被允许的调用(design/11 §3)。它就地把 actions 改写, // 把每个可运行的调用改为携带 not_allowed 提示的跳过, // 并为该拒绝发出一个 AuditEvent。nil 接收者或 nil dispatcher 为空操作。 func (l *Loop) applyToolSecurityGate(ctx context.Context, d *tool.Dispatcher, calls []agent.ToolCall, actions []toolCallAction) { env := sessionEnvFrom(ctx) policy := d.PolicyEngine(env.ActorEnv) for i := range calls { if !actions[i].run { continue } if policy.Allow(calls[i].Name) { continue } reason := notAllowedReason(calls[i].Name) actions[i] = toolCallAction{run: false, reason: reason} l.emitAudit(AuditEvent{ Who: string(env.ActorEnv), What: auditToolRejectNotAllowed, Result: reason, }) } } // notAllowedReason 是受限(群聊/陌生人)会话调用其可编辑 // 标记拒绝该环境的工具时回灌给模型的软提示 // (design/11 §3)。它是软结果 —— 注入对话, // 绝不 panic 或硬崩溃 —— 使模型能重新评估。 func notAllowedReason(name string) string { return "not_allowed: tool " + name + " is not allowed in a group/stranger conversation" } // emitAudit 把 AuditEvent 转发给配置的 OnAudit sink。未接线 // sink 时为空操作,因此在未挂审计 sink 的叶子测试中 // 安全门仍能工作(软 not_allowed 结果)。 func (l *Loop) emitAudit(ev AuditEvent) { if l.onAudit == nil { return } l.onAudit(ev) }