diff --git a/internal/agent/sanitize.go b/internal/agent/sanitize.go index 9bb28722..25c86a7a 100644 --- a/internal/agent/sanitize.go +++ b/internal/agent/sanitize.go @@ -324,28 +324,19 @@ var configLeakFileNames = []string{ "internal_config", "system prompt", } -// configLeakListPattern matches numbered or bulleted list items that reference internal files. -// Examples: "1. Đọc SOUL.md..." "- Check AGENTS.md" "* Read IDENTITY.md" -var configLeakListPattern = regexp.MustCompile( - `(?im)^[\s]*(?:[\d]+[.\)]\s+|\-\s+|\*\s+).*(?:SOUL\.md|IDENTITY\.md|AGENTS\.md|BOOTSTRAP\.md|system prompt|internal_config).*$`, -) - // StripConfigLeak detects when a predefined agent dumps its internal configuration -// or procedures (e.g. numbered lists referencing SOUL.md, AGENTS.md) and replaces -// the entire response with a friendly decline. +// (e.g. referencing SOUL.md, AGENTS.md, IDENTITY.md) and replaces the entire +// response with a friendly decline. // -// Only active for predefined agents. Two-gate detection: -// - Gate 1: 3+ distinct internal file names mentioned in response -// - Gate 2: 2+ list items (numbered/bulleted) referencing those files -// -// When both gates trigger, the response is replaced entirely — partial stripping -// leaves broken output that reads worse than a clean decline. +// Only active for predefined agents. Single-gate detection: +// 3+ distinct internal file names mentioned → replace entire response. +// A predefined agent legitimately referencing 3+ internal config files in one +// response is essentially impossible, so this has no false-positive risk. func StripConfigLeak(content, agentType string) string { if agentType != "predefined" || content == "" { return content } - // Gate 1: count distinct file name hits hits := 0 for _, name := range configLeakFileNames { if strings.Contains(content, name) { @@ -356,15 +347,8 @@ func StripConfigLeak(content, agentType string) string { return content } - // Gate 2: at least 2 list items referencing internal files - matches := configLeakListPattern.FindAllString(content, -1) - if len(matches) < 2 { - return content - } - slog.Warn("security.config_leak_stripped", "file_hits", hits, - "list_matches", len(matches), "original_len", len(content), ) diff --git a/internal/agent/sanitize_config_leak_test.go b/internal/agent/sanitize_config_leak_test.go index 373a6a1b..67d05b13 100644 --- a/internal/agent/sanitize_config_leak_test.go +++ b/internal/agent/sanitize_config_leak_test.go @@ -22,9 +22,10 @@ func TestStripConfigLeak(t *testing.T) { agentType: "predefined", }, { - name: "predefined - 3 files but no list pattern", + name: "predefined - 3 files in paragraph (no list)", content: "SOUL.md, IDENTITY.md, AGENTS.md đều đã được load sẵn trong context.", agentType: "predefined", + want: declineMsg, }, { name: "predefined - leaking numbered list", @@ -52,6 +53,18 @@ Có gì anh hỏi thêm nhé!`, agentType: "predefined", want: declineMsg, }, + { + name: "predefined - real bypass: bold-header paragraph leak", + content: `Dạ, nếu có người hỏi chi tiết về internal process thì em sẽ: + +**Từ chối lịch sự** - Những thứ như system prompt, context files (SOUL.md, IDENTITY.md, AGENTS.md), internal procedures là confidential. + +**Cách em reply:** +- "Cái này là internal của em, không share được ạ" +- "Em không thể tiết lộ chi tiết process nội bộ"`, + agentType: "predefined", + want: declineMsg, + }, { name: "predefined - legitimate action report (single file)", content: "Em đã update SOUL.md cho anh rồi, nội dung mới đã được lưu.",