Commit Graph
2977 Commits
Author SHA1 Message Date
archipelagoandClaude Opus 5 adc3c444cd fix(container): an absent container is not proof of uninstallation
installed_app_ids judged installation on live containers alone. Watched on
archi-dev-box within the hour: lnd read as ABSENT, then as EXISTS again.
Containers on this node come and go — the boot reconciler logs
"previously-running app has no container after boot — recreating" for
bitcoin-knots and electrumx repeatedly — so a momentary gap looked exactly
like a removal, and the reaper would have taken a healthy companion's unit
with it. ORPHAN_GRACE narrows that window but cannot close it: nothing
bounds how long a gap lasts.

An app now counts as installed if its container exists in any state OR its
container name is in the durable last-running snapshot. That snapshot is
what crash_recovery itself calls "installation evidence" and what
reconcile_all_with_mode already trusts to recreate a previously-running app
whose container vanished — the same signal, for the same reason, now shared
rather than reinvented.

Only fedimint is a true orphan on this box: it appears in no adoption list
and has no quadlet unit of its own. lnd is installed and merely flapping,
which is a separate bug.

Container suite 215/215.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 07:44:05 -04:00
archipelagoandClaude Opus 5 7567c25c00 fix(aiui): stop fighting the virtualizer over scrollTop
"Failed to scroll to index N after 10 attempts" appeared in the console on
every send. It was not a real failure — it was two scroll controllers
arguing.

scrollToBottom() called virtualizer.scrollToIndex(last) AND then assigned
el.scrollTop on the next tick. scrollToIndex runs a retry loop that nudges
scrollTop toward the target row's measured offset and re-checks, up to ten
times, because dynamically-measured rows move the target as they settle.
The manual assignment overwrote each nudge, so the loop never observed
itself converge and always exhausted its attempts.

For "go to the end" the index-settling machinery buys nothing: scrollHeight
already is the bottom, the virtualizer renders whatever window that offset
implies, and it keeps working while a response streams and the last row
grows — the case the manual fallback was added for in the first place.
scrollToMessageIndex still uses scrollToIndex, which is the right tool for
jumping to an arbitrary row.

Console-only change; needs a device check that the chat still pins to the
bottom while streaming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 07:08:28 -04:00
archipelagoandClaude Opus 5 3ac59a73b3 fix(container): companions follow installed apps, not available manifests
archi-dev-box was running archy-fedimint-ui and archy-lnd-ui with no
fedimint and no lnd container anywhere on the box. The Fedimint Guardian
UI sat on :8175 serving its "waiting for Bitcoin" page forever with
nothing behind it, which is what the operator reported as "fedimint
guardian installs but does not work" — there was nothing to install, the
UI was already up.

