Skip to content

fix(m13): Phase 0 hardening — relay crash vectors, smoke orphan, lying tooling - #26

Merged
ksdisch merged 8 commits into
mainfrom
fix/phase0-relay-hardening
Jul 11, 2026
Merged

fix(m13): Phase 0 hardening — relay crash vectors, smoke orphan, lying tooling#26
ksdisch merged 8 commits into
mainfrom
fix/phase0-relay-hardening

Conversation

@ksdisch

@ksdisch ksdisch commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Executes Phase 0 of docs/AUDIT-2026-07-09.md (§"Phase 0 — Stop the bleeding"): every finding it names, nothing beyond.

Findings closed

Finding Fix
F-01 (P0) A 4-byte null frame no longer kills the relay: frame parsing moved into pure parseClientMsg() (server/relay.ts), which rejects anything that isn't an object with a string type. 6 new Vitest cases incl. the killer frame (TDD: red confirmed first).
F-02 (P0) 'error' listeners on connection sockets, wss, and httpServer — bad frames / TCP resets log instead of crashing. Bind failures still exit nonzero so the platform restarts.
F-20 maxPayload: 4096 (ws default is 100 MiB on a 256 MB VM; legit messages < 200 B).
F-38a Room-code exhaustion replies server busy instead of throwing inside the message listener.
F-11 smoke:relay no longer orphans the relay or hangs piped callers: detached spawn + process-group SIGTERM on every exit path (incl. fail()'s process.exit via an 'exit' hook), /healthz polling replaces the racy 1200 ms sleep, and a preflight fails loudly if an orphan already holds 3099.
F-10 npm audit 7 advisories (2 critical, 3 high) → 0. ws 8.20.0 → 8.21.0 et al; one in-range npm update tsx (4.21 → 4.23) moved its pinned nested esbuild past GHSA-g7r4-m6w7-qqqr. Lockfile-only.
F-09 protocol-boundary-guard.sh no longer emits "permissionDecision":"allow" — it only injects the reminder context; the permission prompt is back. Verified output is valid JSON.
F-12 .mcp.json playwright MCP pinned to --browser chromium so /verify-planet works in cloud containers (only local Chrome worked before).
F-58 Inline data-URI favicons (🚀 game, ✨ phone) silence the 404 console noise.

The smoke also gained two hostile-frame wire probes: a raw null frame earns an error reply with the relay alive, and an over-limit frame closes its sender with 1009 while the existing room keeps casting — end-to-end proof of F-01/F-02/F-20 against the real server.

Gate (all verified this session)

  • npm run typecheck clean
  • npx vitest run162/162 (156 + 6 new parseClientMsg cases)
  • npm run build green (on bumped vite/esbuild)
  • npm run smoke:relay | tee /dev/null terminates green — the exact invocation that used to hang forever — and lsof confirms no listener left on 3099
  • git status clean (lockfile committed)

Notes

🤖 Generated with Claude Code

ksdisch and others added 8 commits July 10, 2026 22:13
…, F-38a)

- F-01: a 4-byte `null` frame no longer kills the process. Frame parsing
  moves into pure parseClientMsg() (server/relay.ts) which rejects anything
  that isn't an object with a string `type`; unit-tested incl. the killer
  frame (6 new Vitest cases).
- F-02: 'error' listeners on connection sockets, wss, and httpServer — bad
  frames / TCP resets from a dropping phone log instead of crashing. Boot
  bind failures still exit nonzero.
- F-20: maxPayload 4096 (ws default is 100 MiB; legit messages are <200 B).
- F-38a: room-code exhaustion replies with an error instead of throwing
  inside the message listener.

Per docs/AUDIT-2026-07-09.md Phase 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-11)

Process fix: spawn the relay DETACHED and SIGTERM the whole process group
on every exit path ('exit' hook covers fail()'s process.exit, signals route
through exit). relay.kill() only reached the npx wrapper — the tsx grandchild
survived holding port 3099 and the inherited stdio pipe, so any piped caller
(`npm run smoke:relay | tee …`, i.e. CI) hung forever after a green pass.
Also: poll /healthz (up to 10s) instead of a racy fixed 1200ms sleep, and
fail loudly if something already answers on 3099 (an orphan would otherwise
get smoke-tested instead of the fresh relay).

New wire probes for the Phase 0 hardening: a raw `null` frame earns an error
reply with the relay alive, and an over-maxPayload frame closes the sender
with 1009 while the existing room keeps casting.

Verified: `npm run smoke:relay | tee /dev/null` terminates green, no
listener left on 3099.

Per docs/AUDIT-2026-07-09.md Phase 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm audit fix clears the 7 advisories (2 critical, 3 high) reported in the
audit: ws 8.20.0 → 8.21.0 (GHSA-58qx-3vcg-4xpx memory disclosure,
GHSA-96hv-2xvq-fx4p DoS), plus shell-quote, @babel/core, undici, and vite
in-range bumps. One straggler needed an explicit in-range `npm update tsx`
(4.21.0 → 4.23.0) to move its pinned nested esbuild past
GHSA-g7r4-m6w7-qqqr. Lockfile-only; package.json ranges unchanged; no
majors.

Verified post-bump: typecheck, 162 Vitest, build, and smoke:relay all green.

Per docs/AUDIT-2026-07-09.md Phase 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t guards (F-09)

The hook emitted "permissionDecision":"allow", which SKIPPED the permission
prompt for src/shared/protocol.ts edits — the opposite of its intent. Now it
only injects the additionalContext reminder; the normal permission flow
decides. Verified: hook output is valid JSON carrying additionalContext and
no permissionDecision.

Per docs/AUDIT-2026-07-09.md Phase 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…F-12)

Without --browser, @playwright/mcp wants the `chrome` channel, which cloud
containers don't ship (only Playwright Chromium) — so /verify-planet and the
?test=1 playbook failed in exactly the sessions .mcp.json was vendored for.

Per docs/AUDIT-2026-07-09.md Phase 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🚀 for the game entry, ✨ for the Starglow phone entry. No network request,
no asset file.

Per docs/AUDIT-2026-07-09.md Phase 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parseClientMsg was inserted between relayForward and its doc comment.
Comment move only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <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.

1 participant