Final-review fix wave — P1 exit gate
Status: DONE. All four items closed; live stack redeployed and verified.
Item 1 — fatal on ingester boot death
cmd/observatory/main.go: split run() into runFromEnv() (loads config
from OBSERVATORY_CONFIG) + run(cfg *config.Config) error (testable core).
Added a case err := <-ingestDone: arm to the startup select: since
ingestCtx is only canceled later (via cancelIngest(), after this select
returns), reaching this arm at all — error or nil — means the ingester died
or exited before any shutdown was requested. Both cases now return a fatal
error from run(), which main() turns into log.Fatal.
Test: cmd/observatory/main_test.go — TestRunFailsFastWhenIngesterCannotStart
points RedisURL at 127.0.0.1:1 (connection refused, immediate) with the
real test DB, asserts run() returns non-nil within 10s. Skips cleanly if the
test DB isn't up (mirrors store.TestPool).
Item 2 — /series error mapping + bucket validation
internal/api/server.go handleSeries: bucket <= 0 now rejected at the API
layer (400/BAD_REQUEST) before ever reaching the store. store.QuerySeries
errors now map to 500/QUERY_FAILED (matching handleSummary/handleCalls)
instead of 400/BAD_REQUEST.
Test: extended TestSummaryAndSeriesReturnData in internal/api/server_test.go
with bucket=0s and bucket=-1m → both assert 400. The 500/QUERY_FAILED path
isn't independently exercised (forcing store.QuerySeries to fail would mean
breaking the shared test pool, taking out every other assertion in that test)
— verified by reading: the mapping is identical to the already-tested
handleSummary/handleCalls 500 paths.
Item 3 — doc caveats
deploy/observatory.example.yaml:retention_daysnow carries a comment —
declared but NOT yet enforced, retention job is backlog, see README.README.md: new "Known limitations" section — retention backlog + tripwire
(body-heavy engine flip /ai_call_bodies> 5GB / disk < 25GB free);
hash-chain envelope boundaries (full_body_sha256/tsoutsiderow_hash,
trigger-protected only; mid-chain verify trusts storedprev_hash, only a
genesis run proves the full prefix; tamper-evident not tamper-proof, no
external anchor);/api/verify-chainresponse shape
({"ok": bool, "bad_chain_pos": int64}). File is 73 lines (≤80 budget).
All three committed together.
Verification
go build ./cmd/observatory -> OK
go vet ./... -> OK
go test ./internal/api/ ./internal/ingest/ ./cmd/... -count=1 -race -> ok (all 3 packages)
go test ./... -count=1 -p 1 -> ok (all 5 packages)
Item 4 — live token rotation + redeploy (ai-ahu, 192.168.83.20)
- Confirmed the three tokens named in the leaked report matched the live
~/ahu-ai-observatory/deploy/observatory.yamlexactly (executive/operator/
auditor). - Generated three new
openssl rand -hex 16tokens on the host, edited
observatory.yamlin place (same roles, new values) — the new values were
never printed to, or transmitted through, this session; only pass/fail
checks and HTTP status codes were echoed back. docker compose restart observatory; verified/healthzOK, an authed
call with a new token → 200, the same call with an old token →
401.- Sanitized
.superpowers/sdd/task-9-report.md: all four occurrences of the
three old tokens (the credentials table + two examplecurlcommands)
replaced withREDACTED-ROTATED, plus a dated note pointing at this file.
Re-uploaded (URL below) so the exposed version is replaced. Repo-wide
grep confirms no other file contains the old token strings. - Deployed the Item 1/2 code fix to the host:
rsync -avz --exclude='.git' --exclude='.superpowers' --exclude='/observatory' ./to
~/ahu-ai-observatory/(host'sdeploy/observatory.yamluntouched — no
local copy of that file to overwrite it with), then
docker compose up -d --build. Post-redeploy:/healthzOK, authed call
with the rotated operator token → 200. - New tokens recorded only in
~/ahu-ai-observatory/deploy/observatory.yaml
onai-ahu— nowhere else, including not in this report.
Links
- README (rendered): https://x056.think.val.id/README.md
- Sanitized task-9 report (rendered): https://x056.think.val.id/task-9-report.md