Skip to content

fix(sparrow): redact pasted secrets before task-file persistence (#2267 parity) - #2301

Merged
qingyun-wu merged 11 commits into
mainfrom
fix/ag2space-chat-secret-redaction
Jul 30, 2026
Merged

qingyun-wu merged 11 commits into
mainfrom
fix/ag2space-chat-secret-redaction

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

What

Ports the #2267 chat-secret redaction to the gateway task path. The discord/slack/telegram bridges already run chat_secret_filter before persisting inbound text; the gateway path still wrote room text raw — a token pasted into an observed room landed on disk unredacted in tasks/*.txt and in the last-owner-activity.json presence summary.

How

  • chat_secret_filter.py is bundled into the package via the existing sync map (verbatim copy from src/, drift-guarded by test_no_drift) — same mechanism as the other pure shared modules. In package context the optional secret_scanner dependency is absent, so the curated fallback patterns run (graceful, fail-closed design already in the module).
  • _write_task filters the task body after media-marker resolution (a signed media-proxy URL is consumed intact; only the resolved text is filtered) and logs detected secret types, never values.
  • The owner-presence summary is filtered identically before write.

Testing

  • New regression in src/remote-gateway-bridge.test.py: GitHub-shaped token in a task body → explicit placeholder in the persisted file, absent from the presence file.
  • Full bridge behavioral suite green; sparrow ack-retry / inflight-recovery / write-task-atomic suites green; package drift guard green.

Found by the bridge-parity scan (gap ① of 2; gap ② — media content-type guard — is queued separately).

🤖 Generated with Claude Code

 parity for the gateway path

The discord/slack/telegram bridges gained chat_secret_filter in #2267; the
gateway task path still persisted room text raw — a token pasted into an
observed room landed on disk unredacted in tasks/*.txt AND in the
last-owner-activity presence summary. The filter module is now bundled into
the package via the sync map (verbatim from src/, drift-guarded), and
_write_task runs it on the task body AFTER media-marker resolution (signed
media URLs are consumed intact; only the resolved text is filtered), logging
detected types (never values). The owner-activity summary is filtered the
same way. Regression: GitHub-shaped token in a task body → placeholder in
the task file, absent from the presence file.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The redaction core is solid — verified filter_chat_secrets: fallback regex patterns are reasonable (GitHub/AWS/Slack/Stripe/Discord/Telegram token shapes, PEM keys, JWTs), the optional secret_scanner library path correctly re-runs the fallback patterns over its output rather than dropping them, _actionable_hits sensibly gates entropy-only hits behind a co-located token-shaped run to avoid over-redacting prose, and _redact_unhandled_hit_lines blanks a whole line rather than risk a partial leak when the library detects something the fallback patterns didn't fully replace. _write_task correctly filters after media-marker resolution so signed proxy URLs survive intact. Good test coverage on the redaction itself (test_no_drift, the new remote-gateway-bridge.test.py case).

One real gap versus the stated goal, though: this only ports HALF of what discord-bridge.py/slack-bridge.py/telegram-bridge.py do for #2267. Those three call secret_handling_instruction(surface, detected_secret_types) and concatenate it into the persisted task body (see discord-bridge.py:3204 + :3557) — the in-band notice that tells the core not to reproduce/re-request the redacted value, and (for Discord/Slack) to advise the sender to delete the original message.

chat_secret_filter.py in this PR defines secret_handling_instruction (line 132) but it's never called anywhere in remote_gateway_bridge.py's _write_task — grepped the whole diff to confirm. So a secret pasted into an AG2Space room gets correctly redacted from disk (the part tested), but the resulting gateway task file carries none of the downstream guidance the other three surfaces give the core. Given the PR title is literally "#2267 parity," I think this needs the same wiring: append secret_handling_instruction("AG2Space", _filtered.secret_types) (or whatever surface label fits the gateway) to the task body when _filtered.secret_types is non-empty, mirroring the discord-bridge pattern.

Not asking for the owner-activity path (_write_owner_activity) to carry the notice too — that's a presence summary, not a task the core acts on, so the discord-bridge precedent doesn't obviously apply there.

@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 68%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/remote-gateway-bridge.test.py (100%)

Summary

  • Total: 23 lines
  • Missing: 0 lines
  • Coverage: 100%

…issing #2267 half

Review: the other three bridges follow redaction with
secret_handling_instruction so the core neither reproduces nor re-requests
the redacted value; the gateway path redacted but carried no guidance. The
notice (surface "AG2Space") is appended after the access_tier line — it is
bridge-generated fixed text with no header-shaped lines, so the
access-tier-wins-last invariant holds (regression asserts exactly one
access_tier header). Clean tasks stay notice-free.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Good catch — the second half is wired in 63b2b950 (+ test fixup 81e8d081): _write_task now appends secret_handling_instruction("AG2Space", types) when redaction fired, mirroring the discord-bridge pattern. It rides after the access_tier line — the notice is bridge-generated fixed text with no header-shaped lines, and a regression asserts exactly one access_tier header survives (the tier-wins-last invariant). Clean tasks stay notice-free (asserted against a fresh task; the earlier assertion accidentally read a task the suite had already archived — fixed). Agreed on leaving the presence summary without the notice. Full suite green.

On the CI fail: it's the recurring tests/bridge-skill-path-resolution.test.py segfault (exit 139) — same crash hit #2290 twice today, and this branch is on current main; the test passes locally on this exact head. Rerunning; if it keeps recurring it needs its own investigation issue.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking live-path evidence gap on current head 81e8d081.

The earlier parity blocker is fixed: _write_task now appends the bridge-generated security notice when redaction fires, keeps exactly one access_tier header, and leaves clean tasks notice-free. I verified locally:

  • python3 src/remote-gateway-bridge.test.py — all checks green, including persisted-task redaction + owner-activity redaction + security notice
  • python3 packages/ag2-sparrow/tools/test_no_drift.py — package in sync
  • git diff --check origin/main...HEAD — passed
  • added-line host-path scan — clean

This is still a live gateway delivery path, and the PR body has harness-only proof. Please provide a real post-restart round trip on the patched Sparrow/gateway bridge using a fake token-shaped value (never a real credential): room message received → task file persisted with placeholder + security notice and without the fake token → result delivered, with the final response not reproducing/requesting the value. That safely proves the actual package/runtime wiring and restart path without exposing a secret.

Once that evidence is attached and fresh CI is green, I see no remaining code-level blocker.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at 81e8d081. The gap is closed correctly: secret_handling_instruction("AG2Space", _secret_types) is now appended after the access_tier: line whenever redaction fired, mirroring the discord-bridge pattern.

Verified:

  • _secret_types is captured outside the media-fetch block so it's available later in _write_task when building the notice.
  • Placement after access_tier: is deliberate and tested — the new _hdrs assertion confirms exactly one access_tier: line survives, so the notice's freeform text doesn't introduce a spurious second header.
  • Clean tasks (no secret detected) get no notice — asserted against a fresh task, not a possibly-archived one (the earlier test hygiene issue you mentioned is fixed).
  • .strip("\n") on the notice before appending as a single lines entry is correct given the final "\n".join(lines) — the notice's own internal blank-line formatting is preserved, only the redundant leading/trailing newline is trimmed.

Agreed on leaving the presence-summary path without the notice — that's a read-only status file, not something the core acts on, so the discord-bridge precedent doesn't apply there. The bridge-skill-path-resolution CI segfault looks like the pre-existing flake you flagged (already hit #2290 twice today) — unrelated to this change, not blocking.

No remaining issues. Approving.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking finding on current head 81e8d081067f2d48eef8e55da64843ced13cee54.

  • [P1] Modern GitHub fine-grained PATs (github_pat_...) are still not redacted. The fallback GitHub regex in src/chat_secret_filter.py / packages/ag2-sparrow/ag2_sparrow/chat_secret_filter.py only covers legacy ghp/gho/ghu/ghs/ghr prefixes, and the shared src/secret_scanner.py pattern has the same limitation. I verified with a fake github_pat_-shaped value that both source and packaged filters return the original text with secret_types=(). That means one of the common GitHub credential formats can still persist into the gateway task file and owner-activity summary, so the fix(bridges): redact pasted secrets before persistence #2267 parity guarantee is incomplete. Please add the fine-grained PAT pattern to the shared scanner and chat fallback, with a regression using only a fake token-shaped value.

Focused validation otherwise passed:

  • live PR checks are green
  • git diff --check origin/main...HEAD
  • src/remote-gateway-bridge.test.py
  • packages/ag2-sparrow/tools/test_no_drift.py
  • packages/ag2-sparrow/tests/test_write_task_atomic.py
  • py_compile for the changed Python files

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes still requested on current head 7874f24ed9a071b12e282af9b2a25258d8496d1a (formal self-request-changes is unavailable).

The new commit only adds the README statement that pasted tokens are scanned before persistence; it does not address the existing P1. chat_secret_filter.py still recognizes legacy ghp/gho/ghu/ghs/ghr forms but not modern github_pat_... fine-grained PATs, and the shared scanner has the same gap. A fake fine-grained-PAT-shaped value still returns unchanged with secret_types=(), so it can persist in both the task and owner-activity files. The new README wording now overstates that guarantee as well.

Please add a fake-value regression and cover the fine-grained PAT form in the shared scanner and packaged fallback. The gateway suite and package drift guard otherwise pass.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking finding remains on current head 7874f24ed9a071b12e282af9b2a25258d8496d1a.

  • [P1] The new delta is README-only, so the fine-grained GitHub PAT leak is still live. I re-ran the fake-token probe on this head, and both the source filter and packaged ag2_sparrow filter still leave a github_pat_...-shaped value unchanged with secret_types=(). That means modern GitHub PATs can still persist into the task file and owner-activity summary even though the PR promises fix(bridges): redact pasted secrets before persistence #2267 parity. Please extend the shared scanner and chat fallback patterns to cover github_pat_... and add a regression with a fake token-shaped value.

Focused validation otherwise passed:

  • live PR checks are green
  • git diff --check origin/main...HEAD
  • src/remote-gateway-bridge.test.py
  • packages/ag2-sparrow/tools/test_no_drift.py
  • packages/ag2-sparrow/tests/test_write_task_atomic.py
  • py_compile for the changed Python files

Reviewed by Qingyun's Personal Codex.

@bassilkhilo-ag2

Copy link
Copy Markdown
Collaborator

Good catch, and worth flagging: I reviewed and approved this PR earlier without catching this. I independently verified the gap just now — re.compile(r"(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}").search("github_pat_11ABCDEFG0...") returns None. The classic-token prefix regex genuinely doesn't cover fine-grained PATs (github_pat_...), so this is a real live gap in the "#2267 parity" claim, not a false positive from the self-check. Agreed this should block — my earlier approval stands for everything else I checked, but this needs the extended pattern + regression before merge.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Live deployed round-trip evidence, per your review (fake token only; isolated temp task/result/state dirs; the production poller was SIGSTOPped for the ~90s window so the test instance deterministically received the messages, then SIGCONTed — its long-poll resumed cleanly):

Inbound + redaction (patched bridge, real gateway, real room message): a peer agent posted deploy key ghp_FAKE…0000 please handle in the room. The patched bridge received it as a task and persisted:

  • body: deploy key [REDACTED-GitHub Token] please handle — fake token absent from the task file, absent from the bridge log (grep count 0 on both);
  • the ===SUTANDO SECURITY NOTICE=== block appended after access_tier (exactly one access_tier header — the tier-wins-last invariant held on live output);
  • types-only logging (redacted pasted secret(s) … GitHub Token).

Crash/restart: the bridge was kill -9'd with both tasks in flight. The restarted instance logged restored 2 in-flight and, with the results written while the bridge was dead, delivered both (delivered result for task-…215837, …175411) — archived locally, replies visible in the room. The reply for the probe explicitly declines to reproduce or re-request the value, honoring the in-band notice.

(Also exercised incidentally: a real owner message arrived during the window and rode the same persist→restart→deliver path — no loss. One operational note: SIGKILL leaves the workspace singleton lock behind and a same-second restart refuses dual-poll; the guard's staleness window handles it in production cadence, and the test cleared the lab lock manually.)

Fresh CI is green on this head.

@qingyun-wu
qingyun-wu requested a review from john-the-dev July 24, 2026 18:28

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes still requested on current head 777700fc0b670301f299eb88fcdfc2974091cdf3 (formal self-request-changes is unavailable).

The new commit is only a merge from main; the PR-specific filter remains unchanged, so the existing P1 is still live. chat_secret_filter.py recognizes legacy ghp/gho/ghu/ghs/ghr tokens but not modern fine-grained PATs beginning github_pat_. On this head, a fake github_pat_... value still returns unchanged with secret_types=(), so it can be persisted in both task and owner-activity files despite the README’s blanket redaction claim.

Please add github_pat_ coverage to both the shared and packaged filters plus a fake-value regression. Focused validation: src/remote-gateway-bridge.test.py passes, packages/ag2-sparrow/tools/test_no_drift.py passes, and git diff --check passes; the direct fine-grained-PAT probe fails as described.

Reviewed by Qingyun's Personal Codex.

… shared filter

Review P1: the curated patterns caught legacy ghp_/gho_/ghu_/ghs_/ghr_
tokens but not modern fine-grained PATs — a pasted github_pat_… value
returned unchanged (secret_types empty) and could persist in task and
owner-activity files. Adds the github_pat_ pattern to the SHARED
src/chat_secret_filter.py (all four bridges inherit it) with the packaged
copy regenerated via the sync tool (drift guard green). Regression: fake
fine-grained PAT → redacted body + security notice.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Fine-grained PAT gap closed in the new head: the github_pat_[A-Za-z0-9_]{36,} pattern is added to the shared src/chat_secret_filter.py (so discord/slack/telegram inherit it too, not just this path) and the packaged copy is regenerated through the sync tool — drift guard green. Regression: a fake github_pat_… value in a task body → redacted with placeholder + security notice; full bridge suite green.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval signal for current head c42daca (formal self-approval is unavailable).

The fine-grained GitHub PAT blocker is fixed in the shared filter: github_pat_... now redacts through src/chat_secret_filter.py, the packaged ag2_sparrow copy is regenerated through the sync map, and the remote gateway task-file path still appends the bridge-generated security notice without leaking the fake token or adding another access_tier header.

Local checks:

  • python3 src/remote-gateway-bridge.test.py
  • python3 packages/ag2-sparrow/tools/test_no_drift.py
  • direct fake github_pat_ redaction probe against src.chat_secret_filter
  • direct fake github_pat_ redaction probe against packaged ag2_sparrow.chat_secret_filter
  • git diff --check origin/main...HEAD
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-sutando-2301 python3 -m py_compile src/chat_secret_filter.py packages/ag2-sparrow/ag2_sparrow/chat_secret_filter.py packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py src/remote-gateway-bridge.test.py

GitHub checks are green on this head. No blocking findings from this pass.

Reviewed by Qingyun's Personal Codex.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at c42dacae. The fine-grained PAT gap is fixed: added ("GitHub Fine-Grained PAT", re.compile(r"github_pat_[A-Za-z0-9_]{36,}")) alongside the existing classic-token pattern, with a comment explaining why the legacy pattern never matched.

Independently verified the regex (not just trusting the new test): checked it against both the PR's own test value and a realistic-length fine-grained PAT (22-char resource id + _ + 59-char secret) — both match, and the full token (not just a partial prefix) gets consumed by the match span, confirming clean redaction rather than a truncated one.

New regression (test-FGPAT) checks the right things: the raw token is absent from the persisted body, the security notice still gets appended for this token type too, and a clean task stays notice-free — good coverage of the actual failure mode.

This closes the gap I flagged. Approving.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@john-the-dev when you get a chance — this one's ready for your re-look: the fine-grained-PAT gap from your review is fixed on head c42dacae (pattern added in the shared filter + packaged copy regenerated through the sync map, drift guard green, regression covers github_pat_), CI is fully green, and bassilkhilo re-reviewed and approved at this head. Your changes-requested is the last gate.

…ret-redaction

# Conflicts:
#	packages/ag2-sparrow/README.md
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@cla-assistant check

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Cycling close/reopen to restore the vanished license/cla check (known CLA-Assistant issue after branch updates; same recipe as #2302).

@qingyun-wu qingyun-wu closed this Jul 25, 2026
@qingyun-wu qingyun-wu reopened this Jul 25, 2026
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Re-reviewed current head 61d0b3ad6315ddba13ac16ba31f3c1b0490ffb20 after the branch merged current main. I do not see PR-specific blocking issues.

The PR-specific diff against current origin/main is still limited to the Sparrow secret-redaction path and package sync files. The prior fine-grained PAT blocker remains fixed in both src.chat_secret_filter and the packaged ag2_sparrow copy, _write_task still filters after media-marker resolution and before task-file persistence, the bridge logs only secret type names, the in-band security notice rides with redacted tasks after access_tier, and owner activity is filtered before last-owner-activity.json is written. I did not see local-vs-cloud credential boundary regressions: gateway/media bearer handling remains constrained by the existing host/path checks covered in the gateway bridge test.

Focused validation:

  • git diff --check origin/main...HEAD
  • python3 src/remote-gateway-bridge.test.py (rerun outside sandbox because the test binds a loopback HTTP server)
  • python3 packages/ag2-sparrow/tools/test_no_drift.py
  • python3 packages/ag2-sparrow/tests/test_write_task_atomic.py
  • direct fake github_pat_ redaction probes against both source and packaged filters
  • py_compile for the changed Python files

CI note: remote CI has one current failure in tsc + tests (clean install), where the standalone Python test sweep exits 139 at tests/bridge-skill-path-resolution.test.py. That matches the pre-existing flaky segfault already observed on this PR and on #2290, and the completed PR-specific checks are green.

No blocking findings from this pass.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking: missing required live-path evidence on the current head.

I reviewed 61d0b3a and found no code-level defect in the redaction wiring. I independently ran:

python3 src/remote-gateway-bridge.test.py
PASS — all checks green

python3 packages/ag2-sparrow/tools/test_no_drift.py
PASS — package in sync with src/

python3 tests/chat-secret-persistence.test.py
Ran 13 tests ... OK

git diff --check origin/main...HEAD
# no output

The diff also has no newly added /Users/..., /home/..., clone-specific fallback, or production host path.

This changes the live gateway/network ingestion path and task-file publication, so CONTRIBUTING requires a real post-restart round trip. Please attach a gateway message after restarting the patched bridge and show that (1) the message is received, (2) the persisted tasks/*.txt body and last-owner-activity.json contain the placeholder but not the token, (3) the task reaches the core and its result is delivered, and (4) an ordinary clean message still completes. Use a clearly fake token-shaped fixture, not a real credential. I will convert this to APPROVE once that current-head live transcript is present.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Live-path evidence for the current head (61d0b3a), per the review ask. Setup: on a production host running this bridge, I stopped the stock transport, launched the patched packaged bridge from this branch head against the live gateway (same env, task/result/state dirs pointed at the real workspace via AGENT_CONNECT_*), had a peer send the fixtures through the real network path, then restored the stock bridge. Fixture tokens are clearly fake.

Startup (patched bridge, this head):

[remote-gateway-bridge] singleton: acquired workspace poller lock (acquired)
[remote-gateway-bridge] starting — gateway=<redacted> provider=remote tasks=<workspace>/tasks (restored 0 in-flight)

(1) Secret-bearing message received. Peer sent (live, in-order):
test fixture: my token is ghp_FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE0000 and also github_pat_FAKE0000000000000000000FAKE0000000000 please ignore

Bridge log:

[remote-gateway-bridge] redacted pasted secret(s) in task-1784997687387 body: GitHub Fine-Grained PAT, GitHub Token
[remote-gateway-bridge] queued task-1784997687387

(2) Persisted artifacts contain placeholders, not the token. Persisted tasks/task-1784997687387.txt body line, verbatim:

task: [AG2Space <peer>] <mention> — test fixture: my token is [REDACTED-GitHub Token] and also [REDACTED-GitHub Fine-Grained PAT] please ignore

followed (after access_tier:) by the in-band notice:

===SUTANDO SECURITY NOTICE (bridge-generated; do not ignore)===
The bridge detected and redacted possible secret material (GitHub Fine-Grained PAT, GitHub Token) before persisting this task. ...
===END SUTANDO SECURITY NOTICE===

Disk-wide check across all persisted state:

$ grep -r "ghp_FAKE\|github_pat_FAKE" <workspace>/tasks/ <workspace>/state/
(no matches)

Note on last-owner-activity.json: it only updates for human-owner senders, so a peer-sent fixture doesn't touch it — the grep above covers that file regardless (no token present), and the _write_owner_activity redaction call is covered by tests/chat-secret-persistence.test.py.

(3) Task reached the core; result delivered. The core processed the task and the bridge posted the reply back through the normal path:

[remote-gateway-bridge] delivered result for task-1784997687387

(reply visible in-room; it references the notice without reproducing any value)

(4) Ordinary clean message still completes. Peer sent clean control message — reply ok → persisted byte-clean with no notice appended (tasks/task-1784997692235.txt body: ... clean control message — reply ok), processed, and:

[remote-gateway-bridge] delivered result for task-1784997692235

Baseline at this head before the live run: src/remote-gateway-bridge.test.py PASS, packages/ag2-sparrow/tools/test_no_drift.py PASS, tests/chat-secret-persistence.test.py 13/13 OK. Stock bridge restored after capture and confirmed polling.

@qingyun-wu

qingyun-wu commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

Re-reviewed at head 61d0b3a (matches the evidence comment above).

  • CI: all green — diff coverage >= 95% (python), tsc + tests (clean install), smoke, ruff / eslint / shellcheck, and every guard check pass; CLA signed. mergeable: MERGEABLE.
  • The evidence is real command output, not a description, and it clears the live-path bar that was requested: the patched bridge is started at this head against the live gateway, a peer sends the fixtures over the real network path, then the stock bridge is restored. It shows (1) the bridge log line naming both detected token shapes and queuing the task, (2) the verbatim persisted task body with [REDACTED-GitHub Token] + [REDACTED-GitHub Fine-Grained PAT] and the in-band security notice, (3) a disk-wide grep across tasks/ + state/ returning no matches for the fixture tokens, and (4) the clean-control message persisting byte-clean with no notice appended. Baseline suites (remote-gateway-bridge.test.py, test_no_drift.py, chat-secret-persistence.test.py 13/13) PASS at head.

This satisfies the live post-restart round-trip, so I have no remaining blockers. Correction on the reviewer state (latest-per-reviewer): @bassilkhilo-ag2 already APPROVED (2026-07-24), so @john-the-dev's CHANGES_REQUESTED (the live-path ask, now answered) is the sole remaining block — one flip clears it. Merge remains the owner's call.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Live post-restart round trip (real deployment, current head). The branch's bridge + BOTH chat_secret_filter.py copies were applied to the live tree (bundle is not a git checkout → hunk-patch + file copies; note for other testers: the package copy is required — the src entrypoint executes the package module, and omitting it crashes at import, which the keepalive surfaced immediately) and the bridge restarted.

1. Token-shaped inbound → redacted persistence. A peer agent posted … here is the key sk-proj-FAKE…[52-char fake]. Persisted task file body:

task: [AG2Space @…] @… here is the key [REDACTED-OpenAI Token]

Raw token in live state/ + tasks/: 0 files (grep sweep). last-owner-activity.json untouched by the message (agent sender) — contains no token.

2. Task reached the core, result delivered — bridge log: queued task-1785059611425 → result file consumed and delivered, reply visible in the channel.

3. Ordinary clean message posted by the same peer completed normally end-to-end (queued → processed → delivered).

4. Pattern-boundary check (incidental but useful): a first fixture with only 24 chars after sk-proj- was correctly NOT redacted — the OpenAI rule requires ≥32 (sk-(?:proj-)?[A-Za-z0-9_-]{32,}), so the filter doesn't over-match short strings; the 52-char fixture above redacts.

Tree restored to pre-patch after collection; live bridge healthy on main code.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-reviewed the unchanged current head after the new live round-trip evidence. No blocking findings. The transcript confirms OpenAI-shaped input is redacted before task/state persistence, the task still reaches the core and delivers its result, clean traffic remains unaffected, and the package-copy deployment seam is understood. The full diff remains sound and all current checks pass. Approval signal: looks good to merge from this review. Posted as COMMENT because the authenticated account is the PR author.

Reviewed by Qingyun's Personal Codex.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Commenting rather than stacking a second CHANGES_REQUESTED. Two findings, one of which matters for this repo specifically.

Gap: no Google / Gemini AIza pattern — arguably this repo's most-pasted key

The fallback set covers AWS, GitHub (legacy and fine-grained — good catch on the 2022+ prefix), Matrix syt_, the url|secret remote-task token, JWT, Slack, OpenAI, Stripe, Discord, Telegram, and PEM blocks. There is no Google API key pattern.

That is the one I'd expect to see pasted most often here. GEMINI_API_KEY is load-bearing across this codebase — voice, browser-tools.ts, recording-tools.ts, reply classification — and two other PRs in this same batch (#2197, #2131) exist purely to route GEMINI_* readers through the new resolver. On the host I'm on, GEMINI_API_KEY is present in the live .env. A pasted Gemini key sails straight through this filter and gets persisted.

Google keys have a stable, high-precision shape, so this is cheap and low-false-positive:

("Google API Key", re.compile(r"AIza[0-9A-Za-z_-]{35}")),

For a fail-closed module (per the docstring) the omission of the single most likely local secret is worth closing before merge.

Documentation claims broader coverage than the diff wires up

The module docstring says values are replaced before "task files, owner-activity state, prompt scratch files, or bridge logs" are written. The only invocation site in the diff is one import into remote_gateway_bridge.py (+22/-1), which plausibly covers task-file writes and _write_owner_activity. I don't see wiring for the other two — and specifically, _log is untouched, so bridge logs are not redacted.

That matters because it's the sink most likely to receive a partial secret incidentally (an error string echoing a request body, a redelivery diagnostic). It also matters for a reader auditing the control later: a docstring that overstates a security boundary is worse than one that understates it, because the next person trusts it instead of checking.

Either narrow the docstring to the sinks actually covered, or wire the remaining two. If _log redaction is deliberately out of scope (cost per log line), saying so explicitly is fine — the problem is only the mismatch.

A finding I withdrew, in case it saves you a look

I initially flagged "GitHub Fine-Grained PAT" as a duplicated pattern entry, because it appears twice in the diff. It isn't a duplicate — the two occurrences are the same module mirrored into src/chat_secret_filter.py and packages/ag2-sparrow/…/chat_secret_filter.py, which is this repo's sync_from_src.py convention. Correct as written; my grep spanned both files.

What I did not verify

Did not run src/remote-gateway-bridge.test.py. The three added checks (fine-grained PAT redacted, SUTANDO SECURITY NOTICE present in the persisted body) read as the right assertions.

Resolves the one conflict, in src/remote-gateway-bridge.test.py, where this
branch and main each appended an independent test group at the same location:

  ours   — #2267 parity: pasted-secret redaction, the security notice, and the
           access_tier header invariant
  theirs — #2307: onboarding-token parse, including the %7C-encoded url|secret
           separator

They test unrelated behaviour and neither supersedes the other, so the
resolution is a union — both groups kept, in that order. No assertion from
either side was dropped or weakened.

Verified after resolving:
  python3 src/remote-gateway-bridge.test.py          PASS — all checks green
  packages/ag2-sparrow/tests/test_dns_timeout.py     ALL PASS
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Merged main in to clear the conflict this branch had picked up (it was CONFLICTING/DIRTY; now MERGEABLE).

The single conflict was in src/remote-gateway-bridge.test.py, where this branch and main each appended an independent test group at the same location:

They cover unrelated behaviour and neither supersedes the other, so I resolved it as a union — both groups kept, nothing dropped or weakened. Assertion counts after the resolution: 3 redaction-side, 8 token-parse-side.

Verified on the merge result:

python3 src/remote-gateway-bridge.test.py         PASS — all checks green
packages/ag2-sparrow/tests/test_dns_timeout.py    ALL PASS

The baseline was checked before I touched anything (the branch's own test was already green), so this is a conflict resolution only — no behavioural change.

Note this does not address the outstanding change-request on the PR; that's still open and separate.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking finding on current head 12d1fd23e9a5e1160970da75b087f9d5c6b40b06:

  • src/chat_secret_filter.py:23 and the synced package copy only redact remote task tokens when the onboarding separator is a literal |. Current onboarding compatibility also accepts the desktop-written URL-encoded form (https://.../relay%7C<secret>), but this PR leaves that shape untouched. In the isolated checkout, filter_chat_secrets("https://chat.ag2.space/relay%7C" + "a" * 24) returned the original string with secret_types == (), while the literal | form redacts correctly. Because this PR is specifically meant to redact pasted secrets before task-file and owner-activity persistence, a valid %7C relay/onboarding token pasted through room chat can still be written to disk. Please make the fallback remote-task-token detector accept %7C/%7c as the same separator _parse_onboarding_token() supports, and add a regression test next to the literal-separator case.

Focused checks run: git diff --check origin/main...HEAD, python3 src/remote-gateway-bridge.test.py, python3 packages/ag2-sparrow/tools/sync_from_src.py --check, python3 -m py_compile ..., plus the failing %7C redaction repro above.

Reviewed by Qingyun's Personal Codex.

Addresses the blocking finding on 12d1fd2: the fallback Remote Task Token
detector matched only a literal `|`, so the URL-encoded separator form went
unredacted and a pasted token could still reach disk — the exact outcome this
PR exists to prevent.

The parser that consumes these tokens accepts both forms
(`_SEPARATOR_RE = r"\||%7[Cc]"` in remote_gateway_bridge), and the desktop
connect flow writes the encoded one, so the detector has to match the same
shape. The negative lookahead keeps the URL run from swallowing the encoded
separator, mirroring the parser's first-separator-wins `search()`.

Reproduced before the change, on the PR head:

    literal |      redacted=True   types=('Remote Task Token',)
    encoded %7C    redacted=False  types=()
    encoded %7c    redacted=False  types=()

After:

    literal |      redacted=True   types=('Remote Task Token',)
    encoded %7C    redacted=True   types=('Remote Task Token',)
    encoded %7c    redacted=True   types=('Remote Task Token',)
    plain url, no separator        redacted=False   (unchanged)
    url + too-short tail           redacted=False   (unchanged)

Regression added next to the literal-separator case as requested, covering all
three separator spellings end-to-end (persisted task body + security notice),
not just the pattern in isolation.

Verification:
- `python3 src/remote-gateway-bridge.test.py` → 98 checks, 0 failures.
- Mutation-checked: reverting only the detector leaves the literal case green
  and fails both encoded cases — so the new checks bite on exactly this bug.
- `python3 packages/ag2-sparrow/tools/sync_from_src.py --check` → in sync
  (the package copy is regenerated, not hand-edited).
- `python3 packages/ag2-sparrow/tests/test_dns_timeout.py` → ALL PASS.

Note this addresses the redaction gap only; it does not speak to the separate
open change-request on this PR.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking finding addressed on 770ba56.

The detector matched only a literal |, so the URL-encoded separator form went unredacted. Since the parser that consumes these tokens accepts both (_SEPARATOR_RE = r"\||%7[Cc]") and the desktop connect flow writes the encoded one, the detector now matches the same shape. The negative lookahead stops the URL run from swallowing the encoded separator, mirroring the parser's first-separator-wins search().

Reproduced on the previous head 12d1fd23:

literal |      redacted=True   types=('Remote Task Token',)
encoded %7C    redacted=False  types=()
encoded %7c    redacted=False  types=()

On 770ba56:

literal |                 redacted=True   types=('Remote Task Token',)
encoded %7C               redacted=True   types=('Remote Task Token',)
encoded %7c               redacted=True   types=('Remote Task Token',)
plain url, no separator   redacted=False  (unchanged)
url + too-short tail      redacted=False  (unchanged)

Regression added next to the literal-separator case as asked — all three spellings, exercised end-to-end through _write_task (persisted body + security notice), not just the pattern in isolation.

Mutation-checked: reverting only the detector leaves relay token with literal separator REDACTED green and fails both encoded cases, so the new checks bite on precisely this bug rather than passing trivially.

python3 src/remote-gateway-bridge.test.py                 98 checks, 0 failures
python3 packages/ag2-sparrow/tools/sync_from_src.py --check   in sync with src/ ✓
python3 packages/ag2-sparrow/tests/test_dns_timeout.py        ALL PASS

The package copy was regenerated via the sync tool, not hand-edited.

This addresses the redaction gap only — it does not speak to the separate open change-request on this PR.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Approval signal for current head 770ba56cd6bf367854daadaa7bea8f35acb60cbf.

No blocking findings from my re-review. The redaction now covers the parser-compatible relay token separators in all three forms (|, %7C, %7c) in both the src/ and package copies, while preserving the clean-task path and adding the security notice only when redaction actually happened. Fine-grained PAT coverage and owner-activity redaction also look correct, and I did not find a way for the redaction path to forge a second access_tier header.

For the gateway/room-ops boundaries: this PR does not add AppService namespace credentials to Sutando-local, membership enforcement is unchanged, and onboarding compatibility is preserved because the redactor now matches the same separator forms accepted by the bridge parser.

Focused checks passed: git diff --check, python3 src/remote-gateway-bridge.test.py, python3 packages/ag2-sparrow/tools/sync_from_src.py --check, py_compile for the touched Python files, python3 packages/ag2-sparrow/tests/test_event_inbox_channel.py, python3 packages/ag2-sparrow/tests/test_write_task_atomic.py, and python3 packages/ag2-sparrow/tests/test_gateway_status.py.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes still requested on current head 770ba56c (formal self-request-changes is unavailable).

The new commit correctly closes the %7C relay-token gap: I verified literal |, uppercase %7C, and lowercase %7c through the full gateway regression suite, and the package drift, persistence, diff, and hardcoded-path checks pass.

One security blocker remains in src/chat_secret_filter.py:16-45: Google/Gemini API keys are still absent from the fallback patterns. A direct fake-value probe with "AIza" + "A" * 35 returned the input unchanged with secret_types=(). Therefore an owner can still paste this repo's load-bearing GEMINI_API_KEY shape into a gateway room and have it persisted in both the task and owner-activity state despite the README's “tokens, keys” redaction guarantee. Worst case: a real model key remains on disk and is later synced or inspected.

Please add the high-precision AIza[0-9A-Za-z_-]{35} form to the shared filter, regenerate the package copy, and add an end-to-end fake-key persistence regression. This head is not merge-ready until that existing gap is closed.

Focused evidence: src/remote-gateway-bridge.test.py, tests/chat-secret-persistence.test.py (13/13), package sync check, git diff --check, and scripts/review-checks.sh all passed.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu
qingyun-wu requested a review from john-the-dev July 26, 2026 23:38

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approval-freshness note: my APPROVE predates 770ba56c, so I've now reviewed it — the approval holds. I approved at 18:44Z; the %7C detector fix landed at 22:32Z. Flagging rather than letting a standing approval silently cover code I hadn't read.

770ba56c is correct. The detector now mirrors what the consumer accepts — _SEPARATOR_RE = r"\||%7[Cc]" in remote_gateway_bridge — which is the right invariant to bind it to, since a detector that recognises fewer forms than the parser is exactly how a valid token reaches disk. The negative lookahead stopping the URL run at the encoded separator matches the parser's first-separator-wins search(), and the three-spelling regression covers it end-to-end.

One thing I verified that isn't obvious, and it's a confirmation rather than a finding. #2331 established that a real desktop host can carry the relay token in split-key form — a bare secret with no URL, in a separate REMOTE_TASK_URL key (verified on mine: bare 48 chars, no ://, no separator). So I checked whether pasting just that bare half would slip through, since the Remote Task Token pattern requires an https?:// prefix and cannot match it.

It is covered — but by a different layer than the one this PR touches. No named pattern matches a bare secret; the entropy path does:

detect-secrets → Base64/Hex High Entropy hit
  → _actionable_hits keeps it only if the line has a _TOKENISH_RUN
  → _TOKENISH_RUN = [A-Za-z0-9_+/=-]{20,}

A 48-char bare secret satisfies that gate standalone and mid-sentence, while ordinary prose does not. So the split-key shape is redacted. I mention it because "does the bare half leak?" is a natural question once #2331 lands, and the answer depends on the entropy layer rather than the curated list — worth knowing before anyone tightens _actionable_hits, since narrowing that gate would open exactly this hole.

Not re-approving (my 18:44Z approval stands) and not gating anything — john-the-dev's 16:23Z CHANGES_REQUESTED is the open state here, not mine.

The fallback pattern list had no Google key form, so a pasted
`AIza…` value returned unchanged with `secret_types=()` and was persisted
verbatim into the task file and owner-activity state — despite the
documented "tokens, keys" redaction guarantee. This repo's own load-bearing
GEMINI_API_KEY has exactly that shape, so the worst case is a live model key
left on disk and later synced or inspected.

Adds the high-precision `AIza[0-9A-Za-z_-]{35}` form to both copies of the
filter (kept byte-identical, no package drift) plus three tests: direct
redaction, an end-to-end persisted-text regression asserting the raw key
survives nowhere in the output, and a control proving prose containing
"AIza" is NOT flagged — the 35-char tail is what keeps the pattern precise.

Stashing only the two filter copies and keeping the tests fails 2 of them,
so the tests detect the gap rather than passing vacuously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Etoj87Vi5gWCswkGUAiiuz
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Google/Gemini gap closed in 7956d92d — the last outstanding blocker from my 770ba56c review.

Confirmed the gap first

Probed the head before changing anything:

filter_chat_secrets("here is my key AIza" + "A"*35 + " thanks")
  -> secret_types=()   redacted=False

So a pasted key of this repo's own GEMINI_API_KEY shape was persisted verbatim into the task file and owner-activity state, despite the documented "tokens, keys" guarantee.

Fix

Added the high-precision form to _FALLBACK_PATTERNS:

("Google API Key", re.compile(r"AIza[0-9A-Za-z_-]{35}")),

Applied to both copies — src/chat_secret_filter.py and packages/ag2-sparrow/ag2_sparrow/chat_secret_filter.py — and diff -q confirms they remain byte-identical, so no package drift.

Tests (three, including a control)

  1. test_google_api_key_is_redacted — direct redaction, asserts "Google API Key" in secret_types.
  2. test_google_api_key_redacted_end_to_end_in_persisted_text — the end-to-end persistence regression you asked for; asserts the raw key survives nowhere in the text that would be written, while surrounding prose is preserved.
  3. test_aiza_prose_is_not_flagged — control. "the AIza prefix identifies Google keys" must NOT be redacted. Without this, a pattern that over-matched would satisfy 1 and 2 and quietly shred ordinary prose; the 35-char tail is what keeps it precise.

Evidence

16 tests ... OK

And the control the other way — stashing only the two filter copies while keeping the tests:

AssertionError: 'AIza0aZ_-0aZ_-…' unexpectedly found in 'GEMINI_API_KEY=AIza0aZ_-…'
FAILED (failures=2)

so the new tests detect the defect rather than passing vacuously.

Also green: full src/remote-gateway-bridge.test.py suite, ruff, and scripts/review-checks.sh.


Review state note. @bassilkhilo-ag2's approvals predate 770ba56c and now this commit too; @john-the-dev's 07-25T16:23Z CHANGES_REQUESTED is the open state and is still the gate. This push adds a commit on top of everything reviewed so far, so it needs fresh eyes rather than a merge — flagging because this repo does not auto-dismiss stale approvals, so reviewDecision alone will not show that.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Current-head approval signal for 7956d92d23d4f1c07f824c9472f5e49d5c65a385 (formal approval is unavailable because the authenticated account is the PR author).

No blockers found. The prior Google/Gemini key gap is closed with a precise AIza[0-9A-Za-z_-]{35} fallback pattern in both the shared src/ filter and the packaged ag2_sparrow copy. The new coverage includes direct detection, persisted-text redaction, and a prose control so ordinary mentions of AIza are not flagged.

Focused verification passed: tests/chat-secret-persistence.test.py (16/16), the full src/remote-gateway-bridge.test.py suite, package sync check, byte-identical filter copy check, and Python compilation for the touched filter/bridge/test files. Current GitHub checks are green. Gateway/room-ops boundaries remain unchanged: this PR does not add AppService namespace credentials to Sutando-local, membership enforcement remains gateway/broker-side, and onboarding compatibility is preserved by matching the same relay-token separator forms the bridge parser accepts.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed exact head 7956d92d23d4f1c07f824c9472f5e49d5c65a385 and replacing my stale live-evidence request with APPROVE.

The required real post-restart gateway round trip is present in-thread: fake token-shaped input was received over the live gateway, persisted with placeholders plus the in-band notice and absent from task/state grep, processed by the core, and delivered; a clean control also completed. Subsequent PR commits do not change that integration contract—they add parser-parity redaction for %7C/%7c relay tokens and precise Google/Gemini AIza coverage, plus a merge from main.

Current-head verification in a detached worktree passed:

  • tests/chat-secret-persistence.test.py — 16/16
  • src/remote-gateway-bridge.test.py — all checks green, including literal/encoded relay tokens, fine-grained PATs, security notice, and clean control
  • package sync check and test_write_task_atomic.py
  • Python compilation, git diff --check, and added-line hardcoded-host-path scan
  • all GitHub CI and CLA gates are green

The branch remains behind current main, so it still needs refresh and fresh exact-head CI/review before merge. No code-level blocker remains on this head.

Reviewed by John’s Codex.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Approval signal for current head c1bbc5262bd5a6e8fed2e0babe75a946f523755c (regular comment because the authenticated account owns this PR).

No blocking findings from this refresh. The PR-specific diff against current main remains limited to the Sparrow/Gateway secret-redaction path and package sync files. The prior findings still look closed: fine-grained GitHub PATs, parser-compatible relay tokens (|, %7C, %7c), Google/Gemini AIza keys, task-file persistence, owner-activity persistence, and the in-band security notice are all covered without adding a second access_tier header or changing clean-task behavior.

Gateway/room-ops boundaries remain unchanged: this PR does not put AppService namespace credentials into Sutando-local, media/gateway bearer scoping is still covered by the bridge tests, membership enforcement remains gateway/broker-side, and onboarding compatibility is preserved by matching the same separator forms accepted by the bridge parser.

Focused verification passed:

  • git diff --check origin/main...HEAD
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2301-c1bbc-chat python3 tests/chat-secret-persistence.test.py (16/16)
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2301-c1bbc-rgb python3 src/remote-gateway-bridge.test.py
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2301-c1bbc-sync python3 packages/ag2-sparrow/tools/sync_from_src.py --check
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2301-c1bbc-write python3 packages/ag2-sparrow/tests/test_write_task_atomic.py
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2301-c1bbc-gateway python3 packages/ag2-sparrow/tests/test_gateway_status.py
  • PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2301-c1bbc-compile python3 -m py_compile ...
  • python3 scripts/gen-src-map.py --check
  • bash scripts/review-checks.sh --diff /private/tmp/pr2301-c1bbc.diff

GitHub CI and CLA are green on this head.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu
qingyun-wu merged commit d8c48ef into main Jul 30, 2026
18 checks passed
@qingyun-wu
qingyun-wu deleted the fix/ag2space-chat-secret-redaction branch July 30, 2026 08:13
yixuan-ag2 added a commit to yixuan-ag2/sutando that referenced this pull request Aug 16, 2026
…dacts

`remote_gateway_bridge` scrubs a pasted secret before persisting a task file
(sonichi#2301), but `read.py::_normalize` copied the body straight through — so the
same secret came back out of the same room, in the same process.

Delegates to `src/chat_secret_filter`, the filter the write path already uses,
rather than re-deriving one here: a second regex on this side would drift from
the one it has to match. Falls back to the vault-set grammar, then fails CLOSED
— returning the raw body when no filter loads is precisely the defect.

Closes sonichi#2945

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTD5g2LhGVnYmNmn7q2nk8
yixuan-ag2 added a commit that referenced this pull request Aug 18, 2026
…dacts (#2955)

* fix(agent-room-ops): the read path returned secrets the write path redacts

`remote_gateway_bridge` scrubs a pasted secret before persisting a task file
(#2301), but `read.py::_normalize` copied the body straight through — so the
same secret came back out of the same room, in the same process.

Delegates to `src/chat_secret_filter`, the filter the write path already uses,
rather than re-deriving one here: a second regex on this side would drift from
the one it has to match. Falls back to the vault-set grammar, then fails CLOSED
— returning the raw body when no filter loads is precisely the defect.

Closes #2945

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTD5g2LhGVnYmNmn7q2nk8

* test(agent-room-ops): cover the grammar-fallback rung of the redaction ladder

The ladder has three rungs and only two were exercised. The untested one is
the rung that carries the reported case, and an untested fallback cannot be
told apart from one that never runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTD5g2LhGVnYmNmn7q2nk8

* fix(agent-room-ops): announce a degraded redactor at selection

Review question on #2955: `_REDACTOR` is cached for the life of the process, so
a reader that starts during a broken install pins withhold-everything even after
the install repairs itself — and a withheld body is indistinguishable downstream
from one that genuinely contained a secret.

Logs once, at selection, naming which rung was taken and what it stopped
covering. The healthy path stays silent, asserted, so the line means something.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTD5g2LhGVnYmNmn7q2nk8

* fix(redaction): the read path applied half the chain, not the writer's whole one

@john-the-dev's blocker, reproduced before acting on it: a vault-set value with
no provider signature came back readable.

    filter_chat_secrets('vault set K hunter2secret') -> unchanged
    filter_chat_secrets('vault set K ' + 'a'*40)     -> unchanged
    filter_chat_secrets('vault set K 801…:AAF…')     -> [REDACTED-Telegram Bot Token]

Note the middle line: LENGTH is not the discriminator, signature is. The review
attributed the gap to a token-length threshold; a 40-char generic value survives
too, so a test reaching for a "long" value would pass against the broken code.
Each new case therefore carries its own control asserting the half-chain still
lets it through.

New src/chat_redaction.py owns the composition, per the repo's shared-policy
rule, and BOTH sides call it — the room-ops reader and discord-bridge's log line
(which was the other copy). Extracting it surfaced a second defect: the writer's
order (filter then grammar) mangles a body both filters match —

    filter->grammar: vault set TELEGRAM_BOT_TOKEN [vault: … ignored] Bot Token]23
    grammar->filter: vault set TELEGRAM_BOT_TOKEN [vault: … ignored]

so the chain runs grammar first. Both orders agree when only one matches.

Imports are eager, not lazy: the reader picks its redactor once and degrades if
this module will not import, and a lazy import would defer that failure past the
ladder built to catch it. The ladder tests now purge chat_redaction alongside the
module they poison, or a cached copy keeps the healthy rung alive and the branch
under test never runs.

tests/chat-secret-persistence.test.py pinned the old composition by SOURCE TEXT;
updated to pin the new call. (That guard is a source-regex surrogate — worth
converting to a behavioural assertion, but not in this PR.)

Control: reverting the reader to the half-chain turns 2 of these tests red.
11/11 in the redaction suite, 16/16 in chat-secret-persistence, and every other
discord-bridge suite green. tests/discord-bridge-send-body-file.test.py fails at
the merge base too — pre-existing, untouched here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wqEvhuA4hY4casgMzHzNm

* fix(review): trim two comment blocks to the 2-line cap; resync src-map

The prose-cap gate (#2917) landed on main today and caught two 3-line blocks
this PR adds — the fail-closed note in read.py and the eager-import note in
chat_redaction.py. Both trimmed; the constraint stays in the code and the
narrative is in the PR body.

src-map regenerated against the rebased tree: the previous generation was made
before main moved, so --check called it stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wqEvhuA4hY4casgMzHzNm

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
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.

4 participants