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 ——
半成品状态长期红的判据会被人习惯性忽略,那时它就等于不存在。
157 lines
6.0 KiB
Go
157 lines
6.0 KiB
Go
package core
|
||
|
||
// 前提验证(C7):内核的 `@{别名}` 回填是否让插件的 `enc_shell` 变得**多余**。
|
||
//
|
||
// ## 为什么要先验这个再删代码
|
||
//
|
||
// C7 要「大幅删减插件的加密 shell 工具族」。删的依据是一个断言:
|
||
// 用户可以用**任何**工具(如内置的 cmd_run)配合 `@{别名}`,
|
||
// 由内核对所有工具参数统一回填 —— 而不必依赖插件专有的 enc_shell。
|
||
//
|
||
// 这个断言不成立的话,删掉 enc_shell 就是**削功能**而非去冗余。
|
||
//
|
||
// 已验证过 `TestFirewall_ToolArgsFilledBeforePlugin`(插件工具收到明文)。
|
||
// 这里补的是**决定性**的那一条:用**内置工具名**(cmd_run)也走同一条回填路径。
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||
)
|
||
|
||
// ★★ 判据:`cmd_run`(内置终端工具,非插件)的参数同样被内核回填。
|
||
//
|
||
// 这条成立 ⇒ 用户不必用插件专有的 enc_shell:
|
||
//
|
||
// enc_shell(command="git push https://x:{token}@host/r.git fav")
|
||
// ↓ 等价于
|
||
// cmd_run(command="git push https://x:@{sec_xxx}@host/r.git fav")
|
||
//
|
||
// 后者完全走内核机制,插件无需自带加密库与别名表。
|
||
func TestKernelFillCoversBuiltinToolsToo(t *testing.T) {
|
||
rt := newScrubRuntime(t)
|
||
store := rt.Store()
|
||
a, _, _ := newRootWithSecurity(t, &countingProvider{}, rt)
|
||
|
||
const tok = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
alias, err := store.Submit(tok, "测试")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 注册一个**模拟 cmd_run 的内置工具**(不真的执行 shell),
|
||
// 验证它通过 StageHost.ExecuteTool 拿到的是明文。
|
||
var seen string
|
||
if err := a.stageHost.RegisterTool("cmd_run", sdk.ToolDef{
|
||
Name: "cmd_run", Description: "模拟内置终端工具",
|
||
Parameters: map[string]interface{}{"type": "object"},
|
||
}, func(args map[string]interface{}) (interface{}, error) {
|
||
seen, _ = args["command"].(string)
|
||
return "ok", nil
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if _, err := a.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
|
||
"command": "git push https://x:@" + "{" + alias + "}@host/r.git feat/x",
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if seen == "" {
|
||
t.Fatal("工具未被调用")
|
||
}
|
||
if !strings.Contains(seen, tok) {
|
||
t.Fatalf("★ 内核回填未覆盖内置工具名 —— 那 enc_shell 就有存在理由(不能删)\n实际: %q", seen)
|
||
}
|
||
t.Logf("✅ 内置工具名的参数也被回填 ⇒ enc_shell 的填充功能是冗余的: %q", seen)
|
||
}
|
||
|
||
// ★★ 判据:内核回填**发生在插件 handler 之前** ⇒ 插件自己的别名表对
|
||
// 内核别名是**死代码**(永远不会命中)。
|
||
//
|
||
// 这个顺序很关键:
|
||
//
|
||
// StageHost.ExecuteTool → security.FillMap(args) → handler(args)
|
||
//
|
||
// 所以当内核安全模块启用时,插件的 enc_shell 收到的 command 里
|
||
// `@{sec_xxx}` **已经变成明文**了;插件自己的别名正则再扫也扫不到。
|
||
//
|
||
// ⇒ 结论:插件的「填充 @{别名}」这部分是**冗余**的(内核先做了)。
|
||
//
|
||
// 但它的 argv 保护(脚本走 stdin)与输出脱敏**仍然独有**,不可删。
|
||
func TestKernelFillHappensBeforePluginHandler(t *testing.T) {
|
||
rt := newScrubRuntime(t)
|
||
store := rt.Store()
|
||
a, _, _ := newRootWithSecurity(t, &countingProvider{}, rt)
|
||
|
||
const tok = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
alias, err := store.Submit(tok, "测试")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
var handlerSaw string
|
||
if err := a.stageHost.RegisterTool("probe_encshell", sdk.ToolDef{
|
||
Name: "probe_encshell", Description: "模拟 enc_shell",
|
||
Parameters: map[string]interface{}{"type": "object"},
|
||
}, func(args map[string]interface{}) (interface{}, error) {
|
||
handlerSaw, _ = args["command"].(string)
|
||
return "ok", nil
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if _, err := a.stageHost.ExecuteTool("probe_encshell", map[string]interface{}{
|
||
"command": "curl -H 'Authorization: token @{" + alias + "}'",
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// handler 里看到的**已经没有 @{别名}** 了(已被内核换掉)
|
||
if strings.Contains(handlerSaw, "@{"+alias+"}") {
|
||
t.Fatalf("内核回填应在 handler 之前完成;实际 handler 仍看到别名: %q", handlerSaw)
|
||
}
|
||
if !strings.Contains(handlerSaw, tok) {
|
||
t.Fatalf("handler 应看到明文: %q", handlerSaw)
|
||
}
|
||
t.Logf("✅ 内核先回填,插件看不到别名 ⇒ 插件自带别名表对内核别名是死代码")
|
||
}
|
||
|
||
// ★★ 判据:内核与插件的 `@{...}` 引用**语法不冲突**(带连字符的名字归插件)。
|
||
//
|
||
// 这是个**偶然但真实**的兼容,必须钉住,否则将来有人"统一语法"就炸:
|
||
//
|
||
// 内核 refPattern: @\{([A-Za-z0-9_]{1,64})\} ← 不含 - 和 .
|
||
// 插件 credRefPattern: @\{([A-Za-z0-9_.-]{1,64})\} ← 含 - 和 .
|
||
//
|
||
// 于是:
|
||
//
|
||
// @{sec_3718c87a11e9478b} 内核吃(无连字符)
|
||
// @{gitea-local} 内核**不吃**(有连字符)⇒ 留给插件解析
|
||
//
|
||
// ★ 若把内核的字符集放宽到含 `-`,插件自己的凭据(gitea-local)会被内核
|
||
//
|
||
// 先吃掉 —— 那些名字不在内核变量库里,内核会原样保留(认识不到),
|
||
// 看起来"没坏",但只要恰好有一个同名内核变量,就会静默换错值。
|
||
func TestKernelAndPluginAliasSyntaxDoNotCollide(t *testing.T) {
|
||
store := newScrubStore(t)
|
||
const tok = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
alias, err := store.Submit(tok, "内核变量")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 内核别名(无连字符)→ 被回填
|
||
if got, n, _ := store.Fill("x:@" + "{" + alias + "}"); n != 1 || !strings.Contains(got, tok) {
|
||
t.Fatalf("内核别名应被回填: n=%d got=%q", n, got)
|
||
}
|
||
// 插件风格的名字(含连字符)→ **不被内核碰**
|
||
pluginStyle := "git push https://x:@" + "{gitea-local}@host/r.git"
|
||
if got, n, _ := store.Fill(pluginStyle); n != 0 || !strings.Contains(got, "{gitea-local}") {
|
||
t.Fatalf("含连字符的引用内核不应碰(那是插件的凭据): n=%d got=%q", n, got)
|
||
}
|
||
t.Logf("✅ 内核只吃 [A-Za-z0-9_],@{gitea-local} 留给插件")
|
||
}
|