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 ——
半成品状态长期红的判据会被人习惯性忽略,那时它就等于不存在。
174 lines
6.8 KiB
Go
174 lines
6.8 KiB
Go
package sdk
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||
)
|
||
|
||
// 事件类型/事件类型别名:插件经 SDK 订阅/发布内核事件,
|
||
// 无需直接 import internal/events(内核事件总线仅通过 SDK 暴露)。
|
||
|
||
type EventType = events.EventType
|
||
type Event = events.Event
|
||
|
||
const (
|
||
EventRawInput = events.EventRawInput
|
||
EventAgentOutput = events.EventAgentOutput
|
||
EventAgentLLMChain = events.EventAgentLLMChain
|
||
EventToolCall = events.EventToolCall
|
||
EventReasoning = events.EventReasoning
|
||
EventStage = events.EventStage
|
||
EventSystem = events.EventSystem
|
||
EventTerminalOutput = events.EventTerminalOutput
|
||
EventAll = events.EventAll
|
||
|
||
// 流式增量事件(token 级):核心 process() 流式化后每收到一个增量块发布。
|
||
// 客户端可选订做真逐 token 渲染;聚合事件仍照常发布,旧订阅者不受影响。
|
||
EventReasoningDelta = events.EventReasoningDelta
|
||
EventContentDelta = events.EventContentDelta
|
||
|
||
// EventMemoryAccess 是图记忆的真实读写事件(哪个块被读/写/删/合)。
|
||
//
|
||
// 星图靠它做「反馈」:此前只能拿 tool_call 的工具名猜节点,而工具名是英文、
|
||
// 实体名是中文概念(实测命中 0),只能退化成点亮一批固定节点。
|
||
EventMemoryAccess = events.EventMemoryAccess
|
||
)
|
||
|
||
// ───────────────────── 受限事件总线(插件可见) ─────────────────────
|
||
//
|
||
// **本层只做适配,不做策略**(用户 2026-10-27 的指正):
|
||
//
|
||
// internal/security 谁可以发/订阅什么事件(EventPolicy)+ 载荷过闸
|
||
// internal/sdk(本文件) 查策略 → 发/订 → 过闸 → 落到 *events.Bus
|
||
// sdk(公开) 只给接口与事件名
|
||
//
|
||
// 为什么策略必须住在 security:事件的边界就是**授权**。能发 skill_detected
|
||
// 等于能驱动 skillmgr 接管;能发 update_request 等于能触发内核升级流程。
|
||
// 这类判断与工具黑白名单是同一类,必须与它们同处一处,否则策略会有两份真相,
|
||
// 换个实现就换口径。
|
||
|
||
type eventsAPI struct {
|
||
pluginName string
|
||
bus *events.Bus
|
||
sec *security.Runtime // 可 nil:未装配时按**默认策略**(最小集合)判定
|
||
unsubs []func()
|
||
// unavailable 非空表示宿主没接总线:Publish/Subscribe 一律返回该原因。
|
||
unavailable string
|
||
}
|
||
|
||
// newEventsAPI 构造插件的受限事件面。
|
||
//
|
||
// ★ bus 为 nil 时返回**不可用实现**而不是 nil:
|
||
// 返回 nil 会让 `s.Events().Publish(...)` 在未接总线的宿主里直接 panic
|
||
// (实测:internal/plugins 的集成测试在 loadOne 阶段崩在 clawhubadapter 上)。
|
||
// 插件不该为"宿主没接总线"写 nil 判断——那是一个该报错、而不是该崩溃的状态。
|
||
func newEventsAPI(pluginName string, bus *events.Bus, rt *security.Runtime) pubsdk.EventsAPI {
|
||
// sec 由内核注入(不再读 RuntimeGlobal 包级单例)。
|
||
api := &eventsAPI{pluginName: pluginName, bus: bus, sec: rt}
|
||
if bus == nil {
|
||
api.unavailable = "event bus is not wired in this host"
|
||
}
|
||
return api
|
||
}
|
||
|
||
func (e *eventsAPI) AllowedPublish() []string {
|
||
pol := e.EventPolicy()
|
||
out := make([]string, len(pol.Publishable))
|
||
copy(out, pol.Publishable)
|
||
return append(out, "plugin."+e.pluginName+".*")
|
||
}
|
||
|
||
func (e *eventsAPI) AllowedSubscribe() []string {
|
||
pol := e.EventPolicy()
|
||
out := make([]string, len(pol.Subscribable))
|
||
copy(out, pol.Subscribable)
|
||
return out
|
||
}
|
||
|
||
// EventPolicy 取当前策略:优先用 security Runtime 的(可热改),
|
||
// Runtime 为 nil 时退回 security 包的默认策略(同样是最小集合,失败方向收紧)。
|
||
func (e *eventsAPI) EventPolicy() security.EventPolicy {
|
||
if e.sec != nil {
|
||
return e.sec.EventPolicy()
|
||
}
|
||
return security.DefaultEventPolicy()
|
||
}
|
||
|
||
// Publish 以**本插件身份**发布事件。
|
||
//
|
||
// ## ★ 为什么必须走 PublishAs(2026-10-29 收敛)
|
||
//
|
||
// 此前这里调的是 `bus.Publish`,而它的实现是 `publishAs(Publisher{}, ...)` ——
|
||
// 空 Publisher 的 `IsKernel()` 为真,于是 security 的 `AllowPublish`
|
||
// **第一行就直接放行**。也就是说:总线上的事件策略对这条路径从未生效过,
|
||
// 它之所以“安全”,只是因为本函数自己又查了一遍策略。
|
||
//
|
||
// 后果是**同一条策略有两份实现**(本文件 + security.eventGuard),
|
||
// 一旦口径漂移就会出现“总线放行、适配层拦截”或反之的分裂。
|
||
//
|
||
// 现在只保留一份:
|
||
//
|
||
// e.bus.PublishAs(Publisher{Name: e.pluginName}, ...)
|
||
// → security.eventGuard.AllowPublish (授权,唯一一处)
|
||
// → security.eventGuard.ScrubPayload (出向脱敏)
|
||
// → 投递时 security.eventGuard.ScrubInbound(入向清洗)
|
||
//
|
||
// 本文件只剩“把身份如实告诉总线”这一件职责 —— 插件模块不再持有策略判断
|
||
// (用户裁定:所有安全部分归安全模块)。
|
||
func (e *eventsAPI) Publish(eventType string, payload map[string]interface{}) error {
|
||
if e.unavailable != "" {
|
||
return fmt.Errorf("events: %s", e.unavailable)
|
||
}
|
||
if payload == nil {
|
||
payload = map[string]interface{}{}
|
||
}
|
||
// 载荷键数上限仍在这里判:它是**当前唯一实现**(eventGuard.AllowPublish
|
||
// 只拿到 eventType、看不到 payload,故无法在那里做)。
|
||
// 删掉它 = 直接失去这项保护,所以不属“重复”而不应一并丢弃。
|
||
if d := e.checkSize(len(payload)); !d.Allowed {
|
||
return fmt.Errorf("%s", d.Reason)
|
||
}
|
||
return e.bus.PublishAs(events.Publisher{Name: e.pluginName}, &events.Event{
|
||
Type: events.EventType(eventType),
|
||
Source: e.pluginName,
|
||
Payload: payload,
|
||
})
|
||
}
|
||
func (e *eventsAPI) checkSize(keys int) security.Decision {
|
||
if e.sec != nil {
|
||
return e.sec.CheckEventPayloadSize(keys)
|
||
}
|
||
return security.DefaultEventPolicy().CheckEventPayloadSize(keys)
|
||
}
|
||
|
||
func (e *eventsAPI) Subscribe(eventType string, handler func(map[string]interface{})) (func(), error) {
|
||
if e.unavailable != "" {
|
||
return nil, fmt.Errorf("events: %s", e.unavailable)
|
||
}
|
||
if handler == nil {
|
||
return nil, fmt.Errorf("events: handler must not be nil")
|
||
}
|
||
// 走 SubscribeAs:授权交给 security.eventGuard.AllowSubscribe(唯一一处),
|
||
// 入向清洗已由总线在投递前统一执行 —— 这里不再自己洗一遍。
|
||
unsub, err := e.bus.SubscribeAs(events.Publisher{Name: e.pluginName},
|
||
events.EventType(eventType), func(evt *events.Event) {
|
||
handler(evt.Payload)
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
e.unsubs = append(e.unsubs, unsub)
|
||
return unsub, nil
|
||
}
|
||
|
||
// stop 解除本插件建立的全部订阅(插件卸载时由宿主调用)。
|
||
func (e *eventsAPI) stop() {
|
||
for _, u := range e.unsubs {
|
||
u()
|
||
}
|
||
e.unsubs = nil
|
||
}
|