Commit Graph
3116 Commits
Author SHA1 Message Date
archipelagoandClaude Opus 5 a28b3b696d docs(registry-manifest-design): stop describing the pre-Phase-1 state as "today"
The header says Phases 1-3 shipped, then §1 "Where we are today" described the
world before any of them: catalog carrying "version + image override only", the
manifest "never registry-distributed", counts of 48 disk manifests and 28
catalog entries. A reader hits the contradiction immediately and can't tell
which half is current.

Retitled §1 as the pre-Phase-1 baseline it is, and added the actual state:
`releases/app-catalog.json` has 66 entries and 56 embed a full `manifest` block
— one for every `apps/*/manifest.yml` in the tree (the stale counts were 48 and
28). What's genuinely left is Phase 4 (build-context apps) and Phase 5 (drop
`apps/` from the OTA rsync), which the phase list already marks .

Also:
- The install arrow claimed "render Quadlet unit"; same overstatement corrected
  in architecture.md and app-manifest-spec.md — Quadlet is opt-in, the default
  is podman create+start.
- §8's open question "generated_files with inline content — already supported?"
  is answered: `app.files[]` takes inline `content` with placeholder rendering.
  Marked answered rather than leaving a resolved question looking open.

Verified present and unchanged: `catalog_manifest_to_overlay`,
`install_stack_via_orchestrator`, `install_immich_stack`, and the catalog-wins
merge semantics.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:53:15 -04:00
archipelagoandClaude Opus 5 2c53a7d77f docs: fix the CSRF-exempt list and describe how secret_env actually reaches a container
**COMMANDS.md** named six CSRF-exempt read-only methods, two of which
(`bitcoin.getinfo`, `monitoring.current`) are not exempt — a client trusting the
doc would send them with the cookie alone and get rejected. The real set is
twelve (`api/rpc/mod.rs:326-340`); listed all of them and said plainly that
everything else needs the header. The rest of the doc verified clean: the 480 /
200 / 160-char caps, the four `assistant_*` config keys, both default model ids,
`is_sender_allowed`, `strip_archy_trigger` / `run_node_cmd`, the three
unauthenticated HTTP endpoints, and `auth.login.totp` all match the code.

