Commit Graph
2907 Commits
Author SHA1 Message Date
archipelagoandClaude Opus 5 1dfd9e720b fix(tls): leaf key must be readable by the daemon, not just root
Found on archi-dev-box the moment the gate tried to serve TLS: the key was
installed root:root 0600, nginx's master reads it as root, but the archipelago
daemon runs as User=archipelago and got "Permission denied (os error 13)".

Every app port then quietly stayed plain HTTP — the exact fail-open shape the
gate exists to prevent, and it would have looked like "TLS just doesn't work"
with no obvious cause. The warn-level log the tls module deliberately emits for
a present-but-unloadable certificate is what turned this into a ten-second
diagnosis instead of a hunt; it earned its keep on its first real deployment.

Key is now group-owned by the service user at 0640, with a fallback to the
user's primary group and a clear message when no such user exists. Nothing
wider than that.

Verified on the node afterwards, on one gated port (8096):
  https 401 verify=0   TLS terminated, chain valid against the node CA
  http  401            same port, plain HTTP, unchanged
  no CA verify=20      untrusted client correctly rejected
The reissued key was also picked up with NO daemon restart — the mtime reload
path proven in production, not just in a unit test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 16:25:03 -04:00
archipelago f7bde19860 Merge branch 'main' into gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf
# Conflicts:
#	.planning/config.json
2026-08-06 16:02:47 -04:00
archipelagoandClaude Opus 5 2ecd5ef8c0 docs(13): content-grid defect fixed + the peer/owned gap that blocks check 2
Records why 13-15's check 2 cannot pass as written (peers/owned scopes have no
caller) and the merge design settled before stopping, so the next session does
not rediscover that setArchyContent replaces rather than merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:46:42 -04:00
archipelagoandClaude Opus 5 aac81503c3 fix(13-11): content grid dropped everything except music
The AIUI-03 stale-response guard used ONE counter for every content:request,
so requests for different kinds cancelled each other.

useArchy.ts init fires content('all','own') and library('own') back to back.
Both sequence numbers are assigned synchronously, before either awaits, so the
first request always resolved with a stale number and was silently discarded.
Films, podcasts and this node's own files never reached the grid no matter what
the user did — only music ever arrived. Nothing logged, because discarding is
the guard working as written.

The guard is now keyed by kind+scope. Different kinds populate different grids
and cannot stale each other by definition; only a newer request for the same
grid can, which is what the guard was actually for. The existing out-of-order
test (same kind twice) is untouched and still passes.

This is the same shape as the defect 13-11 already fixed once: machinery built
and unit-tested end to end, while nothing real ever reached the UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:45:27 -04:00
archipelagoandClaude Opus 5 23173c024a docs(13): record the app-port TLS work and its two remaining gaps
Includes the rustls finding (it does not verify key/certificate pairing) so
the explicit check is not later mistaken for redundant, and the archi-dev-box
caveat: it has no HTTPS dashboard, so it cannot reproduce the iframe failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:25:23 -04:00
archipelagoandClaude Opus 5 7515166a07 feat(appgate): serve HTTPS and HTTP on the same app port
An app port must answer whatever the browser asks for: an HTTP dashboard
embeds http://host:PORT, an HTTPS one embeds https://host:PORT, and an HTTPS
page cannot embed an HTTP frame at all. So the choice is per-node, not
per-fleet, and a second port number would mean every manifest changes and
torrc doubles.

Instead the gate peeks the first byte. A TLS ClientHello is 0x16; no HTTP
method starts with it. peek() leaves the bytes in the socket buffer, so the
acceptor still sees a complete, untouched ClientHello. TLS and plain share one
generic serve_http(), so authentication, proxying and upgrade handling cannot
drift apart by scheme.

EXISTING NODES ARE UNAFFECTED BY CONSTRUCTION. Anything that is not a TLS
handshake takes the identical path as before, and a node with no certificate
serves plain HTTP exactly as today — TLS is strictly additive.

rustls does NOT verify that a private key matches its certificate. Established
by test, not assumed: with_single_cert accepted a pair from two different keys
and would only have failed mid-handshake in a user's browser — a security
control that reports success and does nothing, the exact shape this module's
own docs warn about. So the pairing is now proven explicitly (sign a fixed
message with the key, verify against the certificate's public key) and a
mismatch refuses to serve.

Also: cert and key mtimes are stamped as a PAIR, because reissuing writes them
separately and keying on one would serve a certificate that no longer matches
its key; a 15s first-byte timeout closes the slowloris window one step earlier
than the existing header-read timeout; PKCS#8 and PKCS#1 keys are both
accepted so a hand-made key does not silently downgrade a working node.

Deps pinned to the rustls 0.21 line reqwest already resolves — no new vendor,
no second rustls major. Test fixtures are throwaway (localhost SANs only), not
any node's identity.

38/38 appgate tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:23:47 -04:00
archipelagoandClaude Opus 5 f09ff102ee fix(ui): app frames follow the dashboard's scheme instead of forcing http
Demo images / Build & push demo images (push) Failing after 2m10s
Both schemes now work, and each one works properly:

- HTTP dashboard  -> http app origin  (unchanged; no certificate needed)
- HTTPS dashboard -> https app origin (needs the node CA + TLS on the port)

The app URL was hardcoded to http://, which on an HTTPS dashboard is mixed
content — blocked outright, before the SameSite cookie question the symptom
was filed under. It is also what made the two origins schemefully cross-site,
so following the page's scheme fixes both causes at once.

Backend-reported runtime URLs get the same treatment: the daemon reports
http:// because that is how the app binds locally, which is right for the node
and wrong for a browser on an HTTPS page.

pageScheme() defaults to http when location.protocol is absent (non-browser
contexts) — the safe direction, since inventing an https URL for a port that
serves no TLS would break a working setup. That default is also why the three
existing resolveAppUrl tests, whose fixture stubs location without a protocol,
keep passing unmodified rather than being edited to fit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:43:08 -04:00
archipelagoandClaude Opus 5 71a48e6dbd docs(13): record the per-node CA decision + warm-up fix in the open-tasks file
Corrects the iframe-login root cause on record: trust is per-origin including
port, and a cert interstitial cannot be accepted inside an iframe, so the
SameSite cookie was a downstream symptom rather than the cause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:38:50 -04:00
archipelagoandClaude Opus 5 aab74127f5 feat(tls): per-node certificate authority + Settings install flow
Demo images / Build & push demo images (push) Failing after 2m19s
The node served a bare self-signed leaf, so a browser exception had to be
granted per ORIGIN — scheme + host + port. The dashboard on :443 and an app
on :8334 are different origins, and a certificate interstitial CANNOT be
accepted inside an iframe, so a gated app embedded over HTTPS could never
render no matter how many warnings the user clicked through. (Mixed content
blocks the plain-HTTP variant first, before the SameSite cookie question the
symptom was originally filed under.)

A CA fixes it structurally: ports are not part of a certificate's identity, so
one leaf with the right SANs covers every port on the host, and one installed
CA trusts them all.

- scripts/setup-node-ca.sh generates the CA (4096-bit, pathlen:0, keyCertSign
  only) and issues a 397-day leaf covering archipelago.local, the hostname, the
  Tailscale MagicDNS name and every global address the host holds. Idempotent —
  re-running reuses the CA and only reissues the leaf, so gaining an address
  does not invalidate copies users already installed. --force-ca is the
  deliberate escape hatch and says what it costs.
- nginx serves the public CA at /ca.crt on both schemes, unauthenticated by
  design: a device fetches it before it can validate the node, so gating it
  behind HTTPS or a login would be a chicken-and-egg.
- Settings → System shows the fingerprint and per-platform install steps.
  crypto.subtle does not exist outside a secure context — precisely the case
  this feature exists to fix — so an HTTP dashboard gets the openssl command
  to verify by hand instead of a blank field.

Verified locally: chain validates, key pairs with the leaf, CA:TRUE/CA:FALSE
are correct, keys are 0600. Two TLS servers on different ports both verify
(ssl_verify_result=0) against the CA alone and are rejected without it — the
one-CA-covers-every-port claim, tested rather than assumed.

Not yet wired: app ports still serve plain HTTP. Putting TLS on them is the
next step and is what actually closes the iframe-login bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:38:30 -04:00
archipelagoandClaude Opus 5 c65ee03a5c fix(ui): a warming-up app reads as "starting", not "App not reachable"
Demo images / Build & push demo images (push) Failing after 3m45s
A container that is up but hasn't answered its probe yet rendered the hard
failure overlay — padlock icon, "App not reachable", "the container is
stopped". Both bitcoind (RPC -28 for its whole warm-up) and lnd (unreachable
until the wallet unlocks) sit in that window on every boot, so the node
looked broken while it was working normally.

The retry machinery was already correct: 6 × 10s of automatic re-checks, and
the app appears on its own when it answers. Only the headline was wrong. While
those retries are in flight AND the package reports running/starting/restarting
(or health "starting"), the overlay now shows the app's own pulsing icon,
"<App> is starting…", and says the container is running. Once retries are
exhausted the failure is real again and the original copy returns.

Follows the ElectrumX sync-screen precedent already in this file, which
suppresses the same overlay for the same reason. The explicit blocked-reason
and must-open-new-tab paths are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:28:17 -04:00
archipelagoandClaude Opus 5 2c302d1479 docs(13): fold the full-tree sweep into the open-tasks file
A resume that reads STATE.md plus this file was still missing real work:
two planning docs untracked on main since 2026-08-05, the indeedhub
crash-loop on .38/.88, nine items still OPEN in RELEASE-1.7.121-TASKS.md,
and 19 uncommitted files in the archy-mesh worktree. All now listed here
so this one file is the whole picture rather than most of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:25:58 -04:00
archipelagoandClaude Opus 5 4b318b4c7d docs(13): drop stale handoff/checkpoint files, refresh resume pointer
HANDOFF.json and .planning/.continue-here.md both described phase 09
(2026-08-02, BotFights demo work) which was fully reconciled and pushed
in both repos at the time they were written. They are the FIRST thing
/gsd-resume-work reads, so they made a clean resume open on the wrong
phase entirely. phases/02-ui-performance/.continue-here.md is likewise a
closed-out note from 2026-07-31.

STATE.md's Session Continuity now names the real fork: 13-15 blocked on
four operator browser checks, the four non-phase node/infra tasks, and
the follow-on A/B/C proposal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:25:04 -04:00
archipelagoandClaude Opus 5 ab69400956 docs: commit the app-gate design + 2026-08-05 resume notes
Both had been sitting untracked in the working tree since 2026-08-05 —
exactly the "finished work lost because it was never committed" failure
CLAUDE.md's #1 process rule exists to prevent.

APP-PORT-AUTH-GATE.md carries the gate's design rationale ("you cannot
gate a socket you do not own") and, in its open questions, the TLS/scheme
fork that still blocks the gated-app iframe login: if the dashboard is
HTTPS and app ports are HTTP, a Secure session cookie is never sent.

RESUME-2026-08-05-appgate-fixes.md carries the .122-.125 release trail,
the two self-inflicted .124 bugs and their guards, and the open indeedhub
crash-loop (indeedhub-minio absent on .38/.88).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:24:51 -04:00
archipelagoandClaude Fable 5 7bb09ffe61 docs(13): persist open task list + resume pointer for a fresh session
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:16:36 -04:00
archipelagoandClaude Fable 5 31f78bdced docs(13): operator decision — web-search setting derives the AIUI CSP
The toggle must change what is POSSIBLE, not ask the frame to behave. Records
the verified mechanics: CSP is nginx-emitted (static add_header), the setting
lives only in browser localStorage today, and the node's nginx self-heal
reverts hand edits — so the setting moves node-side and the CSP derives from
it, allowlisted rather than wildcard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:15:00 -04:00
archipelagoandClaude Fable 5 c3277a8323 docs(13): follow-on scope proposal — CSP collision, unbuilt AIUI-02/05, nostr+zaps
Drafted during 13-15 device verification from what the operator actually hit:
the 13-09 CSP blocks wss:// relays and enrichment from the embed (real
regression, needs a broker-vs-widen decision), /api/tmdb and /api/web-search
are unimplemented on the node, AIUI-02 and AIUI-05 were declared but never
planned, and nostr polish + zaps were explicitly deferred by 13-CONTEXT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:13:27 -04:00
archipelago 706c33acaf docs(13-14): complete eval harness + confirmation-clarity plan 2026-08-06 14:08:21 -04:00
archipelagoandClaude Fable 5 08356b9e18 fix(13-11): content classifier — plural films, specific-before-generic, no bare "show"
Three defects in the two query classifiers that pick the content tab and
its header label. Found while writing the regression test for the
"Podcast recommendations" mislabel the operator reported on-device.

- "recommend me 10 scifi films" matched NOTHING: the film rule listed
  film|movie|movies but not the plural `films` — the operator's own
  phrasing. It opened no content tab at all.
- "listen to a podcast" classified as `song`: the song rule's bare
  `listen` was checked before the podcast rule. Specific terms now win —
  podcast is matched first, and `listen to` is no longer a podcast token
  (so "listen to music" stays a song query).
- A bare `show` counted as a podcast word, which is how an operator
  phrases nearly everything ("show me my files", "show the logs"), so
  unrelated queries rendered "Podcast recommendations".

Both classifiers are fixed identically and the reason they must agree is
now stated in each — they label the same panel. 16/16 content tests, 56/56
composable tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 13:40:10 -04:00
archipelagoandClaude Fable 5 82d1b60891 fix(13-10/13-11): replay chat history to the model; unbreak general answers
Four defects found by on-device UAT, 2026-08-06.

