Skip to content

Gap-hunt audit (PR #31/#32/#33 surface): 18 fixes + catalogued-entry cleanup - #34

Merged
yashau merged 33 commits into
mainfrom
claude/elastic-khorana-78b100
Jul 8, 2026
Merged

Gap-hunt audit (PR #31/#32/#33 surface): 18 fixes + catalogued-entry cleanup#34
yashau merged 33 commits into
mainfrom
claude/elastic-khorana-78b100

Conversation

@yashau

@yashau yashau commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

An adversarial correctness/security audit of the freshly-landed PR #31/#32/#33
surface (provisioning node state, agent update-available, scheduling defaults,
web-console polish), run as the repeatable loop in
docs/contributing/audit-workflow.md, converged at 5 consecutive clean runs
(Runs 9–13, 13 runs total). This PR bundles the 18 audit fixes plus the
post-convergence catalogued-entry cleanup (21 more items) the ledger tracked.

Ledger: docs/internal/audits/2026-07-08-gap-hunt.md.

Audit fixes (18)

Headlines:

  • Critical G1 — a Hono static-vs-param route-registration collision left the
    entire PR Recorder-agent update-available check; promote node lifecycle to ✅ #32 "update available" feature silently dead in production (its
    isolated unit test stayed green). Reproduced empirically, fixed, locked with a
    full-surface route test.
  • N4 — a heartbeat could un-promote a live node to provisioning and
    suppress its offline alert; the controller now owns the lifecycle state machine.
  • R7-IPCAP — a multi-homed node's over-cap ipAddresses desynced it
    permanently (every heartbeat 400'd → flipped offline while live); the schema now
    truncates instead of failing closed.
  • Provisioning-reachability class (N1/N2), stub-upload removal completion
    (H3-1/H3-2/H3-3), pagination re-clamp (H4-1), UI-honesty fixes
    (H4-2W/R8), and route-shadow residual (H1-1).

Post-convergence cleanup (21 fixed, 6 deferred)

Worked through the catalogued backlog after convergence. None were confirmed
regressions (that's why it converged) — they are hardening / consistency /
UX-honesty improvements. Highlights:

  • API: server-side uploadPolicyIds dedup (R4-1), per-node liveness error
    isolation (R4-2), accurate node_provisioning action reason (H1-3), agent-release
    paging + bounded body read (N-3A/3B), request-contract + cap coverage.
  • Web: offline status → Critical/red (operator decision, R3-1), shared
    buffered NumberField + decimal-guarded numericInputCommit across the settings
    cards (H4-2/H4-3/R7-NUMCOMMIT-HEX), stale node/interface/default guards
    (R9/S3), nodeStatusLabel (H2), dead-export removal (S2), and W2/W3/W4A/R6.
  • Rust agent: ipAddresses capped at the documented heartbeat limit
    (R7-IP-AGENT-CAP).

Deferred (6), with rationale in the ledger: switcher password-clear affordance
(R3-3, operator decision) + its coupled coverage (R3-6); render-harness-gated items
(H4-1-CLAMP-HOOK-COV, W1, W4); and R7-SEED-LIVENESS as won't-fix — deriving
liveness on the frozen-timestamp demo seed store would flip the whole demo offline
~120s after startup (production always uses Postgres, which derives).

Two operator product decisions were honored: offline tone → Critical and
switcher password-clear → defer.

Testing

Every fix with a unit seam has a red→green or coverage-lock test; React-render-only
items are verified by tsc + oxlint + build (the web unit harness has no RTL/jsdom).

Full gate green at close: mise run check exit 0 — tsc, API + web node tests,
oxlint, oxfmt, check:loc, db:verify (Drizzle replay 0001–0046), all baseline
verifiers, rust:check/clippy/fmt/miri (138 pass / 0 fail / 28 miri-ignored),
and agent:fake-controller-smoke — plus mise run build.

🤖 Generated with Claude Code

yashau and others added 30 commits July 8, 2026 13:30
GET /api/v1/nodes/agent-release was registered after GET
/api/v1/nodes/:nodeId. The static+param collision forces Hono onto the
registration-order-sensitive TrieRouter, so the release route was
swallowed by the detail handler (404), silently killing the whole PR #32
update-available feature in production while isolated unit tests stayed
green. Register the static route first (mirroring /export); make the
release service injectable through registerNodeRoutes; add a
full-surface route test (red: 404, green: 200). Extract large node-route
test fakes to node-routes-helpers.ts to stay under the LOC guard.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
A never-contacted provisioning node was reported as reachable by both
the rakkr_node_online metric gauge and the dashboard Active Nodes count,
because each used a naive status !== "offline" that predates the new
provisioning state — inflating the reachable count and masking the
RakkrNodeOffline alert. Add a shared isNodeReachable predicate (online/
recording/degraded/alerting) and use it in both sites so they cannot
diverge again. Red->green via a metrics test (provisioning -> 0) and a
dashboardReportingNodes helper test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The calendar day-cell create path called defaultDraft(firstNode) without
the controller settings, so it always fell back to the built-in profile/
policies and ignored the operator's configured scheduling defaults (the
schedules list page passes them). Thread the already-fetched controller
settings into the create draft, mirroring the list page.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…(audit S4)

The keep-vs-?? merge (omitted field keeps, explicit null clears a
default) was only exercised by a DB-gated test, so a regression of keep()
to ?? would clear-silently-broken with the whole in-memory suite still
green. Add an in-memory route test that sets two defaults independently,
confirms an unrelated PATCH preserves them, and confirms explicit null
clears exactly one.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ne (audit N3)

The node-lifecycle baseline verifier (shipped to promote node lifecycle
to done) never asserted the provisioning-gating invariant PR #31
introduced: its source list omitted node-liveness.ts and
watchdog-node-liveness.ts and no phrase/snippet mentioned provisioning or
the offline gate. Document the provisioning/offline-liveness contract and
extend the verifier to assert the gate source, the isNodeReachable
predicate, and the liveness/watchdog test titles.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
useServerPagination only reset offset on filter/page-size change, never
when the row total shrank below the current offset. Deleting the last
page's rows (bulk delete / retention sweep) stranded the operator on a
blank page past the end with only Previous to escape. Add a pure
clampedOffset helper (red->green) + a clampToTotal hook method the pages
call during render with the server meta.total, mirroring the filter-reset
pattern. Wired into all paginated pages.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
PR #31 filtered the test-only stub upload policy from settings/scheduling
but missed two paths: (H3-1) the recordings page fed the unfiltered list
to the per-recording and bulk Queue Upload dropdowns, so the stub was a
selectable — and default-first — real upload target; (H3-2) editing a
legacy schedule persisted with the stub silently re-saved it (the form's
filtered toggles left no way to clear it). Add a selectableUploadPolicies
helper for the recordings action dropdowns (labeling keeps the full list),
and strip the stub in scheduleToDraft. Red->green helper + draft tests.
(recordings.tsx also carries its pagination clampToTotal wiring, H4-1.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…H1-1)

The residual instance of the G1 route-shadow class: the node-auth
GET /recording-jobs/:jobId (registered before the operator export route)
swallowed GET /recording-jobs/export under TrieRouter, answering it with a
node-credential 401. The agent handler must stay registered first (so
agent job-reads work), so defer the reserved 'export' segment to the
downstream operator handler — a job id is never literally 'export'.
Red->green in the agent-job-read harness (production registration order).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…it N4)

The heartbeat write persisted the agent-reported status verbatim and the
schema accepted 'provisioning'/'offline', so a first heartbeat only
promoted a provisioning node because the shipped agent hardcodes 'online'
— and any node-credential holder could POST status:'provisioning' to
un-promote a live node, which deriveNodeStatus short-circuits, suppressing
its offline alert forever. Coerce a heartbeat's provisioning/offline to
online in both stores (heartbeatStatus); the controller owns lifecycle
state. Red->green transform tests (also closes the H2-2 coverage gap).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
PR #31 intended every upload policy to target a real destination, but the
create input left destinationId optional and the console eager-created a
destination-less policy on New — assignable, and reconciling its
recordings to partial (provider_not_configured). Enforce destinationId at
the operator create route (the store stays lenient for seeds/tests);
client seeds the first destination and disables New until one exists.
Red->green route test; fixture updates for the stricter route.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The node inventory status filter omitted 'provisioning', so operators
could not filter to the enrolled-but-never-contacted cohort the API fully
supports. Add it and lock the dropdown against the full NodeStatus enum.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Each schedule upload policy id fans a recording out to its own upload
queue item, but the list was uncapped and un-deduped — a schedule:manage
holder could multiply queue work per recording with a long/duplicate list
(the sibling switcher-mappings list already caps at 256). Cap the schedule
create/update schemas at 32 and dedup the client draft before submit.
Red->green dedup test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…3 coverage)

The create route rejects a destination-less policy; add the matching
coverage on the update path — an empty destinationId is schema-rejected
(400) and an omitted one is preserved — so a future nullable/empty-allowed
change to the update schema can't silently reopen the H3-3 hole.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Run 4 convergence attempt: adversary-on-Run-3 + completeness critic (all
85 changed files), data-integrity/concurrency, permission-boundary sweeps
— zero code changes; db:verify migration replay green. R4-1/R4-2 logged
as suspected/mitigated. First clean run.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Run 5 convergence (fresh angles): end-to-end feature-trace, deploy/ops +
docs/baseline consistency, observability/error-surface — all clean, zero
code changes. node:test-db 22/22 green. Second consecutive clean run.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…-triage)

Run-6 re-triage found H4-2 was mis-classified as cosmetic for the watchdog
editor: clearing a numeric field yields Number("")===0, and thresholdDbfs
(dbfsSchema [-160,24]) and the score thresholds ([0,1]) ACCEPT 0 — so a
cleared threshold silently persists 0 and arms an always-fire alert
(watchdog-signal fires low-signal for all healthy audio), flooding the
operator with spurious critical health events. Add a shared numericInputCommit
helper (empty/invalid -> no commit, never 0) + a local text buffer in the
shared NumberField (keeps the field editable while typing) so all 16 watchdog
numeric fields are fixed at one point. Red->green helper test. Other editors'
clear->0 is server-rejected (no persist) and stays the catalogued cosmetic.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…reset 2->0)

