fix(m13): Phase 0 hardening — relay crash vectors, smoke orphan, lying tooling - #26
Merged
Conversation
…, 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>
This was referenced Jul 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Executes Phase 0 of docs/AUDIT-2026-07-09.md (§"Phase 0 — Stop the bleeding"): every finding it names, nothing beyond.
Findings closed
nullframe no longer kills the relay: frame parsing moved into pureparseClientMsg()(server/relay.ts), which rejects anything that isn't an object with a stringtype. 6 new Vitest cases incl. the killer frame (TDD: red confirmed first).'error'listeners on connection sockets,wss, andhttpServer— bad frames / TCP resets log instead of crashing. Bind failures still exit nonzero so the platform restarts.maxPayload: 4096(ws default is 100 MiB on a 256 MB VM; legit messages < 200 B).server busyinstead of throwing inside the message listener.smoke:relayno longer orphans the relay or hangs piped callers: detached spawn + process-group SIGTERM on every exit path (incl.fail()'sprocess.exitvia an'exit'hook),/healthzpolling replaces the racy 1200 ms sleep, and a preflight fails loudly if an orphan already holds 3099.npm audit7 advisories (2 critical, 3 high) → 0. ws 8.20.0 → 8.21.0 et al; one in-rangenpm update tsx(4.21 → 4.23) moved its pinned nested esbuild past GHSA-g7r4-m6w7-qqqr. Lockfile-only.protocol-boundary-guard.shno longer emits"permissionDecision":"allow"— it only injects the reminder context; the permission prompt is back. Verified output is valid JSON..mcp.jsonplaywright MCP pinned to--browser chromiumso/verify-planetworks in cloud containers (only local Chrome worked before).The smoke also gained two hostile-frame wire probes: a raw
nullframe 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 typecheckcleannpx vitest run— 162/162 (156 + 6 newparseClientMsgcases)npm run buildgreen (on bumped vite/esbuild)npm run smoke:relay | tee /dev/nullterminates green — the exact invocation that used to hang forever — andlsofconfirms no listener left on 3099git statusclean (lockfile committed)Notes
Hub.ts/testBridge.ts/planets/).🤖 Generated with Claude Code