Strict RED-GREEN TDD over PR review blockers; every fix landed test-first with the failing run observed before implementation. - failureClass: closed privacy-safe vocabulary (worker.unavailable, vision.timeout, model.error) under strict dotted slug grammar. Newlines, carriage returns, ANSI/control characters, secrets, medical text, SQL, oversized values, and off-vocabulary classes fail the entire evidence file closed; nothing hostile can reach rendering. - depth: added to the counter vocabulary so queue.backlog can fire at all; bounded counters (0..1,000,000, integer) fail closed above the ceiling. 9/10/11 edge tests pin silent/at-threshold/above-threshold; backlog is suppressed while worker.outage pages (depth is residual from the same incident) and returns the moment the page clears. - drill integrity: the pre-drill baseline is now read from the fixture itself and must be genuinely healthy; already-outaged fixtures are refused with exit 2 without touching their switch, and a flip that produces no real healthy-to-outage transition reports DRILL FAIL instead of passing vacuously. - manual fallback: deterministic contract replaces the tautology. available+none-required when healthy, available+local-journal under any degradation, unavailable+app-down only when the app boundary itself is down. - fail-closed telemetry: evidence missing any of the four boundaries is rejected; unknown statuses surface as a warn telemetry.gap alert with owner/threshold/runbook instead of passing as healthy (documented in the runbook inventory). - drill origin: validateLoopbackOrigin gates every network path before any fetch. Only a bare http://127.0.0.1:<port> URL passes; credentials, DNS names, hex/decimal/percent-encoded IP encodings, IPv6 forms, paths, queries, fragments, and non-http schemes are refused pre-contact (raw-string grammar gate plus parse round-trip, because the URL parser canonicalizes hostile encodings). - terminal safety: controlSafe() strips C0/C1 control characters from all dynamically produced CLI output so hostile evidence paths cannot inject ANSI escapes into a terminal. Gates: npm test 98/98, check:syntax, npm audit (0 vulns), check:diff, deploy_staging status read-only; 30 adversarial probes against sanitizer, alert edges, and live loopback CLI all pass. No merge, no deploy.
9.9 KiB
Release observability and incident-response runbook
This runbook defines the privacy-safe release health surface for Timmy staging and release review. It is a reviewed operator document; it does not authorize changes to any live host. Everything here consumes sanitized release/health evidence only.
Privacy boundary (non-negotiable)
Telemetry in this system is release telemetry, not user telemetry. Evidence files, dashboard output, alerts, and drill transcripts must never contain:
- photos or medical imagery of any kind;
- stool records or journal entry content;
- session identifiers, cookies, tokens, credentials, or environment dumps;
- infrastructure addresses beyond the loopback
127.0.0.1drill fixture; - user identity of any kind (no emails, usernames, device IDs).
src/release-observability.js enforces this mechanically: sanitizeEvidence() allowlists bounded fields (release tag, full commit, UTC timestamp, per-check id/boundary/status/failure-class/latency/counters) and drops anything else — including any check whose extra fields look like keys, sessions, base64 payloads, image hashes, notes, or free text. Failure classes are a closed vocabulary (worker.unavailable, vision.timeout, model.error) matched against a strict dotted lowercase slug grammar; anything else — newlines, ANSI/control characters, secrets, medical text, unknown classes — fails the whole evidence file closed rather than being rendered. Counters are bounded non-negative integers capped at 1,000,000. Every one of the four boundaries (app, api, queue, model) must be present; partial telemetry is refused instead of rendered as silent health. If sanitization fails, the tool exits nonzero and echoes nothing from the input, and all CLI output is scrubbed of control characters so hostile file paths can never inject terminal escapes. The sanitizer itself is tested with hostile payloads (tests/release-observability.test.js) and the CLI is tested to never echo rejected content (tests/release-dashboard-cli.test.js).
Dashboard
Build a sanitized evidence file (schema below) and render:
node scripts/release_dashboard.mjs --evidence /secure/inbox/evidence.json
Output groups checks under four boundaries — app, api, queue, model — with pass/fail/degraded counts, max latency, bounded counters, failure-class totals, active alerts, and manual-fallback state. Exit code is 0 when no page-severity alert fires, 1 when one does, 2 on bad input. The CLI never contacts a live host; it reads local files or a loopback drill fixture only.
Evidence schema (v1)
{
"schemaVersion": 1,
"releaseTag": "daily-2026-08-22.1",
"commit": "40 lowercase hex characters (12+ accepted)",
"generatedAtUtc": "YYYY-MM-DDTHH:MM:SSZ",
"checks": [
{
"id": "lowercase-dot-or-dash id",
"boundary": "app | api | queue | model",
"status": "pass | fail | degraded | unknown",
"failureClass": "dotted.class.name or null",
"latencyMs": integer milliseconds or omitted,
"counters": { "ok|fail|abstain|retry|timeout|rejected|fallback|depth": non-negative integer }
}
]
}
Any other field is dropped, and checks carrying forbidden-shaped fields are discarded individually. Counters are capped to the fixed vocabulary above so free text can never smuggle in through a label. Issue #19 owns producing these signals from the worker; until that lands, evidence can be hand-authored for drills.
Alert inventory
Alerts are evaluated by evaluateAlerts() over the sanitized dashboard. Every rule carries an owner, a threshold, severity, and a runbook anchor in this file. Page-severity alerts make the CLI exit 1.
| Alert id | Severity | Owner | Threshold (observed >=) | Condition | Runbook |
|---|---|---|---|---|---|
worker.outage |
page | release-operator | 1 | any check reports worker.unavailable, or both model and queue boundaries report failures |
Simulate a worker outage |
telemetry.gap |
warn | release-operator | 1 | one or more checks report status unknown (missing telemetry fails closed as a warning, never as health) |
Alert inventory |
queue.backlog |
warn | release-operator | depth >= 10 | queue depth counter at or above 10; suppressed while worker.outage pages (depth is residual from the same incident) |
Alert inventory |
vision.degraded |
warn | vision-owner | 3 | three or more vision.timeout failure classes |
Alert inventory |
app.unhealthy |
page | release-operator | 1 | app boundary healthz reports fail; this is also the only condition that makes the manual fallback unavailable (reason: app-down) |
Alert inventory |
Escalation: pages go to the release operator on call (currently Alexander as release owner); warns are batched into the next release-review pulse. An unresolved page after one manual fallback verification is raised in the Gitea issue for the affected release epic rather than paged repeatedly.
Simulate a worker outage
This drill proves the issue #41 acceptance criterion: one simulated outage produces exactly one actionable page alert while the graceful manual fallback stays available. It runs entirely against a throwaway loopback fixture — never against staging or production.
- Start any loopback fixture that serves:
GET /api/drill/checksreturning{ "checks": [...] }, healthy first, with at least one check for each boundary (app, api, queue, model);POST /drill/outageflipping the fixture's simulated worker off (subsequent/api/drill/checksresponses then reportstatus: "fail",failureClass: "worker.unavailable"across the api, queue, and model checks).
- Run the drill:
node scripts/release_dashboard.mjs --drill-origin http://127.0.0.1:<port>
The origin must be a bare http://127.0.0.1:<port> URL. Credentials (user:pass@), DNS names, alternative IP encodings (hex octets, decimal integers, percent-encoding), IPv6 forms, non-http schemes, and any path or query are rejected before the CLI contacts anything.
-
Required outcome (the automated tests assert all of it):
- the pre-drill baseline is read from the fixture itself and must be genuinely healthy — an already-outaged fixture is refused with exit code 2 and its switch is never touched;
- after the flip, exactly one page alert fires —
worker.outage; if the flip produced no real healthy-to-outage transition, the drill reportsDRILL FAILwith exit code 1 instead of passing vacuously; - the alert names owner
release-operator, its threshold, and this runbook section; manual fallback: available — local journal remains usable;- exit code
0(DRILL PASS).
-
Manual fallback procedure if a real outage ever matches this signature: users keep logging locally (the browser journal never depends on the vision worker), operators capture one sanitized evidence file, run the dashboard once, record the single page alert, and follow rollback via the staging runbook. No user data leaves the device.
The drill is exercised end-to-end by tests/release-dashboard-cli.test.js ("outage drill flips one simulated switch…") using an in-test loopback fixture; CI runs it on every PR without touching any real host.
Incident flow (documented, local-first)
- Detect — dashboard run or drill shows a page alert.
- Triage — read the alert's failure class and boundary; open the linked runbook anchor above.
- Contain — confirm the manual fallback line says
available; if it does not, treat the app boundary as down too and escalate immediately. - Verify scope — re-run the dashboard on fresh sanitized evidence; never attach raw logs or user content to the incident.
- Recover — use
scripts/deploy_staging.py rollback --commit <known-good>inside an approved window (seedocs/STAGING-RUNBOOK.md), or fix forward through a reviewed PR. - Review — file the sanitized dashboard output (text only) plus commit/tag identity as evidence in the release issue; delete stale evidence files from the inbox.
Receipts
npm test— includestests/release-observability.test.js,tests/release-dashboard.test.js,tests/release-dashboard-cli.test.js.- Sanitizer hostile-payload test proves session tokens, cookies, environment dumps, private keys, base64 payloads, image hashes, note text, and emails cannot survive into dashboard output.
- Hostile failure-class test proves newlines, carriage returns, ANSI escapes, control characters, secrets, medical text, SQL, oversized values, wrong types, and off-vocabulary classes all fail closed.
- Depth-counter tests prove the
queue.backlogwarn stays silent at 9, fires at exactly 10, reports observed counts above threshold, sums across queue checks, is suppressed whileworker.outagepages, and cannot be smuggled in above the bounded counter ceiling. - Boundary-completeness tests prove evidence missing any of app, api, queue, or model fails closed; unknown statuses surface as a
telemetry.gapwarn instead of passing as healthy. - Manual-fallback tests prove a deterministic contract: journal available with no degradation, active (
local-journal) under any non-fatal degradation, and unavailable only when the app boundary itself is down. - CLI tests prove rejected evidence exits nonzero without echoing contents, drill origins are validated to bare loopback http before any network contact (credentials, DNS names, hex/decimal/percent-encoded IP encodings, IPv6, paths, queries, and foreign schemes are refused), the outage drill reads a genuine fixture baseline, refuses already-down fixtures without touching their switch, fails when no real transition occurs, and yields exactly one actionable alert with graceful manual fallback.