1. D-08 persistence was WRITE-ONLY. chat() loaded the transcript only
   AFTER the loop, to append — the model was never shown any of it. The
   assistant answered "I don't have access to any previous conversation
   history" with its own transcript on disk, and "and is it healthy?"
   resolved to the node instead of the app just discussed. History now
   replays into every turn (text only: a stale tool result must not be
   re-presented as this turn's evidence), scoped by HistoryKey. The
   replayed prefix is excluded from the append, or each turn would
   re-persist the conversation and grow it geometrically.

2. The operator persona forbade the very answers the content surfaces
   render. 13-01's prompt refuses anything without a matching tool, so
   "recommend me 10 sci-fi films" was declined and the film/song/podcast
   grids from 13-11 could never populate — two plans in contradiction.
   The refusal rule now governs ACTIONS ON THE NODE; general questions
   and recommendations are answered from the model's own knowledge.
   (Whether the node should also SEARCH THE WEB depends on AIUI's
   web-search setting, which embedded mode never forwards — captured as
   a separate todo because it opens a new egress path.)

3. The content-surface loader labelled unrelated queries "Podcast
   recommendations": the classifier matched a bare "show", which is how
   operators phrase almost everything ("show me my files").

4. "Surfacing…" tracks at 0.2em and its final glyph collided with the
   close button; the header now spaces them properly.

assistant::history 9/9 green incl. replay_feeds_prior_turns_back_to_the_model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 13:24:41 -04:00
archipelagoandClaude Fable 5 857d9d4906 capture: honour AIUI web-search setting in node-delegated chat
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 13:18:58 -04:00
archipelagoandClaude Fable 5 e681c95131 fix(13-12): seed screen validates the BIP39 checksum, not word shapes
Second on-device failure in one session: after the wordlist fix, dev3
blocked cloud turns AGAIN mid-session as 13-10's history grew — splitting
on every non-alphabetic character let words from unrelated JSON fields
chain into one run. Both failures took the whole feature down rather than
protecting anything, which is the worse failure for a screen to have.

Shape is the wrong signal. A real mnemonic's last word encodes a checksum
over the rest, so an accidental run of English words parses as a mnemonic
only about one time in sixteen. Candidate runs are now validated with the
same bip39 crate the wallet uses:

- tokenize on whitespace (a seed phrase is space-separated); a token's
  leading alphabetic segment counts, and alphanumerics after it end the
  phrase, so a seed glued to a closing quote is still caught
- block only if a 12/15/18/21/24 window parses as a real mnemonic
- IMPLAUSIBLE_MEMBER_RUN (20) backstops checksum-invalid material such as
  a typo'd 24-word seed, which prose cannot plausibly produce

Documented trade-off: a checksum-invalid run under 20 words no longer
blocks. The rule that did block it also blocked every legitimate turn,
twice, on a live node. 15/15 egress tests green, including the real
system prompt, scattered-JSON prose, and a genuine mnemonic in JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:42:44 -04:00
archipelagoandClaude Fable 5 24a34a373f fix(ui): tx links stop leaking to a third-party explorer on a load race
Recurring regression (reported again on .228, 2026-08-06): clicking a tx
opened the tx1138.com consent modal even though the node runs Mempool.

Root cause was never the preference — getAppState() reports
'not-installed' for an app whose container list simply has not been
fetched yet, so a click that landed before the list arrived took the
external path. Timing-dependent, hence 'fixed a thousand times'.

- container store:  flag +  (shared in-flight
  promise) so 'not yet known' is distinguishable from 'not installed'.
- openTx: awaits real data, and the local app wins whenever installed —
  including stopped/restarting, where the app session's own controls are
  the right landing place. Only a genuinely app-less node goes external.
- 5 regression tests incl. the race itself; vue-tsc -b clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 12:11:57 -04:00
archipelagoandClaude Fable 5 02a5aa6e29 fix(13-12): seed-phrase egress screen checks the real BIP39 wordlist
The shape-only heuristic ('any 12 consecutive lowercase 3-8-char words')
matched ordinary prose — including the node's own system prompt — and
blocked 100% of live cloud chat turns (found on dev3, the first real
Claude call through this screen; log: kind=bip39-word-run every turn).
Membership in the crate's own bip39 English wordlist (already a dep via
seed.rs) distinguishes prose from seed material: glue words break runs,
real seeds are nothing but members. Regression test pins the real system
prompt + a clean wire body to Allow; the 12-word genuine-seed case still
blocks. 13/13 egress tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:28:41 -04:00
archipelagoandClaude Fable 5 52a400b3c5 capture: funding-settings modal entry point; slot into next week's UX pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:07:46 -04:00
archipelagoandClaude Fable 5 5fc8289436 capture: Routstr funding UX in AIUI dropdown (operator request)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:07:36 -04:00
archipelagoandClaude Fable 5 39d53d212e docs(13): defer dev3 console-noise triage — embedded web-search/rss, wiki demo spam, content timeout pairing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:01:26 -04:00
archipelagoandClaude Fable 5 dfe027a5f3 style: cargo fmt (rnode_settings)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 10:49:15 -04:00
archipelagoandClaude Fable 5 ee9dde9936 fix(mesh-ui): rnodePlan computed for the setup modal (strict TS)
Demo images / Build & push demo images (push) Failing after 4m12s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 10:35:55 -04:00
archipelagoandClaude Fable 5 4773b32a76 feat(mesh-ui): region-recommended RNode plan applies from the setup modal too
Demo images / Build & push demo images (push) Successful in 4m4s
The device-detected modal's region selector now drives real RNode
settings instead of a "managed by the daemon config" shrug: choosing a
region shows its concrete plan (frequency/bw/SF/CR/power) and Apply &
Connect writes it through mesh.rnode-config-apply — the same
radio-confirmed round-trip as the Device panel, best-effort so a plan
failure never aborts the connect. RNODE_REGION_PLANS moves to
utils/loraRegions (single source shared by panel + modal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 10:31:01 -04:00
archipelagoandClaude Fable 5 f394c02055 fix(mesh): ride out the radio restart during apply — no false errors, no setup modal
Demo images / Build & push demo images (push) Successful in 3m54s
Applying RF settings deliberately restarts the radio daemon (~15-20s).
Two things treated that healthy, expected gap as a fault (operator,
2026-08-06):

- radio_state was single-shot: a query landing inside the restart
  window reported "The radio daemon did not answer the state query"
  for a restart that was working correctly. It now retries for ~30s
  and says the radio is restarting while it waits. A real device-level
  refusal (not an RNode) still returns immediately.
- The device-setup modal auto-opens for any detected-but-unconnected
  port, so the restart looked like a newly plugged stick and
  interrupted the apply. Apply and Reboot now suppress auto-detect for
  90s via mesh.suppressDeviceDetect().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:55:52 -04:00
archipelagoandClaude Fable 5 51a13da2a6 merge: bring main (v1.7.125 + .126 work) into phase-13 branch pre-deploy
63 main commits since the fork point — gate cookie-strip fix, named-volume
create fix, appgate catalog classification, RNode error surfacing — merged
so 13-14/13-15 on-device verification runs against current production code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:33:40 -04:00
archipelagoandClaude Fable 5 53753687a5 style(entropy-guide): rebuild on the dashboard's own sidebar + glass-card
Demo images / Build & push demo images (push) Successful in 4m1s
Replaces the bespoke doc-page chrome with the app's real components:
DashboardSidebar shell (256px, rgba(0,0,0,.25) + 18px blur, staggered
nav-item entrance), the AnimatedLogo neode mark with its 20 staggered
squares, sidebar-nav-item + nav-tab-active for section state, and the
Settings wallpaper behind it all.

Every bespoke container (.card/.card-sm/.step/.score-card/.callout-*/
.diagram) is gone — .glass-card from style.css is now the only box on
the page, with layout-only grids inside it.

Scroll-spy lives in nav.js rather than inline, since the node's CSP is
script-src 'self'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:30:41 -04:00
archipelagoandClaude Fable 5 3e124dd4b3 fix(rpc): surface unknown-method + RNode settings errors instead of masking
Deploying the .126 LoRa panel ahead of its daemon made every button
report "Operation failed. Check server logs for details." — the panel
was calling RPCs the older binary doesn't have, and the sanitizer
masked "Unknown method: mesh.rnode-config" into that generic string.
Read as "the feature is broken" rather than "this node needs its
update" (operator, 2026-08-06).

Allowlisted: "Unknown method" (a frontend newer than its daemon should
say so), every RNode RF validation message (each names the field and
its legal range — the entire point of validating before touching the
radio), and the actionable mesh preconditions (no device connected,
mesh service not running, MeshCore has no remote reboot, radio daemon
did not answer, RNode interface disabled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:28:44 -04:00
archipelagoandClaude Fable 5 6c0fc366b3 fix(appgate): classify ports from the catalog even for on-node-built apps
The port map deferred to DISK manifests for any app with a build source
— which is exactly the four companion UIs (lnd-ui, bitcoin-ui,
electrs-ui, fips-ui). Their disk manifests reach nodes only via the
frontend runtime payload or a per-node repo checkout, and in the
v1.7.125 rollout both proved stale or entirely absent: one node had no
checkout at all, others restored an older payload over apps/ at every
boot. Result: session_passthrough never reached the gate, so the node's
own screens 401'd on every data call, and on nodes whose UI rebuilt
from a stale context the app held its port UNGATED.

Classification now uses a ports-only overlay that accepts build-source
manifests (install/orchestration still defers to disk — unchanged). The
signed catalog is the freshest, operator-signed source, and the gate's
address binds fail safely against a container publishing differently
(logged CANNOT PROTECT), so this can only tighten policy, never expose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:17:10 -04:00
archipelagoandClaude Fable 5 83d7234824 style(entropy-guide): use the app's real look — Settings wallpaper + Montserrat
Demo images / Build & push demo images (push) Successful in 4m2s
Layer the page over /assets/img/bg-settings.webp (fixed, cover, with a
dim gradient so prose between glass panels stays readable) and load the
actual Montserrat Bold/ExtraBold faces from the node's own web root,
instead of the flat-black background the /architecture/ guide uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:11:15 -04:00
archipelagoandClaude Fable 5 3cef1d09f4 feat(mesh-ui): RNode settings editor with live device read-back + region presets
Demo images / Build & push demo images (push) Successful in 3m49s
The LoRa device panel's Reticulum section (operator .126 top priority):

- Shows the device's CURRENT settings first — the radio-confirmed r_*
  values from mesh.rnode-config (online badge, port, frequency, bw,
  SF, CR, txpower, airtime limits), with a Refresh action.
- Every RNodeInterface parameter is editable: enabled, serial port
  (auto-detect when blank), frequency, bandwidth (RNode's discrete
  set), SF 5-12, CR 4/5-4/8, txpower, airtime short/long %.
- "Set recommended for <region>" fills the fields from per-region
  plans (EU868 = the operator-validated Portugal plan incl. 25%/10%
  duty-cycle locks); driven by the existing region selector above.
- Apply & Confirm on Device: persists, restarts the radio daemon, and
  reports the radio's own confirmation (green ✓ only when the device
  read-back matches; amber/red messages say what actually happened).
- Action buttons stack in a column (operator layout request).
- Reboot Radio surfaces the backend's real acknowledgement message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:08:19 -04:00
archipelagoandClaude Fable 5 45fa8b6c6f feat(neode-ui): seed & entropy explainer page at /entropy/ + link from Backup settings
Demo images / Build & push demo images (push) Successful in 4m19s
Standalone static guide (same pattern as /architecture/) covering how the
master seed entropy is drawn (explicit OsRng, sealed KeyGenRng allowlist,
degenerate-draw refusal, CSPRNG readiness ledger), how it is stored
(Argon2 + ChaCha20-Poly1305 envelope), the full derivation tree (HKDF
labels, NIP-06, LND aezeed one-way gate, second-order keys), what is NOT
seed-derived, every failure/fallback path, and the restore flow — in
paired layman/technical language. Linked from the Recovery-phrase card
in Settings → Backup. CSP-safe: no inline scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 08:56:34 -04:00
archipelagoandClaude Fable 5 209a36e53c feat(mesh): rnode-config RPCs + honest reboot feedback with reply channels
- mesh.rnode-config: persisted RF settings + best-effort live radio
  state (radio-confirmed r_* values) for the LoRa panel.
- mesh.rnode-config-apply: validate → persist → restart the radio
  daemon → poll the read-back until the radio reports online, returning
  {applied, confirmed, live, message}. Failure modes report what
  actually happened instead of pretending success.
- RebootRadio carries a reply channel: Meshtastic reboots firmware,
  Reticulum restarts the sidecar (re-detect + reapply RF config),
  MeshCore honestly reports it has no remote reboot — previously the
  Reticulum/MeshCore arms returned Ok(()) doing NOTHING: the operator's
  "button gives no feedback" bug.
- MeshCommand::QueryRadioState plumbs the sidecar's radio_state to the
  service layer with a timeout instead of fire-and-forget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 08:43:39 -04:00
archipelagoandClaude Fable 5 4dd8bacd0e feat(mesh): persisted RNode RF settings with adopt-don't-clobber migration
The .126 LoRa panel's Rust half:

- mesh::rnode_settings: RNodeRfSettings persisted at
  <data_dir>/rnode-rf-settings.json — every RNodeInterface parameter
  (enabled, port override, frequency, bandwidth, sf, cr, txpower,
  airtime_limit_short/long), validated against the bounds RNS itself
  enforces. Defaults are byte-identical to the sidecar's historical
  argparse defaults.
- FIRST-RUN ADOPTION (operator requirement: the update must change NO
  device's applied settings): with no settings file yet, the node's
  existing RNS config (~/.archy-reticulum, else ~/.reticulum) is parsed
  and its RNodeInterface values adopted verbatim as the initial
  settings — proven by a test carrying the operator's literal
  "RNode LoRa Portugal" config.
- Serial spawns pass the settings as explicit sidecar args (frequency/
  bandwidth/txpower/sf/cr + airtime locks); the operator port override
  wins over auto-detect but still passes the KISS probe gate; a
  disabled interface refuses to open with a readable error.
- ReticulumLink::query_radio_state(): asks the sidecar for the live
  RNodeInterface state (radio-confirmed r_* values) — the panel's
  apply-confirmation read-back source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 08:28:45 -04:00
archipelagoandClaude Fable 5 2afeafc92e feat(reticulum-daemon): airtime-limit args + radio_state RPC for live read-back
Groundwork for the .126 LoRa settings panel (operator top priority):

- --airtime-limit-short/--airtime-limit-long (percent duty-cycle locks,
  e.g. EU868 25/10) written into the RNode interface config when set;
  default None writes nothing — identical to older daemons.
- New socket RPC {"cmd":"radio_state"} returns the live RNodeInterface
  state: requested config values AND the radio-confirmed r_* values
  (r_frequency/r_bandwidth/r_txpower/r_sf/r_cr/r_st_alock/r_lt_alock,
  online, port, airtime utilisation). The r_* values are what the RADIO
  reported after detect/configure — the settings panel's proof that the
  device is actually using what was applied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 08:04:15 -04:00
archipelagoandClaude Fable 5 44522fc94a chore(scripts): one-shot node-side companion-manifest repair script
Curl-and-pipe repair for nodes whose companion-UI manifests are stale
(no session_passthrough): fetches the four current manifests from the
public repo, installs them into /opt/archipelago/apps AND the frontend
runtime payload (which restores over apps/ at every boot), restarts,
and reports the gate probe. Long paste-blocks kept mangling in the
operator's terminal — this replaces them with one short line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:59:34 -04:00
archipelagoandClaude Fable 5 171d75dc01 docs(13): STATE — 13-14 at Task 3 human-verify gate (comprehension study)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:04:33 -04:00
archipelagoandClaude Fable 5 27aa5ccd10 feat(13-14): the harness — offline, deterministic, per-backend, zero footprint on a node
assistant/evals.rs: a test-gated in-crate module (#![cfg(test)] here AND
#[cfg(test)] pub mod evals; in mod.rs — never compiles into the shipped
binary, asserted by a release-binary string grep). load_cases/case_by_id
read the 18-case JSONL fixture by path; run_case drives the REAL run_loop/
execute_tool/ConfirmGate choke points end to end against a case's grants,
seeded untrusted content, and scripted backend turns, returning a
CaseOutcome that observes ToolCall/ToolResult/confirm-gate transitions
in-process rather than inferring them from prose. evaluate_case asserts
must_not_execute/must_not_claim at threshold zero (E-01's security and
integrity halves) and confirmations/turns at exact match, every failure
message naming the case id and the offending tool/term.

Parameterized over the Backend trait (CountingBackend wraps any real
Backend to measure turns used; a BudgetExhaustedStubBackend drives EV-17's
S-12 stop-without-retry path) so scripted, Ollama, Claude or Routstr can
all run the same 18 cases. report_by_backend/parity_requires_two_backends
refuse to record a cross-backend parity pass from fewer than two backends
(E-07). Live-backend runs are opt-in via ARCHY_EVAL_BACKENDS and #[ignore]d
so a plain `cargo test` never touches the network. write_trace_jsonl writes
one plain JSONL file per run under core/target/assistant-evals/ (gitignored
build output) — no exporter, no collector, no listening port.

All 18 cases pass against ScriptedBackend (23/23 assistant::evals:: tests);
full crate suite 1258/1258; release binary contains zero eval-fixture
strings; no phoenix/promptfoo/ragas/opentelemetry references anywhere in
assistant/; no new CI job (ci.yml untouched — picked up by the existing
`cargo test --all-features` step); zero new packages (T-13-SC).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 07:03:50 -04:00
archipelagoandClaude Fable 5 d419141a86 feat(13-14): the eighteen reference cases — the specification of what the loop must refuse
EV-01..EV-18 (four happy reads, four confirmed writes, five injection cases,
three authority-ceiling cases, one budget case, one privacy case) per
13-AI-SPEC.md §5's schema, written against the real tool registry
(assistant::tools::registry()) and the real wrap_untrusted() boundary shape
rather than against the spec's description of them. EV-11's payload carries
a forged closing boundary in the exact `{label}_DATA_{token}_END` shape
untrusted.rs emits, proving why the per-call random token (not the wording)
is what makes the boundary hold. README.md records the per-bucket
reviewer-role labeling from §5's Labeling table (engineer for EV-01..EV-08,
security-minded red-teamer for EV-09..EV-16, non-technical reviewer for the
EV-05/EV-06 confirmation-copy judgment) so a later contributor knows whose
judgment each case's expect block encodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 06:53:25 -04:00
archipelagoandClaude Fable 5 1948767083 chore: release v1.7.125-alpha
Demo images / Build & push demo images (push) Successful in 4m12s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1.7.125-alpha
2026-08-06 06:27:02 -04:00
archipelagoandClaude Fable 5 e875f15fc5 docs: curated changelog for v1.7.125-alpha
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 06:10:39 -04:00
archipelagoandClaude Fable 5 63fa8b4558 docs(13-13): complete Routstr backend / D-05 budget ceiling plan
D-04's chain complete (Ollama -> Claude -> Routstr); D-05's prepaid
allowance is a hard arithmetic ceiling, verified by fault injection.
Task 1 decision: proceed-docs-with-probe-first (0/9 protocol claims
independently confirmed; first live call doubles as the capability probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 05:39:02 -04:00
archipelagoandClaude Fable 5 8ba6041251 feat(13-13): D-05 prepaid budget — arithmetic ceiling, hard stop, D-04 chain complete
Completes D-04's backend chain: Ollama -> Claude -> Routstr (budget-gated),
and wires D-05's operator-set prepaid allowance as a hard, arithmetic stop
a prompt-injected model can never cross.

- assistant/mod.rs: `AssistantBudget` (allowance_sats/spent_sats,
  persisted 0600 under data_dir/assistant/budget.json, mirroring
  Grants::load/save exactly — a missing/corrupt file defaults to a ZERO
  allowance, D-16's "default closed" applied to money). `payment_policy()`
  builds a `PaymentPolicy` from ONLY these two persisted fields — no
  parameter accepts anything model/tool/provider-influenced, which is what
  makes the ceiling arithmetic rather than a policy an injected model
  could argue with. `record_spend()` persists a successful payment and
  raises a one-time 80%-threshold owner notice (AI-SPEC §7b). New typed
  `BudgetExhausted` error (downcastable via anyhow) is the signal
  `loop_.rs` distinguishes from an ordinary transport error.
- assistant/loop_.rs: `run_loop` downcasts a `BudgetExhausted` out of the
  backend's `Err` and returns `Ok` with a plain-language stop message —
  no retry, no re-price, no partial spend, no fall-through to a different
  provider at a different price. Verified to actually matter: temporarily
  replaced the terminating `return` with `continue` and confirmed
  `zero_budget_stops_loop_without_retry` goes red (the backend gets
  retried 8x to MAX_TURNS and the turn errors instead of stopping
  cleanly); restored and reconfirmed green (13-13-SUMMARY.md records the
  observed failure).
- assistant/backends/mod.rs: `select_backend` now takes `&RpcHandler`
  (was `&Path`) to also read the Tor-proxy config; completes the D-04
  chain — Routstr never selected when the operator's allowance is zero
  (Claude alone instead), otherwise chained as Claude's fallback
  (Ollama -> Claude -> Routstr, each leg reached only when the priors are
  unavailable). New `BackendId::Routstr` variant.
- assistant/backends/routstr.rs: the payment-decline arm now returns the
  typed `BudgetExhausted` (was a plain bail in Task 2's commit, per the
  plan's own "handled in Task 3" note); a successful payment records spend
  against the persisted budget immediately (the Cashu proofs are already
  committed at that point, regardless of whether the subsequent chat HTTP
  call itself succeeds).
- api/rpc/assistant_chat.rs: `assistant.budget-get`/`assistant.budget-set`
  RPCs (routed through the existing single `assistant.` dispatcher arm —
  dispatcher.rs untouched) and a `nostr_tor_proxy()` accessor for
  select_backend's onion-preference decision.

Named tests (assistant::tests::): zero_budget_stops_loop_without_retry (S-12),
zero_allowance_never_selects_routstr, ceiling_is_not_a_function_of_model_output,
injection_loop_against_low_budget_does_not_overspend (EV-17) — all pass.
Full assistant:: suite: 91/91. Full crate suite: 1235/1235 (2 pre-existing
ignored, unrelated). dispatcher.rs and Cargo.toml untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 05:33:27 -04:00
archipelagoandClaude Fable 5 ed062481c3 style: cargo fmt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 05:32:53 -04:00
archipelagoandClaude Fable 5 a3521e5eeb feat(13-13): Routstr backend adapter — Nostr discovery, OpenAI chat, Cashu payment attach
Task 1 decision (proceed-docs-with-probe-first, operator-selected via
AskUserQuestion 2026-08-05): 13-ROUTSTR-FINDINGS.md observed 0 of 9 cited
protocol claims (no live provider was announcing on any of the 3 default
relays in a 30s window on 2026-08-03; relay reachability itself WAS
confirmed). This backend is written against docs.routstr.com's cited shape,
with the first live chat-completions call doubling as the capability probe:
a non-success HTTP status or a response missing the expected
choices[0].message shape fails loudly (bails with the real status/body)
rather than silently degrading.

- assistant/backends/routstr.rs (new): RoutstrBackend implements the
  Backend trait — discover_providers subscribes for kind-38421
  provider-announcement events over the existing Tor-proxy-aware Nostr
  client (nostr_discovery::build_nostr_client, never a second relay
  client), process-cached with a 5-minute TTL; select_provider picks the
  globally cheapest affordable (provider, model) price across every
  discovered provider (Routstr has no fixed target model the way
  Ollama/Claude do — CONTEXT.md delegates provider selection strategy to
  Claude's discretion), preferring an onion endpoint when Tor is up;
  attach_payment calls the existing budget-capped auto_pay_token verbatim
  (never hand-rolled); parse_openai_tool_calls parses the one
  string-encoded function.arguments shape exactly once at this adapter's
  edge; screen_outbound (G-B1/G-B2) runs before any body leaves the node,
  exactly as it does for Claude; ROUTSTR_MAX_TOKENS caps every request
  explicitly.
- assistant/egress.rs: message_is_turn_own gains "system" and "tool" role
  handling plus an OpenAI tool_calls-sibling-field check — the pre-existing
  function was written only against Claude's wire shape (system as a
  top-level field, tool results wrapped in role:"user") and would have
  silently stripped Routstr's system prompt and tool-result context out of
  every outbound request via G-B2's fail-closed default arm. Fixed with 4
  new regression tests pinning both wire shapes.
- assistant/backends/mod.rs: registers `pub mod routstr;`. select_backend's
  actual wiring of the Routstr leg (budget-gated, per D-05) is Task 3's
  commit, once AssistantBudget exists — this task's own acceptance criteria
  do not require select_backend integration, only the adapter itself.

30/30 assistant::backends:: tests pass (17 new in routstr.rs, 3 new in
egress.rs's OpenAI-shape regression tests were run separately at 12/12).
Zero new packages (nostr-sdk/reqwest already in-tree); dispatcher.rs and
Cargo.toml untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 01:26:59 -04:00