vue-tsc rejected the double cast; vitest strips types so it slipped
through. Narrow once into a local instead.
Co-Authored-By: Claude <noreply@anthropic.com>
- serve_content takes owner_session: a validated operator session skips the
availability/paid gates (Availability::Nobody stays delisted); the cookie
is re-validated in the content handler, same discipline as the model proxy
- the Tor proxy serves already-purchased items from the local content_owned
cache with Range slicing (206) instead of re-hitting the seller's 402 —
the buyer-side store exists so an owned item is never bought twice, and
its cards were rendering as permanent placeholders
- adapter: 'own'-scope items never render locked (a locked card suppresses
the playable URL — the placeholder-only grid the operator reported)
- broker: normalize 'purchased' OwnedRpcItems per item with the seller's
onion, and group 'peers' items per seller onion, so buildMediaUrl gets a
peerOnion and card URLs stop coming out empty
Co-Authored-By: Claude <noreply@anthropic.com>
SearXNG defaults to `formats: [html]`. Its JSON API answers 403 —
and JSON is the only thing AIUI's web search speaks, since
`/aiui/api/web-search` proxies straight through to `/search`. Both
places that seed settings.yml (the first-boot script and the installer)
omitted `search.formats`, so web search has never worked on a node
whose SearXNG was installed, running and healthy. It reads as the
assistant being unable to search rather than as one missing config key.
Verified on archi-dev-box: `format=json` went 403 -> 200, returning 28
results for "bitcoin halving" from Brave and DuckDuckGo. Google and
Startpage self-suspend on a self-hosted instance (access denied /
CAPTCHA), which is expected and costs little given Brave's independent
index.
Existing nodes need the same two lines added to
/var/lib/archipelago/searxng/settings.yml and a restart; this commit
only fixes what new installs get.
Also fixes a build break: `fetchLibraryContent` built a bundle literal
that predates the images bucket.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four defects, one visible symptom: a correct prose answer beside an
empty grid.
1. The assistant's curated RPC bridge had an arm only for
`content.list-mine`. `tools.rs` mapped the `peers`, `purchased` and
`films` scopes onto three real, dispatcher-registered handlers that
`assistant_dispatch_tool` had never heard of, so every non-"own"
scope died on its catch-all. Downstream that read as "the peers have
no content" — it was a missing match arm, and the tool never ran.
Regression test added: every scope the schema advertises must reach a
real handler.
2. `content.browse-all-peers` wrapped its whole fan-out in one
`timeout(..).unwrap_or_default()`, which DISCARDED every completed
batch the moment the budget expired. One slow peer turned a
partly-successful browse into "0 reached, 16 unreachable". Observed
live on archi-dev-box: back-to-back calls returned real peer items,
then nothing. Now accumulates per batch and checks a deadline between
them, so partial results always survive. Budget 20s -> 45s: two
batches of eight at a 10s per-peer timeout had no headroom at all.
3. `assistant.chat` returned only `{ text }`. The structured results of
any content tool the turn ran were dropped inside the loop, so the
surface had nothing to render. The turn now carries them through
(captured raw, before the untrusted wrap, since they go to a renderer
that treats every field as inert data, never back into the prompt).
4. The adapter classified images as 'excluded' and dropped them. A node
sharing mostly photos rendered as an empty grid while AIUI's image
grid sat unused. Images now have a bucket, with the paid-lock and
extension-fallback handling audio and video already had.
Also: the panel says "Loading…" while a turn is in flight and "Nothing
found" when it comes back empty, instead of leaving the previous
query's heading standing as though it answered this one; the system
prompt tells the model to call the content tool and summarise rather
than re-list what the cards already show; and a refused tool now names
its permission category so the trusted chrome can offer the settings
screen instead of leaving "I don't have a tool for that" as the only
clue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
content.browse-all-peers had a per-peer timeout but no OVERALL budget. On this
node that meant >45s with no answer, which the assistant reported to the
operator as "having trouble accessing the peer content list". Measured cause:
16 federated peers, 1 reachable. Now bounded to 20s total, returning partial
results with peers_reached / peers_total / peers_unreachable / partial, so the
assistant can say "1 of 16 peers answered" instead of implying the rest have
nothing. Verified on the node: 20.015s, was >45s.
NodeCertificateSection had no container — I copied a section that sits INSIDE a
card rather than one that provides its own. Now uses the same
`glass-card px-6 py-6 mb-6` shell and heading level as every other settings
section, so it matches on desktop and mobile.
setup-node-ca.sh now also ensures the nginx HTTPS listener, because a CA is
useless if nothing serves TLS. It binds LAN addresses ONLY: tailscaled already
owns :443 on the tailnet addresses with its own Let's Encrypt cert, so a plain
`listen 443 default_server` binds 0.0.0.0 and fails EADDRINUSE — and nginx then
keeps running the OLD config while the reload reports success. Hit exactly that
on archi-dev-box. Port 80 keeps serving: nodes are reached by IP on LANs where
forcing a redirect would strand anyone who has not installed the CA.
Live now: https://192.168.63.240/ and https://<host>.local/ both 200 with
verify=0 against the node CA, http still 200, tailscaled's 443 untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"[Vue Error] ReferenceError: Cannot access 'b' before initialization" from
Ye.immediate, taking the whole Mesh view down.
A watcher with `immediate: true` runs DURING setup. This one calls
handleFetchContent, whose body touches consts declared further down the setup
block — so on any session where history already contained an inline
content_ref, it dereferenced a binding that did not exist yet. handleFetchContent
itself is a hoisted `function`, which is why the call site looked innocent.
The initial pass moves to onMounted, which runs after setup completes: every
binding is initialized, and already-loaded history still gets the same
treatment as new messages, which is what `immediate` was there for.
Also adds .planning/todos/pending/2026-08-07-open-task-list.md — one flat list
of everything open, including the app-lifecycle reports (fedimint guardian
installs but does not work, BTCPay wipe not wiping, Bitcoin Knots vanishing,
fedimint gateway dying at 88%), the missing app_install tool behind
"!ai install bitcoin knots", and the LND UI 401s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator: "the AI Data Access settings are not persistent through sessions,
often turns them all off."
They were stored in localStorage, which is scoped to an ORIGIN — and a node
answers on several: LAN address, Tailscale address, <host>.local, hostname.
Granting Media over the LAN and returning over Tailscale showed every switch
off again. Not reset: never set *there*. It also made a working content path
look broken, because every scope silently returns nothing without a grant, so
an ungranted permission is indistinguishable from an empty library — that is
exactly what an empty films search turned out to be.
The grant answers "what may the assistant read about THIS NODE", which is a
property of the node, not of one browser at one address. New
settings/ai_permissions.rs (same shape as session_policy: atomic temp+rename,
sanitised on read and write, fails closed on a corrupt file — an unreadable
grant file must never read as "everything allowed"). New ai.permissions.get /
.set, absent from the unauthenticated allowlist so they require a session.
Migration, not replacement: if this browser holds grants and the node holds
none, the local set is pushed UP rather than wiped. Without that, upgrading
would silently revoke the grants of everyone who set them before this change.
The node still wins in every other direction, so a revocation made on one
device takes effect everywhere — otherwise revoking would be impossible from a
second device.
Unknown category ids are stored verbatim rather than validated against a
hardcoded list: a third copy of that list would silently drop a new category on
upgrade. Storing a category grants nothing by itself — the broker checks before
fetching and the node re-checks before answering (T-13-33).
Hydration happens ONCE at broker start, not inside each permission gate: the
gates are hot-path, and awaiting there adds an RPC to every content and context
request. The first attempt did it per-gate and the existing broker tests caught
it by failing on consumed mocks.
Rust 7/7, store 18/18, broker 23/23.
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>
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>
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>
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 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>
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>
Operator-reported (2026-08-05):
- Send/Receive modals reset to a blank slate on every open. Stale state —
destination, amount, memo, and above all an armed "send all funds"
toggle — silently carried into the next payment.
- Arming "send all funds" now shows the swept balance in the (disabled)
amount field instead of a confusing 0; disarming or leaving the
on-chain tab clears it.
- The scan modal no longer hides "Scan with camera" on plain-http desktop
(browsers only allow getUserMedia on secure origins): the option stays
visible with a one-line explanation, and choosing it surfaces the HTTPS
requirement with photo/paste fallbacks. The companion app's native
scanner path is untouched and still takes priority.
- What's New entry for v1.7.125-alpha.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Declaring the Guardian UI port on the fedimint app made the orchestrator try
to publish 8175 from fedimintd, colliding with archy-fedimint-ui which
already holds it: start_container failed on every reconcile and fedimint
crash-looped (100.82.34.38). The companion's nginx pinned to 127.0.0.1 is
what actually closes that port; the gate reports it rather than fronting it.
Also: app-login page uses the sidebar's 'A' mark instead of the full
wordmark, is pinned to the small viewport so it stays centred and the
keyboard overlays rather than scrolls it, and the install-version modal
icon uses object-contain so a non-square icon is no longer cropped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130s TTL predated the UAT timeout bump — it disarmed the approve/deny
listener while the dialog was still legitimately open (self-healing via
the next poll's re-announce, but a click in the gap dropped silently).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AIUI asks for the library the same way it asks for content, and the
fetch now actually fires without anyone typing a magic phrase:
- useArchy.ts: requestArchyLibrary(scope) sibling of requestArchyContent
(13-06), same bridge call with kind: 'library', routed through the
existing setArchyContent so the songs bucket fills exactly the way
films already does.
- init() now calls both requestArchyContent('all','own') and
requestArchyLibrary('own') once, fire-and-forget, immediately after
archyBridge.init() — the GAP-FOUND fix. 13-06 built the whole
content:request/content:push machinery and unit-tested it end to end,
but nothing in the live UI ever called it (13-06-SUMMARY.md's Known
Limitations); the fetch is now triggered by a real init-time UI event,
not merely callable.
- useContentPanel.ts's setArchyContent now also opens the panel and
populates availableTabs/activeTab/panelTitle when Archy supplied
non-empty content — previously only the data refs were set while the
tab bar and panelOpen stayed whatever the last regex-driven chat turn
left them, so real content could sit fully populated and still never
render. An empty bundle never force-opens the panel.
Deviation (Rule 2, mirrors 13-06's own archyBridge.ts precedent): kind:
'library' genuinely needs a different node-side RPC (music.list-tracks,
real tag-extracted metadata) than content.* (ContentItem has no artist/
album/duration field at all) — contextBroker.ts's handleContentRequest
gained one branch (fetchLibraryContent) to route it, and
aiui-protocol.ts's AIUIContentRequest.kind union gained the 'library'
literal, and archyBridge.ts's requestArchyContent kind param widened to
match. No second channel, no new message type, no new listener — the
existing content:request/content:push channel and its kind discriminator
carry this exactly as 13-06 designed it to. Full detail in the SUMMARY.
neode-ui: 926/926 tests green, vue-tsc -b clean. aiui: 341/344 (3
pre-existing, documented failures unrelated to this plan — 13-06/13-10
already recorded them), vue-tsc --noEmit clean.
Adds the four missing audio extensions to ShareModal.vue's extension-to-
MIME map (m4a->audio/mp4, aac->audio/aac, opus->audio/opus, wma->audio/
x-ms-wma), extracted to an exported module-scope SHARE_MIME_MAP so it's
directly fixture-testable (useFileType.test.ts convention). All three
maps agree that these eight extensions are audio/*: SHARE_MIME_MAP,
archyContentAdapter.ts's classifyByMime (13-06), and content.rs's
auto-filing check, which is prefix-only (mime_type.starts_with("audio/"))
so any correct audio/* value here already satisfies it. Existing four
entries (mp3/flac/ogg/wav) and the generic-fallback behavior for unknown
extensions are unchanged. Whole neode-ui suite green (924/924).
adaptLibraryTracks/adaptLibraryAlbums in archyContentAdapter.ts: real
tag-extracted title/artist/album/duration from the music.* index (13-07),
artist falls back to album_artist then '', order preserved from the
index's own deterministic sort (never re-sorted browser-side), no
cover-art URL (Track carries no artwork field — SongGrid's no-artwork
state renders), own-library tracks resolve through the existing
FileBrowser raw-file route, peer tracks through the existing Range-
streaming proxy, no credential ever in a query string. 34/34 tests green.
Portainer's image reaches the public catalog (the release gate caught the
manifest and catalog disagreeing), and fips-ui 8336 joins the mesh relay's
port list now that it declares a port — it is auth: gated, so the relay
withholds it rather than bridging it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-bumped so the release gate compiles the test profile at the final
version — create-release bumps after the gate, so the gate would otherwise
run on the old version and the bump would invalidate the cache, timing out
cargo-test-weekly on the compile rather than the tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leads with the update that switched nodes off and left them unable to
switch back on — the one an operator most needs to understand, and the
reason to take this release promptly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two more on-device UAT findings:
1. Deny-retry loop: the model, told 'the user declined', simply called the
tool again — each retry minted a fresh pending and re-opened the dialog
(T-13-50 habituation, mechanized). ToolExecCtx now remembers declined
actions for the turn, keyed by confirm::action_key — the same canonical
(tool_name, validated_args) identity the nonce binds — and execute_tool
refuses a re-ask before the gate, minting nothing. Regression test
declined_action_never_reprompts_same_turn.
2. Timeout chain: rpcClient's 15s default aborted every confirmable turn
client-side while the node kept the pending alive — the next turn then
re-announced it (modal over and over) and every wait read as 'timed
out'. assistant.chat now rides a 420s timeout; AIUI's bridge goes
180s→430s so the host's error path (which also expires the dialog)
always fires first. Declined ToolResult text now also tells the model
to stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
auth.session-policy.get/set plus a card under Account. Presented as two
plain questions rather than the token mechanism underneath, because the
distinction that matters to an operator is which control actually ends a
session: the dashboard polls constantly, so an idle timeout alone never
fires on an open tab — the absolute cap is what guarantees it.
Values are clamped server-side and the stored result is echoed back, so
the bounds are discoverable instead of an error. Presets rather than a free
number field: a box accepting '5' invites locking yourself out. A short
idle choice warns that it is the payments-industry posture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On-device UAT: the operator was timed out mid-read (120s), the chat turn
returned 'declined' while the dialog was still up, and their Approve then
hit a dead entry ('no such pending confirmation', 13:37:12 log). Nothing
executed — the gate failed safe — but the UX was a lie in both directions.
- CONFIRM_TIMEOUT 120s→300s: human-speed per T-13-51's own rubric.
- ContextBroker dispatches aiui:tool-confirm-expired when a pending action
vanishes node-side (poll) or the turn ends; Chat.vue closes the modal on
it. Same host-only CustomEvent discipline; iframe has no path to it.
- Two new tests; 21/21 green across toolConfirm + chatAiuiEmbed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mesh right panel: a >=2560px screen hid the tab bar and stacked all five
tool panels in fixed grid rows. On a real display that clipped the Bitcoin,
Dead Man and AI headings to a few pixels each, letterboxed the map, and
pushed Radio Settings into a scroll — more screen producing a worse view.
Very wide now uses the same tabbed column as every other desktop width,
with the selected panel filling the column and the map running edge to edge
(it is the one panel with nothing to scroll).
Session policy: idle timeout, absolute cap and a re-prompt-for-funds flag,
persisted and clamped. Two tokens already existed — a session token and a
30-day login token — so the knob changes how long a quiet tab stays usable
without putting a long-lived credential on every request. Kiosk screens are
exempt from the idle timeout (nobody is there to log a TV back in) but keep
the absolute cap so a stolen box does not stay authenticated forever. The
cap is not optional theatre: idle alone never fires on a polling dashboard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm run build runs vue-tsc -b (project references, noUncheckedIndexedAccess),
stricter than the flat --noEmit used during Task 2 verification: indexed
CustomEvent accesses need non-null assertions, the suspended-chat Promise
needs an explicit <unknown> ctor, and one unused import. 41/41 still green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executor stopped deliberately for a session restart (bypass-permissions relaunch).
Executor's final report: 'cargo test assistant confirm-gate suite 28/28 green,
individual nonce test passes; committing Task 1 next — first verify the
tools.rs/grants.rs/backends diffs are formatting-only.'
Task 1 (D-07/D-11 confirm gate, backend) is implemented and test-green but this
checkpoint is verbatim-uncommitted-state, NOT the reviewed atomic Task 1 commit:
continuation executor should verify diffs, then reset --soft or commit-on-top
into proper feat(13-08) task commits. Task 2 (ToolConfirmModal.vue trusted
chrome, Chat.vue + contextBroker.ts wiring) is partially built, tests written.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leads with the honest version: five screens were open and the previous
release's own audit reported them as fine, found by scanning from another
machine rather than asking the node. States plainly that what leaked was
the page, not credentials — the macaroon path was verified, not assumed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leads with what changes for the operator: app screens now require the node
password across LAN, Tailscale, mesh and Tor; the wallet/protocol ports that
must stay open stayed open; the mesh leak found during on-node verification;
nodes repairing their own legacy containers; and the signing-key rotation.
Known gaps disclosed, including the eleven still-undeclared ports and that
non-browser clients will now meet the login page.
The new block uses <strong> rather than the literal ** markers in earlier
entries, which render as asterisks in the modal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Content-Security-Policy header to both nginx `location /aiui/`
blocks whose connect-src is scoped to the AIUI path prefix, so AIUI's
own JavaScript is browser-prevented from issuing a same-origin fetch
to /rpc/v1 with the ambient session cookie. Explicitly rejects the
`sandbox` iframe attribute (allow-scripts + allow-same-origin is the
known escape; dropping allow-same-origin breaks AIUI's storage and
its origin-checked bridge) and records why in both the nginx comment
and a new comment above the Chat.vue iframe. Adds
referrerpolicy="no-referrer" to the iframe so a media URL or page path
never leaks upstream via Referer.
Also adds an explicit `location /aiui/api/openrouter/ { return 404; }`
to both server blocks, closing 13-02's Task 3 checkpoint finding
(operator-accepted deviation 2026-08-03): the relay was already
structurally gone but the SPA catch-all served 200/405 instead of 404.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lane merged main at 0c4826f8, one commit before 0de67ca6 added
auth/auth_rationale to PortMapping's test constructors in prod_orchestrator.rs.
That left the lane unable to compile ANY test in the archipelago crate, which
is why 13-05 could not observe its 13 tests pass (window 19). Not a defect in
this phase's work — just staleness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
# core/archipelago/src/main.rs
The release gate requires every CHANGELOG version to have a matching
block in Settings > What's New. Generated by scripts/sync-whats-new.py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- aiui-protocol.ts: AIUIContentRequest (kind + optional scope, no RPC
method or params) and ArchyContentPush (adapted bundle + permitted
flag).
- contextBroker.ts: handleContentRequest gates on the media/files
permission categories (either grants access), resolves scope to
content.list-mine / content.browse-peer (fanned out across every known
federation peer) / content.owned-list, and routes results through
archyContentAdapter's adaptContentItems before crossing the iframe
boundary. contentRequestSeq is a monotonic guard: a stale RPC response
that resolves after a newer content:request has started is discarded
rather than posted (AIUI-03 concurrency edge).
- contextBroker.test.ts: permission-denied, own-scope, and stale/
out-of-order coverage. Fixed a pre-existing latent flake risk in this
file — perms.toggle() is not idempotent across tests because the
permissions store persists to localStorage, which vi.clearAllMocks()
does not reset; switched the new tests to perms.enableAll().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- archyContentAdapter.ts: hand-written adaptContentItems mapping (D-12),
fixture-pinned at the adjacency, empty, ordering and paid-lock edges
named in AIUI-03; classifyByMime covers the m4a/aac/opus/wma extension
gap ShareModal.vue's mime map leaves today; buildMediaUrl never puts a
credential in a query string (T-13-32).
- filebrowser-client.ts: streamUrl now returns a query-free same-origin
raw-file URL, relying on the path=/ cookie login() already sets instead
of also putting the JWT in the URL (T-13-39 — closes the pre-existing
leak CONTEXT.md names, rather than merely not repeating it).
- filebrowserStreamUrl.test.ts: regression pin for the fix, including a
traversal case confirming sanitizePath behavior is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cmd/Ctrl+K search could only match text against known screens; anything it
did not recognise dead-ended at 'No results'. That text is now handed to the
assistant instead: a blue accented row (chat-bubble + sparkle) appears while
there is a query, always last in the keyboard order, so Cmd+K -> type -> Enter
reaches AIUI without the mouse. On a zero-match query it is the only option.
The prompt travels by postMessage, NOT as an iframe URL param. Chat.vue's
aiuiUrl is deliberately free of reactive dependencies so the iframe src stays
byte-identical and AIUI survives a tab switch (see the D14_FLAGS comment);
threading the question through the URL would reload AIUI and discard the
conversation on every ask — the opposite of the intent. Two regression tests
pin this: the src is byte-identical across an ask, and ask/askedAt are
stripped afterwards so a refresh cannot silently re-ask.
The ask is queued and flushed on AIUI's 'ready' handshake, because arriving
from Cmd+K on a cold Chat tab means the iframe has not connected yet.
AIUI-side receiver lands separately; until then this posts a message AIUI
ignores, which is inert rather than broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A vertical line crossing both dashboard cards, appearing at random on
hover and hard to catch deliberately.
Diagnosed from the screenshot rather than by reproduction. Decoding it
and scanning column by column found a lone brightness step at CSS x=633
that never returns — every legitimate container edge in the page shows
up as a PAIR of steps 2px apart (the card borders at CSS 255, 288, 850,
875, 1437), so an unpaired one is not a border. Sampling by region
placed it inside the cards and nowhere else: 10/13 rows inside My Apps,
11/11 inside Wallet, 2/10 in the gap between them, 2/13 above them. Same
screen x in both cards, which means the boundary lives in screen space
and cuts whatever backdrop-filter surface it crosses.
style.css already neutralises backdrop-filter for the shared glass
classes inside the dashboard's animated perspective/scroll containers,
because Chromium/Brave mis-rasterise it there — that block was written
for the black-rectangle corruption. `.home-card-shell` declares its own
`backdrop-filter: blur(18px)` in Home.vue and was never added to the
list, so it was the only unmitigated blur surface on the dashboard.
That is exactly the set of pixels the seam appears in. A hover repaint
re-rasterises part of the backdrop, and the refreshed half meets the
stale half at the damage boundary.
Adding it to the existing list also makes the shell consistent with the
tiles beside it: its fill is already rgba(0,0,0,0.65), the same as
.glass-card, which renders unblurred here.
The list is hand-maintained, which is how this shipped — a component
declaring backdrop-filter in its own <style> is simply not covered and
nothing fails. So the fix comes with a test that parses Home.vue for
locally-declared backdrop-filter rules and asserts each is in the
mitigation list. Verified it catches the real bug: reverting the
one-line fix makes it fail naming `.home-card-shell`.
Tests: 3/3 new, vue-tsc clean, mitigation confirmed in the built CSS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings AIUI's full 230-commit history under aiui/ via git subtree, plus main's
current head. Operator decision 2026-08-03: AIUI moves into this repo rather
than staying at git.tx1138.com. This also lands e30ac1d (13-01 Task 3), which
was stranded local-only while that remote was unreachable.
Plans 13-06, 13-09 and 13-11 still target /home/archipelago/Projects/AIUI paths
and must be re-planned against aiui/ before wave 2 runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>