Commit Graph
3080 Commits
Author SHA1 Message Date
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
archipelagoandClaude Opus 5 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>
2026-08-07 12:19:24 -04:00
archipelagoandClaude Opus 5 44b50a39d4 fix(registry): repair two regressions from the domain migration; port the manifest validator to python
Fixes 4 test failures introduced by 8e814ca0, which I pushed after running
only the container-crate tests while the full suite was still compiling. Both
failures were real defects, not stale assertions.

1. Catalog-driven installs would have failed fleet-wide.
   8e814ca0 dropped the old registry address from TRUSTED_REGISTRIES, but the
   signed catalog still advertises image refs on it — deliberately, since
   rewriting a signed artifact invalidates its signature. Nodes resolve apps
   through the catalog, so every install would have been refused with "not
   from a trusted registry". Reinstated as LEGACY_REGISTRY_HOST, documented
   as transitional and removable only once the catalog is re-signed.

2. The update fallback lost the property it exists for.
   update.rs keeps two mirrors on purpose: the domain as primary, and the
   old IP over plain HTTP as a fallback, because a node whose DNS or clock is
   wrong (both break TLS) must still be able to update itself — the signature,
   not the transport, is what makes either source safe. The bulk rewrite
   pointed both constants at the domain, leaving the escape hatch dependent on
   exactly what it exists to survive. Restored to its original value.

Separately, validate-app-manifest.sh is ported from ruby to python3+PyYAML.

It shelled out to ruby with stderr discarded, so on any machine without ruby
a missing interpreter was reported as "Valid YAML with top-level app block:
FAIL" and every manifest came back REJECTED. This is the first tool an app
developer runs, and it sent them to fix YAML that was never broken. Ruby was
also the odd dependency out — the repo already ships three python scripts.

It now checks for python3 and PyYAML up front and names what is missing, then
parses with PyYAML. Missing keys resolve to an absent-value object that
indexes to itself and prints empty, so call sites lost their per-hop guards:
  (((app["container"] || {})["build"] || {})["context"])
becomes app["container"]["build"]["context"]. Booleans still print as
true/false rather than Python's True/False — call sites compare == "true",
so Python's capitalisation would have silently inverted the readonly_root
and no_new_privileges security checks.

Verified: full rust suite 1148/1148, 0 failed. All 56 app manifests validate
(0 rejected, 0 errored) where previously every one was rejected. No signed
artifact modified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:08:39 -04:00
archipelagoandClaude e669a3e436 fix(iso): AIUI stage picks the NEWEST candidate and copies with --delete
RC1's aiui/index.html pointed at the stale checked-in demo bundle while
today's dist sat beside it unreferenced: demo/aiui was tried first, and the
rsync without --delete merged it over the fresh capture from /opt. Now the
newest index.html across all candidates wins (demo/aiui remains the
fresh-clone fallback) and the copy deletes before writing.

Found by mounting the RC1 ISO and diffing bundle hashes against the tree —
exactly the 'verify the frontend INSIDE the ISO' rule.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 11:55:24 -04:00
archipelagoandClaude 6815a7d1eb fix(assistant): disabled tools are listed and callable — the gate's refusal IS the Settings signal
Live evidence, two ways: the 9abc1623 banner never fired because D-16 hides
ungranted tools (model never calls → refused_categories always empty), and
the [[needs:id>]] marker fix failed because a small local model answers with
a workaround narrative instead of emitting structured markers.

The model's reliable, trained behavior is tool CALLING — so disabled tools
are now listed in a DISABLED prompt section and remain in the schema. A call
hits the execution gate, which refuses and records the category → the
trusted chrome offers Settings → AI Data Access. Deterministic and
model-independent. The prompt split is UX/attack-surface shaping; the
security boundary remains the server-side grant re-check in execute_tool
(loop_.rs), unchanged and now the single enforcement layer by design.

Tests: ungranted_tool_only_ever_in_disabled_section (section-aware),
disabled_tools_are_listed_as_callable_but_refused, marker extraction kept
as a harmless safety net. 127/127 assistant suite green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 11:51:02 -04:00
archipelago 5e71f256e4 docs: resume artifact — session 2 state (banner fix in flight, ISO cut) 2026-08-07 11:49:46 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 11:31:20 -04:00
archipelagoandClaude f15e2b5025 fix(assistant): a disabled-category request now fires the Settings offer
The 9abc1623 banner waited on refused_categories, but refused_categories
only fills when the model CALLS a gated tool — and D-16 hides ungranted
tools from the prompt, so the model never calls: it answered 'I can't do
that' in prose and the banner never fired. Live-verified: revoke media,
ask for content, no banner.

