package core // 插件声明限式的**生效点**:inputch 输入进来时激活,轮次内据此判定。 // // ## 为什么在这里(而不是在工具调用时) // // 插件在注入时带上 `security_profile`,那一刻是**唯一**能确定 // "这条输入属于哪个身份档位"的时刻。等 LLM 决定调工具时,插件早已 // 不在调用栈上(异步通道 + 多轮工具循环),档位信息就断了。 // // 所以:输入到达 → 激活档位(带 TTL)→ 该轮的工具/内容判定都读它。 // // ## 缺省语义 // // 没有携带 `security_profile` = **不使用档位**(沿用内核默认策略)。 // 这是可选参数:不声明档位的插件行为与之前完全一致。 // // 携带了**未声明**的档位名 ⇒ 记日志并回退到默认策略,但**不改写输入**: // 注入是 fire-and-forget,插件看不到内核错误;至少要在日志里留下 // 「你声明的档位没生效」的痕迹(与 cleaner_name 同一纪律)。 import ( "log" "github.com/JianFeeeee/HologramAgent/internal/security" ) // activateSecurityProfile 按输入 payload 激活插件声明的安全档位。 // // 返回被激活的档位名(空 = 未使用档位),仅供日志/诊断。 func (a *Agent) activateSecurityProfile(payload map[string]interface{}) string { if a == nil || a.security == nil || payload == nil { return "" } name, _ := payload["security_profile"].(string) if name == "" { return "" } if err := a.security.ActivateProfile(name, 0); err != nil { // 响亮但不断流:拼错档位名不该让整条输入丢失, // 但也绝不能静默 —— 那等于悄悄降级到默认策略。 log.Printf("[agent] input declared security_profile=%q but activation failed: %v", name, err) return "" } return name } // securityDecisionFor 用当前活跃档位 + 主体判定某工具是否可用。 // // 工具执行前的统一检查点:调用方拿到 !Allowed 时应把 Reason 原样回给模型 // (统一拒绝语义 —— 模型据此能修正,而不是反复重试同一调用)。 // // ★ 2026-10-29:本函数此前是**死代码**(`a.security` 从未被赋值 ⇒ 永远 // 走 nil 分支 ⇒ 永远放行),`Policy.CheckTool` 因此对工具执行链完全不生效。 // 修复分两步:① 补 `SetSecurity` 注入通道;② 在 `executeToolCallInner` // 的**所有实际执行分支之前**调用本函数。 func (a *Agent) securityDecisionFor(tool string) security.Decision { if a == nil || a.security == nil { return security.Decision{Allowed: true} } return a.security.CheckTool(tool) } // SetSecurity 注入安全运行时(装配期调用,幂等)。 // // ## 为什么需要显式注入点 // // `Agent.security` 曾经是**只有测试赋过值**的字段:生产装配链里没有任何 // 地方写它,于是 `securityDecisionFor` 恒定放行。这不是"策略写错了", // 而是"策略根本没接上",而两种故障在日志上看起来一样(都是没有拒绝记录)。 // // ## 为什么允许 nil // // 测试与独立工具(memgc 等)不需要安全模块;传 nil 表示显式不启用。 func (a *Agent) SetSecurity(rt *security.Runtime) { if a == nil { return } a.security = rt }