Skip to content

fix: four defects found reviewing the stack - #330

Open
ivanmkc wants to merge 7 commits into
lifeboard/18-activityfrom
lifeboard/19-review-fixes
Open

fix: four defects found reviewing the stack#330
ivanmkc wants to merge 7 commits into
lifeboard/18-activityfrom
lifeboard/19-review-fixes

Conversation

@ivanmkc

@ivanmkc ivanmkc commented Sep 2, 2026

Copy link
Copy Markdown
Owner

The stack

# PR What it adds Tests at this point
1 #311 A real localStorage for the test suite on Node 25 viewer 747
2 #312 @ivanmkc/termchart-canvas; injectable interact transport canvas 473 · viewer 278
3 #313 lifeboard PWA: fact log, boards, service worker lifeboard 36
4 #314 Shell, family profiles, tap layer lifeboard 82
5 #315 The agent turn, four providers, Connections lifeboard 142
6 #316 Packs, recipes, shopping-list arithmetic lifeboard 231
7 #317 Provenance and lineage lifeboard 257
8 #318 Calendar, Gmail, mail triage lifeboard 301
9 #319 The bridge and long jobs cli 314 · lifeboard 320
10 #310 Proactivity, scheduled boards, Google sign-in cli 320 · lifeboard 343
11 #320 Persistence audit fixes: multi-tab, stale builds, no-storage boot lifeboard 350
12 #321 User guide, bridge setup, QA guide, persistence audit lifeboard 350
13 #322 A plan for third-party integrations (docs only) unchanged
14 #323 A practice drill a board can define: audio + Q&A canvas 485 · lifeboard 372
15 #325 Export and restore lifeboard 389
16 #326 Draft a message, approve it, then send lifeboard 399
17 #327 Hand long work to the Mac lifeboard 406
18 #328 A Packs screen, and authoring one by describing it lifeboard 418
19 #329 Activity — what changed, who changed it, undo lifeboard 428
20 #330 Four defects found reviewing the stack cli 323 · lifeboard 431

Every branch in the stack was checked out on its own and verified independently: npm install, tsc --noEmit for every package, npm test across the workspace, and the offline e2e where it exists. All twenty are green — no branch depends on a later one to build or pass. Full viewer e2e (11 suites, 106 assertions) was run on #312 as the behaviour-preservation evidence for the extraction.


