Everything needed to continue cold: the loop protocol with real deploy and
verify commands, the ordered work list with each item's evidence, Phase 13's
exact remaining state (13-15 only, check 4 passed on-device), and the traps
that cost time tonight — verify on the node not from source, rustls does not
check key/cert pairing, build-aiui.sh hangs after succeeding, AIUI needs
VITE_BASE_PATH=/aiui/.
STATE.md's stopped_at now points at it, so /gsd-resume-work lands correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IndeeHub worked all year and broke when the gate rolled out. Cause, verified on
the node: GET /manifest.json returns 401 + the gate's login HTML. A browser
fetches <link rel="manifest"> in no-credentials mode unless the tag opts in
with crossorigin="use-credentials", so the session cookie is NEVER offered and
the gate challenges a fully authenticated user. The app's service worker then
serves its cached shell, whose every network call fails — which reads as "the
app is broken" rather than "the gate refused it". Any gated app with a PWA
manifest has the same failure.
Passed through unauthenticated on purpose, and deliberately as small as the
problem: an EXACT-match allowlist of /manifest.json, /site.webmanifest and
/favicon.ico. Static, non-user-specific, and no more revealing than the gate's
own login page, which already shows the app's name and icon.
Exact match, never a prefix — a prefix would let /manifest.json/../api/secrets
ride through. A test pins that: 8 near-miss paths (traversal, query-string
traversal, /api/manifest.json, /manifest.jsonx, case variants, /admin,
/api/auth/nostr/session) must all still be challenged.
19/19 appgate tests pass. This does NOT address the app's own auth endpoints
being intercepted — that needs a session-aware decision and is recorded
separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every item was observed on archi-dev-box or read from source, not inferred:
the content-card parser mispairing titles with the previous description (the
real cause of "idiotic responses" — the model's prose was correct), IndeeHub's
three independent faults (empty public library, Nostr-only private auth, relay
502 on loopback), the fleet-wide gate bug that 401s credential-less PWA
manifest fetches and app-owned auth endpoints, and AI Data Access grants living
in per-origin localStorage when they are a property of the node.
Input for a research + plan pass, explicitly not the plan itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On-device, an empty films search looked like a broken fetch. The console said
only "library: not permitted" — the content scopes returned null without a
word, so an ungranted Media/File permission was indistinguishable from "this
node genuinely has no films". That ambiguity cost real diagnosis time and sent
me looking for a code fault that was not there.
Each scope now names itself when denied. The permission was the whole cause;
no content path was broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported on-device: searching for films in AIUI returns nothing. Init only ever
asked for scope 'own' (content.list-mine — this node's own shared files), so
IndeeHub and everything else purchased, which lives in 'owned'
(content.owned-list), and other nodes' catalogs in 'peers' were never fetched.
Both scopes existed only as type-signature options with no call site anywhere
in the app.
requestArchyAllContent() now loads all three concurrently and merges once.
Merged rather than three setArchyContent calls because that sink REPLACES
films/podcasts — separate pushes would leave only whichever resolved last, the
same class of bug as the shared sequence guard fixed in aac81503. Deduped by
id, since a title can legitimately appear both owned locally and offered by a
peer. Each scope is caught individually so one dead or slow peer costs only its
own results, which is normal rather than exceptional.
requestArchyContent also stops clobbering songs with an empty array, mirroring
what requestArchyLibrary already did for films/podcasts.
vue-tsc clean, 3/3 useArchy tests pass, and the change is verified present in
the built bundle rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>