Skip to content

fix: six confirmed audit findings (token lifecycle, msg-drop, recall scaling) - #12

Merged
saucam merged 1 commit into
mainfrom
fix/audit-confirmed-highs
Jun 14, 2026
Merged

fix: six confirmed audit findings (token lifecycle, msg-drop, recall scaling)#12
saucam merged 1 commit into
mainfrom
fix/audit-confirmed-highs

Conversation

@saucam

@saucam saucam commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes the six confirmed, high-severity findings from a full-codebase audit. Each was adversarially verified against the actual source before fixing (one skeptical verifier per finding, quoting the code). The scarier "stuck-status / race" claims from the first pass were refuted on verification and are deliberately not touched.

Fixes

# Area Problem → Fix
1 session.ts Dropped message after interrupt-then-fast-send. The consumer finally nulled #inputQueue/#consumerTask without the identity guard it already used for #query/#abortController; an un-awaited interrupt() + fast send() let the stale loop clobber the new loop and the next push was silently dropped. → Loop-local queue/task snapshot + identity-guarded nulls.
2 auth.ts · server.ts · types.ts Token never re-verified after handshake → an open socket honored an expired/revoked token forever. → Carry exp into AuthContext, reject missing/expired exp in verifyToken (60s skew), close 4003 on a per-message expiry check.
3 web/lib/ws.ts · connection.ts Web reconnect replayed the dead JWT forever.getToken() supplier re-exchanges the stored zid_sk_ key on every (re)connect; falls back to the last token. +2 tests.
4 memory/store.ts Vector recall had no cache despite the comment — every recall() re-read + re-decoded all embeddings and brute-forced cosine. → Memoize the decoded matrix per workspace; invalidate on insert-with-embedding / setEmbedding.
5 memory/engine.ts Memory init was all-or-nothing — an embedder download hiccup nulled the whole engine (also killing FTS recall + usage persistence). → try/catch embedder.init(), run FTS-only on failure; recall + embed-pump guard on a ready flag.
6 session-manager.ts Unbounded session resume blocked startup (#6). → Newest-first sort, cap to 50, time-box to 20s, log what was left on disk.

The #2 + #3 pair closes the token-lifecycle gap that made the documented "kill a token → immediately lose access" guarantee false on long-lived connections. (Instant revocation of a still-valid token still needs a periodic re-verify — tracked as a follow-up.)

Verification

  • bun run typecheck clean.
  • 554 daemon tests pass; 95 web tests pass (vitest). (bun test globbing the web vitest files reports false fails — those are the known vi.advanceTimersByTimeAsync-under-bun incompatibility, green under cd web && bun run test.)
  • bun run build produces cli.js.
  • Live smoke against the dev daemon: restarted on this code, the new per-message exp check accepted a valid dev token, auth.ok + session.list succeeded, clean boot.

Closes #6.

The remaining medium/low findings (input schema validation, WS payload/backpressure limits, synchronous=NORMAL, shutdown ordering, retention/pruning, aud default, empty-tenancy reject, search-snippet tenant scoping, dead retry.ts, Telegram client pruning) are filed as separate issues.

🤖 Generated with Claude Code

…scaling)

Adversarially-verified high-severity findings from a full-codebase audit.
(The scarier "stuck-status" race claims were refuted on verification and are
not touched here.)

#1 Dropped message after interrupt-then-fast-send (session.ts). The consumer
   `finally` nulled #inputQueue/#consumerTask without the identity guard it
   already used for #query/#abortController, so an un-awaited interrupt() + a
   fast send() let the stale loop clobber the new loop's queue/task and the
   next push was silently dropped. Capture a loop-local queue/task snapshot
   and guard the nulls by identity.

#2 Token never re-verified after handshake (auth.ts/server.ts/types.ts). An
   open socket honored an expired/revoked token forever. Carry `exp` into
   AuthContext, reject missing/expired exp in verifyToken (60s skew), and
   close 4003 on a per-message expiry check. (Instant revocation of a still
   -valid token still needs a periodic re-verify — tracked separately.)

#3 Web reconnect replayed the dead JWT forever (ws.ts/connection.ts). Add a
   getToken() supplier called on every (re)connect open that re-exchanges the
   stored zid_sk_ key for a fresh JWT; fall back to the last token if none.
   +2 tests.

#4 Vector recall had no cache despite the comment (memory/store.ts). Every
   recall re-read + re-decoded all embeddings and brute-forced cosine.
   Memoize the decoded matrix per workspace; invalidate on insert-with
   -embedding / setEmbedding.

#5 Memory init was all-or-nothing (engine.ts). An embedder download hiccup
   nulled the whole engine, also killing FTS recall + usage persistence. Wrap
   embedder.init() in try/catch and run FTS-only (vector signal off) on
   failure; recall and the embed pump guard on the ready flag.

#6 Unbounded session resume blocked startup (session-manager.ts, issue #6).
   Sort newest-first, cap to RESUME_MAX_SESSIONS, time-box to
   RESUME_DEADLINE_MS, and log what was left on disk.

Typecheck clean; 554 daemon tests + 95 web tests pass; build OK; live smoke
(auth+exp accepted, session.list) verified against the dev daemon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

robustness: cap session resume by count + deadline

1 participant