Module: DataRedactor::Integrations::RubyLLM

Defined in:
lib/data_redactor/integrations/ruby_llm.rb

Overview

Transparent outbound redaction for the ‘ruby_llm` gem (crmne/ruby_llm).

Calling RubyLLM.install! prepends a small module onto ‘RubyLLM::Protocol` that deep-redacts the **rendered request payload** before it is posted to any provider. `Protocol#render` is the single point where every provider (Anthropic, OpenAI/chat_completions, Gemini, Bedrock/Converse, Responses) has assembled its final request Hash, so one hook covers them all without knowing any provider-specific shape.

Because the payload is walked with DataRedactor.redact_deep, this scrubs **every String leaf** in the request: the user prompt, the system prompt, tool definitions, and — crucially — any file contents or shell-command output that an agent fed back in as a tool result, since those are already inlined as strings in ‘messages` by the time `render` runs.

This is a monkeypatch (a ‘prepend` onto a private internal class). It is opt-in and pinned: RubyLLM.install! raises unless a supported `ruby_llm` version is loaded and `RubyLLM::Protocol#render` still exists, so an upstream refactor fails loudly at install time rather than silently leaking data. Prefer this only when you need redaction to be transparent; otherwise redact per call with DataRedactor.redact before `chat.ask`.

## What is NOT redacted

  • **Base64 attachments** (PDFs, images, audio sent inline as base64) — the sensitive bytes are encoded, so patterns cannot see into them.

  • **URL-referenced files/images** — the content lives on a remote server and never enters the payload.

Examples:

Make every ruby_llm request redacted, app-wide

require "data_redactor/integrations/ruby_llm"
DataRedactor::Integrations::RubyLLM.install!

chat = RubyLLM.chat(model: "claude-opus-4-8")
chat.ask("my card is 4111111111111111")  # sent as "my card is [REDACTED]"

Scope the redaction with the usual filters

DataRedactor::Integrations::RubyLLM.install!(only: [:financial, :contact])

Defined Under Namespace

Modules: PayloadPatch

Constant Summary collapse

SUPPORTED_VERSION =

ruby_llm versions whose ‘Protocol#render` chokepoint this integration has been verified against. Bump (and re-verify) on each ruby_llm release.

"~> 1.16"

Class Method Summary collapse

Class Method Details

.install!(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT) ⇒ void

This method returns an undefined value.

Prepend the redaction patch onto ‘RubyLLM::Protocol`. Idempotent: a second call with the patch already installed is a no-op (the filter options from the first successful install are kept).

The ‘only:`/`except:`/`placeholder:` filters are captured here and applied to every subsequent request.

Parameters:

Raises:

  • (RuntimeError)

    if ‘ruby_llm` is not loaded, the loaded version is outside SUPPORTED_VERSION, or `RubyLLM::Protocol#render` is missing (i.e. an upstream refactor moved the chokepoint).



63
64
65
66
67
68
69
70
# File 'lib/data_redactor/integrations/ruby_llm.rb', line 63

def install!(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
  ensure_compatible!

  @options = { only: only, except: except, placeholder: placeholder }
  return if installed?

  ::RubyLLM::Protocol.prepend(PayloadPatch)
end

.installed?Boolean

Returns whether the redaction patch is currently on ‘RubyLLM::Protocol`.

Returns:

  • (Boolean)

    whether the redaction patch is currently on ‘RubyLLM::Protocol`.



74
75
76
77
# File 'lib/data_redactor/integrations/ruby_llm.rb', line 74

def installed?
  defined?(::RubyLLM::Protocol) &&
    ::RubyLLM::Protocol.ancestors.include?(PayloadPatch)
end