fix(memory): 堵住 knowledge 工具绕过群聊/陌生人过滤的 P0 / owner 召回 hits 如实上报 / 静默吞点补来源日志
This commit is contained in:
1 parent
e792baa316
commit
eeb5f26668
5 files changed
+250
-52
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
@@ -94,6 +95,14 @@ func (k *knowledgeTool) Execute(ctx context.Context, args any) (any, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("io: knowledge: retrieve: %w", err)
|
||||
}
|
||||
// 群聊/陌生人(受限环境)只读非敏感的共享域记忆:复用循环注入路径上的
|
||||
// 同一隔离判据(loop.FilterSharedVisible),绝不另造第二套 —— 否则一旦
|
||||
// 管理面板把本工具放行给群聊/陌生人,就会返回角色日常(ScopePersona,
|
||||
// 主人私有)与 L4 意图记录(主人逐句原话),触碰 design/03 §5
|
||||
// 「不泄露主人敏感信息」(审计 2026-10-05,P0 红线)。
|
||||
if loop.SessionEnvFrom(ctx).Restricted() {
|
||||
results = loop.FilterSharedVisible(results)
|
||||
}
|
||||
out := make([]map[string]any, 0, len(results))
|
||||
for _, r := range results {
|
||||
out = append(out, map[string]any{
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
|
||||
)
|
||||
@@ -194,3 +196,76 @@ func TestKnowledge_MissingQuery(t *testing.T) {
|
||||
t.Fatalf("expected query-required error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// restrictedKnowledgeResults 是群聊/陌生人场景的固定命中集:共享域非敏感项
|
||||
// (用户习惯 user + 项目 project)与两类敏感项(角色日常 persona + L4 意图记录)。
|
||||
func restrictedKnowledgeResults() []memory.Result {
|
||||
return []memory.Result{
|
||||
{Item: memory.MemoryItem{ID: "u", Content: "用户习惯:喜欢冰美式", Kind: memory.KindRelationship, Scope: memory.ScopeUser}, Score: 0.9},
|
||||
{Item: memory.MemoryItem{ID: "p", Content: "项目记忆:Cyrene 用 Go 编写", Kind: memory.KindWorldEvent, Scope: memory.ScopeProject}, Score: 0.8},
|
||||
{Item: memory.MemoryItem{ID: "s", Content: "角色日常:周末喜欢爬山", Kind: memory.KindRelationship, Scope: memory.ScopePersona}, Score: 0.7},
|
||||
{Item: memory.MemoryItem{ID: "i", Content: "intent:isTask=true;taskKind=task;confidence=0.9;text=帮我订一杯冰美式", Kind: memory.KindRelationship, Scope: memory.ScopeUser}, Score: 0.6},
|
||||
}
|
||||
}
|
||||
|
||||
// knowledgeContents 把工具返回的 results 抽成 content 集合,便于断言泄露/保留。
|
||||
func knowledgeContents(t *testing.T, got any) map[string]bool {
|
||||
t.Helper()
|
||||
results := got.(map[string]any)["results"].([]map[string]any)
|
||||
out := make(map[string]bool, len(results))
|
||||
for _, r := range results {
|
||||
out[r["content"].(string)] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestKnowledge_RestrictedEnv_OnlySharedVisible 是审计 2026-10-05 高风险项的证据型
|
||||
// 测试:群聊/陌生人(受限环境)调用 knowledge 时,只返回非敏感共享域记忆;角色日常
|
||||
// (ScopePersona,主人私有)与 L4 意图记录(主人逐句原话)绝不返回 —— 复用循环注入
|
||||
// 路径的同一隔离判据(loop.FilterSharedVisible),不另造第二套。
|
||||
func TestKnowledge_RestrictedEnv_OnlySharedVisible(t *testing.T) {
|
||||
t.Parallel()
|
||||
kt := knowledgeToolForTest(t, WithKnowledgeRetriever(&stubRetriever{results: restrictedKnowledgeResults()}))
|
||||
ctx := loop.WithSessionEnv(
|
||||
tool.WithTurnPersona(context.Background(), "persona-a"),
|
||||
loop.SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false},
|
||||
)
|
||||
|
||||
got, err := kt.Execute(ctx, map[string]any{"query": "回忆"})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if n := got.(map[string]any)["count"].(int); n != 2 {
|
||||
t.Fatalf("受限环境 count = %d, want 2(只留共享域非敏感项)", n)
|
||||
}
|
||||
contents := knowledgeContents(t, got)
|
||||
for _, want := range []string{"用户习惯:喜欢冰美式", "项目记忆:Cyrene 用 Go 编写"} {
|
||||
if !contents[want] {
|
||||
t.Fatalf("受限环境漏了可查的共享域记忆 %q: %v", want, contents)
|
||||
}
|
||||
}
|
||||
for _, leaked := range []string{"角色日常:周末喜欢爬山", "intent:isTask=true;taskKind=task;confidence=0.9;text=帮我订一杯冰美式"} {
|
||||
if contents[leaked] {
|
||||
t.Fatalf("受限环境泄露了敏感记忆 %q: %v", leaked, contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestKnowledge_OwnerEnv_AllScopes 钉住反向边界:owner(全权限)环境不做隔离过滤,
|
||||
// 四类命中都照常返回 —— 修复只收窄群聊/陌生人,绝不误伤主人自己的完整读取。
|
||||
func TestKnowledge_OwnerEnv_AllScopes(t *testing.T) {
|
||||
t.Parallel()
|
||||
kt := knowledgeToolForTest(t, WithKnowledgeRetriever(&stubRetriever{results: restrictedKnowledgeResults()}))
|
||||
ctx := loop.WithSessionEnv(
|
||||
tool.WithTurnPersona(context.Background(), "persona-a"),
|
||||
loop.OwnerEnv(),
|
||||
)
|
||||
|
||||
got, err := kt.Execute(ctx, map[string]any{"query": "回忆"})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if n := got.(map[string]any)["count"].(int); n != 4 {
|
||||
t.Fatalf("owner 环境 count = %d, want 4(完整召回)", n)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user