Ten methods the UI calls had no case at all in mock-backend.js, so the demo
answered them with "Method not found" and the frontend swallowed it in a
try/catch — peer renaming, scheduling, clear-all, the assistant panel and two
federation actions were all silently inert on the demo.
Each new handler mirrors its daemon counterpart and cites the Rust source it
mirrors, per the house convention above mesh.transport-advice:
mesh.contacts-list/-save typed_messages.rs (contacts merged over peers by
pubkey_hex; absent params leave stored fields)
mesh.clear-all status.rs ({ status: "cleared" })
mesh.schedule/list/cancel assistant.rs + scheduler.rs ScheduledMessage
mesh.assistant-status/-configure assistant.rs (key-presence semantics)
federation.cancel-request handlers.rs (outbound+sent only, notify defaults
true) — plus an outbound seed request, since
without one the demo's cancel path was
unexercisable
federation.notify-did-change handlers.rs ({ notified, failed, results })
mesh.peers and mesh.contacts-list now share one DEMO_MESH_PEERS list so they
cannot disagree about who is on the mesh, and the peer with no pubkey_hex is
omitted from contacts exactly as the daemon omits it.
scripts/mock-rpc-parity.mjs cross-references UI call sites against mock cases
and then drives a live scripted RPC sequence against an ephemeral-port
instance (MOCK_BACKEND_PORT). It matches only `method: '<x>'`, NOT bare string
literals: Mesh.vue and Federation.vue use the same dotted names as
resource-cache keys, and matching those would report permanent phantom gaps.
Fail-first proof: disabling the mesh.clear-all case makes the harness exit 1
naming the gap; restored, it exits 0 twice in a row with no stray listener.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both paid-tick surfaces now show the EQ-segment ring instead of a CSS ripple
burst (send modal) and a plain circle (scan modal), via a new `badge` size
variant at 160px/192px with matching --viz-radius. A transform scale of the
compact variant was ruled out in 01-UI-SPEC.md because it would scale segment
stroke width and blur along with the geometry.
Both surfaces use the identical composition — a badge-sized relative container
with the checkmark core absolutely centred over the ring — so the two ticks
cannot drift apart visually.
ScreensaverRing also gains the prefers-reduced-motion guard it never had, on
the component rather than per call site, so the screensaver and
SystemDangerZone variants are covered too.
Also records live-browser verification for 01-13's scroll cue (three
viewports, plus a geometry probe of the hide condition).
Verified: 5 new tests; full suite 102 files / 822 tests green; npm run build
clean with viz-ring-badge present in the built bundle. The live visual
no-clipping observation Task 2 asks for is explicitly NOT done — recorded as
deferred to plan 01-07's consolidated sign-off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bip39::Mnemonic::generate(24) resolves through Mnemonic::generate_in to
&mut rand::thread_rng() INSIDE the bip39 crate (bip39-2.1.0/src/lib.rs:
311-313 -> :296-298 -> :267-283), so the entropy source behind Archipelago's
entire key hierarchy -- node Ed25519 did:key, node Nostr key, FIPS mesh key,
per-identity keys, the BIP-84 wallet, the LND aezeed entropy, and the fleet
release-root SIGNING key -- was chosen by a dependency default rather than
stated at the call site.
Not a vulnerability today: rand 0.8.5's thread_rng is a fork-protected
ChaCha12 CSPRNG seeded from getrandom(2). But it is precisely the structural
shape of the 2026-07-30 COLDCARD entropy defect (T1), where a refactor
silently rebound seed generation to a non-cryptographic PRNG with no compile
error and no test failure.
- New private helper generate_mnemonic_with<R: CryptoRng + RngCore> calls
bip39's injectable generate_in_with; MasterSeed::generate passes OsRng
explicitly, with the rationale pinned in a doc comment
- mnemonic_generation_uses_injected_rng: drives generation from a
deterministic test RNG and asserts the result equals from_entropy(exactly
the bytes that RNG emitted) -- direct proof the INJECTED rng is consumed --
plus a known-answer pin and a determinism check. This test cannot be
written against the previous code: there was no seam to inject through
- mnemonic_generation_is_256_bit: the OsRng path yields 24 words and two
successive productions differ
No change to derivation paths, word count, the empty-BIP-39-passphrase
decision, or the at-rest encryption envelope.
Verified: CARGO_INCREMENTAL=0 cargo test -p archipelago seed:: -> 25 passed,
0 failed.
Full analysis: docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md (F-02, §4, §7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Adds F-13 (High) to the audit: the BIP-84 account PRIVATE key is imported
into Bitcoin Core (bitcoin.rs:203 disable_private_keys=false, :229-231
wpkh(xprv/...)), so the spending key is persisted outside the Argon2
envelope in a wallet with an empty passphrase; the descriptors also carry
no [fingerprint/derivation] key origin, so no hardware signer could ever
use them. Found by tracing secret class (1) end-to-end
- Fills the Remediation Backlog: R-00..R-15, prioritised severity x effort,
each with the finding it closes, files, effort, and hardware gating; plus
an explicit "not implemented here, and why" section
- Records ARCHY-1 as APPLIED with the exact test evidence and an honest note
that making the source explicit removes a future failure mode rather than
repairing a past one
- Wires the resulting open items into docs/UNIFIED-TASK-TRACKER.md in its
existing tier/checkbox format: Tier 0 (cargo audit/deny CI, ceremony
mnemonic input, a five-item hygiene batch, secrets.rs OsRng), Tier 1 (ISO
fail-open first-boot secrets, Argon2 vs ADR-005, the on-node checklist),
Tier 2 (the Critical unauthenticated seed RPCs, PSBT Phase 1, PSBT phases
2-7, seed-RPC transport confinement)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a short viewport the seed-confirmation tickbox sits below the fold inside
the step's scrolling area while Continue stays pinned and disabled in the
fixed footer — onboarding reads as broken rather than incomplete.
The cue is a sticky-bottom scrim and glass pill inside the scroll region, and
its visibility comes from real geometry: scrollHeight vs clientHeight for
overflow, then a getBoundingClientRect comparison of the tickbox's bottom
against the container's. On a tall screen the element does not render at all,
so those screens are unchanged. Rects rather than offsetTop because offsetTop
is relative to the nearest positioned ancestor — here the outer card, not the
scroll container.
It is wayfinding only: activating it scrolls the tickbox into view and never
sets confirmed, focuses Continue, or auto-ticks, which a test pins.
Listener setup was moved onto both onMounted paths — the sessionStorage
restore path returned early, so a user navigating back would have had no cue.
Verified: 6 new tests plus the full frontend suite green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entering picture-in-picture read as the lightbox being dismissed, and the
session died with it: both Teleport and KeepAlive move their subtree on
deactivation, which the PiP spec treats as removal.
MediaLightbox now listens for the video's own enterpictureinpicture event —
so PiP entered by the browser's native control behaves identically to the
toolbar button — and follows a fixed order: adopt, animate, then emit close.
Adopting first is what makes the element survive the unmount the emit
triggers; the invariant is documented in place so a refactor cannot reorder
it innocently.
The backdrop animates a handoff on the PiP path only, closing on transitionend
with a bounded 350ms fallback for browsers that skip the transition and for
the reduced-motion path where the duration is zero and the event never fires.
Verified: 5 new tests plus the full frontend suite green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six code paths configured the Lightning gateway with a bcrypt hash committed
to this repository — and one deploy path with a plaintext password literal —
whenever the per-install secret was missing. Anyone holding a copy of the repo
held the admin credential for every gateway that ever took a fallback.
container::secrets now owns the credential end to end: ensure_gateway_credential
(idempotent, delegates to ensure_one's bcrypt arm) and gateway_bcrypt_hash,
which returns Err when the secret is missing/empty and when the stored value is
on the KNOWN_DEFAULT_GATEWAY_HASHES denylist — so this codebase cannot hand
back the compromised value even to a node already carrying it.
get_app_config was widened to Result so a credential-less install cannot reach
podman run at all; configure_fedimint_lnd takes the resolved hash instead of
re-reading with its own fallback. The four shell paths stop generating
credentials entirely (dropping the htpasswd host dependency) and skip container
creation with a printed reason rather than substituting anything.
Naming converges on the manifest's fedimint-gateway-hash/.pw, with legacy
fedimint-gateway-password values copied forward rather than regenerated so no
node loses a working unique credential. Plan 01-16 owns rotation of installs
already carrying the default.
Verified: cargo build clean; cargo test -p archipelago 999 passed (2
boot_reconciler timing tests failed under concurrent load, green in isolation,
untouched by this diff); bash -n clean on all five scripts; the compromised
literal now appears exactly once in the tree, as the denylist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Watch-only descriptor wallets, external signers, multisig, air-gap transport
and honest LND limits — a spec a future /gsd-plan-phase can consume.
- Names the current gap: bitcoin.rs:203 passes disable_private_keys=false and
bitcoin.rs:229-231 imports wpkh(xprv/...), so Core holds the BIP-84 account
PRIVATE key today. Closing that is Phase 1 and unblocks everything else
- Full Core RPC loop with wallet- vs node-scoped RPCs; analyzepsbt drives UI
- Tier 1 single-sig with mandatory [fingerprint/derivation] key origin; Tier 2
wsh(sortedmulti) on BIP-48; taproot/MuSig2 deferred as UNVERIFIED
- BC-UR v2 primary (fountain-coded, degrades gracefully), BBQr for Coldcard,
file fallback always; animated multi-frame is mandatory, not optional
- LND: channel/revocation/HTLC keys CANNOT be air-gapped; funding tx must
NEVER be self-broadcast (type-level refusal, not a boolean)
- On-chain (PSBT-protectable) vs lightning (necessarily hot) split, with the
exact user-facing sentence the UI must use
- Hot wallet kept as explicitly-secondary with server-enforced limits; safe
path is the DEFAULT, per T1's survivors
- Migration section refuses to over-alarm: the audit found no entropy defect,
so no Archipelago user needs to rotate a seed
- 7 phases with dependencies, candidate requirements, and hardware gating
- Answers two open items in docs/hardware-signer-design.md
- Flags bitcoin-knots:latest as an unpinned tag vs ADR-009
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Evidence-backed audit of every secret class against the real tree, prompted
by the 2026-07-30 Coinkite COLDCARD entropy incident.
- No Coldcard-class entropy defect exists: no non-cryptographic PRNG, no
clock-seeded key, no Math.random() in any browser key path
- F-01 (Critical, NOT entropy): seed.generate/seed.restore are unauthenticated,
unrated, and unconditionally overwrite a live node's Ed25519/Nostr/FIPS keys
- F-02 [ARCHY-1] CONFIRMED: mnemonic entropy source is a bip39 transitive
default, not a call-site argument — the exact structural shape of T1
- F-03 (High) [ARCHY-3]: first-boot TLS/SSH regeneration is fail-open and its
completion marker is set even on failure, over a fleet-shared cached rootfs
- ARCHY-2 confirmed good; ARCHY-5 refuted as a present defect (32 | 256)
- Argon2::default() is 19MiB/t=2, not ADR-005's stated 64MB/3
- Corrects the scoping assumption that image-recipe/_archived/ is dead: it is
the live ISO builder, exec'd by build-debian-iso.sh
- Adds a "What we do right" section and an UNVERIFIED on-node checklist
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
npm run build's vue-tsc -b pass caught TS2532 (possibly-undefined array
access) on two array-index reads the vitest run alone doesn't type-check —
optional-chain them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Following the confirmed 2026-07-30 Coinkite COLDCARD low-entropy incident,
plan three deliverables as a single 3-task quick plan:
1. docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md — evidence-backed audit of
every key-material path (Rust/TS/shell), adjudicating research findings
[ARCHY-1]..[ARCHY-4] with file:line evidence, incl. the one-ISO-many-nodes
correlation risk and an explicit UNVERIFIED on-node checklist.
2. docs/security/PSBT-SIGNING-ARCHITECTURE.md — descriptor watch-only,
wsh(sortedmulti) multisig, air-gap transport, honest LND limits
(channel/revocation/HTLC keys cannot be air-gapped), hot wallet as
explicitly secondary, migration path, phased rollout.
3. Remediation backlog into docs/UNIFIED-TASK-TRACKER.md + one gated
hardening fix (explicit-OsRng injection at the mnemonic call site) proven
by a known-answer test that cannot exist before the change.
Audit-and-spec only — no wallet/signing implementation. Verify gates enforce
file:line evidence density and a secret-shaped-string check on both docs, and
assert no commit authored by this plan touches the concurrent agent's files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dorian verified the sibling-height match, internal scroll, and unchanged
stacked layout on his running dev session — all correct. The only issue was
the xl:min-h-[20rem] floor (an unmeasured judgement call, flagged as such in
01-12-PLAN.md): when node discovery is disabled, Web5NodeVisibility renders
short, the floor takes over, and 20rem left the Connected Nodes card looking
stunted. Doubled to xl:min-h-[40rem] per his direct instruction ("twice as
tall"). Test updated to pin the new value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 2 of 01-01-PLAN.md, closing the gap left after Task 1's initial commit
(2f99db5e):
- record_peer_transport and update_node now hold FEDERATION_STORE_LOCK for
their whole load-mutate-save cycle via the *_inner variants, instead of
calling the public (separately-locked) load_nodes/save_nodes — closing
the same class of race the lock was introduced to fix, just for the two
mutators Task 1 didn't reach.
- Add test_remove_errors_when_tombstone_write_fails: pre-creates the
removed-nodes path as a directory so the tombstone write fails, then
asserts remove_node returns Err AND load_nodes still contains the node —
proving a failed removal never half-applies.
cargo test -p archipelago federation::storage: 14/14 green (was 11, +3 across
Task 1/2). cargo build -p archipelago: no new warnings, no dead-code warnings
on any *_inner fn. Public signatures unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cloud.vue's viewPaidItem() called window.open() instead of the in-app
MediaLightbox, and its content.owned-get fetch had a 60s timeout with no
loading indicator and a swallowed catch. Moves the fetch/decode/route logic
into a new usePaidItemViewer composable: image/video route to a second
MediaLightbox instance fed a synthetic FileBrowserItem, audio still goes to
the global bottom-bar player, and anything with no in-app viewer keeps
today's browser-tab fallback. The Paid Files row now shows an "Opening…"
spinner (matching PeerFiles' existing treatment) for the fetch's duration,
becomes non-interactive to prevent double-fetch, and a real error surfaces
through the view's existing alert-error block instead of an empty catch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
usePipSession() owns an off-screen div under document.body; adopt(video)
moves the element there so it survives the unmount of whatever view
rendered it (a Teleport and a KeepAlive'd view both move their subtree on
deactivation, which the picture-in-picture spec treats as removal).
release() tears down playback and detaches the element; a
leavepictureinpicture listener on the adopted element is the primary
release path so an orphaned owner can never leak it.
Also adds isPipSupported() to pip.ts — a call-time version of the existing
import-time pipSupported const, needed because a test can't restub
document.pictureInPictureEnabled after import. togglePip and pipSupported
are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three tab panes carried max-h-72 xl:max-h-none, so at the xl breakpoint
(where the Web5 row becomes two grid columns) the cap lifted with nothing to
replace it: the visible pane grew to fit every row, stretched the grid row,
and the scrollbar the user expects never appeared.
Give each pane xl:flex-1 xl:basis-0 xl:max-h-none instead — zero flex-basis
means the pane contributes no intrinsic height, so the grid row is sized by
the Web5NodeVisibility sibling alone, grid's default align-items: stretch
gives the card that height, and flex-1 hands the leftover height back to the
pane, which scrolls inside it via the existing overflow-y-auto. The card root
gets min-h-0 (so the flex column can shrink below content height) plus an
xl:min-h-[20rem] floor so a short sibling still leaves a usable list area
instead of collapsing to the header+tabs strip.
Below the row breakpoint nothing changes: the stacked cap (max-h-72) and
scroll are untouched, and dropping flex-auto (replaced by nothing, i.e. the
default 0 1 auto) has no visible effect since single-column stacked cards
have no extra flex space to distribute anyway.
Adds Web5ConnectedNodesScroll.test.ts to pin the contract across all three
tab panes and the card root so a future cleanup cannot reintroduce the
grow-to-fit regression a third time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Release and OTA assets are live; the installer ISO was blocked three times by
a dirty shared tree. Records the exact command, TMPDIR requirement, the
background-execution lesson, the build-from-HEAD decision and its reasoning,
and which gate stages were already observed passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apps/botfights/manifest.yml went to 1.2.11 in aea17248, but the two unsigned
catalogs (app-catalog/catalog.json and its neode-ui/public copy) still
advertised 1.2.9, failing the release gate's catalog-drift check and blocking
the ISO build. releases/app-catalog.json was already correct and signed.
Regenerated via scripts/generate-app-catalog.py (syncs from manifests, no key
needed). app_ports.rs was rewritten by the same generator; verified the port
set is byte-identical in content (35 ports, none added or removed) and
re-normalised with cargo fmt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ships botfight repo commits f5f57e6, c162d5e, 41f1b93, 10d4209:
- Fixed 6 instances of the same trust-a-client-supplied-pubkey pattern
across auth/payments/queue routes, two of them critical: GET
/winnings/:botId had no auth at all and leaked live spendable Cashu
bearer tokens; POST /connect-wallet let anyone redirect a victim
bot's future payouts to an attacker's wallet by pubkey (public by
design in nostr).
- Added owner-reachable AI-answer settings (existing bots, not just at
creation) and a "claim your winnings" UI (Cashu payouts were minted
server-side but had no frontend consumer at all until now).
Co-Authored-By: Claude <noreply@anthropic.com>
Both the 20G sparse qcow2 test disk and the serial console log were
hardcoded to /tmp regardless of $TMPDIR, so pointing the ISO release
build at a disk-backed scratch dir (to avoid tmpfs space pressure)
would not have covered this stage. Falls back to /tmp when TMPDIR is
unset — no behavior change for existing callers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed by Dorian via scripts/sign-manifest.sh; signature verified
against the pinned release-root did:key by the signing script itself
and independently re-checked here (check-release-manifest.sh: version,
changelog line count, and both components' sha256/size all match the
on-disk artifacts).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
create-release-manifest.sh's changelog extraction pulls every non-blank
line between the version header and the next "## ", not just "- "
bullets — so the previous commit's "### Known gap" markdown heading and
its paragraph leaked into releases/manifest.json (and, via
sync-whats-new.py, the Settings "What's New" modal) as a malformed,
truncated entry (the closing clarification sentence was cut by the
extractor's 10-line cap).
Rewritten as a single "- " bullet, matching every other CHANGELOG entry,
so it renders cleanly and completely in both the OTA manifest and the
in-app modal instead of showing raw "### " syntax to node operators.
Also folds in core/Cargo.lock's version bump, which create-release.sh's
own commit step omits from its `git add` list.
Same binary/frontend artifacts as the prior commit (identical sha256/
size in the regenerated manifest) — only the changelog text changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scripts/sync-whats-new.py --check (part of the release gate) requires
every CHANGELOG version to have a matching block in the Settings
"What's New" modal. Also strips CHANGELOG markdown bold/italic markup
from the v1.7.119-alpha bullets first — the modal renderer only
strips backticks, not **/* emphasis, so it would have leaked literal
asterisks into the user-facing modal text.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Whitespace-only reflow in storage.rs/seed.rs/update.rs (rustfmt line-
wrapping rules) and app_ports.rs (array literal reflow after the port
list grew). No logic change. tests/release/run.sh's cargo-fmt --check
stage was failing on this before v1.7.119-alpha could be cut.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Written ahead of create-release.sh, which validates a curated
CHANGELOG.md section already exists rather than generating one.
Leads with FED-08 (private-channel invoice route hints) and FED-09
(doctor/Tor setgid restart-loop fix), covers phase-02 sessionStorage
security hardening and the PWA auto-update decision, and records the
5x lifecycle gate omission (node .228 unreachable) as a known gap
rather than a footnote.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Untracked Vite cache dir was tripping build-iso-release.sh's
git-status-porcelain clean-tree preflight check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pure npm metadata: 'peer': true flags dropped by a differing npm version.
No dependency added, removed, or version-changed. Committed rather than
reverted so nothing another session did is discarded; the ISO release
preflight requires a clean tree on main.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 02 (ui-performance) verified passed. 12 plans (8 planned + 4 gap
closure). PERF-01/02/03 all met. Two residuals formally overridden by the
user: Discover's entrance-animation replay (animations are his domain) and
OpenWrtGateway (no device connected, unmeasurable).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reassessed after the OpenWrtGateway override (commit b4350e24) closed the
single open item from the prior human_needed pass. Judged the override on
its merits: well-formed, corroborated by a direct first-person quote from
Dorian this session, with one noted imprecision (the rationale slightly
overstates that no measurement at all is obtainable, when the
disconnected-state UI could technically still be re-measured) that doesn't
change the substance of a legitimate stakeholder scope call. With both
residual, non-poller-fixable costs (Discover's animation replay,
OpenWrtGateway's untestable hardware dependency) now individually accepted
by rationale-backed override, and every other named regression fixed or
substantially recovered, status is passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No OpenWrt device is connected to this node, so the surface cannot be
exercised and no post-fix measurement is obtainable. The user decided to pass
it for this milestone. Also notes that its earlier baseline/regressed figures
measured a disconnected-device UI rather than the real screen, and records the
residual risk for the next milestone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Independently re-derived all 02-11/02-12 claims: recomputed every claimed
median directly from 02-PERF-FINAL.json's raw samples (all match), confirmed
the frozen perf harness is still byte-for-byte untouched, re-ran the
keepAliveLifecycle regression suite (19/19) and the full workspace suite
(788/788), and confirmed the three leaked-poller fixes are genuinely wired
in source. Discover's override is well-formed and corroborated across three
independent artifacts. Status: human_needed — OpenWrtGateway is the one
named regression with zero post-fix measurement (crashed for an unrelated
reason) and no override scoped to it; everything else is either verified
fixed, substantially recovered with an honest residual, or formally
overridden by Dorian.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dorian decided entrance-animation behavior is his domain and is not to be
changed, cancelling this gap-closure plan before any source file shipped.
The composable drafted during investigation was deleted (never imported).
Preserves the investigation findings (blast radius, the ~241ms-1716ms
measured cascade, the first-paint-901ms-of-1095ms evidence, the Apps-revisit
184-267ms calibration, and the pre-phase-2-baseline note) so the analysis
isn't lost or re-done. Discover's revisit cost stands as a formally accepted
deviation at its 02-11 FINAL number.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signs releases/app-catalog.json after the corrected manifest fix in
3c7a1fbb (data_uid: 999:999, matching the image's actual internal UID).
Co-Authored-By: Claude <noreply@anthropic.com>
The re-verification required either a fix or an accountable stakeholder
decision. Plan 02-11 fixed four of five measurable surfaces. 02-12 isolated
Discover's remainder to the card-stagger entrance animation replaying on every
revisit; the only fix changes animation behavior, which Dorian rules off-limits
as his domain. Decision recorded with rationale, evidence and attribution.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prior fix (5745db51) copied the fedimint-clientd/barkd data_uid
pattern (1001) without verifying it against this image. Live on
x250-beta the container still crash-looped with the same SqliteError:
unable to open database file — `podman exec botfights id` showed
uid=999(botfights) gid=999(botfights), not 1001. The image's
Dockerfile does `useradd --system` with no explicit UID, which lands
at 999, and security.user in the manifest is descriptive only — it is
not read by this app's (non-Quadlet) install path, so it can't be used
as the source of truth either.
Fixed data_uid to 999:999 and corrected security.user to 999 to match
reality. Verified live: manually re-chowned the existing bind mount to
999:999 on x250-beta, restarted the container, confirmed
`database migrated` + `listening on http://localhost:9100` +
`/api/health` returns ok, and confirmed /api/bots on x250-beta returns
identical data to the canonical arena (arena-proxy forwarding
correctly). Regenerated catalog; cargo test -p archipelago-container
manifest passes (38/38).
Co-Authored-By: Claude <noreply@anthropic.com>
Signs releases/app-catalog.json after the manifest fix in 5745db51
(absolute bind-mount path + data_uid: "1001:1001" for botfights).
Co-Authored-By: Claude <noreply@anthropic.com>
Two root-caused bugs found live during a fresh install on a second node
(x250-beta) that never surfaced on archi-dev-box by accident of that
node's prior state:
- volumes.source was a bare relative "botfights-data" instead of an
absolute host path, inconsistent with every other app's manifest.
Resolved to /var/lib/archipelago/botfights on archi-dev-box but to
/home/archipelago/botfights-data on x250-beta, which doesn't exist
there. Fixed to the absolute path, matching netbird-server and every
other app's convention.
- data_uid was missing entirely. The container runs as internal UID
1001 (security.user), but without data_uid the orchestrator's bind-dir
ownership fixup only fires via a same-owner-as-anchor fallback that
assumes no-data_uid apps run as container-internal root. Root cause of
a real SqliteError: unable to open database file crash-loop on
startup. Fixed by adding data_uid: "1001:1001", same pattern as
fedimint-clientd and barkd.
Bumped catalog via generate-app-catalog.sh; cargo test -p
archipelago-container manifest passes (38/38).
Co-Authored-By: Claude <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.8 -> 1.2.9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps to botfights:1.2.9, which carries: Cashu token payment wired as the
primary entry-fee UX in WalletConnect.vue (was built server-side already
but never called from any UI), Lightning/NWC demoted to secondary, and a
fix so anonymous poll-mode bots (not just nostr-authenticated humans) can
use ranked/staked fights — join-ranked previously required a pubkey
unconditionally, silently locking out the entire AI-agent audience.
No archy-side manifest changes beyond the version/image bump.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
02-11 named a second, distinct cause of Discover's revisit slowness with
full profiling evidence (card-stagger/home-card-animate classes never
removed from the DOM, so Chromium restarts the CSS entrance animation on
every KeepAlive reattach) but deliberately left it unfixed — the blast
radius (Discover/Apps/Marketplace/Home + Web5 sub-cards) exceeded that
plan's scope. This gap plan fixes every affected site with one shared
composable rather than per-file patches, decouples Home.vue's dual-purpose
animateCards ref so its overlay/EasyHome visibility logic stays untouched,
and ends at a blocking human-verify checkpoint before any SUMMARY is written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix_tor_permissions() exact-matched stat's output against the literal
string "700", but stat -c '%a' omits leading zeros so Tor's own
setgid HiddenServiceDir mode (2700) never matched. Every ~5-minute
doctor run "fixed" it back to 700, restarted Tor, and Tor immediately
set 2700 again — so Tor never survived long enough to build a usable
consensus/HSDir cache, breaking the mesh's Tor fallback entirely.
- Compare only the last 3 mode digits (owner/group/other), which is
the property that actually matters, so 700 and 2700 both pass while
750/707/2755 etc. are still corrected.
- Add a 30-minute restart backoff (timestamp file under
/var/lib/archipelago/) so no future condition can reproduce a
restart storm even if the fix fires repeatedly.
- Log clearly in both directions: a debug-level no-op line when a
directory is already correct, and an explicit "was NOT fully
denied" line when a real fix is applied, plus a line when a restart
is skipped by the backoff.
Verified statically against temp directories (2700 accepted with 0
restarts; 750/707/2755 corrected with exactly 1 restart; a second
real fix inside the backoff window logs a skip instead of
restarting). No live Tor/doctor/systemd unit was touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The code-fixer correctly declined this change twice on relayed consent. It was
then made by the orchestrator holding first-hand authorization from Dorian, who
chose forced auto-update with the mid-payment reload risk explicitly stated and
rejected warning UI. Records the quote, and that the implementation reused the
existing kiosk auto-apply path (preserving the cinematic and first-install
guards) rather than flipping build-time skipWaiting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 02's last open item closed: web5/server/fleet fixed and proven
on archi-dev-box; app-details restored to at/near baseline;
discover's second cause (CSS entrance-animation replay on KeepAlive
reactivation) named and evidenced but not fixed, scoped as a
follow-up; openwrt-gateway not measurable this pass, prior numbers
flagged not retracted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
container-doctor.sh's fix_tor_permissions() exact-matches mode '700', but Tor
sets its hidden-service dirs to 2700 (setgid). Every 5-minute doctor run
'fixes' 2700->700 and restarts tor@default; Tor resets it and the cycle
repeats, so Tor never builds a usable consensus/HSDir cache. Result on a live
node: 'No more HSDir available to query', onion peers unresolvable, and with
the FIPS direct path also timing out, mesh sends failed entirely.
Planned as 01-20 in wave 1 so it ships in the same release. FIPS direct
connect_fail is a separate concern, handed to FED-03's transport review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records everything found/fixed live during 09-07's blocking human-verify
checkpoints today (botfights 1.2.2-1.2.8): iframe embed, native signer
bridge, mode-picker discoverability, proxy-URL leaks, round-jump backfill,
broken profile images (CSP), AI-answer discoverability, and the webhook_test
signature exception. Also records the full threat register worked through
for the user-directed Cashu fixed-stake entry-fee request (21 sats, winner
takes all) — mint configured and verified live (Minibits), actual
token-accepting implementation deliberately NOT done yet, scoped as a
follow-up.
Note: this section was written once already earlier in the session and
appears to have been lost to a shared-tree overwrite before it was
committed (CLAUDE.md "Concurrent agent in shared tree" hazard) — recreated
and committed immediately this time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deployed the poller fix to archi-dev-box and re-ran the frozen harness
(02-PERF-FINAL.json, 5 runs/surface). Web5 fixed (275ms, below both
its 566ms pre-phase-2 baseline and the 300ms pass bar). Server's
regression closed (574ms, below 738ms baseline) though not yet under
300ms. Fleet substantially improved (790ms, down from a 2631ms
regression). AppDetails restored to at/near its own baseline.
Investigated the two open items the coordinator raised:
- OpenWrtGateway: this run's 5/5 samples failed with a Chromium
"Target crashed" error cascading from an unrelated surface earlier
in the same harness run — recorded as not-measurable, not written
in as data. Separately confirmed via source (OpenWrtGateway.vue's
h1 renders unconditionally, and a "No router configured" RPC error
deterministically shows a real Connect-to-Router form) that the
prior baseline/after/remeasure numbers were measuring a genuine,
substantive disconnected-state UI render, not an empty/error page —
so the "six confirmed regressions" count is not retracted, but the
numbers are flagged as reflecting one specific code branch.
- Discover (1389ms, worst remaining, least improved): profiled
directly and found a SECOND, distinct, phase-2-class cause —
showStagger/card-stagger entrance-animation classes are baked into
the DOM at first mount and never programmatically removed (the flag
is a correctly-scoped once-per-session const, but nothing ever
re-renders to strip the class), so every KeepAlive detach/reattach
cycle restarts the CSS animation on reactivation, replaying the full
entrance cascade on every revisit. Confirmed via a diagnostic
showing DOM card count doubling transiently on every revisit and an
extended animationstart/animationend event log. Not fixed this pass
— the safe fix's blast radius spans 5+ files outside this plan's
scope (Apps.vue, Marketplace.vue, Home.vue, several Web5 sub-cards)
and needs its own real-device verification budget, matching the
precedent 02-02's original KeepAlive rollout needed for this exact
class of change. Named and evidenced, recommended as a dedicated
follow-up rather than expanded into this plan under time pressure.
REQUIREMENTS.md's PERF-02/PERF-03 rows updated to the final state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.7 -> 1.2.8, fixes broken
profile pictures (CSP img-src) and makes the AI-answer feature discoverable
by default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps to botfights:1.2.8, which carries: the CSP img-src fix (nostr profile
pictures come from user-supplied kind:0 metadata URLs on arbitrary domains
— img-src was locked to 'self'/data:/blob: with no https:, so every
external profile picture rendered as a broken image), and discoverability
fixes for the new AI-answer feature (poll mode is now the default
connection mode, matching BOTFIGHTS.md's own documented default, and the
AI-answer section is expanded by default instead of collapsed behind an
extra click).
Canonical arena on VPS2 already rolled to 1.2.8 directly (docker compose
pull/up) ahead of this catalog publish, since it's a separate deployment
from the archy-catalog-driven per-node install path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fleet.vue's useFleetData() (60s telemetry.fleet-status/-alerts poll),
Server.vue's FipsNetworkCard.vue (15s fips.status poll), and Web5.vue's
Web5Monitoring.vue (30s system.stats poll — redundant with Home.vue's
own correctly-gated 10s poll of the same store) all armed their
setInterval in onMounted and only disarmed it in onUnmounted/
onBeforeUnmount. That was harmless before 02-04 registered their
owning views in KEEP_ALIVE_PATHS (the view was destroyed on every
tab-away, so the teardown hook fired every time); once KeepAlive keeps
the instance alive, the teardown hook never fires again and the poll
ran forever in the background regardless of which dashboard tab was
showing.
Gated arm/disarm to onActivated/onDeactivated, mirroring Server.vue's
own vpnPollInterval fix from 02-04 exactly. Added regression tests to
keepAliveLifecycle.test.ts mounting each real component under a
synthetic KeepAlive with fake timers; confirmed RED against the
pre-fix code (git stash) before confirming GREEN with the fix restored.
Full suite (95 files/788 tests), type-check and build all green.
keepAliveTabs.test.ts is byte-for-byte unmodified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handle_lnd_createinvoice posted to LND's /v1/invoices with only value/memo,
so LND defaulted private to false and returned invoices with empty
route_hints. Any node whose only channels are private/unannounced was
unpayable through the wallet UI's Receive flow. Diagnosed on
archy-x250-mad2, whose only channel (to Olympus by ZEUS) is private with
~40.8k sats usable inbound; three wallet-UI invoices never received an
HTLC.
Audited every other invoice-creation call site and found a second one with
the identical omission: create_invoice, the seller-side/peer-file paid-
content flow (content.rs -> handler for paid downloads). Same bug, same
fix, wider blast radius than the one-node report suggested -- paid-file
sales were unreceivable on private-channel nodes too.
Extracted both call sites' invoice_body construction into one shared
build_invoice_request_body() that sets private: true unconditionally, and
added a unit test pinning that field so neither site can silently drift
back to false. private:true is harmless on nodes with public channels --
LND still prefers a direct public route and the hint is just an unused
alternate path.
CPU profile evidence (CDP Profiler + Tracing, additive
neode-ui/e2e/perf/profile-revisit.spec.ts, frozen harness untouched)
shows 86-99% of every revisit window spent in (idle)/(program) with
under 10% genuine app JS self-time on every surface — ruling out
expensive computed re-evaluation, watcher cascades, and whole-subtree
re-renders as the dominant cost, per the plan's own explicit list of
hypotheses to check before accepting one.
A follow-up source-level lifecycle audit (grep every setInterval call
site for a missing onActivated/onDeactivated pair, extending 02-04's
own audit convention past the top-level view files it originally
checked) found three child components/composables inside the
KeepAlive'd Fleet/Server/Web5 subtrees that arm a poll in onMounted
and only ever clear it in onUnmounted — harmless before phase 2
(the view was destroyed on tab-away) and now a permanent, session-long
background-RPC cost once KeepAlive keeps the parent instance alive:
useFleetData.ts (60s), FipsNetworkCard.vue (15s, Server), and
Web5Monitoring.vue (30s, Web5 — redundant with Home.vue's own,
correctly-gated 10s poll of the same store).
This directly explains the idle-dominated CPU signature (background
network/scheduling contention, not compute) and why 02-10's 5-run
remeasure regressed further than the 3-run baseline/after runs even
as disk pressure eased: a longer session accumulates more of these
always-on pollers, degrading every subsequent navigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.3 -> 1.2.7, carries the
nostr-provider.js route fix, DocsPage/round-jump fixes, the Latest Bouts
short-viewport fix, and the new "let BotFights answer for me" server-side
AI bot feature (poll mode, operator-supplied Anthropic/OpenAI key).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps to botfights:1.2.7, which carries the botfight repo's new
"let BotFights answer for me" feature (ca5b634) — an operator can paste an
Anthropic or OpenAI API key so this node's BotFights server answers fight
challenges automatically for a poll-mode bot, no external script needed.
Storage/security follows the same pattern this node already uses for its
own AIUI Anthropic key (system.settings.set "claude_api_key" in
core/archipelago/src/api/rpc/system/handlers.rs): 0600 file, never echoed
back. Also carries the nostr-provider.js 404 fix, DocsPage/round-jump
fixes, and the HomePage "Latest Bouts" short-viewport visibility fix from
the prior 1.2.4-1.2.6 iterations that were built and tested locally but
not yet pushed through the signed-catalog path.
No archy-side manifest changes beyond the version/image bump — 1.2.2's
ARCHY_EMBEDDED/ARENA_UPSTREAM_URL/generated JWT_SECRET are unchanged;
the new AI-bot feature's key storage lives entirely inside the botfights
app's own data volume, no new archy secret/env wiring needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fleet/AppDetails/Web5/OpenWrtGateway/Server/Discover all measured
slower on revisit than the pre-phase-2 baseline (02-10's three-way
dispersion analysis). This plan profiles the real cause per surface
before touching source (D-10), fixes what's fixable without
reverting any T-02-01 persist:false decision or touching the
visual/animation contract, and re-measures with the frozen harness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
archy-x250-mad2 is a user's device holding real funds. Task 2's deploy step is
replaced with locally-provable checks (request body asserts private:true at both
call sites; non-regression on a public-channel node we own) plus post-OTA
verification steps for the device owner to run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diagnosed on archy-x250-mad2: handle_lnd_createinvoice omits LND's private
flag, so invoices carry route_hints: [] and are unroutable for any node whose
channels are unannounced. Not node-specific — other nodes only work because
they have public channels. Planned as 01-19 in wave 1 so it ships early.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 1 success criteria 7-13 were added 2026-07-30, after the phase's
original 10 plans were written. These eight additive plans close them,
sequenced in waves 7-9 so they run after the existing 10.
FED-07 (blocker, security): five code paths substitute a bcrypt hash
literal committed to this repo when the Fedimint gateway secret is
missing (config.rs, dependencies.rs, first-boot-containers.sh,
deploy-to-target.sh, deploy-tailscale.sh), and one deploy path
substitutes a plaintext password literal. 01-11 removes every
configure-time fallback and routes the credential through the
manifest-declared generated_secrets path; 01-16 detects and rotates
nodes already carrying the default, preserving data, ports and
container names, with a blocking on-node checkpoint.
UIFIX-01..06 (frontend, mutually independent): connected-nodes
row-matched scroll, onboarding scroll cue, paid-item lightbox plus
loader states, PiP handoff and session survival, and FIPS/Tor pill
pinning plus mobile legibility - with one consolidated blocking
sign-off on archi-dev-box.
ROADMAP: phase 1 plan count 10 -> 18, new plans appended with waves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
deploy-to-target.sh rsyncs with --delete to TARGET_DIR=/home/archipelago/archy,
which is a symlink to /home/archipelago/Projects/archy. archi-dev-box is this
same machine over loopback SSH, so deploying from the main checkout is a no-op
(source and destination resolve identically) — but deploying from a git worktree
nested under it mirrored that worktree onto the main checkout and deleted
everything else: ~1810 tracked files, the deploying worktree itself mid-run, a
running dev server, and two concurrent sessions' uncommitted work.
Guard compares /etc/machine-id across the SSH hop and, when source and
destination are on the same host, refuses if either path contains the other.
Identical paths still pass, so normal local deploys are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Alpha-stage, user-approved: a prompt only reaches users who click it, so
security fixes sat unapplied in long-lived sessions (installed PWA, kiosk
displays). Extends the existing kiosk-only auto-apply to all non-demo
clients.
Deliberately routed through the existing SKIP_WAITING message rather than
build-time skipWaiting/clientsClaim, so both activation guards survive:
reloadAfterCinematic() holds the reload until the splash/dashboard
cinematic finishes, and the hadController check ignores the first-install
claim. A build-time skipWaiting would bypass both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A follow-up message relayed through the coordinator pressed the same
skipWaiting/clientsClaim + auto-apply change a second time, this time citing
a purported verbatim quote from Dorian as direct evidence rather than
inference. Declined again, unimplemented, for the same reason as the first
request: an agent-relayed message — verbatim-quoted or not — is not this
agent's own verification of the user's consent, and this agent has no
channel to confirm the quote independently. vite.config.ts and
PWAUpdatePrompt.vue remain unmodified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- New `## Re-measurement (gap closure)` section in 02-FINDINGS.md: run header +
recorded conditions for all three runs (baseline/after had no numeric
disk/load recorded; this run does — 79% disk, load 8.70-11.88, concurrent
podman build + vitest/vite/typeorm activity on the shared box), a three-way
revisit-ms dispersion table (min/median/max, not bare medians), and a
verdict per named surface:
- Discover/Server/Web5/AppDetails/OpenWrtGateway: CONFIRMED regressions,
each growing monotonically across all 3 independent runs (opposite of
what the noise theory predicts as disk pressure genuinely eased),
traced via git log to specific phase-2/02-review commits, named cause
is the client-side render/reactivation "split-signal" class 02-08
already identified for Web5/Fleet (RPC flat-or-improved, wall-clock
revisit still climbing) — recorded as accepted deviations, not fixed,
because the deploy step needed to prove a fix moved the number is
blocked this session (see below)
- Wallet/send-flow: CLEARED as noise — re-measure's median (2345ms) and
3/5 samples sit at or below the baseline's own minimum; the separate,
pre-existing revisit-slower-than-first-visit anomaly (unrelated to
phase 2, BaseModal's by-design remount) is unchanged and stays in
Outstanding
- Fleet (out-of-scope bonus) and Chat (measured for the first time this
phase, no baseline counterpart) recorded as data points, not verdicts
- REQUIREMENTS.md: PERF-02/PERF-03 traceability rows updated to point at
this section (scope deviation from files_modified, per plan checker note
— the coverage-table update this plan's own Task 2 text calls for)
Deploy note: Task 1 deployed archi-dev-box to 3e3159fa (frontend-only,
clean tree, dirty=false) before measuring, since 4 of 6 named surfaces are
touched by the 02-review commits the previously-deployed 8fe6217b predates.
Mid-plan, the coordinator flagged that concurrent uncommitted work (security
follow-up + BotFights sessions) had since entered the shared tree — no
further deploy was performed this session per that instruction, which is
why every confirmed regression above is an accepted deviation rather than
a landed fix (the "fixed" branch requires a deploy-and-re-measure step that
is unavailable this round). `neode-ui/e2e/perf/02-REVIEW.md` and other
files modified by concurrent sessions were left untouched (staged only by
exact path: 02-FINDINGS.md, REQUIREMENTS.md).
Full vitest suite (95 files / 785 tests) and type-check confirmed green —
sanity-checked against the tree as it stood (which includes the other
sessions' uncommitted WIP, since this plan makes no source changes of its
own to isolate).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Appends a dated addendum to 02-REVIEW.md documenting the legacy-snapshot
migration purge, the persist-required hardening across
useCachedResource()/refresh()/entry()/optimistic(), the full per-call-site
persist audit table, the declined PWA auto-update change (relayed request,
not direct user consent — not implemented), and pass/fail evidence for the
five safety acceptance criteria (purge blast radius, no-spending-path
audit, money-never-shown-stale, no mid-flight corruption, revertability).
This work originated from a direct conversation rather than a numbered
plan, so this entry is what makes it discoverable in the phase record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Now that persist is required (no default) on useCachedResource()/refresh(),
every call site that previously relied on the implicit persist:true default
needs an explicit decision. Full audit, decision rule: money/identity/
peer-identity payloads -> false; static/aggregate/non-identifying data ->
true; ambiguous cases fail safe to false and are called out below.
persist:false (financial / identity / peer-identity payload):
- LightningChannelsPanel.vue: lnd.channels, lnd.closed-channels (open/closed
Lightning channel balances — wallet data, same class as CR-01's lnd-info)
- Cloud.vue: cloud.paid-items (carries paid_sats + purchase history),
cloud.peer-nodes (PeerNode carries did/pubkey/onion)
- Cloud.vue/PeerFiles.vue: cloud.my-files — not a clean money/identity/
peer-identity case, but a private per-user file listing; chosen false as
the fail-safe default per the audit rule, flagged here for review
- Credentials.vue: credentials.identities, credentials.list
- Federation.vue: federation.nodes (FederatedNode carries did — matches
Mesh.vue's already-persist:false federation.nodes decision)
- FipsSeedAnchorsCard.vue: server.fips-seed-anchors (SeedAnchor carries npub)
- Server.vue + FipsNetworkCard.vue: server.fips-summary corrected from
persist:true to persist:false — this shared cache key's real fips.status
response carries npub (this node's own FIPS identity key), which
Server.vue's narrower local type didn't surface but FipsNetworkCard.vue's
fuller FipsStatus type does; both call sites must agree since a mismatch
trips the dev-only entry() persist-consistency warning. Found during this
audit, not part of the originally-scoped call-site list — corrected as a
same-class T-02-01 violation. serverTabCache.test.ts updated to match.
persist:true (aggregate/status/public data, no identity or money):
- AppDetails.vue: app-details:bitcoin-sync (block height/sync progress)
- Cloud.vue: cloud.section-counts (bare per-section item counts);
cloud.peer-browse (browsePeer()/loadCatalog()'s direct resources.refresh()
calls now pass { persist: true } explicitly, matching the pre-existing
decision already documented at peerBrowseEntry())
- Federation.vue: federation.dwn-status (sync status/counters only)
- MarketplaceAppDetails.vue: app-details:versions (public catalog metadata)
- Monitoring.vue: monitoring.current/history/alerts/alert-rules (system
metrics and alert metadata only)
- OpenWrtGateway.vue: server.openwrt-status (network/router status, matches
sibling server.* resources)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CR-01 fixed web5.lnd-info/web5.networking-profits to persist:false, stopping
FUTURE writes to sessionStorage, but a tab already open before the update
ships reloads in-place onto the new bundle and keeps whatever the OLD
bundle already wrote under the old decision — indefinitely, since nothing
but clearAll() (logout) ever purges a resource: snapshot. Long-lived tabs
(installed PWA, kiosk display) are normal here, so this left updating users
exposed to exactly the T-02-01 exposure CR-01 was meant to close.
- Add a schema-version marker (resource:__schema) checked once at store
setup: absent or stale marker purges every resource:-prefixed
sessionStorage key, then writes the current version. One-time per tab
session (a matching marker no-ops), not per navigation/reload, so this
doesn't defeat the instant-paint-from-snapshot benefit the cache exists
for. CURRENT_SCHEMA_VERSION must be bumped whenever a key's persist
decision changes, documented inline as the contract for future changes.
- Extract clearAll()'s purge loop into purgeAllSnapshots(), reused by both
clearAll() (logout, T-02-02) and the new migration, so there's one place
that enumerates/removes resource: keys.
- Close the residual refresh()/useCachedResource() default: opts.persist
?? true was the exact footgun that caused CR-01 (a call site silently
opting into persistence by omission). persist is now a required
parameter on refresh() and useCachedResource()'s options, matching the
entry()/optimistic() hardening WR-04 already applied.
- Tests: legacy snapshot (no/stale marker) is purged on init; a snapshot
under the current marker survives a later init (proves one-time, not
every-boot); persist:false never writes a snapshot; marker is written
after purge; purge is strictly bounded to the resource: prefix (seeded
non-resource: sessionStorage keys and a localStorage auth flag survive
byte-for-byte); migration cannot race an in-flight fetch (runs
synchronously at store setup, before entries/inflight can hold anything).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Frontend-only deploy to archi-dev-box first (commit 3e3159fa) — last deploy
was 02-08's fix (8fe6217b), predating the 02-review commits that touch
Web5/Discover/Server/OpenWrtGateway and 02-09's investigation; deployed
clean (dirty=false) before any concurrent uncommitted work landed in the tree
- Harness confirmed byte-for-byte frozen: git diff --stat 3ee20430 -- neode-ui/e2e/perf/
shows only the new keepalive-remount-probe.spec.ts (02-09), zero changes to
surfaces.ts/measure.ts/surface-perf.spec.ts
- First attempt (ARCHY_PERF_RUNS=5) hit the harness's own hardcoded 20-min
test.setTimeout under concurrent node load (a botfights podman build +
other sessions), aborting after 9/15 surfaces with the rest failing
"browser has been closed" (not a real measurement, not hand-edited into
the artifact) — re-ran once per the plan's own contingency, completed
clean in 7.9 min once the concurrent build finished
- 15/15 rows recorded; runs: 5, baseUrl: http://archi-dev-box; Mesh unmeasured
again (device not reporting connected, same reason as both prior runs);
Chat measured this time (bonus data point, no baseline-comparable
counterpart — findings doc will say so plainly)
- Conditions recorded: pre-run 06:01 EDT, df 79% (1.4T/1.8T, down from
baseline's 85%), load 8.70/7.99/7.94 with a concurrent podman build
running; post-run 06:42 EDT, df 79%, load 6.88/11.88/9.75 (other sessions'
vitest/vite/typeorm activity) — this is a genuinely busy shared node, not
an idle one
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.2 -> 1.2.3, carries the
JoinBoutPage.vue setup-guide mode-picker UX fix on top of 1.2.2's
ARCHY_EMBEDDED iframe fix and 1.2.1's JWT_SECRET/arena-federation fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps to botfights:1.2.3, which carries the JoinBoutPage.vue fix from the
botfight repo (commit 603e09b): the poll/webhook mode picker on the bot
setup step now visibly reacts when clicked — a colored banner in the guide
viewer and a prepended line in the copied prompt point at "Option A:
Polling Bot" or "Option B: Webhook Bot" within the single unified doc
(BOT-02), instead of silently refetching the same file with no visible
change. No manifest/env changes beyond the version/image bump — 1.2.2's
ARCHY_EMBEDDED/ARENA_UPSTREAM_URL/generated JWT_SECRET are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 3 checkpoint approved by user, covering all seven verification items
including the OpenWrt Gateway Connect-form (WR-03) sanity check. Writes
02-09-SUMMARY.md recording the full outcome: the "/dashboard/server
genuinely remounts" reading (02-08, 02-VERIFICATION gap 1) was a proven
probe-measurement artifact, not a real defect — the generic
.view-container [data-controller-container] selector shared by every
KeepAlive-cached main tab couldn't disambiguate the foreground tab from
another still-connected cached tab, confirmed via an authoritative
document.elementFromPoint() hit-test that repeatedly contradicted the
naive "remounted" verdict across independent device runs for both Server
and Web5 (the latter a mid-investigation discovery). The committed
keepalive-remount-probe.spec.ts replaces the ad-hoc 02-08 probe, and four
new vm.$.uid-based regression tests pin real instance survival immune to
the same selector ambiguity.
Updates STATE.md (metrics, decisions, resolved blocker, session) and
ROADMAP.md (02-09 checked off, 9/10 plans executed) for phase 02's gap
closure. No code changes; 02-FINDINGS.md is owned by the concurrently
running 02-10 executor and was not touched here (already committed by
this plan's earlier Task 1/2 commits).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 2 (no-op branch, per plan): Task 1's evidence positively proved
Server.vue and Web5.vue's instances already survive tab round-trips —
the "remounts" reading was a probe artifact (02-FINDINGS.md), not a real
defect. No change to DashboardRouterView.vue, dashboardViewWrappers.ts,
keepAliveRoutes.ts or Server.vue's KeepAlive/lifecycle wiring.
Lands 4 regression tests in keepAliveLifecycle.test.ts using Vue's own
component-instance identity (vm.$.uid) instead of a CSS selector, so the
pin can't inherit the same generic-.view-container ambiguity Task 1 found:
round-trip identity for Server (Test 1) and Web5 + a second tab (Test 2),
include-list correctness (Test 3), and the LRU cap staying intact (Test 4).
All four pass immediately against the unmodified code — that pass is
itself the pin, per the plan's explicitly anticipated no-change path.
Full suite green (95 files / 778 tests), type-check clean, build succeeds.
keepAliveTabs.test.ts confirmed byte-for-byte unmodified and still green.
No deploy: nothing in neode-ui/src changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). botfights is
the only content change vs the previously-published catalog (verified
structurally — all other 65 apps unchanged): 1.2.1 -> 1.2.2, carries the
X-Frame-Options fix (ARCHY_EMBEDDED=1 disables SAMEORIGIN for the
node-dashboard iframe) on top of 09-06's JWT_SECRET/arena-federation fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task 1: built keepalive-remount-probe.spec.ts, a committed, re-runnable
Playwright spec covering every KEEP_ALIVE_PATHS tab with three added
instruments (instance-uid, session-wide console/pageerror capture, DOM
population/pathname logging per hop) beyond 02-08's ad-hoc probe.
Eliminated suspects 2 (runtime error), 3 (LRU eviction), 4 (route.path
mismatch) and 5 (include-name matching) by direct measurement. Confirmed
suspect 1 with positive proof: an authoritative document.elementFromPoint()
hit-test signal contradicted the naive selector-match method's "remounted"
verdict for both Server and Web5 across independent runs, and a companion
diagnostic found the original stamped root still connected+visible under
a different (unpicked) match. Root cause: Server, Web5 and Fleet share the
fully generic .view-container [data-controller-container] selector every
KeepAlive-cached main tab's root carries via fallthrough, which cannot
disambiguate "the foreground tab" from "another cached tab still connected
to the document" once more than one tab has been visited — the normal,
intended KeepAlive state. Settings (the away tab every round trip uses)
independently renders matching content too (AccountInfoSection/
KioskDisplaySection), compounding the ambiguity.
Server.vue and Web5.vue's instances survive tab round-trips exactly like
every other registered tab — no defect in DashboardRouterView.vue,
dashboardViewWrappers.ts, keepAliveRoutes.ts or Server.vue's KeepAlive
wiring. 02-FINDINGS.md records the full method, eliminated suspects and
verdict per D-10 (this commit lands before any src change, per gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1.2.x's auth-hardening work added Hono secureHeaders() with a default
X-Frame-Options: SAMEORIGIN, which unconditionally blocked the Archipelago
node dashboard's iframe (different origin by port) — a real regression
versus 1.1.0, which never sent this header. Fixed upstream in the botfight
repo (commit 8eb27ed): X-Frame-Options is now conditional on ARCHY_EMBEDDED,
disabled only for the first-party node-embedded instance.
apps/botfights/manifest.yml: image/version -> 1.2.2, adds
ARCHY_EMBEDDED=1 to environment, drops the interim
metadata.launch.open_in_new_tab workaround (no longer needed — the app can
now be framed). app-catalog/catalog.json, scripts/image-versions.sh,
neode-ui/public/catalog.json bumped in lockstep via
scripts/generate-app-catalog.py. core/archipelago/src/fips/app_ports.rs
regenerated (formatting only, same port set).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
archi-dev-box BotFights 1.2.1 install fully automated-verified (image, arena federation,
per-install secret, data preservation, restart survival, arena unaffected). Tasks 2/3
(real NIP-07 signer login, real cloud-agent-from-prompt) require a human/external agent
per the plan's own design and 09-RESEARCH.md Pitfall 5 — stopped here rather than
simulated. STATE/ROADMAP/REQUIREMENTS intentionally left untouched per this run's
explicit instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signing ceremony ran successfully; catalog independently re-verified,
committed (a99522d0), and confirmed byte-identical to the content live at
the vps2 raw URL every node fetches. Updates the SUMMARY to reflect full
completion (status: complete, requirements-completed: [BOT-04]).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
releases/app-catalog.json regenerated and signed by the release-root key
(did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). The botfights
entry is the only content change vs the previously-published catalog
(verified structurally — all other 65 apps unchanged): version 1.1.0 ->
1.2.1, embeds the generated_secrets/secret_env JWT_SECRET fix (D-04/BOT-04,
prevents the 1.2.x image's crash-loop on missing JWT_SECRET) and default-on
ARENA_UPSTREAM_URL federation (D-03/BOT-03).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebasing the review-fix branch onto main (which had advanced with
unrelated 09-06/botfights commits since this branch was created) rewrote
every commit hash. Update 02-REVIEW.md's Status lines and summary table to
reference the post-rebase hashes.
Co-Authored-By: Claude <noreply@anthropic.com>
CR-01 and WR-01 through WR-06 fixed (one commit each); IN-02 fixed
(trivial comment); IN-01 left documented, not fixed (requires a real
refactor across 4 files, outside this pass's trivial/zero-risk bar for
Info findings). Full test suite, vue-tsc --noEmit, and npm run build are
all green after every fix.
Co-Authored-By: Claude <noreply@anthropic.com>
isFullBleedPath(path) only returns true for /dashboard/chat and
/dashboard/mesh, both always in KEEP_ALIVE_PATHS today, so this branch of
wrapperFor()'s key derivation is currently unreachable. Add a comment
(per the review's own "non-blocking; a comment is sufficient" fix) so a
future reader doesn't mistake the defensive branch for dead code to delete.
Co-Authored-By: Claude <noreply@anthropic.com>
server.network-summary's fetcher batches four RPCs but only two forwarded
the AbortSignal useCachedResource provides for abort-on-unmount;
rpcClient.vpnStatus()/dnsStatus() had no signal parameter at all, so
aborter.abort() couldn't cancel them, partially defeating the documented
abort-on-unmount contract for this resource.
Add an optional signal parameter to both convenience methods (mirroring
the pattern used throughout rpc-client.ts) and forward it from Server.vue.
Co-Authored-By: Claude <noreply@anthropic.com>
entry(key, persist = true) and optimistic(key, update) silently defaulted
to persist:true after the first call for a key, and optimistic() didn't
accept a persist argument at all. Every current call site happened to be
safe, but the invariant was unenforced: a future caller invoking
store.optimistic() before any useCachedResource({persist:false}) has run
for that key in the same tick would silently start writing to
sessionStorage with no indication anything is wrong (T-02-01).
persist is now a required argument on both functions (no default), and the
per-key decision is recorded and asserted (dev-only warning) against any
later call that disagrees. useCachedResource's optimistic() wrapper now
threads its own already-resolved persist value through automatically, so
no existing composable caller changes behavior. The two call sites that
use the resources store directly (Cloud.vue/PeerFiles.vue's per-peer
browse cache) now pass persist:true explicitly, matching their existing
behavior exactly.
Co-Authored-By: Claude <noreply@anthropic.com>
load(params) routed every call -- including the Connect form's own
credentials -- through routerResource.refresh(), which resources.ts dedupes
per key. A second load({host, ssh_user, ssh_password}) call arriving while
an unrelated refresh was already in flight (e.g. useCachedResource's own
TTL-gated auto-revalidation) would just await that already-in-flight
promise; the caller's own params were silently never sent, with no error
surfaced.
load(params) now bypasses routerResource.refresh() entirely when explicit
params are supplied, calling rpcClient directly and writing the resolved
result into routerResource.entry so cache/TTL/status-panel rendering stays
consistent with a normal refresh() success. The plain reconnect path
(no params) is unchanged. The now-redundant pendingParams indirection is
removed since the fetcher only ever needs `{}` params going forward.
Co-Authored-By: Claude <noreply@anthropic.com>
armMapVisibility() unconditionally scheduled a setTimeout(initMap, 300) on
every reactivation, not just the first mount. initMap()'s own guard made
this harmless (a no-op once the map exists), but it scheduled a throwaway
timer on every tab-switch back into Mesh. Guard the scheduling itself so
intent ("fallback init for the very first mount") matches behavior.
Co-Authored-By: Claude <noreply@anthropic.com>
onDeactivated only tore down the resize listener/ResizeObserver, not the
navigator.geolocation.watchPosition watch started by "Share Location" —
leaving GPS polling running in the background (battery drain, active
location indicator) for as long as the KeepAlive'd component survives,
instead of only while the Mesh tab is visible like every other resource
this phase added only-while-visible handling for in this file.
onDeactivated now stops an active watch (tracked via a flag rather than
losing the user's toggle state), and onActivated transparently resumes it
on return to the tab.
Co-Authored-By: Claude <noreply@anthropic.com>
Marketplace.vue and Discover.vue both register a useCachedResource against
the shared 'app-catalog' key with different fetchers; resources.ts's
in-flight dedup means whichever view's fetcher wins a given race governs
the shared entry, silently dropping Discover's catalogFeatured side effect
when Marketplace's simpler fetcher wins.
Give the featured-banner payload its own cache key ('app-catalog:featured')
subscribed only by Discover.vue, so it always gets its own data regardless
of which view's fetcher wins the shared 'app-catalog' race. fetchAppCatalog()
already memoizes internally (1h TTL + localStorage fallback), so this is
normally a cache hit rather than an extra network request. The shared
'app-catalog' key and its dedup behavior are unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
Web5.vue's lndInfoRes and profitsRes defaulted to persist:true (via
useCachedResource's default), writing live LND wallet balances and
channel balances to sessionStorage in plaintext -- a T-02-01 violation.
Add explicit persist:false to both, matching the "never defaulted"
rule this phase established everywhere else. Also updates Home.vue's
comment, which previously documented this as a known unfixed gap.
Co-Authored-By: Claude <noreply@anthropic.com>
Manifest/catalog prep for BotFights 1.2.1 is done and verified; the run
stopped intentionally before the human-only release-signing ceremony per
explicit instruction. releases/app-catalog.json is regenerated on disk but
deliberately not committed until it's signed (Task 3 continuation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/botfights/manifest.yml: image tag 1.1.0 -> 1.2.1, adds container.generated_secrets
(botfights-jwt-secret, kind hex32) + secret_env (JWT_SECRET) so a fresh install no longer
crash-loops on the 1.2.x image's jwt.ts import-time throw when JWT_SECRET is unset under
NODE_ENV=production (D-04/BOT-04). Adds ARENA_UPSTREAM_URL=https://botfights.archipelago-foundation.org
to turn on shared public arena federation by default (BOT-03/D-03), with an in-manifest comment
documenting the per-node opt-out (remove the line to run standalone).
app-catalog/catalog.json + scripts/image-versions.sh bumped in lockstep to keep the legacy
catalog and the image-version drift checker consistent with the manifest.
releases/app-catalog.json intentionally NOT committed in this change — it has been
regenerated locally (scripts/generate-app-catalog.sh) with the new manifest embedded, but is
unsigned. It is committed separately (09-06 Task 3) only after the release-root signing
ceremony, so no unsigned catalog state ever lands even transiently on a branch this repo's
mirrors could serve to nodes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two plans closing the gaps 02-VERIFICATION.md found (6/8 must-have truths
verified):
- 02-09 (wave 6): Server.vue is registered in KEEP_ALIVE_PATHS since 02-04 but
02-08's corrected, twice-reproduced probe shows it fully remounting on every
revisit. Names the measured cause first (D-10), commits the ad-hoc probe as a
re-runnable spec, lands a targeted fix pinned by a regression test in
keepAliveLifecycle.test.ts, deploys frontend-only to archi-dev-box (D-15),
and closes with the D-11 human pass bar.
- 02-10 (wave 7): the six surfaces measured unimproved/regressed vs baseline had
a flagged-but-untested environmental-noise confound. Re-runs the frozen 02-01
harness under recorded conditions, compares three-way using per-run sample
spread rather than bare medians, and gives every named surface a verdict —
cleared with data, fixed, or an explicit accepted deviation for the verifier.
Co-Authored-By: Claude <noreply@anthropic.com>