Catalogue re-triage found H4-2 mis-classified (watchdog thresholdDbfs/score
thresholds accept 0 -> persisted always-fire alert); fixed as H4-2W. Other
19 catalogued items confirmed correctly triaged. New suspected: R6-ENROLL-DUP,
R6-INSTALL-URL-QUOTE.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…dit R7-IPCAP)

The agent's collect_ip_addresses() reports every address from 'hostname -I'
uncapped, but the heartbeat schema caps ipAddresses at .max(16). A multi-homed
node (>16 IPs: IPv6 SLAAC/privacy + Docker/libvirt/VLAN bridges) had every
heartbeat rejected 400 — and since the agent freezes the IP list at startup,
that desynced the node forever, freezing lastSeenAt and flipping the live,
recording node offline. A liveness heartbeat must not fail closed over a
cosmetic field: preprocess ipAddresses to truncate to the documented cap and
accept the heartbeat (keeping the first 16, the primary addresses). Red->green
route test (20 IPs -> 202 + truncated, was 400). Agent-side .take(16) noted as
defensive follow-up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…(streak 0)

Rust agent-contract angle found the agent's uncapped ipAddresses vs the
heartbeat .max(16) reject -> permanent desync for multi-homed nodes; fixed
controller-side (truncate). H4-2W verified sound. New catalogue: R7-IP-AGENT-CAP,
R7-NUMCOMMIT-HEX, R7-SEED-LIVENESS.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…R8-RETENTION-SELECT)

The schedule form wrapped its recording-profile and watchdog Selects in
withSelectedOption (which prepends a synthetic option so a current-but-absent
id stays visible), but the retention Select rendered the raw list. A stale/
deleted retention id (a prefilled controller-settings default, or an edited
schedule whose policy was removed) made the controlled Select fall back to its
placeholder — reading as 'unselected' while the draft silently kept and
re-saved the id. Route retention through the same helper (moved to schedule-draft
as a shared, now-tested export). Red->green helper test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…k 0)