- build_system_prompt takes the disabled categories and teaches a marker:
  'say it can be switched on in Settings → AI Data Access and end with
  [[needs:<id>]]' — category names only, never tool names (D-16 holds)
- extract_needs_markers strips the markers from the reply and folds them
  into refused_categories; unknown ids pass through as text (an offer is
  the worst a bad marker can cause — never a grant)
- egress's seed-screen test now covers the new paragraph too

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:54:35 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 10:53:25 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 10:37:20 -04:00
archipelagoandClaude c2e71bc7e5 fix(aiui): play the node's own files before reaching for the network
usePlayer.play() never looked at song.sources[] — a real library track went
straight to (CSP-blocked) Wavlake and reported 'Not found on Wavlake' while
its bytes sat on the operator's disk. Node sources (same-origin /content/<id>,
Range-streamed) now play first; Wavlake is the metadata-only fallback.

FilmDetail likewise only played YouTube sources; own/peer/IndeeHub sources
(same-origin, media-src 'self') now win, YouTube stays the free-films
fallback.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:31:57 -04:00
archipelagoandClaude Opus 5 cc00884b98 docs: repair links left dangling by the operations-doc removal
Untracking the ops docs broke every reference to them. Repoints or removes
those references across README, architecture, ROADMAP, the archive index,
lifecycle TESTING, the hardening plan, and the security docs — pointing at
the issue tracker where a live task list was meant, and dropping the entry
entirely where it only existed to link an internal file.

Also fixes two pre-existing broken links found by validating every relative
link in the tracked docs:
- README linked docs/OPEN_SOURCE_READINESS.md, which never existed
  (underscores vs hyphens).
- reticulum-daemon/README.md linked a local Claude session plan at
  ../../.claude/plans/enchanted-strolling-rocket.md — outside the repo, and
  a path that would have shipped publicly pointing at nothing.

All relative links in tracked markdown now resolve: 0 broken.
audit-secrets.sh still 5/5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:10:25 -04:00
archipelagoandClaude Opus 5 fda7feda60 security: untrack operations docs; scrub infra identifiers from public docs
Operations docs move out of git entirely rather than being sanitized. They
stay on disk for local use and are gitignored, so the Phase 6 export (which
takes HEAD) can never carry them. 15 files: the fleet runbook, hotfix
process, node inventories, internal trackers, session handoffs, the key
rotation/signing-posture records, and the open-source plan itself.

For the docs that remain public, infra identifiers are replaced with things
that are better documentation rather than placeholders: curl examples now
use `archipelago.local`, the product's own mDNS name, so a reader can run
them as-is instead of substituting an address that was never theirs.

Deliberately NOT scrubbed, both verified as functional rather than leaked:
- `tx1138.com` is the shipped default block explorer (DEFAULT_TX_EXPLORER in
  useTxExplorer.ts, surfaced in WalletSettingsModal). Product behavior.
- `git.tx1138.com` in core/container/{image_policy,registry}.rs is a retired-
  registry constant the code matches on to strip stale entries from legacy
  node configs. Removing it would break migration for older nodes.
- `192.168.1.254` in bulletproof-containers.md is the LAN gateway in a podman
  bug description, and `192.168.1.x` in user-walkthrough.md is already generic.

Whether a personal domain should be the shipped explorer default in a public
product is a separate product question, not a security one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:08:18 -04:00
archipelagoandClaude Opus 5 39eb6b0553 docs(open-source): move Phase 0 rotation to a pre-publish gate
Sequencing change per user decision: credential rotation/revocation runs
last, immediately before Phase 6, instead of first. Safe under fresh-history
publish — the scrub commits never become public — but recorded as a HARD
blocking gate on Phase 6, with an explicit rotation sign-off added as a
numbered pre-publish step so "scrubbed" cannot be mistaken for "rotated".

Also corrects the plan against what execution actually found:
- the fleet password was in 8 tracked files, not 7 (3 in .planning/)
- both Gitea tokens are already dead (401); only the `ai` password is live
- the Framework node's SSH password was rotated out-of-band and is unrecorded,
  which would block it from receiving the fleet rotation
