454388226c7d5b140fec656acc6708f2f68653d9
2407
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
454388226c |
feat(01-05): surface federation sync failures to the operator (FED-02)
A failed federation sync existed only as a `debug!` line on the node, so a peer that had not synced in days looked identical in the UI to one that synced a minute ago. Now the failure is persisted per peer and rendered. - `FederatedNode.last_sync_error` / `.last_sync_error_at` — the failure-side mirror of the existing `last_transport` / `last_transport_at` pair. - `federation::record_sync_result(data_dir, did, outcome)` — records the message on `Err`, CLEARS both fields on `Ok` so the badge disappears when the peer recovers. Runs under FEDERATION_STORE_LOCK via the `*_inner` load/save convention established by plan 01-01. An unknown DID is a silent Ok that writes nothing, so a peer removed mid-pass is never resurrected by an in-flight sync's error write. Skips the save entirely when nothing changed, keeping the steady state read-only rather than rewriting nodes.json (and contending for the lock) every 90s. - Message truncated to MAX_SYNC_ERROR_CHARS (256), counted in chars not bytes so truncation cannot split a UTF-8 sequence (T-01-18). - The 90s auto-sync loop calls it on both arms; the existing `debug!` line is kept — persisting is additive, not a replacement for logs. - `federation.list-nodes` emits both fields when set, omits them when unset. - NodeList renders a red SYNC badge beside the transport badge on both the trusted-node and peer rows, message + age in the `title` so the row stays single-line. Tests (written first, confirmed failing — 16 compile errors, E0425 on `record_sync_result` and E0609 on `last_sync_error`): - persists_error / success_clears_error / missing_did_is_noop / on_empty_store_is_noop / truncates_long_error - NodeList: badge present when set, ABSENT when unset (the guard against a badge that always renders), and present on an observer peer row. cargo test -p archipelago federation — 42 passed, 0 failed. vitest NodeList.test.ts — 4 passed. npm run build — green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
262998747e |
feat(10-05): report BIP-32 key origin on lnd.create-psbt, and record the honest signing posture (D-07b/D-09)
With Bitcoin Core's wallet deleted, LND's PSBT round trip is the only external-signer path Archipelago has, and D-09's key-origin protection moves from Core descriptors (of which none remain) to the PSBT itself. Adds `psbt_key_origin_report(&str) -> Result<PsbtKeyOriginReport>` to lnd/wallet.rs, reporting `input_count`, `inputs_with_key_origin` and `all_inputs_have_key_origin`. An input counts as carrying key origin when either its `bip32_derivation` or `tap_key_origins` map is non-empty. A PSBT with zero inputs reports false rather than vacuous truth. Parsed with the already-present `bitcoin` and `base64` crates; no dependency added. `lnd.create-psbt` gains an additive `key_origin` object on its response and a `tracing::warn!` with the counts when key origin is missing, because that is the exact condition under which a hardware signer refuses the PSBT. Computed best-effort: a decode failure degrades to `null`, never to an error, so a user's send cannot fail because an inspection helper could not parse something. `handle_lnd_finalize_psbt` and `handle_lnd_create_raw_tx` (which deliberately auto-signs with LND's hot keys) are untouched. Three tests, with fixtures built programmatically from the `bitcoin` crate rather than pasted as opaque base64: with-derivations, without-derivations, and malformed-is-an-error-not-a-panic. KEY-03-SIGNING-POSTURE.md gains an honest per-step coverage map of the fund -> export -> sign offline -> import -> finalize -> broadcast round trip. Of six steps, only the new inspection has automated coverage; steps 1, 4, 5 and 6 have none, and there is no air-gap transport (no animated QR, no .psbt file exchange) — export/import is copy-paste of base64. Untested paths are named as untested. Records the verdict that decides whether any of this is an air gap: on a default node an external signer CANNOT meaningfully sign a PSBT from `lnd.create-psbt`, because LND holds the keys for every input it selects. Evidence: the PSBT is funded from LND's own wallet; `ensure_wallet_initialized` creates a full key-holding wallet via /v1/initwallet; the generated lnd.conf carries no `remotesigner.*` block; and a search of apps/, scripts/, core/archipelago/src and image-recipe/ for remotesigner/createwatchonly/ nochainbackend returns zero matches. No fleet node is provisioned watch-only. What ships is PSBT transport, not air-gapped custody — the gap is provisioning, not plumbing. Adds the standing honesty statement in its own subsection: Lightning channel, revocation and HTLC keys are NOT air-gappable at all. They must sign in real time to answer counterparty commitments; remote signing relocates them to a hardened host, it does not cool them. Also adds a status banner to PSBT-SIGNING-ARCHITECTURE.md recording that its Phase 1 was superseded by deletion rather than delivered, so §0's "single highest-value change" and §2.1's invariant now read against a code path that no longer exists. Banner only; §5.4's honesty table is byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
40b77e392a |
fix(10-03): don't bless a cert minted under an untrustworthy clock
The failure fail-closed cannot catch, because generation SUCCEEDS. This unit runs very early (DefaultDependencies=no, Before=ssh/nginx), long before time has synced. `openssl req -x509` stamps notBefore from whatever the clock says, so on a node with a dead RTC or a flat CMOS battery the cert can be years out: clock ahead -> clients reject it as "not yet valid", a harder failure than the usual self-signed warning; clock behind -> notAfter is already in the past once time syncs. The completion marker was then set and never revisited — a node permanently serving a cert nothing accepts. Finding 1, reported rather than assumed: this image does NOT use systemd-timesyncd. It installs and enables chrony, and chrony-wait.service — the unit that is Before=time-sync.target — is not enabled. So time-sync.target is inert here and ordering After= it would buy nothing. Enabling chrony-wait to make it meaningful would stall boot behind NTP on a node with no network, and these nodes are routinely offline at first boot. Not deadlocking boot outranks cert-date elegance, so the ordering is deliberately left alone. Fixed locally instead, in two parts: 1. Backdate notBefore by 24h so ordinary skew between node and client cannot invalidate a fresh cert. -not_before/-not_after arrived in OpenSSL 3.5 and the rootfs is debian:trixie which ships it, but the capability is PROBED, not assumed — guessing wrong would fail every attempt and brick the node, the exact outcome all of this exists to prevent. Without the flags we simply do not backdate and rule 2 still covers the dangerous case. 2. Refuse to bless a cert dated by a clock outside a plausible window (2026-01-01 .. 2056-01-01). The material stays installed so the node is usable and sshd comes up, but the bad dates are recorded as failed=cert-dates and the cert is regenerated automatically once time syncs. Generation is now driven by need rather than by "is the marker absent", and ConditionPathExists=! is removed from the unit so a node that already completed can still be re-examined — skipping the unit is precisely how such a node stays broken forever. The script exits in milliseconds when everything is fine. Anti-spin is one condition: a date-driven regeneration happens ONLY when the clock is currently plausible. A node whose clock is still wrong re-checks and mints nothing. Regression caught while writing this: driving generation purely by content made needs_ssh() false whenever any host key existed, which would have left an image-baked fleet-shared key in place forever — F-03 reopened. The marker check is back in both needs_ functions and case 1 (which prestages a baked key and asserts it was replaced) is what caught it. Case 8 covers mint-under-wrong-clock, repair-after-sync, and both spin directions. Controls: blessing regardless of clock reddens only case 8 (run1-BAD-DATES-NOT-RECORDED); removing the anti-spin guard reddens only case 8 (SPINNING-reminted-while-clock-still-wrong(1->2)). The second control initially passed against a broken guard because the assertion compared certificate dates, and a re-mint under a frozen clock produces a byte-identical notBefore — the assertion now counts mints, which is the only thing that distinguishes "left alone" from "regenerated again". Not covered here: nodes already deployed from earlier ISOs never receive this script (it is installed by the installer, not by OTA), so fleet remediation for them remains 10-04/OTA work in core/**, which is held by other executors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9622926868 |
fix(10-05): delete the Bitcoin Core wallet path that duplicated the spending key (F-13, D-07b)
`handle_bitcoin_init_wallet_from_seed` derived the BIP-84 account extended *private* key, stringified it, and imported `wpkh(xprv/0/*)` / `wpkh(xprv/1/*)` into a Bitcoin Core descriptor wallet created with `disable_private_keys=false` and an empty passphrase. That put a second copy of the node's spending key in Core's `wallet.dat`, outside the daemon's Argon2 + ChaCha20-Poly1305 envelope. That duplication into weaker protection was audit finding F-13 (High). Deleted rather than rewritten watch-only (D-07b supersedes D-07/D-07a): - No caller anywhere. Repo-wide search leaves exactly one occurrence of the method name (its own dispatcher registration) and two of the symbol in code (definition + dispatch call); every other hit is prose in docs. - LND is the wallet the product drives. Across neode-ui/src every `bitcoin.*` call is read-only status (getinfo/prune-status/onion); the wallet UI sends via `lnd.sendcoins`. - It never ran on archi-dev-box: no wallet named `archipelago` exists there, and the one loaded wallet reports blank=true, keypoolsize=0, txcount=0. - It was authenticated AND password-gated, so F-13 was key-at-rest duplication, not an exposed endpoint. No migration is performed and none is planned. This removes code, not wallets: nothing on disk is touched, no funds move, no wallet.dat is modified. If a node is ever found holding a wallet this handler created, that is a finding to surface and stop on, not a trigger to auto-migrate. `seed::derive_bitcoin_xprv` loses its only non-test caller and is retained deliberately with `#[allow(dead_code)]` and a stated reason: it keeps its existing test coverage and it is the derivation D-07c's deferred BDK cold vault will need. Records the evidence, the D-08/D-09 consequences and the D-07c deferral in docs/security/KEY-03-SIGNING-POSTURE.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8255b69af2 |
fix(01-17): pin the FIPS/Tor pills and let the peer-card badge row wrap (UIFIX-01)
Audited every transport-pill render site in the cloud surfaces at 390x740
and 320x640 in a real browser. Two sites render a pill (Cloud.vue peer
cards, PeerFiles.vue header) and both already appear on a phone; three
file-level sites carry none, by decision recorded in the SUMMARY.
- Cloud.vue peer-card badge row: add flex-wrap + shrink-0 on the transport
badge. Measured at 320px, a longer trust label squeezed the badge until
its own text broke mid-label ("TOR ." / "120.0s"). It now drops to a
second line intact. Inert whenever the row fits, so desktop is unchanged.
- New TransportPills.test.ts: one site-specific assertion per render site,
so removing a pill fails the build. Dorian asked that these never be
removed in a future cleanup; nothing in the repo pinned them before.
- Unknown-transport cases assert no pill is fabricated (T-01-78), and the
labels/colours are asserted against PeerFiles.vue's canonical mapping
rather than a duplicated table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d9b3a7d5e0 |
fix(10-03): quote the Dockerfile heredoc so comments cannot execute
`cat > "$WORK_DIR/Dockerfile.rootfs" <<DOCKERFILE` was unquoted, so the build shell performed command substitution on the Dockerfile body. Any backtick in a Dockerfile COMMENT was executed on the build host and its output spliced into the generated file. Six comments did this. One of them ran `systemctl start archipelago-fips.service` against the build machine on every ISO build; the others were harmless only by accident of being command-not-found. Fixes the class, not the six instances. The delimiter is now quoted, so the body is emitted verbatim and a future backticked comment is inert. Verified the boundary by line range first: the other backticked comments in this file (:264, :809, :1188, :1289, :1506, :1605, :3597, :3651) are ordinary shell comments outside any unquoted heredoc and were never at risk — they are untouched. The body needs exactly four build-time values and they are all package names (LINUX_IMAGE_PKG, GRUB_EFI_PKG, GRUB_EFI_SIGNED_PKG, GRUB_PC_PKG), on four consecutive lines. So quoting was practical: the heredoc is split into DOCKERFILE_HEAD and DOCKERFILE_TAIL, both quoted, with a single explicit printf interpolating those four names between them. Escapes that existed only because the heredoc was unquoted are undone in the same pass: six trailing `\\` become `\` (Docker line continuations) and four `\$` become `$` (RUN arguments reach the shell verbatim — Docker does not substitute variables in RUN). Verified by rendering the generated Dockerfile before and after with the same inputs and diffing them normalised (continuations joined, whitespace collapsed). Both are 190 normalised lines and the ONLY differences are the six comments regaining their text — every instruction is byte-identical. Before: "# the archipelago backend calls" / after: "# the archipelago backend calls `systemctl start archipelago-fips.service`". Test: case 7 asserts every heredoc writing Dockerfile.rootfs has a quoted delimiter, and when one is not, reports which body lines would execute. The assertion is on the delimiter, not on backticks — with quoting a backticked comment is legal and six of them are back in the body on purpose, so flagging backticks would flag a non-bug and fail on the very comments this restored. This bug is invisible to `bash -n`; an instance of it introduced earlier in this plan hung a syntactically-clean build for two minutes before being caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2efab5f219 |
fix(10-03): unify secret generation to a single producer + self-heal (F-03)
Unify rather than delete. The defect in F-03 was never "a second attempt to
create a key exists" — it was that failure was silent and the completion marker
lied about it. A second attempt is only dangerous when it is an unaudited second
PRODUCER carrying its own idea of success, its own absent retry policy and its
own absent failure record.
Single producer. gen_tls() is now the only code in the ISO build that creates
/etc/archipelago/ssl/archipelago.{key,crt}; gen_ssh() the only code that creates
/etc/ssh/ssh_host_*. Two secondary producers are gone:
- the Dockerfile's `openssl req` layer, which baked a keypair the strip layer
deleted moments later in the same build;
- the installer's "ensure SSL cert exists for nginx HTTPS" block, which before
the strip almost never fired and after it would have fired on every install.
Proof is mechanical, not a claim: every executable `openssl req` / `ssh-keygen
-A` invocation in the builder now lives inside the generator heredoc, and the
test suite fails if one appears outside it.
Build-time assertion. The one realistic total failure is a missing generator
binary, which is deterministic — no retry or reboot fixes it. A rootfs RUN layer
now fails the build if openssl or ssh-keygen is missing or non-executable.
openssl and openssh-server are both already in the package list (and
openssh-server hard-depends openssh-client, which ships ssh-keygen), so today
this is cheap insurance; it earns its place the first time someone edits that
list.
Self-heal, never dead-end. Fail-closed governs SERVING; retry governs
RECOVERING, and they are different things. Adds
archipelago-first-boot-secrets.timer (OnBootSec=5min, OnUnitActiveSec=15min),
installed and enabled with a hand-written symlink fallback because chroot
systemctl enable can fail silently. The service's own ConditionPathExists=!
makes every trigger a no-op once the marker exists, so a healthy node pays
nothing. On success the script now restarts consumers that are in `failed` —
try-reload-or-restart is a no-op on a failed unit, so without this a recovered
node would have valid keys on disk and nginx still down.
Never serve a bogus key. gen_tls parses both halves back with `openssl pkey`
and `openssl x509` before the swap, so a truncated or half-written artefact is
never what nginx reads.
Tests: 6 cases, each with an isolated negative control (transcripts in SUMMARY).
- case 4, TLS fails every attempt on a stripped root -> no key from any source.
Control: reintroduce a fallback key creation -> only case 4 red.
- case 5, self-heal: a failed run then a later successful run -> key present,
marker set, failed units restarted. Control: dead-end on a node that already
failed -> only case 5 red.
- case 6, single-producer invariant. Control: reintroduce the installer block
-> only case 6 red, naming the line.
Residual risk, stated plainly: a machine where generation can never succeed
still ends up with no SSH and no TLS. Build-time assertion removes the
deterministic cause, retry plus timer removes the transient ones, so what
remains is genuinely broken hardware — and it says so on the console and in
/var/lib/archipelago/first-boot-secrets.failed rather than quietly serving a
key nobody audited.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ff6902dd9d |
docs(10): add independent verification guide for auditors
A guide a third-party security auditor can use to verify Phase 10's claims without trusting our test harness — and that we use ourselves. Every claim carries four parts, all required: the claim stated falsifiably; how to REPRODUCE THE DEFECT on the parent commit; how to verify the fix; and a negative control that must go red on exactly that defect and nothing else. A test passing on both fixed and unfixed code proves nothing, and reproduce-first is the step most often omitted in security theatre. Prefers external checks (curl from another host, tar listing, cross-node file comparison) over our own tests wherever a claim can be checked from outside. Tiered by hardware needed: Tier 0 any checkout, Tier 1 running node, Tier 2 ISO build host, Tier 3 two physical nodes, Tier 4 pre-release gate. Status marked per claim — verifiable now, pending a plan, or hardware-gated — so an unmarked absence is never read as a pass. States what is explicitly NOT claimed (Lightning custody is not air-gappable; no claim against a compromised kernel CSPRNG or supply chain; KEY-05 is structural not exploitable), the known-accepted risks with where each was decided, and carries the C-6 warning that probing with seed.status reports the surface closed while the real door stands open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
201ef474e7 |
docs(10-03): record C-4 build-host evidence procedure (UNVERIFIED)
Task 3 of 10-03 is a blocking checkpoint: proving the shipped rootfs tar is identity-free needs a real ISO build host with podman/docker and disk for a full rootfs rebuild. This commits the prepared evidence document with the exact command sequence, marked UNVERIFIED, rather than claiming the check passed. The document states the inverted expectation explicitly. The audit's C-4 entry expected SSH host keys and the TLS key to be PRESENT — that described the broken state it was measuring. After the strip layer those must be ABSENT, so the audit's stated expectation is now the failure condition. A future reader comparing the two would otherwise conclude the check regressed. Also records two things the operator would otherwise get wrong: - RECIPE_HASH must be read from the stamp file, not computed from the repo file. build-debian-iso.sh rewrites the builder's relative paths into a temp copy before exec, and the hash covers "$0"; the hashed region has 35 such rewritten expressions plus an absolutised SCRIPT_DIR, so the value is specific to the build host and checkout path. - C-4 is a build-host check only. Two-node key divergence is C-3 and stays separately UNVERIFIED; the note explains why SSH host keys are the sharper signal there than TLS, given the installer's per-install TLS fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
408b328c39 |
fix(10-03): strip fleet-shared identity material from the rootfs tar (F-03)
The rootfs is a container image exported to a tar and extracted verbatim onto every disk flashed from the ISO, and the ISO is published. It baked two things nobody asked for: Debian's openssh-server postinst generates /etc/ssh/ssh_host_* during the container build, and the `openssl req` layer writes the TLS keypair. Both were therefore identical on every node and known to every downloader. Add a final RUN layer to Dockerfile.rootfs that removes /etc/ssh/ssh_host_*, removes the archipelago TLS keypair (keeping the ssl directory so the first-boot staging swap has somewhere to land), truncates /etc/machine-id to systemd's documented "regenerate on next boot" state, and drops a non-shared /var/lib/dbus/machine-id if one exists as a real file rather than a symlink. It also writes /opt/archipelago/rootfs-identity-stripped so a node can answer after the fact whether its rootfs came from a stripped build; no timestamp, so the RECIPE_HASH cache stays reproducible. This is what makes 10-03's fail-closed regeneration structural instead of procedural: with the material gone, a regeneration failure degrades to "no key, service refuses to start" rather than "fleet-shared key, silently". The `openssl req` layer is deliberately left in place — it keeps proving openssl is present and keeps the SAN template next to its consumer; the strip layer is what makes the output non-shared. Two comment corrections that follow from the strip: - The installer's TLS block is no longer a rarely-taken safety net; it now fires on every install. It is per-install and never image-wide, so it does not reopen F-03, but it does mean a first-boot failure still leaves the web UI with a cert while SSH has nothing. Comment updated to say so. - The first-boot script header overstated the fail-closed cost for TLS for the same reason; corrected to claim certainty only for SSH. This edit is inside the RECIPE_HASH region, so the next build is forced to rebuild the rootfs tar — required for the C-4 evidence to mean anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
210430967d |
fix(10-03): fail closed on first-boot secret regeneration failure (F-03)
The first-boot per-device secret regeneration was fail-open: both branches logged a warning and continued, and `touch "$MARKER"` ran unconditionally outside both `if` blocks. Combined with the unit's ConditionPathExists=! and the script's own marker fast-path, one transient failure left that node on the image-wide shared SSH host key and TLS private key permanently and silently — and the ISO is a published artefact, so every downloader holds those keys. - Retry each generator 3 times with backoff (D-05), so a transient first-boot condition recovers inside the same boot instead of being terminal. - Write the completion marker ONLY when both TLS and SSH succeeded, so a failed boot leaves the unit eligible to run again on the next boot. - On terminal failure: durable record at /var/lib/archipelago/first-boot-secrets.failed naming which generator failed, plus console + logger + stderr, and exit 1 so the unit lands in `failed` rather than `active`. The record is cleared on a later success. - Add FIRST_BOOT_SECRETS_ROOT / FIRST_BOOT_SECRETS_BACKOFF seams. Unset in production the behaviour is byte-identical; set, they let the fail-closed property be asserted rather than claimed. - Order the unit After=systemd-random-seed.service (no-op today, correct if a seed file is ever baked). - State the operational trade in the script header: after the rootfs strip, a terminal failure means no SSH and no TLS and needs the physical console. That was chosen deliberately over running on fleet-shared keys. tests/first-boot-secrets/run-tests.sh extracts the shipped heredoc body from the builder and drives it against a temp root with stubbed generators: both succeed, openssl fails every attempt, ssh-keygen fails twice then succeeds. Moving the marker touch back outside the success branch makes case 2 fail with MARKER-SET-ON-FAILURE, which is the regression this pins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c502ff0e0a |
docs(todo): migrate VPS2 IP to domain across registry references
98 operational files still carry 146.59.87.168 — the domain was only adopted for the git remote, not for container registry references. Bulk is app manifests' image: lines, plus .gitmodules, both CI workflows, the signed catalog.json, and two Android companion files with compiled constants. The 117 hits in .planning/ are historical records and stay. Not a find-and-replace: the domain serves Gitea over HTTPS:443 while images are pulled from :3000 over plain HTTP, and podman treats host:3000 and domain as different registries — so every node re-pulls under the new name and any node that can't resolve or trust the new host fails to pull. It also invalidates the signed catalog (needs a re-sign ceremony) and the APK ships compiled constants. Rollout order: registry serving on the domain → manifests → catalog re-sign → APK rebuild. Steps 2-4 are actively breaking until step 1 holds. Analysis from the concurrent agent's session before it ended; recorded so it is not lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5a11fa7588 |
docs(todo): capture companion 0.5.27 handover — node/web-side clipboard + QR work
Companion build 0.5.27 (versionCode 47) shims navigator.clipboard natively, so in-app copy/paste is fixed with zero web changes — but the contract must not be clobbered (no unconditional re-define, no Object.freeze). Still open web-side: main.ts's fake readText() makes SendBitcoinModal's Paste button render and silently no-op in plain-HTTP browsers; 30 writeText call sites across three inconsistent patterns, ~10 of which toast 'Copied!' regardless of success; scanner prewarm/torch/constraints/no-reinit. Also records three factual corrections to docs/qr-scanner-snappiness-handover.md (ZXing not ML Kit; FORMAT_QR_CODE + KEEP_ONLY_LATEST already in place; do NOT drop to 720p — 1080p is a deliberate 0.5.22 fix for dense bolt11 QRs). Routed at Phase 11: the signed-PSBT paste affordance and the scanner items are the same surface as WALLET-05. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
204d4523da |
fix(ui): a modal must not outlive the screen that raised it
Demo images / Build & push demo images (push) Successful in 3m20s
Clicking "Open a channel" or "Setup Guide" navigated correctly but left the wallet's send/receive modal floating over the destination. The Lightning modal itself did close — the parent did not. Tab views are KeepAlive'd, so navigating deactivates the owner rather than unmounting it, and its Teleported modal keeps rendering. BaseModal now emits close on any route change while shown, fixing the class in one place rather than per button. Every modal here is a transient dialog; none should survive navigation. Two tests pin it, including that a hidden modal stays quiet. Also fixes a test-only regression from |
||
|
|
c3d5bcd271 |
fix(wallet): gate lightning on CHANNELS, not just node state
Demo images / Build & push demo images (push) Successful in 3m24s
A running LND with zero channels happily mints an invoice — it is simply unpayable, because nobody has a route in. So the state-only gate let receive through and handed the user a useless invoice, and let send walk to confirm. Neither errored, so the funding modal (wired to failures) never fired. requireLightningReady(direction) now asks lnd.listchannels and checks the liquidity that actually matters for the attempt: total_inbound to receive, total_outbound to send. It fails OPEN on an RPC error — a transient blip should not block a working wallet. The no-funds mode says plainly that a channel is needed, in the direction's own terms (inbound vs outbound), and offers both routes: "Open a channel" straight to the channels screen where the Zeus/Olympus flow is already prefilled, and "Setup Guide" to the run-lightning-node walkthrough for someone who wants the whole path explained. Buttons wrap rather than squeeze on narrow screens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
700947ea3c |
feat(wallet): app icons, mobile layout, and a funding mode on the Lightning modal
Demo images / Build & push demo images (push) Successful in 3m18s
Icons: each node choice now shows its app icon (lnd.png; Core Lightning's is vendored from the Umbrel gallery as core-lightning.svg). Vendored rather than hotlinked on purpose — these nodes run offline/airgapped, and a remote image would both break there and leak a request to a third-party host on every render. A missing asset falls back to a neutral bolt glyph so a row can never render a broken-image box. Mobile: the choice row keeps icon + name + blurb together and drops the action to its own full-width line under 26rem, instead of squeezing the description into a two-word column next to a button. Funding mode: a node that is running but has no funds / no inbound liquidity is neither "install one" nor "start it", so the same modal gains a third mode that explains it and routes to the run-lightning-node goal, where funding and channel-opening already live — reusing that flow rather than duplicating it. It fires where the user actually meets the problem: on a failed attempt. handleLightningFailure() maps a running node's send/receive failure onto the funding modal, matched on message text because LND surfaces "no route", "no channels" and "insufficient balance" as plain strings with no distinct code — and all three mean the same thing to a user: fund me. Verified: 5 gate tests; npm run build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fa26c5fc56 |
fix(wallet): gate lightning on node STATE, gate send too, add a shared CopyButton
Three defects from testing the previous commit on archi-dev-box: 1. The gate keyed on `id in packages`, which is not "installed and usable" — package-data carries an entry for a Lightning app that is known but not running. On a box with no lnd container at all the gate passed and the raw error came through as "Operation failed. Check server logs for details." Now keyed on PackageState.Running. 2. Because installed-but-stopped is a real and different situation, the modal has two modes: absent offers the install choices, stopped says the node isn't running and offers "Open My Apps". Neither dead-ends in an error. 3. Lightning SEND let you walk all the way to confirm-send with no node. The gate now runs in review(), before the confirm step — failing at submit after a review screen is the defect, not a smaller version of it. Also adds CopyButton, the start of one consistent copy affordance: icon + label, an emerald tick held 1.6s, a fixed box so the width never jumps, and a document.execCommand fallback so copy still works over plain http on a LAN IP (navigator.clipboard rejects on insecure origins, which is how a lot of nodes are reached). Converted the wallet's own copies — the lightning invoice the user reported, plus the on-chain/Ark addresses and the payment hash/txid. 20 of 25 copy sites across 15 other files still use ad-hoc markup; converting them is mechanical but was not attempted here rather than half-done. Verified: 5 gate tests; npm run build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
90ce4bcc46 |
docs(10): correct F-10a's own overstatement — x3dh sites are identifiers, not key material
The F-10a scope correction committed hours earlier asserted semantics its evidence did not support. The KEY-05 planner caught it against the code: - mesh/x3dh.rs:100/:114 are u32 prekey IDENTIFIERS (spk_id, otk_id), not key agreement material. The X25519 secrets come from crypto::generate_x25519_ephemeral() at :99/:113 and were never in scope. - session.rs's 16 raw matches read as 16 production token sites; #[cfg(test)] begins at :470, so it is 4 production + 12 test. - wallet/bdhke.rs is 2 production of 4 (#[cfg(test)] at :143) — and those two ARE genuine key material: generate_secret() :133 and random_blinding_factor() :139. The Medium rating still holds, on narrower grounds: bdhke's two production sites plus storage_crypto.rs:39's AEAD nonce. It no longer rests on x3dh. Struck rather than silently rewritten. F-10 was corrected on the grounds that understatement misleads the next reader; overstatement does the same, and this table managed both within a day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dba3a30af9 |
docs(10-06): plan KEY-05 crate-wide defaulted-RNG enforcement
Adds 10-06-PLAN.md covering KEY-05 (F-10a / R-16). 10-01..10-05 untouched. Six tasks, sequenced so CI stays green at every intermediate commit: classify all 43 call sites with file:line evidence; a tracer that wires the sealed KeyGenRng allowlist, the degenerate-entropy predicate and the CSPRNG readiness ledger end-to-end through the mnemonic seam; two migration tasks; a blocking human checkpoint for cargo-deny scope and legitimacy; then the gates are enabled last and observed failing a real build. The clippy ban is a compile failure under the existing -D warnings CI step, so core/clippy.toml is deliberately not committed until every site -- including test code, since --all-targets counts it -- has migrated. Wave 2: shares seed.rs with 10-05 and api/rpc/auth.rs with 10-01. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5718179e2f |
feat(wallet): offer to install a Lightning node instead of failing an invoice
Demo images / Build & push demo images (push) Successful in 3m16s
Creating a Lightning invoice with no Lightning implementation installed failed
at the RPC layer — lnd.createinvoice returned connection-refused and the
Receive screen rendered it as a red error. That reads as the wallet being
broken when the node simply has no Lightning node installed yet.
useLightningRequired() gates the three invoice paths (wallet Receive, the Web5
send/receive sheet, and the app launcher's paywall — both arms there, since
paying an invoice needs a node as much as minting one). With none installed it
raises a modal offering to install one and the caller bails without surfacing
an error at all.
The modal lists the choice rather than assuming LND: LND installs today, Core
Lightning is listed greyed as "Coming soon" so the platform doesn't read as
LND-only. When CLN ships it is two lines — flip `available` and add the id to
LIGHTNING_NODE_APP_IDS.
Detection is install state, NOT reachability, deliberately: an installed node
that is merely stopped or still starting is a different problem ("start it")
and must not be answered with "install a Lightning node".
Also fixes the credentials modal, which painted its own rgba(8,10,18,.98)
navy card instead of the house glass-card — it read as blue against every
other modal. It existed twice (Apps.vue and apps/AppIconGrid.vue); both now
use BaseModal, so they also inherit Esc/focus handling, body scroll lock and
the standard pinned-header/footer scroll contract they were missing. Dead
panel CSS removed from both.
Verified: 4 new tests; full suite 103 files / 826 tests green; npm run build
clean with the new strings present in the built bundle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
06e0e6954e |
fix(01-16): recreate the gateway when its credential was rotated (FED-07)
The checkpoint on archi-dev-box proved rotation alone doesn't close FED-07: the credential file went unique while the RUNNING container kept serving the compromised one, because the Quadlet path rewrites a unit without restarting it and fedimint-gateway is classified restart-sensitive, so drift was detected and deliberately ignored on every tick. Rotation now records the app id, and the drift check consumes that flag to recreate even a restart-sensitive app, with a WARN naming the reason. This mirrors the published-port carve-out a few lines above, which already makes the same trade for the same reason: a container that is already broken (there) or already compromised (here) is not protected by leaving it running. Restart-sensitivity protects working services. A gateway answering to a credential published in this repository is not working, it is compromised, and gateway admin can drain Lightning liquidity — indefinite exposure loses to a few seconds of restart. Rotating-but-only-alerting was rejected: the monitoring system fires on metric thresholds only, so it would have needed new event-alert plumbing to deliver something strictly weaker. Re-verified on the same node, same scenario: rotation at 06:39:23, recreate at 06:39:27, PID 3923125 -> 148426, running credential now matches the file, container healthy with the same name and ports, gatewayd.db intact at 18 files with IDENTITY present, 32 containers untouched, no repeat rotation. 3 new tests. Also lands the missing 01-19 and 01-20 SUMMARYs: both had code committed 2026-07-31 but no summary and no roadmap tick, so they read as unstarted. Phase 1 is 11/20. FED-09 carries 15h of Tor uptime and 0 permission-fixes across 542 doctor runs on archi-dev-box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5cf44c9a58 |
docs(10): correct F-10's scope and add KEY-05 — crate-wide CSPRNG enforcement
The audit recorded F-10 as two call sites in container/secrets.rs. The real defaulted-RNG surface is 41 sites across 15 files: session.rs (16), pine_ha.rs (6), wallet/bdhke.rs (4 — ecash key material), mesh/x3dh.rs (2 — key-agreement material), storage_crypto.rs (1 — AEAD nonce), +10 more. Nothing is broken today: rand::random()/thread_rng() are ChaCha12 seeded from getrandom(2). What changes is blast radius — F-10's Low rating rested on 'per-app credentials rather than the master key hierarchy', which does not survive the true scope. Re-rated Medium as F-10a. Records why the original audit missed it: F-10 was reached by tracing the manifest-secrets path, and no step enumerated defaulted-RNG use across the crate independently of the traced paths. F-10's original text is left unedited so the correction is auditable rather than retroactive. R-13 superseded by R-16; tracker item replaced. Adds KEY-05 to Phase 10: sealed allowlist trait at key-gen seams, clippy disallowed-methods ban (compile-time, CI-enforced), cargo-deny on duplicate rand majors, degenerate-entropy runtime check, persisted CSPRNG-readiness verdict. Also retires the false 'impl CryptoRng for CountingRng' at seed.rs:656. Records the user's execution gate: Phase 10 does not start until the concurrent Phase 1 agent is finished and their changes are synced. KEY-05 is unplanned — the existing 5 plans predate it and a 6th is required. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f9b0659c4a |
wip: save session context — BotFights demo-prep work, off-plan (Phase 09 already complete)
Handoff for the reactive demo-day session that followed 09-06/09-07 (both already complete). Covers: security audit (6 IDOR fixes across the botfight repo), Cashu payout claim UI, existing-bot AI-config UI, botfights 1.2.11 built+deployed to both demo nodes, catalog signed+published. Also: discovered and fixed 4 botfight-repo commits that were local-only and never pushed to origin — pushed as part of this handoff step (botfight @ d00e792..10d4209 -> origin/main). Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
23ae86c13a |
docs(state): record Phase 10 planned and ready to execute
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1623b4f764 |
docs(quick-260731-upz): close out the entropy audit — research, summary, follow-up todo
The executor was instructed to leave docs artifacts to the orchestrator; this commits them: the research that drove the audit, the task summary, and the archi-dev-box test-node todo raised during the same session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
56f7b367ff |
docs(01-16): checkpoint FAILED on archi-dev-box — rotation never reaches the container
Ran Task 2's blocking checkpoint on a real node. The rotation works and was
proven end to end: it fired ~15s after restart, wrote a fresh unique
credential (0600, service-owned), logged exactly one line naming the .pw path
with no value in it, left every other secret and the gateway's data untouched,
and kept the container's name and ports.
But the assumption the plan rests on is WRONG, and the checkpoint is what
caught it. 25 minutes after rotating, /proc/<pid>/environ showed the running
gatewayd still using the PRE-ROTATION credential while the file and podman
secret held the new one. The orchestrator explains itself in its own logs:
Quadlet unit drift-synced — file rewritten, .service NOT restarted
(operator restart picks up new config)
container drift detected during boot reconcile;
leaving running restart-sensitive app untouched
Two deliberate guards: the Quadlet path never restarts a unit it rewrites, and
fedimint-gateway is classified restart-sensitive so drift is detected on every
tick and then ignored — logged at 15:51, 15:53, 15:54, 15:56 and counting.
So on a real affected node the credential file becomes unique while the
gateway keeps answering to the compromised one until an unrelated reboot, and
the operator reading the .pw gets a password the gateway rejects — T-01-77
inverted. FED-07 is NOT closed and this plan alone cannot close it.
Not hand-rolled around, per the plan's own instruction. The fix needs a design
decision: whether a compromised credential is the case that should override
restart-sensitivity, or whether rotation must raise an operator-facing
"restart required" alert instead of logging into the void.
Incidentally disproved: restarting archipelago does NOT kill containers here
(29/29 then 31/31 survived; "Adopted 31 existing container(s)"). The service
is system.slice/KillMode=control-group while containers live in
user-1000.slice/…/libpod-*. The CLAUDE.md SIGKILL rule predates Quadlet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
095664a8a7 |
docs(01-16): hold FED-07's on-node checkpoint, correct its requirement status
FED-07 was marked Complete when 01-11 landed, which was premature: the requirement text explicitly includes "existing installs with the default password get a migration path", and that migration has never been exercised on a node. Corrected to code-complete/verification-pending. Checkpoint step 1 was run read-only on archi-dev-box: the node is CLEAN (hash present, 600, service-owned, not the shipped default) and has NO gateway container — the app is installed but nothing runs and its data dir is empty. So rotation cannot fire naturally here, and the steps that matter most (data survives the recreate, new credential authenticates, old one rejected) have nothing to exercise without installing and seeding first. Deferred deliberately rather than run unattended: 30 containers are up with 4-8 days uptime (IndeeHub, Immich, BTCPay, netbird, strfry, …), the archipelago system service is active, and restarting it SIGKILLs containers until Quadlet is the default. The todo carries the full context plus two adjacent findings: fedimint-gateway is missing from handle_package_credentials (so a rotated password has no UI retrieval path), and photoprism ships a fixed admin password in its manifest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9e2d2ef236 |
feat(01-16): rotate existing installs off the shipped gateway credential (FED-07)
01-11 stopped new installs from ever taking a shipped credential, but did
nothing for the nodes that already did — those gateways still answer to a
password published in this repository.
rotate_compromised_gateway_credential() detects an EXACT match against the
denylist and replaces the pair; absent, unique, or merely unrecognised values
are left alone and return false. That distinction is the point: an operator
who deliberately set their own credential also has an "unrecognised" one, and
rotating it would be the same class of harm as leaving the default in place.
It hangs off resolve_dynamic_env beside ensure_generated_secrets, gated on the
gateway's app id, so an affected node heals on its next reconcile tick. There
is deliberately no teardown here: the new hash changes the resolved secret env,
which changes secret_env_hash, which the drift check reads as a container-label
mismatch — so the platform's own recreate path rebuilds the gateway around its
unchanged data directory, ports, volumes and name.
Rotation is self-terminating (the value written is not on the denylist, so the
next tick is a no-op) and errors propagate rather than being swallowed, because
the atomic write leaves the previous credential intact on failure.
Bcrypt generation was factored out of ensure_one into write_bcrypt_pair, which
both generation and rotation call — 01-11's SUMMARY claimed such a helper
existed but the arm was still inline, and rotation cannot reuse
ensure_gateway_credential because its idempotent fast path returns early
exactly when the file is present, which is the case rotation acts on.
Also fixes cargo fmt drift left by
|
||
|
|
d238bad012 |
docs(roadmap): add WALLET-05 — make the PSBT air-gap round trip real
The two-scan dance (node shows unsigned PSBT as animated QR, signer signs, node scans the signed PSBT back, finalize + broadcast) is ~80% plumbed and 0% usable. Verified gaps: - No UI: lnd.create-psbt / lnd.finalize-psbt and their rpc-client.ts:417 wrappers are called by nothing but unit tests. - No animated-QR encoder: qrcode/qrloop are deps and the inbound path (useAnimatedQRDecoder + WalletScanModal) works, but nothing encodes a PSBT. - Wrong format for real signers: qrloop is Ledger's; Passport/SeedSigner speak BC-UR (ur:crypto-psbt), Coldcard Q speaks BBQr. BC-UR is the priority given the existing Passport-Prime-compatible SeedQR work. Gated on 10-05: create-psbt funds from LND's own wallet, so until LND is watch-only against the external signer the offline device signs inputs whose keys the node already holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6c821730b0 |
docs(roadmap): add Phase 11 — Wallet Experience & LND UI Parity
First-run wallet-type chooser, seed handling reusing the shipped SeedQR + seed-words components, and evidence-based umbrelOS LND UI parity. Gated on Phase 10's 10-05: the set of wallet types WALLET-01 can offer is a direct consequence of the watch-only verdict that plan produces, and 10-05 also deletes the dead Core wallet path so this phase never represents it in the UI. Records the already-shipped inventory (channels panel, send/receive/scan/ settings modals, SeedRevealPanel, LndSeedBackupPrompt, utils/seedqr.ts) so the parity matrix closes real gaps instead of rebuilding existing surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2781bd5a6c |
docs(10): correct D-03's gate signal — key_exists is true on every booted node
D-03 named NodeIdentity::key_exists as one of the two gate signals. Server::new (server.rs:63-72) calls load_or_create on both branches, and load_or_create (identity.rs:48-51) generates and writes a random temporary node key when none exists — so key_exists is true on any node that has booted once, onboarded or not. A gate keyed on it would refuse seed.generate on a fresh node and brick onboarding fleet-wide. The flaw came from the audit's own suggested remediation (§214-221) and was repeated in the planning brief; the planner caught it against the code. D-03's intent (two signals, OR-ed, fail safe on drift) is unchanged. Corrected signal set: is_setup() / is_onboarding_complete() / seed_exists(), pinned by a test rather than a comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
04c4d3460c |
docs(10): plan Key-Material Hardening — 5 plans, 2 waves (KEY-01..KEY-04)
Closes the three exploitable findings from the 2026-07-31 entropy/seed audit. Wave 1 (parallel): - 10-01 KEY-01: shared onboarding gate refuses seed.generate/seed.restore/ seed.save-encrypted/backup.restore-identity/auth.setup on a provisioned node, plus an auth.onboardingComplete guard and retry-budget-derived rate limits. Independently shippable (D-11): no depends_on, no shared files. - 10-03 KEY-02: first-boot secret regeneration retries with backoff then fails closed; rootfs tar ships identity-free so failure degrades to "no key". - 10-05 KEY-03: delete the uncalled bitcoin.init-wallet-from-seed xprv-import path (D-07b); make LND's PSBT round trip first-class with a key-origin report. Wave 2: - 10-02 (deps 10-01) KEY-01/KEY-04: on-node C-6 exposure measurement, live refusal proof, fresh-node onboarding non-regression. - 10-04 (deps 10-03) KEY-02/KEY-04: fleet detection of image-baked host secrets, guarded rotation behind a D-06 decision checkpoint, C-3 two-node verification. Planning-time scoping correction recorded in 10-01: D-03 names NodeIdentity::key_exists as the on-disk "onboarded" signal, but server.rs:63-71 writes a temporary node_key on every boot, so that signal is true on fresh nodes and would brick first-boot onboarding. D-03's dual-signal intent is preserved with is_setup / is_onboarding_complete / seed_exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
97b8fa485d |
docs(10): KEY-03 final scope — delete Core wallet path, harden LND PSBT
Core's wallet is outdated and used by nothing, so bitcoin.init-wallet-from-seed is deleted outright rather than migrated: uncalled, authenticated and password-gated, never ran on archi-dev-box, and its only job is deriving and stringifying the master BIP-84 xprv. D-07's parity-proof migration and its one-way checkpoint are withdrawn — there is no wallet to migrate. A small discovery check folds into KEY-04; a wallet found there is a finding to stop on, not an auto-migration trigger. PSBT is already solved by LND and already implemented: lnd.create-psbt (WalletKit FundPsbt) and lnd.finalize-psbt (finalize + broadcast), both rate-limited, on LND v0.18.4-beta. KEY-03 becomes: delete the Core path and make that flow first-class, tested and documented, including that the PSBT carries the BIP-32 key-origin data a hardware signer needs. Records the standing honesty constraint that Lightning channel/revocation/HTLC keys are not air-gappable at all, and defers the BDK+ElectrumX cold vault to its own phase (D-07c). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cb021a991c |
docs(10): record KEY-03 scoping correction — Core wallet path is uncalled
Bitcoin Core's wallet is legacy and unused: bitcoin.init-wallet-from-seed has no caller outside its dispatcher registration, the wallet UI is LND-only (lnd.sendcoins/estimatefee/getinfo), archi-dev-box has no bitcoin/wallets/ dir so the handler's named descriptor wallet was never created there, and the endpoint is authenticated + password-gated so F-13 was never remotely reachable. F-13 is therefore latent, not live. D-07's migration premise is unproven, so KEY-03 is re-scoped discovery-first: check the fleet for any wallet this handler created before planning any migration. Migration + checkpoint stay, conditional on discovery. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
90884e6259 |
feat(01-02): chat mutations mutate demo state instead of acking (FED-04)
Demo images / Build & push demo images (push) Successful in 4m21s
Reactions, replies, read-receipts, edits, deletes, forwards and channel sends
shared one bare `{ ok: true, sent: true }` case, so none of them rendered on
the demo — the UI derives reaction chips and reply quotes from the message
store, and there was nothing in it to derive from.
Each now mirrors its daemon counterpart. Reactions/replies/receipts push typed
messages carrying the { sender_pubkey, sender_seq } target key Mesh.vue's
reactionIndex and replyTargetPreview read. Edits rewrite the text and set
edited_at; deletes tombstone IN PLACE (plaintext, typed_payload.deleted,
message_type 'delete') because that is what mesh/mod.rs apply_local_delete
does — it does not remove the row.
Edits and deletes go through a per-session overrides overlay keyed by
sender_seq, because mesh.messages rebuilds its seed array on every read, so
in-place mutation would only ever work for messages sent this session.
mesh.refresh and mesh.reboot-radio stay acknowledgements on purpose — the
daemon's handlers have no message-store effect either — with a comment saying
so, so a later reader does not "fix" them into divergence.
Also completes the phase bookkeeping for 01-02/03/11/12/13/14/15 and lands the
orphaned 01-12/01-14 SUMMARYs.
Verified: parity harness 17/17 live assertions; full frontend suite 102 files
/ 822 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9c2ec82bdb | docs(10): capture phase context | ||
|
|
b8979f3687 |
feat(01-02): demo answers every mesh/federation RPC the UI calls (FED-04)
Demo images / Build & push demo images (push) Successful in 4m14s
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>
|
||
|
|
579398981f |
feat(01-03): paid tick renders the branded screensaver ring (FED-06)
Demo images / Build & push demo images (push) Failing after 1m14s
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> |
||
|
|
8b51b7e2dc |
fix(quick-260731-upz): make the master-seed RNG explicit (ARCHY-1 / F-02)
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> |
||
|
|
5ba80e49b7 |
docs(quick-260731-upz): entropy-audit remediation backlog + tracker items
- 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> |
||
|
|
5f366f7589 |
fix(01-13): on-brand scroll cue makes the onboarding tickbox findable (UIFIX-03)
Demo images / Build & push demo images (push) Successful in 4m25s
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> |
||
|
|
46bf5a7870 |
fix(01-15): hand the video off to a custodial PiP session (UIFIX-05)
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> |
||
|
|
4265254700 |
fix(01-11): remove every shipped Fedimint gateway credential (FED-07)
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> |
||
|
|
5faf1a3c5f |
docs(quick-260731-upz): PSBT-first signing architecture spec
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> |
||
|
|
f11db4ea1d |
docs(quick-260731-upz): entropy & seed-generation security audit
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> |
||
|
|
cc52719e81 | docs(01-01): complete federation node-store lock plan | ||
|
|
4a8925f09e |
fix(01-14): satisfy vue-tsc strict-null checks in usePaidItemViewer.test.ts
Demo images / Build & push demo images (push) Successful in 3m41s
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> |
||
|
|
0c63a8518c |
docs(quick-260731-upz): plan entropy/seed audit + PSBT signing architecture
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> |
||
|
|
b5628d968e |
fix(web5): raise connected-nodes row-breakpoint height floor to 40rem (UIFIX-02)
Demo images / Build & push demo images (push) Has been cancelled
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>
|
||
|
|
4b5367ebc4 |
fix(01-01): route remaining federation mutators through the store lock (FED-01)
Task 2 of 01-01-PLAN.md, closing the gap left after Task 1's initial commit
(
|