fix(webui): make timeline session filter truthful and redact evidence refs (#637)
Remediates the two blocking findings in review #522 on PR #849. F1 — the session filter dimension was dead end to end. No source could produce an event carrying a session identifier, so filter_events dropped every event whenever session was supplied and the API answered with a green, empty page. An empty result reads to an operator as "no such session activity", which is a stronger and false claim. Each source now declares which filter dimensions its records can actually carry. The control-plane events table is (event_id, work_item_id, event_type, message, created_at) and records no session, so that source declares the session dimension unsupported rather than pretending to answer it; the dead read of a non-existent session_id column is removed. A CTH handoff comment declares its own Session field, so the handoff adapter populates session_id from that declared field — authoritative source data, never inferred from an actor, work item, or message text. When no source that ran can carry a requested dimension, load_timeline refuses with ok=false and a structured error naming the unsupported filters and the per-source reason, and the route answers 422. A source that can answer the dimension and simply matched nothing still returns 200 with an honest empty page. Ordering, pagination, and the issue/PR filters are unchanged. F2 — evidence_refs bypassed redaction and could emit a credential verbatim. proof and decision were passed to _extract_evidence_refs before redaction, and the SHA pattern matched any 7-40 character lowercase hex run, which is exactly the shape of a Gitea access token. Redaction now runs first and every derived value is taken from the redacted text. A commit reference is recognised only where the source text declares one (commit, head, base, sha, ...), so an undeclared hex run is never lifted out of prose into a structured field; this also drops the ordinary-word noise the reviewer noted. Every reference is then independently revalidated against an allowed shape and a second redaction pass immediately before serialization, failing closed by dropping anything unproven and flagging the event sensitive. Actor is redacted for the same reason, and a secret-shaped session value is dropped rather than emitted. Legitimate issue, PR, short-SHA and full 40-character SHA references stay usable. Tests: 16 added. Session filtering is now driven through the CTH adapter and the composed load_timeline/API path rather than a hand-built WorkflowEvent, covering a match, an honest empty result, pagination and ordering under the filter, the 422 refusal, and the per-source support declaration. Redaction coverage asserts a synthetic 40-character hex value (not a real credential) appears nowhere in the complete serialized payload including evidence_refs, that the independent revalidation drops unproven references, and that legitimate references still resolve. pytest tests/test_webui_timeline.py: 42 passed (was 26). pytest -k webui: 340 passed, 270 subtests passed (was 324). Full suite: 4607 passed, 12 failed, 6 skipped, 684 subtests passed — the same 12 failures as the master baseline, none under webui/. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+35
-3
@@ -348,14 +348,46 @@ and `offset` for pagination; `remote`, `org`, `repo` to override the default
|
||||
registry-project scope. Events sort ascending by
|
||||
`(timestamp, source_rank, event_key)`; missing timestamps sort last.
|
||||
|
||||
### Filter authority, and refusing what cannot be answered
|
||||
|
||||
A filter dimension is only meaningful for a source whose records carry it.
|
||||
Each source declares its own support in `_SOURCE_FILTER_SUPPORT` and reports it
|
||||
per response as `supported_filters` / `unsupported_filters`:
|
||||
|
||||
| Source | issue | pr | session |
|
||||
|---|---|---|---|
|
||||
| `control_plane` | yes | yes | **no** — the `events` table is `(event_id, work_item_id, event_type, message, created_at)` and records no session |
|
||||
| `gitea_handoff` | yes | yes | yes — a CTH comment declares its own `Session:` field |
|
||||
|
||||
`session_id` is read only from that declared CTH field. It is never inferred
|
||||
from a work item, an actor, or message text, and a value that is
|
||||
redaction-altering or bare-secret-shaped is dropped rather than emitted.
|
||||
|
||||
When **no source that ran** can carry a requested dimension, the request is
|
||||
refused rather than answered: the response is `422` with `ok:false` and a
|
||||
structured `error` naming `unsupported_filters` and the per-source reason. A
|
||||
`200` with zero events would tell an operator that no such activity exists,
|
||||
which is a stronger — and false — claim than "this cannot be answered here".
|
||||
A source that *can* answer the dimension and simply matched nothing still
|
||||
returns `200` with `ok:true` and an empty page.
|
||||
|
||||
### Redaction
|
||||
|
||||
Every free-text field (event messages, decision/proof text, roles) is passed
|
||||
through the console redaction policy (`webui.console_redaction`, backed by
|
||||
`gitea_audit.redact`) before it leaves the module, failing closed to the
|
||||
Every free-text field (event messages, decision/proof text, roles, actors) is
|
||||
passed through the console redaction policy (`webui.console_redaction`, backed
|
||||
by `gitea_audit.redact`) before it leaves the module, failing closed to the
|
||||
placeholder. No unredacted tool arguments or secrets are ever emitted, and a
|
||||
generation error never drops raw data to a caller or a log.
|
||||
|
||||
Redaction also runs *before* any structured value is derived from free text.
|
||||
`evidence_refs` are extracted from already-redacted proof/decision text, and a
|
||||
commit reference is recognised only where the text declares one (`commit`,
|
||||
`head`, `base`, `sha`, …). An undeclared 40-character hex run has the exact
|
||||
shape of a Gitea access token, so it is never lifted out of prose into a
|
||||
structured field. Every reference is then independently revalidated against an
|
||||
allowed shape and a second redaction pass immediately before serialization;
|
||||
anything unproven is dropped and the event is flagged `sensitive`.
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user