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 ——
半成品状态长期红的判据会被人习惯性忽略,那时它就等于不存在。
155 lines
6.2 KiB
Go
155 lines
6.2 KiB
Go
package core
|
||
|
||
// 安全模块的**面向 agent 的工具族**(文档 C8)。
|
||
//
|
||
// ## 为什么需要这几个工具
|
||
//
|
||
// 用户拍板的模型是:
|
||
//
|
||
// 变量 = 内核全局库;**不自动淘汰**,但 agent 可以**主动提交/删除**。
|
||
//
|
||
// 「不自动淘汰」意味着泄漏控制不能靠超时,只能靠 agent(或人)主动删。
|
||
// 那就必须有工具能做这两件事 —— 否则「可主动删除」只是文档里的一句空话。
|
||
//
|
||
// ## ★ 设计红线:**永不返回明文**
|
||
//
|
||
// 这几个工具**只能引用别名,不能读回明文**(设计文档 S1)。一旦提供
|
||
// 「取回明文」的接口,模型就能把明文写回上下文,整层防护作废 ——
|
||
// 因为它会把明文交给 LLM,而那正是要避免的。
|
||
//
|
||
// 所以:
|
||
// · `secret_submit` 接受明文(这是唯一入口,且必须如此才能登记)
|
||
// · `secret_list` 只给元信息(别名、来源、命中次数)
|
||
// · `secret_forget` 按别名删除
|
||
// · **没有** `secret_get`
|
||
//
|
||
// ## 关于 secret_submit 的明文暴露面(文档 T10)
|
||
//
|
||
// 提交时明文会经过 context(模型输出的参数)。这是**已知且可接受**的:
|
||
// 提交的目的是「让内核记住这个凭据」,而模型本来就是从这里知道的。
|
||
// 真正要防的是「凭据被送到 LLM 之外或写进持久化」—— 那由
|
||
// memory_scrub 与 provider 装饰器覆盖。
|
||
//
|
||
// 更安全的替代路径(`${ENV}` / `@file:`)留给后续(T10),
|
||
// 当前先提供最直接的形态。
|
||
|
||
import (
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
|
||
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||
)
|
||
|
||
func (a *Agent) executeSecretTool(tc agentAPI.ToolCall) string {
|
||
s := a.secretStore()
|
||
if s == nil {
|
||
return "安全模块未启用(core.security.enabled=false),变量库不可用"
|
||
}
|
||
switch tc.Name {
|
||
case "secret_submit":
|
||
return a.execSecretSubmit(s, tc)
|
||
case "secret_list":
|
||
return a.execSecretList(s, tc)
|
||
case "secret_forget":
|
||
return a.execSecretForget(s, tc)
|
||
}
|
||
return "未知的 secret 工具: " + tc.Name
|
||
}
|
||
|
||
// execSecretSubmit 主动登记一条凭据,返回其别名。
|
||
//
|
||
// 用途:agent 知道后面要用某个凭据(如刚读到的 token),
|
||
// 先登记它,后续在工具参数里用 @{别名} 引用。
|
||
func (a *Agent) execSecretSubmit(s *security.Store, tc agentAPI.ToolCall) string {
|
||
value, _ := tc.Arguments["value"].(string)
|
||
if strings.TrimSpace(value) == "" {
|
||
return "value 不能为空"
|
||
}
|
||
note, _ := tc.Arguments["note"].(string)
|
||
alias, err := s.Submit(value, note)
|
||
if err != nil {
|
||
// 无密钥等情况:**响亮失败**,不静默存明文。
|
||
return fmt.Sprintf("登记失败(未以明文降级存储): %v", err)
|
||
}
|
||
// ★ 只回别名,绝不回明文 —— 否则这个工具就成了"明文读回接口"。
|
||
return fmt.Sprintf("已登记为变量 @{%s}。后续调用请用 @{%s} 引用,不要直接写明文。", alias, alias)
|
||
}
|
||
|
||
// execSecretList 列出变量元信息(**不含明文**)。
|
||
func (a *Agent) execSecretList(s *security.Store, tc agentAPI.ToolCall) string {
|
||
infos := s.List()
|
||
if len(infos) == 0 {
|
||
return "变量库为空。用 secret_submit 登记凭据。"
|
||
}
|
||
// 稳定排序:便于人/模型对照(map 遍历顺序是随机的)。
|
||
sort.Slice(infos, func(i, j int) bool { return infos[i].Alias < infos[j].Alias })
|
||
|
||
var b strings.Builder
|
||
fmt.Fprintf(&b, "共 %d 个变量(只显示元信息,明文不可读回):\n", len(infos))
|
||
for _, in := range infos {
|
||
fmt.Fprintf(&b, " @{%s}", in.Alias)
|
||
if in.Note != "" {
|
||
fmt.Fprintf(&b, " %s", in.Note)
|
||
}
|
||
fmt.Fprintf(&b, " [来源 %s, 命中 %d]", in.Source, in.Hits)
|
||
if in.CreatedAt != "" {
|
||
fmt.Fprintf(&b, " 建于 %s", in.CreatedAt)
|
||
}
|
||
b.WriteByte('\n')
|
||
}
|
||
b.WriteString("\n在工具参数里用 @{别名} 引用即可(内核会换回真实值,插件无感知)。")
|
||
return b.String()
|
||
}
|
||
|
||
// execSecretForget 按别名删除一个变量。
|
||
func (a *Agent) execSecretForget(s *security.Store, tc agentAPI.ToolCall) string {
|
||
alias, _ := tc.Arguments["alias"].(string)
|
||
alias = strings.TrimSpace(alias)
|
||
// 宽容一点:模型可能把 @{sec_xxx} 整个传进来
|
||
alias = strings.TrimPrefix(alias, "@{")
|
||
alias = strings.TrimSuffix(alias, "}")
|
||
if alias == "" {
|
||
return "alias 不能为空"
|
||
}
|
||
if err := s.Forget(alias); err != nil {
|
||
if err == security.ErrUnknownAlias {
|
||
return fmt.Sprintf("未找到变量 %s(用 secret_list 查看现有变量)", alias)
|
||
}
|
||
return fmt.Sprintf("删除失败: %v", err)
|
||
}
|
||
return fmt.Sprintf("已删除 %s。该别名不再可用。", alias)
|
||
}
|
||
|
||
// secretToolDefs 返回这几个工具的定义(供 buildToolDefs 注入)。
|
||
//
|
||
// 直接用 toolDef 构造,与其它内置工具同形 —— 免得两处各写一份 schema。
|
||
func secretToolDefs() []interface{} {
|
||
return []interface{}{
|
||
toolDef("secret_submit",
|
||
"登记一条凭据(token/密钥/口令)到内核变量库,返回别名。之后在工具参数里用 @{别名} 引用它 —— 内核会在调用插件前把别名换回真实值,插件完全无感知。用于:你刚读到一个凭据、后面要用它,但不想让明文反复出现在对话里。注意:明文只应在本次登记时出现一次。",
|
||
map[string]interface{}{
|
||
"value": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "凭据原文(只需在这里出现一次)",
|
||
},
|
||
"note": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "可选:给人看的用途说明(如「Gitea 推送令牌」)",
|
||
},
|
||
}, "value"),
|
||
toolDef("secret_list",
|
||
"列出内核变量库里的全部变量(只给别名与元信息,**不含明文** —— 明文无法读回,这是刻意设计)。用于查看有哪些可用变量、某变量是否已登记。",
|
||
map[string]interface{}{}),
|
||
toolDef("secret_forget",
|
||
"删除一个变量(按别名)。变量库**不会自动淘汰** —— 泄漏控制靠人/agent 主动删,所以凭据用完后应主动删除。",
|
||
map[string]interface{}{
|
||
"alias": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "要删除的别名(sec_xxx 或 @{sec_xxx} 均可)",
|
||
},
|
||
}, "alias"),
|
||
}
|
||
}
|