Skip to content

fix(privacy): quarantine LCM credential-bearing keys instead of Receipt - #541

Merged
ScriptedAlchemy merged 7 commits into
cursor/rusqlite-runtime-maintenance-checkpoint-port-2ce6from
cursor/lcm-quarantine-not-receipt-ea75
Aug 20, 2026
Merged

fix(privacy): quarantine LCM credential-bearing keys instead of Receipt#541
ScriptedAlchemy merged 7 commits into
cursor/rusqlite-runtime-maintenance-checkpoint-port-2ce6from
cursor/lcm-quarantine-not-receipt-ea75

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes the live-log error storm (685 counts of privacy sanitizer receipt construction failed): a successfully parsed LCM/structured JSON payload whose object keys carry credential material now surfaces as its own fail-closed quarantine (DetectionError::CredentialKeyQuarantine), never as DetectionError::Receipt.
  • Splits StructuredSanitizationError into truthful typed states so quarantines, detector unavailability, and canonical-encoding faults are no longer collapsed into one variant.
  • Runtime-core crate only; no WAL/truncate, ENOSPC, or purge (fix(privacy): close the remaining Codex P1 gaps after #522 merged #537) work.

Motivation

The usecases LCM projection drain calls sanitize_lcm_payload_text. When redact_sensitive_values returns quarantine_findings (an object key carrying credential material, which cannot be redacted in place without rewriting the document's structure), sanitize_parsed/sanitize_malformed collapsed that into StructuredSanitizationError::SanitizerUnavailable, and detection_error_from_structured_sanitization mapped InvalidLimits | SanitizerUnavailable → DetectionError::Receipt. The sanitizer's own fail-closed quarantine was therefore labelled as a receipt-construction fault in every projection-drain refusal — a misclassification, not a broken SanitizationReceiptV1 constructor. (sanitize_memory_fact_payload already treats the same findings as MemoryFactSanitizationV1::Quarantined.)

Changes

  • src/privacy/structured.rs
    • New StructuredSanitizationError::CredentialKeyQuarantine: returned by sanitize_parsed/sanitize_malformed when quarantine_findings are present.
    • New StructuredSanitizationError::CanonicalEncoding: returned by validate_expansion when the payload cannot be canonically re-encoded (a real construction fault).
    • SanitizerUnavailable now means exactly one thing: the detector kernel failed to initialize (credential_patterns() compile failure).
  • src/privacy/detect.rs
    • New DetectionError::CredentialKeyQuarantine with its own Display (privacy sanitizer quarantined credential-bearing keys), so key quarantine does not share the "ambiguous structured document" message with parse ambiguity (Codex P2 follow-up).
    • DetectionError now derives PartialEq, Eq (matching StructuredSanitizationError's derive set) so tests can assert the typed variant directly.
    • Receipt remains solely for real receipt/id/canonical construction failures (issue_text_receipt, memory_fact_receipt, quarantine_lcm_payload_text, serde_json::to_string after a real Value, SanitizationReceiptV1::new).
  • src/privacy/structured_text.rs
    • detection_error_from_structured_sanitization: CredentialKeyQuarantine → DetectionError::CredentialKeyQuarantine; UnsafeJsonStructure | InvalidEncoding → StructuredQuarantine; InvalidLimits | SanitizerUnavailable → Initialization (a misconfigured or unavailable detector is an initialization failure, not a receipt fault); CanonicalEncoding → Receipt. Limit overruns stay ScanLimitExceeded.
    • The two call sites that bypassed the mapping with blanket map_errs (StructuredSanitizationLimits::new → Receipt, validate_structured_text_limits → ScanLimitExceeded) now route through it.

Downstream callers (tracedecay-sessions raw.rs, tracedecay-global-db registered_lcm_privacy.rs) format the error into LcmError::SanitizationRefused { reason } and never match on DetectionError variants, so no other crate changes are needed; cargo check -p tracedecay-sessions -p tracedecay-global-db -p tracedecay-usecases passes on this branch.

Test plan

  • Baseline captured on the tip before the fix: lcm_json_credential_bearing_keys_quarantine_instead_of_faulting_the_receipt fails (payload yields Receipt, not a quarantine)
  • New isolated tests pass after the fix (pure functions, no ~/.tracedecay or repo .tracedecay/ access):
    • privacy::structured_text_tests::lcm_json_credential_bearing_keys_quarantine_instead_of_faulting_the_receipt (asserts the typed variant and its exact Display)
    • privacy::structured_text_tests::lcm_json_credential_values_under_ordinary_keys_still_redact_durably (credential in value position stays a durable redaction, not quarantine)
    • privacy::structured_tests::credential_bearing_object_keys_are_a_typed_quarantine_state (asserts CredentialKeyQuarantine, proving no SanitizerUnavailable on that path)
  • All 107 privacy:: tests in tracedecay-runtime-core pass, including existing StructuredQuarantine/ScanLimitExceeded contract tests
  • Full cargo test -p tracedecay-runtime-core: 656 passed, 2 failed — both failures (store_runtime::resolver::tests::{the_running_platform_mounts_an_ordinary_local_store, project_graph_locator_is_shared_while_code_scope_resolution_fails_closed}) reproduce identically on the untouched base tip in the same environment (FilesystemLocalityUnverified { filesystem_type: "overlay" } — the dev VM's overlayfs), so they are pre-existing environmental failures unrelated to this change
  • cargo clippy -p tracedecay-runtime-core --all-targets: no warnings

Checklist

  • No secrets, credentials, or .env files included
  • Breaking changes documented: DetectionError gains the CredentialKeyQuarantine variant and PartialEq, Eq derives; no workspace caller matches exhaustively on it, and dependent crates compile unchanged
Open in Web Open in Cursor 

@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4842360

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ScriptedAlchemy
ScriptedAlchemy force-pushed the cursor/lcm-quarantine-not-receipt-ea75 branch from 72eceb6 to 59acc77 Compare August 19, 2026 23:58
@ScriptedAlchemy
ScriptedAlchemy changed the base branch from codex/tracedecay-total-redesign-plan to cursor/rusqlite-runtime-maintenance-checkpoint-port-2ce6 August 19, 2026 23:58
@ScriptedAlchemy
ScriptedAlchemy force-pushed the cursor/lcm-quarantine-not-receipt-ea75 branch 2 times, most recently from 8b61818 to 1c98cc4 Compare August 19, 2026 23:59
@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review August 20, 2026 00:00
@ScriptedAlchemy
ScriptedAlchemy force-pushed the cursor/lcm-quarantine-not-receipt-ea75 branch from 084f981 to 0e1d9a8 Compare August 20, 2026 00:00
@ScriptedAlchemy
ScriptedAlchemy marked this pull request as draft August 20, 2026 00:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c98cc4869

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/tracedecay-runtime-core/src/privacy/detect.rs

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed crate-only. Quarantine stays Quarantined: CredentialKeyQuarantineDetectionError::StructuredQuarantine, and Receipt is only for real construction (CanonicalEncoding / payload-reference). Tests cover key-quarantine vs value-redaction. That matches the lock.

Two nits, not blockers:

  1. DetectionError::StructuredQuarantine still displays as "quarantined an ambiguous structured document". Credential-key refusals will log as parse-ambiguity, which is the same class of misread that produced the 685-count Receipt storm. Tighten the Display (or split a variant) so key-quarantine does not look like a parse failure.

  2. sanitize_malformed now also returns CredentialKeyQuarantine when quarantine_findings is non-empty. That path wraps the raw text as Value::String. Confirm a credential value in malformed text still redacts and does not take this new variant (the new LCM test only covers parsed objects).

Not wiring Database. Not flattening. Not merging.

Comment thread crates/tracedecay-runtime-core/src/privacy/structured_text.rs Outdated
Comment thread crates/tracedecay-runtime-core/src/privacy/detect.rs

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep re-check on 3cfb111. LCM JSON key-quarantine is right: detection_error_from_structured_sanitization maps to DetectionError::CredentialKeyQuarantine and the LCM test asserts the tight Display. CanonicalEncoding stays Receipt. Quarantine stays Quarantined.

Leftover Display routing: the non-JSON detect_lcm_payload branch and sanitize_code_source_bytes still fold quarantine_findings into DetectionError::StructuredQuarantine ("ambiguous structured document"). YAML/TOML/dotenv credential keys and declared structured code sources will log as parse-ambiguity again. Route those findings to CredentialKeyQuarantine. Leave StructuredQuarantine as parse-ambiguity only.

sanitize_malformed wrapping Value::String is safe — no object keys, and malformed_json_is_scanned_without_claiming_structural_parse already redacts a credential value. Not merging. Not changing the base.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked 2d1137a. quarantine_detection_error is the right split: MalformedRecord → StructuredQuarantine (parse-ambiguity Display), everything else → CredentialKeyQuarantine. Both leftover sites (detect_lcm_payload non-JSON and sanitize_code_source_bytes) use it. Doc that the two finding classes never mix matches the emit sites. LCM JSON path still goes through detection_error_from_structured_sanitization. Mapping lock holds. Not merging. No restack.

ScriptedAlchemy added a commit that referenced this pull request Aug 20, 2026
#538 landed on this branch and conflicted with #540 in attachment.rs.
Take the current #540 file (pub(crate) drain, blockers-before-drain).
Do not change the #540 branch.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-review of draft b507b5c2b. Comments only. Not editing runtime-core. Not changing the base. Not retriggering CI.

Crate — not blocking ready: LCM key-quarantine still splits correctly. quarantine_detection_error maps PrivacyDetectorV1::MalformedRecord to DetectionError::StructuredQuarantine (parse-ambiguity Display) and everything else to CredentialKeyQuarantine. Neither path is a Receipt. Merge kept #540's attachment.rs (pub(crate) drain, blockers-before-drain). Mergeable vs that parent.

Blocking ready: draft; stacked on #540 (clippylint fix f534aa6f9 just pushed, not actually green). Commit Messages already failed; rest of the suite is still in flight — not mine, not retriggering. Ready watch is empty until Zack names one.

Leftover: no remaining crate bug from this pass. Stack/CI only.

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review August 20, 2026 03:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b507b5c2bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/tracedecay-runtime-core/src/privacy/structured_text.rs
Comment on lines +443 to +445
if task_consumes_session_evidence(task)
&& let Some(exceeded) = live_session_evidence_budget_exhaustion(records, task)?
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist exhaustion for immediate runner calls

This backoff only activates when an exhausted skip exists in the ledger, but the public immediate session-reflector and skill-writer runners return retrieval rejections through unpersisted_rejected_parts (for example, session_reflector.rs:683-694) before their immediate publication path appends anything. Thus callers of run_session_reflector_with_backend[_and_retrieval] or the analogous skill-writer APIs repeatedly execute the same budget-exhausted retrieval on every invocation because this lookup never receives an anchor; ensure those immediate rejection paths publish their terminal record before returning.

AGENTS.md reference: AGENTS.md:L75-L77

Useful? React with 👍 / 👎.

@cursor
cursor Bot force-pushed the cursor/lcm-quarantine-not-receipt-ea75 branch from b507b5c to ae73d9a Compare August 20, 2026 04:43
cursoragent and others added 4 commits August 20, 2026 05:55
A successfully parsed LCM JSON payload whose object keys carry
credential material was collapsed into
StructuredSanitizationError::SanitizerUnavailable and then mapped to
DetectionError::Receipt, so the sanitizer's own fail-closed quarantine
surfaced as 'privacy sanitizer receipt construction failed' in every
projection-drain refusal. Give quarantine findings their own typed
state (CredentialKeyQuarantine -> StructuredQuarantine), keep
SanitizerUnavailable for detector initialization failure
(-> Initialization, with InvalidLimits), and reserve Receipt for real
canonical/receipt construction faults (new CanonicalEncoding).

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Codex P2: key quarantine no longer shares the "ambiguous structured
document" message. Parse ambiguity stays StructuredQuarantine.
The credential-key quarantine test asserts variant equality directly,
which requires PartialEq on the public error enum. Matches the derive
set already carried by StructuredSanitizationError.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
detect_lcm_payload's non-JSON route and sanitize_code_source_bytes
collapsed every non-empty quarantine-finding set into
DetectionError::StructuredQuarantine, so a parsed TOML/YAML document
whose keys carry credential material displayed as parse ambiguity.
Route by finding: a malformed-record finding stays the parse-ambiguity
quarantine; key-anchored findings from a parsed document surface as
CredentialKeyQuarantine, matching the JSON container path.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/lcm-quarantine-not-receipt-ea75 branch from a8ea1ac to 0103cff Compare August 20, 2026 06:03
cursoragent and others added 2 commits August 20, 2026 06:07
Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…n-graph-port-cb93

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Addressed in 4842360. Parsed sensitive fields whose byte span cannot be proven now return the distinct DetectionError::SensitiveFieldQuarantine; malformed records remain StructuredQuarantine and actual credential-bearing keys remain CredentialKeyQuarantine. Added direct LCM and structured code-source regression tests. Exact head evidence: runtime-core 663/663 library tests, strict runtime-core clippy, and the previously failing snapshot test passed 10/10 in isolated anti-vacuity runs.

@ScriptedAlchemy
ScriptedAlchemy merged commit 95649a0 into master Aug 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants