Specification: Redacted Output Capture and Timeout Reporting
Status: Authoritative — Implemented and Verified Sources:
crates/terraphim_spawner/src/redaction.rs(~6,706 bytes)crates/terraphim_spawner/src/output.rs(~12,997 bytes) Issue: #1924 (re-scoped from PR #1788 Slice 8) Date: 2026-06-01 Test Coverage: All invariants verified viacargo test -p terraphim_spawner redactionandcargo test -p terraphim_spawner output
Overview
The spawner captures agent stdout/stderr line-by-line, detects @mention patterns,
and stores a bounded buffer of events for timeout diagnostics. All events stored in
the buffer are redacted before storage so that if the buffer is attached to a timeout
report or Gitea comment, no secrets leak into issue trackers or logs.
Redaction
Patterns
The DEFAULT_REDACTION_PATTERNS array in redaction.rs defines seven regex patterns:
| Pattern | What it matches |
|---------|----------------|
| api[_-]?key\s*[:=]\s*([^\s]+) | api_key=<value>, api-key: <value>, etc. |
| token\s*[:=]\s*([^\s]+) | token=<value>, TOKEN: <value>, etc. |
| secret\s*[:=]\s*([^\s]+) | secret=<value>, etc. |
| password\s*[:=]\s*([^\s]+) | password=<value>, etc. |
| sk-[a-zA-Z0-9]{20,} | OpenAI-style API keys |
| ghp_[a-zA-Z0-9]{36} | GitHub personal access tokens |
| bearer\s+[a-zA-Z0-9_\-]{20,} | Bearer tokens in Authorization headers |
All patterns are case-insensitive. The last capture group in each pattern holds the secret value; prefix groups are preserved.
Replacement Token
Matched secret values are replaced with the literal string ***REDACTED***.
verify_redacted
A companion function checks whether a string is clean. It returns true if either:
- no pattern matches, or
- the string already contains
***REDACTED***(considered clean; was already processed)
Output Capture
OutputEvent Variants
| Variant | Redacted on storage? |
|---------|----------------------|
| Stdout { process_id, line } | Yes — line is passed through redact() |
| Stderr { process_id, line } | Yes — line is passed through redact() |
| Mention { process_id, target, message } | Yes — message is passed through redact(); target is not |
| Completed { process_id, exit_code } | n/a (no text payload) |
Bounded Buffer
OutputCapture maintains a VecDeque<OutputEvent> capped at MAX_CAPTURED_EVENTS = 4096.
When the buffer is full, the oldest entry is evicted (pop_front) before inserting the
new event (push_back). This is a FIFO circular buffer.
Live Streaming
In addition to the bounded buffer, every event is broadcast on a tokio::sync::broadcast
channel (capacity 256). Subscribers (e.g., WebSocket clients) receive raw events before
redaction, because they are direct observers of the live stream. Stored events are always
redacted.
Invariants
| # | Invariant | Source |
|---|-----------|--------|
| I1 | Events written to the bounded buffer are always redacted. | record_event calls event.redacted() before push_back |
| I2 | The buffer never exceeds MAX_CAPTURED_EVENTS entries. | pop_front when len >= MAX_CAPTURED_EVENTS |
| I3 | Completed events carry no text; redaction is a no-op. | OutputEvent::redacted() match arm |
| I4 | Mention.target is never redacted (it is an agent name, not a secret). | target: target.clone() in redacted() |
| I5 | An empty line is skipped and never stored. | if line.is_empty() { continue; } in capture loops |
Failure Modes
| Failure | Observable Effect | Recovery |
|---------|-------------------|---------|
| Regex compilation error in redact() | Pattern silently skipped; other patterns still apply | Validate patterns at startup |
| Buffer full | Oldest events evicted; tail of output retained | Increase MAX_CAPTURED_EVENTS if diagnosis is impaired |
| Live broadcast receiver lagged | Receiver is dropped; no data loss in buffer | Subscriber reconnects |
| Stdout/Stderr read error | Error logged at ERROR level; capture goroutine exits | Agent process has exited or EOF |
Verification Note
The following tests were verified to pass on gitea/main as of 2026-06-01:
Tests verified (redaction):
test_redact_api_keytest_redact_tokentest_redact_secrettest_redact_passwordtest_redact_sk_keytest_redact_bearer_tokentest_no_false_positives_on_safe_texttest_verify_redacted_detects_leaktest_verify_redacted_after_redactiontest_redact_preserves_structuretest_redact_multiple_secrets
Tests verified (output capture):
test_mention_regextest_output_event_redacted_scrubs_secretstest_output_event_redacted_preserves_structuretest_captured_events_boundedtest_captured_events_redacts_before_storage