- .planning/ is 199 tracked files of internal agent state — added to Phase 2
  as the largest un-triaged internal block still in the tree

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:00:32 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 09:59:10 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 09:58:51 -04:00
archipelagoandClaude 7d57e2c39d fix(aiui): recommendation previews render in buckets the node left empty
The archy content latch was global and permanent: once mount-time content
latched archyContentActive, no extracted [[film_ext:…]] recommendation card
could ever render again, and an empty tool result kept the latch — so a
'recommend me films' turn beside an empty catalogue showed prose only, with
'Nothing found' overwriting nothing. The chat had lost its rich previews.

- setArchyContent records which buckets the node actually supplied
  (archySupplied) and latches active only on a non-empty delivery
- updatePanelFromText's no-overwrite guard is now per-bucket: node truth
  wins buckets it filled; empty buckets stay writable for extracted previews
- the extraction fallback title no longer clobbers 'Nothing found'/'Loading…'

Regression tests: previews render in an empty bucket, node truth survives
tags in a filled bucket, 'Nothing found' survives a both-empty turn.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:16:52 -04:00
archipelagoandClaude a91bc55df8 fix(assistant): surface shape bugs — films scope mime hint, apps_list items wrap
Two reasons the content surface 'often doesn't surface the content':

- content.indeehub-projects items carried no mime/filename, so the UI
  adapter classified every film 'excluded' and the films grid could never
  render. They are films: they now declare video/mp4.
- apps_list surfaced the container-list RPC's BARE ARRAY; the broker reads
  { items: [...] }, so the apps grid was silently dropped every turn.
  Wrapped at the tool boundary — the shared RPC's own shape is untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:16:52 -04:00
archipelagoandClaude 1ac08a3ed3 feat(assistant): recommendations check the catalogue first and tag knowledge picks
'recommend me 10 scifi films' answered prose-only and OFFERED to check the
catalogue — the preamble invited knowledge recommendations (paragraph 2) but
only ordered tool calls for existence questions (paragraph 3), so the model
never ran the tool and emitted none of the tags the iframe renders as rich
preview cards.

- discovery of a kind the node could hold (films/music/books…) now gets a
  catalogue-and-peers check FIRST, knowledge picks on top
- the 'would you like me to look?' stall is banned outright — looking is
  one tool call, do it then answer
- the preamble teaches the exact [[film_ext:Title|Year|Director]] /
  tv_ext / song_ext / book_ext / podcast_ext formats, same-line with the
  one-line reason, real titles only, never for items the tool returned

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:13:47 -04:00
archipelagoandClaude 3f22e4375d fix(ui): strict-TS cast in peers per-onion grouping
vue-tsc rejected the double cast; vitest strips types so it slipped
through. Narrow once into a local instead.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 08:45:46 -04:00
archipelagoandClaude c25fd8b650 fix(content): owner never pays for their own files; purchased serves from cache
- serve_content takes owner_session: a validated operator session skips the
  availability/paid gates (Availability::Nobody stays delisted); the cookie
  is re-validated in the content handler, same discipline as the model proxy
- the Tor proxy serves already-purchased items from the local content_owned
  cache with Range slicing (206) instead of re-hitting the seller's 402 —
  the buyer-side store exists so an owned item is never bought twice, and
  its cards were rendering as permanent placeholders
- adapter: 'own'-scope items never render locked (a locked card suppresses
  the playable URL — the placeholder-only grid the operator reported)
- broker: normalize 'purchased' OwnedRpcItems per item with the seller's
  onion, and group 'peers' items per seller onion, so buildMediaUrl gets a
  peerOnion and card URLs stop coming out empty

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 08:40:49 -04:00
archipelagoandClaude f0d2cdb7b2 fix(aiui): node-content surfaces stay open when the reply text infers no tabs
Reproduced live: 'show me paid for peer files' returned surfaces=1 with 3
purchased images over the bridge, then updatePanelFromText set panelOpen
from the REGEX-inferred tab list — a plain markdown list matches nothing,
so the panel closed and the user saw prose only. panelOpen now follows
orderedTabs (Archy tabs lead). Regression test pins the exact turn.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 08:20:41 -04:00
archipelagoandClaude d0c9ea6e61 fix(security): session-gate web search + screen forwarder egress (S3/S4)
S4: /aiui/api/web-search proxied straight to SearXNG with no auth — anyone
reaching the web port ran searches attributed to the node's IP. Now routed
through the daemon's session-gated model proxy like the claude/ollama legs
(both nginx server blocks), forcing format=json upstream (the client never
sent it — search could 200 with HTML that parsed as nothing).

S3: the forwarder also serves the STANDALONE frontend, whose bodies carry
full history/images with no assistant loop behind them — a pasted seed
phrase went to Anthropic unscreened. The forwarder now runs the egress
secret-shape scan (G-B1) with the node's own secrets dir as deny corpus on
Claude bodies and search queries; blocked requests get a plain-language 400.

Also fixes a REAL gap in the egress tokenizer found by these tests: a JSON
key glued to a string value's first word ('content":"abandon...') dropped
that word, so an exactly-12-word seed pasted as a bare message yielded an
11-member run — checksum misses, backstop misses. Non-member words now
rescan within the token. egress 15/15 + model_proxy 10/10 green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 08:03:07 -04:00
archipelagoandClaude d746fbd812 fix(assistant): app_logs redacts credential shapes before model context (S5)
The browser broker redacted log lines (password=/token=/macaroon key=value,
64+ hex, 64+ base64) while the node-side tool only untrusted-wrapped — so a
log line carrying rpcpassword=<32-hex> crossed to cloud backends below the
egress screen's threshold. Port the broker's three patterns to the tool
boundary as a pure line redactor + JSON walker; unit-tested (124 assistant
tests green).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 07:12:48 -04:00
archipelagoandClaude 3a3ceb2238 fix(permissions): assistant grants file is the one authority (live desync fix)
ai_grants_unified UNIONED the assistant grants.json with the legacy
settings/ai_permissions.json on every read. On archi-dev-box legacy held
all-ten and grants.json held four, so the Settings UI and the AIUI frame
saw every category ON while the assistant refused six — and no UI toggle
could fix it, because both write paths existed but only ai.permissions.set
synced both files. Now: an existing grants.json answers alone; the legacy
file is consulted only when no grants file exists (pre-unification
upgrade), and that read migrates forward and persists the authority.
assistant.grants-set now also rewrites the legacy file in step. Regression
tests: authority is not widened by legacy; migration folds forward once.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 07:12:37 -04:00
archipelagoandClaude 9bb38c36d3 fix(aiui): fabricated demo content is demo-build-only, fake credential scrubbed (S7)
Per operator decision 2026-08-07 (mock content is isolated to
demo.archipelago-foundation.org, never in shipped code): the auto-seeded
'node-demo' conversation (invented balances, file listings, bitcoin.conf)
no longer ships on nodes — a VITE_DEMO_CONTENT build flag (or dev) gates
it, /seed, and the Guide 'Load Demo' button. The genuine onboarding guide
still seeds everywhere. The fixture's bitcoin.conf rpcpassword is now
unmistakably example-shaped: fake must never look like a real credential.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:28:51 -04:00
archipelagoandClaude 4e6df488fc fix(aiui): Claude API key never persisted in plaintext again (S2)
The key rode the wholesale settings→localStorage save, sitting at rest
readable by any same-origin script, while the AES-256-GCM key-vault built
for exactly this sat bypassed. Now: the key lives in a memory-only store
ref, persists only into the encrypted vault when a passphrase session is
active (migrating into the vault on unlock), and a one-time migration lifts
any existing plaintext key out of localStorage and re-saves the scrubbed
settings object immediately. Settings UI reports honestly how the key is
held. Typecheck clean; test suite unchanged (348 pass, 3 pre-existing fails).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:28:51 -04:00
archipelagoandClaude a3edb848e1 docs(13): correct peer-files finding per bca18c03 — code bugs, not fleet outage
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:14:15 -04:00
archipelagoandClaude 9f579162d1 fix(assistant): network_status strips WAN IP + Wi-Fi SSID from model context (S1)
The Network permission's Settings label promises 'no IP addresses', and the
browser-side broker honours it — but the node-side tool forwarded
network.diagnostics verbatim, so a granted Network category sent the node's
WAN IP and SSID (both location-identifying) to cloud model backends. Strip
both at the tool boundary; NAT/UPnP/Tor/DNS connectivity shape stays.
Pure helper + unit test (123 assistant tests green).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:14:15 -04:00
archipelagoandClaude Opus 5 c9de1e6c53 wip: phase 13 AIUI paused at 6/17 (operator demo list)
Handoff carries the peer-files correction, the podman-lifecycle trap, the
concurrent-agent warning, and the release-binary drift that blocks the ISO.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 06:04:23 -04:00
archipelagoandClaude Opus 5 bca18c03d2 docs: resume artifact — AIUI surfaces, the peer-files correction, SearXNG
Carries the full task list with per-item status, the three commits'
rationale, the live measurements that overturned the earlier
peers-have-no-content conclusion, the browser-verification recipe, and
the binary-drift blocker that must clear before the ISO.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 06:00:54 -04:00
archipelagoandClaude 9cf1c12213 docs(13): operator decision — mocks isolated to demo site, not shipped code
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 05:53:59 -04:00
archipelagoandClaude a7368b8b1d docs(13): assessment plan tracks b1c5d138 + demo-mode decision for mocks
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 05:52:26 -04:00
archipelagoandClaude 1eb75a1ed9 docs(13): full AIUI assessment + four-wave fix plan (security/mission/content)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 05:49:43 -04:00
archipelagoandClaude Opus 5 b1c5d13850 fix(aiui): node content outranks the prose surface, and web search resolves
Browser-verified on archi-dev-box; all three were only visible by
driving the real UI.

The tab bar. `setArchyContent` put the node's grids up, then
`updatePanelFromText` replaced the bar with tabs inferred from the reply
text. "show me my own shared content" therefore landed on an "AI Brief"
— a prose restatement of the answer already on the left — with the
populated image grid no longer reachable. Guarding the panel arrays was
not enough: they held the right data while the tab bar had discarded the
way to see it. Archy tabs now lead, and the title follows the leading
tab. The prose stays; it just is not the only thing shown.

Tab order follows bucket size. A node with 13 photos and 2 tracks opened
on Songs and titled itself "2 Songs" for a 15-item answer.

Web search never worked embedded. `searchWeb` hardcoded
`/api/web-search` while every other call is built from BASE_URL. Under
`/aiui/` that asked the HOST for a path only the AIUI-scoped nginx
location serves, so it hit the node's API gate for a 403 and the CSP
refused the connection on top. Now BASE-relative. Additionally, the
embedded path skips the client-side search entirely: `streamViaArchy`
sends only the user's text, so the system prompt those results were
folded into is never transmitted — it was a round trip and a console
error per turn whose output provably reached no model. Web search for
the embedded path belongs node-side, with the other tools.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 05:48:39 -04:00
archipelagoandClaude Opus 5 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>
2026-08-07 05:07:10 -04:00
archipelagoandClaude Opus 5 9abc162394 fix(aiui): the content surface renders what the assistant found
Four defects, one visible symptom: a correct prose answer beside an
empty grid.

1. The assistant's curated RPC bridge had an arm only for
   `content.list-mine`. `tools.rs` mapped the `peers`, `purchased` and
   `films` scopes onto three real, dispatcher-registered handlers that
   `assistant_dispatch_tool` had never heard of, so every non-"own"
   scope died on its catch-all. Downstream that read as "the peers have
   no content" — it was a missing match arm, and the tool never ran.
   Regression test added: every scope the schema advertises must reach a
   real handler.

2. `content.browse-all-peers` wrapped its whole fan-out in one
   `timeout(..).unwrap_or_default()`, which DISCARDED every completed
   batch the moment the budget expired. One slow peer turned a
   partly-successful browse into "0 reached, 16 unreachable". Observed
   live on archi-dev-box: back-to-back calls returned real peer items,
   then nothing. Now accumulates per batch and checks a deadline between
   them, so partial results always survive. Budget 20s -> 45s: two
   batches of eight at a 10s per-peer timeout had no headroom at all.

3. `assistant.chat` returned only `{ text }`. The structured results of
   any content tool the turn ran were dropped inside the loop, so the
   surface had nothing to render. The turn now carries them through
   (captured raw, before the untrusted wrap, since they go to a renderer
   that treats every field as inert data, never back into the prompt).

4. The adapter classified images as 'excluded' and dropped them. A node
   sharing mostly photos rendered as an empty grid while AIUI's image
   grid sat unused. Images now have a bucket, with the paid-lock and
   extension-fallback handling audio and video already had.

Also: the panel says "Loading…" while a turn is in flight and "Nothing
found" when it comes back empty, instead of leaving the previous
query's heading standing as though it answered this one; the system
prompt tells the model to call the content tool and summarise rather
than re-list what the cards already show; and a refused tool now names
its permission category so the trusted chrome can offer the settings
screen instead of leaving "I don't have a tool for that" as the only
clue.

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