Commit Graph
2543 Commits
Author SHA1 Message Date
archipelagoandClaude Fable 5 44522fc94a chore(scripts): one-shot node-side companion-manifest repair script
Curl-and-pipe repair for nodes whose companion-UI manifests are stale
(no session_passthrough): fetches the four current manifests from the
public repo, installs them into /opt/archipelago/apps AND the frontend
runtime payload (which restores over apps/ at every boot), restarts,
and reports the gate probe. Long paste-blocks kept mangling in the
operator's terminal — this replaces them with one short line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:59:34 -04:00
archipelagoandClaude Fable 5 1948767083 chore: release v1.7.125-alpha
Demo images / Build & push demo images (push) Successful in 4m12s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1.7.125-alpha
2026-08-06 06:27:02 -04:00
archipelagoandClaude Fable 5 e875f15fc5 docs: curated changelog for v1.7.125-alpha
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 06:10:39 -04:00
archipelagoandClaude Fable 5 ed062481c3 style: cargo fmt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 05:32:53 -04:00
archipelagoandClaude Fable 5 d8748ee7ba fix(orchestrator): map container uids into the subuid range in the chown fallback
chown_for_rootless_container prefers `podman unshare chown` (which maps
container uid N through the userns), but when that failed once it fell
back to `sudo chown -R <literal>` — writing e.g. host uid 999 for
container uid 999 and reporting success. Host-999 maps to nobody inside
the userns, so the app could not open its own data while everything
claimed the chown worked: botfights on framework-pt crash-looped every
10s on SQLITE_CANTOPEN over a data dir the daemon itself had just
"fixed".

The sudo fallback now translates container ids (1..99999) to
subuid_base + id - 1 (fleet base 100000; container root maps to the
service user, 1000). Already-mapped ids and uid 0 pass through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 23:48:36 -04:00
archipelagoandClaude Fable 5 8469af5f4e fix(wallet): surface LND sweep refusals as readable errors
A sweep of 92 unconfirmed/dust sats failed with LND's debug-flavored
"insufficient input to create sweep tx: input_sum=0 BTC, output_sum=
0.00000092 BTC" — and the RPC sanitizer then masked even that behind
"Operation failed. Check server logs." (framework-pt, 2026-08-06).
The sweep mechanics are untouched (balance minus fee, as always) —
this only makes the refusal say WHY in plain language.

