fix(redaction): close forgery gap in event-level hook (v2.0.2) - #51
Merged
Conversation
…ration Adversarial review of the redact_event hook found the RESTORED_FIELDS set was too narrow — a hook could forge or erase actor identity, tags, and properties despite those being protected from the string-level hook. Widen it to match, tighten the hook's typing to RedactEventFunction/Event|None so a non-conforming return is a type error rather than a silent contract, document the mutate-and-return / full-dump-not-diff contract everywhere a hook author would read it, drop the undocumented "or times out" claim, and dedupe the id-generation snippet in event_queue.py into one helper. Bump to 2.0.2 (patch).
RESTORED_FIELDS omitted client_name/client_version/server_name/server_version, so a redact_event hook could forge or erase MCP client/server identity even though those fields are protected from the string-level hook. The restored tags/properties/identify_data were also aliased rather than copied, letting in-place mutation of one event's dict corrupt another that shared the reference. Also backfills test coverage: two tests that passed regardless of the behavior they claimed to check, plus new coverage for SystemExit handling in the event-level hook, an async hook resolving to None, the original-event- unmutated invariant, a hook returning a non-Event value, server/client field protection, and the partial-Event field-nulling behavior.
kashishhora
approved these changes
Aug 8, 2026
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.
Summary
Follow-up to #50 (
redact_eventevent-level redaction hook), based on an adversarial code review of that change:RESTORED_FIELDS— the event-levelredact_eventhook could previously forge or erase actor-identity fields (actor_id,identify_actor_given_id,identify_actor_name,identify_data),tags/properties, and client/server identity fields (client_name,client_version,server_name,server_version), even though those same fields are protected from the string-levelredact_sensitive_informationhook viaPROTECTED_FIELDS. Now restored after the hook runs, same asid/session_id/project_id/event_type/timestamp.tags/properties/identify_datawere the same dict objects as the original event, not copies; mutating one event's dict in place could silently corrupt another event sharing the reference. Now deep-copied._sync_event_redactorandapply_event_redactionnow takeRedactEventFunctionand returnEvent | Noneinstead ofCallable[..., Any], so a hook that doesn't conform to theEvent | Nonecontract is a type-checker error, not a silent runtime surprise.Eventit's handed (the return value replaces the event's fields wholesale, not as a diff — a partial/freshEventsilently nulls the rest), and the pydantic-Eventreturn type is enforced by static typing only, not at runtime. Added to the docstrings onapply_event_redaction, theRedactEventFunctiontype alias,AgentCatOptions.redact_event, and the README example.if not event.id: event.id = generate_prefixed_ksuid(...)snippet was copy-pasted three times inevent_queue.py's_process_event(one variant hardcoding"evt"instead of theEVENT_ID_PREFIXconstant); extracted into one_ensure_event_idhelper.event_redaction_fn-clearing test whose mock fixture pre-baked the expected result, and a function-fields-exclusion test that couldn't detect a missing exclusion because the hook's input type never declares those fields anyway), and added coverage forSystemExithandling in the event-level hook, an async hook resolving toNone, the original-event-unmutated invariant, a hook returning a non-Eventvalue, client/server field protection, and the partial-Event field-nulling behavior. Every new/fixed test was mutation-tested against the behavior it covers.2.0.1→2.0.2(patch).Test plan
pytest tests/test_redaction.py tests/test_event_queue.py— all passing, including new coverage for actor/tags/properties/client/server forgery resistancepytest(full suite) — all passing (828 collected)mypy src/agentcat— no new errors vs. baseline (pre-existing, unrelatedagentcat_apistub-package errors only)