Module: Kodo::Memory::Redactor

Defined in:
lib/kodo/memory/redactor.rb

Constant Summary collapse

PLACEHOLDER =
"[REDACTED]"
SENSITIVE_PATTERNS =
[
  /\b\d{3}-\d{2}-\d{4}\b/,                          # SSN
  /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/,    # Credit card
  /\b(sk|pk|api|key|token|secret|password)[-_]?\w{10,}/i, # API keys/tokens
  /\bpassword\s*[:=]\s*\S+/i,                        # password: value
].freeze
LLM_PROMPT =
<<~PROMPT
  You are a sensitive data classifier. Analyze the following message and identify any sensitive information that should be redacted before storage. This includes but is not limited to:
  - Passwords, passphrases, or secrets mentioned in natural language
  - API keys, tokens, or credentials
  - Personal identifiers (SSN, credit card numbers, etc.)
  - Private keys or certificates

  Return ONLY a JSON array of objects with "start" and "end" character offsets (0-based, exclusive end) for each sensitive span. If nothing is sensitive, return an empty array [].

  Example: for "my database password is fluffybunny and that's it"
  Response: [{"start": 24, "end": 35}]

  Message to analyze:
PROMPT

Class Method Summary collapse

Class Method Details

.redact(text) ⇒ Object

Regex-only redaction (fast, free)



38
39
40
41
42
43
44
# File 'lib/kodo/memory/redactor.rb', line 38

def redact(text)
  result = text.dup
  SENSITIVE_PATTERNS.each do |pattern|
    result.gsub!(pattern, PLACEHOLDER)
  end
  result
end

.redact_smart(text) ⇒ Object

Layered redaction: regex first, then LLM for anything regex missed



47
48
49
50
51
52
53
# File 'lib/kodo/memory/redactor.rb', line 47

def redact_smart(text)
  if sensitive?(text)
    redact(text)
  else
    redact_with_llm(text)
  end
end

.redact_with_llm(text) ⇒ Object

LLM-assisted redaction for context-dependent secrets



56
57
58
59
60
61
62
63
64
65
# File 'lib/kodo/memory/redactor.rb', line 56

def redact_with_llm(text)
  response = Kodo::LLM.utility_chat.ask("#{LLM_PROMPT}#{text}")
  spans = parse_spans(response.content)
  return text if spans.empty?

  apply_redactions(text, spans)
rescue StandardError => e
  Kodo.logger.debug("LLM redaction skipped: #{e.message}")
  text
end

.sensitive?(text) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
# File 'lib/kodo/memory/redactor.rb', line 33

def sensitive?(text)
  SENSITIVE_PATTERNS.any? { |pattern| text.match?(pattern) }
end