fix(brainstorm-server): address adversarial review findings

From a two-reviewer adversarial pass:

- [High] EADDRINUSE fallback clobbered the shared .last-port: onListen wrote the
  bound port unconditionally, so a fallback to a random port overwrote the
  preferred port another live session still owns — stranding that session's open
  tab forever. Now persist only when we bound the preferred port (not on
  fallback). The fallback test now asserts .last-port integrity (teeth-verified).

- [Medium] maybeOpenBrowser ran the URL through a shell (exec + JSON.stringify),
  which does NOT neutralize $(...) in a url-host. Platform launchers now use
  execFile with the URL as an argv element (no shell). The operator-set
  BRAINSTORM_OPEN_CMD path stays shell-based (trusted input).

- [Medium] --open was a silent no-op on native Windows (no win32 branch). Added.

- [Medium] helper.js reconnect/status/tombstone had only substring-grep tests.
  Added behavioral tests driving the state machine against a mocked browser:
  Reconnecting+backoff (500->1000->2000), tombstone after the grace period, and
  reload-on-recovery.

- [Low] status pill showed a false 'Connected' before the socket opened; now
  starts 'Connecting…' until onopen.

Not changed (flagged): stop-server.sh's PID-ownership check still matches any
'node ... server.cjs' (narrow residual — a recycled PID onto an unrelated node
server.cjs); robust fix needs fragile cross-platform process introspection.
This commit is contained in:
Jesse Vincent
2026-06-09 15:59:59 -07:00
parent 7b815ed8c8
commit f8f87ff43a
5 changed files with 109 additions and 14 deletions

View File

@@ -197,7 +197,7 @@
<body>
<div class="header">
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
<div class="status">Connected</div>
<div class="status">Connecting…</div>
</div>
<div class="main">

View File

@@ -28,6 +28,7 @@
const el = document.querySelector('.status');
if (!el) return;
const map = {
connecting: ['Connecting…', 'var(--text-tertiary)'],
connected: ['Connected', 'var(--success)'],
reconnecting: ['Reconnecting…', 'var(--warning)'],
disconnected: ['Disconnected', 'var(--error)']
@@ -55,7 +56,7 @@
function connect() {
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
setStatus(everConnected ? 'reconnecting' : 'connected');
setStatus(everConnected ? 'reconnecting' : 'connecting');
ws = new WebSocket(WS_URL);
ws.onopen = () => {

View File

@@ -278,14 +278,21 @@ function maybeOpenBrowser() {
if (HOST !== '127.0.0.1' && HOST !== 'localhost') return;
if (clients.size > 0) return; // the user already opened it
const url = 'http://' + URL_HOST + ':' + PORT;
let cmd = process.env.BRAINSTORM_OPEN_CMD;
if (!cmd) {
if (process.platform === 'darwin') cmd = 'open';
else if (/microsoft/i.test(require('os').release())) cmd = 'cmd.exe /c start ""'; // WSL → Windows browser
else if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) cmd = 'xdg-open';
else return; // headless: nothing to open
const cp = require('child_process');
// Operator-provided launcher: run as given (this env var is trusted operator input).
if (process.env.BRAINSTORM_OPEN_CMD) {
try { cp.exec(process.env.BRAINSTORM_OPEN_CMD + ' ' + JSON.stringify(url), () => {}); } catch (e) { /* best effort */ }
return;
}
try { require('child_process').exec(cmd + ' ' + JSON.stringify(url), () => {}); } catch (e) { /* best effort */ }
// Platform launchers: pass the URL as an argv element via execFile (no shell),
// so a url-host containing shell metacharacters can't inject a command.
const isWSL = process.platform === 'linux' && /microsoft/i.test(require('os').release());
let bin, args;
if (process.platform === 'darwin') { bin = 'open'; args = [url]; }
else if (process.platform === 'win32' || isWSL) { bin = 'cmd.exe'; args = ['/c', 'start', '', url]; }
else if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) { bin = 'xdg-open'; args = [url]; }
else return; // headless: nothing to open
try { cp.execFile(bin, args, () => {}); } catch (e) { /* best effort */ }
}
// ========== Activity Tracking ==========
@@ -397,9 +404,16 @@ function startServer() {
}
}
// If the preferred port is already taken (e.g. a previous server is still
// alive), fall back to a random port once instead of failing.
let triedFallback = false;
function onListen() {
// Record the bound port so the next restart of this session can reuse it.
if (PORT_FILE) {
// Record the bound port so the next restart of this session reuses it — but
// ONLY when we got our preferred port. On a fallback we bound a *different*
// port because someone else holds the preferred one; persisting it would
// overwrite the shared .last-port and strand that other session's open tab.
if (PORT_FILE && !triedFallback) {
try { fs.writeFileSync(PORT_FILE, String(PORT)); } catch (e) { /* best effort */ }
}
const info = JSON.stringify({
@@ -411,9 +425,6 @@ function startServer() {
fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
}
// If the preferred port is already taken (e.g. a previous server is still
// alive), fall back to a random port once instead of failing.
let triedFallback = false;
server.on('error', (err) => {
if (err.code === 'EADDRINUSE' && !triedFallback) {
triedFallback = true;