mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-15 08:51:49 +08:00
test(security): 补 Gate 明文拦截/回填与 Store 跨重启持久化契约
Codec 原有 7 条保留。新增模块职责层判据:
TestGateReplacesSecretOutboundAndBackfillsForExecution
出向文本里出现真实凭据 → 被替换成 @{alias},明文不再出现
执行前 FillForExecution 把别名换回真实凭据
TestGateScrubsSecretInbound
入向泄漏的凭据被清洗,订阅侧看不到明文
TestGateDisabledPassesTextThrough
关闭状态原样透传、拦截计数 0
TestStoreAliasRoundTripSurvivesReopen
Submit 拿别名 → 重新打开同一存储 → Fill 回填出原文
变异验证:
去掉回填分支 → 别名不再换回真实凭据(先前的失败)
出向不产出别名 → outbound secret was not intercepted
过程中修正了两处我自己写错的期望(回填目标必须先取别名、
引用格式是 @{alias}),不是放宽实现。
This commit is contained in:
@ -4,6 +4,8 @@ package test
|
||||
// 不读源码、不碰 Codec/Store 内部字段、不用任何 ForTest 钩子。
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||||
@ -138,3 +140,109 @@ func TestCodecOpenRejectsCorruptedCiphertext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Gate 的模块职责:明文凭据在出向被换成别名,执行前再回填原值 ----
|
||||
|
||||
func newGateStore(t *testing.T) *security.Store {
|
||||
t.Helper()
|
||||
st := security.New(security.Options{
|
||||
Path: filepath.Join(t.TempDir(), "vars.json"),
|
||||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil },
|
||||
Enabled: true,
|
||||
})
|
||||
if err := st.Load(); err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
const contractSecret = "sk-proj-abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
func TestGateReplacesSecretOutboundAndBackfillsForExecution(t *testing.T) {
|
||||
store := newGateStore(t)
|
||||
gate := security.NewGate(store)
|
||||
|
||||
out, n := gate.PrepareOutbound(security.Inspection{Stage: "to_llm"}, "key="+contractSecret)
|
||||
if n == 0 {
|
||||
t.Fatal("outbound secret was not intercepted")
|
||||
}
|
||||
if strings.Contains(out, contractSecret) {
|
||||
t.Fatalf("outbound text still carries plaintext: %q", out)
|
||||
}
|
||||
|
||||
// 出向文本里给出的是别名;执行前必须换回真实凭据。
|
||||
alias := extractAlias(out)
|
||||
if alias == "" {
|
||||
t.Fatalf("alias not found in outbound text: %q", out)
|
||||
}
|
||||
back, filled := gate.FillForExecution(security.Inspection{Stage: "to_plugin"}, "key=@{"+alias+"}")
|
||||
if filled != 1 {
|
||||
t.Fatalf("backfill reported %d places", filled)
|
||||
}
|
||||
if back != "key="+contractSecret {
|
||||
t.Fatalf("backfilled text = %q", back)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateScrubsSecretInbound(t *testing.T) {
|
||||
gate := security.NewGate(newGateStore(t))
|
||||
out, n := gate.ScrubInbound(security.Inspection{Stage: "tool_result"}, "leaked "+contractSecret)
|
||||
if n == 0 || strings.Contains(out, contractSecret) {
|
||||
t.Fatalf("inbound scrub failed: n=%d out=%q", n, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGateDisabledPassesTextThrough(t *testing.T) {
|
||||
st := security.New(security.Options{Enabled: false,
|
||||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil }})
|
||||
gate := security.NewGate(st)
|
||||
in := "key=" + contractSecret
|
||||
if out, n := gate.PrepareOutbound(security.Inspection{}, in); out != in || n != 0 {
|
||||
t.Fatalf("disabled gate modified text: %q n=%d", out, n)
|
||||
}
|
||||
if !gate.Enabled() == true {
|
||||
t.Fatal("unexpected Enabled() result for disabled store")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreAliasRoundTripSurvivesReopen(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "vars.json")
|
||||
first := security.New(security.Options{Path: path,
|
||||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil }, Enabled: true})
|
||||
if err := first.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
alias, err := first.Submit(contractSecret, "contract")
|
||||
if err != nil {
|
||||
t.Fatalf("Submit: %v", err)
|
||||
}
|
||||
if alias == "" || alias == contractSecret {
|
||||
t.Fatalf("alias = %q", alias)
|
||||
}
|
||||
|
||||
second := security.New(security.Options{Path: path,
|
||||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil }, Enabled: true})
|
||||
if err := second.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
back, n, _ := second.Fill("@{" + alias + "}")
|
||||
if n != 1 || !strings.Contains(back, contractSecret) {
|
||||
t.Fatalf("fill after reopen = %q (n=%d)", back, n)
|
||||
}
|
||||
}
|
||||
|
||||
// extractAlias 从安全模块产物里取出别名(公开契约:@\{alias\})。
|
||||
func extractAlias(text string) string {
|
||||
const marker = "@{"
|
||||
i := strings.Index(text, marker)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
rest := text[i+len(marker):]
|
||||
j := strings.Index(rest, "}")
|
||||
if j < 0 {
|
||||
return ""
|
||||
}
|
||||
return rest[:j]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user