Skip to content

fix(brainstorm-server): guard handleMessage against null event payload#1504

Open
ambicuity wants to merge 1 commit into
obra:mainfrom
ambicuity:fix/handleMessage-null-guard
Open

fix(brainstorm-server): guard handleMessage against null event payload#1504
ambicuity wants to merge 1 commit into
obra:mainfrom
ambicuity:fix/handleMessage-null-guard

Conversation

@ambicuity
Copy link
Copy Markdown

What problem are you trying to solve?

The brainstorm WebSocket server in `skills/brainstorming/scripts/server.cjs` crashes — process exits with code 1 — when a client sends the 4-byte JSON message `null`. `JSON.parse('null')` returns `null`, and `null.choice` (line 234) throws `TypeError: Cannot read properties of null (reading 'choice')`. The throw escapes the `socket.on('data')` handler as an uncaughtException, terminating the server.

Any local WebSocket client can trigger this — a browser tab loading a malicious page, any local process, or simply a misbehaving extension that sends an unexpected payload. The bind is `127.0.0.1`, so the threat is local-only, but it's a real availability bug for anyone who has the brainstorm companion running.

Live repro on `main` (commit f2cbfbe, v5.1.0) — start the server and send `null` over a fresh WebSocket:

```
{"type":"server-started","port":3399,"host":"127.0.0.1",...}
{"source":"user-event"}
/private/tmp/.../skills/brainstorming/scripts/server.cjs:234
if (event.choice) {
^

TypeError: Cannot read properties of null (reading 'choice')
at handleMessage (.../server.cjs:234:13)
at Socket. (.../server.cjs:198:11)
at Socket.emit (node:events:509:20)
...
*** SERVER EXITED (code=1) ***
```

What does this PR change?

Adds a truthiness check (`event &&`) before the `event.choice` access in `handleMessage()`. Adds three regression tests in `tests/brainstorm-server/server.test.js` covering: JSON `null` does not crash the server, JSON primitives (number/string/bool) do not crash the server, and JSON `null` does not produce a stray `state/events` write.

Is this change appropriate for the core library?

Yes. `server.cjs` is the WebSocket server backing the `brainstorming` skill — core infrastructure used by every Superpowers user who uses the brainstorming visual companion. The fix is one operator (`&&`) plus a comment explaining why; no new dependency, no behavioral change to legitimate object payloads.

What alternatives did you consider?

  1. `if (event && typeof event === 'object' && event.choice)` — broader guard rejecting primitives outright. Discarded as over-engineering: the only JSON values that throw on property access are `null` and `undefined`. `undefined` cannot be produced by `JSON.parse`. Primitives yield `undefined` for unknown properties (no throw) and are correctly ignored by the existing falsy check. The minimal `event && event.choice` precisely covers the crash without changing semantics for any valid JSON value.

  2. Wrap `handleMessage` in a top-level `try/catch` — would also prevent the crash, but it would mask other latent bugs by silently swallowing errors. The narrow guard at the actual throw site is cleaner and the test fixture (`uncaughtException` would propagate any other bug) keeps future regressions visible.

  3. Add a `process.on('uncaughtException')` handler — addresses the symptom (process exit) rather than the cause (unsafe property access on `null`). Discarded for the same reason as 2.

Does this PR contain multiple unrelated changes?

No. One bug, one fix, plus regression tests for that fix. Both files modified are the bug site and its test suite.

Existing PRs

Environment tested

Harness (e.g. Claude Code, Cursor) Harness version Model Model version/ID
Claude Code (VS Code extension) 2.x (May 2026) Claude Opus claude-opus-4-7 (1M context)

OS: macOS 25.3.0 (Darwin). Node: v25.9.0. Tests run as `node tests/brainstorm-server/server.test.js` and `node tests/brainstorm-server/ws-protocol.test.js` — both standalone, no harness needed for the test itself.

New harness support (required if this PR adds a new harness)

N/A — this PR does not add a new harness. The change is internal to an existing skill's runtime script.

Evaluation

  • Initial trigger: A code review of `server.cjs` traced the `socket.on('data')` handler call chain and identified that `handleMessage` had no guard before `event.choice`. Wrote a minimal Node script that opens a WebSocket and sends `null`. Confirmed the server crashed with the stack trace shown above on a fresh `main` checkout (commit `f2cbfbe`, v5.1.0).

  • After the fix: ran `node tests/brainstorm-server/server.test.js` — 28 passed, 0 failed (3 new tests added: 25→28). Ran `node tests/brainstorm-server/ws-protocol.test.js` — 31 passed, 0 failed (no change, sanity check that frame encode/decode wasn't affected). Re-ran the live repro script: server stays alive, the WebSocket connection survives the `null` payload, subsequent HTTP `GET /` returns 200.

  • Negative control: before opening this PR, I temporarily reverted the one-line fix (`git stash` of `server.cjs`) and re-ran the test suite. The new `handles JSON null from client without crashing` test fails — the server crashes during the test, the test runner reports `FAIL`, and remaining tests cannot run. This proves the new test actually catches the bug it claims to catch (i.e., the test isn't tautological).

Rigor

  • If this is a skills change: I used `superpowers:writing-skills` and completed adversarial pressure testing — N/A, this is a runtime-script bugfix, not a skills change.
  • This change was tested adversarially, not just on the happy path — three regression tests cover `null`, primitives (`42`, `"hello"`, `true`), and the happy path is preserved by the existing `writes choice events to state/events` test. The negative control above proves the bug-detection test is real.
  • I did not modify carefully-tuned content (Red Flags table, rationalizations, "human partner" language) — no `.md` skill files touched.

Human review

  • A human has reviewed the COMPLETE proposed diff before submission

The diff is two files (4-line code change in `server.cjs` including a 2-line WHY comment, and 47 lines of regression tests in `server.test.js` mirroring the existing `handles malformed JSON from client gracefully` test pattern at line 284). The diff was reviewed in its entirety before push, including the captured live-repro stack trace and the negative-control test.

JSON.parse('null') yields null. Reading .choice on null throws a
TypeError that escapes the socket 'data' handler as an
uncaughtException, terminating the brainstorm server process. Any
local WebSocket client (browser tab, Node REPL, etc.) on
127.0.0.1:<port> can trigger this with the 4-byte message `null`.

Other JSON values are unaffected: primitives have a property access
of undefined (no throw), and objects/arrays already reach the
intended `if (event.choice)` semantics.

Adds a truthiness guard before the property access and three
regression tests covering JSON null, JSON primitives, and the
absence of an events-file write for null payloads.
Copilot AI review requested due to automatic review settings May 8, 2026 23:15
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Guards the brainstorm WebSocket server against JSON.parse('null') payloads that previously crashed the process, and adds regression tests to prevent reintroducing the issue.

Changes:

  • Add a null-check before accessing event.choice in handleMessage()
  • Add regression tests ensuring null and primitive JSON payloads don’t crash the server
  • Add a regression test ensuring null payloads don’t create a stray state/events file

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
skills/brainstorming/scripts/server.cjs Adds a guard before event.choice access to prevent null payload crashes
tests/brainstorm-server/server.test.js Adds regression tests covering null, primitives, and preventing unintended events writes

@@ -231,7 +231,9 @@ function handleMessage(text) {
}
touchActivity();
console.log(JSON.stringify({ source: 'user-event', ...event }));
Comment on lines +304 to +309
ws.send('null');
await sleep(300);

const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert.strictEqual(res.status, 200, 'Server should still be running after JSON null');
ws.close();
ktenman added a commit to ktenman/superpowers that referenced this pull request May 23, 2026
…#1596, obra#1562)

Cherry-picks three verified fixes from obra/superpowers open PRs into the fork.

- obra#1504: guard handleMessage against null/primitive WebSocket payloads.
  JSON.parse('null') returns null; reading .choice on it threw a TypeError
  that escaped the socket 'data' handler and killed the server process.

- obra#1596: make the brainstorm server idle timeout configurable via
  --idle-timeout-minutes (BRAINSTORM_IDLE_TIMEOUT_MS) and raise the default
  from 30 minutes to 2 hours. Surface idle_timeout_ms in server-info.

- obra#1562: detect the package manager by lockfile in using-git-worktrees Step 3
  (pnpm/yarn/bun/npm for Node, uv/poetry/pip for Python) instead of running
  npm install / poetry install unconditionally.

Verified: brainstorm-server suite 32/32 green (includes new null-crash and
idle-timeout regression tests); --idle-timeout-minutes checked end-to-end
(5 -> 300000ms, default -> 7200000ms, 0/abc -> error + exit 1).
@obra obra added bug Something isn't working brainstorming Brainstorming skill and visual companion needs-rebase-to-dev-branch PR targets main but should target dev labels May 23, 2026
@YOMXXX
Copy link
Copy Markdown

YOMXXX commented May 24, 2026

Review note after reading the PR body and diff:

This looks like a narrow runtime bug fix with real regression coverage. The failure mode is concrete (JSON.parse('null') returns null, then event.choice throws), and the test is not tautological because the PR body describes a negative-control run where the new test fails when the one-line guard is reverted.

I did not see a code-level blocker in the patch. The event && event.choice guard is minimal and preserves current behavior for normal object payloads. The added tests also cover JSON primitives and ensure null does not create state/events.

Process items for reviewers:

  • This targets main; current repo flow appears to expect dev.
  • After rebasing to dev, the useful verification is node tests/brainstorm-server/server.test.js plus node tests/brainstorm-server/ws-protocol.test.js because adjacent brainstorm-server PRs have touched the same files.

This seems substantially easier to evaluate than the larger skill-prose PRs because it changes only runtime code and focused tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

brainstorming Brainstorming skill and visual companion bug Something isn't working needs-rebase-to-dev-branch PR targets main but should target dev

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants