183 Commits

Author SHA1 Message Date
archipelago
e074a117d2 feat(pine): node-status endpoint + Claude voice brain + mesh announcements + wake-word groundwork
- GET /api/pine/status: public tier (version/uptime/bitcoin/mesh facts) for
  the Pine page; bearer-token tier (Lightning balances, latest mesh message)
  for the seeded Home Assistant sensors. Token minted 0600 at
  secrets/pine-status-token; nginx location ships via the bootstrap
  self-heal + canonical ISO conf. Replaces the per-node socat forwarder and
  bitcoind RPC credentials living in HA's configuration.yaml.
- pine_ha seeder: REST sensors + four ask-Archy intents (block height,
  peers, sync %, Lightning balance) with LLM tool descriptions; Claude
  conversation agent (anthropic entry from the node's claude-api-key) set as
  pipeline default with prefer_local_intents so exact phrases stay local and
  fuzzy ones tool-route through Claude; mesh-message announce automation on
  every Assist satellite; bounded config markers with legacy-block migration.
- pine-openwakeword joins the pine stack (all lifecycle sites) — on-node
  wake-word engine groundwork for the custom "Yo Archy" model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 02:59:34 -04:00
archipelago
8791ef60f5 feat(pine): wire the Pine 3-container stack into the orchestrator
Register "pine" as a manifest-driven stack (like netbird) across every
lifecycle site so install/stop/start/restart/uninstall/crash-recovery treat
the launcher + two Wyoming engines as one app:

- stacks.rs: pine_stack_app_ids() + install_pine_stack() (orchestrator-first,
  adopt existing, else bail — no hardcoded fallback)
- install.rs: dispatch package_id == "pine" to the stack installer
- app_ops.rs: canonical stack-member table + owning_package STACKS
- dependencies.rs: startup_order + needs_archy_net
- config.rs: all_container_names
- crash_recovery.rs: auto-start members + a StackRecoverySpec (anchor
  pine-whisper) so a genuinely-installed stack self-heals but orphan debris
  does not crash-loop
- docker_packages.rs: exclude the engines from the apps list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 05:14:32 -04:00
Dorian
cf4a8eef0e fix(core): throttle volume-ownership sweep to first-seen + hourly per container
The sweep podman-execs into every running container; doing that on each 30s
reconcile tick was a permanent conmon 'Failed to create container' storm on
hosts where exec from the backend's cgroup context fails (Debian 13 first
boot). Ownership drift is an install/OTA-time event — sweep on first pass
after a container appears, then at most hourly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 06:07:56 +01:00
archipelago
2fce4fb842 fix(network): derive the host IP from the default route, not hostname -I order
On a fresh ISO node, NetBird's own WireGuard tunnel (10.44.0.1) sorted
ahead of the real NIC in hostname -I, so the NetBird launch URL (and the
{{HOST_IP}} baked into its cert/config) pointed at the tunnel instead of
the LAN address. The main routing table's default route names the
physical uplink even while a VPN is active (NetBird/Tailscale steer
traffic via policy-routing rules, not the main table), so read src/dev
from 'ip -4 route show default' first, then fall back to a connected UDP
socket's source address, then to the old hostname -I scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 04:21:39 -04:00
Dorian
128c13e965 Merge remote-tracking branch 'origin/main' into ark-merge 2026-07-14 22:08:55 +01:00
Dorian
bdb9826aba feat(wallet): Ark protocol support via barkd sidecar
barkd (Ark wallet daemon, pinned 0.3.0, checksum-verified release binary)
packaged as an installable app; thin HTTP bridge in wallet/ark_client.rs
mirrors the fedimint_client pattern — the bark SDK stays out of the node
binary. wallet.ark-* RPCs cover status/balance/address/send/invoice/
board/offboard/history/configure; Ark movements merge into the unified
ecash history (kind="ark") and spendable Ark sats into total_sats.
Signet defaults (Second's public Ark server) until Ark matures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:56:21 +01:00
archipelago
db4de0ac96 style: cargo fmt the workspace (release-gate cargo-fmt was red)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 15:38:32 -04:00
archipelago
9471d3727f fix(orchestrator): boot reconciler no longer installs apps on manifest presence alone
936b4cca made ANY loaded manifest with an absent container self-heal via
install_fresh — but the signed catalog and the ISO ship manifests for
EVERY app, installed or not, so 'manifest loaded' is not installation
evidence. On a fresh node this mass-installed the catalog: the framework
node grew portainer, vaultwarden, searxng, strfry, mempool, immich and
fedimint uninvited within an hour (fleet nodes showed the same loop as
reconcile-failed noise for unpullable images).

ExistingOnly (boot) reconcile now creates containers from nothing only
for required-baseline apps (filebrowser, fedimint-clientd); every other
absent app is Left('absent') and recreated ONLY via desired-state
recovery when it was running at the last snapshot — which still covers
the vanished-container regressions (indeedhub/immich) that motivated
936b4cca. Reconcile suite 16/16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 09:26:09 -04:00
archipelago
b48ea85961 chore(apps): remove six fake bookmark apps
484.kitchen, Arch Presentation, Call the Operator, Next Web News
Network, Syntropy Institute, and T-0 were hardcoded web-only bookmark
apps (no manifests/containers). Removed across the frontend app grid /
curated / marketplace / session / launcher data, the backend
AppMetadata table, their icons, and the nginx /ext/ proxy blocks
(external-app-proxies.conf deleted — nothing installed it anymore).
botfights/nostrudel/gitea keep the shared external-app code paths.

Frontend: vue-tsc clean, 672 tests pass, production build verified
clean of the removed slugs. Backend: cargo check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 06:56:03 -04:00
Dorian
b09142e0df feat(lnd): capture aezeed at wallet init + encrypted backup + reveal UI
- aezeed words are now captured at BOTH wallet-init paths and stored
  encrypted (Argon2 + ChaCha20-Poly1305, per-node wallet secret) at
  identity/lnd_aezeed.enc; ack marker cleared when a wallet is recreated
- lnd.init-wallet-from-seed was posting seed_entropy to /v1/initwallet,
  which is a GenSeed field — the wallet was never actually derived from
  the master seed; now GenSeed(entropy) → InitWallet(words)
- new RPCs: lnd.seed-backup-status / lnd.seed-reveal (password + 2FA
  gated, same as seed.reveal via shared verify_reveal_auth) /
  lnd.seed-backup-ack
- LND app detail page: Lightning-seed card with first-launch backup
  prompt, tap-to-reveal modal, 'I've backed it up' acknowledgment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:51:02 +01:00
Dorian
4b0e44fc6c chore: purge retired git.tx1138.com registry host from the codebase
The tx1138 Gitea was retired as a release server 2026-06-13 and its registry
frontend is fully dead (500 on every /v2 manifest read, observed 2026-07-10).
Nothing may reference it anymore:

- registry.rs: no longer a default registry, no longer force-enabled on
  load; saved configs are stripped of it on load (same one-time migration
  treatment as the decommissioned Hetzner mirror), with a regression test.
- image_policy.rs: removed from TRUSTED_REGISTRIES — refs through the dead
  host are now refused at the pull site (rejection test added).
- api/handler: dropped the legacy catalog-proxy fallback URL.
- .gitmodules: indeedhub submodule repointed to the OVH Gitea.
- scripts, image-recipe, app-catalog data, neode-ui strings, docs, and all
  test fixtures repointed to 146.59.87.168:3000 (or neutral example hosts).
- image-versions.sh: ARCHY_REGISTRY_FALLBACK emptied (guarded consumers
  skip it); reconcile-containers.sh candidate guard hardened.

The only remaining occurrences of the host string are the strip/reject
enforcement paths and their regression tests — the code that guarantees it
is never used again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:55:32 +01:00
Dorian
ce6ec13e28 feat(lnd): auto-generate btcpay internal-LND connection secret
Materialise /var/lib/archipelago/secrets/btcpay-lnd-connection
(type=lnd-rest;server=https://lnd:8080/;macaroon=<hex>;certthumbprint=<sha256>)
in ensure_app_secrets before btcpay-server env resolution.

The macaroon travels inline as hex because LND's datadir is owned by its
container subuid (100999) — bind-mounting it into btcpay's userns EACCESes.
Reads the macaroon via the existing read_file_as_root sudo fallback; pins the
TLS cert by DER SHA256 thumbprint and rewrites on cert rotation. No-op while
LND is absent (btcpay's secret_env entry is optional), and generation errors
log-and-continue so btcpay never fails to start over this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:46:25 +01:00
archipelago
140b703838 feat(orchestrator): surface apps running with failed dependencies
btcpay-server started and reported healthy while its declared
dependencies (archy-btcpay-db, archy-nbxplorer) failed every boot
reconcile (live-testing report 2026-07-10, Fix 3). After each
reconcile pass, flag every app the pass left up whose manifest
dependency landed in the failure list — a loud DEGRADED journal error
instead of silence. True start-gating on dependency health is a
larger design change (boot-ordering/deadlock risk) and stays open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:39:24 -04:00
archipelago
ebd904262e fix(apps): resolve latent host-port collisions + detect at load time
The strfry/vaultwarden 8082 collision shipped in a signed catalog
because nothing checks cross-app host ports. Fix the remaining latent
pairs from the cfb8d953 audit — did-wallet 8083→8088 (filebrowser
keeps 8083), morphos-server 8086→8089 (netbird-server keeps 8086),
lightning-stack 8087→8091/9737→9738 (netbird and fedimint-gateway
keep theirs) — and fix their healthchecks, which probed the HOST port
inside the container (same class as strfry 6fe62355; now 127.0.0.1 +
container port). load_manifests now flags collisions across the
merged disk+catalog set (bitcoin variants and the mempool umbrella
pair are exempt as mutually exclusive), and a repo-level test parses
apps/*/manifest.yml so a future latent pair fails CI instead of
shipping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:25:42 -04:00
archipelago
755cd4f235 fix(orchestrator): reconcile recreates of bitcoin cascade-restart lnd
24fd97ed cascades on package.start/restart, but a reconcile pass that
recreates or starts a bitcoin backend (desired-state recovery, repair
recreate, boot InstallMissing) also moves the RPC address behind a
running lnd's back — §C 'restart lnd after ANY bitcoin recreate'.
After the pass, dependents from the shared address_caching_dependents
table (moved to app_ops as the single source of truth) that sat
untouched (NoOp) and aren't user-stopped are restarted under their op
lock; a dependent the pass itself (re)created already resolved the
fresh address and is left alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:27:36 -04:00
archipelago
854925c598 fix(orchestrator): user stop aborts host-port readiness/repair waits
wait_for_manifest_host_ports polls up to 420s while the caller holds
the per-app lock, so a user's stop request queued behind it and the
scanner painted the app Restarting for the whole wait (the dd3afbba
transitional-state stick; gate take-2 iter 5 vaultwarden). The wait
now selects against a user-stop-marker watcher (2s poll on
user-stopped.json, by app id or container name) and aborts with a
recognizable error, and the host-port repair path skips its restart
when a stop was requested while it settled — restarting there would
fight the queued stop worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:13:33 -04:00
archipelago
b7be296953 fix(quadlet): HealthCmd falls back to bash /dev/tcp before exit 0
The generated HealthCmd reported healthy (exit 0) whenever the image
shipped neither wget nor curl — btcpay-server sat 'healthy' in podman
ps while completely non-functional (live-testing report 2026-07-10).
Try a bash /dev/tcp connect to the health URL's host:port before
giving up; only images with none of wget/curl/bash still skip Podman
health (dropping exit 0 entirely would restart-loop those). Podman's
health timeout bounds the connect attempt.

Note: the rendered unit text changes, so every http-healthcheck app
gets a one-time unit rewrite + restart on first reconcile after this
binary lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:45:06 -04:00
archipelago
3a5c5db187 fix(orchestrator): install_fresh skips pull when the image is local
Reconcile recreates route through install_fresh, which pulled
unconditionally: with git.tx1138.com dead, every boot reconcile of
archy-btcpay-db/archy-nbxplorer hard-failed on 'pulling ...' even
though both image:tags sat in local storage the whole time
(live-testing report 2026-07-10). Check image_exists first and only
pull when the ref is truly absent — the same semantics as
ensure_resolved_source_available and the generated quadlets'
Pull=never.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:48:36 -04:00
archipelago
7e866c7c94 fix(catalog): compare/cache raw catalog bytes, not a re-serialization
Live verification on .228 showed catalog_changed:true on every
check-updates call: AppCatalog.apps is a HashMap, so the re-serialized
cache bytes flap with key order and the changed-detection never reports
"unchanged" — which would have reloaded the manifest overlay every
hourly poll. Cache the raw fetched body instead and compare against
that; as a bonus the cache now holds the exact signed preimage, so a
present release-root signature stays verifiable from disk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:11:25 -04:00
archipelago
ceb319c292 feat(catalog): reload manifest overlay when a refreshed catalog changes
load_manifests() only ran at startup, so a manifest published via the
signed catalog sat dormant until the next service restart (found while
shipping the strfry manifest fixes: package.check-updates refreshed the
cache but the orchestrator kept rendering the old overlay).

- refresh_catalog now reports whether the cached bytes actually changed
  (write_cache skips identical rewrites), so unchanged hourly polls
  don't churn the manifest map.
- New ContainerOrchestrator::reload_manifests (default no-op); prod
  delegates to load_manifests — the rebuild is atomic under the state
  write lock, and the reconciler's durable user-stopped/uninstalled
  marker filters make a mid-session reload equivalent to the restart
  path that already runs on every boot.
- package.check-updates reloads on change and reports catalog_changed +
  manifests_reloaded; the hourly update-scheduler tick (and its startup
  refresh) do the same.

Tests: app_catalog 5/5 (new write_cache changed-detection test),
reconcile 16/16, knows_app 1/1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:54:00 -04:00
archipelago
55c20e0d6e feat(install): route any manifest-known app through the orchestrator
package.install routed to the manifest-driven orchestrator only for a
hardcoded per-app allowlist; every other app fell through to the legacy
flow, which ignores manifests entirely and creates a bare container with
no ports or volumes (strfry crash-looped this way on .228, 2026-07-09).

New ContainerOrchestrator::knows_app(app_id) (default false; prod checks
its loaded-manifest map, disk + signed-catalog overlay). The install gate
is now allowlist OR knows_app — no per-app Rust for manifest-driven apps,
matching the packaging invariant. Apps without a manifest keep the legacy
flow including the container-exists adopt block; the unknown-app_id →
legacy fallback inside the orchestrator branch is unchanged.

Tests: knows_app_reflects_loaded_manifests + install suite 37/37.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:46:50 -04:00
archipelago
dce541dc49 fix(state): pending-boot Restarting overlay must not mask a user stop
Formal gate take-2 went 4/5: iteration 5's vaultwarden stop-wait saw
'restarting' for the full 120s window. A reconcile pass had queued every
app into pending_boot_starts moments before the stop marker landed, and
the scanner's overlay painted the deliberately-stopped app Restarting
until the slow sequential pass reached it (legacy container, so no unit
state to contradict it).

Two filters: the scanner overlay skips user-stopped ids (the marker is
written before the stop runs, so it's reliably present), and the
reconciler no longer queues apps whose lifecycle op is in flight into the
pending overlay (it skips them via the same probe anyway).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:45:06 -04:00
archipelago
e275494a68 fix(lifecycle): reconciler skips apps with an in-flight lifecycle op
The reconciler doesn't take the RPC layer's per-app FIFO op lock (known
limit of 891cbba4): between a restart worker's stop and start halves it saw
the mempool frontend "missing", repair-recreated it behind systemd's back,
killed the worker's fresh container 11s after start, and left the unit down
for ~3.5 min until the next heal — gate test 123 measured exactly that
window (.228 iteration 3, 2026-07-09).

New crate::app_ops module owns the op-lock registry + stack member table
(runtime.rs and dependencies.rs now delegate) so the reconciler can probe
lifecycle_op_in_flight(app_id) — covering both the app's own key and its
owning stack package — and skip that app for the cycle. The ownership-sweep
podman restart gets the same guard. Health monitor is a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 06:49:52 -04:00
archipelago
eb6ec71a56 fix(security): drop unbindable publish binds instead of crash-looping the app
The dd61a204 bind hardening published Bitcoin RPC on the archy-net gateway
10.89.0.1 — but rootlessport binds in the HOST netns, where that address
does not exist. First real deploy (.228, 2026-07-09) crash-looped
bitcoin-knots AND bitcoin-core the moment the gate's stop→start regenerated
the unit from the re-signed catalog: 'rootlessport listen tcp
10.89.0.1:8332: bind: cannot assign requested address', restart counter 132.
Hand-edits to the unit don't survive — the orchestrator regenerates it from
the signed catalog manifest within seconds.

- New archipelago_container::manifest::host_can_bind_publish_ip(): empty/
  wildcard/loopback accepted without probing, anything else ephemeral-bind
  probed. Applied at all three publish paths — quadlet from_manifest
  (PublishPort), podman API create (host_ip), and the legacy -p string
  table loop — each dropping the publish with a warn instead of taking the
  container down. This neutralizes the bad binds already in the SIGNED
  catalog, so nodes recover on binary deploy alone (no ceremony needed).
- Remove the gateway publishes from bitcoin-knots/-core manifests and the
  legacy config.rs tables: verified on .228 that every in-node consumer
  (lnd, btcpay/nbxplorer, fedimint, mempool-api) dials the container's
  archy-net alias directly (bitcoin-knots:8332) and lnd uses RPC polling
  (no ZMQ) — the gateway publish had zero consumers. Loopback-only RPC/ZMQ
  (the approved LAN lockdown) stands; P2P 8333 stays public.

Catalog still ships the gateway binds until the next signing ceremony
regenerates it from these manifests; the guard makes that non-urgent.

Tests: container crate 65/65 (2 new guard tests), quadlet 40/40.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:41:39 -04:00
archipelago
dd61a20413 feat(security): bind Bitcoin RPC/ZMQ publishes to loopback + archy-net gateway (§C)
USER DECISION 2026-07-08: accept breaking external wallets pointed at
nodeIP:8332. The 0.0.0.0 publish exposed auth-only RPC (and unauthenticated
ZMQ 28332/28333 on the legacy path) to the whole LAN.

- New `bind` field on manifest port mappings (validated as an IP; the same
  host port may repeat with distinct binds). Rendered as
  PublishPort=<ip>:<host>:<container> in quadlet units and host_ip in the
  podman API create — unbound ports render byte-identical to before, so no
  fleet-wide false-drift wave.
- bitcoin-knots/-core manifests publish 8332 on 127.0.0.1 + 10.89.0.1 only.
  In-node consumers (lnd, fedimint-gateway, btcpay/nbxplorer) are unaffected:
  they dial host.archipelago / host.containers.internal, which the
  orchestrator pins to the archy-net gateway 10.89.0.1. P2P 8333 stays public.
- Legacy config.rs port strings get the same treatment incl. ZMQ.

Tests: new quadlet bind-render + manifest bind-validation tests;
container:: suite 167/167, archipelago-container 63/63.

DEPLOY NOTE: PublishPort strings change for bitcoin containers → one
planned recreate per node; restart lnd afterwards (it caches the backend
IP — see the backend-recreate cascade tracker item). Catalog manifests for
bitcoin apps must be regenerated + re-signed for catalog-covered nodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:29:02 -04:00
archipelago
161a6e4dbd fix(orchestrator): make legacy mempool id start/stop/restart the split stack
Found by the .228 lifecycle gate (2026-07-08, first quadlet-mode run):
stop→start on the legacy `mempool` umbrella app id destroyed the mempool
deployment. Under quadlet, package.stop removes the containers; package.start
then failed with `unknown app_id: mempool` because load_manifests drops the
umbrella manifest whenever the three split-stack members are loaded — leaving
nothing to recreate from and the stack down.

Two fixes:
- prod_orchestrator start/stop/restart now resolve the historical
  `mempool`/`mempool-web` id to the split members (archy-mempool-db,
  mempool-api, archy-mempool-web) whenever the umbrella manifest was dropped —
  the same alias install already implements ("installing mempool assembles the
  split stack"). Restart falls back to start for members whose container/unit
  is gone. Legacy umbrella-only nodes are unaffected (alias inactive while the
  umbrella manifest is live).
- is_missing_container_error (3 sites) now recognizes podman 5.x's
  `no such object` inspect phrasing, so a genuinely absent container is
  classified as missing instead of surfacing as a hard inspect error.

Tests: 2 new alias tests + 2 classifier tests; prod_orchestrator suite 61/61
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 19:34:46 -04:00
archipelago
9ce8a0df05 fix(orchestrator): normalize YAML-folded newlines in command drift comparison
Second false-drift source found live on .228 right after the entrypoint
split fix: a YAML `>-` script with more-indented continuation lines
keeps literal newlines in custom_args, but shell_join flattens them to
spaces when writing the quadlet Exec= line, so Config.Cmd stores spaces
where the manifest has newlines. bitcoin-knots and fedimint-gateway
read as permanently command-drifted over line breaks alone (only the
restart-sensitive guard kept them from recreate-looping like electrumx).

Normalize both sides the way shell_join does before comparing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 17:27:50 -04:00
archipelago
77c5f5641a fix(orchestrator): stop quadlet entrypoint split from reading as permanent command drift
container_command_drifted compared the manifest's entrypoint and
custom_args against Config.Entrypoint and Config.Cmd separately, but
quadlet's Entrypoint= takes a single value, so a manifest entrypoint of
[sh, -lc] is written as Entrypoint=sh + Exec=-lc ... and podman reports
entry=[sh], cmd=[-lc, script]. Same argv, different split — every
quadlet-created app with a multi-element entrypoint read as drifted
forever. On .228 this recreated electrumx on every reconcile pass (114
times in 6h); bitcoin-knots and fedimint-gateway showed the same false
drift and were only spared by the restart-sensitive guard.

Compare the concatenated argv (entrypoint ++ args vs Entrypoint ++ Cmd)
instead — that is what actually runs. When the manifest declares no
custom_args, only the entrypoint prefix must match, since the trailing
Cmd may be the image's baked-in CMD.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 17:08:41 -04:00
archipelago
4665e497d7 feat(security): move secret env out of podman inspect and Quadlet unit files
Secret env used to merge into manifest.app.environment, landing in
'podman inspect' Config.Env on the API backend and — worse — as
plaintext Environment= lines in Quadlet unit files on disk. Now:

- expand_and_partition_env (container crate, pure + tested) expands
  ${KEY} placeholders and splits env into plain entries and
  secret-bearing pairs. Plain entries that interpolate a secret
  (btcpay's Password=${BTCPAY_DB_PASS} connection strings) are
  tainted and travel as secrets too. Secret values themselves are
  never expanded (a generated value containing '${' passes verbatim).
- values register as podman secrets: stdin (never argv/tempfile),
  --replace, content-hash label to skip no-op rewrites; a per-app hash
  cache in the orchestrator makes steady-state reconciles free of
  podman secret calls. Registration goes through the runtime trait
  (default no-op keeps mocks/docker inert).
- containers reference secrets by name: secret_env map in the libpod
  create spec, Secret=<name>,type=env,target=<KEY> in Quadlet units.
  Verified empirically on fleet podman 5.4.2: value absent from
  inspect Config.Env, runtime injection works rootless.
- rotation detection: io.archipelago.secret-env-hash container label
  (API) / the changed unit bytes (Quadlet). Pre-upgrade containers
  lack the label, so every secret-bearing app recreates ONCE on the
  first reconcile after deploy — deliberate, it scrubs the plaintext
  secrets out of existing container configs. Data dirs untouched.
- docker dev fallback keeps plain -e injection (no secret store);
  podman secrets persist across uninstall, matching the
  preserve-credentials invariant (reinstall re-registers by hash).

In-container /proc/<pid>/environ is unchanged — env remains the
app-compat contract; the closed leaks are inspect output and unit
files on disk.

Tests: archipelago-container 61/61 (3 new: taint partition, verbatim
secrets, hash order-independence), archipelago container:: 160/160
(fedimint install test now asserts the secret arrives as a ref, not
env; quadlet render test asserts Secret=/Label= lines). NEEDS the
on-node gate re-run before the item counts as verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:55:15 -04:00
archipelago
2c8c99fd28 fix(security): bind seq into mesh signatures (v2 preimage), guard DID slice, cfg-gate dev password
- mesh: verify_signature accepts a v2 preimage (t,v,ts,seq) alongside
  legacy v1 (t,v,ts); signed_with_seq() is the v2 sender path, not yet
  wired — senders stay v1 until the fleet verifies v2 (receivers
  hard-drop bad sigs, so flipping send-side first would break
  mixed-fleet alerts). Tests: v2 verify, v2 seq-tamper rejection,
  v1 sign-then-set-seq compat.
- mesh listener: malformed radio-supplied DID shorter than the
  'did🔑' prefix can no longer panic advert_name (slice -> .get()).
- auth: the pre-setup password123 dev login and the constant itself are
  now #[cfg(debug_assertions)] — no release binary carries the bypass,
  whatever its runtime config says.
- orchestrator: canned host-facts under #[cfg(test)] — awaiting real
  subprocesses under tokio's paused test clock deadlocks against
  auto-advanced timers (the old blocking detection only worked by never
  yielding).
- drop two now-unused std::process::Command imports left by 4c75bb3d.

Tests: mesh 110/110 (incl. 2 new), api 68/68, container 159/159,
archipelago-container check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:49:52 -04:00
archipelago
4b4a1f88fb feat(security): enforce trusted-registry image policy at the orchestrator pull sites
Catalog- and manifest-supplied image refs reached pull_image without
ever passing the RPC boundary's validator — a malicious catalog entry
or manifest could pull from an arbitrary registry. The allowlist now
lives in container::image_policy (the RPC check delegates to it) and
both orchestrator pull sites (install_fresh and
ensure_resolved_source_available) refuse refs that fail it.

The shared policy accepts trusted-registry refs and registry-less
Docker Hub shorthand (grafana/grafana etc., used by 8 shipped
manifests — a registry-less ref cannot name an attacker host), and
rejects explicit non-allowlisted hosts, shell metacharacters, and
malformed refs. §A of the 1.8.0 hardening plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 09:52:31 -04:00
archipelago
4c75bb3d38 perf(async): remove blocking std::process::Command from async paths
Every production process spawn reachable from a tokio worker now uses
tokio::process: the install path's podman-port probe, the dependencies
disk check, factory-reset restart, config host-IP detection, the
orchestrator's host-facts helpers (resolve_dynamic_env and its call
sites made async to carry it through), and AutoRuntime's podman/docker
probes.

The FIPS transport probe is the special case: is_available() is a sync
trait method called from async route(), so instead of blocking ~50ms
on systemctl per stale-cache hit it now serves the cached value and
refreshes on a background thread (stale-while-revalidate) — bounded
staleness, zero stalled workers.

§C of the 1.8.0 hardening plan; container/transport/config/package
suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 09:00:50 -04:00
archipelago
01cbec27ed fix(robustness): surface swallowed persistence-write failures + federation tombstone durability
§C of the 1.8.0 hardening plan: persistence writes whose Results were
silently dropped now log a warn/error with context (mesh contact
blocklist, scheduler state, content catalog, container registry,
update state, bitcoin relay, package install markers, server shutdown
state). §I: federation tombstones are now flushed durably in
storage/sync so cleared peers can't resurrect after a crash.

Tracker updated with shas in docs/1.8.0-RELEASE-HARDENING-PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:02:54 -04:00
archipelago
f5d2479605 Merge branch 'iso-feedback-fixes-2026-07-02' into merge-iso-feedback
# Conflicts:
#	core/archipelago/src/api/rpc/middleware.rs
2026-07-02 08:03:25 -04:00
archipelago
c375ecc441 fix: fresh-ISO feedback bug-bash — onboarding, status truthfulness, recovery, kiosk, logs
Fixes from real fresh-install feedback (Framework node .81) + its log bundle:

Backend:
- websocket: subscribe before initial snapshot — broadcasts in the gap were
  silently lost, stranding clients on stale state until a hard refresh
  (the "everything needs ctrl-r" bug: My Apps stuck Loading, App Store
  stuck Checking, containers-scanned never arriving)
- crash recovery: check the crash marker BEFORE writing our own PID —
  recovery had never run on any node (always saw its own PID and skipped);
  PID-reuse guard via /proc cmdline
- boot status: pending-boot-starts registry (recovery, stack recovery,
  reconciler, adoption) — scanner overlays queued-but-down apps as
  Restarting instead of Stopped after a reboot; scanner-authored
  Restarting resolves immediately on a settled scan (no transitional wedge)
- install deps: bounded wait (36x5s) when a dependency is installed but
  still starting ("Waiting for Bitcoin to start…") instead of instant
  rejection; dependency-gate rejections remove the optimistic entry (no
  phantom Stopped tile) and surface as a notification
- seed backup: auth.setup persists the onboarding mnemonic as the
  encrypted seed backup (reveal previously failed on EVERY node — nothing
  ever wrote master_seed.enc); seed.restore stashes too; error sanitizer
  lets seed/2FA errors through instead of "Check server logs"
- lnd: bitcoind.rpchost resolved from the running Bitcoin variant
  (hardcoded bitcoin-knots broke Core nodes); manifest uses derived_env
- bitcoin status: clean human message for connection-reset/startup; raw
  URLs + os-error chains no longer reach the app card
- fedimint-clientd: chown /var/lib/archipelago/fmcd to 1000:1000 (root-
  created dir crash-looped the rootless container, EACCES) — first-boot
  script + pre-start self-heal
- log volume (>1GB/day on a day-old node): journald caps drop-in (ISO +
  bootstrap self-heal), bitcoind -printtoconsole=0 everywhere (90% of the
  journal was IBD UpdateTip spam), tracing default debug→info

Frontend:
- Login: Enter advances to confirm field then submits; submit always
  clickable with inline errors (was silently disabled on mismatch);
  Restart Onboarding needs a confirming second click (the mismatch →
  "onboarding restarted" trap)
- sync store: 30s state reconciliation + refetch on re-entrant connect;
  20s containers-scanned escape hatch so Checking can never show forever;
  fresh empty node reaches the real "no apps yet" state
- intro video: CRF20 re-encode (SSIM 0.988) + faststart — moov was at EOF
  so playback needed the full 15MB first (the intro lag)
- backgrounds: 10 heaviest JPEGs → WebP q90 (9.4MB→6.6MB); 7 stayed JPEG
  (WebP larger on noisy sources)
- Web5ConnectedNodes: drop unused template ref that failed vue-tsc -b

ISO/kiosk:
- nginx: /assets/ 404s no longer cached immutable for a year; HTTPS block
  gained the missing /assets/ location (served index.html as images)
- kiosk: launcher/service spliced from configs/ at ISO build (stale
  heredoc force-disabled GPU); MemoryHigh/Max 1200/1500→2200/2800M (kiosk
  rode the reclaim throttle = the lag); firmware-intel-graphics +
  firmware-amd-graphics (trixie split DMC blobs out of misc-nonfree)

Verified: cargo test 898/898 green, npm run build green with dist
contents confirmed (webp refs, lnd.png, faststart video, new strings).
Handover for ISO build + deploy: docs/HANDOVER-2026-07-02-iso-feedback.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 08:00:39 -04:00
archipelago
936b4cca29 fix(orchestrator): self-heal ANY installed app, not just baseline ones
The boot reconciler only self-healed a fully-absent container for one of
8 hardcoded "required baseline" apps (bitcoin-knots, electrumx, lnd,
mempool*, filebrowser, fedimint-clientd) — every other genuinely-installed
app whose container went missing (crash, lost record, wedged teardown)
was left as Left("absent") forever, with no path back short of an
explicit manual reinstall.

Surfaced live: indeedhub's backend containers (minio/postgres/relay) went
absent on .116 and never recovered despite indeedhub still being
installed. By the time this code path runs, the app is already confirmed
NOT user-stopped and NOT user-uninstalled (both checked earlier in the
same function, backed by durable markers correctly cleared on
reinstall/start) — so gating self-heal further behind a hardcoded app-id
list was an unnecessary restriction, not a safety measure. An app the
user installed and never removed should come back on its own, same as
baseline services always have.

Deleted the now-dead is_required_baseline_app(); updated the test that
had locked in the old (wrong) behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 17:27:16 -04:00
archipelago
4c3aa8cc8e fix(icons): remove remaining electrs icon references, use electrumx.png
GoalDetail.vue, EasyHome.vue, and the backend's docker_packages.rs
metadata still pointed electrs-family app ids at the old electrs
icon (svg). Point them at electrumx.png like every other reference,
and delete the now-unused electrs.svg asset.
2026-07-01 14:48:14 -04:00
archipelago
ed95d54ffe chore(assets): replace lnd icon svg with png
lnd.svg no longer exists; every reference now points at lnd.png.
2026-07-01 14:41:15 -04:00
archipelago
d0710e7491 fix(orchestrator,content): bound repair-recreate loops; self-heal stale content catalog entries
- prod_orchestrator.rs: the boot reconciler's zombie-guard and start-failed
  recreate paths (Created/Stopped/Exited states) had no attempt cap, unlike
  health_monitor's independent restart tracker. A container whose entrypoint
  fatally crashes right after `podman start` succeeds got stop+remove+
  install_fresh'd every ~30s reconcile tick forever (portainer on .198,
  2026-07-01: a DB schema newer than the pinned binary could read -- no
  amount of recreating fixes that). Added a 5-attempts/30-minute circuit
  breaker; once exhausted the container is left alone with an error! log
  instead of looping, and an explicit install/start clears the counter.
- content_server.rs: serve_content now prunes a catalog entry whose backing
  file is missing on disk, instead of leaving it advertised to every peer
  forever with no way to distinguish "gone" from "transient failure."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 08:19:54 -04:00
archipelago
5b7cd5d5d0 fix(orchestrator): durable uninstall marker for baseline apps + archival-bitcoin/version-report gaps
- mempool-api now declares dependencies:[bitcoin:archival] directly, closing a
  gap where installing it standalone (a legitimate direct orchestrator-install
  target) bypassed the mempool umbrella's pruning gate entirely.
- New durable user-uninstalled marker (crash_recovery.rs, mirrors user_stopped)
  fixes required-baseline-app self-heal (bitcoin-knots/electrumx/lnd/mempool/
  etc.) resurrecting itself after an explicit uninstall survives a restart or
  reboot, since the in-memory disabled set is wiped by every load_manifests().
- installed_version() (set_config.rs) no longer trusts a floating image tag
  ("latest") as the reported running version -- a stale local :latest cache
  reported "latest" forever regardless of what latest had moved on to. Now
  falls back to asking the Bitcoin backend directly via `bitcoind --version`
  when the tag is floating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 06:29:11 -04:00
archipelago
306b6356ee fix(orchestrator): generalize launch-port fallback + archival-bitcoin dependency gating
Master-plan backlog §10b/§10c: replace two per-app-hardcoded lookups with
generic, manifest-driven behavior so future apps are covered automatically
instead of needing a code edit.

- extract_lan_address (docker_packages.rs) now skips container-side ports
  that are known non-HTTP (SSH, FTP, common DB ports) instead of blindly
  taking podman's first-listed port. Fixes the whole class of bug the gitea
  SSH-before-web static override was a one-off patch for.
- requires_unpruned_bitcoin (dependencies.rs) now checks the app's own
  manifest for a `bitcoin:archival` dependency declaration first, falling
  back to the old hardcoded id list. electrumx and mempool manifests now
  declare it explicitly as the proof case.

869/869 Rust tests green, catalog drift clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 03:59:00 -04:00
archipelago
df9d3a55be integration: preserve deployed 1.8.0 OTA work 2026-06-30 05:08:17 -04:00
archipelago
095a76cd20 fix(bitcoin): bulletproof multi-version switching (Knots & Core)
Three stacked bugs made "switch version" silently fail / crash-loop, and
the data-access mismatch corrupted a node's index during recovery attempts.

Backend renderer:
- sync_quadlet_unit ignored the per-app pinned version and re-rendered the
  quadlet with the manifest's :latest every reconcile tick, reverting any
  switch. Factor the install-time catalog/pin resolution into a shared
  resolve_catalog_image() and call it in BOTH install_fresh and
  sync_quadlet_unit.
- The renderer folded manifest `entrypoint: ["sh","-lc"]` into Exec=, which
  only worked when the image entrypoint was a passthrough shell wrapper. The
  versioned images use ENTRYPOINT ["bitcoind"], so Exec=sh -lc ... became
  `bitcoind sh -lc ...` and crash-looped. Emit a real Entrypoint= override;
  exec_changed now also compares Entrypoint=.

Images:
- Build all bitcoin images (Core + Knots, every version) as container-root
  (USER removed) like the legacy :latest image. Chain data is owned by the
  data_uid (container uid 102); root reads it via CAP_DAC_OVERRIDE (granted in
  the manifest). A non-root USER (the previous uid 1000) can't read existing
  chain data → "Error initializing block database". Still fully rootless:
  container-root maps to the unprivileged host service user.

Catalog:
- bitcoin-knots versions[]: 29.3.knots20260508/20260507/20260210 +
  29.2.knots20251110, "latest" tracking newest.
- bitcoin-core versions[]: add 29.2 + a "latest" entry. All images rebuilt
  root and published to the mirror.

Frontend:
- AppSidebar version dropdown: rename the latest option to "Always use the
  latest version" (no v prefix), fix right padding, and guarantee the current
  selection matches a real option (was rendering blank).
- New InstallVersionModal: full-screen version chooser shown from the App
  Store / Discover install button for multi-version apps (Bitcoin Knots/Core),
  app icon + "Install <name>", latest pre-selected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 05:46:04 -04:00
archipelago
6aa74c7386 feat(bitcoin): multi-version support for Core & Knots (install/switch/pin/auto-update)
Lets a node runner choose which Bitcoin Core / Knots version to install
(latest pre-selected), then switch, pin, or opt into auto-update from the
app's interface — all manifest/catalog-driven, rootless, signed-registry,
zero-data-loss. Motivated by upcoming BIP-110 signalling: runners need a
real choice of software version.

Backend:
- version_config.rs: per-app pin + auto-update persistence (atomic, merge-
  preserving), downgrade detection, auto-update enumeration (+ unit tests).
- app_catalog.rs: CatalogVersion / versions[] schema, catalog_versions(),
  catalog_image_for_version() (same-repo guard); a pin suppresses the update
  badge.
- prod_orchestrator.rs: pinned version wins over the catalog default on every
  install/recreate.
- install.rs: install-time `version` param persisted (default = unpinned).
- set_config.rs: package.versions (read) + package.set-config (write) RPCs;
  downgrade is gated behind explicit confirm (warn + confirm + allow).
- update.rs/main.rs: hourly per-app auto-update tick via the orchestrator
  (opt-in, pin-respecting); fix handle_package_update to be non-fatal for
  orchestrator-managed apps lacking a catalog primary image (bitcoin-core).

UI:
- MarketplaceAppDetails.vue: install-time version selector (shown when an app
  offers >=2 versions).
- appDetails/AppSidebar.vue: "Version & Updates" card (switch / pin / auto-
  update toggle / downgrade warning), per app.
- rpc-client.ts + en.json: RPC methods, types, strings.

Phase 0 image pipeline:
- scripts/build-bitcoin-image.sh: download official tarball + SHA256SUMS(.asc),
  verify SHA-256 + pinned-maintainer OpenPGP signature (fail-closed), build a
  minimal rootless image, smoke-test, tag + push.
- apps/bitcoin-core/Dockerfile rewritten (drops stale community base);
  apps/bitcoin-knots/Dockerfile added.
- generate-app-catalog.sh: emit curated versions[]; published + catalog now
  offers Core 25.2/26.2/27.2/28.4/29.3/30.2/31.0 + Knots 29.3.knots20260508.

docs/bitcoin-multi-version-design.md: live progress tracker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:46:17 -04:00
archipelago
83344b9f3a fix(orchestrator): drop legacy mempool umbrella manifest on catalog-driven nodes
The split-mempool-stack guard that skips the legacy monolithic `mempool`
manifest (whose container collides with its split-stack frontend member
`archy-mempool-web`) only ran over DISK manifests. On catalog-driven nodes
(no disk manifests — e.g. the Phase-3/registry-manifest path), the legacy
`mempool` manifest arrives via the registry-catalog overlay AFTER that
guard, so both `mempool` and `archy-mempool-web` end up owning container
`mempool` and rewrite+restart each other forever ("port binding drift" /
"network alias drift" loop observed on .228, leaving mempool down).

Enforce the guard once more over the merged (disk + catalog) manifest set:
drop the `mempool` umbrella whenever all three split members are present.
Installing `mempool` assembles the split stack, so `archy-mempool-web`
owns the frontend container either way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:04:41 -04:00
archipelago
4519dbf04f fix(orchestrator): render manifest certs on the adopted-running reconcile path
WS-F #10: a netbird reinstall that adopts a leftover running container
skipped ensure_manifest_certs, so when its data dir was wiped the self-
signed tls.crt/key were never regenerated; the next nginx.conf rewrite +
restart then died on the missing cert (proxy 502, login broken). The
Running branch of ensure_running_with_mode now calls ensure_manifest_certs
before ensure_manifest_files, mirroring prepare_for_start's certs-before-
files ordering. Idempotent: a no-op when crt+key already exist.

Live-validated on .228: deleted netbird tls.crt/key under a Running
container; reconciler regenerated a fresh CN=<host_ip> self-signed cert
(1000:1000), https :8087 = 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:49:50 -04:00
archipelago
fd3a4ee4ef fix(orchestrator): chown the whole fresh bind subtree, not just the leaf
ensure_bind_mount_dirs chowned a freshly-created no-data_uid bind dir
with --reference={immediate_parent}. For a NESTED bind source like
jellyfin's /var/lib/archipelago/jellyfin/config (or netbird's .../netbird/
data), `mkdir -p` creates the intermediate <app> dir root:root too, so
referencing the immediate parent just copied ROOT — leaving the dir
unwritable and the app EACCES-crash-looping on reinstall (found by the
all-apps-lifecycle pass: jellyfin "/config/log denied" exit 139;
netbird-server "unable to open database file"). It only ever worked for
direct children of the data root (immich).

Fix: anchor to the nearest PRE-EXISTING ancestor (the rootless data root,
owned by the service user) and chown -R the entire newly-created subtree
to it. Extracted the walk into fresh_subtree_anchor() with a unit test
covering nested / direct / second-volume cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 04:46:35 -04:00
archipelago
71cc9ac46a fix(uninstall): bound systemctl/podman teardown so uninstall can't hang
Uninstalling immich/grafana could hang with a frozen full-red progress
bar, leave a ghost entry stuck in My Apps, and then refuse reinstall.
Single root cause: quadlet::disable_remove() — called first in the
uninstall task (via companion + orchestrator teardown) — ran
`systemctl --user stop`, daemon-reload, and `podman rm -f` with NO
timeout. On rootless podman a generated unit can wedge in "deactivating"
while podman hangs underneath, so `systemctl stop` blocks forever. The
spawned uninstall task then never returns Ok or Err, so:
  - set_uninstall_stage() (after the stop) never fires → progress frozen;
  - remove_package_state_entry() never runs → entry stranded in
    `Removing` → ghost in My Apps;
  - the install guard rejects reinstall with "already Removing".

The spawn wrapper already reverts state on Err and removes the entry on
Ok — the only failure mode was a hang that returns neither. Bound the
teardown so it always terminates:
  - systemctl stop → QUADLET_STOP_TIMEOUT, escalate to kill+reset-failed
    on timeout (reuses the existing helpers);
  - daemon_reload_user() → bounded systemctl_user_status (30s);
  - defensive `podman rm -f` → wrapped in tokio timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 04:27:02 -04:00
archipelago
0a8db9044f fix(orchestrator): recreate zombie "Up" containers whose process is dead
podman trusts its own state DB: when a container's conmon dies without
podman observing it (cgroup-cascade SIGKILL on archipelago.service
restart, a crash), `podman ps` keeps reporting it "Up" long after the
process is gone. The reconciler NoOp'd such a zombie forever, so a dead
dependency with no published host port never recovered.

Observed live on .228 (2026-06-25): netbird-dashboard reported "Up" with
a dead State.Pid → its nginx proxy 502'd → NetBird login broke
("Unauthenticated"). The dashboard publishes no host port, so the
Running branch had nothing to probe and never recreated it.

Add a zombie guard to the Running branch: verify the recorded State.Pid
is alive (its /proc entry exists) before trusting "running"; on a
concrete dead PID, stop+remove+install_fresh from the manifest.
Conservative by design — any uncertainty (inspect failed, PID
unparseable) assumes alive, so a transient podman hiccup never destroys
a healthy container. Unit test covers live/dead/out-of-range PIDs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:25:52 -04:00
archipelago
89d397bb74 refactor(netbird): delete legacy Rust installer — #20 ph4 (manifest-driven only)
netbird is fully manifest-driven (apps/netbird-*/manifest.yml via the signed
catalog): install_stack_via_orchestrator renders the 3-member stack with
generated_certs (self-signed TLS for the #15 OIDC secure context), base64
generated_secrets, and templated config — and adopts the running stack by live
container name. The hardcoded `podman run` fallback was therefore dead code on
any node with the embedded catalog (verified live: .228 https:8087 -> 200).

Removes the per-app Rust installer anti-pattern the master plan calls out:
- install_netbird_stack: orchestrator -> adopt -> bail! (no in-Rust installer)
- deletes 6 now-dead helpers (write_netbird_config_files, ensure_netbird_tls_cert,
  read_or_generate_b64_secret, netbird_net_resolver_ip, detect_netbird_public_host_ip,
  wait_for_netbird_oidc_ready), 3 NETBIRD_*_IMAGE consts, unused base64::Engine import
- ~485 lines removed; prod_orchestrator doc-comments updated

Behavioural parity: the manifest path already executed on the fleet, so this
changes no live behavior. The legacy #10 OIDC-readiness wait was already bypassed
by the manifest path; if that race resurfaces, add an OIDC-ready gate to the
manifest rather than resurrecting the Rust fn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:04:01 -04:00