fix(tools): address review — close async bypass, fix false positives
- Sanitize subagent callback results in subagent.go (the primary attack vector from #972 — async results bypassed ExecuteWithContext) - Remove generic hex pattern that matched git SHAs, Docker digests, and checksums in go.sum/package-lock.json - Order regex patterns most-specific-first to prevent partial matches - Add Stripe key patterns (sk_live_, sk_test_, rk_live_, rk_test_) - Fix IsSecretEnvVar false positives on AUTHOR, AUTHORITY, etc. by switching from substring to suffix matching - Preserve error chain via Unwrap() on redactedError - Use strings.Contains instead of custom helpers in tests - Add idempotency test, git SHA preservation test, checksum test
This commit is contained in:
parent
0d207e1174
commit
b743286fc5
3 changed files with 96 additions and 57 deletions
|
|
@ -6,16 +6,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// secretPatterns matches common API key and credential formats.
|
// secretPatterns matches common API key and credential formats.
|
||||||
// Each pattern is compiled once at init and reused across calls.
|
// Ordered from most specific to least specific to avoid partial matches.
|
||||||
|
// Each pattern is compiled once at package init and reused across calls.
|
||||||
var secretPatterns = []*regexp.Regexp{
|
var secretPatterns = []*regexp.Regexp{
|
||||||
// OpenAI / OpenAI-compatible (sk-...)
|
|
||||||
regexp.MustCompile(`sk-[A-Za-z0-9_-]{20,}`),
|
|
||||||
// OpenAI project keys (sk-proj-...)
|
|
||||||
regexp.MustCompile(`sk-proj-[A-Za-z0-9_-]{20,}`),
|
|
||||||
// Anthropic (sk-ant-...)
|
|
||||||
regexp.MustCompile(`sk-ant-[A-Za-z0-9_-]{20,}`),
|
|
||||||
// OpenRouter (sk-or-v1-...)
|
// OpenRouter (sk-or-v1-...)
|
||||||
regexp.MustCompile(`sk-or-v1-[A-Za-z0-9_-]{20,}`),
|
regexp.MustCompile(`sk-or-v1-[A-Za-z0-9_-]{20,}`),
|
||||||
|
// Anthropic (sk-ant-...)
|
||||||
|
regexp.MustCompile(`sk-ant-[A-Za-z0-9_-]{20,}`),
|
||||||
|
// OpenAI project keys (sk-proj-...)
|
||||||
|
regexp.MustCompile(`sk-proj-[A-Za-z0-9_-]{20,}`),
|
||||||
|
// OpenAI / OpenAI-compatible (sk-...)
|
||||||
|
regexp.MustCompile(`sk-[A-Za-z0-9_-]{20,}`),
|
||||||
// Google AI / Gemini (AIza...)
|
// Google AI / Gemini (AIza...)
|
||||||
regexp.MustCompile(`AIza[A-Za-z0-9_-]{30,}`),
|
regexp.MustCompile(`AIza[A-Za-z0-9_-]{30,}`),
|
||||||
// GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
|
// GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
|
||||||
|
|
@ -24,14 +25,19 @@ var secretPatterns = []*regexp.Regexp{
|
||||||
regexp.MustCompile(`xox[bpsa]-[A-Za-z0-9-]{20,}`),
|
regexp.MustCompile(`xox[bpsa]-[A-Za-z0-9-]{20,}`),
|
||||||
// Discord bot tokens (base64.base64.base64)
|
// Discord bot tokens (base64.base64.base64)
|
||||||
regexp.MustCompile(`[MN][A-Za-z0-9]{23,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}`),
|
regexp.MustCompile(`[MN][A-Za-z0-9]{23,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}`),
|
||||||
// Generic long bearer/api tokens (40+ hex or base64 chars)
|
// Stripe keys (sk_live_, sk_test_, rk_live_, rk_test_)
|
||||||
regexp.MustCompile(`\b[A-Fa-f0-9]{40,}\b`),
|
regexp.MustCompile(`[sr]k_(?:live|test)_[A-Za-z0-9]{20,}`),
|
||||||
// AWS access keys (AKIA...)
|
// AWS access keys (AKIA...)
|
||||||
regexp.MustCompile(`AKIA[A-Z0-9]{16}`),
|
regexp.MustCompile(`AKIA[A-Z0-9]{16}`),
|
||||||
// AWS secret keys (40 char base64-ish after known prefixes)
|
// AWS secret keys (40 char base64-ish after known prefixes)
|
||||||
regexp.MustCompile(`(?i)aws[_\-]?secret[_\-]?access[_\-]?key["'\s:=]+[A-Za-z0-9/+=]{40}`),
|
regexp.MustCompile(`(?i)aws[_\-]?secret[_\-]?access[_\-]?key["'\s:=]+[A-Za-z0-9/+=]{40}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Generic hex patterns (e.g. [A-Fa-f0-9]{40,}) are intentionally excluded.
|
||||||
|
// They match git SHAs, Docker digests, checksums in go.sum/package-lock.json, etc.
|
||||||
|
// The JSON field detection and specific prefix patterns cover real threats without
|
||||||
|
// corrupting everyday tool output.
|
||||||
|
|
||||||
// jsonSecretFields matches JSON keys that typically hold secrets.
|
// jsonSecretFields matches JSON keys that typically hold secrets.
|
||||||
// Captures the key and value so we can redact the value in-place.
|
// Captures the key and value so we can redact the value in-place.
|
||||||
var jsonSecretFields = regexp.MustCompile(
|
var jsonSecretFields = regexp.MustCompile(
|
||||||
|
|
@ -63,7 +69,7 @@ func redactToken(token string) string {
|
||||||
return "[REDACTED]"
|
return "[REDACTED]"
|
||||||
}
|
}
|
||||||
// Show first 4 chars for identification, redact the rest.
|
// Show first 4 chars for identification, redact the rest.
|
||||||
return token[:4] + "..." + "[REDACTED]"
|
return token[:4] + "...[REDACTED]"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeResult applies secret redaction to both ForLLM and ForUser fields
|
// SanitizeResult applies secret redaction to both ForLLM and ForUser fields
|
||||||
|
|
@ -80,20 +86,20 @@ func SanitizeResult(r *ToolResult) *ToolResult {
|
||||||
if r.Err != nil {
|
if r.Err != nil {
|
||||||
cleaned := RedactSecrets(r.Err.Error())
|
cleaned := RedactSecrets(r.Err.Error())
|
||||||
if cleaned != r.Err.Error() {
|
if cleaned != r.Err.Error() {
|
||||||
r.Err = &redactedError{msg: cleaned}
|
r.Err = &redactedError{msg: cleaned, cause: r.Err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// redactedError wraps a redacted error message.
|
// redactedError wraps a redacted error message while preserving the error chain.
|
||||||
type redactedError struct {
|
type redactedError struct {
|
||||||
msg string
|
msg string
|
||||||
|
cause error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *redactedError) Error() string {
|
func (e *redactedError) Error() string { return e.msg }
|
||||||
return e.msg
|
func (e *redactedError) Unwrap() error { return e.cause }
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsSecret checks whether a string contains any known secret pattern.
|
// ContainsSecret checks whether a string contains any known secret pattern.
|
||||||
// Useful for pre-flight validation before logging or outputting content.
|
// Useful for pre-flight validation before logging or outputting content.
|
||||||
|
|
@ -109,19 +115,20 @@ func ContainsSecret(s string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// envSecretKeys lists environment variable name patterns whose values should
|
// envSecretKeys lists environment variable name suffixes whose values should
|
||||||
// never appear in tool output. Used by the shell tool guard.
|
// never appear in tool output.
|
||||||
var envSecretKeys = []string{
|
var envSecretKeys = []string{
|
||||||
"API_KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL",
|
"_API_KEY", "_SECRET", "_TOKEN", "_PASSWORD", "_CREDENTIAL",
|
||||||
"ACCESS_KEY", "PRIVATE_KEY", "AUTH",
|
"_ACCESS_KEY", "_PRIVATE_KEY",
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSecretEnvVar returns true if the environment variable name looks like it
|
// IsSecretEnvVar returns true if the environment variable name looks like it
|
||||||
// holds a secret value (case-insensitive substring match).
|
// holds a secret value. Matches on suffixes to avoid false positives on names
|
||||||
|
// like AUTHOR, AUTHORITY, or OAUTH_REDIRECT_URI.
|
||||||
func IsSecretEnvVar(name string) bool {
|
func IsSecretEnvVar(name string) bool {
|
||||||
upper := strings.ToUpper(name)
|
upper := strings.ToUpper(name)
|
||||||
for _, key := range envSecretKeys {
|
for _, suffix := range envSecretKeys {
|
||||||
if strings.Contains(upper, key) {
|
if strings.HasSuffix(upper, suffix) || strings.Contains(upper, suffix+"_") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ func TestRedactSecrets_OpenAIKey(t *testing.T) {
|
||||||
if result == input {
|
if result == input {
|
||||||
t.Error("expected OpenAI key to be redacted")
|
t.Error("expected OpenAI key to be redacted")
|
||||||
}
|
}
|
||||||
if !containsRedacted(result) {
|
if !strings.Contains(result, "[REDACTED]") {
|
||||||
t.Errorf("expected [REDACTED] in output, got: %s", result)
|
t.Errorf("expected [REDACTED] in output, got: %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +20,7 @@ func TestRedactSecrets_OpenAIKey(t *testing.T) {
|
||||||
func TestRedactSecrets_AnthropicKey(t *testing.T) {
|
func TestRedactSecrets_AnthropicKey(t *testing.T) {
|
||||||
input := `Using key: sk-ant-api03-abcdefghijklmnopqrstuvwxyz123456`
|
input := `Using key: sk-ant-api03-abcdefghijklmnopqrstuvwxyz123456`
|
||||||
result := RedactSecrets(input)
|
result := RedactSecrets(input)
|
||||||
if !containsRedacted(result) {
|
if !strings.Contains(result, "[REDACTED]") {
|
||||||
t.Errorf("expected [REDACTED] in output, got: %s", result)
|
t.Errorf("expected [REDACTED] in output, got: %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,14 +32,14 @@ func TestRedactSecrets_JSONFields(t *testing.T) {
|
||||||
t.Error("expected JSON api_key value to be redacted")
|
t.Error("expected JSON api_key value to be redacted")
|
||||||
}
|
}
|
||||||
// Key name should be preserved for context
|
// Key name should be preserved for context
|
||||||
if !contains(result, `"api_key"`) {
|
if !strings.Contains(result, `"api_key"`) {
|
||||||
t.Error("expected key name to be preserved")
|
t.Error("expected key name to be preserved")
|
||||||
}
|
}
|
||||||
if !containsRedacted(result) {
|
if !strings.Contains(result, "[REDACTED]") {
|
||||||
t.Errorf("expected [REDACTED] in output, got: %s", result)
|
t.Errorf("expected [REDACTED] in output, got: %s", result)
|
||||||
}
|
}
|
||||||
// Model value should NOT be redacted
|
// Model value should NOT be redacted
|
||||||
if !contains(result, "gpt-4") {
|
if !strings.Contains(result, "gpt-4") {
|
||||||
t.Error("expected non-secret fields to be preserved")
|
t.Error("expected non-secret fields to be preserved")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,10 +51,10 @@ func TestRedactSecrets_MultipleSecretFields(t *testing.T) {
|
||||||
"channel_secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4"
|
"channel_secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4"
|
||||||
}`
|
}`
|
||||||
result := RedactSecrets(input)
|
result := RedactSecrets(input)
|
||||||
if contains(result, "sk-abcdefghijklmnopqrstuvwxyz") {
|
if strings.Contains(result, "sk-abcdefghijklmnopqrstuvwxyz") {
|
||||||
t.Error("api_key value should be redacted")
|
t.Error("api_key value should be redacted")
|
||||||
}
|
}
|
||||||
if contains(result, "test-fake-token-value") {
|
if strings.Contains(result, "test-fake-token-value") {
|
||||||
t.Error("token value should be redacted")
|
t.Error("token value should be redacted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +64,7 @@ func TestRedactSecrets_SlackToken(t *testing.T) {
|
||||||
fakeToken := "xoxb-fake" + "-placeholder-abcdefghij"
|
fakeToken := "xoxb-fake" + "-placeholder-abcdefghij"
|
||||||
input := "Bot token: " + fakeToken
|
input := "Bot token: " + fakeToken
|
||||||
result := RedactSecrets(input)
|
result := RedactSecrets(input)
|
||||||
if !containsRedacted(result) {
|
if !strings.Contains(result, "[REDACTED]") {
|
||||||
t.Errorf("expected Slack token to be redacted, got: %s", result)
|
t.Errorf("expected Slack token to be redacted, got: %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +72,7 @@ func TestRedactSecrets_SlackToken(t *testing.T) {
|
||||||
func TestRedactSecrets_AWSAccessKey(t *testing.T) {
|
func TestRedactSecrets_AWSAccessKey(t *testing.T) {
|
||||||
input := `AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE`
|
input := `AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE`
|
||||||
result := RedactSecrets(input)
|
result := RedactSecrets(input)
|
||||||
if contains(result, "AKIAIOSFODNN7EXAMPLE") {
|
if strings.Contains(result, "AKIAIOSFODNN7EXAMPLE") {
|
||||||
t.Errorf("expected AWS key to be redacted, got: %s", result)
|
t.Errorf("expected AWS key to be redacted, got: %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,11 +80,21 @@ func TestRedactSecrets_AWSAccessKey(t *testing.T) {
|
||||||
func TestRedactSecrets_GitHubToken(t *testing.T) {
|
func TestRedactSecrets_GitHubToken(t *testing.T) {
|
||||||
input := `GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef01234`
|
input := `GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef01234`
|
||||||
result := RedactSecrets(input)
|
result := RedactSecrets(input)
|
||||||
if contains(result, "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
if strings.Contains(result, "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
||||||
t.Errorf("expected GitHub token to be redacted, got: %s", result)
|
t.Errorf("expected GitHub token to be redacted, got: %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedactSecrets_StripeKey(t *testing.T) {
|
||||||
|
// Construct dynamically to avoid GitHub push protection triggering on test data
|
||||||
|
fakeKey := "sk_" + "live_" + "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
input := "STRIPE_KEY=" + fakeKey
|
||||||
|
result := RedactSecrets(input)
|
||||||
|
if strings.Contains(result, fakeKey) {
|
||||||
|
t.Errorf("expected Stripe key to be redacted, got: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRedactSecrets_NoSecrets(t *testing.T) {
|
func TestRedactSecrets_NoSecrets(t *testing.T) {
|
||||||
input := `{"model": "gpt-4", "temperature": 0.7, "message": "Hello world"}`
|
input := `{"model": "gpt-4", "temperature": 0.7, "message": "Hello world"}`
|
||||||
result := RedactSecrets(input)
|
result := RedactSecrets(input)
|
||||||
|
|
@ -103,21 +114,47 @@ func TestRedactSecrets_PreservesPrefix(t *testing.T) {
|
||||||
input := `key: sk-proj-abc123def456ghi789jkl012mno345pqr678`
|
input := `key: sk-proj-abc123def456ghi789jkl012mno345pqr678`
|
||||||
result := RedactSecrets(input)
|
result := RedactSecrets(input)
|
||||||
// Should show first 4 chars for identification
|
// Should show first 4 chars for identification
|
||||||
if !contains(result, "sk-p") {
|
if !strings.Contains(result, "sk-p") {
|
||||||
t.Errorf("expected prefix to be preserved for identification, got: %s", result)
|
t.Errorf("expected prefix to be preserved for identification, got: %s", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedactSecrets_DoesNotRedactGitSHA(t *testing.T) {
|
||||||
|
// 40-char hex strings like git SHAs must NOT be redacted
|
||||||
|
input := `commit 3bcbfd9a1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f`
|
||||||
|
result := RedactSecrets(input)
|
||||||
|
if result != input {
|
||||||
|
t.Errorf("git SHA should not be redacted, got: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedactSecrets_DoesNotRedactChecksum(t *testing.T) {
|
||||||
|
input := `sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||||
|
result := RedactSecrets(input)
|
||||||
|
if result != input {
|
||||||
|
t.Errorf("SHA-256 checksum should not be redacted, got: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedactSecrets_Idempotent(t *testing.T) {
|
||||||
|
input := `{"api_key": "sk-ant-secret12345678901234", "token": "test-value-placeholder-1234"}`
|
||||||
|
once := RedactSecrets(input)
|
||||||
|
twice := RedactSecrets(once)
|
||||||
|
if once != twice {
|
||||||
|
t.Errorf("RedactSecrets should be idempotent.\nOnce: %s\nTwice: %s", once, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSanitizeResult_ForLLMAndForUser(t *testing.T) {
|
func TestSanitizeResult_ForLLMAndForUser(t *testing.T) {
|
||||||
r := &ToolResult{
|
r := &ToolResult{
|
||||||
ForLLM: `Read config.json: {"api_key": "sk-ant-secret12345678901234"}`,
|
ForLLM: `Read config.json: {"api_key": "sk-ant-secret12345678901234"}`,
|
||||||
ForUser: `Config: {"api_key": "sk-ant-secret12345678901234"}`,
|
ForUser: `Config: {"api_key": "sk-ant-secret12345678901234"}`,
|
||||||
}
|
}
|
||||||
SanitizeResult(r)
|
SanitizeResult(r)
|
||||||
if contains(r.ForLLM, "sk-ant-secret") {
|
if strings.Contains(r.ForLLM, "sk-ant-secret") {
|
||||||
t.Error("ForLLM should have secret redacted")
|
t.Error("ForLLM should have secret redacted")
|
||||||
}
|
}
|
||||||
if contains(r.ForUser, "sk-ant-secret") {
|
if strings.Contains(r.ForUser, "sk-ant-secret") {
|
||||||
t.Error("ForUser should have secret redacted")
|
t.Error("ForUser should have secret redacted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,15 +166,20 @@ func TestSanitizeResult_NilResult(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSanitizeResult_ErrorField(t *testing.T) {
|
func TestSanitizeResult_ErrorFieldPreservesChain(t *testing.T) {
|
||||||
|
originalErr := fmt.Errorf("auth failed with key sk-proj-abc123def456ghi789jkl012mno345pqr678")
|
||||||
r := &ToolResult{
|
r := &ToolResult{
|
||||||
ForLLM: "error occurred",
|
ForLLM: "error occurred",
|
||||||
Err: fmt.Errorf("auth failed with key sk-proj-abc123def456ghi789jkl012mno345pqr678"),
|
Err: fmt.Errorf("wrapped: %w", originalErr),
|
||||||
}
|
}
|
||||||
SanitizeResult(r)
|
SanitizeResult(r)
|
||||||
if contains(r.Err.Error(), "sk-proj-abc123") {
|
if strings.Contains(r.Err.Error(), "sk-proj-abc123") {
|
||||||
t.Error("Err field should have secret redacted")
|
t.Error("Err field should have secret redacted")
|
||||||
}
|
}
|
||||||
|
// Verify error chain is preserved via Unwrap
|
||||||
|
if r.Err == nil {
|
||||||
|
t.Fatal("Err should not be nil")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainsSecret(t *testing.T) {
|
func TestContainsSecret(t *testing.T) {
|
||||||
|
|
@ -149,6 +191,7 @@ func TestContainsSecret(t *testing.T) {
|
||||||
{`key: sk-proj-abc123def456ghi789jkl012mno`, true},
|
{`key: sk-proj-abc123def456ghi789jkl012mno`, true},
|
||||||
{`{"api_key": "super-secret-value-here"}`, true},
|
{`{"api_key": "super-secret-value-here"}`, true},
|
||||||
{`{"model": "gpt-4"}`, false},
|
{`{"model": "gpt-4"}`, false},
|
||||||
|
{`commit abc123def456`, false},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
got := ContainsSecret(tc.input)
|
got := ContainsSecret(tc.input)
|
||||||
|
|
@ -168,10 +211,14 @@ func TestIsSecretEnvVar(t *testing.T) {
|
||||||
{"CHANNEL_SECRET", true},
|
{"CHANNEL_SECRET", true},
|
||||||
{"BOT_TOKEN", true},
|
{"BOT_TOKEN", true},
|
||||||
{"DATABASE_PASSWORD", true},
|
{"DATABASE_PASSWORD", true},
|
||||||
|
{"AWS_ACCESS_KEY", true},
|
||||||
{"HOME", false},
|
{"HOME", false},
|
||||||
{"PATH", false},
|
{"PATH", false},
|
||||||
{"GOPATH", false},
|
{"GOPATH", false},
|
||||||
{"MODEL_NAME", false},
|
{"MODEL_NAME", false},
|
||||||
|
{"AUTHOR", false}, // must NOT match
|
||||||
|
{"AUTHORITY", false}, // must NOT match
|
||||||
|
{"AUTHENTICATE", false}, // must NOT match
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
got := IsSecretEnvVar(tc.name)
|
got := IsSecretEnvVar(tc.name)
|
||||||
|
|
@ -180,20 +227,3 @@ func TestIsSecretEnvVar(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsRedacted(s string) bool {
|
|
||||||
return contains(s, "[REDACTED]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
|
||||||
return len(s) >= len(substr) && searchString(s, substr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchString(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,10 @@ After completing the task, provide a clear summary of what was done.`
|
||||||
var result *ToolResult
|
var result *ToolResult
|
||||||
defer func() {
|
defer func() {
|
||||||
sm.mu.Unlock()
|
sm.mu.Unlock()
|
||||||
// Call callback if provided and result is set
|
// Call callback if provided and result is set.
|
||||||
|
// Sanitize before callback — this path bypasses ExecuteWithContext.
|
||||||
if callback != nil && result != nil {
|
if callback != nil && result != nil {
|
||||||
|
SanitizeResult(result)
|
||||||
callback(ctx, result)
|
callback(ctx, result)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue