Repository navigation
fix(security): drop the never-flipped secure_error_class! switch; log LLM errors via safe_message (#60) - #68
Open
minerva-sky wants to merge 2 commits into
Conversation
… LLM errors via safe_message SecureErrorMixin shipped two halves: explicit safe_* accessors that LlmClient, Task, and TaskFailure call, and a class-level secure_error_class! switch meant to rewrite #message/#to_s/#inspect at construction. Nothing ever called the switch, so LlmError#message was never sanitized despite the mixin's header promising "automatic sanitization". Remove the dead half and say what the mixin actually does: sanitization is opt-in through safe_*, to_secure_hash, and log_securely. LlmClient had seven log lines writing error.message raw (provider, timeout, parse, unexpected, models, generation stats) while the retry path carefully used log_securely. Route all seven through safe_message. Specs pin the contract both ways: #message stays raw, safe_* redacts, and a provider error echoing an API key logs as [REDACTED_API_KEY]. Closes codenamev#60 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The panel caught that routing the LlmClient rescue blocks through safe_message put a regex gsub on provider-supplied text ahead of handle_error. A provider error body with invalid UTF-8 made gsub raise ArgumentError, which escaped the rescue and broke fail_on_error: false. Scrub the message before sanitizing and withhold it (never fall back to raw) if the sanitizer still fails. Specs cover both layers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #60.
SecureErrorMixinpromised "automatic sanitization of sensitive data in error messages" and delivered it through a class-level switch,secure_error_class!, that nothing ever flipped.LlmErrorincludes the mixin and never calls it, soe.messagewas always raw while the header told the next reader it was clean. That is the worst kind of security control: it exists only in the docstring, and it tells people to stop checking.This PR picks option 1 from the issue.
What changes
secure_error_class!,ClassMethods, theincludedhook, and the privateapply_security_sanitizationare gone. The mixin now says what it does: sanitization is opt-in throughsafe_message,safe_context,safe_response,safe_backtrace,to_secure_hash, andlog_securely.#message,#to_s, and#inspectare untouched, and a spec pins that.LlmClienthad seven log lines writingerror.messageraw (provider, timeout, parse, unexpected,models,query_generation_stats) while the retry path carefully usedlog_securely. The issue counted six; the seventh was inquery_generation_stats. All seven now go throughsafe_message. The returned or raised error keeps the raw message, so nothing a consumer rescues changes.safe_messagecan no longer raise. The panel found that moving a regexgsubinto every rescue block let a provider error body with invalid UTF-8 blow upgsub, escape the rescue, and breakfail_on_error: false. Reproduced on this machine:"sk-abc \xFF\xFE".force_encoding("UTF-8")raisedArgumentError: invalid byte sequenceout ofsafe_message. The message is now scrubbed before sanitizing, and if the sanitizer still fails the log line reads[message withheld: sanitizer raised <class>]. Never the raw text.Semver
The mixin arrived in #7 on 2026-09-02. The last published version is 0.2.0 from 2025-06-05, so
secure_error_class!has never shipped and nobody can have called it. CHANGELOG records the removal under Unreleased anyway.Not in this PR
RetryHandlerstill logserror.messageraw on its retry and give-up lines, andPlanOrchestratorlogsfailure.message. Same shape as theLlmClientlines, pre-existing, and a separate change; one seat raised it and I'd rather keep this diff to the issue's scope. Worth a follow-up.Tests
Full suite: 1396 examples, 1 failure, and the failure is
event_dispatcher_integration_spec.rb:291, the wall-clock flake that fails identically on untouchedmain(documented in #29 and #64). Five new examples cover the opt-in contract, the sanitized log line, the invalid-bytes path, and the sanitizer-raises fallback.Intended labels:
loop:self.Panel
alias_method/define_methodrewrite hook removes abstraction; leaving#messagealone is the Ruby contract. Nit noted: sevensafe_messagecall sites is repetitive, but a helper would be the one-use abstraction the charter forbids.safe_messageinside a rescue could raise on invalid UTF-8 and breakfail_on_error: false. Fix: scrub before sanitizing, rescue into a withheld placeholder, specs at both layers. Second pass cleared every criterion.Security::Configinafter;safe_messageis memoized and only runs on the error path.🤖 Generated with Claude Code