mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
## 用户裁定
> secutiry 的实现也是错误的,所有共享数据统一挂载一个 kernel 提供的
> 挂载点上暴露。
> store也应该调用security的加密能力!
## 缺陷
security 包原有**三份**独立包级单例,各自被 InitXxx/SetXxx 装入、
各自有访问器,三份都绕开了内核统一挂载点 kstate.State:
global *Store 变量库(凭据别名) Global()
runtime *Runtime 安全防火墙 RuntimeGlobal()
globalCodec *Codec 配置落盘加密 ConfigCodec()
## 为什么单例是错的(不是"多一个全局变量")
本 agent 的 a.security 与 security.Global() 是**两个独立变量**。
正常装配下它们恰好指向同一份数据,但"恰好"不是保证:
· 测试替身:测试装单例、被测代码用 agent 字段 ⇒ 测的与跑的不是同一份
· 多 Runtime:两个内核实例/热重载时单例只有一份,另一方被覆盖
· 装配顺序:单例装晚了,先注册的 provider 就没被装饰
第三条**曾真实存在**:ProviderManager.Register 在注册时读单例套装饰器,
而 llm 模块原本不声明 FacSecurity 依赖 —— 顺序正确纯属巧合。
改动过程中判据如实报红 640 次泄漏。
## 本次做的(T3.1 + T3.3)
### 删 global *Store → 注入
变量库的唯一来源是 security 模块产出的 Runtime,经注入面分发:
AgentConfig.Security → a.security.Store() → a.secretStore()
ProviderManager.SetSecretStore
plugin.Registry.SetSecretStore → sdk.NewSecretsAPI(store)
core 的 4 处 security.Global()(output/toolcall/tooldefs/secret_tools)
改为 a.secretStore();context 与 stageHost 改为注入字段;
provider 装饰器改为读注入的 Store。
### 删 runtime *Runtime → 注入
plugin.Registry.SetSecurityRuntime → sdk.NewSecurityAPI(name, rt)
→ sdk.newEventsAPI(name, bus, rt)
Prepare 的幂等缓存改为 kernelmod/security 私有变量 ——
装配器私有,不再是任何包都能取的公开访问器。
### 顺带删死代码
security.FillToolArgs / FillPayload 生产调用者为 0(StageHost 已自己
用注入的 Store 做同一件事),连同只测它们的判据一起删除。
### llm 模块补 FacSecurity 依赖
这是**真实存在**的依赖:注册 provider 时要套出向装饰器,而装饰器需要
变量库。此前靠"单例先装了"掩盖了顺序错误。
## 判据(均经变异验证)
TestNoPackageLevelSingletons 源码扫描;加回 var global ⇒ 红(实测)
TestMustStoreDoesNotRegisterGlobally 两次 MustStore 必须得到不同实例
TestStoreEncryptsWithOwnKey 落盘无明文 + 重载可解
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(git worktree) build + 3 包测试全绿
## 未做(见 kernelplan.md T3.2)
globalCodec 收敛 + Store 复用 Codec
还牵涉 config 的 6 处调用点与**存量密文惰性迁移**,单独一步做。
TestNoPackageLevelSingletons 的禁表暂不含 globalCodec ——
半成品状态长期红的判据会被人习惯性忽略,那时它就等于不存在。
224 lines
8.6 KiB
Go
224 lines
8.6 KiB
Go
package api
|
||
|
||
// 装配顺序判据。
|
||
//
|
||
// ## 为什么这个顺序值得单独立判据
|
||
//
|
||
// 安全模块的初始化必须在**任何 provider 注册之前**。装晚了的后果是:
|
||
// 先注册的那批 provider 没被装饰 ⇒ **静默漏掉一条出网路径**,
|
||
// 而且没有任何报错、日志也正常 —— 属于最难发现的那类失效。
|
||
//
|
||
// 而这个坑在本仓是真实存在的:
|
||
// · `cmd/homed/main.go` 的 initLLMProviders(主路径)
|
||
// · `internal/sdk/llm_impl.go` 的 ReloadFromConfig(配置热重载)
|
||
// · `cmd/homed/failback.go` 的 runFailbackRecovery(降级救援)
|
||
//
|
||
// 三个地方都会 Register。前两个在 security 之后;failback 也必须之后
|
||
// (main.go 里它是第 174 行,安全模块是 162 行)。
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||
)
|
||
|
||
// ★★ 判据:**晚注册的 provider 也自动被装饰**(因为装饰挂在 Register 上)。
|
||
//
|
||
// 这条钉住的是"挂载点选择正确":如果装饰挂在某个初始化函数里
|
||
// (而不是 Register),那么后注册的 provider 就会漏掉。
|
||
func TestLateRegistrationStillDecorated(t *testing.T) {
|
||
dir := t.TempDir()
|
||
store, err := security.MustStore(security.Options{
|
||
Path: filepath.Join(dir, "v.json"),
|
||
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
|
||
Enabled: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
const tok = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
m := NewProviderManager()
|
||
// ★ 注入必须先于 Register(装饰器在 Register 时套上)。
|
||
m.SetSecretStore(store)
|
||
|
||
// ① 先注册一个
|
||
var first string
|
||
m.Register("early", &recordingProvider{name: "early", capture: &first})
|
||
|
||
// ② 模拟"热重载":Reset 后注册全新一批(ReloadFromConfig 的真实流程)
|
||
m.Reset()
|
||
var second string
|
||
m.Register("late", &recordingProvider{name: "late", capture: &second})
|
||
|
||
// ③ 再模拟 failback 的 rescue provider
|
||
var third string
|
||
m.Register("rescue", &recordingProvider{name: "rescue", capture: &third})
|
||
|
||
// 三个都必须被装饰
|
||
for _, tc := range []struct {
|
||
name string
|
||
got *string
|
||
}{{"late", &second}, {"rescue", &third}} {
|
||
if _, err := m.Get(tc.name).Chat(context.Background(),
|
||
&CompletionRequest{Messages: []Message{{Role: "user", Content: "t=" + tok}}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.Contains(*tc.got, tok) {
|
||
t.Errorf("provider %s 未被装饰(明文泄漏): %s", tc.name, *tc.got)
|
||
}
|
||
}
|
||
_ = first
|
||
if store.Count() == 0 {
|
||
t.Error("应登记变量")
|
||
}
|
||
}
|
||
|
||
// ★ 判据:**已装饰的不再包第二层**(避免热路径白跑)。
|
||
func TestNoDoubleWrapping(t *testing.T) {
|
||
dir := t.TempDir()
|
||
store, err := security.MustStore(security.Options{
|
||
Path: filepath.Join(dir, "v.json"),
|
||
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
|
||
Enabled: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
m := NewProviderManager()
|
||
// ★ 注入必须先于 Register(装饰器在 Register 时套上)。
|
||
m.SetSecretStore(store)
|
||
base := &recordingProvider{name: "x", capture: new(string)}
|
||
m.Register("a", base)
|
||
inner := m.Get("a")
|
||
|
||
// 把已装饰的实例再注册一次(某些装配路径会这么干)
|
||
m.Register("b", inner)
|
||
if _, isDecorator := m.Get("b").(*secureProviderDecorator); !isDecorator {
|
||
t.Fatal("应当仍是装饰实例")
|
||
}
|
||
// 再包一层的话,类型仍是装饰器但会嵌套 —— 用指针比较确认是同一个
|
||
if m.Get("b") != inner {
|
||
t.Error("已装饰的实例不应被再包一层(会白跑一遍正则扫描)")
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:**main.go 里安全模块的初始化必须早于 initLLMProviders**。
|
||
//
|
||
// 这是源码级判据(读文件而不是跑逻辑),因为顺序错误在单测里
|
||
// 构造不出来(main 的装配流程没法在测试里复现)。
|
||
// 源码判据在此是有意为之:它盯的是一个**真实的、曾经坑过的**装配顺序。
|
||
func TestMainInitsSecurityBeforeProviders(t *testing.T) {
|
||
src, err := os.ReadFile("../../../cmd/homed/main.go")
|
||
if err != nil {
|
||
t.Skipf("读不到 main.go(相对路径变了?): %v", err)
|
||
}
|
||
text := string(src)
|
||
|
||
// ★ 2026-10-26:安全装配搬进正式内核模块 internal/kernelmod/security。
|
||
// ★ 2026-11-04:包级单例删除,`InitGlobal` 改为 `MustStore`
|
||
// (构造并加载 Store,不注册全局)。
|
||
//
|
||
// 判据必须同时盯住**两个真实点**:
|
||
// · 模块实现里的 Prepare(装配时机与产物:ks.Security)
|
||
// · 模块实现里的 MustStore(Prepare 不是空壳)
|
||
//
|
||
// 只盯其中一个都会变成"改了实现就静默变绿"。
|
||
modSrc, modErr := os.ReadFile("../../kernelmod/security/mod.go")
|
||
if modErr != nil {
|
||
t.Fatalf("读不到 security 模块实现: %v", modErr)
|
||
}
|
||
modText := string(modSrc)
|
||
if !strings.Contains(modText, "sec.MustStore(") {
|
||
t.Fatalf("security 模块实现里找不到 sec.MustStore —— " +
|
||
"Prepare() 退化成空壳,安全审查不会生效")
|
||
}
|
||
if !strings.Contains(modText, "ks.Security = ") {
|
||
t.Fatalf("security 模块没有把 Runtime 挂到 ks.Security —— " +
|
||
"变量库/防火墙对其它模块不可见")
|
||
}
|
||
// 模块自身的装配点(Prepare 在 main.go 或模块内被调用)。
|
||
iSec := strings.Index(modText, "func Prepare(")
|
||
if iSec < 0 {
|
||
t.Fatalf("security 模块里找不到 Prepare —— 装配入口没了")
|
||
}
|
||
_ = text
|
||
// ★ 锚点已随"内核模块化"改写(2026-10-16)。
|
||
//
|
||
// 原先锚 `initLLMProviders(cfg,`(那时它直接写在 main 里)。
|
||
// 模块化后 **provider 注册搬进了 llm 模块**(`internal/kernelmod/llm`),
|
||
// 它在 `RunKernelModules` 内部执行 ⇒ 旧的字符串在 main.go 里**已经不存在**,
|
||
// 判据因"找不到"而失败(假红)。
|
||
//
|
||
// **注意:这不是放宽判据,而是把锚点换到新的真实装配点。**
|
||
// 不变量本身没变:安全模块必须早于**任何 provider 注册**。
|
||
// 新的等价表述:安全模块早于 `RunKernelModules`(llm 模块在它内部)。
|
||
//
|
||
// 为防止"换个锚点就变绿"式的偷懒,另加两条硬校验(见下)。
|
||
iProv := strings.Index(text, "RunKernelModules(")
|
||
if iSec < 0 {
|
||
t.Fatal("main.go 里找不到 securitymod.Prepare / security.InitGlobal —— 安全模块根本没被装配")
|
||
}
|
||
if iProv < 0 {
|
||
t.Fatal("main.go 里找不到 RunKernelModules —— 内核模块装配点没了")
|
||
}
|
||
if iSec > iProv {
|
||
t.Fatalf("装配顺序错误:security.InitGlobal 在第 %d 字节,RunKernelModules 在第 %d 字节\n"+
|
||
"必须先初始化安全模块 —— 内核模块装配时会注册 LLM provider,"+
|
||
"晚于它则 provider 不会被装饰(静默漏掉一条出网路径)",
|
||
iSec, iProv)
|
||
}
|
||
// ★ 加固 ①:provider 注册**确实**在内核模块里(否则新锚点就是空的 ——
|
||
// 那等于把判据锚在一个与 provider 无关的位置上)。
|
||
// 判据:kernelmod/llm 模块必须注册,且其 Init 里出现 initLLMProviders。
|
||
llmSrc, lerr := os.ReadFile("../../kernelmod/llm/mod.go")
|
||
if lerr != nil {
|
||
t.Fatalf("读不到 llm 模块(provider 注册应在这里): %v", lerr)
|
||
}
|
||
if !strings.Contains(string(llmSrc), "initLLMProviders") {
|
||
t.Fatal("★ llm 模块里没有 initLLMProviders —— " +
|
||
"那么上面锚 RunKernelModules 就与 provider 注册无关了(判据会变空)")
|
||
}
|
||
// ★ 加固 ②:llm 模块必须是**真注册过**的(不是死代码)
|
||
allSrc, aerr := os.ReadFile("../../kernelmod/all/all.go")
|
||
if aerr != nil {
|
||
t.Fatalf("读不到模块总表: %v", aerr)
|
||
}
|
||
if !strings.Contains(string(allSrc), "kernelmod/llm") {
|
||
t.Fatal("★ llm 模块没被 import ⇒ 它的 init() 不跑 ⇒ provider 永不注册")
|
||
}
|
||
|
||
// failback 也要在之后
|
||
iFail := strings.Index(text, "runFailbackRecovery")
|
||
if iFail >= 0 && iSec > iFail {
|
||
t.Error("runFailbackRecovery 在安全模块之前 —— rescue provider 会漏装饰")
|
||
}
|
||
}
|
||
|
||
// ★ 判据:阶段断言 —— 源码里装饰发生在 Register 内部(挂载点正确)。
|
||
func TestDecorateHappensInsideRegister(t *testing.T) {
|
||
src, err := os.ReadFile("provider.go")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
text := string(src)
|
||
// Register 函数体内必须出现 WrapProviderForSecurity
|
||
i := strings.Index(text, "func (m *ProviderManager) Register(")
|
||
if i < 0 {
|
||
t.Fatal("找不到 Register")
|
||
}
|
||
// 取到下一个顶层 func 为止
|
||
rest := text[i:]
|
||
if j := strings.Index(rest[1:], "\nfunc "); j > 0 {
|
||
rest = rest[:j+1]
|
||
}
|
||
if !strings.Contains(rest, "WrapProviderForSecurity") {
|
||
t.Fatal("Register 函数体内没有装饰调用 —— 装饰被挪走了(后注册的 provider 会漏掉)")
|
||
}
|
||
}
|