mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 08:47:27 +00:00
## 缺陷
生产日志实证:`[security] module enabled (31 variables)`、`[device] ready:
34 tools, security=true`,691 个日志文件里 `denied by policy` 命中 **0 次**。
`executeToolCallInner` 里的策略闸读 `a.security`,而它**永远是 nil**:
codegraph_callers("SetSecurity") → 4 个调用方,全是 security_toolgate_test.go
全仓唯一给 a.security 赋值的行:security_profile.go(就在 SetSecurity 体内)
buildAgent 完全不读 k.Security;AgentConfig 里也没有 Security 字段
## 为什么判据全绿而生产是坏的
`security_toolgate_test.go` 的 4 条判据**自己调了 a.SetSecurity(...)** ——
它们把缺失的那根线补上了,所以永远绿。测的是「接线之后行为对」,
不是「生产有没有接线」。这是「机制 ≠ 接线」最纯粹的样本。
## 根因:模块边界没连上,不是少一行
`kernelmod/agent` 的 Requires() **没有声明 FacSecurity**。依赖图因此没有
理由把 security 的产物交给 agent —— 只有声明了的 device 拿到了。
即使不声明,security 也在第 1 层、agent 在第 4 层,`ks.Security` 恰好已被
填好,运行期照样能读到。**这正是它藏了整轮发布的原因**:那个"恰好"是偶然
(security 只依赖 config),不是设计。一旦 security 将来加了自己的依赖,
图不会拦,只会静默给出 nil。
所以只补 `agent.SetSecurity(k.Security)` 是错的:那会让代码读一个图不保证的
字段。必须同时改三处。
## 改动
① kernelmod/agent: Requires 加 FacSecurity(让图知道这条边)
② AgentConfig 加 Security 字段;New() 写 a.security;buildAgent 传 k.Security
③ Init 里 ks.Security == nil 时**响亮失败**(与 device 模块对 ks.Security
的检查同型)—— 不能把 nil 一路传下去变成静默放行
④ 子 agent 继承:resident / rtchild 各自 New(AgentConfig{...}),原先从构造起
a.security 就是 nil。它们是**最需要闸门**的执行体(驻留子长期无人盯、
rtchild 通话中一边听一边执行工具),却恰恰完全没有闸门。
## 判据(全部通过变异验证,删掉修复必红)
新增 internal/kernelmod/all/security_wiring_test.go(钉**装配**,不是钉注入):
| 判据 | 变异 |
|---|---|
| TestAgentDeclaresSecurityDependency | 从 Requires 删 FacSecurity ⇒ 红 |
| TestAgentAssemblyWiresSecurity | buildAgent 传 nil ⇒ 红;New() 不写 a.security ⇒ 红 |
| TestSubAgentsInheritSecurity | 删 resident/rtchild 的 Security ⇒ 红 |
| TestAgentAssemblyRefusesNilSecurity | 删 Init 的 nil 检查 ⇒ 红 |
`TestAgentAssemblyWiresSecurity` 走**真实登记表装配**(真 SQLite / 真配置库 /
真 20 模块 6 层),并对一个**未注册**的工具名配黑名单后调真实 `ExecuteTool`:
闸门生效 ⇒ "not permitted";闸门失效 ⇒ "does not exist"。两者文本不同,
无从混淆,因此不需要注册探针工具。
## 实测
生产形态探针(配置写入 `core.security.tool_deny=cmd_run` 后启动真实二进制):
[security] global policy applied: deny=[cmd_run] allow=[] sandbox=false
Agent.ExecuteTool("cmd_run") → denied (Tool cmd_run is not permitted:
tool "cmd_run" is blocked by security policy (denylist entry "cmd_run"))
分层未变(3+10+3+1+2+1 = 20 模块 6 层,digest 14b4f6f7),
build / vet / go test ./... / -race(kernelmod/all + agent/core + security) 全绿。
## 顺带
gofmt 漂移清零(原 1 个 rtchild_route_test.go)。
101 lines
4.0 KiB
Go
101 lines
4.0 KiB
Go
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` 从未被赋值」,于是补了
|
||
// `SetSecurity()`。但**生产装配链从来没调它** —— 唯一的调用方是 4 条测试。
|
||
// 那 4 条判据全绿,因为它们在测试里**手动**调了 `a.SetSecurity(...)`:
|
||
// 判据自己把缺失的那根线补上了,所以永远绿。
|
||
//
|
||
// ★ 第二轮(2026-10-30)才看清根因:`kernelmod/agent` 的 `Requires()`
|
||
// **没有声明 `FacSecurity`**,依赖图就没有理由把 security 的产物交给
|
||
// agent(只有声明了的 device 拿到了)。所以这不是"少一行赋值",
|
||
// 而是"模块边界没连上"。
|
||
//
|
||
// 现在的链路:
|
||
//
|
||
// security.Init → ks.Security
|
||
// → buildAgent 传 AgentConfig.Security
|
||
// → New() 写 a.security
|
||
// → 子 agent(resident/rtchild)继承 a.security
|
||
// → 本函数 → CheckTool
|
||
//
|
||
// 判据钉的是**装配**(kernelmod/all 的整机判据),不是本函数被手动调用后的行为。
|
||
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 注入安全运行时(**仅限测试与独立工具**)。
|
||
//
|
||
// ## 不要在生产装配里调它
|
||
//
|
||
// 生产路径是 `AgentConfig.Security`(构造期落位),因为那一条由依赖图
|
||
// 保证 `ks.Security` 已就绪。用 SetSecurity 事后补会造出一个图看不见的
|
||
// 隐式依赖 —— 正是本仓反复踩的那个坑。
|
||
//
|
||
// 保留它是因为 4 条进程内判据需要一个"只换安全运行时"的开关,
|
||
// 而它们不跑整机装配。
|
||
//
|
||
// ## 为什么允许 nil
|
||
//
|
||
// 测试与独立工具(memgc 等)不需要安全模块;传 nil 表示显式不启用。
|
||
func (a *Agent) SetSecurity(rt *security.Runtime) {
|
||
if a == nil {
|
||
return
|
||
}
|
||
a.security = rt
|
||
}
|