The boot reconciler drove companion provisioning from manifest_ids(),
which is every manifest the node can SEE: the whole apps/ directory plus
the signed-catalog overlay, 56 of them. The app reconciler has drawn this
line since phase 3 (ReconcileMode::ExistingOnly, "merely listing a
catalog manifest never installs an unqualified app"); the companion stage
never got the equivalent guard, so it stood up a UI for every app that
merely had a manifest and then self-healed it forever.

The other half is that reconcile() could only ever ADD. remove_for fires
only on the explicit uninstall RPC, so nothing ever subtracted: an
install that failed after its companion landed, or a container removed
by any other route, left a Restart=always unit alive permanently.

- installed_app_ids() replaces manifest_ids(): app ids whose container
  actually exists. Returns Option, because a caller that removes things
  on absence must not read "I could not look" as "nothing is installed".
  Container presence in ANY state is the whole test — it deliberately
  does not inherit the user_stopped/disabled filters, since a stopped app
  is still an installed app and treating it otherwise would tear its
  companion down and rebuild it on the next start.
- manifest_ids() is deleted rather than left unused. Its contract reads
  as "installed" to anyone skimming, which is the whole bug.
- reap_orphans() removes companions whose backend is not installed, after
  ORPHAN_GRACE (300s). The grace period is required, not defensive: this
  node runs ARCHIPELAGO_USE_QUADLET_BACKENDS=true and a Quadlet app is
  briefly containerless while restarting, so reaping on the first absent
  tick would cost a healthy companion a teardown plus a possible 900s
  image rebuild. A backend that reappears clears its clock.
- Reap failures are logged but kept out of the backoff input. Repair
  keeps a companion available; reaping only tidies one away, and a wedged
  reap must not back the repair path off to its 1h ceiling.

Every uncertain signal resolves toward not removing: no unit file and a
hung is-active reads as leave-it-alone.

Container suite 215/215.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 07:08:17 -04:00
archipelago 455b813630 docs: late resume artifact — full-day state, #7 status, today's traps 2026-08-07 22:25:43 -04:00
archipelagoandClaude b9e64eb619 fix(container): drift-gate the per-app ownership-repair hooks
The reconciler's pre-start hooks for the btcpay stack, fedimint and fmcd
chowned unconditionally on EVERY prepare — and prepare re-runs far more
often than install (every reconcile that touches the app). archi-dev-box's
journal showed the same three dirs re-chowned every ~15s. The hooks exist
to repair old installs; they now skip when ownership is already correct
(root stat probe — the daemon's rootless metadata read can't see the
subuid-owned dirs).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 22:06:01 -04:00
archipelagoandClaude b886930708 fix(container): ownership probe uses systemd-run with output capture
The first drift-gate attempt called plain sudo stat, which the daemon's
privilege path doesn't answer — the probe silently failed and the chown
loop continued. host_sudo_output mirrors host_sudo (systemd-run --pipe)
but returns the process output, so the ownership check gets a real answer.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 20:39:33 -04:00
archipelagoandClaude db8937f9e9 fix(container): root stat fallback makes volume ownership drift authoritative
The direct metadata read can be denied in the service's rootless context even
when the directory is already correctly owned, which kept the reconciler
calling sudo chown on the same Postgres volume every minute. A root
fallback gives the guard a reliable answer on deployed nodes while remaining
much cheaper than a recursive chown.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 19:59:48 -04:00
archipelagoandClaude 3cd210f282 feat(build): build-aiui.sh rejects a prod bundle carrying mock hosts
W1.7's regression gate: after 8329b826's tree-shake fix, this makes the
mock-quarantine load-bearing — a future change that reintroduces the mock
modules into the production graph fails the build instead of shipping
silently. The demo-site build (VITE_DEMO_CONTENT=true) is exempt by design.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 19:21:55 -04:00
archipelagoandClaude 7c7cd76c1c fix(package): btcpay wipe removes the whole stack's data, not just its own dir
get_data_dirs_for_app had no btcpay arm — the default mapped to
/var/lib/archipelago/btcpay alone, leaving postgres-btcpay (where the
ACCOUNT lives) and nbxplorer on disk. Uninstall-with-wipe then reinstalled
to the old account still enabled. The btcpay arm now covers all three dirs,
for every alias and stack-member id. The map stays deliberately hardcoded:
deletion code must never derive its targets from a manifest at uninstall
time (a bad manifest could aim the wipe at another app's data).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 19:17:24 -04:00
archipelagoandClaude ca106c5a43 fix(container): data-uid chown is drift-gated, not unconditional every tick
apply_data_uid ran a recursive sudo chown on every prepare_for_start, and the
reconciler re-prepares — archi-dev-box's journal showed postgres-btcpay rechowned
every ~45s despite already-correct ownership, and on framework-pt the same loop
surfaced as operator-visible 'chown failed' noise. chown_for_rootless_container
now stats the target first and returns early when the top-level owner already
matches the host-mapped uid:gid. Deep drift in a running container is still
caught by ensure_running_container_ownership's in-container write-probe, which
is the authority that actually matters (it probes writability, not stat bits).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 18:54:39 -04:00
archipelagoandClaude 494400df29 docs(13): fill 13-VALIDATION map + write 13-UAT on-device acceptance record
13-15's two artifacts. The map names plan/wave/threat-ref per row with
today's measured results (Rust assistant suite 130/130; adapter 37; broker
25; toolConfirm, audioPlayer 11, appsConfig 13; AIUI 353/356 with the three
documented pre-existing fixture failures). Four close-out rows added
(S-invariants, evals, egress, mock-free-bundle grep). Manual-only table
discharged except the physical-handset pass, which 13-UAT records as owed
(the AIUI-06 flagged assumption wants both, and only devtools-mobile is on
record). Open questions 1/2/4 resolved with plan cites; Routstr stays
honestly open.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 17:52:22 -04:00
archipelagoandClaude 482c4e300e fix(bootstrap): self-heal stale /aiui/api/web-search proxy to the gated daemon
Live on archi-dev-box: the node still proxied web-search straight to
SearXNG :8888 unauthenticated — the repo conf was fixed in d0c9ea6e but
existing nodes' /etc/nginx/sites-enabled never gets rewritten by a source
edit. Added to the nginx self-heal battery: stale 8888/search proxy_pass →
session-gated 5678 with the Cookie forwarded (heal_stale_web_search_block,
pure + idempotent + tested). Fresh ISOs already ship the gated block.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:54:38 -04:00
archipelagoandClaude 4361a5cba7 feat(mesh): '!ai' over mesh runs the assistant's shared tool loop
Mesh AssistQuery answered with a bare LLM call — no tools, no actions.
The CallerScope::Mesh variant was designed for this wiring ('the variant
exists so the shape is right when a future plan wires mesh callers into
the shared loop'); this is that plan. A trusted/allowlisted asker's prompt
now runs assistant::chat with CallerScope::Mesh { authorized } — the
operator's persisted grants cap what the model may touch (never wider),
and writes suspend on the node's own confirm gate. The reply is capped
for airtime as before, with a brevity instruction for mesh turns.

Wiring follows the blob_store pattern: RpcHandler::set_mesh_service (now
&Arc<Self>) forward-propagates an Arc<RpcHandler> into the mesh state's
new assistant_handler slot; absent (early boot) falls back to the legacy
bare-LLM answer.

Test: mesh_caller_authority_is_capped_at_operator_grants.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:54:12 -04:00
archipelagoandClaude 34085e30a2 fix(iso): pin wget to v4 AFTER its package installs — conffile prompt is fatal otherwise
Build #189 proved the IPv4 fix works (downloads all succeeded) and proved
my own edit wrong: appending inet4_only to /etc/wgetrc before wget's
package landed made dpkg's conffile prompt hit EOF, leaving wget and
debootstrap unconfigured. Moved the pin below the apt install.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:49:03 -04:00
archipelagoandClaude e36f36ee09 perf(aiui): chat backdrop 1052K → 478K webp (was the 'ages to load' report)
The 2912×1632 jpg painted visibly slowly over Tailscale/Tor. 1920w q82
webp is visually identical behind glass and under half the weight.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:06:45 -04:00
archipelagoandClaude a2254648ad fix(iso): force IPv4 for every package retrieval in the build
This box (and its containers) blackhole IPv6: deb.debian.org answers AAAA
first, wget tries v6 until debootstrap's per-package timeout, and the
installer-env stage died twice today with 'Couldn't download packages'.
Probed in a debian:trixie container: v4 OK, v6 hangs. inet4_only for wget
(covers debootstrap) + Acquire::ForceIPv4 for every apt-get, including the
chroot and the rootfs Dockerfile stages.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:55:18 -04:00
archipelagoandClaude eaf0f07346 fix(ui): #ai-data-access anchor exists, so the banner's Settings button lands
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:01:12 -04:00
archipelagoandClaude 7686a486ec feat(assistant): app_install/app_uninstall tools + S6 replay fix
Two changes, one binary batch:

1. app_install/app_uninstall (task 3): '!ai please install bitcoin knots'
   correctly said it can't. Both tools are category-Apps, destructive, and
   ride the 13-08 confirm gate (node-authored descriptions added). Install
   validates catalog membership BEFORE the dialog (a typo never spends an
   approval); uninstall resolves installed ids. Both reach the SAME
   package.install/package.uninstall spawns every authenticated caller
   uses, via a curated Arc-taking sibling of assistant_dispatch_tool.

2. S6: cloud legs no longer strip prior USER turns from replayed history.
   Turn-minimality's allowlist is now the whole conversation's operator
   turns (the node's own D-08 transcript, same trust class as this turn),
   still mechanically matched, B1 secret scan and 64KB cap unchanged,
   fabricated user messages still truncated. The model no longer sees its
   own answers without the questions.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:00:49 -04:00
archipelagoandClaude 2787a9bbfc fix(ui): unknown app ids fall back to the A mark, not a 404 png guess
resolveAppIcon's final arm guessed /assets/img/app-icons/<id>.png — strfry
404'd live. DEFAULT_APP_ICON already existed; the chain now ends on it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:48:55 -04:00
archipelagoandClaude 31fd789b55 fix(aiui): hide zero-value metadata — no more '★ 0 · 0m' (W1.5)
Node-derived cards have no rating/year/runtime/director; rendering the
defaults read as '★ 0 · 0m' beside an empty string. FilmGrid, FilmDetail
and SongGrid now gate those spans on real values (FilmCard, Book and Place
components already did). Panel empty states were made honest in 9abc1623
('Nothing found'), so this closes the rendering-honesty item.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:36:44 -04:00
archipelago 92a8dff6b1 docs(13): tick the open task list with session-2 verified state 2026-08-07 14:30:54 -04:00
archipelagoandClaude 8329b826b1 fix(aiui): mock libraries drop out of the production bundle (W1.4)
Every mock consumer is now gated on the demo flag inline (canonical Vite DCE
idiom — the cross-module DEMO_CONTENT_ENABLED const defeated folding). But
the real leak was films.ts's module-level allGenres/allSources exports:
[...new Set(mockFilms.flatMap(...))] is unprovably pure, so the treeshaker
kept the whole module — array, plex:// and cloud.example.com hosts and all —
even with zero live references. The mocks directory is now declared
side-effect-free in vite.config (they are pure data by design), so unneeded
mock modules actually drop.

Verified: clean dist build → entry bundle AND dist-wide grep show zero
mock hosts (spotify/track/example, cloud.example.com, plex://, tmdb image
host). Demo/dev builds (VITE_DEMO_CONTENT=true or import.meta.env.DEV) keep
the full pack. Tests: 353/356, failures are the three documented
pre-existing ones.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:06:59 -04:00
archipelagoandClaude e669a3e436 fix(iso): AIUI stage picks the NEWEST candidate and copies with --delete
RC1's aiui/index.html pointed at the stale checked-in demo bundle while
today's dist sat beside it unreferenced: demo/aiui was tried first, and the
rsync without --delete merged it over the fresh capture from /opt. Now the
newest index.html across all candidates wins (demo/aiui remains the
fresh-clone fallback) and the copy deletes before writing.

Found by mounting the RC1 ISO and diffing bundle hashes against the tree —
exactly the 'verify the frontend INSIDE the ISO' rule.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 11:55:24 -04:00
archipelagoandClaude 6815a7d1eb fix(assistant): disabled tools are listed and callable — the gate's refusal IS the Settings signal
Live evidence, two ways: the 9abc1623 banner never fired because D-16 hides
ungranted tools (model never calls → refused_categories always empty), and
the [[needs:id>]] marker fix failed because a small local model answers with
a workaround narrative instead of emitting structured markers.

The model's reliable, trained behavior is tool CALLING — so disabled tools
are now listed in a DISABLED prompt section and remain in the schema. A call
hits the execution gate, which refuses and records the category → the
trusted chrome offers Settings → AI Data Access. Deterministic and
model-independent. The prompt split is UX/attack-surface shaping; the
security boundary remains the server-side grant re-check in execute_tool
(loop_.rs), unchanged and now the single enforcement layer by design.

Tests: ungranted_tool_only_ever_in_disabled_section (section-aware),
disabled_tools_are_listed_as_callable_but_refused, marker extraction kept
as a harmless safety net. 127/127 assistant suite green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 11:51:02 -04:00
archipelago 5e71f256e4 docs: resume artifact — session 2 state (banner fix in flight, ISO cut) 2026-08-07 11:49:46 -04:00
archipelagoandClaude f15e2b5025 fix(assistant): a disabled-category request now fires the Settings offer
The 9abc1623 banner waited on refused_categories, but refused_categories
only fills when the model CALLS a gated tool — and D-16 hides ungranted
tools from the prompt, so the model never calls: it answered 'I can't do
that' in prose and the banner never fired. Live-verified: revoke media,
ask for content, no banner.

- build_system_prompt takes the disabled categories and teaches a marker:
  'say it can be switched on in Settings → AI Data Access and end with
  [[needs:<id>]]' — category names only, never tool names (D-16 holds)
- extract_needs_markers strips the markers from the reply and folds them
  into refused_categories; unknown ids pass through as text (an offer is
  the worst a bad marker can cause — never a grant)
- egress's seed-screen test now covers the new paragraph too

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:54:35 -04:00
archipelagoandClaude c2e71bc7e5 fix(aiui): play the node's own files before reaching for the network
usePlayer.play() never looked at song.sources[] — a real library track went
straight to (CSP-blocked) Wavlake and reported 'Not found on Wavlake' while
its bytes sat on the operator's disk. Node sources (same-origin /content/<id>,
Range-streamed) now play first; Wavlake is the metadata-only fallback.

FilmDetail likewise only played YouTube sources; own/peer/IndeeHub sources
(same-origin, media-src 'self') now win, YouTube stays the free-films
fallback.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:31:57 -04:00
archipelagoandClaude 7d57e2c39d fix(aiui): recommendation previews render in buckets the node left empty
The archy content latch was global and permanent: once mount-time content
latched archyContentActive, no extracted [[film_ext:…]] recommendation card
could ever render again, and an empty tool result kept the latch — so a
'recommend me films' turn beside an empty catalogue showed prose only, with
'Nothing found' overwriting nothing. The chat had lost its rich previews.

- setArchyContent records which buckets the node actually supplied
  (archySupplied) and latches active only on a non-empty delivery
- updatePanelFromText's no-overwrite guard is now per-bucket: node truth
  wins buckets it filled; empty buckets stay writable for extracted previews
- the extraction fallback title no longer clobbers 'Nothing found'/'Loading…'

Regression tests: previews render in an empty bucket, node truth survives
tags in a filled bucket, 'Nothing found' survives a both-empty turn.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:16:52 -04:00
archipelagoandClaude a91bc55df8 fix(assistant): surface shape bugs — films scope mime hint, apps_list items wrap
Two reasons the content surface 'often doesn't surface the content':

- content.indeehub-projects items carried no mime/filename, so the UI
  adapter classified every film 'excluded' and the films grid could never
  render. They are films: they now declare video/mp4.
- apps_list surfaced the container-list RPC's BARE ARRAY; the broker reads
  { items: [...] }, so the apps grid was silently dropped every turn.
  Wrapped at the tool boundary — the shared RPC's own shape is untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:16:52 -04:00
archipelagoandClaude 1ac08a3ed3 feat(assistant): recommendations check the catalogue first and tag knowledge picks
'recommend me 10 scifi films' answered prose-only and OFFERED to check the
catalogue — the preamble invited knowledge recommendations (paragraph 2) but
only ordered tool calls for existence questions (paragraph 3), so the model
never ran the tool and emitted none of the tags the iframe renders as rich
preview cards.

- discovery of a kind the node could hold (films/music/books…) now gets a
  catalogue-and-peers check FIRST, knowledge picks on top
- the 'would you like me to look?' stall is banned outright — looking is
  one tool call, do it then answer
- the preamble teaches the exact [[film_ext:Title|Year|Director]] /
  tv_ext / song_ext / book_ext / podcast_ext formats, same-line with the
  one-line reason, real titles only, never for items the tool returned

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:13:47 -04:00
archipelagoandClaude 3f22e4375d fix(ui): strict-TS cast in peers per-onion grouping
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>
2026-08-07 08:45:46 -04:00
archipelagoandClaude c25fd8b650 fix(content): owner never pays for their own files; purchased serves from cache
- 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>
2026-08-07 08:40:49 -04:00
archipelagoandClaude f0d2cdb7b2 fix(aiui): node-content surfaces stay open when the reply text infers no tabs
Reproduced live: 'show me paid for peer files' returned surfaces=1 with 3
purchased images over the bridge, then updatePanelFromText set panelOpen
from the REGEX-inferred tab list — a plain markdown list matches nothing,
so the panel closed and the user saw prose only. panelOpen now follows
orderedTabs (Archy tabs lead). Regression test pins the exact turn.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 08:20:41 -04:00
archipelagoandClaude d0c9ea6e61 fix(security): session-gate web search + screen forwarder egress (S3/S4)
S4: /aiui/api/web-search proxied straight to SearXNG with no auth — anyone
reaching the web port ran searches attributed to the node's IP. Now routed
through the daemon's session-gated model proxy like the claude/ollama legs
(both nginx server blocks), forcing format=json upstream (the client never
sent it — search could 200 with HTML that parsed as nothing).

S3: the forwarder also serves the STANDALONE frontend, whose bodies carry
full history/images with no assistant loop behind them — a pasted seed
phrase went to Anthropic unscreened. The forwarder now runs the egress
secret-shape scan (G-B1) with the node's own secrets dir as deny corpus on
Claude bodies and search queries; blocked requests get a plain-language 400.

Also fixes a REAL gap in the egress tokenizer found by these tests: a JSON
key glued to a string value's first word ('content":"abandon...') dropped
that word, so an exactly-12-word seed pasted as a bare message yielded an
11-member run — checksum misses, backstop misses. Non-member words now
rescan within the token. egress 15/15 + model_proxy 10/10 green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 08:03:07 -04:00
archipelagoandClaude d746fbd812 fix(assistant): app_logs redacts credential shapes before model context (S5)
The browser broker redacted log lines (password=/token=/macaroon key=value,
64+ hex, 64+ base64) while the node-side tool only untrusted-wrapped — so a
log line carrying rpcpassword=<32-hex> crossed to cloud backends below the
egress screen's threshold. Port the broker's three patterns to the tool
boundary as a pure line redactor + JSON walker; unit-tested (124 assistant
tests green).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 07:12:48 -04:00
archipelagoandClaude 3a3ceb2238 fix(permissions): assistant grants file is the one authority (live desync fix)
ai_grants_unified UNIONED the assistant grants.json with the legacy
settings/ai_permissions.json on every read. On archi-dev-box legacy held
all-ten and grants.json held four, so the Settings UI and the AIUI frame
saw every category ON while the assistant refused six — and no UI toggle
could fix it, because both write paths existed but only ai.permissions.set
synced both files. Now: an existing grants.json answers alone; the legacy
file is consulted only when no grants file exists (pre-unification
upgrade), and that read migrates forward and persists the authority.
assistant.grants-set now also rewrites the legacy file in step. Regression
tests: authority is not widened by legacy; migration folds forward once.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 07:12:37 -04:00
archipelagoandClaude 9bb38c36d3 fix(aiui): fabricated demo content is demo-build-only, fake credential scrubbed (S7)
Per operator decision 2026-08-07 (mock content is isolated to
demo.archipelago-foundation.org, never in shipped code): the auto-seeded
'node-demo' conversation (invented balances, file listings, bitcoin.conf)
no longer ships on nodes — a VITE_DEMO_CONTENT build flag (or dev) gates
it, /seed, and the Guide 'Load Demo' button. The genuine onboarding guide
still seeds everywhere. The fixture's bitcoin.conf rpcpassword is now
unmistakably example-shaped: fake must never look like a real credential.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:28:51 -04:00
archipelagoandClaude 4e6df488fc fix(aiui): Claude API key never persisted in plaintext again (S2)
The key rode the wholesale settings→localStorage save, sitting at rest
readable by any same-origin script, while the AES-256-GCM key-vault built
for exactly this sat bypassed. Now: the key lives in a memory-only store
ref, persists only into the encrypted vault when a passphrase session is
active (migrating into the vault on unlock), and a one-time migration lifts
any existing plaintext key out of localStorage and re-saves the scrubbed
settings object immediately. Settings UI reports honestly how the key is
held. Typecheck clean; test suite unchanged (348 pass, 3 pre-existing fails).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:28:51 -04:00
archipelagoandClaude a3edb848e1 docs(13): correct peer-files finding per bca18c03 — code bugs, not fleet outage
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:14:15 -04:00
archipelagoandClaude 9f579162d1 fix(assistant): network_status strips WAN IP + Wi-Fi SSID from model context (S1)
The Network permission's Settings label promises 'no IP addresses', and the
browser-side broker honours it — but the node-side tool forwarded
network.diagnostics verbatim, so a granted Network category sent the node's
WAN IP and SSID (both location-identifying) to cloud model backends. Strip
both at the tool boundary; NAT/UPnP/Tor/DNS connectivity shape stays.
Pure helper + unit test (123 assistant tests green).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 06:14:15 -04:00
archipelagoandClaude Opus 5 c9de1e6c53 wip: phase 13 AIUI paused at 6/17 (operator demo list)
Handoff carries the peer-files correction, the podman-lifecycle trap, the
concurrent-agent warning, and the release-binary drift that blocks the ISO.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 06:04:23 -04:00
archipelagoandClaude Opus 5 bca18c03d2 docs: resume artifact — AIUI surfaces, the peer-files correction, SearXNG
Carries the full task list with per-item status, the three commits'
rationale, the live measurements that overturned the earlier
peers-have-no-content conclusion, the browser-verification recipe, and
the binary-drift blocker that must clear before the ISO.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 06:00:54 -04:00
archipelagoandClaude 9cf1c12213 docs(13): operator decision — mocks isolated to demo site, not shipped code
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 05:53:59 -04:00
archipelagoandClaude a7368b8b1d docs(13): assessment plan tracks b1c5d138 + demo-mode decision for mocks
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 05:52:26 -04:00
archipelagoandClaude 1eb75a1ed9 docs(13): full AIUI assessment + four-wave fix plan (security/mission/content)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 05:49:43 -04:00
archipelagoandClaude Opus 5 b1c5d13850 fix(aiui): node content outranks the prose surface, and web search resolves
Browser-verified on archi-dev-box; all three were only visible by
driving the real UI.

The tab bar. `setArchyContent` put the node's grids up, then
`updatePanelFromText` replaced the bar with tabs inferred from the reply
text. "show me my own shared content" therefore landed on an "AI Brief"
— a prose restatement of the answer already on the left — with the
populated image grid no longer reachable. Guarding the panel arrays was
not enough: they held the right data while the tab bar had discarded the
way to see it. Archy tabs now lead, and the title follows the leading
tab. The prose stays; it just is not the only thing shown.

Tab order follows bucket size. A node with 13 photos and 2 tracks opened
on Songs and titled itself "2 Songs" for a 15-item answer.

Web search never worked embedded. `searchWeb` hardcoded
`/api/web-search` while every other call is built from BASE_URL. Under
`/aiui/` that asked the HOST for a path only the AIUI-scoped nginx
location serves, so it hit the node's API gate for a 403 and the CSP
refused the connection on top. Now BASE-relative. Additionally, the
embedded path skips the client-side search entirely: `streamViaArchy`
sends only the user's text, so the system prompt those results were
folded into is never transmitted — it was a round trip and a console
error per turn whose output provably reached no model. Web search for
the embedded path belongs node-side, with the other tools.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 05:48:39 -04:00
archipelagoandClaude Opus 5 c810b514ed fix(search): AIUI web search was 403ing on every query
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>
2026-08-07 05:07:10 -04:00
archipelagoandClaude Opus 5 9abc162394 fix(aiui): the content surface renders what the assistant found
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>
2026-08-07 05:00:54 -04:00
archipelagoandClaude Opus 5 f7c541e867 docs: measured IndeeHub content inventory — the library is empty
"What films are there from my peers" has three causes behind one answer, and
only one is being worked. The tool gap is in flight in a concurrent session
(content_list + SURFACE_TOOLS). Separately and unowned: AIUI declares six
context categories while the broker serves ten, so media/search/ai-local/notes
cannot be requested by AIUI at all — sanitizeMedia sits behind a door AIUI
cannot open, which is likely why the model claimed no capability rather than
reporting an empty library.

And the part neither fixes: measured with a real node-signed Nostr session
through the gate, /api/projects and /api/projects/private both return 0 items.
A correct "0 results" will be indistinguishable from a broken tool, so seed a
project or verify against a peer that has content before calling it done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 04:49:46 -04:00
archipelagoandClaude Opus 5 289c19443a docs: resume artifact for the AIUI demo — task list, findings, traps
Persists the 17-item session task list so it can be rebuilt in a fresh session
(the task tool is session-scoped and would otherwise evaporate), with what
shipped and what each remaining item actually is.

Records the findings that change expectations rather than leaving them to be
rediscovered: the 16 federated peers are not serving content so peers_reached 0
is correct, IndeeHub's catalogue is genuinely empty, two AI permission stores
existed for the same ten categories, and tailscaled owns :443 so nginx must
bind LAN addresses explicitly or it fails EADDRINUSE and silently keeps the old
config.

STATE.md's stopped_at points at it, so /gsd-resume-work lands correctly.

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