a5d81fd5ffaa63888c6387b02d1d48f29291a77d
351
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f10a0d5393 |
fix(nginx): forward the websocket upgrade to mempool — the UI is ws-driven
The dashboard's /app/mempool/ proxy forwarded plain HTTP only: no Upgrade/Connection headers, so the browser's /api/v1/ws handshake reached mempool-api as a bare GET and Express 404'd it. The mempool page loaded fine and every REST probe was green while the user saw a UI that never connects — the backend was fully healthy and completely unreachable in the only way that matters to the page. This hid behind the electrumx initial-sync outage: once sync finished and the API answered, "mempool works" was declared from REST checks while the websocket path stayed dead. Reported by the operator from the browser, which is the only place it was visible. Adds Upgrade/$http_upgrade + Connection "upgrade" to the /app/mempool/ location in both shipped sources (image-recipe snippet + scripts copy). Live-verified on the dev node: ws through the proxy now answers 101; the control probe without upgrade headers reproduces the 404 signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6f1efcbf7e |
Merge remote-tracking branch 'gitea-ai/gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf'
Demo images / Build & push demo images (push) Successful in 3m14s
|
||
|
|
24e378c421 |
fix(tor): make the helper's restart actually recover Tor, and stop lying about it
The dashboard's "Restart Tor" button dispatched to this helper and always
got {"ok":true} back. Two real defects, in order of importance — and one
disproved theory, recorded so nobody re-chases it:
1. reset-failed was missing. Once tor@default fails enough times systemd
latches "Start request repeated too quickly" and refuses to start it at
all; a plain restart is then a no-op no matter which unit you name.
All three fleet nodes found dead on 2026-08-09 were in exactly that
state, which is why the button appeared to do nothing.
2. The result was unconditional. The write-torrc branch waited up to 30s
for SOCKS and then ignored the outcome; the restart branch slept 3s and
claimed success. The UI reported "restarted" over a dead daemon.
Disproved: this was NOT wrong-unit targeting. `systemctl restart tor`
does propagate to tor@default — measured on austin-sapien, MainPID
changed. tor@default is still addressed explicitly because it is the
unit carrying the failed state worth resetting.
restart_tor_daemon() now: reset-failed, restart tor@default (fall back to
tor on single-instance installs), wait up to 30s for SOCKS on 9050, and
return {"ok":false,"error":...} pointing at journalctl when it never
comes up. Callers may no longer report success without a live SOCKS port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
96acd388d7 |
fix(catalog): generate against the fleet's trust floor, not the repo's host
sign-catalog.sh refused to sign a freshly regenerated catalog:
REFUSING: the catalog names registry hosts the deployed fleet does not
trust. source.archipelago-foundation.org — 78 image refs
It was right, and the check earned its place. apps/*/manifest.yml moved to
the public domain in
|
||
|
|
ec8d88a692 |
fix(bitcoin): pin Knots to an explicit build; :latest could stall the fleet
The catalog's default version was {"version": "latest", image:
bitcoin-knots:29.3.knots20260508, default: true}, so a fresh install — or
anyone selecting "latest" — silently got the BIP110/RDTS build. Meanwhile
the manifest's own :latest tag resolves to 29.3.knots20260210 in the
registry, so the two paths installed different daemons.
That matters because 20260508 does not merely enable RDTS, it HALTS until
an operator sets consensusrules=rdts. Node 100.64.204.114 runs it and is
frozen at block 961,692 — blocks AND headers static across 30s, 11 peers,
unpruned — while reporting initialblockdownload=false and
verificationprogress 0.99998, i.e. "synced". The two nodes on 20260210 sit
at the tip, 961,706. Re-pointing a tag could have done that to every node
at once.
Pinned in all three places that would otherwise re-introduce a moving tag:
the manifest, image-versions.sh, and the catalog VERSIONS table. The
"latest" pseudo-version is removed outright — a moving default is not
appropriate for a consensus-critical app — and the default is now the
explicit non-RDTS 29.3.knots20260210. 20260508 remains selectable, so
adopting RDTS stays possible but becomes deliberate.
Verified: both tags' daemons read via `bitcoind --version` — :latest and
:29.3.knots20260210 are both v29.3.knots20260210, so this pin is
behaviour-neutral on nodes already running latest.
NOT DONE HERE: releases/app-catalog.json still carries the old signed
entry. It must be regenerated and re-signed by the operator before it
takes effect — catalog entries override on-disk manifests (origin-wins).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b1523d3e42 |
merge: bring the open-source readiness work onto the phase-13 branch
Merges gitea-ai/main (65 commits) into the phase-13 branch (419) so one
build carries both lines — the AIUI/assistant/container work and the
open-source readiness work (licensing, the marketplace DID signature layer,
the registry domain migration, the secrets and infrastructure scrub).
Every Rust file auto-merged. The container fixes from this branch and main's
registry-domain migration and node-name genericisation coexist without
manual intervention.
Conflict resolution — all of them were modify/delete, and all were resolved
in main's favour deliberately:
`.planning/**`, `scripts/deploy-to-target.sh` and `scripts/setup-aiui-server.sh`
were deleted by main's `6ba05996` ("security: remove all infrastructure and
internal process material from the repo") and added to .gitignore there.
Keeping this branch's copies would have re-committed internal process and
infrastructure material into a repo being prepared for publication, silently
undoing that cleanup. Resolved with `git rm --cached`, so every file remains
on disk locally and in this branch's history — it is untracked, not lost.
The remaining .planning files this branch added after the merge base were
untracked the same way, so the result is consistent rather than half-tracked.
Container suite 221/221 on the merged tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d15cd58d7f |
feat(lnd): rotate Lightning macaroons from the dashboard, and stop stranding BTCPay
Demo images / Build & push demo images (push) Successful in 3m34s
Rotating LND's macaroons was an SSH-only script, which in practice meant it did not happen — while a macaroon is a bearer token with no revocation and no expiry, so anything that ever read one keeps the ability to spend until they are replaced. Settings → Lightning credentials now does it behind the node password, shows a step checklist, and refuses to report success unless it has confirmed the node identity and channel census are unchanged. Three findings from performing a real rotation on a dev node, each fixed here: 1. BTCPay was left holding a dead credential, silently. Its connection string carries the macaroon INLINE (LND's datadir is owned by its container subuid, so btcpay cannot bind-mount the file), and the daemon only regenerates that secret when LND's TLS cert thumbprint changes — which macaroon rotation does not touch. Result: btcpay up, LND up, both healthy, every Lightning payment failing, nothing anywhere saying why. 2. Rewriting the secret is not enough to fix it. `secret_env_hash` makes the change visible as env drift, but the reconcile loop runs `ExistingOnly` at boot AND periodically, and there it deliberately leaves running restart-sensitive apps untouched — observed once per tick for half an hour on the dev node. So this reuses FED-07's `credential_rotated` carve-out via a new default-no-op `ContainerOrchestrator::mark_credential_rotated`, on the same reasoning: restart sensitivity protects apps that are working, and this one is working only in appearance. The shell script cannot reach an in-process flag, so it removes the container and lets desired-state recovery rebuild it. 3. LND stayed locked forever on a loaded node. The unlocker is only served after channel.db/graph.db/wallet.db open, measured at 2m38s on a box running 30 containers; the unlock helper gave up at ~60s. That is not a harmless retry — reconcile records the post-start hook as failed, restarts LND, and the slow open begins again, so the wallet never opens and every LND-dependent app stays broken. The not-ready budget is now ~10 minutes; a genuinely wrong password still exits on the first pass via `all_rejected`. Safety properties worth not regressing: - No macaroon content in any response, error, log line or the polled progress feed — digests and byte counts only. - Rotation unlocks via a new `unlock_existing_wallet_no_wipe`, so there is no code path from "rotate my credentials" to `recreate_wallet_destructively`. A wallet whose password this node lacks fails the rotation with the wallet intact. - Channels are compared as active+inactive totals, not `num_active_channels`, which legitimately dips after any restart while peers reconnect. - Backup verified by file count before anything is deleted. Verified: cargo check + fmt clean, 6 new unit tests and the 6 existing container::lnd tests pass, vue-tsc clean, and the built bundle contains the three new RPC method names (the frontend build can silently no-op). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ff3b3c860e |
fix(iso): stop printing a web password that doesn't work
The installer's completion screen and the login-console banner both told the operator "Web Login password123". No release build accepts that password: no default account is ever created (`main.rs:356-362`), and the `password123` pre-setup path is `#[cfg(debug_assertions)]` + `dev_mode` (`api/rpc/auth.rs:36-46`). A new user following the screen gets "User not set up. Please complete setup first." on their first-ever interaction with the product. Both screens now say the web UI asks you to create a password on first visit, which is what `Login.vue` actually does when `auth.isSetup` returns false. The SSH line is unchanged — `archipelago`/`archipelago` really does still ship (`install-to-disk.sh:205`), and killing that is the open half of the "kill default credentials" hardening item. Note on the path: `image-recipe/build-debian-iso.sh` is a thin wrapper that copies `_archived/build-auto-installer-iso.sh` and rewrites its relative paths, so despite the directory name the archived builder is the live one. Same string fixed in scripts/install-tui-demo.sh, which mirrors the screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3cd210f282 |
feat(build): build-aiui.sh rejects a prod bundle carrying mock hosts
W1.7's regression gate: after 8329b826's tree-shake fix, this makes the mock-quarantine load-bearing — a future change that reintroduces the mock modules into the production graph fails the build instead of shipping silently. The demo-site build (VITE_DEMO_CONTENT=true) is exempt by design. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
a2efdf7358 |
docs: current-state the bitcoin multi-version design; move its rollout handoff local
bitcoin-multi-version-design.md carried three layers of stale internal content: an 80-line HTML-comment work-tracking block (per-phase status with "UNCOMMITTED on the branch", node numbers, "Next action when resuming", "Decisions still needed from user"); a rendered "Status: design (2026-06-22)" header that was wrong — the feature shipped, all four phases, with the downgrade guard added today; two private `MEMORY →` references; and a node-numbered scheduling note. Now: the comment block is gone, the status reflects reality, the MEMORY references and node numbers are removed, and "verify on a real node" replaced the specific fleet addresses. The design content (source-of-truth decision, phase designs, invariants) is unchanged. Separately, bitcoin-version-bulletproof-rollout.md was an inter-agent rollout handoff — node numbers, branch coordination, "the other agent owns" — not a design or reference doc. Moved to local-only (still on disk, gitignored) like the other handoffs; its two path references (a plan doc and a script comment) are generalized. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
308f3cbd84 |
fix(release): publish the manifest only after assets are proven fetchable
Today's outage window came from ordering, and the ordering was baked into the
publish script itself: it pushed main — the branch nodes read the manifest
from — together with the tag, up front, then uploaded and verified assets
afterward. So the manifest advertised the new version for the entire
upload+verify window. When an upload failed inside that window, every polling
node briefly saw a v1.7.126-alpha update whose binary 500'd and whose tarball
did not yet exist.
Reordered so the manifest goes live last:
1. push the TAG only (the Gitea release and asset URLs hang off it; the tag
alone changes nothing for nodes)
2. upload assets
3. verify every asset downloads in full and matches the manifest sha256/size
4. only then push main — the step that actually triggers nodes
Also fixes a way a bad asset could slip through unnoticed: the inline
verification ran in a `while read` pipe subshell, where its `fail` (exit 1)
terminated only the subshell and let the script continue to "published and
verified". Verification now runs in the main shell via a new
check-release-assets.sh, which fails hard on the first bad asset. The same
script is the reusable by-hand verifier used to recover today's release
(both assets confirmed 200 + sha256-match before the manifest was re-published).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cbfda30579 |
fix(update): never advertise a downgrade as an update; clear every stale BTCPay pin
Demo images / Build & push demo images (push) Failing after 2m22s
The app store offered "update to 2.3.9" on a node already running 2.4.2 — the release that fixes an actively exploited 2FA bypass. Taking it would have rolled the node back onto the vulnerable version. Root cause: available_update_for_images compared tags for inequality only. Same repo + different tag meant "update available", with no ordering. Every version claim upstream of it can go stale — the signed catalog, a legacy catalog entry, the image-versions.sh baseline pin — and any one of them lagging turned into a backwards Update button. Guard added: when both tags parse as dotted-numeric versions, a lower pinned version is never offered. Tags that cannot be ordered (RELEASE.2024-11-07…, 14-vectorchord0.4.3) keep the previous behaviour rather than silently losing updates. This makes stale data fail safe, which matters more than any single pin being correct. Four sources still named 2.3.9, three of them able to act on it: - releases/app-catalog.json — a LEGACY `btcpay` entry, distinct from `btcpay-server`, carrying a concrete 2.3.9 image. catalog_primary_image treats that as authoritative, so this is what drove the button. Fixed, but held back from this commit: it needs re-signing. - scripts/image-versions.sh — the baseline pin used when the catalog does not cover an app. - stacks.rs — the legacy BTCPay installer, twice. The fallback install path would have deployed 2.3.9 outright. - neode-ui curatedApps/marketplaceData and public/catalog.json — the store's displayed version, hardcoded rather than read from the catalog, which is why it still showed 2.3.9 after the update landed. Audited every other installer for the same shape. The remaining literals are the immich stack, which currently agrees with its manifests; hits in set_config.rs and app_catalog.rs are test fixtures. To keep it that way, scripts/check-installer-image-pins.py asserts that any installer literal naming the same repository as an app manifest carries the same tag, and runs blocking in CI. Verified it catches a simulated revert to 2.3.9. Tests: 13/13 in image_versions including the exact BTCPay case, a genuine upgrade still offered, equal versions silent, prerelease suffixes ordered on their numbers, and opaque tags unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d0af38e825 |
feat(release): guard the catalog against publishing untrusted registry hosts
Encodes the sequencing rule that nearly shipped a fleet-wide outage today. The signed catalog is authoritative on every node — catalog_image_override makes its image refs beat the on-disk manifest. TRUSTED_REGISTRIES in the working tree describes a binary being built now; nodes run whatever was last shipped to them. Those two diverge for exactly as long as an OTA takes to reach the fleet, and that window is when regenerating the catalog silently breaks every install with "not from a trusted registry". Regenerating today would have done precisely that: the generator embeds each app's manifest, and those now name the new registry domain, which no deployed binary trusts. - releases/registry-trust-floor.json records the hosts DEPLOYED binaries trust, separately from what the source tree accepts, with the new domain parked under `pending` until an OTA carries it. The migration order is written down there rather than living in someone's memory. - scripts/check-catalog-registry-trust.py compares the catalog's hosts against that floor and explains the ordering fix when they diverge. - sign-catalog.sh runs it as a preflight BEFORE prompting for the mnemonic, so a bad catalog is refused at the last reversible moment. - CI runs it blocking, plus the drift report advisory (drift between a manifest landing and the next signed release is expected, since only the ceremony can close it). Also installs PyYAML in the manifests job. That job passed only because GitHub runners happen to ship ruby, which the validator used to require; it now needs python3+PyYAML. Verified: passes on the published catalog (2 hosts, both trusted); refuses a simulated full regenerate (79 refs on the untrusted domain) and blocks the ceremony without requesting the mnemonic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3086f5addb |
fix(btcpay): pin 2.4.2 (actively exploited 2FA bypass); teach drift check the release catalog
BTCPay 2.4.2 fixes a critical vulnerability that upstream reports as actively exploited: a TOTP two-factor bypass via Greenfield Basic authentication (btcpayserver/btcpayserver#7491). Checked the 2.3.9 -> 2.4.2 breaking changes against how Archipelago actually configures BTCPay; both are inert here: - 2.4.0 removed the LNBank and Lightning Charge backends. Ours is a direct LND connection built by container::lnd::ensure_btcpay_lnd_connection_secret. - 2.4.2 disables Greenfield Basic auth five minutes after account creation. Nothing in the daemon or frontend consumes BTCPay's API. The manifest bump alone does NOT reach nodes: catalog_image_override makes the signed catalog authoritative whenever the image repo matches, so a node would be forced back to 2.3.9. The catalog edit is held locally until the signing ceremony runs, because an unsigned catalog published to main would be accepted by nodes (absent signatures are allowed) and would quietly drop authenticity. check-app-catalog-drift.py only understood app-catalog/catalog.json, where `apps` is a list. releases/app-catalog.json — the SIGNED catalog nodes actually resolve apps through — keys `apps` by id and wraps each app's full manifest under manifest.app. So the checker parsed the file that governs nothing and raised ValueError on the file that governs everything. It now reads both shapes. Running it against the release catalog shows the repo and the catalog agree on content: of 34 image differences, all 34 are the registry host alone and every tag is identical. The remaining version-string drift (v1.18.0 vs 1.18.0, 1.30.0-alpine vs 1.30.0) is cosmetic metadata, not image drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
44b50a39d4 |
fix(registry): repair two regressions from the domain migration; port the manifest validator to python
Fixes 4 test failures introduced by |
||
|
|
8e814ca06a |
feat(registry): move image and OTA references to the public domain
Demo images / Build & push demo images (push) Failing after 2m22s
Replaces the registry host across 86 files: 309 references, covering all 40 app manifests, the orchestrator and container crates, the release and catalog scripts, both demo-images workflows, the ISO builder, demo-deploy, and the frontend marketplace data. Verified the domain actually serves the registry before rewriting anything, rather than assuming the web host implies the registry: - TLS verifies clean, HTTP/2 on the web root - an anonymous token grants a manifest fetch (HTTP 200) with no credentials - skopeo inspect --no-creds resolves an image and lists its tags That last check is the one that matters: an outside developer with no account can now pull, which was the functional blocker for publishing at all. Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop crossing the network in the clear. Deliberately NOT rewritten: - The public FIPS anchor on port 8444. It is a functional network endpoint every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded seeds than to leaked infrastructure. The domain does resolve to the same host, so it could become a hostname, but that adds a DNS dependency to the path used precisely when things are broken. Worth a deliberate decision, not a side effect of this change. - The companion APK on port 2100. The domain returns 404 for that path, so rewriting it would swap a working URL for a broken one. The Releases page does serve (200), which is where the plan already wants those binaries. - releases/app-catalog.json, releases/manifest.json and release-manifest.json. These carry `signature` and `signed_by`; editing their contents invalidates the signature and the fleet refuses artifacts that fail verification. They were rewritten in a first pass and reverted — they must be regenerated and re-signed through the signing ceremony instead, which needs the mnemonic. So the catalog still advertises the old host until that ceremony runs. Nodes resolve images through the signed catalog, not the on-disk manifests, so this commit alone does not change what a node pulls. Verified: archipelago-container 75/75; every manifest still parses with a top-level app block; no signed artifact modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b2c7592840 |
security: parameterize node addresses; drop dead APP_URLS config
Demo images / Build & push demo images (push) Failing after 2m16s
Keeps the dev and test tooling an outside contributor would want, and takes our node addresses out of it. Scripts that silently defaulted to one of our nodes now require an explicit host and exit 2 without one: smoke-test.sh, trust-archipelago-cert.sh, dev-container-test.sh (which also derives its RPC and health URLs from the SSH target instead of a second hardcoded copy), and image-recipe/dev-branding.sh. A default that points at a machine the user does not own is worse than no default: it fails confusingly, or reaches a stranger's device. Usage examples, mock data and test fixtures move to the RFC 5737 documentation range (192.0.2.0/24). CGNAT test values stay inside 100.64.0.0/10 so the range-check semantics they exercise still hold, and 192.168.1.0/.1/.254 are left alone — those are gateway logic and UI placeholders, not our addresses. Playwright and the perf spec defaulted their baseURL to one of our nodes; they now default to localhost:8100, the local dev server. Removed neode-ui APP_URLS entirely. It is dead code — exported, never imported — and it pinned fedimint's *prod* launch URL to 192.168.1.228:8175. Had anything consumed it, every user's node would have tried to reach an address that on their LAN is either nothing or someone else's machine. Deleting beats sanitizing dead config. Verified: frontend 868/868 vitest across 108 files; archipelago-container 75/75; mesh tests 9/9; audit-secrets 5/5. Zero node addresses and zero node names remain in tracked files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6ba0599639 |
security: remove all infrastructure and internal process material from the repo
Demo images / Build & push demo images (push) Failing after 2m13s
The repo is source code and guidelines only. Nothing about how Archipelago's own fleet is run, or how the team works, stays in it. Untracked (kept on disk, gitignored) — 250 files: - .planning/ (199) and loop/ — internal development process - fleet operations tooling that targets specific nodes: deploy-to-target, deploy-tailscale, deploy-config-defaults, setup-target-dev, setup-aiui-server, setup-https-dev, debug-frontend, node-profile, fleet-fips-pair/unpair, image-recipe/sync-from-live.sh - image-recipe/INTEGRATION-GUIDE.md and docs/multinode-testing-plan.md, both of which are live-server workflow and fleet node inventories - the Phase 10 on-node verification and evidence records, which cite .planning/ as their evidence base KEY-05-ENTROPY-ENFORCEMENT.md was initially moved out with the other Phase 10 docs and then put back: it is cited as normative rationale from ten places in the codebase, including core/clippy.toml, which bans rand::thread_rng and points at it for the reason. That makes it a guideline, not an internal record. Node names removed from source (48 occurrences across comments, manifests and test fixtures): archi-dev-box, archy-x250*, shorty-s, framework-pt, zaza-optiplex, archi-thinkpad. Comments keep the engineering context and the date, which is what carried the meaning; the machine name did not. Three of those were live test values rather than comments and were replaced with valid stand-ins, not prose: two mDNS hostnames and a mesh peer name. An earlier pass substituted "a test node" into a hostname assertion, producing an invalid hostname; caught and fixed as test-node.local. Wipe mechanism: .local-only/manifest.txt inventories every local-only path and .local-only/wipe.sh deletes them on one confirmation, refusing to touch anything git still tracks. Both are themselves untracked, so the public repo does not carry a map of internal filenames. Verified: cargo check -p archipelago --all-features clean; archipelago-container 75/75 tests pass; appOrigin vitest 7/7; audit-secrets 5/5; every relative link in tracked markdown resolves (0 broken). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
19082a44f0 |
security: remove node credentials from tracked files (open-source Phase 1)
Demo images / Build & push demo images (push) Failing after 2m28s
Scrubs the fleet SSH/UI password from every tracked file (22 occurrences) and removes inline credentials from the code paths that used them. Docs and trackers keep the surrounding context — these are published under docs/history/ per the open-source plan — with the literals replaced by <FLEET_PW> / <FLEET_PW_ALT> so the "two variants exist" detail survives without the values. Three of the eight files were in .planning/ and were NOT in the plan's enumerated list; the reworked audit-secrets.sh found them. Code changes: - neode-ui/test-openwrt.mjs: node URL and password come from ARCHY_NODE_URL / ARCHY_NODE_PW; the SSH target derives from the URL instead of a hardcoded tailnet IP; exits 2 when unset. - scripts/run-post-install-tests.sh: drops the built-in "testpass123!" default and adds --password-stdin; refuses to run unauthenticated instead of silently trying a known password. --phase1-only still needs no password. - .gitea/workflows/post-install-tests.yml: sshpass with an inline literal replaced by key auth (NODE_SSH_KEY secret); password comes from the NODE_UI_PASSWORD secret and is piped over stdin rather than argv, so it stays out of the node's process list and the job log. Default target IP removed. scripts/audit-secrets.sh now reports 5/5 pass, 0 fail. Note: rotation of the exposed credentials is deliberately deferred to the pre-publish gate and is NOT done by this commit — these values are still live. See Phase 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e3b98ed18f |
fix(security): make audit-secrets.sh actually scan the files that leaked
The audit passed for months while two live Anthropic keys and the fleet SSH password sat in tracked files. Three independent reasons: - ALLOW_PATTERNS was matched against the whole "file:line:content" string, not the path, so bare words like "test", "demo" and "example" dropped any hit whose *content* merely mentioned them. - `\.md$` was in that same allowlist and `--include` never listed *.md or *.yml, so docs and CI workflows — where every real leak has lived — were never scanned at all. - The false-positive filter spelled the single-quote class `\x27\x27`, which GNU grep does not expand in an ERE, so the empty-string rule never fired. Now: scans tracked files via `git ls-files` (exactly the set that would be published), covers md/yml/mjs/kt/toml, allowlists by path only, and adds patterns for credentialed URLs and inline `sshpass -p`. Test fixtures under testdata/ are exempted narrowly rather than by substring. Verified by planting canary secrets in docs/api-reference.md and .gitea/workflows/build-iso.yml — both file types the old version ignored — and confirming the audit fails on them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c810b514ed |
fix(search): AIUI web search was 403ing on every query
SearXNG defaults to `formats: [html]`. Its JSON API answers 403 — and JSON is the only thing AIUI's web search speaks, since `/aiui/api/web-search` proxies straight through to `/search`. Both places that seed settings.yml (the first-boot script and the installer) omitted `search.formats`, so web search has never worked on a node whose SearXNG was installed, running and healthy. It reads as the assistant being unable to search rather than as one missing config key. Verified on archi-dev-box: `format=json` went 403 -> 200, returning 28 results for "bitcoin halving" from Brave and DuckDuckGo. Google and Startpage self-suspend on a self-hosted instance (access denied / CAPTCHA), which is expected and costs little given Brave's independent index. Existing nodes need the same two lines added to /var/lib/archipelago/searxng/settings.yml and a restart; this commit only fixes what new installs get. Also fixes a build break: `fetchLibraryContent` built a bundle literal that predates the images bucket. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
75919a2071 |
fix: cap peer browse, containerise the cert section, serve LAN HTTPS
content.browse-all-peers had a per-peer timeout but no OVERALL budget. On this node that meant >45s with no answer, which the assistant reported to the operator as "having trouble accessing the peer content list". Measured cause: 16 federated peers, 1 reachable. Now bounded to 20s total, returning partial results with peers_reached / peers_total / peers_unreachable / partial, so the assistant can say "1 of 16 peers answered" instead of implying the rest have nothing. Verified on the node: 20.015s, was >45s. NodeCertificateSection had no container — I copied a section that sits INSIDE a card rather than one that provides its own. Now uses the same `glass-card px-6 py-6 mb-6` shell and heading level as every other settings section, so it matches on desktop and mobile. setup-node-ca.sh now also ensures the nginx HTTPS listener, because a CA is useless if nothing serves TLS. It binds LAN addresses ONLY: tailscaled already owns :443 on the tailnet addresses with its own Let's Encrypt cert, so a plain `listen 443 default_server` binds 0.0.0.0 and fails EADDRINUSE — and nginx then keeps running the OLD config while the reload reports success. Hit exactly that on archi-dev-box. Port 80 keeps serving: nodes are reached by IP on LANs where forcing a redirect would strand anyone who has not installed the CA. Live now: https://192.168.63.240/ and https://<host>.local/ both 200 with verify=0 against the node CA, http still 200, tailscaled's 443 untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bd98ec6e3d |
fix(tls): leaf key must be readable by the daemon, not just root
Found on archi-dev-box the moment the gate tried to serve TLS: the key was installed root:root 0600, nginx's master reads it as root, but the archipelago daemon runs as User=archipelago and got "Permission denied (os error 13)". Every app port then quietly stayed plain HTTP — the exact fail-open shape the gate exists to prevent, and it would have looked like "TLS just doesn't work" with no obvious cause. The warn-level log the tls module deliberately emits for a present-but-unloadable certificate is what turned this into a ten-second diagnosis instead of a hunt; it earned its keep on its first real deployment. Key is now group-owned by the service user at 0640, with a fallback to the user's primary group and a clear message when no such user exists. Nothing wider than that. Verified on the node afterwards, on one gated port (8096): https 401 verify=0 TLS terminated, chain valid against the node CA http 401 same port, plain HTTP, unchanged no CA verify=20 untrusted client correctly rejected The reissued key was also picked up with NO daemon restart — the mtime reload path proven in production, not just in a unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1dfd9e720b |
fix(tls): leaf key must be readable by the daemon, not just root
Found on archi-dev-box the moment the gate tried to serve TLS: the key was installed root:root 0600, nginx's master reads it as root, but the archipelago daemon runs as User=archipelago and got "Permission denied (os error 13)". Every app port then quietly stayed plain HTTP — the exact fail-open shape the gate exists to prevent, and it would have looked like "TLS just doesn't work" with no obvious cause. The warn-level log the tls module deliberately emits for a present-but-unloadable certificate is what turned this into a ten-second diagnosis instead of a hunt; it earned its keep on its first real deployment. Key is now group-owned by the service user at 0640, with a fallback to the user's primary group and a clear message when no such user exists. Nothing wider than that. Verified on the node afterwards, on one gated port (8096): https 401 verify=0 TLS terminated, chain valid against the node CA http 401 same port, plain HTTP, unchanged no CA verify=20 untrusted client correctly rejected The reissued key was also picked up with NO daemon restart — the mtime reload path proven in production, not just in a unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f7bde19860 |
Merge branch 'main' into gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf
# Conflicts: # .planning/config.json |
||
|
|
aab74127f5 |
feat(tls): per-node certificate authority + Settings install flow
Demo images / Build & push demo images (push) Failing after 2m19s
The node served a bare self-signed leaf, so a browser exception had to be granted per ORIGIN — scheme + host + port. The dashboard on :443 and an app on :8334 are different origins, and a certificate interstitial CANNOT be accepted inside an iframe, so a gated app embedded over HTTPS could never render no matter how many warnings the user clicked through. (Mixed content blocks the plain-HTTP variant first, before the SameSite cookie question the symptom was originally filed under.) A CA fixes it structurally: ports are not part of a certificate's identity, so one leaf with the right SANs covers every port on the host, and one installed CA trusts them all. - scripts/setup-node-ca.sh generates the CA (4096-bit, pathlen:0, keyCertSign only) and issues a 397-day leaf covering archipelago.local, the hostname, the Tailscale MagicDNS name and every global address the host holds. Idempotent — re-running reuses the CA and only reissues the leaf, so gaining an address does not invalidate copies users already installed. --force-ca is the deliberate escape hatch and says what it costs. - nginx serves the public CA at /ca.crt on both schemes, unauthenticated by design: a device fetches it before it can validate the node, so gating it behind HTTPS or a login would be a chicken-and-egg. - Settings → System shows the fingerprint and per-platform install steps. crypto.subtle does not exist outside a secure context — precisely the case this feature exists to fix — so an HTTP dashboard gets the openssl command to verify by hand instead of a blank field. Verified locally: chain validates, key pairs with the leaf, CA:TRUE/CA:FALSE are correct, keys are 0600. Two TLS servers on different ports both verify (ssl_verify_result=0) against the CA alone and are rejected without it — the one-CA-covers-every-port claim, tested rather than assumed. Not yet wired: app ports still serve plain HTTP. Putting TLS on them is the next step and is what actually closes the iframe-login bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
51a13da2a6 |
merge: bring main (v1.7.125 + .126 work) into phase-13 branch pre-deploy
63 main commits since the fork point — gate cookie-strip fix, named-volume create fix, appgate catalog classification, RNode error surfacing — merged so 13-14/13-15 on-device verification runs against current production code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
830b77af18 |
fix(13-09): verify-aiui-deploy false-negative — pipefail EPIPEs curl on grep -q early exit
curl | grep -q under set -o pipefail: grep's first-match exit EPIPEs curl (exit 23) whenever the marker precedes the tail of a >64KB chunk, so a genuine deploy read as FAIL (bit during 13-08's AIUI redeploy — marker at 27% of a 416KB chunk failed 3/3 runs). Fetch to a temp file, then grep. Negative control still fails as it should. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
3756ebffc0 |
feat(13-09): verify-aiui-deploy.sh fetches live chunks via sw.js, never disk
Post-deploy check for AIUI: resolves the LIVE chunk set by fetching the
service worker's precache manifest (sw.js — vite-plugin-pwa's
generateSW-mode workbox.precacheAndRoute([{url:...}]) array) over HTTP,
fetches each live chunk, and greps the fetched bytes for a marker string.
Exits non-zero when the marker is absent from every live chunk.
This exists because the node's assets/ directory is a never-pruned
graveyard (feedback_node_side_frontend_verify_stale_chunks): a disk grep
reports "deployed" before the deploy actually happened, because a dead
chunk from an old build still contains the old string. Never opens a
remote shell onto the node and never greps the node's filesystem directly
— every check is an HTTP fetch, exactly what a browser session would do.
Verified locally against a real AIUI build served over HTTP: a marker
actually present in a live-precached chunk (index.html) passes (exit 0,
2 chunks checked before the match); a nonexistent marker correctly fails
(exit 1) as the negative control — not a check that always passes.
Wired into deploy-to-target.sh's primary AIUI deploy path in the prior
commit (
|
||
|
|
c3bffbd557 |
feat(13-09): widen same-host deploy guard from containment-only to any mismatch
Adds assert_safe_same_host_deploy(local_src, remote_dst) to lib/common.sh: pure, no SSH inside it, callable directly from a test with fixed inputs. Returns 0 only when the two already-resolved paths are equal; refuses (non-zero, message naming both paths + the 2026-07-31 incident) on any other same-host mismatch. This closes a real gap in the 2026-07-31 incident's original fix: the old guard's two `case` blocks refused only containment (source-in-destination or destination-in-source). A SIBLING directory — for example this very worktree, archy-phase13, deploying onto TARGET_DIR's resolved symlink target (archy, the main checkout) — is neither contained by nor containing of the destination, so the old guard let it through and `rsync --delete` would have mirrored the sibling onto the main checkout, deleting everything the sibling lacks. Found while retargeting deploy-to-target.sh for D-19, not a D-19 effect itself. deploy-to-target.sh's guard block now calls assert_safe_same_host_deploy instead of the two inline containment-only case blocks (old logic removed, not left dead alongside the new call). tests/production-quality/deploy-guard-same-host.sh pins all five <behavior> cases (identical/contained/containing/sibling/unrelated) against the function with no SSH, no rsync, no real deploy — including the exact archy-phase13-vs-archy pair as the sibling-directory regression pin. Manually confirmed non-vacuous: flipping the sibling fixture's expectation to "allow" makes the test fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
073bf6f33d |
feat(13-09): retarget AIUI build/deploy to the in-repo aiui/ tree (D-19)
Finishes the 13-09 build-aiui.sh WIP checkpoint (
|
||
|
|
30b2e02f4a |
wip(13-09): checkpoint in-progress executor work before operator reboot
build-aiui.sh (mid-write) + aiui/.gitignore, committed verbatim and UNVERIFIED
— not a task completion. The 13-09 executor will be killed by the reboot; its
continuation should read this checkpoint, judge it against the plan's
must_haves, and reset --soft / build forward as appropriate (same recovery
pattern as the 6ba52b22/13b576da broken-pipe rescue at the start of this phase).
Already committed by 13-09 before this:
|
||
|
|
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> |
||
|
|
7cc58b7ac6 |
merge(13): bring main forward — unblocks the crate's test build
The lane merged main at |
||
|
|
cc9e19589c |
fix(release): refuse to commit an unsigned OTA manifest
Every cycle has needed a manual check that releases/manifest.json got signed, because the script would happily commit and tag one that hadn't. The signing step is conditional: with no TTY and no RELEASE_MASTER_MNEMONIC it prints a warning and falls through. The commit at step 7 then ran regardless, so the release commit — and its tag — carried an unsigned manifest. publish-release-assets.sh already refuses to ship one, but that backstop arrives a step too late. Nodes fetch releases/manifest.json straight from branch `main` (the same URLs this script prints for verification), so the COMMIT is what exposes it to the fleet, not the publish. By the time publishing is refused, the unsigned manifest is already on main and nodes are already declining to auto-apply. So the same gate now runs before the commit: presence of a signature, signed_by matching the release root, and `ceremony verify` for the crypto. A release commit carrying a manifest no node will accept has no valid use, so this refuses to create one rather than leave a tag that has to be re-cut. The earlier warning is corrected too — it promised the run would continue, which is no longer true. Verified the predicate against three manifests: signed -> allow, signature stripped -> refuse, signed_by swapped to another DID -> refuse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b28cc3eeaa |
fix(13-02): retire the Python model-proxy sidecar and the OpenRouter open relay
Closes the live production exposure this plan targets: /aiui/api/claude/
and /aiui/api/ollama/ proxied straight through with "no session gate
needed", and /aiui/api/openrouter/ was a plain unauthenticated relay to a
paid third-party API the node holds no key for (T-13-08/T-13-09/T-13-10).
image-recipe/configs/nginx-archipelago.conf (BOTH server blocks, ~line 49
and ~line 961 — a fix applied to only one leaves the exposure live on
whichever block serves the request, T-13-15):
- /aiui/api/claude/ and /aiui/api/ollama/ proxy_pass re-pointed from
127.0.0.1:3142 / 127.0.0.1:11434 to the Rust daemon at 127.0.0.1:5678
(no trailing path component, so the daemon's own prefix match sees the
full request URI)
- Forward the session Cookie header to the daemon so it can re-derive auth
- location /aiui/api/openrouter/ deleted outright in both blocks
- Old comment "API key managed by proxy, no session gate needed" (the
reasoning error that produced the exposure) replaced with rationale
scripts/deploy-to-target.sh: deleted the embedded claude-api-proxy.py
heredoc, its systemd unit creation/enable/restart, the ANTHROPIC_API_KEY
extraction, and the 3141->3142 sed fixups. Added an unconditional step that
stops/disables/removes any pre-existing claude-api-proxy unit and deletes
/opt/archipelago/claude-api-proxy.py and
<data_dir>/secrets/claude-api-proxy.env on every deploy — so
already-provisioned nodes actually lose the old unauthenticated listener,
not just newly-deployed ones.
scripts/setup-aiui-server.sh: dropped the hard ANTHROPIC_API_KEY
requirement and the patch-nginx-claude.py step; the script's remaining job
is the AIUI dist rsync. (Also drops the FileBrowser-fix step that lived
here — that logic already exists, and is kept, in deploy-to-target.sh; this
script narrows to exactly what its rewritten header now says it does.)
core/archipelago/src/api/rpc/system/handlers.rs: `claude_api_key` setting
branch no longer writes a second key copy to secrets/claude-api-proxy.env
or restarts claude-api-proxy. secrets/claude-api-key (0600) remains the
single ledger, with a comment naming it as such.
`cargo build --package archipelago` succeeds. Verified via grep against
every acceptance criterion in 13-02-PLAN.md's Task 2 (openrouter count 0,
3142 gone from nginx, both location blocks present, PORT=3142 gone,
claude-api-proxy gone from handlers.rs, secrets/claude-api-key present).
Task 3 (real-node curl/systemd verification, S-15) is NOT done in this
commit — see 13-02-SUMMARY.md.
Continues WIP checkpoint
|
||
|
|
5088aef556 |
fix(lnd-ui): pin the image and host-network it so OTA actually updates it
Reported: Framework PT took the OTA and got the new bitcoin-ui but not lnd-ui. Two causes, both in the update path rather than the app. 1. LND_UI_IMAGE was "lnd-ui:latest" while BITCOIN_UI_IMAGE was pinned to 1.7.119-alpha. Podman does not re-pull a tag it already holds locally, so a node that ever pulled lnd-ui:latest keeps that copy forever and every subsequent release silently no-ops. Pinned to 1.7.119-alpha, so a version change is what triggers the pull — the same mechanism that made bitcoin-ui update correctly. 2. first-boot-containers.sh declared lnd-ui as bridge with -p 18083:80. docker/lnd-ui/nginx.conf listens on 18083 DIRECTLY (it must, to proxy the backend on 127.0.0.1:5678 same-origin), so that maps a host port onto a container port nothing serves — reproduced on-node as HTTP 000. This is the THIRD copy of the same declaration: container-specs.sh and apps/lnd-ui/manifest.yml were both already corrected, this one was missed, and it is the copy fresh installs use. Now host-networked with no published ports, matching its siblings and the other two copies. The underlying hazard is that one container spec lives in three files that can disagree; recorded as a follow-up rather than refactored here. Also opens .planning/RELEASE-1.7.121-TASKS.md — every outstanding item for the next release with its evidence, so nothing in a fast-moving queue gets lost between sessions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b2ed27dcfb |
fix(bitcoin-ui): send no-cache for index.html; pin the rebuilt image
Two things needed for the new UI to actually reach users. The rendered nginx.conf served index.html with only ETag/Last-Modified and no Cache-Control, so browsers applied heuristic caching to it. Confirmed on archi-dev-box: after rebuilding and recreating the container, :8334 and /app/bitcoin-ui/ both served the new markup immediately, but the app iframe in the main UI kept showing the previous UI until a hard refresh. docker/lnd-ui/nginx.conf has always carried this header, which is why only bitcoin-ui showed the stale copy. Using "no-cache" (revalidate) rather than "no-store" keeps the ETag doing its job when nothing has changed. Validated by mounting the rendered config into a throwaway container from the built image and running nginx -t. (An earlier attempt to test it inside the running container was meaningless — conf.d/default.conf is a read-only bind mount, so the copy failed and nginx -t just re-checked the original.) The 8 container::bitcoin_ui tests still pass; their assertions cover the placeholder, the 8332 proxy_pass and the listen directive, none of which this touches. BITCOIN_UI_IMAGE was still pinned to 1.7.84-alpha, so a fresh install would pull a bitcoin-ui from many releases ago regardless of what the OTA ships — first-boot-containers.sh tries the registry image before building from source. Bumped to 1.7.119-alpha, matching the current release, and the image is pushed under that tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
aaa89789d2 |
fix(lnd-ui,bitcoin-ui): OTA-breaking lnd-ui spec, 404 channels link, iframe copy, node URI
All four found by verifying on archi-dev-box rather than assuming.
container-specs.sh: archy-lnd-ui was specified as a BRIDGE container with
SPEC_PORTS="18083:80", but docker/lnd-ui/nginx.conf listens on 18083
directly (it must, to proxy the backend on 127.0.0.1:5678 same-origin).
Recreating from that spec publishes host 18083 to container port 80, where
nothing listens. Reproduced on the node: the app came back with :18083
refusing connections, HTTP 000. This never fired before because the running
containers are created by first-boot-containers.sh, which is host-networked
and never reads this file; the spec is only consulted when self-update.sh
rebuilds a UI image, and that only happens when a file under docker/lnd-ui/
changes — which is exactly what the previous two commits did. So the next
OTA would have taken lnd-ui down on every node. Now SPEC_NETWORK="host"
with no port mapping, matching what actually runs. NET_BIND_SERVICE dropped
with it: 18083 is unprivileged.
lnd-ui channels link: pointed at /apps/lnd/channels, but that route is a
CHILD of the /dashboard record in neode-ui's router, so the real path is
/dashboard/apps/lnd/channels. nginx's SPA fallback returns 200 for the
wrong path, so it failed as vue-router's NotFound view rather than an HTTP
404 — both the Payment Channels card and the Manage Channels button.
Both apps, copy buttons: navigator.clipboard only exists in a secure
context, and nodes serve these apps over plain http; the main UI also
embeds them in an iframe, where the async Clipboard API is separately gated
by the clipboard-write permission policy. Every copy button silently did
nothing there. Added an execCommand('copy') fallback behind a copyText()
helper and routed all six call sites through it.
lnd-ui Node ID: showed the bare pubkey whenever getinfo.uris was empty,
which is the common case — LND only populates uris once it is advertising
an external address. The bare pubkey is not what a peer pastes to open a
channel. The full pubkey@host:9735 URI is now built from the Tor onion
where available, falling back to this node's address, with a hint saying
which and what its reachability is. The QR encodes the URI too.
Verified on archi-dev-box: both images rebuilt and containers recreated
from the specs; lnd-ui and bitcoin-ui both serve 200 with the new assets;
and the RPCs the new tabs depend on all answer on the live node —
getblockstats returns every field the charts read, getpeerinfo returns 11
peers carrying relaytxes and network values the classifier handles.
Note for whoever tests bitcoin-ui's Insights/Peers tabs: /bitcoin-rpc/ now
sits behind auth_request /_session_check (
|
||
|
|
0a1d314ffa |
feat(security): add LND macaroon rotation for the /lnd-connect-info leak
Operator tool for the fix in
|
||
|
|
373c3bb302 |
fix(10-04): ship the host-secrets audit unit in the OTA runtime payload
[Rule 3 — blocking] bootstrap.rs installs systemd units from the runtime payload at image-recipe/configs/, but create-release-manifest.sh copies only archipelago-doctor.service and .timer into that directory. The new archipelago-host-secrets-audit.service would therefore never exist on any node: bootstrap looks for it, `src.exists()` is false, and it silently installs nothing. No error, no log line — the whole deployed-node half of 10-04 would have been inert on arrival. Two enumerations of the same list in two languages in two files is the drift that caused it, so the loop now carries a KEEP IN SYNC pointer naming the array in bootstrap.rs, and the redundant `if [ -f doctor.service ] || [ -f doctor.timer ]` wrapper is gone — the per-unit `-f` test inside the loop already does that job, and the wrapper would have skipped the whole block on a tree that had the new unit but not the doctor ones. Outside 10-04's declared files_modified. Taken because the alternative was to ship a deliverable that cannot reach its target and file the gap as a follow-up. Staged by path; no other agent had uncommitted work in this file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0ed9334f15 |
feat(10-04): let a deployed node report — and fix — fleet-shared host keys
10-03 closed the build half of F-03: the ISO no longer bakes SSH host keys or
a TLS keypair into the shared rootfs, and first-boot regeneration fails closed.
Nodes already in the field receive none of that — the first-boot script is
installed by the installer, not shipped by OTA — so a node that hit the old
fail-open path is still running key material that every downloader of its ISO
also holds, and its completion marker guarantees it will never try again.
scripts/security/host-secrets-audit.sh decides, from the node's own disk alone,
which of those it is. Four signals in a fixed precedence: missing material can
never be shared material; the fail-open fingerprint (marker present plus the
literal `WARNING: TLS regeneration failed` / `WARNING: ssh-keygen -A failed`
lines the old script emitted) is direct evidence and outranks timestamps and
also names WHICH class survived; then key mtime against a first-boot anchor
(.secrets-regenerated, falling back to the installer's LUKS key then
machine-id). Verdicts are per-node / shared / fail-closed-missing / unknown,
and every one of them carries the evidence strings that produced it, each
naming the file it was read from.
per-node is never claimed from an absent signal. No anchor means `unknown`, and
a standing first-boot-secrets.failed record also means `unknown` — a clean
mtime is not evidence that generation succeeded. That is T-10-37: a false
per-node verdict leaves an exposed node looking clean, which is worse than no
verdict at all.
Rotation (D-06: detect-report-then-apply, recorded in
docs/security/KEY-02-FLEET-ROTATION.md):
- --detect is the default and is read-only; it always exits 0, because
detection is informational and must never fail a boot.
- --apply without --yes writes nothing at all, not even its own verdict file.
"Touches nothing" is worth being able to say without a footnote.
- --apply --yes refuses unless the verdict is `shared`, so the wrong node
cannot be rotated even deliberately.
- It stages the full replacement TLS pair AND host-key set before touching
anything live and aborts if either fails; records the OLD fingerprints
before the swap; does TLS first (a dead web UI is recoverable over SSH, the
converse is not); replaces host keys by mv-onto-the-existing-path rather
than rm-then-mv, so the directory is never momentarily empty; and RELOADS
sshd, never restarts it, so the operator's own session survives its own
rotation.
bootstrap.rs ships the boot unit through the existing run_runtime_assets
promotion and enables it --now, so the verdict lands with the OTA rather than
at the next reboot. handle_system_stats gains a host_secrets object read from
the on-disk verdict — cheap, never an error however malformed the file, and
deliberately carrying no fingerprints, because a payload polled every few
seconds does not need digests an operator on the node can already read.
tests/first-boot-secrets/rotation-tests.sh: 8 cases against temp roots through
the HOST_SECRETS_ROOT seam. Negative controls run and reverted, each reddening
exactly one case: dry run writing its verdict file (STATE-DIR-CHANGED); the
old fingerprints recorded after the swap instead of before (caught by an
ordering observation, not a content comparison — the systemctl stub records
whether the file existed at the moment of the first reload); a tolerated
generation failure leaving a half-rotated node; and `per-node` claimed with no
anchor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|