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 ——
半成品状态长期红的判据会被人习惯性忽略,那时它就等于不存在。
216 lines
6.5 KiB
Go
216 lines
6.5 KiB
Go
package proc
|
||
|
||
// 端到端:插件的**变量库**能力(SDK `Secrets()`)。
|
||
//
|
||
// ## 这条链路为什么要真机验证
|
||
//
|
||
// 它与 settings 的 setSecret 是**同一个坑的翻版**:新增 proc method 要改
|
||
// **4 个地方**(协议常量 / 外层路由 / 内层分发 / 能力表),少一处就报
|
||
// "未知 method"。单测只能证明各段对,证不了接上了。
|
||
//
|
||
// 而且它跨了**三层**:
|
||
//
|
||
// ① 插件调 sdk.Secrets().Store
|
||
// ② 模板 procSecrets → RPC "secrets.store"
|
||
// ③ 内核 coreHandler.secrets → sdk.SecretsAPI(secrets_impl)
|
||
// ④ → internal/security.Global() 单例(真加密落盘)
|
||
//
|
||
// ## 为什么要有这个 API(去重的意义)
|
||
//
|
||
// 在此之前 commondev 插件自带一套 AES-256-GCM 存储,与内核完全重复。
|
||
// 两套库**互不可见**是这个设计最有害的地方 —— 现在统一到一个单例。
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
|
||
internalSDK "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||
internalSecurity "github.com/JianFeeeee/HologramAgent/internal/security"
|
||
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||
)
|
||
|
||
// secretsE2EPlugin 用真实模板编译:只做变量库往返。
|
||
const secretsE2EPlugin = `package main
|
||
|
||
import (
|
||
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||
)
|
||
|
||
type secPlugin struct{ name string }
|
||
|
||
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||
return &secPlugin{name: name}, nil
|
||
}
|
||
|
||
func (p *secPlugin) Name() string { return p.name }
|
||
func (p *secPlugin) Stop() error { return nil }
|
||
|
||
func (p *secPlugin) Start(s *sdk.PluginSDK) error {
|
||
s.RegisterTool("e2e_secrets", sdk.ToolDef{
|
||
Description: "变量库往返",
|
||
Parameters: map[string]interface{}{"type": "object"},
|
||
}, func(args map[string]interface{}) (interface{}, error) {
|
||
sec := s.Secrets()
|
||
if sec == nil || !sec.Available() {
|
||
return "UNAVAILABLE", nil
|
||
}
|
||
alias, err := sec.Store("e2e-secret-value-9876543210", "e2e")
|
||
if err != nil {
|
||
return "STORE_ERR: " + err.Error(), nil
|
||
}
|
||
// ★ 只应拿到别名,拿不到明文
|
||
if alias == "" {
|
||
return "EMPTY_ALIAS", nil
|
||
}
|
||
out, n, _ := sec.Fill("token=@{" + alias + "}")
|
||
if n != 1 {
|
||
return "FILL_MISS", nil
|
||
}
|
||
return out, nil
|
||
})
|
||
return nil
|
||
}
|
||
`
|
||
|
||
// ★★ 判据:真实模板插件的变量库往返全链路可用。
|
||
func TestE2E_RealTemplateSecretsRoundTrip(t *testing.T) {
|
||
bin := buildPluginWithRealTemplate(t, secretsE2EPlugin)
|
||
|
||
dir := t.TempDir()
|
||
setTestCodec(t)
|
||
// 变量库(2026-11-04:不再有进程级单例,改为显式持有并注入)
|
||
store, err := internalSecurity.MustStore(internalSecurity.Options{
|
||
Path: filepath.Join(dir, "variables.json"),
|
||
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
|
||
Enabled: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_ = internalConfig.NewConfigRegistry
|
||
|
||
host, err := NewHost()
|
||
if err != nil {
|
||
t.Fatalf("NewHost: %v", err)
|
||
}
|
||
defer host.Close()
|
||
|
||
core := newFakeCore()
|
||
core.secretsAPI = internalSDK.NewSecretsAPI(store)
|
||
p := New("e2e", bin, dir, nil, host, nil)
|
||
if err := p.Start(core); err != nil {
|
||
t.Fatalf("Start: %v", err)
|
||
}
|
||
defer p.Close()
|
||
|
||
core.mu.Lock()
|
||
handler, ok := core.tools["e2e_secrets"]
|
||
core.mu.Unlock()
|
||
if !ok {
|
||
t.Fatal("插件未注册 e2e_secrets 工具")
|
||
}
|
||
|
||
res, err := handler(map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("调用失败: %v", err)
|
||
}
|
||
got, _ := res.(string)
|
||
if strings.HasPrefix(got, "UNAVAILABLE") || strings.HasPrefix(got, "STORE_ERR") ||
|
||
strings.HasPrefix(got, "EMPTY_ALIAS") || strings.HasPrefix(got, "FILL_MISS") {
|
||
t.Fatalf("变量库往返失败: %s", got)
|
||
}
|
||
// Fill 应把别名换回明文
|
||
if !strings.Contains(got, "e2e-secret-value-9876543210") {
|
||
t.Fatalf("Fill 未换回明文: %q", got)
|
||
}
|
||
// 变量库里应真的登记了
|
||
if store.Count() != 1 {
|
||
t.Fatalf("变量库应有 1 条,实际 %d", store.Count())
|
||
}
|
||
t.Logf("✅ 插件经内核变量库完成往返: %s", got)
|
||
}
|
||
|
||
// ★★ 判据:插件写入的是**内核那一个**变量库(不是自己又建了一套)。
|
||
//
|
||
// 这是去重的核心断言:插件 Store 之后,内核侧能立刻看到。
|
||
func TestE2E_PluginAndKernelShareOneVault(t *testing.T) {
|
||
bin := buildPluginWithRealTemplate(t, secretsE2EPlugin)
|
||
|
||
dir := t.TempDir()
|
||
setTestCodec(t)
|
||
st, err := internalSecurity.MustStore(internalSecurity.Options{
|
||
Path: filepath.Join(dir, "variables.json"),
|
||
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
|
||
Enabled: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
host, _ := NewHost()
|
||
defer host.Close()
|
||
core := newFakeCore()
|
||
core.secretsAPI = internalSDK.NewSecretsAPI(st)
|
||
p := New("e2e", bin, dir, nil, host, nil)
|
||
if err := p.Start(core); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer p.Close()
|
||
|
||
core.mu.Lock()
|
||
handler := core.tools["e2e_secrets"]
|
||
core.mu.Unlock()
|
||
if _, err := handler(map[string]interface{}{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// ★ 内核侧(同一个单例)应看到插件登记的那条
|
||
infos := st.List()
|
||
if len(infos) != 1 {
|
||
t.Fatalf("内核单例应有 1 条(插件写的),实际 %d —— 说明插件自建了一套库", len(infos))
|
||
}
|
||
if infos[0].Note != "e2e" {
|
||
t.Errorf("备注未透传: %q", infos[0].Note)
|
||
}
|
||
// 内核的 Fill 也能用它
|
||
if out, n, _ := st.Fill("x=@{" + infos[0].Alias + "}"); n != 1 ||
|
||
!strings.Contains(out, "e2e-secret-value") {
|
||
t.Errorf("内核 Fill 用不了插件登记的变量: n=%d out=%q", n, out)
|
||
}
|
||
t.Logf("✅ 插件与内核共用同一个变量库(别名 %s)", infos[0].Alias)
|
||
}
|
||
|
||
// ★ 判据:安全模块未启用时 Available()==false(而非 nil panic)。
|
||
func TestE2E_SecretsUnavailableWhenDisabled(t *testing.T) {
|
||
bin := buildPluginWithRealTemplate(t, secretsE2EPlugin)
|
||
|
||
dir := t.TempDir()
|
||
|
||
host, _ := NewHost()
|
||
defer host.Close()
|
||
core := newFakeCore()
|
||
// 未启用安全模块 ⇒ 传 nil(Available() 必须为 false,且不得 panic)
|
||
core.secretsAPI = internalSDK.NewSecretsAPI(nil)
|
||
p := New("e2e", bin, dir, nil, host, nil)
|
||
if err := p.Start(core); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer p.Close()
|
||
|
||
core.mu.Lock()
|
||
handler := core.tools["e2e_secrets"]
|
||
core.mu.Unlock()
|
||
res, err := handler(map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("不该报错(应优雅报告不可用): %v", err)
|
||
}
|
||
if got, _ := res.(string); got != "UNAVAILABLE" {
|
||
t.Fatalf("未启用时应报 UNAVAILABLE,实际 %q", got)
|
||
}
|
||
_ = os.Remove
|
||
_ = pubsdk.SecretInfo{}
|
||
}
|