module RubyLLM::Browser::Redactor
Sanitization and bounding helpers for anything that could reach a model message, tool result, log line or error.
Page text, model output and provider metadata are untrusted. They are redacted by key name, scrubbed of known secrets, stripped of DevTools URLs, and bounded by size and depth before they leave a backend.
Constants
- DEVTOOLS_URL
A DevTools WebSocket URL anywhere in a string. Full browser control, so it is always removed.
- MAX_DEPTH
Maximum nesting depth kept while redacting.
- MIN_LITERAL_SECRET
Secrets shorter than this are not treated as replaceable literals; replacing very short strings would corrupt ordinary text.
- REDACTED
The replacement for redacted values.
- SECRET_KEY
Keys whose values are always treated as secrets. Matching is case-insensitive and applies to ‘snake_case`, `kebab-case` and `camelCase` spellings.
- TRUNCATED
JSON preview marker when a value is too large to return whole.
Public Instance Methods
Redacts and then bounds value to max_bytes of JSON.
@return [Array(Object, Boolean)] the bounded value and whether it was truncated
# File lib/ruby_llm/browser/redactor.rb, line 77 def bound(value, max_bytes:, secrets: []) unless max_bytes.is_a?(Integer) && max_bytes.positive? raise InvalidInput, "max_bytes must be a positive integer" end redacted = redact(value, secrets: secrets) json = canonical_json(redacted) return [ redacted, false ] if json.bytesize <= max_bytes # Count encoded bytes, not characters: JSON escaping can expand a preview. envelope = { "truncated" => true, "preview" => "", "note" => TRUNCATED } return [ "", true ] if max_bytes < canonical_json(envelope).bytesize && max_bytes >= 2 return [ 0, true ] if max_bytes == 1 low = 0 high = [ json.length, max_bytes ].min while low < high midpoint = (low + high + 1) / 2 envelope["preview"] = json[0, midpoint] if canonical_json(envelope).bytesize <= max_bytes low = midpoint else high = midpoint - 1 end end envelope["preview"] = json[0, low] [ envelope, true ] end
Stable JSON for digests: object keys sorted, no insignificant whitespace.
# File lib/ruby_llm/browser/redactor.rb, line 106 def canonical_json(value) JSON.generate(sort_keys(value)) end
SHA-256 hex digest of the canonical JSON of value.
# File lib/ruby_llm/browser/redactor.rb, line 111 def digest(value) Digest::SHA256.hexdigest(canonical_json(value)) end
Returns a redacted, depth-bounded copy of value.
@param value [Object] @param secrets [Array<String>] exact secret values to remove from strings @param max_depth [Integer] nesting depth at which content is replaced entirely
# File lib/ruby_llm/browser/redactor.rb, line 54 def redact(value, secrets: [], depth: 0, max_depth: MAX_DEPTH) return REDACTED if depth > max_depth case value when Hash value.each_with_object({}) do |(key, nested), result| safe_key = scrub_string(key.to_s, secrets: secrets) result[safe_key] = secret_key?(key) ? REDACTED : redact(nested, secrets: secrets, depth: depth + 1, max_depth: max_depth) end when Array value.map { |item| redact(item, secrets: secrets, depth: depth + 1, max_depth: max_depth) } when String scrub_string(value, secrets: secrets) when Symbol scrub_string(value.to_s, secrets: secrets) else value end end
# File lib/ruby_llm/browser/redactor.rb, line 148 def scrub_string(string, secrets:) scrubbed = string.dup.force_encoding(Encoding::UTF_8).scrub (secrets || []).each do |secret| next if secret.to_s.length < MIN_LITERAL_SECRET scrubbed = scrubbed.gsub(secret.to_s, REDACTED) end scrubbed.gsub(DEVTOOLS_URL, REDACTED) end
True when a key name denotes a secret value.
# File lib/ruby_llm/browser/redactor.rb, line 116 def secret_key?(key) SECRET_KEY.match?(key.to_s) end
# File lib/ruby_llm/browser/redactor.rb, line 135 def sort_keys(value) case value when Hash value.keys.map(&:to_s).sort.to_h do |key| [ key, sort_keys(value.key?(key) ? value[key] : value[key.to_sym]) ] end when Array value.map { |item| sort_keys(item) } else value end end
Removes userinfo, fragments and credential-like query values from provenance.
# File lib/ruby_llm/browser/redactor.rb, line 121 def url(value, secrets: []) uri = URI.parse(value.to_s) uri.userinfo = nil if uri.respond_to?(:userinfo=) uri.fragment = nil if uri.query uri.query = URI.encode_www_form(URI.decode_www_form(uri.query).map do |key, nested| [ key, secret_key?(key) ? REDACTED : nested ] end) end scrub_string(uri.to_s, secrets: secrets) rescue URI::Error, ArgumentError "[REDACTED URL]" end