feat(secrets): exposure detection — exposed flag, chat badge, mask-on-render #108

Closed
opened 2026-07-10 17:37:24 +02:00 by dominik.polakovics · 3 comments

What to build

Disclosure is an event the operator learns about (grill decision). The chat tailer scans transcript content for the repo's secret values via the matcher from #104 (exact + base64/hex/urlencoded). A hit means the value is actually sitting in a provider-owned transcript file on disk — lab cannot rewrite that file, so lab makes it loud and contains the blast radius in its own UI:

  • Mark the secret exposed on the repo's secrets page: which run, when, sticky until cleared.
  • Badge the affected run's chat header with a warning.
  • Mask on render: the chat never re-displays a known secret value — the tailer replaces it with [REDACTED:NAME] in rendered messages (defense-in-depth; the disk copy stays dirty).
  • Rotating the secret clears the exposed flag — rotation is the remediation, so the flag's lifecycle rides the rotate action.

Contained hits elsewhere (broker redaction #105, blocked push #106) stay quiet — those are the harness working, not incidents. Only a transcript hit is a disclosure.

Out of scope: push notification for the exposure event — one-line follow-up once the notification seam (#98–#101) lands; this slice ships flag + badge + masking only.

Implementation note: scanning needs plaintext values at the tailer — keep the vault coupling behind a narrow interface (e.g. the store hands the tailer a per-repo matcher, not values), and never log the value on a hit.

Acceptance criteria

  • A secret value (any matched form) appearing in a transcript marks the secret exposed with run + timestamp on the repo's secrets page
  • The affected run's chat shows a warning badge; the rendered chat masks the value as [REDACTED:NAME] (all matched forms)
  • Rotating the secret clears the exposed flag; delete removes it
  • Broker-redacted output ([REDACTED:NAME] placeholders) does NOT trigger the exposed flag
  • The plaintext value appears in no log, event payload, or API response on a hit
  • Zero-secret repos: tailer scan is a no-op with no per-message overhead

Blocked by

  • #104 (secrets + matcher must exist)
## What to build Disclosure is an event the operator learns about (grill decision). The chat tailer scans transcript content for the repo's secret values via the matcher from #104 (exact + base64/hex/urlencoded). A hit means the value is actually sitting in a provider-owned transcript file on disk — lab cannot rewrite that file, so lab makes it loud and contains the blast radius in its own UI: - Mark the secret **exposed** on the repo's secrets page: which run, when, sticky until cleared. - Badge the affected run's chat header with a warning. - **Mask on render**: the chat never re-displays a known secret value — the tailer replaces it with `[REDACTED:NAME]` in rendered messages (defense-in-depth; the disk copy stays dirty). - **Rotating the secret clears the exposed flag** — rotation is the remediation, so the flag's lifecycle rides the rotate action. Contained hits elsewhere (broker redaction #105, blocked push #106) stay quiet — those are the harness working, not incidents. Only a transcript hit is a disclosure. Out of scope: push notification for the exposure event — one-line follow-up once the notification seam (#98–#101) lands; this slice ships flag + badge + masking only. Implementation note: scanning needs plaintext values at the tailer — keep the vault coupling behind a narrow interface (e.g. the store hands the tailer a per-repo matcher, not values), and never log the value on a hit. ## Acceptance criteria - [ ] A secret value (any matched form) appearing in a transcript marks the secret exposed with run + timestamp on the repo's secrets page - [ ] The affected run's chat shows a warning badge; the rendered chat masks the value as `[REDACTED:NAME]` (all matched forms) - [ ] Rotating the secret clears the exposed flag; delete removes it - [ ] Broker-redacted output (`[REDACTED:NAME]` placeholders) does NOT trigger the exposed flag - [ ] The plaintext value appears in no log, event payload, or API response on a hit - [ ] Zero-secret repos: tailer scan is a no-op with no per-message overhead ## Blocked by - #104 (secrets + matcher must exist)
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: A secret value appearing in a session transcript is a disclosure the operator learns about: the chat tailer detects it, marks the secret exposed (repo secrets page: which run, when, sticky), badges the run's chat header, and masks the value on render. Rotating the secret clears the flag.

Current behavior:
The chat tailer parses provider-owned transcript files and renders messages; it performs no secret scanning. Secrets, the streaming matcher, and rotation landed in #104. Lab cannot rewrite a provider's transcript file on disk — once a value is written there, the copy is permanently dirty; today nothing notices or contains it.

Desired behavior:
The tailer scans transcript content against the repo's secret values (exact + base64/hex/URL-encoded forms). On a hit:

  • The secret is marked exposed on the repo's secrets page — which run, when — sticky until cleared.
  • The affected run's chat header shows a warning badge.
  • Mask on render: the rendered chat replaces the value (any matched form) with [REDACTED:NAME]; the disk copy stays dirty (defense-in-depth, not remediation).
  • Rotation is the remediation: rotating the secret clears the exposed flag; deleting the secret removes it.

Broker-redacted output (#105's [REDACTED:NAME] placeholders) must NOT trigger the flag — the placeholder is proof of containment, not disclosure. Contained hits elsewhere (redaction, blocked push) stay quiet; only a transcript hit is a disclosure. Zero-secret repos: the scan is a no-op with no per-message overhead.

Key interfaces:

  • The chat tailer's message-processing path — scanning and mask-on-render happen here for all matched forms
  • Narrow vault coupling: the store hands the tailer a per-repo secrets.Matcher (or equivalent), not plaintext values; the tailer never logs a value on a hit. Note the matcher is not concurrency-safe — one per stream/session or serialised access
  • Exposed-flag lifecycle on the secret record: set (run id + timestamp) on transcript hit, cleared by rotate, removed by delete; surfaced on the repo secrets page and as a chat-header badge
  • No log line, event payload, or API response may carry the plaintext value

Acceptance criteria:

  • A secret value (any matched form) in a transcript marks the secret exposed with run + timestamp on the repo's secrets page
  • The affected run's chat shows a warning badge; rendered chat masks the value as [REDACTED:NAME] (all matched forms)
  • Rotating the secret clears the exposed flag; delete removes it
  • Broker-redacted output ([REDACTED:NAME] placeholders) does NOT trigger the exposed flag
  • The plaintext value appears in no log, event payload, or API response on a hit
  • Zero-secret repos: tailer scan is a no-op with no per-message overhead
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** A secret value appearing in a session transcript is a disclosure the operator learns about: the chat tailer detects it, marks the secret **exposed** (repo secrets page: which run, when, sticky), badges the run's chat header, and masks the value on render. Rotating the secret clears the flag. **Current behavior:** The chat tailer parses provider-owned transcript files and renders messages; it performs no secret scanning. Secrets, the streaming matcher, and rotation landed in #104. Lab cannot rewrite a provider's transcript file on disk — once a value is written there, the copy is permanently dirty; today nothing notices or contains it. **Desired behavior:** The tailer scans transcript content against the repo's secret values (exact + base64/hex/URL-encoded forms). On a hit: - The secret is marked **exposed** on the repo's secrets page — which run, when — sticky until cleared. - The affected run's chat header shows a warning badge. - **Mask on render:** the rendered chat replaces the value (any matched form) with `[REDACTED:NAME]`; the disk copy stays dirty (defense-in-depth, not remediation). - **Rotation is the remediation:** rotating the secret clears the exposed flag; deleting the secret removes it. Broker-redacted output (#105's `[REDACTED:NAME]` placeholders) must NOT trigger the flag — the placeholder is proof of containment, not disclosure. Contained hits elsewhere (redaction, blocked push) stay quiet; only a transcript hit is a disclosure. Zero-secret repos: the scan is a no-op with no per-message overhead. **Key interfaces:** - The chat tailer's message-processing path — scanning and mask-on-render happen here for all matched forms - Narrow vault coupling: the store hands the tailer a per-repo `secrets.Matcher` (or equivalent), not plaintext values; the tailer never logs a value on a hit. Note the matcher is not concurrency-safe — one per stream/session or serialised access - Exposed-flag lifecycle on the secret record: set (run id + timestamp) on transcript hit, cleared by rotate, removed by delete; surfaced on the repo secrets page and as a chat-header badge - No log line, event payload, or API response may carry the plaintext value **Acceptance criteria:** - [ ] A secret value (any matched form) in a transcript marks the secret exposed with run + timestamp on the repo's secrets page - [ ] The affected run's chat shows a warning badge; rendered chat masks the value as `[REDACTED:NAME]` (all matched forms) - [ ] Rotating the secret clears the exposed flag; delete removes it - [ ] Broker-redacted output (`[REDACTED:NAME]` placeholders) does NOT trigger the exposed flag - [ ] The plaintext value appears in no log, event payload, or API response on a hit - [ ] Zero-secret repos: tailer scan is a no-op with no per-message overhead
Author
Owner

This was generated by AI during triage.

Agent Brief addendum — out of scope:

  • Push notification for the exposure event — one-line follow-up once the notification seam work (#101) lands; this slice ships flag + badge + masking only
  • Rewriting or scrubbing the provider-owned transcript file on disk (lab cannot; rotation is the remediation)
  • Any alerting on contained hits (broker redaction #105, blocked pushes #106 — those stay quiet)
  • Scanning anything other than transcripts (broker output, diffs, and tracker writes are #105/#106/#107)
> *This was generated by AI during triage.* **Agent Brief addendum — out of scope:** - Push notification for the exposure event — one-line follow-up once the notification seam work (#101) lands; this slice ships flag + badge + masking only - Rewriting or scrubbing the provider-owned transcript file on disk (lab cannot; rotation is the remediation) - Any alerting on contained hits (broker redaction #105, blocked pushes #106 — those stay quiet) - Scanning anything other than transcripts (broker output, diffs, and tracker writes are #105/#106/#107)
Author
Owner

This was generated by AI during triage.

Agent Brief amendment — exposure push notification is IN scope.

The earlier out-of-scope line (issue body and addendum above) deferred the exposure push until the notification seam landed. It has landed: the Web Push plumbing (#98) and both triggers (#99 needs-input, #100 done-signal) are merged; only #101 (terse lock-screen toggle) remains open, and it is orthogonal — it changes how notifications render, not whether they send. This amendment supersedes that deferral.

Additional desired behavior:
When a transcript hit sets a secret's exposed flag — the unset→exposed edge, not every subsequent match while the flag is already set — send a push notification through the existing notify seam. Follow the landed trigger pattern (ADR-0039): the tailer/exposure site receives an injected notify callback and never imports the push package; the payload is {title, body, tag, route} with the route pointing at the affected run's chat. Title/body name the secret and the run — never the value. Since rotation clears the flag, a fresh transcript hit after rotation is a new edge and pushes again.

Additional acceptance criteria:

  • Setting the exposed flag sends exactly one push naming secret + run (never the value), routed to the affected run
  • Repeated matches while the flag is already set send no further push; a new hit after rotation pushes again
  • The exposure site uses the injected notify-callback seam (no push import), consistent with the #99/#100 triggers

Heads-up: #109 (open bug) reports a data race in Sender.Broadcast's payload handling. Don't work around it in this slice — it's fixed separately; just call the seam normally.

Still out of scope: terse/content-free rendering (#101), transcript rewriting, alerting on contained hits (#105/#106).

> *This was generated by AI during triage.* **Agent Brief amendment — exposure push notification is IN scope.** The earlier out-of-scope line (issue body and addendum above) deferred the exposure push until the notification seam landed. It has landed: the Web Push plumbing (#98) and both triggers (#99 needs-input, #100 done-signal) are merged; only #101 (terse lock-screen toggle) remains open, and it is orthogonal — it changes how notifications render, not whether they send. This amendment supersedes that deferral. **Additional desired behavior:** When a transcript hit sets a secret's exposed flag — the unset→exposed edge, not every subsequent match while the flag is already set — send a push notification through the existing notify seam. Follow the landed trigger pattern (ADR-0039): the tailer/exposure site receives an injected notify callback and never imports the push package; the payload is `{title, body, tag, route}` with the route pointing at the affected run's chat. Title/body name the secret and the run — never the value. Since rotation clears the flag, a fresh transcript hit after rotation is a new edge and pushes again. **Additional acceptance criteria:** - [ ] Setting the exposed flag sends exactly one push naming secret + run (never the value), routed to the affected run - [ ] Repeated matches while the flag is already set send no further push; a new hit after rotation pushes again - [ ] The exposure site uses the injected notify-callback seam (no push import), consistent with the #99/#100 triggers **Heads-up:** #109 (open bug) reports a data race in `Sender.Broadcast`'s payload handling. Don't work around it in this slice — it's fixed separately; just call the seam normally. **Still out of scope:** terse/content-free rendering (#101), transcript rewriting, alerting on contained hits (#105/#106).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#108
No description provided.