Stack position: 20 of 20 — base lifeboard/18-activity (#329).

A self-review pass over the nineteen PRs in this stack, aimed at the seams where a bug is expensive and where the tests were most likely to be confirming themselves. Four real defects, each reproduced against running code before being fixed, each with a regression test verified to fail without the fix.

1. The bridge refused the app it ships with

npm run serve hosts lifeboard on 8123. DEFAULT_ORIGINS carries no port and originAllowed compared ports exactly. Probed against the real server:

GET  /health from http://localhost:8123 → 200
POST /jobs   from http://localhost:8123 → 403 "origin ... is not paired with this bridge"

Health is a GET answered before auth, so bridge discovery and the e2e's bridge probe both looked healthy while every write was refused. Jobs and notify — the payload of #319 and #327 — were dead on the documented setup path.

A test asserted the bug. bridge.test.ts pinned originAllowed("http://127.0.0.1:9999") to false, and every server test sent a port-less origin that no browser ever sends. That is why 320 green CLI tests said nothing.

Loopback hosts are now trusted on any port. The port was never a boundary: a browser sets Origin itself, so a page on the open internet cannot claim to be loopback, and the hostname check is what stops DNS rebinding. A host paired explicitly stays pinned to the port it was paired on, and the scheme still has to match.

2. A restore could swallow a write that was in flight

Store.restore awaited the write queue without joining it, so an apply issued in the same tick chained off the same already-resolved promise and ran alongside the replace. The entry landed, the replace wiped it, and apply resolved as though it had saved — S1's silent write loss, on the one path where someone is already recovering data.

Reachable without doing anything unusual: transcript lines and the Google catch-up sync both write on their own.

Worth being precise about the blast radius, because my first regression test passed against the buggy code: IndexedDB masks this, serialising overlapping readwrite transactions so the append lands after the replace commits. The guarantee came from the backend, not from the Store. The memory backend — what a private window or a device with storage disabled actually runs on — loses the write outright. The test is pinned to that backend, where it fails without the fix.

3. A pack that declared no writes could write anywhere

Enforcement read a missing key as "allow everything" — the inversion of the premise that a pack is data — and the authoring prompt told the model it only may declare one, so the packs most likely to omit it were the model-authored ones. Deny by default now; the prompt says a pack with no writes is refused every change it tries to make.

Not exploitable today, since the capability implementations only reach item and recipe, and all five bundled packs declare their writes. It was a deny-by-default boundary implemented as allow-by-default.

4. Two smaller ones

Store.restore took a by and dropped it on the floor, promising an attribution the Activity screen could never show — the parameter is gone. Genuinely recording who restored needs a place in the log to put it, which is a feature, not a fix.

readBackup only rejected format numbers above 1, so a file claiming 0, a negative, or a non-integer was read as current.

What this says about the tests

All four sit behind comments asserting the guarantee they break. The unit tests exercise each module through its own vocabulary — port-less origins, sequential awaits, packs that happen to declare writes — rather than through the shapes the app actually produces. The bridge defect is only visible from a real browser origin against a real socket.

Verification

canvas 485 · cli 323 · core 21 · lifeboard 431 · viewer 278, all green, clean typecheck across every package, and the offline e2e passes (19 assertions). Both new regression tests were confirmed to fail against the pre-fix code.

Gaps from the earlier audit remain tracked in #324; these four are separate and are fixed here.

🤖 Generated with Claude Code

A review pass over the nineteen PRs, aimed at the seams where a bug is
expensive and where the tests were most likely to be confirming themselves.
Each finding was reproduced against running code before it was fixed.

The bridge refused the app it ships with. DEFAULT_ORIGINS carries no port
and originAllowed compared ports exactly, so a page served by `npm run
serve` on 8123 got 403 on every write — jobs and notify, the whole point
of the bridge, dead on the documented setup path. Health is a GET answered
before auth, so discovery and the e2e's bridge probe both looked fine.

Worse, a test asserted the broken behaviour: bridge.test.ts pinned
`http://127.0.0.1:9999` to false, and every server test sent a port-less
origin no browser will ever send. Loopback hosts are now trusted on any
port. The port was never a boundary — a browser sets Origin itself, so a
remote page cannot claim to be loopback, and the hostname check is what
stops DNS rebinding. An explicitly paired host stays pinned to its port.

A restore could swallow a write that was in flight. Store.restore awaited
the write queue without joining it, so an apply issued in the same tick
chained off the same resolved promise and ran alongside the replace: the
entry landed, the replace wiped it, and apply resolved as though it had
saved. IndexedDB masks this by serialising overlapping readwrite
transactions, so the guarantee came from the backend rather than from the
Store — and the memory backend, which is what a private window actually
runs on, loses the write outright. The regression test is pinned to that
backend, where it fails without the fix.

A pack that declared no writes could write anywhere. Enforcement read a
missing key as "allow everything", which is the inversion of the whole
premise that a pack is data, and the authoring prompt told the model it
only *may* declare one. Deny by default; the prompt now says a pack with
no writes is refused every change it tries to make. All five bundled packs
already declare theirs.

Also: restore took a `by` and dropped it on the floor, promising an
attribution Activity could never show — the parameter is gone, and
recording who restored needs a place in the log to put it. And readBackup
only rejected format numbers above 1, so a file claiming 0, a negative or
a non-integer was read as current.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ivanmkc and others added 6 commits September 4, 2026 17:23
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refactor: rename lifeboard package to termchart-client
Sweeps every remaining "lifeboard" reference from active repo content
(docs, UI copy, comments, prompts, tests) now that the package itself
has been renamed to @ivanmkc/termchart-client. docs/lifeboard/ moves to
docs/client/ (history-preserving git mv), with all inbound links fixed.

Persisted identifiers are renamed with NO migration, per a pre-launch
controller ruling (no real device data exists yet to lose): the
IndexedDB store name, every "lifeboard.*" localStorage/prefs key
(profiles, bridge url/token, Google OAuth token/verifier, connections,
sync bookkeeping), the service worker cache name, and a transient
prefs probe key. The one exception is the backup file format tag:
export now writes "termchart-backup" / termchart-<date>.json, but
readBackup() also accepts the legacy "lifeboard-backup" tag so backups
already exported by users still restore — the only legacy shim in this
sweep.

docs/superpowers/** and .superpowers/** are left untouched (historical
plan/spec records), so `docs/client/README.md`'s links to those files
and one legacy-acceptance line + test in backup.ts/backup.test.ts are
the only "lifeboard" strings left in scope after this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore: remove lifeboard branding — the client is Termchart
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