Skip to content

fix(probe): run the diagnose egress leak self-check; ship payload schema, not values (#199)#214

Merged
pbean merged 2 commits into
mainfrom
fix/probe-egress-guard-199
Jul 20, 2026
Merged

fix(probe): run the diagnose egress leak self-check; ship payload schema, not values (#199)#214
pbean merged 2 commits into
mainfrom
fix/probe-egress-guard-199

Conversation

@pbean

@pbean pbean commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Closes #199.

probe-adapter's report is documented as sanitized and shareable (the adapter-authoring guide's whole point is pasting it into an issue/PR), but it had neither of the two egress protections diagnose relies on for the same expectation: no assert_no_leak backstop over the rendered bytes, and no Pseudonymizer — identifier-shaped values passed the collection scrubbers verbatim by design. probe.py's own docstring tracked the gap and cited #199.

What changed

One audited guard. The guard loop (_guard / _repair_candidates / _assert_clean / LeakDetected) moved from diagnostics.py into sanitize.py as public guard / assert_clean / LeakDetected. Diagnostics delegates and re-exports LeakDetected (noqa-pinned; an identity test catches the ruff-F401-autofix regression). The diagnostics canary/zero-repair/encoding suites pass unchanged — the proof the move preserved semantics byte-for-byte.

Payloads ship as shape, never values. EventCapture.payload (a whole scrubbed hook-payload dict — the widest identifier-shaped surface) is replaced by payload_schema: dotted key paths + leaf types via the existing _walk_paths (tool_input.command:str), which is the part profile authoring actually needs. payload_keys stays but is identifier-gated. --json schema_version bumps 1 → 2 (captured_events[].payload removed — a field removal, per the machine.py additive-only contract).

Guard on the exact emitted bytes. Both probe renderers run sanitize.guard before returning; render_json now dumps ensure_ascii=False (same safety requirement as diagnose — the guard must see the string as itself, and machine.emit/write_document emit it verbatim). cmd_probe registers the project directory name as a project-ns alias (skipped if it collides with the CLI/binary name), routes it into location redaction at collection time (zero repairs on the canonical run), and on a hit fails closed per the #195 contract: message → stderr, empty stdout, exit ≠ 0, no partial --out file. A stray alias original is repaired and disclosed (backstop_repairs + stderr warning), exactly like diagnose.

Collection hardening the guard surfaced immediately. Running the new guard against the existing test suite caught a live leak: transcript-location components that embed the current username shipped verbatim (pytest-of-<user> in every CI run of the PII tests; any user-named $TMPDIR in production). Path components embedding the username are now redacted; also hardened: home-rooted --binary hints render ~-relative in the summary and the not-found warning, credential-shaped dict keys (ghp_… as a map key is identifier-shaped) collapse to <key> in key paths, and --keep-temp prints a ~-relative tmpdir.

Verification

  • Full suite: 2590 passed, 1 skipped; trunk check --no-fix --all clean.
  • Prove-regression: with src/ stashed to the pre-fix tree, the 13 new/extended egress tests all fail.
  • New coverage: fail-closed renderers, CLI refusal contract (both modes, --out absence), _collect_captures schema-not-values, end-to-end egress through the real capture-hook subprocess seam, location aliasing zero-repair invariant, backstop repair of a stray project name in key paths, non-ASCII guard fidelity, guard mechanics ported to test_sanitize.py.
  • Live smoke on this repo, both modes: rc 0, schema_version: 2, zero occurrences of the username or project name, zero repairs; --json parses whole.

Out of scope

#202 (diagnose _scrub_policy dict-key gap — sibling class, untouched), #212 (documents.py rename), machine.py behavior, the capture hook script (raw retention inside the transient tmpdir is correct by design), _walk_paths depth-guarding (pre-existing).

Summary by CodeRabbit

  • Breaking Changes

    • Probe reports now use schema version 2.
    • Captured hook data now includes payload field/key-path schemas (field names + leaf types), not payload values.
    • Existing captured_events[].payload entries are replaced by captured_events[].payload_schema.
  • Security Enhancements

    • Rendered reports are revalidated before emission; sensitive detections fail closed.
    • Added backstop repair behavior for stray pseudonymized originals; tightened transcript/path redaction and credential-shaped key handling.
    • On unsafe output, no report is produced and standard output is suppressed (including JSON mode).
  • Documentation

    • Updated adapter-authoring guidance to reflect the new schema-centric privacy model and output validation behavior.

…ema, not values (#199)

probe-adapter's report is documented as sanitized/shareable but had neither
of diagnose's two egress protections: no assert_no_leak backstop over the
rendered bytes, and no Pseudonymizer, so identifier-shaped values passed the
collection scrubbers verbatim.

- promote the guard loop (_guard/_repair_candidates/_assert_clean/
  LeakDetected) from diagnostics.py into sanitize.py as public
  guard/assert_clean/LeakDetected — one audited implementation; diagnostics
  delegates and re-exports LeakDetected
- reduce EventCapture.payload to payload_schema (key-path:type via the
  existing _walk_paths; no payload value of any kind ships) and
  identifier-gate payload_keys; --json schema_version 1 -> 2
- guard both probe renderers on their exact emitted bytes
  (ensure_ascii=False); cmd_probe registers the project dir name as a
  project-ns alias (collision-skipped vs CLI/binary names) and fails closed
  per the #195 contract: stderr message, empty stdout, exit != 0, no --out
- collection hardening the guard surfaced live: path components embedding
  the current username are redacted (pytest-of-<user> shipped verbatim
  before), home-rooted --binary hints render ~-relative, credential-shaped
  dict keys collapse to <key> in key paths, --keep-temp prints a ~-relative
  tmpdir

Closes #199
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03502ca1-34c6-4d2c-9049-c2648208f648

📥 Commits

Reviewing files that changed from the base of the PR and between ed9f52f and 1dcc211.

📒 Files selected for processing (2)
  • src/bmad_loop/probe.py
  • tests/test_probe.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/bmad_loop/probe.py

Walkthrough

Probe reports now emit captured payload schemas instead of values, apply alias-aware redaction, and pass final Markdown or JSON through a shared fail-closed egress guard. CLI output handling, diagnostics integration, documentation, changelog entries, and security tests are updated accordingly.

Changes

Probe egress safety and schema hardening

Layer / File(s) Summary
Shared sanitizer guard and repair primitives
src/bmad_loop/sanitize.py, tests/test_sanitize.py
Adds shared leak detection, bounded pseudonymization repair, clean-output validation, username detection, and related tests.
Schema-only capture and redacted discovery
src/bmad_loop/probe.py, tests/test_probe.py
Changes captured events to schema version 2 with payload key paths and leaf types, while tightening transcript, binary, dynamic-key, and username-derived redaction.
Guarded report rendering and shared diagnostics
src/bmad_loop/probe.py, src/bmad_loop/diagnostics.py, src/bmad_loop/machine.py, tests/test_diagnostics.py
Validates serialized Markdown and JSON through the shared guard, records repairs, revalidates final bytes, and centralizes LeakDetected.
CLI fail-closed wiring and contract coverage
src/bmad_loop/cli.py, tests/test_probe.py
Threads pseudonymization through probing and rendering, refuses leaked output, reports repairs, and validates refusal and schema behavior.
Documentation and release contract
docs/adapter-authoring-guide.md, CHANGELOG.md
Documents schema-only capture, guarded emission, redaction rules, and breaking output changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant probe
  participant sanitize.guard
  participant Output
  CLI->>probe: run scan or probe with pseudonymizer
  probe-->>CLI: schema-based finding
  CLI->>sanitize.guard: validate rendered report
  sanitize.guard-->>CLI: clean output or LeakDetected
  CLI->>Output: emit validated bytes
Loading

Possibly related PRs

Poem

I’m a rabbit guarding each report,
No secret crumbs may pass the port.
Keys become shapes, paths disguise,
Clean bytes safely hop the skies.
If leaks appear, I thump and refuse.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main changes: adding the egress leak self-check and switching probe output to payload schema instead of values.
Linked Issues check ✅ Passed The PR adds a final-output leak guard, uses schema-only payload capture, and covers identifier-shaped leaks and probe egress as requested.
Out of Scope Changes check ✅ Passed The changes stay focused on probe egress safety, schema output, redaction hardening, docs, and tests; no unrelated feature work stands out.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/probe-egress-guard-199

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

`_redact_location` filtered only "/" out of `path.parts`, but on Windows a
rooted path's anchor is "\\". It survived the filter, reached the identifier
gate, and was struck as if it were a component -- prepending a phantom
"<redacted>" to every non-home absolute path in a probe report.

The root separator is structure, not a component, so it is dropped on both
flavours. A drive or UNC anchor does carry content, so it still goes through
the gate.

Pinned by a test driven through PureWindowsPath, so the Linux legs catch this
class of bug instead of only the Windows ones.
@pbean
pbean merged commit 5ccf814 into main Jul 20, 2026
9 checks passed
@pbean
pbean deleted the fix/probe-egress-guard-199 branch July 20, 2026 08:26
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.

probe-adapter report has no egress leak guard and no pseudonymizer

1 participant