Module: Pgbus::MCP::Redactor

Defined in:
lib/pgbus/mcp/redactor.rb

Overview

Strips message bodies / headers / job arguments from tool responses before they leave the process. Job payloads can carry PII or secrets, so the diagnostic tools return metadata (ids, counts, ages, read_ct, vt, queue names, job_class) by default and only include the raw payload when an explicit, separately-gated flag is set.

deep_redact is applied centrally at the response boundary (BaseTool.json_response), so redaction is fail-safe: a tool author cannot leak a payload by forgetting to redact — they have to opt in.

See issue #180: “No sensitive payload leakage — diagnostic tools should return metadata … and redact or omit message payloads unless an explicit, separately-gated flag is set.”

Constant Summary collapse

PAYLOAD_KEYS =

Keys whose values are message bodies / headers and must never be returned unless payloads are explicitly allowed.

%i[message headers payload arguments].freeze
REDACTED =

Replacement marker so the agent can see a field exists but was hidden, rather than the field silently vanishing.

"[redacted]"

Class Method Summary collapse

Class Method Details

.deep_redact(value, include_payloads: false) ⇒ Object

Recursively redact payload-bearing keys anywhere in a nested structure of hashes and arrays. This is the fail-safe applied at the response boundary (BaseTool.json_response): a tool that returns a payload key —at any depth, even one its author forgot about — has it stripped unless payloads are explicitly allowed for the call.

A payload key is redacted only when its value is a leaf (the serialized message body / headers string). When the value is a Hash or Array the key is an envelope (e.g. a {…} detail wrapper, not a raw body), so we descend into it instead of blanking the whole subtree.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/pgbus/mcp/redactor.rb', line 45

def deep_redact(value, include_payloads: false)
  return value if include_payloads

  case value
  when Hash
    value.each_with_object({}) do |(key, nested), result|
      result[key] =
        if payload_key?(key) && leaf?(nested) && !nested.nil?
          REDACTED
        else
          deep_redact(nested, include_payloads: include_payloads)
        end
    end
  when Array
    value.map { |element| deep_redact(element, include_payloads: include_payloads) }
  else
    value
  end
end

.leaf?(value) ⇒ Boolean

A leaf is anything that isn’t a structure we recurse into — i.e. the actual serialized payload value rather than an envelope hash/array.

Returns:

  • (Boolean)


67
68
69
# File 'lib/pgbus/mcp/redactor.rb', line 67

def leaf?(value)
  !value.is_a?(Hash) && !value.is_a?(Array)
end

.payload_key?(key) ⇒ Boolean

True if key names a payload-bearing field. Tolerates string or symbol keys and never raises on a non-symbolizable key.

Returns:

  • (Boolean)


31
32
33
# File 'lib/pgbus/mcp/redactor.rb', line 31

def payload_key?(key)
  PAYLOAD_KEYS.include?(key.respond_to?(:to_sym) ? key.to_sym : key)
end