- lnd.sendcoins translates the sweep refusal: balance below Bitcoin's
  dust minimum or not yet confirmed, so no transaction can be built
  (LND's original message kept in parens).
- "Failed to send" joins the sanitizer's user-facing allowlist — the
  same lesson as "Insufficient balance"/"Payment failed" before it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 23:41:35 -04:00
archipelagoandClaude Fable 5 92cffe9d46 fix(reconciler): recreate an absent stack member when its siblings are live
The periodic reconcile runs ExistingOnly — merely listing a catalog
manifest must never install an app — and its only absent-container
recovery keyed on the last running-names snapshot, which ages out after
a few daemon restarts. An absent member of an installed stack then stays
absent forever: .38 ran indeedhub with no minio/postgres for days, nginx
down on 'host not found in upstream "minio"', and nothing ever put the
members back.

A live sibling container is proof the stack is installed on this node,
so an absent member is now treated as a hole to repair, not a choice to
respect: the recovery guard also fires when another member of the same
stack (app_ops::stack_member_app_ids) has a container in any state.
A stack with no containers at all is left untouched, and sibling app ids
resolve through the loaded-manifest container names (immich-postgres
runs as immich_postgres).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:50:42 -04:00
archipelagoandClaude Fable 5 d5ef4ef76e chore(catalog): sign catalog with session_passthrough + indeedhub-redis caps
Carries the two manifest-side halves of the .125 fix batch: the four
companion UIs (lnd-ui, bitcoin-ui, electrs-ui, fips-ui) declare
session_passthrough on their gated ports so the gate forwards the node
session their nginx proxies to the daemon, and indeedhub-redis gains
CHOWN+DAC_OVERRIDE so its entrypoint can traverse its own data dir.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:16:13 -04:00
archipelagoandClaude Fable 5 6110a7a9a7 test: update stale drift guards (login-page A mark, 25 exempt ports)
Both predate this session's changes and were masked by the release
gate's cargo-test-weekly compile timeout:
- login_page_sources_its_art_from_the_gate still asserted the retired
  wordmark (logo-archipelago.svg); the login page ships the sidebar A
  mark (favico-black-v2.svg) since the 2026-08-05 rework.
- unauthenticated_ports_are_all_accounted_for lagged at 17; the
  v1.7.123 port-policy round grew the rationale-carrying exempt set
  to 25 (reviewed and enumerated in the test comment).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 21:49:19 -04:00
archipelagoandClaude Fable 5 c972419840 fix(wallet): blank send/receive on every open; sweep shows amount; camera option stays visible
Demo images / Build & push demo images (push) Successful in 3m31s
Operator-reported (2026-08-05):

- Send/Receive modals reset to a blank slate on every open. Stale state —
  destination, amount, memo, and above all an armed "send all funds"
  toggle — silently carried into the next payment.
- Arming "send all funds" now shows the swept balance in the (disabled)
  amount field instead of a confusing 0; disarming or leaving the
  on-chain tab clears it.
- The scan modal no longer hides "Scan with camera" on plain-http desktop
  (browsers only allow getUserMedia on secure origins): the option stays
  visible with a one-line explanation, and choosing it surfaces the HTTPS
  requirement with photo/paste fallbacks. The companion app's native
  scanner path is untouched and still takes priority.
- What's New entry for v1.7.125-alpha.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 21:22:42 -04:00
archipelagoandClaude Fable 5 d0d9c032de fix(apps): session_passthrough on companion UI ports; indeedhub-redis caps
- lnd-ui/bitcoin-ui/electrs-ui/fips-ui declare session_passthrough: true
  on their gated ports — their nginx forwards the browser's node session
  to the daemon's authenticated endpoints, which the gate's cookie strip
  was discarding (every data call 401'd behind the gate).
- indeedhub-redis gains CHOWN + DAC_OVERRIDE: the alpine entrypoint runs
  as capability-stripped container-root and could not traverse the 0700
  appendonlydir owned by the redis uid — crash-looped ~4k restarts on
  archi-dev-box under the quadlet migration.

These reach nodes via the signed catalog re-sign (manifest overlay).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 21:22:07 -04:00
archipelagoandClaude Fable 5 ada59acdd5 fix(appgate+container): stop stripping app cookies; create named volumes correctly
Two daemon bugs, one debugging arc (2026-08-05, operator-reported):

1. The app gate removed the ENTIRE Cookie header before proxying. That
   broke the data plane of every first-party companion UI behind the gate
   (lnd-ui/bitcoin-ui/electrs-ui/fips-ui render their shell, then every
   /proxy/* and /lnd-connect-info call 401s — observed as "LND UI
   unreachable"), and silently logged users out of every gated app with
   its own cookie login (vaultwarden, nextcloud, gitea) on each request.
   The gate now strips only its own cookie pairs (session, csrf_token);
   a new per-port manifest opt-in `session_passthrough: true` forwards
   the node session to first-party UIs whose nginx proxies the daemon's
   authenticated endpoints. Undeclared ports never get passthrough.

2. podman_client::create_container sent named volumes to the libpod API
   as bind mounts with the bare volume name as source, so creating any
   manifest app with a `type: volume` mount failed. On .38 the reconciler
   removed indeedhub-postgres/-minio for env drift and then could never
   create their replacements, leaving the stack half-missing forever.
   Named volumes now ride the spec's `volumes` field ({Name, Dest,
   Options}). Also: the reconcile-failure log now prints the full anyhow
   chain — `%e` showed only "create_container X" and hid the real error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 21:21:52 -04:00
archipelagoandClaude Fable 5 4ace62fad9 chore(catalog): sign catalog with the bitcoin and fedimint fixes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 19:21:21 -04:00
archipelagoandClaude Fable 5 e7592dc9c9 fix(fedimint): stop declaring 8175 — it belongs to the UI companion, not fedimintd
Demo images / Build & push demo images (push) Successful in 3m32s
Declaring the Guardian UI port on the fedimint app made the orchestrator try
to publish 8175 from fedimintd, colliding with archy-fedimint-ui which
already holds it: start_container failed on every reconcile and fedimint
crash-looped (100.82.34.38). The companion's nginx pinned to 127.0.0.1 is
what actually closes that port; the gate reports it rather than fronting it.

Also: app-login page uses the sidebar's 'A' mark instead of the full
wordmark, is pinned to the small viewport so it stays centred and the
keyboard overlays rather than scrolls it, and the install-version modal
icon uses object-contain so a non-square icon is no longer cropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 19:12:36 -04:00
archipelagoandClaude Fable 5 188411b79c chore(catalog): sign catalog with the repaired bitcoin start script
Unbreaks Bitcoin on every node running the 1.7.124 catalog: the embedded
start script had a shell syntax error, so bitcoind never launched and the
app vanished. Delivered by catalog rather than a release because manifests
reach nodes through the signed catalog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 18:54:37 -04:00
archipelagoandClaude Fable 5 e88c51d80b fix(bitcoin): repair the startup script I broke in 1.7.124, and gate against it
The bitcoin app vanished from updated nodes: the container exited instantly
with 'sh: Syntax error: "fi" unexpected'. My 1.7.124 change added an
explanatory comment INSIDE the manifest's folded YAML scalar (>-), where
'#' is not a comment — it is literal text that reaches the shell. Folding
joins lines with spaces, so the comment swallowed the 'if ... then' while
the more-indented echo survived as its own line, leaving an orphan 'fi'.
bitcoind never ran, the container exited, and the app disappeared from the
UI because detection is container-based.

Explanations now live above the '- >-' line where YAML really treats them
as comments. The loopback-conf tolerance (-allowignoredconf=1) is unchanged
and still needed.

Adds scripts/check-manifest-shell.py to the release gate: it runs 'sh -n'
over every embedded manifest script and rejects '#' inside these scalars.
Nothing validated this shell before — no YAML parse or Rust test could have
caught it, and it only failed on the node, after signing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 18:45:02 -04:00
archipelago c9f1d87dd6 chore: release v1.7.124-alpha v1.7.124-alpha 2026-08-05 16:42:57 -04:00
archipelagoandClaude Fable 5 4f8c76c67e style: rustfmt the regenerated app_ports list
generate-app-catalog.py writes APP_LAUNCH_PORTS one entry per line; rustfmt
packs it. The release gate checks formatting, so the generated file has to
be formatted after regeneration or every catalog sync fails the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:30:13 -04:00
archipelagoandClaude Fable 5 d5ca612e2b chore(catalog): sync catalogs to the manifests for 1.7.124
Demo images / Build & push demo images (push) Successful in 3m42s
Portainer's image reaches the public catalog (the release gate caught the
manifest and catalog disagreeing), and fips-ui 8336 joins the mesh relay's
port list now that it declares a port — it is auth: gated, so the relay
withholds it rather than bridging it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:27:19 -04:00
archipelagoandClaude Fable 5 0a374c80a6 style: rustfmt the merged PR #125 hunks and the mirror test; sync Cargo.lock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:24:31 -04:00
archipelagoandClaude Fable 5 6668359875 chore: bump to 1.7.124-alpha ahead of the release run
Demo images / Build & push demo images (push) Successful in 3m42s
Pre-bumped so the release gate compiles the test profile at the final
version — create-release bumps after the gate, so the gate would otherwise
run on the old version and the bump would invalidate the cache, timing out
cargo-test-weekly on the compile rather than the tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:10:49 -04:00
archipelagoandClaude Fable 5 cc709fd7da docs(1.7.124): curate release notes and add the in-app What's New block
Demo images / Build & push demo images (push) Successful in 3m46s
Leads with the update that switched nodes off and left them unable to
switch back on — the one an operator most needs to understand, and the
reason to take this release promptly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 14:58:28 -04:00
archipelagoandClaude Fable 5 9dd02359e4 feat(settings): session timeout is configurable from the UI
Demo images / Build & push demo images (push) Successful in 3m43s
auth.session-policy.get/set plus a card under Account. Presented as two
plain questions rather than the token mechanism underneath, because the
distinction that matters to an operator is which control actually ends a
session: the dashboard polls constantly, so an idle timeout alone never
fires on an open tab — the absolute cap is what guarantees it.

Values are clamped server-side and the stored result is echoed back, so
the bounds are discoverable instead of an error. Presets rather than a free
number field: a box accepting '5' invites locking yourself out. A short
idle choice warns that it is the payments-industry posture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 14:29:17 -04:00
archipelagoandClaude Fable 5 81033ed6f5 fix(ota): repair a stale Restart=on-failure unit that leaves nodes dead after update
austin-sapien (100.70.96.88) sat dead for over two hours after taking
v1.7.122 — 'server starting' in the UI, service inactive, exit status
0/SUCCESS. It did not crash: the in-process updater replaces the binary and
exits cleanly for systemd to restart it, and that node's unit still carried
Restart=on-failure from an older install. systemd read the clean exit as
success and left it stopped. Every node with the old unit has this waiting
for it on the next update.

self-update.sh does refresh units, but the in-process update path never
runs it, so nothing was repairing them. The daemon now checks its own unit
at boot and rewrites only the Restart= line, so a node that starts even
once ends up with a policy that survives the next update.

Also carries the session-policy wiring: validate() now honours the
configured idle and absolute limits and the per-device class, instead of
the single hard-coded 24h constant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 13:50:16 -04:00
archipelagoandClaude Fable 5 d8647f6576 fix(mesh): tabbed tools column on very wide screens; add configurable session policy
Demo images / Build & push demo images (push) Successful in 3m36s
Mesh right panel: a >=2560px screen hid the tab bar and stacked all five
tool panels in fixed grid rows. On a real display that clipped the Bitcoin,
Dead Man and AI headings to a few pixels each, letterboxed the map, and
pushed Radio Settings into a scroll — more screen producing a worse view.
Very wide now uses the same tabbed column as every other desktop width,
with the selected panel filling the column and the map running edge to edge
(it is the one panel with nothing to scroll).

Session policy: idle timeout, absolute cap and a re-prompt-for-funds flag,
persisted and clamped. Two tokens already existed — a session token and a
30-day login token — so the knob changes how long a quiet tab stays usable
without putting a long-lived credential on every request. Kiosk screens are
exempt from the idle timeout (nobody is there to log a TV back in) but keep
the absolute cap so a stolen box does not stay authenticated forever. The
cap is not optional theatre: idle alone never fires on a polling dashboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 13:03:31 -04:00
archipelagoandClaude Fable 5 91bbe4faa1 fix: portainer pin, bitcoin conf tolerance, gate login UI, named OTA origin
Portainer: nodes have been running :latest — which is 2.39.1 — while the
manifest pinned 2.19.4 from two years ago. The port migration recreated the
container onto that old pin and Portainer refused to start: it migrates a
database forward, never backward, so an existing install died with 'schema
version does not align' and My Apps showed 'app is not responding'
(100.82.34.38). 2.39.1 published as an immutable tag and pinned forward, so
existing databases keep working and older ones migrate up.

Bitcoin: complements PR #131. That removes the code which kept writing a
datadir bitcoin.conf; -allowignoredconf=1 additionally makes an existing
one non-fatal, so a node already carrying the file recovers on restart
instead of crash-looping until something reinstalls it.

App gate login: rebuilt against the dashboard's own design — rotating
intro backgrounds served from the gate, the glass panel, the Archipelago
mark in its gradient ring, the app's icon as a My Apps tile, and the glass
button. Crucially it no longer sends X-Frame-Options: DENY, which made
every gated app render as unreachable inside My Apps' embedded frame;
frame-ancestors expresses 'only this node may frame me', which
X-Frame-Options cannot.

OTA origin: primary mirror is now source.archipelago-foundation.org over
TLS instead of a bare IP on plaintext. The IP stays as an automatic
fallback for nodes whose DNS or clock is broken — both break TLS, and the
signature, not the transport, is what establishes trust.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 12:44:57 -04:00
archipelagoandClaude Fable 5 08a725ba12 Merge PR #131: stop writing a datadir bitcoin.conf that conflicts with -conf
Root cause of the Bitcoin crash-loop on 100.82.34.38: since a597c1d9
bitcoind launches with -conf=/tmp/rpc.conf and never reads the datadir
bitcoin.conf, but write_bitcoin_conf / ensure_bitcoin_rpc_config /
run_bitcoin_rpc_repair kept writing one on every install and restart.
Bitcoin Core's own datadir-conflict check then refuses to start at all.

Conflict resolved in favour of the PR: HEAD still carried
write_bitcoin_conf, whose deletion is the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 12:25:26 -04:00
archipelago a9f9be2f0d Merge PR #125: FIPS last-known-good endpoint fallback for direct peering
LAN -> last-known-good -> anchor-tree escalation, npub-keyed endpoint store
persisted with 30-day retention.
2026-08-05 12:22:22 -04:00
archipelago e5612fff0f Merge PR #132: translate Cashu NUT error codes into plain-language messages
Mint failures surfaced raw JSON ({"detail":"proofs already spent"}) to
the user; now the top-level message is actionable while the raw body stays
in logs via {:#}.
2026-08-05 12:22:22 -04:00
archipelagoandClaude Fable 5 4ec53a9805 fix(ota): republish the .122 manifest — the rotation stranded every pre-.122 node
The manifest advertises exactly one version, so publishing .123 (new-key
signed) removed the only stepping stone across the rotation. A node on
.121 pins the OLD root, fetches the .123 manifest, fails signature
verification and refuses — permanently, because .122 is no longer offered
anywhere. Reproduced against the live URL: 'signed_by does not match the
pinned release-root anchor'. archy-shorty-s (.228) is on 1.7.121-alpha-dev
and in exactly this state.

Restoring the old-key-signed .122 manifest as the OTA pointer lets those
nodes take .122, which installs the new pin; .123 is republished once the
fleet has crossed. Safe in both directions: is_newer() is a strict
greater-than on the version triple, so a node already on .123 sees .122 as
older and does not downgrade.

The .123 release itself is untouched — tag, assets and catalog stand; only
the pointer moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 10:24:09 -04:00
archipelagoandClaude Fable 5 9b30daaf9c fix(security): restart a companion whose image was rebuilt underneath it
A rebuilt image never reached a running companion. ensure_image_present
rebuilds in place under the same tag, so the quadlet body is identical,
write_if_changed reports no change, and enable_now is a no-op on a running
service — the container keeps the old layers indefinitely.

That is precisely how archi-dev-box kept serving the LND, FIPS, Electrs and
Guardian screens on 0.0.0.0 after v1.7.123 rebuilt every one of those images
to bind loopback: correct images on disk, three-day-old containers still
running. Closing those ports needed a manual 'podman rm -f' per container,
which no other node would ever get. Compare the running container's image ID
against the built one and restart when they diverge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 10:17:58 -04:00
archipelago ea4c072183 chore: release v1.7.123-alpha
Demo images / Build & push demo images (push) Successful in 3m58s
v1.7.123-alpha
2026-08-05 09:35:47 -04:00
archipelagoandClaude Fable 5 cfd1b4c731 chore(trust): flip the signing checks to the new release root
v1.7.122-alpha was the last release signed with the old root — it is the
release that installed the new pin on every node. From v1.7.123 the new
root signs, and a node running .122+ rejects an old-key signature. The
ARCHY_RELEASE_ROOT_PUBKEY override is no longer needed either: the signer
built from this tree pins the same key we now sign with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 09:31:28 -04:00
archipelagoandClaude Fable 5 27c1b151f8 docs(1.7.123): curate release notes and add the in-app What's New block
Demo images / Build & push demo images (push) Successful in 3m59s
Leads with the honest version: five screens were open and the previous
release's own audit reported them as fine, found by scanning from another
machine rather than asking the node. States plainly that what leaked was
the page, not credentials — the macaroon path was verified, not assumed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 09:08:07 -04:00
archipelagoandClaude Fable 5 9c736f20b6 fix(security): publish the loopback-pinned UI images and pin the new tags
Fresh installs pull *-ui images from the registry, so the source fix alone
left a newly flashed node serving the Bitcoin, LND, Electrs, FIPS and
Guardian screens with no login. All five rebuilt and pushed to
146.59.87.168:3000/lfg2025 as 1.7.123-alpha AND :latest — both tags,
because first-boot resolves the pinned tag from image-versions.sh while the
daemon's companion installer hardcodes :latest, and a stale :latest would
have quietly undone the fix on exactly the path that rebuilds companions.

Verified by pulling each image back from the registry anonymously and
reading /etc/nginx/conf.d/default.conf inside it — a private package would
make fresh nodes fall back to a stale local image without saying so.

Also fixes the FOURTH copy of bitcoin-ui's listen directive
(scripts/reconcile-containers.sh wrote 'listen 8334' into the rendered
nginx.conf on every reconcile, which would have re-opened the port after
the image and template were both corrected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 09:07:05 -04:00
archipelagoandClaude Fable 5 abf0f56afc fix(security): close the five host-networked app UIs the audit could not see
Scanning archi-dev-box from OUTSIDE found five ports serving their screens
with no login — lnd-ui 18083, bitcoin-ui 8334, fips-ui 8336, electrs-ui
50002 and the Fedimint Guardian 8175 — none of which appeared in the gate's
unprotected list. They are host-networked, so Podman publishes nothing to
pin and their manifests declared 'ports: []'; the gate builds its map from
declared ports, so it neither protected them nor reported them. An audit
that reports success while five screens are open is worse than no audit.

Their nginx now listens on 127.0.0.1 instead of 0.0.0.0, and each port is
declared 'auth: gated' so the daemon owns the outside. 'bind:' on a
host-networked app is a statement of where the container listens, not a
publish instruction — quadlet already skips PublishPort in host mode.
Guardian 8175 is declared on the fedimint app because its companion has no
manifest, and the gate keys on port, not container.

Credential paths were NOT exposed and are verified so: /lnd-connect-info,
the /proxy/lnd/ passthrough, container logs and every RPC method through
these screens all return 401 unauthenticated. What leaked was the page
shell.

Also fixes the delivery gap that would have made this unshippable: only
bitcoin-ui, lnd-ui and electrs-ui were ever rsynced to
/opt/archipelago/docker, so edits to fips-ui and fedimint-ui reached nodes
through no path at all. All five now sync; the two whose rebuilds the
daemon owns are synced without being handed to container-specs.

Every remaining undeclared port is now declared with a stated reason —
gated: botfights 9100, router 8084, pine 10380; exempt with rationale:
fedimint consensus 8173/8174, gateway 8176/9737, netbird 8086/8087 (TLS +
own auth, and enrolled devices cannot hold a session), pine TLS 10381,
lightning-stack REST 8091 (macaroon, mirrors lnd). Zero undeclared ports
remain across all 56 manifests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 08:46:09 -04:00
archipelago 634640944c chore: release v1.7.122-alpha v1.7.122-alpha 2026-08-05 07:57:07 -04:00
archipelagoandClaude Fable 5 b92e16abc0 fix(release): sign v1.7.122 with the OLD root — the rotation moved the checks a release early
Demo images / Build & push demo images (push) Successful in 4m12s
The rotation commit pointed create-release.sh and publish-release-assets.sh
at the NEW root in the same commit that pins it in the binary. But the
release CARRYING the rotation must be signed with the OLD root: every node
is still running the previous binary, which pins the old key. So the
tooling would have rejected the only signature the fleet can accept, and
the signature it demanded would have ended OTA fleet-wide.

Both checks now expect the old DID for this cycle, with the flip to the new
one called out for v1.7.123+. sign-manifest.sh documents the
ARCHY_RELEASE_ROOT_PUBKEY override needed because the signer built from
this tree already pins the new anchor and would fail to verify its own
correct output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 07:48:51 -04:00
ssmithxandClaude Sonnet 5 53b158ce5a fix(wallet): translate Cashu NUT error codes into plain-language messages
Mint HTTP failures (swap/melt/mint-quote) were surfacing raw JSON bodies
like {"detail":"proofs already spent","code":11001} straight to the
user. Add a translator for the NUT-02/03/04/05 transaction-validation
error codes (10001-11017, 12001-12003; see
https://github.com/cashubtc/nuts/blob/main/error_codes.md) and layer it
onto the mint_client bail sites via anyhow context, so the top-level
message is actionable while the raw status/body stays available via
{:#} for logs. receive_token now surfaces the real reason (e.g. "This
ecash has already been redeemed") instead of a generic "Failed to
receive any proofs from token" when every mint in a token fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 09:41:44 +00:00
archipelagoandClaude Fable 5 c35e33d0a7 docs(1.7.122): curate release notes and add the in-app What's New block
Leads with what changes for the operator: app screens now require the node
password across LAN, Tailscale, mesh and Tor; the wallet/protocol ports that
must stay open stayed open; the mesh leak found during on-node verification;
nodes repairing their own legacy containers; and the signing-key rotation.
Known gaps disclosed, including the eleven still-undeclared ports and that
non-browser clients will now meet the login page.

The new block uses <strong> rather than the literal ** markers in earlier
entries, which render as asterisks in the modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 16:33:03 -04:00
archipelagoandClaude Opus 5 3f4b5524b1 chore(trust): rotate the release root to z6Mkfu5LT…DLWT
DO NOT MERGE INTO A RELEASE SIGNED WITH THE NEW KEY. See below.

The previous release root (z6Mkkid…q7ur, pinned 2026-07-02) was exposed
in a chat transcript and is treated as compromised. It signs both OTA
manifests and the app catalog, so anyone holding it could sign updates
the fleet would install.

Pins the new key in trust::anchor and moves EXPECTED_DID in all three
signing/publishing scripts.

ORDERING IS CRITICAL — nodes pin the OLD key:

  * The release CARRYING this commit must be signed with the OLD key.
    That is the only signature a node running the previous binary will
    accept, and it is what installs the binary pinning the new key.
  * Only the release AFTER that may be signed with the new key.
  * Signing this release with the new key makes every node reject it,
    ending OTA fleet-wide and requiring hands-on recovery per node.

sign-catalog.sh moves in the same commit, so the app catalog must also be
re-signed with the new key once this ships, or nodes accept the binary
and reject the catalog.

Key verified before pinning: the hex and the did:key are the same
keypair, checked with a base58 decoder round-tripped against the previous
known-good pair. An earlier candidate hex (cb830e13…) was rejected
because it decoded to a different DID than the one supplied — pinning it
would have made every node reject every future update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:10:32 -04:00
archipelagoandClaude Fable 5 0f21f598aa fix(security): FIPS mesh relay must not republish auth: local ports
Caught verifying the gate fixes on archi-dev-box: [fips0-ULA]:32838
answered HTTP 200 straight from nbxplorer with no credential. The
catalog declares that port auth: local — host-local by intent, pinned to
loopback, the gate deliberately keeps its hands off — but the mesh relay
bridges a STATIC port list to 127.0.0.1, so it republished it to the
whole mesh. Same bug class as the Tor onion gap: a transport that
converges on the app loopback without consulting the declaration.

PortMap now records declared-local ports and the relay withholds them
(tearing down an existing bridge if a catalog refresh newly declares
one), alongside the declared-gated withhold. Undeclared ports keep
todays behaviour — silence is not an instruction in either direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:40:28 -04:00
archipelagoandClaude Fable 5 d2e4b00789 fix(security): gate classifies from the catalog overlay and releases withdrawn claims
Dev-box verification of the Tor/FIPS fixes caught a pre-existing split
brain: the orchestrator publishes containers from the signed catalog's
embedded manifests (origin-wins), but the gate classified ports from the
stale disk manifests — so it externally bound nbxplorer 32838, a port
the catalog declares auth: local and pins to loopback. Reachable behind
a login, but reachable where it deliberately was not.

- build_port_map now consults the catalog overlay first, via the same
  parse/validate/image-only filter the orchestrator uses (moved to
  app_catalog::catalog_manifest_overlay so the two cannot diverge again).
- GatedPort carries . The gated set still includes undeclared
  Session-default ports for challenge/audit, but every action that
  REDIRECTS traffic — the torrc 127.0.0.2 repoint, the FIPS relay
  stand-down, the Tor-upstream bind — now keys on the declaration.
- The sweep releases held claims whose port left the gated set, so a
  catalog refresh that withdraws a port (gated → local/none) takes
  effect without a daemon restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:50:46 -04:00
archipelagoandClaude Fable 5 e46af8cfe5 feat(security): self-heal legacy containers on declared bind drift
Legacy pre-quadlet containers kept publishing 0.0.0.0 after the catalog
pinned their app to loopback, because host_port_bindings_drifted only
compared host PORT numbers — closing them needed a manual package.update
per app per node. The drift check now also compares the bind ADDRESS,
but only when the manifest declares one: an empty bind never fires,
since recreating a loopback-published container to wildcard on silence
is exactly the v1.7.121 Bitcoin-RPC incident. With this, every node
recreates its legacy containers to the declared state on its own after
the OTA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:14:01 -04:00
archipelagoandClaude Fable 5 f08ed79b8a fix(security): FIPS v6 relay hands gated ports to the app gate
The mesh relay is a raw unauthenticated forward to the app's loopback,
and whether it or the gate owned a fips0 ULA port was decided by a bind
race — the dev box happened to be safe because the gate bound first.
The relay now skips ports declared auth: gated and tears down any
existing bridge for a port that became gated since it was bridged
(catalog refresh), releasing the bind for the gate's next sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:14:00 -04:00
archipelagoandClaude Fable 5 3760a00ea3 fix(security): Tor onions for gated ports forward to the gate, not the app
Tor carries no session cookie, so HiddenServicePort → 127.0.0.1:<port>
reached the app around the gate — the last transport the gate did not
cover. The gate now binds 127.0.0.2 (its own loopback, distinct from the
app's 127.0.0.1, so no app needs a second port), and regenerate_torrc
forwards declared-gated ports there. Undeclared ports keep today's
target: absence of the field is not an instruction.

The 127.0.0.2 claim deliberately does not count toward the unprotected
audit — a port whose only claim is the Tor loopback is still wide open
on the LAN and must keep warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:13:59 -04:00
archipelagoandClaude Fable 5 8210ca0a2a chore(catalog): sign catalog with port auth policy — 20 UIs gated, exemptions declared
Embeds the manifest port declarations (bind: 127.0.0.1 + auth: gated on 20
HTTP UIs, auth: local on loopback backends, auth: none + rationale on
protocol ports) into the signed catalog so nodes enforce the app gate.
Also carries bitcoin-ui/lnd-ui 1.7.119 version drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:28:06 -04:00
archipelagoandClaude Fable 5 6d9d87caa6 chore: sync Cargo.lock with the 1.7.121-alpha version bump
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:09:37 -04:00
archipelagoandClaude Fable 5 16642ad8b8 feat(security): declare port auth policy across the app manifests
20 HTTP UIs move to bind: 127.0.0.1 + auth: gated (the daemon owns their
external addresses and authenticates every connection); 5 loopback-only
backends declare auth: local so the gate keeps its hands off. Protocol
ports (LND, bitcoin p2p, electrum, CLN, gitea SSH, Wyoming, mDNS/SSDP)
were already declared auth: none with rationales in earlier commits.

Inert until the catalog is re-signed: nodes act only on declared fields
delivered via the signed catalog, and the catalog overlay overrides these
disk manifests everywhere they are installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:09:37 -04:00
archipelagoandClaude Opus 5 4e455167e9 fix(release): accept https remotes when publishing assets
Publishing v1.7.121-alpha failed on auth after the manifest had already
passed every check. The script required an `http://user:token@` remote,
which left only `gitea-vps2` — whose token is dead — and rejected
`gitea-ai`, the https remote whose credential actually works for git
push. Same Gitea instance (146.59.87.168, v1.27.1) either way, so the
restriction bought nothing and blocked the one usable path.

Accepts http and https, and carries the scheme through to the API URL
instead of hardcoding it.

Note for diagnosis next time: `/api/v1/repos/.../releases` is publicly
readable, so a 200 there does NOT prove the credential works. Use
`/api/v1/user`, which requires real auth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:23:28 -04:00