fix: four defects found reviewing the stack - #330
Open
ivanmkc wants to merge 7 commits into
Open
Conversation
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>
This was referenced Sep 2, 2026
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
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.
The stack
localStoragefor the test suite on Node 25@ivanmkc/termchart-canvas; injectable interact transportEvery branch in the stack was checked out on its own and verified independently:
npm install,tsc --noEmitfor every package,npm testacross 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 servehosts lifeboard on 8123.DEFAULT_ORIGINScarries no port andoriginAllowedcompared ports exactly. Probed against the real server: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.tspinnedoriginAllowed("http://127.0.0.1:9999")tofalse, 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
Originitself, 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.restoreawaited the write queue without joining it, so anapplyissued in the same tick chained off the same already-resolved promise and ran alongside the replace. The entry landed, the replace wiped it, andapplyresolved 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
writescould write anywhereEnforcement 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
writesis refused every change it tries to make.Not exploitable today, since the capability implementations only reach
itemandrecipe, and all five bundled packs declare theirwrites. It was a deny-by-default boundary implemented as allow-by-default.4. Two smaller ones
Store.restoretook abyand 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.readBackuponly rejected format numbers above 1, so a file claiming0, 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