Broad web sweep found the schedule-form retention Select not wrapped in
withSelectedOption (stale id renders blank but re-saves); fixed. R7-IPCAP
verified sound+complete. Runs 6-8 each closed one marginal PR-surface defect.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adversary verified R8 sound; completeness critic over all 85 changed files
found no new real defect. Zero code changes. R9-NODEIFACE-SELECT catalogued
(pre-existing/bounded). First clean run after the 6-8 dirty streak.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Security-lens + production-scenario edge passes both clean; zero code changes.
Cross-replica schedule double-fire investigated -> documented single-writer
constraint, rejected. Two consecutive clean runs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fresh-eyes dense-logic re-read + test-durability critic (all 18 fixes'
tests re-derived to catch a revert) both clean; zero code changes. Three
consecutive clean runs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diff-as-reviewer (raw PR hunks) + React effect/query-timing passes, both
clean (run inline after session limits killed the subagents; diff-reviewer
rerun independently confirmed). Zero code changes. Four consecutive clean runs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Run 13 final convergence: last-chance broad sweep + agent-contract re-confirm
both clean; full 'mise run check' (incl. rust+miri+fake-controller-smoke+
db:verify+all baselines) and 'mise run build' green. 18 findings fixed across
13 runs against static base 619b6f1; remaining opens all catalogued cosmetic/
suspected/pre-existing/by-design.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Post-convergence cleanup of the 2026-07-08 gap-hunt catalogue (API items):

- R4-1: dedup uploadPolicyIds server-side in buildSchedule +
  sanitizeScheduleUpdate (mirrors uniqueTags) so the server is authoritative.
- R4-2: isolate each node's reconcile in reconcileNodeLivenessEvents with a
  per-node try/catch -> {skipped, reconcile_failed} so one failing node no
  longer aborts the whole tick.
- H1-3: treat a provisioning node as unavailable in node-action-routes with an
  accurate node_provisioning reason (not node_offline).
- N-3A/3B: agent-release fetch now follows Link rel=next (bounded to 5 pages,
  same-origin) and reads the body through a bounded reader (Content-Length
  pre-check + streaming byte cap); an over-cap body is rejected, last-good kept.
- N-COV-FETCH + R11: lock the GitHub request contract and the uploadPolicyIds
  >32 rejection.

Each with a red->green/coverage test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
yashau and others added 3 commits July 8, 2026 19:48
Post-convergence cleanup of the 2026-07-08 gap-hunt catalogue (web items):

- R3-1 (operator decision: Critical): offline node status now reads critical
  (red); nodeStatusBadgeClass rewritten as an exhaustive switch.
- R3-2: toneFillClass gets an explicit neutral (muted) branch.
- H4-2/H4-3: extract the buffered NumberField into shared settings-fields and
  wire it into the recording-profile, upload-policy, and watchdog cards;
  retention's optionalNumber routes through numericInputCommit.
- R7-NUMCOMMIT-HEX: numericInputCommit gates on a decimal-shape regex.
- R9: node/interface selects show a synthetic "(unavailable)" option for a
  stale id (mirrors withSelectedOption).
- R6-INSTALL-URL-QUOTE: shell-quote every interpolated install-command value.
- H2: nodeStatusLabel (Title Case) replaces raw status on dashboard,
  room-detail, and the nodes table.
- S2: remove the dead schedulingDefaultsFrom export.
- S3: defaultDraft validates operator defaults against available lists
  (threaded from schedules list + calendar via cached query data).
- W2: TruncateCell effect depends on [children] (no per-render observer churn).
- W3: nodes.tsx omits the Actions column entirely for read-only users.
- W4A: withWatchdogDisplayDefaults folds display fallbacks into the draft so the
  watchdog form is what-you-see-is-what-you-save.
- R6-ENROLL-DUP: invalidate ["nodes"] on enroll error too.

Unit-tested where a seam exists (node-status, settings-updates, schedule-draft,
node-page-helpers); render-only items verified via tsc + oxlint + build.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
R7-IP-AGENT-CAP: refactor collect_ip_addresses to a pure parse_ip_addresses
helper bounded to MAX_IP_ADDRESSES (16, matching nodeHeartbeatSchema), so a
well-behaved agent never emits a payload the controller must truncate. Pure
Rust unit test (cap at 16, normal parse, empty).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Document the disposition of the 2026-07-08 gap-hunt catalogue after convergence:
21 items fixed (each with a test where a unit seam exists), 6 deferred with
rationale (render-harness-gated or product decisions, plus R7-SEED-LIVENESS as
won't-fix). Records the two operator product decisions (offline tone -> Critical;
switcher password-clear -> defer) and the green full-check gate at close.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@yashau
yashau merged commit 686d199 into main Jul 8, 2026
1 check passed
@yashau
yashau deleted the claude/elastic-khorana-78b100 branch July 8, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant