Commit Graph
2489 Commits
Author SHA1 Message Date
archipelagoandClaude Opus 5 5bd39eedad merge(13-01): AIUI tracer slice — assistant spine + chat over the origin-checked bridge
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:55:09 -04:00
archipelagoandClaude Opus 5 e169beb6f7 docs(13-01): record self-check results in SUMMARY
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:52:28 -04:00
archipelagoandClaude Opus 5 6efe42d3ae docs(13-01): complete AIUI tracer-slice plan — SUMMARY + defect ledger
Records the continuation ground-truth review of WIP checkpoint 6ba52b22,
the atomic per-task re-commit (fe6ccff7 Rust spine, 0ab9bdc7 neode-ui
broker), and the external-repo Task 3 commit (AIUI e30ac1d, not yet
pushed). Logs two open WINDOWS.md items: the cargo test run that never
completed under machine resource contention (id 16), and the AIUI push
blocked by an unreachable git.tx1138.com (id 17).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:52:02 -04:00
archipelagoandClaude Opus 5 b8634baf94 chore(13): execute plans sequentially — this box thrashes on parallel Rust builds
4 cores, load 35, 15G of 23G swap in use, rustc at 8.3G RSS while a live
node (bitcoind/electrumx/lnd) shares the machine. Two concurrent cargo
builds in separate worktrees (no shared target dir) made wave 1 crawl for
over an hour with zero commits. Wave 2 has four plans, so this would have
gotten worse before it got better.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:37:43 -04:00
archipelagoandClaude Opus 5 0ab9bdc7d3 feat(13-01): neode-ui carries chat over the existing origin-checked bridge
Adds chat:request/chat:response to the AIUI postMessage protocol
(AIUIChatRequest, ArchyChatResponse) and a handleChatRequest handler in
contextBroker.ts that calls assistant.chat over rpcClient on the page's own
session, then posts the result back through the existing postToIframe
helper. Reuses the broker's existing allowedOrigin guard unchanged — no
second postMessage channel, no relaxed origin check.

No permission category is threaded through the chat handler on purpose:
authority is resolved node-side from CallerScope (Task 1), and duplicating
a browser-side gate here would recreate the second, divergent security
model D-02 exists to prevent. tool-call is deliberately NOT added to
AIActionType — tool selection stays node-side by D-01/D-03.

On RPC failure the handler posts only the error message, never the raw
exception object.

Verified: contextBroker.test.ts (16/16) and chatAiuiEmbed.test.ts (7/7)
green; vue-tsc --noEmit clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:37:41 -04:00
archipelagoandClaude Opus 5 fe6ccff73c feat(13-01): Rust assistant spine — one curated tool, one backend, one RPC surface
D-01/D-02/D-06 tracer slice: a new crate::assistant module (CallerScope,
PermissionCategory, ToolExecCtx, chat()) runs a multi-turn tool-calling loop
(run_loop/execute_tool, MAX_TURNS=8) against a curated single-tool registry
(system_disk_status, hand-written JSON Schema — no schemars) via a Claude
Messages API backend. execute_tool is the single choke point: unknown tools
are refused not ignored, D-16 category grants are re-checked even though the
system prompt already omits ungranted tools, and every real tool dispatches
through the SAME handle_system_disk_status RPC handler every other
authenticated caller uses (assistant_dispatch_tool bridge in
api/rpc/assistant_chat.rs) — never an AI-only backdoor.

assistant.chat is registered in dispatcher.rs as a single guarded
`m if m.starts_with("assistant.")` arm reached only after the existing
session-cookie + CSRF + role.can_access() gate in api/rpc/mod.rs — asserted
directly by assistant_methods_require_session against the live
UNAUTHENTICATED_METHODS list (visibility only widened to pub(crate) for that
assertion; the list's contents are untouched, per the Phase-10 hard
constraint).

Key read from data_dir/secrets/claude-api-key — the same path
mesh/rpc/mesh/assistant.rs already probes — never a second key location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:37:16 -04:00
archipelagoandClaude Opus 5 7ad0cdd195 docs(13): resume phase 13 execution after broken-pipe interruption
Wave 1 recovery: 13-03 was complete and is merged into the lane; 13-01 and
13-02 had uncommitted executor work rescued into WIP checkpoints and are
being continued in their existing worktrees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 13:06:05 -04:00
archipelagoandClaude Opus 5 29b9c1e280 docs(13-03): record summary self-check result
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:41:00 -04:00
archipelagoandClaude Opus 5 bbd5be9dfc docs(13-03): add plan summary
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:40:03 -04:00
archipelagoandClaude Opus 5 f8987d12f3 docs(13): routstr protocol findings + coverage matrix from live probe
13-ROUTSTR-FINDINGS.md records the routstr_probe run against all three
docs.routstr.com default relays: zero kind-38421 events and zero
#d=routstr-provider fallback events in a 30s window each, with all
three relay connections succeeding (ruling out a connectivity
failure as the explanation). Every claim is labelled OBSERVED or
DOCS-ONLY per the plan's scope note; RESEARCH assumption A2 is
recorded as neither confirmed nor refuted, risk unchanged.

COVERAGE.md's three former "INTEGRATE — UNCONFIRMED" rows (tool
calling, Cashu payment header, Nostr provider discovery) are
downgraded to explicit opt-outs with dated, evidenced reasons — zero
rows retain unconfirmed-integration status. The Gate section states
13-13 may not proceed directly and must open with a checkpoint:
decision, which 13-13-PLAN.md's Task 1 already is (proceed-observed /
proceed-docs-with-probe-first / defer-with-residual) — no edit to
13-13-PLAN.md was needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:34:05 -04:00
archipelagoandClaude Opus 5 ea90ef05a5 feat(13): live Routstr Nostr probe — no shipped code path
examples/routstr_probe.rs subscribes to the docs-cited default relays
(damus.io, nostr.band, nos.lol) for kind-38421 provider announcements
plus a #d=routstr-provider fallback filter in case the kind number
drifted, then issues at most two unauthenticated GETs against any
discovered endpoint. Spends nothing: no Cashu token is ever built or
sent, no Authorization header, no Nostr event published, ephemeral
subscription key.

Reproduces (does not import) nostr_discovery.rs::build_nostr_client's
Tor-proxy-aware client shape, since this package ships no [lib]
target and an examples/ binary cannot reach binary-crate internals.

Live run against the three default relays (60s total wait budget)
found zero matching events under either filter — recorded honestly
as NO LIVE PROVIDER OBSERVED, exit 0, per the plan's "no provider
found is a first-class outcome" requirement. Full output feeds
13-ROUTSTR-FINDINGS.md in the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:09:38 -04:00
archipelago 15774d266f docs(13): begin phase 13 execution on isolated lane 2026-08-03 11:29:25 -04:00
archipelago f0b71f86aa docs(13): declare AIUI-01..06 in the canonical ROADMAP requirements line 2026-08-03 11:19:26 -04:00
archipelago 223afc26f7 docs(reqs): register AIUI-01..06 for phase 13 traceability 2026-08-03 11:18:33 -04:00
archipelago eb224709d7 docs(13): create phase plan — 15 plans in 8 waves 2026-08-03 11:16:09 -04:00
archipelagoandClaude Opus 5 60625499ae fix(13): make D-13 track independence real in the wave graph
Plan-checker revision iteration 1 — 1 blocker + 2 warnings.

BLOCKER (context_compliance, D-13): the music track blocked phase
completion despite being locked as non-blocking. 13-15 depended on
13-11, which chains back through 13-07 to 13-04, so the phase could
not close without the entire music chain. Took the checker's option
(b): 13-15 depends_on is now ["13-06","13-09","13-14"] — 13-06 added
so the content-grid check stays a real gate, 13-11 dropped so no path
reaches 13-04/13-07/13-11. UAT step 7 is now content-only and blocking;
new step 7b is the music view as record-and-defer, the same shape step
10 already used for Routstr. Verified: 13-15's transitive closure
contains no music plan.

WARNING (scope_reduction): T-13-32 claimed the filebrowser-client.ts
JWT-in-query-string leak was "fixed" while only guaranteeing it was not
propagated. Now actually fixed — streamUrl returns a query-free
same-origin URL and relies on the path=/ cookie login() already sets;
filebrowser-client.ts and a new regression test are in 13-06's
files_modified. T-13-32 is scoped to new code; new T-13-39 owns the
pre-existing leak and names the residual (the JWT is still 24h, now
confined to the cookie jar).

WARNING (verification_derivation): the edge-probe reconciliation did
not match the files. Corrected in 13-VALIDATION.md — 10 probe findings
vs 9 edge entries kept apart, 13-07's 3 truths retagged as authored
rather than probe-surfaced. No truths deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:12:06 -04:00
archipelagoandClaude Opus 5 203966030b docs(13): create phase plan — 15 plans in 8 waves
AIUI conversational node control & content surfaces, decomposed tracer-first:
13-01 leads with one end-to-end read-only tool proving the whole spine
(AIUI chat -> postMessage -> authenticated RPC -> Rust agent loop -> real
node data), then expands.

Waves 1-8 across three tracks that stay independent per D-13:
- control/assistant: 13-01, 13-05, 13-08, 13-10, 13-12, 13-13, 13-14
- content: 13-06
- music library: 13-04, 13-07, 13-11 (no control/content plan depends on it)
- security & delivery: 13-02, 13-03, 13-09
- on-device sign-off: 13-15

Notable decisions recorded in the plans:
- Open Q1: delete-and-replace the live unauthenticated port-3142 Claude proxy
  with a session-gated Rust forwarder; the OpenRouter open relay is removed.
- Open Q2: /aiui/-scoped CSP connect-src plus a per-session rate limit;
  the iframe sandbox attribute is explicitly rejected with reasons.
- Open Q3: a live Routstr spike (13-03) gates the Routstr backend (13-13).
- Open Q4: one "assistant." dispatcher prefix arm, so the existing
  session/CSRF/RBAC gate applies unchanged before dispatch.
- Promote (not add-alongside) CallerScope as the primary caller/permission
  noun; the mesh-specific controls become one variant's resolution inputs.
- schemars rejected as an unaudited crate; JSON Schema is hand-written.
- AI-SPEC's `cargo test --test assistant_evals` corrected to an in-crate
  module: core/archipelago is a binary-only crate with no lib target.

Also adds COVERAGE.md (Routstr capability matrix, every opt-out reasoned).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 10:42:25 -04:00
archipelagoandClaude Opus 5 e00c73ed2d docs(1.7.121): pause point — task list, status and resume notes
v1.7.120-alpha is shipped and verified; do not re-cut it. Two fixes landed
after it: federation trust escalation (c0cfc72a) and the lnd-ui OTA pin +
host networking (5088aef5).

The task file now carries a RESUME HERE block with the groundwork already
located for the next item (the password gate on granting Trusted) — the
exact helper, both entry points with line numbers, and the rule that
demotion stays ungated — so the next session does not repeat the search.

Paused here deliberately rather than starting the app-port auth work at
low context: it is the largest item, the operator asked for umbrelOS and
StartOS research first, and it is the same bug class as the leaks fixed
in v1.7.120 but across every app port and transport.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 10:34:36 -04:00
archipelagoandClaude Opus 5 5088aef556 fix(lnd-ui): pin the image and host-network it so OTA actually updates it
Reported: Framework PT took the OTA and got the new bitcoin-ui but not
lnd-ui. Two causes, both in the update path rather than the app.

1. LND_UI_IMAGE was "lnd-ui:latest" while BITCOIN_UI_IMAGE was pinned to
   1.7.119-alpha. Podman does not re-pull a tag it already holds locally,
   so a node that ever pulled lnd-ui:latest keeps that copy forever and
   every subsequent release silently no-ops. Pinned to 1.7.119-alpha, so
   a version change is what triggers the pull — the same mechanism that
   made bitcoin-ui update correctly.

2. first-boot-containers.sh declared lnd-ui as bridge with -p 18083:80.
   docker/lnd-ui/nginx.conf listens on 18083 DIRECTLY (it must, to proxy
   the backend on 127.0.0.1:5678 same-origin), so that maps a host port
   onto a container port nothing serves — reproduced on-node as HTTP 000.
   This is the THIRD copy of the same declaration: container-specs.sh and
   apps/lnd-ui/manifest.yml were both already corrected, this one was
   missed, and it is the copy fresh installs use. Now host-networked with
   no published ports, matching its siblings and the other two copies.

The underlying hazard is that one container spec lives in three files
that can disagree; recorded as a follow-up rather than refactored here.

Also opens .planning/RELEASE-1.7.121-TASKS.md — every outstanding item
for the next release with its evidence, so nothing in a fast-moving queue
gets lost between sessions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 10:31:39 -04:00
archipelagoandClaude Opus 5 c0cfc72a05 fix(security): peers must not be able to grant themselves Trusted
Reported: "peers seem to be slipping into trusted status somehow which is
absolutely terrible for security". Two independent fail-open paths, both
granting Trusted with no operator decision anywhere in the loop.

1. federation.peer-joined is UNAUTHENTICATED (middleware's no-session
   list — federated peers call it over Tor without cookies) and reachable
   on /rpc/v1, which is peer-allowed. It does verify an ed25519 signature,
   but against THE PUBKEY THE CALLER SUPPLIED, so it proves the caller
   holds its own key and nothing about whether we ever invited it. A join
   presenting no invite_token fell through to

       None => TrustLevel::Trusted.min(claimed_trust)

   and claimed_trust itself defaults to Trusted when the field is absent.
   So anything able to reach the node could generate a keypair, omit the
   token, and be recorded as Trusted. Now capped at Observer: an invite
   WE minted is the only path to Trusted. `min` is kept so a peer's own
   lower claim is still honoured — this can only ever reduce trust.

2. merge_transitive_peers added every peer advertised by a Trusted source
   as Trusted. That makes trust viral rather than transitive-by-one-hop:
   the merged node is itself synced with, its peers merged in turn, so a
   single invite anywhere in the graph eventually marked the entire graph
   Trusted on every node. Now Observer — which is what this feature's own
   spec always said. NodeStateSnapshot.federated_peers is documented as
   "adds them as Observers on her side… doesn't auto-promote Observer-via-
   Bob to Trusted". The code contradicted the comment directly above it.

Observer is deliberate rather than Untrusted: the merge exists for
routing, and Observer still passes the `!= Untrusted` gates that
federation, DWN and messaging actually check, so a legacy peer degrades
instead of breaking. Per the operator's decision, existing peers are NOT
auto-demoted — silently rewriting live trust relationships across the
fleet would be worse than the bug.

Instead they are made auditable: FederatedNode.trust_source records WHY a
level was granted (invite | uninvited-join | transitive-merge | manual).
It deliberately has no default provenance — None means "recorded before
this existed", which is exactly the population worth reviewing.

The one failing test was asserting the vulnerable behaviour
(merge_transitive_peers_skips_source_and_local_node expected Trusted); it
now asserts the security property and says why, so the escalation cannot
be reintroduced by making a test go green.

Verified: 42/42 federation tests, cargo check --all-targets clean.

Still open, tracked in .planning/RELEASE-1.7.121-TASKS.md: surface
trust_source in the UI, and require the node password to grant Trusted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 10:31:16 -04:00
archipelago ff2cb5aa0e docs(13): add pattern map 2026-08-03 09:54:47 -04:00
archipelago 72b07fbe9c docs(13): generate AI-SPEC.md — hand-written Rust agent loop + domain context + eval strategy 2026-08-03 09:47:55 -04:00
archipelago d3ee5486ab docs(13): add validation strategy 2026-08-03 09:23:10 -04:00
archipelagoandClaude Opus 5 7134ae903d docs(13): research phase domain — AIUI conversational control
Verifies the AIUI-01 gating question against source (no tool-calling
anywhere in this codebase today; Pine's HA intents are read-only Q&A,
not an action-executing loop), surfaces a live unauthenticated
Claude-proxy exposure (port 3142) and a same-origin iframe sandbox
gap not previously named, and maps existing Cashu/Nostr primitives
onto the Routstr integration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:20:09 -04:00
archipelagoandClaude Opus 5 3b8ac7cb1c docs(state): v1.7.120-alpha shipped and verified live
Records the two release-process traps for the next cut: the manifest is
committed before signing (so the fleet would refuse the OTA), and
gitea-vps2 is the same server as gitea-ai with a dead token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 08:58:43 -04:00
archipelago 7f9dd2172d docs(state): record phase 13 context session 2026-08-03 08:57:51 -04:00
archipelago 48c7f5d02f docs(13): capture phase context 2026-08-03 08:57:42 -04:00
archipelagoandClaude Opus 5 4a5588c59a chore(release): commit the signed v1.7.120-alpha manifest
create-release.sh builds and commits the manifest BEFORE the signing
step, so the release commit carried an UNSIGNED manifest. Nodes fetch
releases/manifest.json from branch main and refuse to auto-apply an
unsigned one, so publishing without this would have shipped an OTA the
fleet silently declines.

Signature verified against the pinned release root before committing:
  signed_by did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur

Cargo.lock carries the 1.7.120-alpha version bump from the release build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v1.7.120-alpha
2026-08-03 08:46:01 -04:00
archipelago 9de0a17670 chore: release v1.7.120-alpha
Demo images / Build & push demo images (push) Successful in 3m30s
2026-08-03 08:37:25 -04:00
archipelagoandClaude Opus 5 0fec507af3 docs(roadmap): add Phase 13 — AIUI conversational node control and content surfaces
AIUI is embedded and styled but not functional: the chat cannot act on the
node and its content views are not wired to real data. Phase 13 scopes making
it work — Pine's human-language intent->action capability reachable from typed
chat, conversational settings, and the peer-files/music/movies/node-content
surfaces rendered live.

The gating requirement is AIUI-04: a user-granted capability sandbox. An LLM in
the browser is now adjacent to wallet keys, macaroons and node identity, so
secrets stay server-side behind scoped tokens, capability grants default closed
and stay revocable, destructive operations need a human confirmation, and
peer-supplied text is treated as untrusted input to the model context. This must
not widen the Phase 10 hard-refuse gates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 08:34:18 -04:00
archipelagoandClaude Opus 5 6a617f3a6c docs(whats-new): add the v1.7.120-alpha block to the in-app modal
Demo images / Build & push demo images (push) Successful in 3m45s
Generated by scripts/sync-whats-new.py, which the release gate checks.
Without it the Settings > What's New modal would have skipped straight
from v1.7.119 to v1.7.121 — the release notes users actually read, as
opposed to CHANGELOG.md which they do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 01:36:37 -04:00
archipelagoandClaude Opus 5 b15b160294 style: rustfmt the code added in 01-04 and the reconcile fix
The release gate's cargo-fmt stage failed on my own additions — the
tests in message_types.rs and lnd/info.rs and the reconcile branch in
prod_orchestrator.rs were written programmatically and never passed
through rustfmt. Formatting only; rustfmt is semantics-preserving and
the gate re-runs the suites before building.

Caught by the gate rather than in review, which is the gate working. Also
a reminder that a piped command's exit code is the pipe's, not the
script's: the task notification reported success while the log said
CREATE_RELEASE_EXIT=1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 01:34:23 -04:00
archipelagoandClaude Opus 5 41de23e71f docs(requirements): mark UIFIX-04 and UIFIX-06 complete
Bookkeeping left uncommitted by an earlier session. It records work that
is already shipped — bc9a210c routed Paid Files pictures/videos into the
app lightbox with a visible wait — so the checklist and the mapping table
were simply lagging the code.

Landed as its own commit rather than swept into the release: it is not my
edit, and the release script refuses to run with a dirty tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 01:32:17 -04:00
archipelagoandClaude Opus 5 57891099a4 docs(changelog): record the stuck-nav fix for v1.7.120-alpha
States plainly that the speed is unchanged — the fix gates the teleported
chrome, not the KeepAlive caching that made tab switching instant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 01:28:26 -04:00
archipelagoandClaude Opus 5 a5b923fa0b fix(ui): teleported nav must not outlive the screen that raised it
Demo images / Build & push demo images (push) Successful in 3m30s
Reported: the nav above the bottom bar — back buttons, the mesh tabs —
stayed stuck across other screens.

Cause is the KeepAlive work from phase 2, and specifically the half of it
that is invisible from the view's own file. Main tabs are KeepAlive'd, so
navigating DEACTIVATES a view instead of unmounting it. Content the view
Teleports to <body> is not in the view's DOM subtree, so deactivation
does not remove it and it keeps rendering over the destination screen.

Two offenders, matching the report exactly:

- Mesh.vue teleports its mobile TAB BAR and its chat BACK BUTTON to
  <body>, gated only on `mobileShowChat` — never on whether Mesh was the
  screen you were looking at.
- components/BackButton.vue teleports the shared mobile back button with
  NO gate at all, so it leaked out of every view that uses it. Fixing the
  shared component fixes every caller at once: Vue propagates
  activated/deactivated from the KeepAlive boundary down through the
  subtree, so a child can guard itself.

BaseModal already solved the transient-dialog half of this class in
204d4523 by closing on route change. That is the right fix for a dialog
and the wrong one for chrome: a tab bar has no "closed" state to fall
back to, and forcing one would lose the user's place. New
useViewActive() composable instead — chrome is simply not rendered while
its owner is off screen, and returns exactly as it was.

THE PERFORMANCE IS NOT SACRIFICED, which was the explicit constraint.
The Teleport is gated, not the view, so the instance stays cached and
revisiting a tab is still instant. A test pins this: setup() must run
exactly ONCE across a navigate-away-and-back round trip. If someone
later "fixes" this by dropping KeepAlive, that test fails.

Deliberately untouched: AppSession.vue, whose teleport is load-bearing —
its own comment records that moving the iframe node reloads the app, and
app-session is excluded from KeepAlive anyway so it cannot leak. Toasts,
the app launcher and the connection banner are app-level rather than
view-owned; gating those would be wrong.

Verified: 3 new tests; full suite 105 files / 848 tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 00:55:42 -04:00
archipelagoandClaude Opus 5 b945738d62 docs(state): record v1.7.120-alpha staging and its on-node verification
Includes what was NOT verified — the torrc block is deployed but dormant,
since regenerate_torrc only fires on a Tor services change and the change
is inert until bitcoind gets an -onion flag in Phase 12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 00:18:47 -04:00
archipelagoandClaude Opus 5 4d67f56bc4 docs(changelog): curated notes for v1.7.120-alpha
Leads with the reason to take the update: two ports handed anyone who
could reach them full control of the node's money. Written for an
operator, not a developer — what was exposed, who could reach it, and
what to treat as compromised.

Includes the gaps rather than burying them: the 5x lifecycle gate was not
run, two fleet nodes still share SSH host keys (rotation is a deliberate
operator decision, not an oversight), and Core can now reach Tor but is
not yet routed through it.

create-release.sh hard-fails without this section, so it lands before the
release run rather than during it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:58:35 -04:00
archipelagoandClaude Opus 5 8ee81e04c9 docs(roadmap): add Phase 12 — Bitcoin node settings, Core/Knots parity
Per the operator: every option umbrelOS surfaces must be reachable in the
UI, Knots-only options surfaced separately from the ones Core shares, and
network mode a setting whose DEFAULT is Tor rather than clearnet.

Scoped as a phase rather than done inline because bitcoind's arguments are
currently hardcoded in three places (first-boot-containers.sh,
container-specs.sh, apps/bitcoin-knots/manifest.yml) — the same
triplication that produced the lnd-ui HTTP 000 defect. There is nowhere
for a UI to write, so BTCSET-01 is a settings model those three render
FROM, not another restatement.

Two constraints recorded up front so they are not discovered late:

- Knots-only flags gated to Knots is a CORRECTNESS requirement — offering
  one on Core yields a node that refuses to start.
- Several options are not freely reversible: txindex forces a reindex,
  prune is destructive and needs a full resync to undo. On a node that is
  somebody's wallet backend those must be labelled and gated, not
  silently applied. Any change at all restarts bitcoind, interrupting
  LND, electrs and the fedimint gateways.

Inbound onion is explicitly out of scope: it needs Tor's ControlPort,
which is deliberately disabled for security, so the node reaches .onion
peers but stays unlisted. The UI must say so rather than imply otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:50:37 -04:00
archipelagoandClaude Opus 5 f04941934b feat(tor): give archy-net containers a SOCKS path so Core can use Tor
Enabling half of "Bitcoin Core has no Tor proxy at all", handed over from
the app-UI work. Core reported `onion reachable=False, proxy=''` with all
11 peers on clearnet, and the reason was not a missing bitcoind flag: the
container sits on the archy-net bridge (10.89.0.0/24 here), so
127.0.0.1:9050 inside it is its OWN loopback. The host's Tor was
genuinely unreachable, and no flag on bitcoind could have fixed that
alone.

torrc now binds a second SOCKS listener on the archy-net gateway.

The gateway is DERIVED at runtime via `podman network inspect`, never
hardcoded: archy-net is created without an explicit subnet, so podman
allocates one. It is 10.89.0.0/24 on this node with no guarantee of that
elsewhere, and a hardcoded guess would fail silently — binding SOCKS to
an address no container can reach, which looks identical to working.

Two deliberate safety properties:

- FAIL CLOSED. If archy-net is absent or its inspect output does not
  parse, no second listener is emitted and SOCKS stays loopback-only. An
  exposure boundary is not something to widen on a guess.
- 127.0.0.1 is accepted FIRST in the SocksPolicy. SocksPolicy applies to
  every SocksPort, so an accept-list naming only the bridge subnet would
  have locked the daemon out of its own loopback SOCKS — breaking the
  node's Tor usage in a way that looks nothing like "we added a
  listener". The list is accept-loopback, accept-subnet, reject *.

This widens Tor SOCKS from loopback-only to the archy-net subnet, which
is a real change to the node's exposure surface and was explicitly
approved by the operator rather than assumed. Inbound onion for Core
remains impossible without reversing the deliberate "ControlPort disabled
for security" decision — this is outbound only, and the node stays
unlisted on Tor.

Not yet wired: bitcoind still has no -onion flag, because the operator
wants network mode to be a UI setting with Tor rather than clearnet as
the default. Hardcoding the flag in the three places that currently
define bitcoind's arguments would be the wrong shape for that, so it is
deferred to the settings work rather than done twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:49:00 -04:00
archipelagoandClaude Opus 5 fbec70069f fix(nginx): serve AIUI's absolute /assets/ requests from aiui/assets/
Taking over a parked item from the app-UI work. AIUI's built index.html
emits ABSOLUTE /assets/<hashed> paths, so the browser asks for
/assets/index-BC2fBBaW.js. That lands in the MAIN UI's assets dir, where
it does not exist — the real files are in aiui/assets/. Both of AIUI's
two entry assets 404'd, so the embedded sidebar loaded nothing.

The config already contained a /aiui-assets/ location whose comment names
this exact problem ("AIUI may reference /assets/ without /aiui/ prefix"),
but it only catches requests to /aiui-assets/, a path AIUI never asks
for. It described the bug without fixing it.

/assets/ now falls back to a named location that rewrites into
aiui/assets/ and 404s from there. A fallback rather than copying the two
files up one level, because a frontend deploy replaces web-ui wholesale —
update.rs preserves the aiui/ DIRECTORY, not copies made into assets/ —
so a copy is erased by the very next deploy while this survives one.

Both server blocks (HTTP and HTTPS) are patched; named locations are
per-server, so each needs its own.

Verified on archi-dev-box after reload:
  /assets/index-BC2fBBaW.js   200, 305256 bytes, application/javascript
  /assets/index-BJkaQ2c4.css  200, 150716 bytes, text/css
  /assets/does-not-exist.js   404  (the fallback is not over-broad)
  /assets/index--lyLAgu1.js   200  (real main-UI chunks still come from
  /assets/vendor-CmYeCqL_.js  200   the main dir — try_files hits them
  /assets/index-CiMaoNII.css  200   before the fallback is consulted)
  /  /aiui/  /health          200

Hash collision between the two builds is not a concern: Vite hashes are
content-derived, and any main-UI asset that exists is served by try_files
before the fallback runs.

Noted while doing this, not fixed here: the node's own
/etc/nginx/sites-enabled/archipelago is 378 lines BEHIND this repo file
(984 vs 1362) — it predates the IPv6 listener and the @asset_missing
no-store handling, among others. The node was patched minimally in its
own shape rather than overwritten, since a wholesale copy of a config
this diverged is not a safe unattended action.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:40:34 -04:00
archipelagoandClaude Opus 5 c4aece4883 docs(01-04): on-node verification, and the send half is inert here
Deployed 6b3693dc to archi-dev-box off a clean tree and exercised the
real RPC surface. lnd.getinfo returns a real identity_pubkey through the
field that did not exist before this plan. mesh.lightning-peers returns
an empty array with success — the FED-05 empty edge proven on hardware,
not just in a unit test. Both refusal paths of mesh.send-lightning-info
observed live, including T-01-13's "no broadcast form".

The finding worth carrying to 01-06: this node's LND advertises no URI
(uris: []), so send-lightning-info correctly refuses rather than
shipping an empty advertisement a peer would store as an undialable
target. The receive and list halves work; the SEND half is inert on any
node whose LND has no externally reachable address. The picker must not
assume the local node always has something to share.

Corrects this summary's own earlier claim that nothing was exercised on
hardware — that was true when written and is not now. The mesh leg
proper (an advertisement crossing real RF into a peer's lightning_uri)
remains unproven and is still called out as such.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:24:16 -04:00
archipelagoandClaude Opus 5 6b3693dcc8 docs(state): 01-04 complete; Phase 1 resumed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:08:07 -04:00
archipelagoandClaude Opus 5 666990c684 feat(01-04): expose meshed Lightning peers and the send path over RPC (FED-05)
Task 3, completing 01-04.

mesh.lightning-peers returns the peers that have advertised a Lightning
URI: filtered, deduplicated, deterministically ordered, and an empty
array rather than an error when nobody has — "nobody yet" is a normal
state on a fresh node, not a fault.

mesh.send-lightning-info advertises this node's own URI to ONE chosen
peer. There is deliberately no broadcast form: this discloses the node's
payment endpoint, and who learns it is the operator's choice rather than
a side effect of being in radio range (T-01-13). It refuses to send when
LND advertises no URI, instead of sending an empty one a peer would
store as an undialable target.

The list-building and target-parsing logic is extracted into pure
functions because this file has no handler test harness and the
handlers need a live mesh service. That keeps the three contracts that
actually matter provable rather than merely readable:

- dedup is keyed on identity_pubkey_hex() — the AUTHENTICATING key,
  lowercased — never the firmware routing key, so a radio contact and
  its federation twin collapse to one entry (T-01-11)
- "newest advertisement wins" compares PARSED RFC3339 timestamps, not
  strings: 09:30-01:00 is later than 10:00Z while sorting earlier as
  text, and there is a test that fails if that is ever string-compared
- ordering is name-then-contact_id and asserted byte-identical across
  eight rotations of the input, because a HashMap's iteration order is
  not stable and a picker that reshuffles between reads means an
  operator can click a different node than the one they aimed at

The peer allow-list is untouched: server.rs has an empty diff and
is_peer_allowed_path still occurs 13 times (T-01-15).

Verified: cargo test -p archipelago 1087 passed / 0 failed; clippy
--all-targets clean in every touched module (two useless_format lints in
the new test code fixed, not waived).

The SUMMARY records one deviation honestly: Task 1's tests were written
alongside its implementation rather than before, so no pre-implementation
failing output exists. A mutation test was run in its place — disabling
the pubkey validation fails 3 of the 5 tests — which proves the
assertions bind, and the mutation was reverted and verified gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:48:02 -04:00
archipelagoandClaude Opus 5 decb7c713b feat(01-04): the two Lightning facts the channel-open picker needs (FED-05)
Tasks 1 and 2 of 01-04. This node's own shareable URI, and a mesh
message a peer uses to advertise theirs.

lnd.getinfo now deserializes identity_pubkey and uris, which its
response struct simply did not declare before (RESEARCH.md Pitfall 5).
The identity mapping is split into a pure map_identity() so it is
testable without a live LND. A pubkey that is not 66 hex characters maps
to None rather than being forwarded: the same rule lnd.openchannel
enforces, applied where the operator is reading their own node's
identity instead of at the moment they try to open a channel. An absent
field yields an honest absence — never a fabricated or placeholder
identity.

MeshMessageType::LightningInfo = 26 is additive on a wire format shared
with every fleet node: 26 was unused, so a peer that predates this fails
to decode it rather than mis-decoding it as something else. Its payload
is deliberately two fields — this rides LoRa, where every byte is paid
for on air, and the optional alias is skip_serializing_if so an absent
one costs nothing (asserted, not assumed).

is_valid_lightning_uri() validates before anything is stored, because
this is unauthenticated RF input: 66-hex pubkey, non-empty host, optional
numeric :port, exactly one '@'. It deliberately does NOT resolve or dial
the host — that would turn a received advertisement into an outbound
connection an attacker chose.

Two preservation hazards found while wiring MeshPeer.lightning_uri, both
of which would have silently emptied the picker:

- decode.rs's identity-advert path does a WHOLESALE insert, preserving
  only advert_name and lat/lon by hand. Reticulum re-emits identity
  adverts every announce tick, so a stored URI would have been wiped
  about once a minute. Now preserved, alongside the same guard the name
  and position already had.
- session.rs's refresh_contacts and mod.rs's federation seeding rebuild
  the peer record wholesale too. Neither carries a Lightning datum, so
  both now carry the previous value forward rather than nulling it.

A malformed inbound URI is rejected before the write, leaving any
previously stored good URI intact — otherwise anyone in range could
blank out a real peer's picker entry (T-01-12).

Verified: 5/5 new lnd::info tests, 18/18 mesh::message_types (5 new),
cargo check --all-targets clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 20:42:45 -04:00
archipelagoandClaude Opus 5 e205f2c34a docs(security): prove the delivery path on hardware; close window 15
Controlled test on archi-dev-box with operator approval. The daemon was
stopped first so the reconciler could not repair the state before the
re-exposure was confirmed — without a confirmed 200, the later 401 would
be consistent with the state never having been broken at all.

  1. stale conf installed + container restarted -> POST /bitcoin-rpc/
     returned 200 with a real block height and Allow-Origin: *
  2. daemon started 20:00:36, nothing else touched
  3. 20:02:19 reconcile rendered the conf and logged the expected warn
     line naming bitcoin-ui/archy-bitcoin-ui, then restarted it
  4. POST -> 401, Allow-Origin origin-scoped
  5. conf byte-identical to the pre-test known-good, container healthy

Both halves are now proven on real hardware: a05956c4's template (the
gate works) and f6b5245b's delivery path (the gate reaches a container
the reconciler had been skipping).

Also records the operator's decision AGAINST credential rotation — no
macaroon, no Bitcoin RPC password — with the trade it accepts stated
plainly, so it is not silently re-litigated later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 20:09:07 -04:00
archipelagoandClaude Opus 5 186a2c36c8 fix(lnd-ui): manifest declared bridge 18083:80 like the spec did
Second copy of the wiring fixed in aaa89789. apps/lnd-ui/manifest.yml still
declared network_policy: bridge with a 18083 -> 80 port mapping, while
docker/lnd-ui/nginx.conf listens on 18083 directly — it has to, so it can
proxy the backend on 127.0.0.1:5678 same-origin; the cross-origin fallback
is what broke this app on http-only nodes.

Publishing a host port to a container port where nothing listens is exactly
the failure reproduced on archi-dev-box when recreating from the matching
container-specs.sh entry: :18083 refusing connections, HTTP 000. Fixing
only the spec would have left the manifest as a live footgun for any code
path that provisions this app from its manifest instead.

Now host networking with an empty ports list, matching both what actually
runs and apps/bitcoin-ui/manifest.yml, which had it right all along.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 20:09:06 -04:00
archipelagoandClaude Opus 5 b2ed27dcfb fix(bitcoin-ui): send no-cache for index.html; pin the rebuilt image
Two things needed for the new UI to actually reach users.

The rendered nginx.conf served index.html with only ETag/Last-Modified and
no Cache-Control, so browsers applied heuristic caching to it. Confirmed on
archi-dev-box: after rebuilding and recreating the container, :8334 and
/app/bitcoin-ui/ both served the new markup immediately, but the app iframe
in the main UI kept showing the previous UI until a hard refresh.
docker/lnd-ui/nginx.conf has always carried this header, which is why only
bitcoin-ui showed the stale copy. Using "no-cache" (revalidate) rather than
"no-store" keeps the ETag doing its job when nothing has changed.

Validated by mounting the rendered config into a throwaway container from
the built image and running nginx -t. (An earlier attempt to test it inside
the running container was meaningless — conf.d/default.conf is a read-only
bind mount, so the copy failed and nginx -t just re-checked the original.)
The 8 container::bitcoin_ui tests still pass; their assertions cover the
placeholder, the 8332 proxy_pass and the listen directive, none of which
this touches.

BITCOIN_UI_IMAGE was still pinned to 1.7.84-alpha, so a fresh install would
pull a bitcoin-ui from many releases ago regardless of what the OTA ships —
first-boot-containers.sh tries the registry image before building from
source. Bumped to 1.7.119-alpha, matching the current release, and the
image is pushed under that tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:56:52 -04:00
archipelagoandClaude Opus 5 aaa89789d2 fix(lnd-ui,bitcoin-ui): OTA-breaking lnd-ui spec, 404 channels link, iframe copy, node URI
All four found by verifying on archi-dev-box rather than assuming.

container-specs.sh: archy-lnd-ui was specified as a BRIDGE container with
SPEC_PORTS="18083:80", but docker/lnd-ui/nginx.conf listens on 18083
directly (it must, to proxy the backend on 127.0.0.1:5678 same-origin).
Recreating from that spec publishes host 18083 to container port 80, where
nothing listens. Reproduced on the node: the app came back with :18083
refusing connections, HTTP 000. This never fired before because the running
containers are created by first-boot-containers.sh, which is host-networked
and never reads this file; the spec is only consulted when self-update.sh
rebuilds a UI image, and that only happens when a file under docker/lnd-ui/
changes — which is exactly what the previous two commits did. So the next
OTA would have taken lnd-ui down on every node. Now SPEC_NETWORK="host"
with no port mapping, matching what actually runs. NET_BIND_SERVICE dropped
with it: 18083 is unprivileged.

lnd-ui channels link: pointed at /apps/lnd/channels, but that route is a
CHILD of the /dashboard record in neode-ui's router, so the real path is
/dashboard/apps/lnd/channels. nginx's SPA fallback returns 200 for the
wrong path, so it failed as vue-router's NotFound view rather than an HTTP
404 — both the Payment Channels card and the Manage Channels button.

Both apps, copy buttons: navigator.clipboard only exists in a secure
context, and nodes serve these apps over plain http; the main UI also
embeds them in an iframe, where the async Clipboard API is separately gated
by the clipboard-write permission policy. Every copy button silently did
nothing there. Added an execCommand('copy') fallback behind a copyText()
helper and routed all six call sites through it.

lnd-ui Node ID: showed the bare pubkey whenever getinfo.uris was empty,
which is the common case — LND only populates uris once it is advertising
an external address. The bare pubkey is not what a peer pastes to open a
channel. The full pubkey@host:9735 URI is now built from the Tor onion
where available, falling back to this node's address, with a hint saying
which and what its reachability is. The QR encodes the URI too.

Verified on archi-dev-box: both images rebuilt and containers recreated
from the specs; lnd-ui and bitcoin-ui both serve 200 with the new assets;
and the RPCs the new tabs depend on all answer on the live node —
getblockstats returns every field the charts read, getpeerinfo returns 11
peers carrying relaytxes and network values the classifier handles.

Note for whoever tests bitcoin-ui's Insights/Peers tabs: /bitcoin-rpc/ now
sits behind auth_request /_session_check (a05956c4 et al), so it answers
401 to an unauthenticated curl by design. A logged-in browser sends the
session cookie same-origin, which is how those tabs get their data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:33:21 -04:00
archipelagoandClaude Opus 5 5c9d5dc424 docs(state): record the on-node verification outcome and what stayed open
Names the four open items explicitly, including the one that is easy to
lose: the reconcile fix is deployed but unexercised, so the node's 401
proves the template and not the delivery path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:22:49 -04:00
archipelagoandClaude Opus 5 5a21b58f69 docs(security): record what actually closed :8334, and what it does not prove
The node is closed and verified 401 with origin-scoped CORS. But an
unrelated bitcoin-ui rebuild at 18:36 cleared the stale conf before the
reconcile fix was deployed at 19:06, so the 401 proves a05956c4's
template and NOT the delivery path f6b5245b adds.

Window 14 closed (exposure gone, verified). Window 15 opened for the
delivery path, which is deployed but never exercised — bitcoin-ui is
still in the uninstall marker, so this node depends on that untested
path the next time its config has to change.

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