diff --git a/core/archipelago/src/bootstrap.rs b/core/archipelago/src/bootstrap.rs index caa8f08d..f28873ac 100644 --- a/core/archipelago/src/bootstrap.rs +++ b/core/archipelago/src/bootstrap.rs @@ -130,6 +130,11 @@ pub async fn ensure_doctor_installed() { Ok(false) => debug!("Bitcoin RPC bind settings already usable"), Err(e) => warn!("Bitcoin RPC repair failed (non-fatal): {:#}", e), } + match run_apps_dir_repair().await { + Ok(true) => info!("Populated /opt/archipelago/apps from installer copy at /etc/archipelago/apps"), + Ok(false) => debug!("/opt/archipelago/apps already populated (or no installer copy)"), + Err(e) => warn!("Apps dir repair failed (non-fatal): {:#}", e), + } match run_journald_dropin().await { Ok(true) => info!("Installed journald log-volume policy drop-in"), Ok(false) => debug!("journald log-volume policy already in place"), @@ -402,6 +407,39 @@ fn path_dot(path: &Path) -> String { p.to_string_lossy().to_string() } +/// ISO installs before the auto-install.sh path fix copied the app manifests +/// to /etc/archipelago/apps while the backend loads them from +/// /opt/archipelago/apps — so fresh nodes had ZERO disk manifests and only +/// catalog-covered apps could install (netbird "manifests not available", +/// framework node 2026-07-14). Self-heal: when /opt has no manifests and the +/// installer copy exists, populate /opt from /etc. Never overwrites existing +/// /opt manifests (OTA runtime-assets sync owns those afterwards). +async fn run_apps_dir_repair() -> Result { + let script = r#" +set -eu +src=/etc/archipelago/apps +dst=/opt/archipelago/apps +[ -d "$src" ] || exit 0 +# Only heal when the destination has no manifests at all. +if [ -d "$dst" ] && [ -n "$(ls -A "$dst" 2>/dev/null)" ]; then exit 0; fi +ls "$src"/*/manifest.yml >/dev/null 2>&1 || exit 0 +mkdir -p "$dst" +cp -r "$src"/. "$dst"/ +exit 2 +"#; + let status = host_sudo(&["sh", "-lc", script]) + .await + .context("populate /opt/archipelago/apps from installer copy")?; + match status.code() { + Some(0) => Ok(false), + Some(2) => Ok(true), + _ => { + warn!("Apps dir repair helper exited with {}", status); + Ok(false) + } + } +} + async fn run_bitcoin_rpc_repair() -> Result { // Older installs can have a container-owned bitcoin.conf with only rpcauth // and printtoconsole. Repair it at startup so OTA fixes existing nodes @@ -421,6 +459,12 @@ ensure_line() { fi } ensure_line server=1 +# rpcbind=0.0.0.0 is required inside the container: with rpcallowip set but +# no rpcbind, bitcoind binds RPC to the container's loopback only and every +# dial over the container network (LND, bitcoin-ui) is refused — the fresh- +# install "LND took 5 attempts" / bitcoin-rpc 502 failure (host publish stays +# 127.0.0.1-only, so exposure is unchanged). +ensure_line rpcbind=0.0.0.0 ensure_line rpcallowip=0.0.0.0/0 ensure_line listen=1 # Log-volume fix: printtoconsole=1 duplicated every log line (incl. per-block diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index abe90baa..d3078656 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -2482,6 +2482,12 @@ fi if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/target/etc/archipelago/ + # The backend loads disk manifests from /opt/archipelago/apps — copying + # only to /etc/archipelago/apps left fresh installs with ZERO manifests, + # so any app not covered by the signed catalog (e.g. netbird) could not + # install at all. Ship them where the orchestrator actually looks. + mkdir -p /mnt/target/opt/archipelago/apps + cp -r "$BOOT_MEDIA/archipelago/apps/." /mnt/target/opt/archipelago/apps/ fi # Copy pre-bundled container images