Files
HomeAgent/internal/sdk/secrets_impl.go
JianFeeeee 47afd67ca1 refactor(security): 删除三份包级单例,共享数据统一挂 kstate
## 用户裁定

> 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 ——
  半成品状态长期红的判据会被人习惯性忽略,那时它就等于不存在。
2026-11-04 11:57:48 +08:00

120 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
// SecretsAPI 的内核实现:桥接到注入的变量库(`*security.Store`)。
//
// ## 为何是"桥接"而不是"另建一套"
//
// 变量库是**全进程唯一的一份**:主/子/驻留子 agent 共享同一套别名表
// (设计 S10)。插件若自建一套,两库互不可见 —— 内核拦截时登记的变量
// 插件看不到,插件登记的拦截又不认识,同一个凭据可能两处各存一份而
// 使用者不知哪个权威。
//
// 所以这个实现只是把调用转发给注入的 Store。**没有第二份存储**。
//
// ## 注入而不是读单例(2026-11-04)
//
// 此前读 `security.Global()` 包级单例。那个单例绕开了内核的统一挂载点
// (kstate.State.Security),已删除。现在由 plugin.Registry 从
// ks.Security 取出 Store 传进来 —— 单一事实来源、单一注入路径。
//
// ## 未启用时的行为
//
// 注入的 Store 为 nil 时**不返回 nil API**,而是返回一个 Available()
// 为 false 的实现。理由:nil 会让插件到处判空,而判空漏一处就 panic
// (本仓在 `*SensitiveInterceptor` 上吃过这个亏)。给一个明确的
// "不可用" 实现,插件只需问 `Available()`。
import (
"fmt"
internalSecurity "github.com/JianFeeeee/HologramAgent/internal/security"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// secretsImpl 实现 pubsdk.SecretsAPI。
type secretsImpl struct{ store *internalSecurity.Store }
// NewSecretsAPI 构造变量库接口(内核装配时注入给每个插件)。
func NewSecretsAPI(store *internalSecurity.Store) pubsdk.SecretsAPI {
return &secretsImpl{store: store}
}
func (s *secretsImpl) Available() bool { return s.store != nil }
// AvailableForRPC 是 proc 桥用来**明确报告**可用性的入口。
//
// ★ 为什么不能靠 List 推断:未启用时 List 返回空列表(**不报错**),
// 于是"库是空的"与"库不存在"在 RPC 层长得一样 —— 实测踩到:
// 插件的 Available() 误判为 true,接着 Store 才失败,错误信息变成
// "存储失败"而不是"能力不可用",排查时方向完全错。
func (s *secretsImpl) AvailableForRPC() bool { return s.Available() }
func (s *secretsImpl) Store(value, note string) (string, error) {
st := s.store
if st == nil {
// ★ 响亮失败:不静默降级成"存到别处"或"不存"。
// 插件以为存好了而实际没存,比直接报错危险得多。
return "", fmt.Errorf("安全模块未启用(core.security.enabled=false),无法登记凭据")
}
return st.Submit(value, note)
}
func (s *secretsImpl) List() []pubsdk.SecretInfo {
st := s.store
if st == nil {
return nil
}
infos := st.List()
out := make([]pubsdk.SecretInfo, len(infos))
for i, in := range infos {
out[i] = pubsdk.SecretInfo{
Alias: in.Alias, Source: in.Source, Note: in.Note,
CreatedAt: in.CreatedAt, UpdatedAt: in.UpdatedAt, Hits: in.Hits,
}
}
return out
}
func (s *secretsImpl) Forget(alias string) error {
st := s.store
if st == nil {
return fmt.Errorf("安全模块未启用,变量库不可用")
}
return st.Forget(alias)
}
func (s *secretsImpl) Fill(text string) (string, int, []string) {
st := s.store
if st == nil {
return text, 0, nil
}
return st.Fill(text)
}
func (s *secretsImpl) Encrypt(plain []byte) (string, error) {
c := internalSecurity.ConfigCodec()
if c == nil || !c.Enabled() {
// ★ 拒绝明文落盘:插件以为存的是密文而实际是明文,是最危险的静默降级。
return "", fmt.Errorf("内核未装配加密能力,拒绝以明文存储(core.security.enabled=false?)")
}
return c.Seal(string(plain))
}
func (s *secretsImpl) Decrypt(stored string) ([]byte, error) {
// 非密文原样返回(兼容插件已有的明文文件,避免升级即坏)
if !internalSecurity.IsEncrypted(stored) {
return []byte(stored), nil
}
c := internalSecurity.ConfigCodec()
if c == nil || !c.Enabled() {
return nil, fmt.Errorf("有密文但内核未装配加密能力(密钥换了?)")
}
plain, err := c.Open(stored)
if err != nil {
return nil, err
}
return []byte(plain), nil
}
var _ pubsdk.SecretsAPI = (*secretsImpl)(nil)