**secrets.md** said `secret_env` "sets `<key>` in the container's environment",
which reads as a plain `-e KEY=value` and undersells the design. It isn't:
resolved pairs are registered as podman secrets named
`archy-env-<app-id>-<key>` and referenced by name, precisely so the value stays
out of `podman inspect` and out of plaintext `Environment=` lines in Quadlet
units. Also documented the interpolation-taint rule — a plain `environment`
entry that expands `${SECRET}` (BTCPay's connection strings) is itself treated
as secret-bearing rather than left in the clear, which is what makes it safe to
build connection strings from secrets.

Everything else in secrets.md verified against `container/secrets.rs`: the four
kinds and their file shapes, the bare-filename rule, the every-tick idempotent
`ensure_generated_secrets`, and the atomic 0600 temp-fsync-rename writer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:51:53 -04:00
archipelagoandClaude Opus 5 7adc3260a6 docs(app-manifest-spec): fix the id rule, the Quadlet claim, and the declarative overstatement
Narrative pass over the manifest spec, plus one correction to the guide I
committed in ba052736.

- **`app.id` allowed `_`.** `is_valid_app_id` accepts lowercase ASCII letters,
  digits and single hyphens only — no underscores, no leading/trailing hyphen,
  no `--`. The spec's "alphanumeric + `-`/`_`" would have a developer write an id
  that fails to parse.
- **"Must match the directory name"** is a convention, not a rule. The loader
  (`prod_orchestrator.rs:1455-1474`) walks `*/manifest.yml` and keys off
  `app.id`, never comparing it to the folder, so a mismatch silently registers
  the app under a different id. Said so rather than implying enforcement.
- **The Quadlet claim was the same one architecture.md was corrected for**
  (f55ed6bf): install does NOT compile to a `user.slice` Quadlet unit today.
  `config.use_quadlet_backends` defaults false, so apps take the legacy
  `podman create + start` path; Quadlet is opt-in per node and companion UIs are
  the exception that already use it.
- **"no per-app installer code"** — true of installers, but
  `run_pre_start_hooks` is a hardcoded `match app_id` covering seven first-party
  apps (bitcoin-ui, filebrowser, lnd, archy-nbxplorer, btcpay-server,
  fedimint-clientd, grafana). Documented as the caveat it is; anyone reading the
  source will find it in a minute and the doc should not look like it's hiding it.
- `derived_env` now names the full closed allow-list including `{{BITCOIN_HOST}}`
  and what it resolves to.

Correction to ba052736: I wrote there that an unknown `derived_env` placeholder
passes through verbatim. It doesn't — `validate_derived_template` rejects both
unknown names and unbalanced `{{`. Fixed that row in the guide.

Verified accurate and left alone: the capability allow-list, network_policy
values, `/dev/*` device rule, volume option allow-list, bind-source confinement,
the four generated_secret kinds, `hooks.pre_start` being schema-only, and the
30s reconciler interval (`BootReconciler::DEFAULT_INTERVAL`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:49:17 -04:00
archipelagoandClaude Opus 5 ba052736be docs(app-developer-guide): describe the security rules the parser actually enforces
Narrative pass. The Security Requirements section described a blocklist where
the code enforces an allow-list, and attributed enforcement to the wrong layer:

- **"Forbidden: mounting system paths /, /etc, /var, /usr, /proc, /sys"** — the
  real rule (`manifest.rs:1290-1313`) is the inverse: `volumes[].source` must be
  absolute and under `/var/lib/archipelago/`, or a plain named volume, or one of
  two reviewed exceptions (`/run/user/1000/podman/podman.sock`, `/var/run/dbus`).
  Anything else is a parse error. The old wording also listed `/var` as
  forbidden while every app in the repo binds `/var/lib/archipelago/<id>` — a
  developer reading it would not know where their own data goes.
- **"enforced by the marketplace/catalog pipeline and the node"** — split by
  layer instead. The capability allow-list is parser-enforced (verified against
  the 9 entries at `manifest.rs:1089-1099`); `:latest` is NOT — only
  `validate-app-manifest.sh` checks it, and a `:latest` manifest still installs.
  readonly_root / no_new_privileges / network_policy=isolated are parser
  defaults, so omitting them is safe rather than dangerous.

Also:
- `derived_env` documented `HOST_IP`/`HOST_MDNS`/`DISK_GB` "such as"; the set is
  closed and includes a fourth, `{{BITCOIN_HOST}}`. Noted that unknown
  placeholders pass through verbatim rather than erroring, so a typo silently
  ships `{{FOO}}` into the container.
- The networking example hardcoded `bitcoin-knots`; `{{BITCOIN_HOST}}` resolves
  to knots or core depending on what's installed.
- Documented the `files[].content` placeholder set, which is a different set
  from derived_env and wasn't mentioned at all — notably `{{NETWORK_GATEWAY}}`
  (the nginx `resolver` fix for post-restart 502s) and `{{secret:NAME}}`.
- The "check the UI" URL `/app/my-app/` is not a route; it's
  `/dashboard/apps/:id` (detail) or `/dashboard/app-session/:appId` (embed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:46:17 -04:00
archipelagoandClaude b886930708 fix(container): ownership probe uses systemd-run with output capture
The first drift-gate attempt called plain sudo stat, which the daemon's
privilege path doesn't answer — the probe silently failed and the chown
loop continued. host_sudo_output mirrors host_sudo (systemd-run --pipe)
but returns the process output, so the ownership check gets a real answer.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 20:39:33 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 20:27:50 -04:00
archipelagoandClaude Opus 5 2ed6c71e0c docs(troubleshooting): fix advice that doesn't match the node
Narrative pass over troubleshooting.md against the code. Seven claims were
wrong, several of them actively misleading:

- **Tor is not a container.** §15/§16 told operators to run
  `podman ps --filter name=tor` / `podman restart tor` and to read
  `/var/lib/archipelago/tor/hidden_service/hostname`. Tor is the host's Debian
  package running as `debian-tor`; Archipelago drives it by staging a torrc and
  poking `archipelago-tor-helper` (`scripts/tor-helper.sh`, which does
  `systemctl restart tor`). The hidden-service dir is
  `hidden_service_archipelago` (suffixed), it's root-owned 0700, and the file a
  normal user can actually read is the synced copy at
  `/var/lib/archipelago/tor-hostnames/<service>`.
- **The USB installer has no "Repair" mode.** Cited three times as the recovery
  path. The boot menu has exactly three entries: Install, Install (verbose),
  Boot from local disk. Replaced with what those entries can actually do, plus
  the fact that the installer prompts for a disk and requires typing `yes`, so
  booting it isn't itself destructive.
- **`bitcoin-cli -datadir=/data`** — the container's datadir is
  `/home/bitcoin/.bitcoin` and RPC creds are in a generated `/tmp/rpc.conf`;
  the documented command could not have authenticated.
- **"edit bitcoin.conf to add addnode="** — the entrypoint passes an explicit
  `-conf` and logs "ignoring legacy datadir bitcoin.conf". Flags come from the
  manifest (and the signed catalog entry that overrides it).
- **"Bitcoin requires 600GB+"** — only above the manifest's 1000 GB threshold;
  below it the node runs pruned at `-prune=550`.
- **`sudo systemctl restart podman`** — apps run under rootless Podman as the
  `archipelago` user, so that restarts an unrelated root socket.
- **"Settings > Network"** — DNS config and disk cleanup are both on the Server
  page (`/server`), not Settings.

Also: header claimed "the 20 most common issues" over 21 sections, and §16
presented Tor as required for peering when it's the last fallback after
mesh → LAN → FIPS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:26:19 -04:00
archipelagoandClaude Opus 5 e7d8dfb633 docs: correct the "default password123" claim — production nodes have none
The walkthrough told new users to log in with `password123` and said they'd
be "prompted to change this password immediately". Neither is true on a
release build:

- `AuthManager::ensure_default_user` is never called. `main.rs:356-362`
  says so explicitly ("Don't auto-create default user — let onboarding flow
  handle password setup via auth.setup"), and the function is `#[allow(dead_code)]`.
- The only `password123` login path is `api/rpc/auth.rs:36-46`, which is
  `#[cfg(debug_assertions)]` AND `dev_mode` AND only fires *before* setup —
  no release binary carries it.
- `Login.vue` calls `auth.isSetup` on mount and renders the "Set Up Your
  Node" password-creation form when it returns false. That is the real
  first-boot screen, and it is the only `auth.setup` caller in the frontend.

So there is nothing to be "prompted to change" — the user creates the
password themselves, and the doc's version taught them to look for a
default that does not exist.

Fixed in four places:
- user-walkthrough Step 8 rewritten as "Create Your Password"
- troubleshooting's "Default password is password123" solution replaced,
  including the warning that deleting user.json does NOT recover a lost
  password (the onboarding gate refuses auth.setup on a provisioned node)
- api-reference cURL example uses a placeholder, not the fake default
- 1.8.0 hardening plan's "kill default credentials" item now reflects that
  the web half is done and only the SSH defaults still ship

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:17:44 -04:00
archipelagoandClaude db8937f9e9 fix(container): root stat fallback makes volume ownership drift authoritative
The direct metadata read can be denied in the service's rootless context even
when the directory is already correctly owned, which kept the reconciler
calling sudo chown on the same Postgres volume every minute. A root
fallback gives the guard a reliable answer on deployed nodes while remaining
much cheaper than a recursive chown.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 19:59:48 -04:00
archipelagoandClaude Opus 5 e0cc41d31e docs(developer-guide): fix stale ISO-builder path and CLAUDE.md label
Verified the project-structure tree against the tree. Two stale entries:
- image-recipe/build-auto-installer-iso.sh was the old builder, now under
  _archived/; the current builder is image-recipe/build-debian-iso.sh (the
  release workflow drives it via scripts/build-iso-release.sh). Repointed.
- CLAUDE.md was labelled "AI development instructions"; it is now the sanitized
  public contributor guide. Relabelled.

Everything else verified: run-tests.sh, first-boot-containers.sh, container.rs,
vpn.rs all exist; the add-an-endpoint / add-a-Vue-page tutorials match the
current dispatch pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:56:35 -04:00
archipelagoandClaude Opus 5 f55ed6bf45 docs(architecture): correct the "apps run as Quadlet units" overstatement
The overview stated apps install as user.slice Quadlet units. Verified against
prod_orchestrator.rs: use_quadlet_backends defaults to false, so regular apps
install via the raw podman path today; the companion UI containers are the ones
that run as Quadlet units (companion.rs owns them), and the Quadlet flip to
default for all apps is opt-in/held.

Reworded both places (the layer diagram and the App Platform section) to match
reality and the container-lifecycle / quadlet-compilation dev docs: the
orchestrator owns and self-heals app containers; companion UIs run as Quadlet
units, the validated path being flipped to default. Everything else in the doc
verified accurate — crate table, module map, data paths, security model, and
the note that the four orphan crates still exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:41:20 -04:00
archipelagoandClaude 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>
2026-08-07 19:21:55 -04:00
archipelagoandClaude 7c7cd76c1c fix(package): btcpay wipe removes the whole stack's data, not just its own dir
get_data_dirs_for_app had no btcpay arm — the default mapped to
/var/lib/archipelago/btcpay alone, leaving postgres-btcpay (where the
ACCOUNT lives) and nbxplorer on disk. Uninstall-with-wipe then reinstalled
to the old account still enabled. The btcpay arm now covers all three dirs,
for every alias and stack-member id. The map stays deliberately hardcoded:
deletion code must never derive its targets from a manifest at uninstall
time (a bad manifest could aim the wipe at another app's data).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 19:17:24 -04:00
archipelagoandClaude Opus 5 4155cefdf5 docs(security): genericize a node address in the RPC-proxy incident record
BITCOIN-RPC-PROXY-EXPOSURE.md's port claims verify against code (Bitcoin RPC on
127.0.0.1:8332, the bitcoin-ui proxy on 127.0.0.1:8334). But its incident
narrative named a specific node's LAN address (192.168.63.240, five times) on a
subnet the earlier 192.168.1.x sweep did not cover. Replaced with the RFC 5737
documentation address 192.0.2.240. The incident content — the exposure, the
probes, the fix — is unchanged and remains a legitimate public security record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:00:29 -04:00
archipelagoandClaude Opus 5 dc2d79ce77 docs(security): self-contain KEY-05 and PSBT; record the completed entropy migration
Verified the security subsystem's design-doc claims against code:

- KEY-05's foundational claims are accurate: entropy::draw_key_bytes exists,
  KeyGenRng is sealed with OsRng as its sole production member, MIN_GUARDED_LEN
  is 12, and core/clippy.toml bans rand::random/thread_rng exactly as stated.
- But its per-site table listed every production nonce/key site as disposition
  "migrate" (pending), when all of them have since been migrated to
  draw_key_bytes(OsRng) — storage_crypto, credentials/store, wallet/bdhke,
  mesh/x3dh — and zero rand::random/thread_rng remain in production. Added a
  completion note so the doc no longer reads as pending work.

Both KEY-05 and PSBT-SIGNING-ARCHITECTURE referenced
ENTROPY-SEED-AUDIT-2026-07-31.md five times as their evidence base — a doc that
was moved to local-only, so a public reader could not follow it. Reworded all
five to state the audit's findings inline ("the internal entropy audit found
...") without the unresolvable path. No published doc references it now. The
link-checker missed these because they were inline code, not markdown links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:59:11 -04:00
archipelagoandClaude ca106c5a43 fix(container): data-uid chown is drift-gated, not unconditional every tick
apply_data_uid ran a recursive sudo chown on every prepare_for_start, and the
reconciler re-prepares — archi-dev-box's journal showed postgres-btcpay rechowned
every ~45s despite already-correct ownership, and on framework-pt the same loop
surfaced as operator-visible 'chown failed' noise. chown_for_rootless_container
now stats the target first and returns early when the top-level owner already
matches the host-mapped uid:gid. Deep drift in a running container is still
caught by ensure_running_container_ownership's in-container write-probe, which
is the authority that actually matters (it probes writability, not stat bits).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 18:54:39 -04:00
archipelagoandClaude Opus 5 bcbd4a7032 docs(app-developer-guide): add the missing local manifest-validation step
The guide walked a developer from manifest to install but never told them how
to validate the manifest locally first — despite scripts/validate-app-manifest.sh
existing for exactly that. A developer's first signal that their manifest was
wrong would have been an install failure on a node.

Adds a "Validate Your Manifest" step at the top of Testing, pointing at the
script (recently fixed — it had been rejecting every manifest because it shelled
out to a missing ruby). Notes the strict behaviour a new submitter hits, e.g.
an unpinned :latest tag is rejected, and that the Rust parser is canonical.

Verified: the install RPC example in this guide (id + dockerImage) matches the
handler; the cargo test target crate name (archipelago-container) is correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:49:08 -04:00
archipelagoandClaude Opus 5 8a341de4b5 docs(api-reference): fix the one fabricated RPC method
Verified all 144 documented RPC methods against the dispatcher. 143 are live;
one was fabricated: `mesh.discover` (params { timeout_secs? }, returns
{ nodes: MeshNode[] }) does not exist — "mesh discovery" appears only in code
comments as a concept, never as a method. A developer calling it gets "unknown
method".

Replaced with the real peer-listing method `mesh.peers` (no params, returns
{ peers, count }), which the frontend actually uses and which was undocumented.

Also verified: every source path cited across the docs resolves (placeholders
and a correctly-recorded deletion aside), and every documented app-manifest
field exists in the schema (no fabricated fields).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:47:10 -04:00
archipelagoandClaude Opus 5 68e3f61121 docs: remove the last private MEMORY references from design docs
Three `MEMORY → <note>` see-also references pointed at the private agent-memory
system from public docs (demo-deployment-design.md x2, registry-manifest-design.md
x1). Removed. No tracked doc references the memory system now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:35:43 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 18:34:30 -04:00
archipelagoandClaude Opus 5 b4e4189407 docs: reframe bulletproof-containers as a historical record; scrub internals
This 2026-04 plan has been implemented, but it still read as an active plan
("implementation started"), linked private agent-memory paths, and ended with a
stale "To resume" work block naming fleet nodes, dated fleet state, and the next
file to edit.

- Header now marks it a historical design record and points at
  container-lifecycle.md for the current behaviour.
- Removed the two private ~/.claude/.../memory/ references from the header and
  the entire "To resume" section (private paths, node numbers, 2026-04-22 fleet
  snapshot — none of it belongs in a public design doc).
- Genericized the one remaining node-number reference in the incident narrative.

The valuable content — the six failure modes and the reconciler reasoning that
answered them — is kept intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:28:39 -04:00
archipelagoandClaude Opus 5 599787690a docs: write the three missing app-developer docs (secrets, quadlet, lifecycle)
The open-source plan flagged three references as "the real gaps for app
developers", and the docs index named them as not-yet-written. Written now,
each from the code rather than stubbed:

- secrets.md — generated_secrets/secret_env: the two halves, the four kinds
  (hex16/hex32/base64/bcrypt) and which files each writes, the idempotent
  self-healing 0600 materialisation, and the rules a developer must not break
  (no hardcoded fallbacks, one canonical name, right encoding). From
  container/secrets.rs and the manifest schema.

- quadlet-compilation.md — manifest -> .container unit: the full directive
  mapping (including Secret= by reference, never value, and Pull=never), where
  units land (~/.config/containers/systemd, systemctl --user), the
  render/write/enable/disable lifecycle with write-if-changed, and how to
  inspect one. From container/quadlet.rs, scoped accurately to the companion-UI
  path it drives today.

- container-lifecycle.md — the level-triggered 30s reconciler: desired state
  from user-stopped/user-uninstalled/manifest set, the operations table, the
  self-heal-vs-respect-a-deliberate-stop rule, and migrations-never-destroy-data.
  From prod_orchestrator.rs and boot_reconciler.rs.

Index updated to link all three under App development and the "known gap" note
removed. Every link across the docs tree resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:22:26 -04:00
archipelagoandClaude Opus 5 460eccd368 docs(CLAUDE): tighten prose and correct the manifest-delivery claim
Follow-up to 73970cf3. Two improvements:

- Corrects a stale claim — manifests are no longer "loaded from disk, goal is
  the catalog". The signed catalog has been the delivery mechanism since
  2026-06-23 (origin-wins over disk), so the guide states that, plus the
  consequence contributors need: editing a disk manifest alone does not change
  a catalog-covered app.
- Tightens the north-star paragraph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:18:48 -04:00
archipelagoandClaude Opus 5 73970cf32d docs: sanitize CLAUDE.md into a public contributor guide
CLAUDE.md was the internal agent guide: a dated "gate is GREEN" status banner
naming a specific node, pointers to now-local-only planning docs
(PRODUCTION-MASTER-PLAN, UNIFIED-TASK-TRACKER, multinode-testing-plan), the
gitea-ai push account mechanics, and references to the private memory system.

Rewritten as a contributor guide that keeps everything public-worthy — the
invariants (rootless podman, declarative apps, manifest-declared secrets,
non-destructive migrations), the build/verify notes, the commit-and-push
discipline, and the production test-gate definition — and drops the status,
node numbers, push-account specifics, and memory references. Points at
docs/ROADMAP.md and docs/README.md instead of the internal trackers.

No infra identifiers or internal mechanics remain; all links resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:15:47 -04:00
archipelagoandClaude Opus 5 661f3eda25 docs: add a grouped documentation index; fix references to now-local-only docs
Two concrete, verifiable documentation gaps from the open-source review:

- docs/ had no index. Adds docs/README.md grouping the 60-odd published docs by
  task — getting started, architecture, app development, design docs, ADRs,
  security, roadmap — in the bitcoin/bitcoin doc/ style the plan called for.
  Every link in it resolves (checked). The top-level README now points at it as
  the front door rather than duplicating the list.

- ROADMAP.md and tests/lifecycle/TESTING.md linked docs/multinode-testing-plan.md,
  which moved to local-only (it is a fleet node inventory, not published). Those
  references now describe the scope split in prose instead of pointing at a file
  that is not in the public tree.

The index is honest about what is missing: it names the three app-developer
docs the plan flagged as gaps (quadlet compilation, container lifecycle,
secrets materialisation) as not-yet-written, and points at the authoritative
code for each rather than pretending they exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:11:05 -04:00
archipelagoandClaude Opus 5 bf76955114 chore(license): declare MIT on the crates (open-source Phase 4a A4)
The repo ships an MIT LICENSE and the README carries an MIT badge, but the
crates themselves declared no license, so `cargo metadata`, packaging and any
downstream mirror saw "license: null". Adds [workspace.package] license = "MIT"
and inherits it in all five members via license.workspace = true. Verified with
cargo metadata: all five now report MIT.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:07:59 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 18:05:55 -04:00
archipelagoandClaude 494400df29 docs(13): fill 13-VALIDATION map + write 13-UAT on-device acceptance record
13-15's two artifacts. The map names plan/wave/threat-ref per row with
today's measured results (Rust assistant suite 130/130; adapter 37; broker
25; toolConfirm, audioPlayer 11, appsConfig 13; AIUI 353/356 with the three
documented pre-existing fixture failures). Four close-out rows added
(S-invariants, evals, egress, mock-free-bundle grep). Manual-only table
discharged except the physical-handset pass, which 13-UAT records as owed
(the AIUI-06 flagged assumption wants both, and only devtools-mobile is on
record). Open questions 1/2/4 resolved with plan cites; Routstr stays
honestly open.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 17:52:22 -04:00
archipelagoandClaude Opus 5 35f992fdb4 release: re-publish the v1.7.126-alpha manifest — assets verified downloadable
Restores the signed v1.7.126-alpha manifest to main now that both artifacts are
confirmed fetchable end-to-end:
  - archipelago            HTTP 200, sha256 matches the manifest
  - frontend tarball       HTTP 200, sha256 matches the manifest

The earlier publish was rolled back (e346e552) because the manifest went live
before its assets resolved. Two separate asset faults, now fixed: the binary's
first upload landed corrupt server-side and returned 500 on download (deleted
and re-uploaded, clean); the tarball's first upload returned an empty response
and never attached (re-uploaded, 201, full 210 MB).

This is byte-for-byte the manifest the tag already carries, so its signature is
unchanged and re-verified against the pinned release root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:50:44 -04:00
archipelagoandClaude Opus 5 e346e5526f revert(release): serve the v1.7.125-alpha manifest until .126 assets are up
The v1.7.126-alpha manifest went live on main — which is where nodes read it
from — before its artifacts were reachable. The binary returns HTTP 500 and the
frontend tarball never uploaded (404), so any node polling would advertise an
update it cannot fetch.

Restores the previously published, still-validly-signed .125 manifest
byte-for-byte from 19487670, so nodes see the last release that actually
resolves. The v1.7.126-alpha tag and its signed manifest are unchanged in git
history; only what main serves is rolled back.

Publishing order was the mistake: the manifest is the trigger, so assets must
be verified downloadable before it lands on main, not after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:40:44 -04:00
archipelagoandClaude Opus 5 7a6b1af509 chore: release v1.7.126-alpha
Demo images / Build & push demo images (push) Failing after 2m8s
Signed release manifest for v1.7.126-alpha, verified against the pinned
release root before committing.

Committed by hand rather than by re-running create-release.sh: the script
regenerates the manifest at step 6, which would overwrite the signature
applied at step 6b. Its own "sign it, then re-run this script" advice destroys
the thing it just asked for.

Version bump, changelog and What's New landed earlier in 1cd068e4 — the
frontend build embeds the version via the curated What's New list, so those
must exist before the build step rather than after it.

Release gate: 8 of 9 stages passed in-run. cargo-test-weekly hit its 1500s
ceiling (exit 124, a timeout not a failure) because the non-incremental
all-targets compile does not fit on this machine; the suites were then run
separately and passed 100/100, including the downgrade guard and 30 update::
tests over the OTA apply/rollback path. Right-sizing that ceiling is a
follow-up — an override that becomes routine stops being a gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v1.7.126-alpha
2026-08-07 17:38:04 -04:00
archipelagoandClaude 482c4e300e fix(bootstrap): self-heal stale /aiui/api/web-search proxy to the gated daemon
Live on archi-dev-box: the node still proxied web-search straight to
SearXNG :8888 unauthenticated — the repo conf was fixed in d0c9ea6e but
existing nodes' /etc/nginx/sites-enabled never gets rewritten by a source
edit. Added to the nginx self-heal battery: stale 8888/search proxy_pass →
session-gated 5678 with the Cookie forwarded (heal_stale_web_search_block,
pure + idempotent + tested). Fresh ISOs already ship the gated block.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:54:38 -04:00
archipelagoandClaude 4361a5cba7 feat(mesh): '!ai' over mesh runs the assistant's shared tool loop
Mesh AssistQuery answered with a bare LLM call — no tools, no actions.
The CallerScope::Mesh variant was designed for this wiring ('the variant
exists so the shape is right when a future plan wires mesh callers into
the shared loop'); this is that plan. A trusted/allowlisted asker's prompt
now runs assistant::chat with CallerScope::Mesh { authorized } — the
operator's persisted grants cap what the model may touch (never wider),
and writes suspend on the node's own confirm gate. The reply is capped
for airtime as before, with a brevity instruction for mesh turns.

Wiring follows the blob_store pattern: RpcHandler::set_mesh_service (now
&Arc<Self>) forward-propagates an Arc<RpcHandler> into the mesh state's
new assistant_handler slot; absent (early boot) falls back to the legacy
bare-LLM answer.

Test: mesh_caller_authority_is_capped_at_operator_grants.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:54:12 -04:00
archipelagoandClaude 34085e30a2 fix(iso): pin wget to v4 AFTER its package installs — conffile prompt is fatal otherwise
Build #189 proved the IPv4 fix works (downloads all succeeded) and proved
my own edit wrong: appending inet4_only to /etc/wgetrc before wget's
package landed made dpkg's conffile prompt hit EOF, leaving wget and
debootstrap unconfigured. Moved the pin below the apt install.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:49:03 -04:00
archipelagoandClaude e36f36ee09 perf(aiui): chat backdrop 1052K → 478K webp (was the 'ages to load' report)
The 2912×1632 jpg painted visibly slowly over Tailscale/Tor. 1920w q82
webp is visually identical behind glass and under half the weight.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:06:45 -04:00
archipelagoandClaude a2254648ad fix(iso): force IPv4 for every package retrieval in the build
This box (and its containers) blackhole IPv6: deb.debian.org answers AAAA
first, wget tries v6 until debootstrap's per-package timeout, and the
installer-env stage died twice today with 'Couldn't download packages'.
Probed in a debian:trixie container: v4 OK, v6 hangs. inet4_only for wget
(covers debootstrap) + Acquire::ForceIPv4 for every apt-get, including the
chroot and the rootfs Dockerfile stages.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:55:18 -04:00
archipelagoandClaude eaf0f07346 fix(ui): #ai-data-access anchor exists, so the banner's Settings button lands
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:01:12 -04:00
archipelagoandClaude 7686a486ec feat(assistant): app_install/app_uninstall tools + S6 replay fix
Two changes, one binary batch:

1. app_install/app_uninstall (task 3): '!ai please install bitcoin knots'
   correctly said it can't. Both tools are category-Apps, destructive, and
   ride the 13-08 confirm gate (node-authored descriptions added). Install
   validates catalog membership BEFORE the dialog (a typo never spends an
   approval); uninstall resolves installed ids. Both reach the SAME
   package.install/package.uninstall spawns every authenticated caller
   uses, via a curated Arc-taking sibling of assistant_dispatch_tool.

2. S6: cloud legs no longer strip prior USER turns from replayed history.
   Turn-minimality's allowlist is now the whole conversation's operator
   turns (the node's own D-08 transcript, same trust class as this turn),
   still mechanically matched, B1 secret scan and 64KB cap unchanged,
   fabricated user messages still truncated. The model no longer sees its
   own answers without the questions.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:00:49 -04:00
archipelagoandClaude 2787a9bbfc fix(ui): unknown app ids fall back to the A mark, not a 404 png guess
resolveAppIcon's final arm guessed /assets/img/app-icons/<id>.png — strfry
404'd live. DEFAULT_APP_ICON already existed; the chain now ends on it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:48:55 -04:00
archipelagoandClaude 31fd789b55 fix(aiui): hide zero-value metadata — no more '★ 0 · 0m' (W1.5)
Node-derived cards have no rating/year/runtime/director; rendering the
defaults read as '★ 0 · 0m' beside an empty string. FilmGrid, FilmDetail
and SongGrid now gate those spans on real values (FilmCard, Book and Place
components already did). Panel empty states were made honest in 9abc1623
('Nothing found'), so this closes the rendering-honesty item.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:36:44 -04:00
archipelago 92a8dff6b1 docs(13): tick the open task list with session-2 verified state 2026-08-07 14:30:54 -04:00
archipelagoandClaude Opus 5 1cd068e4f7 docs(release): curate the v1.7.126-alpha changelog and What's New entry
create-release.sh builds the frontend at step 4 and validates the curated
changelog at step 5, then requires the freshly built bundle to contain the new
version. The version reaches the bundle only through the hand-written What's
New list, so on a fresh release that check can only pass if the changelog and
What's New entries are written BEFORE the script runs. Writing them after is
what aborted the first attempt.

Leads with the downgrade bug, since that is the one users saw: an Update button
offering the release withdrawn for an actively exploited 2FA bypass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:15:01 -04:00
archipelagoandClaude 8329b826b1 fix(aiui): mock libraries drop out of the production bundle (W1.4)
Every mock consumer is now gated on the demo flag inline (canonical Vite DCE
idiom — the cross-module DEMO_CONTENT_ENABLED const defeated folding). But
the real leak was films.ts's module-level allGenres/allSources exports:
[...new Set(mockFilms.flatMap(...))] is unprovably pure, so the treeshaker
kept the whole module — array, plex:// and cloud.example.com hosts and all —
even with zero live references. The mocks directory is now declared
side-effect-free in vite.config (they are pure data by design), so unneeded
mock modules actually drop.

Verified: clean dist build → entry bundle AND dist-wide grep show zero
mock hosts (spotify/track/example, cloud.example.com, plex://, tmdb image
host). Demo/dev builds (VITE_DEMO_CONTENT=true or import.meta.env.DEV) keep
the full pack. Tests: 353/356, failures are the three documented
pre-existing ones.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:06:59 -04:00
archipelagoandClaude Opus 5 f1b61731ec style: rustfmt after the registry domain migration
The release gate failed cargo-fmt. The domain that replaced the IP-based
registry is longer, pushing several test assertions past the width limit, so
rustfmt wanted to re-wrap them. Pure line re-wrapping — no semantic change.

Caught by the pre-flight gate rather than after tagging, which is what it is
for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:44:12 -04:00
archipelagoandClaude Opus 5 f6f455fa31 release(catalog): clear the legacy btcpay 2.3.9 entry — signed
The legacy `btcpay` entry (distinct from `btcpay-server`, no embedded manifest)
still carried a concrete 2.3.9 image. catalog_primary_image treats that as
authoritative, which is what drove the UI to offer "update to 2.3.9" on nodes
already running 2.4.2 — a rollback onto the actively exploited release.

Now 2.4.2 in both entries, signed by the pinned release root and verified.

This lands the fix for every node immediately, without waiting for the binary
carrying the downgrade guard (cbfda305) to reach them. The guard remains the
durable fix: it makes any future stale pin fail safe rather than relying on
every version claim being correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:30:55 -04:00
archipelagoandClaude Opus 5 37c77f17ab fix(versions): stop reporting a stack sibling's version as the app's own
package.versions answered installedVersion "15.17" for btcpay-server while
offering "2.4.2" — 15.17 being its postgres dependency's tag. With BTCPay's
own container absent, installed_version fell back to `containers.first()`,
which for a multi-container stack is an arbitrary sibling.

That is the number the update decision is made from, and it is what the UI
shows next to the available version, so a nonsense pair like "installed 15.17,
available 2.4.2" is presented as a legitimate upgrade.

The fallback now only applies when there is exactly one container, which still
covers apps whose container is named differently from their id (immich_server
for immich). With several containers and no identifiable backend, the honest
answer is "unknown" rather than a guess at a sibling.

Extracted as select_backend_container so the rule is testable directly.

Tests: the BTCPay stack case, the lone differently-named container, and the
archy- prefixed preference. Full suite 1157/1157.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:14:30 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 12:42:11 -04:00
archipelagoandClaude Opus 5 fa9d75de98 fix(release): track the registry trust floor — releases/** was swallowing it
d0af38e8 shipped check-catalog-registry-trust.py without the file it reads:
`releases/**` in .gitignore silently dropped registry-trust-floor.json, so the
guard would have failed in CI and on any fresh clone. app-catalog.json only
stays tracked because it predates that rule.

Both are source rather than build output — nodes fetch the catalog from this
path on main, and the floor is what the guard checks it against — so both now
have explicit negations, with the reason recorded next to them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:25:44 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 12:24:34 -04:00
archipelagoandClaude Opus 5 7cf6980894 release(catalog): BTCPay Server 2.4.2 — signed
Signed by the pinned release root and verified before publishing
(`ceremony verify` → OK). A present-but-invalid signature is a hard reject on
nodes, so verification is the gate, not the presence of a signature field.

Surgical edit rather than a regenerate: only the btcpay-server entry changed
(66 apps in, 66 out; 49 entries still resolve through the legacy registry,
untouched). A full regeneration would have embedded the repo's manifests,
which now name the new registry domain that no deployed binary trusts yet —
publishing that would have broken every app install fleet-wide, during a
security push. That sequencing is being fixed separately.

BTCPay's image comes from docker.io, so it is unaffected by the registry
migration either way.

Nodes pick this up via package.check_updates → refresh_catalog → reload
manifests, then package.update (stop → pull → remove → recreate → verify,
with rollback on failure).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:21:45 -04:00