Commit Graph
2949 Commits
Author SHA1 Message Date
archipelagoandClaude Fable 5 db11c625c8 test(13-08): failing tests for the D-07/D-11 confirm gate (RED)
- confirm.rs: ConfirmGate/PendingConfirmation/Confirmed/PendingSnapshot/
  ResolveRefusal API skeleton (request/resolve/mint_nonce/build_description
  still todo!()) plus the five named confirm tests: S-02 nonce binding,
  S-03 no-model-text, S-08 distinct resources, S-09 restart drops pending,
  timeout declines, and the no-shared-lock-across-the-wait case
- mod.rs: ToolExecCtx gains the confirm gate (global by default, injectable
  for tests) and the S-01 destructive_tool_requires_confirm test with a
  seeded installed-app snapshot
- verified RED: 7 new tests fail (todo! cores + unfilled destructive branch)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 07:12:12 -04:00
ssmithxandClaude Sonnet 5 53b158ce5a fix(wallet): translate Cashu NUT error codes into plain-language messages
Mint HTTP failures (swap/melt/mint-quote) were surfacing raw JSON bodies
like {"detail":"proofs already spent","code":11001} straight to the
user. Add a translator for the NUT-02/03/04/05 transaction-validation
error codes (10001-11017, 12001-12003; see
https://github.com/cashubtc/nuts/blob/main/error_codes.md) and layer it
onto the mint_client bail sites via anyhow context, so the top-level
message is actionable while the raw status/body stays available via
{:#} for logs. receive_token now surfaces the real reason (e.g. "This
ecash has already been redeemed") instead of a generic "Failed to
receive any proofs from token" when every mint in a token fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 09:41:44 +00:00
archipelagoandClaude Fable 5 c35e33d0a7 docs(1.7.122): curate release notes and add the in-app What's New block
Leads with what changes for the operator: app screens now require the node
password across LAN, Tailscale, mesh and Tor; the wallet/protocol ports that
must stay open stayed open; the mesh leak found during on-node verification;
nodes repairing their own legacy containers; and the signing-key rotation.
Known gaps disclosed, including the eleven still-undeclared ports and that
non-browser clients will now meet the login page.

The new block uses <strong> rather than the literal ** markers in earlier
entries, which render as asterisks in the modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 16:33:03 -04:00
archipelagoandClaude Opus 5 3f4b5524b1 chore(trust): rotate the release root to z6Mkfu5LT…DLWT
DO NOT MERGE INTO A RELEASE SIGNED WITH THE NEW KEY. See below.

The previous release root (z6Mkkid…q7ur, pinned 2026-07-02) was exposed
in a chat transcript and is treated as compromised. It signs both OTA
manifests and the app catalog, so anyone holding it could sign updates
the fleet would install.

Pins the new key in trust::anchor and moves EXPECTED_DID in all three
signing/publishing scripts.

ORDERING IS CRITICAL — nodes pin the OLD key:

  * The release CARRYING this commit must be signed with the OLD key.
    That is the only signature a node running the previous binary will
    accept, and it is what installs the binary pinning the new key.
  * Only the release AFTER that may be signed with the new key.
  * Signing this release with the new key makes every node reject it,
    ending OTA fleet-wide and requiring hands-on recovery per node.

sign-catalog.sh moves in the same commit, so the app catalog must also be
re-signed with the new key once this ships, or nodes accept the binary
and reject the catalog.

Key verified before pinning: the hex and the did:key are the same
keypair, checked with a base58 decoder round-tripped against the previous
known-good pair. An earlier candidate hex (cb830e13…) was rejected
because it decoded to a different DID than the one supplied — pinning it
would have made every node reject every future update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:10:32 -04:00
archipelagoandClaude Fable 5 7025c5f26f docs(13-07): state + roadmap — 13-07 complete, next 13-08
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:34:26 -04:00
archipelagoandClaude Fable 5 17da7a7233 docs(13-07): summary — music index + music.* surface complete, 23/23 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:15:41 -04:00
archipelagoandClaude Fable 5 d3b0ed8a1d feat(13-07): music.* RPC surface behind one dispatcher arm
- api/rpc/music.rs: handle_music prefix sub-dispatcher (assistant_chat.rs
  shape) with list-albums / list-artists / list-tracks / status / reindex
- one guarded dispatcher.rs arm for the whole music. prefix, adjacent to
  the content.* block; the only registration point for the surface
- list-tracks: optional album_id filter, limit/offset pagination, limit
  clamped to [1,500] (default 100) — out-of-range degrades, never errors
  (T-13-41)
- reindex spawns the scan and returns immediately; second call while one
  runs reports already-running with the last stats; optional
  incremental:true routes to refresh_incremental so a changed library is
  reflected without a full re-extraction
- newer-schema index served as an empty library, never overwritten or
  reinterpreted by readers (13-MUSIC-MODEL.md downgrade contract)
- nothing music.* in UNAUTHENTICATED_METHODS — the surface rides the
  session/CSRF/RBAC gate; asserted by music_methods_require_session
  (T-13-40)
- 7 tests, one per Task 2 behavior bullet plus the incremental mode

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:09:18 -04:00
archipelagoandClaude Fable 5 0f21f598aa fix(security): FIPS mesh relay must not republish auth: local ports
Caught verifying the gate fixes on archi-dev-box: [fips0-ULA]:32838
answered HTTP 200 straight from nbxplorer with no credential. The
catalog declares that port auth: local — host-local by intent, pinned to
loopback, the gate deliberately keeps its hands off — but the mesh relay
bridges a STATIC port list to 127.0.0.1, so it republished it to the
whole mesh. Same bug class as the Tor onion gap: a transport that
converges on the app loopback without consulting the declaration.

PortMap now records declared-local ports and the relay withholds them
(tearing down an existing bridge if a catalog refresh newly declares
one), alongside the declared-gated withhold. Undeclared ports keep
todays behaviour — silence is not an instruction in either direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:40:28 -04:00
archipelagoandClaude Fable 5 d2e4b00789 fix(security): gate classifies from the catalog overlay and releases withdrawn claims
Dev-box verification of the Tor/FIPS fixes caught a pre-existing split
brain: the orchestrator publishes containers from the signed catalog's
embedded manifests (origin-wins), but the gate classified ports from the
stale disk manifests — so it externally bound nbxplorer 32838, a port
the catalog declares auth: local and pins to loopback. Reachable behind
a login, but reachable where it deliberately was not.

- build_port_map now consults the catalog overlay first, via the same
  parse/validate/image-only filter the orchestrator uses (moved to
  app_catalog::catalog_manifest_overlay so the two cannot diverge again).
- GatedPort carries . The gated set still includes undeclared
  Session-default ports for challenge/audit, but every action that
  REDIRECTS traffic — the torrc 127.0.0.2 repoint, the FIPS relay
  stand-down, the Tor-upstream bind — now keys on the declaration.
- The sweep releases held claims whose port left the gated set, so a
  catalog refresh that withdraws a port (gated → local/none) takes
  effect without a daemon restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:50:46 -04:00
archipelagoandClaude Fable 5 49687f7ebd feat(13-07): persisted music index — scan, group, atomic save, incremental refresh
- music/index.rs: reindex + refresh_incremental over the media roots,
  (path, mtime, size) diffing so unchanged files never re-extract, rows
  removed when files disappear (derived albums vanish with their last
  track), per-file extraction errors counted in ScanStats.skipped
- save_atomic: temp sibling + fsync + rename — a concurrent read sees a
  complete index or the previous one, never a partial file (T-13-42)
- load refuses schema_version > MUSIC_SCHEMA_VERSION with a distinct
  NewerSchema error and never overwrites the newer file (T-13-43)
- symlinks whose canonical target escapes the media roots are skipped,
  not followed (T-13-39); confinement enforced here and in tags.rs
- reindex guard: AtomicBool + RAII release; a second concurrent scan
  reports already-running instead of duplicating the walk (T-13-41)
- music/mod.rs: media_roots(Config) (filebrowser/Music +
  purchased-content) and LibrarySnapshot (tracks + derived albums/artists)
- 9 tests, one per 13-07 Task 1 behavior bullet, programmatic FLAC
  fixtures into tempdirs (no committed binaries)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:18:06 -04:00
archipelagoandClaude Fable 5 e46af8cfe5 feat(security): self-heal legacy containers on declared bind drift
Legacy pre-quadlet containers kept publishing 0.0.0.0 after the catalog
pinned their app to loopback, because host_port_bindings_drifted only
compared host PORT numbers — closing them needed a manual package.update
per app per node. The drift check now also compares the bind ADDRESS,
but only when the manifest declares one: an empty bind never fires,
since recreating a loopback-published container to wildcard on silence
is exactly the v1.7.121 Bitcoin-RPC incident. With this, every node
recreates its legacy containers to the declared state on its own after
the OTA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:14:01 -04:00
archipelagoandClaude Fable 5 f08ed79b8a fix(security): FIPS v6 relay hands gated ports to the app gate
The mesh relay is a raw unauthenticated forward to the app's loopback,
and whether it or the gate owned a fips0 ULA port was decided by a bind
race — the dev box happened to be safe because the gate bound first.
The relay now skips ports declared auth: gated and tears down any
existing bridge for a port that became gated since it was bridged
(catalog refresh), releasing the bind for the gate's next sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:14:00 -04:00
archipelagoandClaude Fable 5 3760a00ea3 fix(security): Tor onions for gated ports forward to the gate, not the app
Tor carries no session cookie, so HiddenServicePort → 127.0.0.1:<port>
reached the app around the gate — the last transport the gate did not
cover. The gate now binds 127.0.0.2 (its own loopback, distinct from the
app's 127.0.0.1, so no app needs a second port), and regenerate_torrc
forwards declared-gated ports there. Undeclared ports keep today's
target: absence of the field is not an instruction.

The 127.0.0.2 claim deliberately does not count toward the unprotected
audit — a port whose only claim is the Tor loopback is still wide open
on the LAN and must keep warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:13:59 -04:00
archipelagoandClaude Fable 5 589cbb030f docs(13-04): complete music model + tag extraction plan — summary, state, config
- 13-04-SUMMARY.md: all 3 tasks, broken-pipe recovery (verbatim wip
  checkpoint be8f24b4), MP3 fixture off-by-one deviation, 7/7 tests green
- STATE.md: 13-04 complete (7/15 phase-13 plans), D-13 decision + lofty
  gate recorded in accumulated context, next = waves 3+
- config.json: fold in pre-existing use_worktrees=false from the broken
  session (wave-continue bookkeeping, intentionally kept)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 07:06:10 -04:00
archipelagoandClaude Fable 5 4b577493b1 feat(13-04): tag extraction across mp3/flac/m4a/ogg with media-root confinement
Completes Task 3 on top of the recovered wip checkpoint (be8f24b4):

- fix the programmatically-generated MP3 fixture's frame length: lofty's
  Header::read computes samples*bitrate*125/sample_rate with truncating
  integer division BEFORE adding the padding byte, so the FF FB 52 C4
  frame is 209 bytes, not 210 — the off-by-one made cmp_header miss the
  second frame sync and reject the whole file as containing an invalid
  frame (mp3_id3v24_yields_full_record now passes; fixture-only fix,
  production code untouched)
- all 7 music::tags tests green; no binary audio fixtures committed
  (fixtures are built byte-by-byte into tempdirs at test run time)
- extract_tags canonicalizes and confines to caller-supplied media_roots
  before opening any file (T-13-20); non-audio is a distinct NotAudio
  error vs the Ok/has_tags=false untagged fallback (T-13-21)
- entity types in music/mod.rs implement 13-MUSIC-MODEL.md exactly:
  hybrid-identity TrackId, derived albums/artists, MUSIC_SCHEMA_VERSION=1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:42:40 -04:00
archipelagoandClaude Fable 5 8210ca0a2a chore(catalog): sign catalog with port auth policy — 20 UIs gated, exemptions declared
Embeds the manifest port declarations (bind: 127.0.0.1 + auth: gated on 20
HTTP UIs, auth: local on loopback backends, auth: none + rationale on
protocol ports) into the signed catalog so nodes enforce the app gate.
Also carries bitcoin-ui/lnd-ui 1.7.119 version drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:28:06 -04:00
archipelagoandClaude Fable 5 6d9d87caa6 chore: sync Cargo.lock with the 1.7.121-alpha version bump
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:09:37 -04:00
archipelagoandClaude Fable 5 16642ad8b8 feat(security): declare port auth policy across the app manifests
20 HTTP UIs move to bind: 127.0.0.1 + auth: gated (the daemon owns their
external addresses and authenticates every connection); 5 loopback-only
backends declare auth: local so the gate keeps its hands off. Protocol
ports (LND, bitcoin p2p, electrum, CLN, gitea SSH, Wyoming, mDNS/SSDP)
were already declared auth: none with rationales in earlier commits.

Inert until the catalog is re-signed: nodes act only on declared fields
delivered via the signed catalog, and the catalog overlay overrides these
disk manifests everywhere they are installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:09:37 -04:00
archipelagoandClaude Fable 5 be8f24b4e3 wip(13-04): checkpoint Task 3 tag-extraction work recovered after broken pipe
Verbatim checkpoint of uncommitted executor work (music/mod.rs, music/tags.rs,
mod music; in main.rs) before verification. Tests not yet run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:05:12 -04:00
archipelagoandClaude Fable 5 6156444004 chore(13-04): add lofty 0.24.0 for music tag extraction
Task 2 checkpoint:human-verify resolved by operator approval
2026-08-04: crates.io lofty — 811,138 total downloads, 248,280
recent, 56 versions spanning 2021-04-23 to 0.24.0 (2026-04-12);
repo github.com/Serial-ATA/lofty-rs — 348 stars, 1,962 commits,
fuzzing + benchmark infra, MIT/Apache-2.0 dual license, active CI,
no squatting indicators. Dependency tree reviewed via `cargo tree -p
lofty`: byteorder, data-encoding, flate2, log, ogg_pager, paste — no
networking or process-spawning crates. `cargo build --package
archipelago` exits 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 05:13:30 -04:00
archipelagoandClaude Fable 5 bca4660a95 docs(13-04): record D-13 one-way music entity model decision
Task 1 checkpoint:decision resolved by operator: hybrid-identity (path
row key, lazily-backfilled content-hash dedupe column), derived-albums
(computed at read time from track tags, not stored rows), a single
JSON index at data_dir/music/index.json matching content_server.rs's
load_catalog precedent, and both own-library + peer sources indexed.
MUSIC_SCHEMA_VERSION starts at 1; a newer-version index on an older
binary is treated as absent rather than reinterpreted or overwritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 05:08:29 -04:00
archipelagoandClaude Fable 5 bf9b4e6ad3 docs(13): 13-09 complete — nginx sync merged live, CSP boundary browser-verified
Recovered after a broken-pipe session cut off right after Task 4 finished:
the summary was fully written (Self-Check PASSED) but never committed.
Re-verified on resume before committing: /aiui/-scoped CSP header live on
archi-dev-box, build/verify scripts present+executable, render screenshot
intact. STATE.md advanced: 6/15 plans done, next is 13-04.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 04:57:30 -04:00
archipelagoandClaude Opus 5 b3b580aab5 docs(13): 13-09 checkpoint pause point — Tasks 1-3 done, Task 4 needs nginx sync + human browser verify
Continuation executor (post-reboot) ground-truthed the 30b2e02f WIP
build-aiui.sh checkpoint as complete/correct, finished Tasks 2-3 and
scripts/verify-aiui-deploy.sh, and proved the build+deploy+verify cycle
end-to-end on the real archi-dev-box node (this machine). Paused at Task
4's remaining human/browser-required steps because the live node's nginx
config predates even 13-02 — syncing it is a bigger diff than this plan's
own CSP addition and belongs to a human-supervised deploy, not an
unsupervised executor push to a live node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:25:35 -04:00
archipelagoandClaude Opus 5 3756ebffc0 feat(13-09): verify-aiui-deploy.sh fetches live chunks via sw.js, never disk
Post-deploy check for AIUI: resolves the LIVE chunk set by fetching the
service worker's precache manifest (sw.js — vite-plugin-pwa's
generateSW-mode workbox.precacheAndRoute([{url:...}]) array) over HTTP,
fetches each live chunk, and greps the fetched bytes for a marker string.
Exits non-zero when the marker is absent from every live chunk.

This exists because the node's assets/ directory is a never-pruned
graveyard (feedback_node_side_frontend_verify_stale_chunks): a disk grep
reports "deployed" before the deploy actually happened, because a dead
chunk from an old build still contains the old string. Never opens a
remote shell onto the node and never greps the node's filesystem directly
— every check is an HTTP fetch, exactly what a browser session would do.

Verified locally against a real AIUI build served over HTTP: a marker
actually present in a live-precached chunk (index.html) passes (exit 0,
2 chunks checked before the match); a nonexistent marker correctly fails
(exit 1) as the negative control — not a check that always passes.

Wired into deploy-to-target.sh's primary AIUI deploy path in the prior
commit (073bf6f3), which already calls this script by name after the copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:22:52 -04:00
archipelagoandClaude Opus 5 c3bffbd557 feat(13-09): widen same-host deploy guard from containment-only to any mismatch
Adds assert_safe_same_host_deploy(local_src, remote_dst) to lib/common.sh:
pure, no SSH inside it, callable directly from a test with fixed inputs.
Returns 0 only when the two already-resolved paths are equal; refuses
(non-zero, message naming both paths + the 2026-07-31 incident) on any
other same-host mismatch.

This closes a real gap in the 2026-07-31 incident's original fix: the old
guard's two `case` blocks refused only containment (source-in-destination
or destination-in-source). A SIBLING directory — for example this very
worktree, archy-phase13, deploying onto TARGET_DIR's resolved symlink
target (archy, the main checkout) — is neither contained by nor containing
of the destination, so the old guard let it through and `rsync --delete`
would have mirrored the sibling onto the main checkout, deleting everything
the sibling lacks. Found while retargeting deploy-to-target.sh for D-19,
not a D-19 effect itself.

deploy-to-target.sh's guard block now calls assert_safe_same_host_deploy
instead of the two inline containment-only case blocks (old logic removed,
not left dead alongside the new call).

tests/production-quality/deploy-guard-same-host.sh pins all five
<behavior> cases (identical/contained/containing/sibling/unrelated)
against the function with no SSH, no rsync, no real deploy — including the
exact archy-phase13-vs-archy pair as the sibling-directory regression pin.
Manually confirmed non-vacuous: flipping the sibling fixture's expectation
to "allow" makes the test fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:19:49 -04:00
archipelagoandClaude Opus 5 073bf6f33d feat(13-09): retarget AIUI build/deploy to the in-repo aiui/ tree (D-19)
Finishes the 13-09 build-aiui.sh WIP checkpoint (30b2e02f) that survived the
operator reboot ground-truthed and confirmed correct: require_base_path,
frozen-lockfile install, verify_dist's asset-href/commit-attribution checks
all verified working against a real build. Fixed one grep-forbidden leftover
(a comment mentioning the retired scripts/aiui.pin path).

Rewires both AIUI sections of deploy-to-target.sh (primary --live path and
the --both/secondary path) and setup-aiui-server.sh to build/deploy from
aiui/packages/app/dist instead of the retired ../AIUI sibling checkout:

- Primary section now calls scripts/build-aiui.sh instead of an inline
  `pnpm build`, then scripts/verify-aiui-deploy.sh after the copy. The
  demo/aiui/ fallback now prints a loud, unmissable warning naming that it
  is shipping a checked-in dist rather than a fresh build.
- Secondary/--both section retargeted to the in-repo dist path; its
  fallback-to-.228-streaming behavior is otherwise unchanged.
- setup-aiui-server.sh calls scripts/build-aiui.sh automatically when the
  dist is missing or stale, instead of printing a manual `cd ../AIUI/...`
  command and exiting (D-15: enforced, not remembered).

No remaining `../AIUI` reference in either script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:19:21 -04:00
archipelagoandClaude Opus 5 442122083f docs(13): reboot pause point — full resume state in stopped_at
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:29:21 -04:00
archipelagoandClaude Opus 5 30b2e02f4a wip(13-09): checkpoint in-progress executor work before operator reboot
build-aiui.sh (mid-write) + aiui/.gitignore, committed verbatim and UNVERIFIED
— not a task completion. The 13-09 executor will be killed by the reboot; its
continuation should read this checkpoint, judge it against the plan's
must_haves, and reset --soft / build forward as appropriate (same recovery
pattern as the 6ba52b22/13b576da broken-pipe rescue at the start of this phase).
Already committed by 13-09 before this: 6ac0ebbf (CSP sandbox task).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:28:54 -04:00
archipelagoandClaude Opus 5 4e455167e9 fix(release): accept https remotes when publishing assets
Publishing v1.7.121-alpha failed on auth after the manifest had already
passed every check. The script required an `http://user:token@` remote,
which left only `gitea-vps2` — whose token is dead — and rejected
`gitea-ai`, the https remote whose credential actually works for git
push. Same Gitea instance (146.59.87.168, v1.27.1) either way, so the
restriction bought nothing and blocked the one usable path.

Accepts http and https, and carries the scheme through to the API URL
instead of hardcoding it.

Note for diagnosis next time: `/api/v1/repos/.../releases` is publicly
readable, so a 200 there does NOT prove the credential works. Use
`/api/v1/user`, which requires real auth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:23:28 -04:00
archipelagoandClaude Opus 5 57628a2aa4 fix(aiui)+docs: keyboard-resize parity for standalone AIUI; companion handover note
Operator-reported: keyboard opening in chat pads the tab bar and scrolls the
page instead of scaling the chat window. Triage: neode-ui already ships
interactive-widget=resizes-content + the --visual-viewport-height var, so
mobile-web Chrome resizes correctly — but an Android WebView ignores that meta
entirely, and the described pan-plus-padding is the adjustPan/edge-to-edge-
without-IME-insets signature. Companion-side fix documented for handover in
docs/companion-keyboard-viewport.md (manifest adjustResize, or IME insets when
edge-to-edge, plus a chrome://inspect verification recipe). Web side gets the
one real parity gap: AIUI's standalone index.html lacked the meta neode-ui has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:21:53 -04:00
archipelagoandClaude Opus 5 86129d5da2 fix(aiui): banner follows the selected item — reset fallback state on identity change
Operator-reported: selecting a different item in the content window left the
context-surface banner showing the previous item's image. Root cause: detail
views are reused, not remounted, and useBannerFallback kept primaryIndex/
stage/apiUrl alive across the prop change — once stage hit 'api' or 'done' it
never re-evaluated. Reset is keyed on title + the primary URL set, with a
generation guard so an in-flight fetch for the old item cannot stamp its
artwork onto the new one. Heals Film/TVSeries/Book detail at once; 3
regression tests pin it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:16:26 -04:00
archipelago cd58242935 chore: release v1.7.121-alpha
Demo images / Build & push demo images (push) Successful in 3m26s
v1.7.121-alpha
2026-08-04 03:13:45 -04:00
archipelagoandClaude Opus 5 6ac0ebbf0a feat(13-09): enforce the /aiui/-scoped CSP sandbox boundary (AIUI-04)
Adds a Content-Security-Policy header to both nginx `location /aiui/`
blocks whose connect-src is scoped to the AIUI path prefix, so AIUI's
own JavaScript is browser-prevented from issuing a same-origin fetch
to /rpc/v1 with the ambient session cookie. Explicitly rejects the
`sandbox` iframe attribute (allow-scripts + allow-same-origin is the
known escape; dropping allow-same-origin breaks AIUI's storage and
its origin-checked bridge) and records why in both the nginx comment
and a new comment above the Chat.vue iframe. Adds
referrerpolicy="no-referrer" to the iframe so a media URL or page path
never leaks upstream via Referer.

Also adds an explicit `location /aiui/api/openrouter/ { return 404; }`
to both server blocks, closing 13-02's Task 3 checkpoint finding
(operator-accepted deviation 2026-08-03): the relay was already
structurally gone but the SPA catch-all served 200/405 instead of 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:11:36 -04:00
archipelagoandClaude Opus 5 5e3aab30a2 fix(aiui): header overlay panels get readable opacity — scoped, not blanket
Operator-reported: the model-picker and conversation-menu overlays are hard to
read over busy chat content at path-glass-card's shared rgba(0,0,0,0.65).
Scoped .header-overlay-panel (0.88) in ChatHeader.vue only — path-glass-card
itself is untouched, so BookDetail/ArticleDetail/TVSeriesDetail/WebsiteDetail/
ContentPanel/ChatWindow keep their existing glass. Unlayered scoped rule beats
the @layer components class without !important, and reaches the panels through
their Teleport to body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:11:02 -04:00
archipelagoandClaude Opus 5 06c934bd59 docs(todos): capture two operator-reported AIUI issues
Mobile load speed and the 'how to use AIUI' brief not opening. Both noted with
the caveat that the deployed AIUI bundle is stale (pre-D-14), so they must be
reproduced against a fresh in-repo build before being chased.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:00:06 -04:00
archipelagoandClaude Opus 5 484b1209a8 docs(13): close windows 16 and 19 — assistant tests observed passing
21 passed / 0 failed across assistant::, plus assistant_methods_require_session
run explicitly. Both windows had non-defect root causes: window 19 was lane
staleness (missing 0de67ca6's PortMapping test-constructor fix, which made the
whole crate's test build fail), and window 16's repeated kills were the
orchestrator's own too-short timeout sending SIGTERM on a cold build, which I
had wrongly attributed to memory contention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:57:49 -04:00
archipelagoandClaude Opus 5 9d225473b1 docs(1.7.121): record what shipped, both gate incidents, and the .122 queue
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:49:36 -04:00
archipelago 7cc58b7ac6 merge(13): bring main forward — unblocks the crate's test build
The lane merged main at 0c4826f8, one commit before 0de67ca6 added
auth/auth_rationale to PortMapping's test constructors in prod_orchestrator.rs.
That left the lane unable to compile ANY test in the archipelago crate, which
is why 13-05 could not observe its 13 tests pass (window 19). Not a defect in
this phase's work — just staleness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	core/archipelago/src/main.rs
2026-08-04 01:50:39 -04:00
archipelago 0a0a2fde43 docs(13-05): complete curated tool registry and default-closed grants plan 2026-08-04 01:39:28 -04:00
archipelagoandClaude Opus 5 c098124d93 test(13-05): assert the D-09 ceiling over the whole registry, not a hardcoded tool list
Task 3: four registry-wide structural tests in tools.rs, iterating registry()
so a future tool that crosses the D-09 ceiling fails CI rather than depending
on a reviewer noticing:

- registry_never_exposes_excluded_authority (S-04/T-13-24): scans every
  ToolDef's name+description for EXCLUDED_AUTHORITY_TERMS.
- read_tools_never_confirm (S-07/T-13-31): every non-destructive tool
  executes via the real execute_tool choke point without raising anything
  confirmation-shaped. bitcoin_status/network_status excluded from live
  execution (their handlers make real outbound network calls that would
  make this test flaky on a sandboxed box); their destructive:false
  placement is still covered by the other assertions.
- loop_is_bounded (S-13/D-05): MAX_TURNS is enforced, and 3 consecutive
  malformed-argument calls for the same tool name abort the turn with an
  apology before a 4th scripted backend turn is ever polled.
- every_tool_has_explicit_category_and_destructive: sanity-checks the
  registry has exactly the 13 hand-written tools (4 destructive) that made
  it in, as a runtime backstop to the acceptance criteria's static grep for
  `..Default::default()`.

Negative-case demonstration (per the plan's acceptance criteria): a
hypothetical `wallet_send_sats` tool with a description mentioning
"spending sats" trips EXCLUDED_AUTHORITY_TERMS's "spend" term, verified by
tracing the exact haystack-contains logic registry_never_exposes_excluded_authority
runs (see 13-05-SUMMARY.md for why this was traced rather than executed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 01:34:43 -04:00
archipelagoandClaude Opus 5 90706fe053 feat(13-05): expand curated tool registry to full D-06/D-09 allowlist, wire D-16 default-closed grants
Task 1: registry() grows from the tracer's single system_disk_status tool to
the full 13-tool D-06 curated allowlist (9 read tools, 4 destructive write
tools), each hand-written with its own JSON Schema, PermissionCategory and
destructive flag -- nothing derived from api::rpc's method table. Adds
EXCLUDED_AUTHORITY_TERMS (D-09's excluded authority, scanned by Task 3's
registry-wide test), SETTABLE_KEYS/READABLE_SETTINGS_KEYS (AIUI-02's
hand-picked settings surface, claude_api_key permanently absent from
SETTABLE_KEYS), tools::dispatch (per-tool RPC dispatch) and
tools::validate_business_rules (allowlisted-key / installed-app-id
validation that runs before the destructive/confirm gate so a plainly-wrong
request is refused with the real reason instead of the generic
"not yet implemented" placeholder). assistant_dispatch_tool gains a params
argument and the RPC method table Task 1's tools need.

Task 2: grants.rs adds Grants (D-16 default-closed permission-category
store, persisted 0600 under data_dir/assistant/grants.json; a missing file
is default_closed(), never permissive). CallerScope::granted_categories
becomes async and reads the persisted store instead of a hardcoded default;
CallerScope::Mesh gains an `authorized` field so a mesh peer's ceiling is
never wider than the operator's own grants. ToolExecCtx gains the AI-SPEC
S-13 consecutive-validation-failure counter (>2 failures for the same tool
name aborts the turn with an apology, checked in run_loop). build_system_prompt
appends only currently-granted-category tools' names/descriptions -- an
ungranted tool never appears in the prompt string (defense in depth; the
execute_tool grant re-check is the actual gate). assistant_chat.rs adds
assistant.list-tools / assistant.grants-get / assistant.grants-set, all
routed through the existing single assistant.* dispatcher arm (dispatcher.rs
untouched, verified by git diff --exit-code).

dispatcher.rs is not touched -- all new RPC surface goes through 13-01's
assistant.* prefix arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 01:28:29 -04:00
archipelagoandClaude Opus 5 e20d7a14fb docs(whats-new): add the v1.7.121-alpha block to the in-app modal
Demo images / Build & push demo images (push) Successful in 3m41s
The release gate requires every CHANGELOG version to have a matching
block in Settings > What's New. Generated by scripts/sync-whats-new.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 01:22:16 -04:00
archipelagoandClaude Opus 5 1929f6a870 style: rustfmt the appgate, federation and manifest changes
The release gate runs cargo fmt --check and these were hand-written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 01:00:29 -04:00
archipelagoandClaude Opus 5 9abf9072a3 docs(changelog): curate v1.7.121-alpha release notes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 00:51:46 -04:00
archipelagoandClaude Opus 5 ab2c8b6e96 fix(security): silence is not consent — undeclared ports are never acted on
Two live incidents on archi-dev-box today, one bug. Both times a safety
decision read an ABSENT manifest field as if it were a value, and a
node's installed manifests always lag the binary — so "absent" is the
state of essentially every port on every node.

  1. Gating any `session` port regardless of `bind` published Bitcoin's
     loopback-only RPC 8332 on the LAN, Tailscale and IPv6 within seconds
     of deploy.
  2. The `bind`-keyed replacement looked safe because it protected
     `bind: 127.0.0.1` ports — but LND's gRPC 10009 and REST 18080 carry
     an EMPTY bind, so they fell through. One container recreate from
     pinning them to loopback and breaking Zeus and every remote wallet.

`auth` is now `Option<PortAuth>`, separating two questions that were
conflated:

  * `auth_policy()` — what to CLASSIFY the port as. Undeclared reports as
    Session, i.e. shows in the audit as something that should be behind
    the gate. Reporting is always safe.
  * `auth_is_declared()` — whether the daemon may ACT. Only an explicit
    declaration authorises changing how a port is published.

Also reverts the daemon-side publish rewriting entirely. The node proved
it wrong twice over: the recreate path that actually ran was in
package::install, not podman_client, so the pin never fired; and even
`bind: 127.0.0.1` written directly into the node's manifest was
overridden by the signed catalog. Publishes are built in several places
and all of them already honour `bind`, so the migration belongs in the
catalog as data — not in daemon-side inference that can only ever cover
one path and guess wrong on the rest.

Tests: 75/75 container, incl. the LND wallet-port shape (`host: 10009`,
empty bind, no auth) asserted to be non-actionable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 00:36:43 -04:00
archipelagoandClaude Opus 5 edc9a172e9 fix(mesh): federated peers are messageable without meeting over LoRa first
Peering a node was not enough to message it — you had to be in radio
range once before chat worked, which defeats the point of federating.

`send_message` chose its transport from the attached radio:

    let use_typed_envelope =
        archy && matches!(device_type, Meshcore | Reticulum);

Only the typed path knows about FIPS/Tor. Everything else fell through to
`peer_dest_prefix`, which resolves an over-the-air ROUTING key — so on a
node running Meshtastic, or with no radio at all, sending to a federated
peer failed. It only worked once a LoRa advert had created a radio twin
for the same archipelago identity, which is precisely the "connect on
LoRa first" the operator hit.

Federation contacts are reachable off-radio by definition — that is what
`upsert_federation_peer` records with `reachable: true` — so the
transport choice must not depend on which radio is plugged in. A
federation-synthetic contact id now always takes the typed path.

This loses no radio-first behaviour: `send_typed_wire` already prefers a
REACHABLE radio twin when the payload fits the frame, and only then falls
back to FIPS and Tor. The fix routes federation contacts INTO that logic
rather than around it.

Test pins the predicate across every device type, including the two that
failed (Meshtastic, Unknown), and asserts ordinary radio contacts and
stock clients still route exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 00:35:02 -04:00
archipelagoandClaude Opus 5 61ebce0598 docs(phase-13): 13-06 complete
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:56:42 -04:00
archipelagoandClaude Opus 5 03ceedbae1 docs(13-11): require content to actually render, not merely be fetchable
13-06 delivered the content pipeline and unit-tested it, but nothing in the
live UI invokes it, and 13-11 as written only added an equally-uncalled
sibling. No plan in the phase triggers the fetch from a UI event. Without this
AIUI-03 ships green-tested and visibly broken — empty grids. Wiring belongs
here, where useArchy.ts and ChatPage.vue's render tree are already in scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:56:25 -04:00
archipelagoandClaude Opus 5 fe54f95d4e docs(13-06): complete AIUI content-surfaces plan
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:54:14 -04:00
archipelagoandClaude Opus 5 b771357902 feat(13-06): AIUI renders Archy content through setArchyContent (D-12)
- archyBridge.ts: content:push case resolving the pending content:request
  by id, and requestArchyContent(kind, scope) mirroring requestContext's
  shape. Not in the plan's files_modified list, but required to satisfy
  Task 3's own instruction to register the content:push handler on the
  existing single bridge listener rather than adding a second
  window.addEventListener('message') — see SUMMARY deviations.
- useContentPanel.ts: setArchyContent + archyContentActive; guards only
  the panelFilms/panelSongs/panelPodcasts assignments inside
  updatePanelFromText so Archy-sourced grids stay the source of truth
  once populated, per plan scope. Books/TV/images/places/magazine/code/
  recipes/news are untouched (13-PATTERNS.md: partial deprecation).
- useArchy.ts: requestArchyContent(kind, scope) calling
  archyBridge.requestArchyContent then useContentPanel().setArchyContent.
  No FilmGrid/SongGrid/NewsGrid/ContentGridView/content.ts edits (D-12).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:47:51 -04:00