diff --git a/workspace-server/internal/handlers/mcp.go b/workspace-server/internal/handlers/mcp.go index c02f9bb9..a15ece4b 100644 --- a/workspace-server/internal/handlers/mcp.go +++ b/workspace-server/internal/handlers/mcp.go @@ -31,6 +31,7 @@ import ( "net/http" "net/url" "os" + "regexp" "strings" "time" @@ -715,6 +716,42 @@ func (h *MCPHandler) toolSendMessageToUser(ctx context.Context, workspaceID stri return "Message sent.", nil } +// redactSecrets scans content for credential-like patterns and replaces them with +// [REDACTED]. This prevents plain-text API keys, tokens, and passwords from +// landing in the agent_memories table (fixes #838). +// +// The workspaceID parameter is available for audit logging in future enhancements. +// Currently unused but reserved for structured audit trail. +// +// Patterns matched (case-insensitive): +// - Generic credentials: "key", "secret", "password", "token", "api_key", +// "api-key", "auth", "bearer", "credential", "passphrase" +// - Prefix patterns: (k="", k='', k:'', k:"", k: "") +// - Token patterns: Bearer , Token +// - Variable assignments: KEY_NAME=value, API_KEY_NAME=value +// +// The redaction is conservative - it only masks the value portion, not the +// surrounding context, so the memory remains human-readable for audit/debugging. +func redactSecrets(workspaceID string, content string) string { + // Generic credential word boundaries. + content = regexp.MustCompile(`(?i)(key|secret|password|token|api_?key|auth|bearer|credential|passphrase)[:=\s]*([a-zA-Z0-9_\-+=/]{8,})`). + ReplaceAllString(content, "$1=[REDACTED]") + + // Bearer/Token label patterns. + content = regexp.MustCompile(`(?i)(bearer|token)\s+([a-zA-Z0-9_\-+=/]{16,})`). + ReplaceAllString(content, "$1 [REDACTED]") + + // ENV-style KEY=VALUE pairs where the key looks like a credential name. + content = regexp.MustCompile(`(?i)([A-Z][A-Z0-9_]*(?:KEY|SECRET|PASSWORD|TOKEN|API|AUTH)[A-Z0-9_]*)=([^\s]{8,})`). + ReplaceAllString(content, "$1=[REDACTED]") + + // JSON/ini-style "key": "value" or 'key': 'value' with long values. + content = regexp.MustCompile(`(?i)"(key|secret|password|token|api_?key|auth|bearer)":\s*"([a-zA-Z0-9_\-+=/]{8,})"`). + ReplaceAllString(content, `"$1": "[REDACTED]"`) + + return content +} + func (h *MCPHandler) toolCommitMemory(ctx context.Context, workspaceID string, args map[string]interface{}) (string, error) { content, _ := args["content"].(string) scope, _ := args["scope"].(string)