Commit Graph
21 Commits
Author SHA1 Message Date
archipelagoandClaude Opus 5 73813e4738 fix(aiui): a context gather that hangs must not strand the request
Reported: "`files` context request times out". sanitizeFiles makes three
sequential calls into the File Browser app — login, getUsage, listDirectory
— wrapped in a try/catch. A catch only sees a REJECTION. A socket that
connects and then says nothing leaves the promise pending forever, so
handleContextRequest never posts a `context:response` and the AIUI side sits
until its own bridge timeout instead. The File Browser is a plausible source
of exactly that: on this node `/app/filebrowser/api/resources/` does not even
route (404), and its session-cookie path is the subject of a separate open
bug.

The guard goes at handleContextRequest rather than inside sanitizeFiles, so
no category — present or future — can strand the bridge. `files` is merely
the one with three network hops today; sanitizeSystem is also async.

withTimeout resolves rather than rejects, because the caller's one job is to
always answer, and a rejection would just relocate the problem into a catch.
A late null is safe by the protocol's existing shape: the AIUI reader
already treats a response with no usable data as "nothing to show", the same
as an empty category.

Two tests: a never-settling File Browser still produces a
`context:response`, and a healthy category still returns real data rather
than being flattened to null. 27/27 contextBroker, vue-tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 09:18:44 -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 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 762c72b4d0 feat(ai): AI Data Access grants live on the node, not in localStorage
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>
2026-08-06 20:47:40 -04:00
archipelagoandClaude Opus 5 aac81503c3 fix(13-11): content grid dropped everything except music
The AIUI-03 stale-response guard used ONE counter for every content:request,
so requests for different kinds cancelled each other.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:45:27 -04:00
archipelagoandClaude Fable 5 686616375c fix(13-08): confirm-response listener TTL follows the 300s gate timeout
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>
2026-08-05 18:36:03 -04:00
archipelago d25aea125f feat(13-11): wire requestArchyLibrary + fire content/library fetch from a live init-time event (GAP-FOUND)
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.
2026-08-05 18:29:17 -04:00
archipelagoandClaude Fable 5 2d1f09d8c8 fix(13-08): declined actions never re-prompt + timeout chain covers the human wait
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>
2026-08-05 14:50:17 -04:00
archipelagoandClaude Fable 5 31f9a4d5bf fix(13-08): confirm timeout 120s→300s + chrome closes an expired dialog
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>
2026-08-05 13:56:49 -04:00
archipelagoandClaude Fable 5 cfbbd268e5 test(13-08): satisfy vue-tsc -b build-mode checks in test files
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>
2026-08-05 12:05:02 -04:00
archipelagoandClaude Fable 5 fc09d7a292 wip(13-08): checkpoint before operator session restart — Task 1 GREEN (28/28), Task 2 in progress
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>
2026-08-05 11:34:31 -04:00
archipelagoandClaude Opus 5 ce7a2b2cd4 feat(13-06): content:request/content:push channel on the existing bridge
- 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>
2026-08-03 19:36:50 -04:00
archipelagoandClaude Opus 5 0ab9bdc7d3 feat(13-01): neode-ui carries chat over the existing origin-checked bridge
Adds chat:request/chat:response to the AIUI postMessage protocol
(AIUIChatRequest, ArchyChatResponse) and a handleChatRequest handler in
contextBroker.ts that calls assistant.chat over rpcClient on the page's own
session, then posts the result back through the existing postToIframe
helper. Reuses the broker's existing allowedOrigin guard unchanged — no
second postMessage channel, no relaxed origin check.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:37:41 -04:00
DorianandClaude Opus 4.6 1697af725b test: achieve 80%+ branch/function coverage on frontend logic (E2E-03)
515 tests across 38 files. Branch coverage 88%, function coverage 83%
on testable logic (stores, composables, api, utils, services, router).

New test files: websocket, useLoginSounds, useMobileBackButton,
useControllerNav, routes. Extended: rpc-client (99.5%), container store
(100%). Fixed: useNavSounds AudioContext mock, type errors across tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:18:37 +00:00
DorianandClaude Opus 4.6 589adb8b18 fix: alpha release hardening — onboarding, security, and ISO build
- Convert "Choose Your Path" screen to informative (read-only cards)
- Harden "Choose Your Setup" (gray out Coming Soon options, auto-select Fresh Start)
- Auto-fetch DID on mount with retry and auto-advance after success
- Improve backup download for mobile compatibility
- Add retry logic to verify step with graceful skip option
- Route verify → done → login for complete onboarding flow
- Add AIUI install confirmation via custom event (SEC-001)
- Add file path whitelist for AIUI file access (SEC-002)
- Add log redaction for container logs sent to AIUI (SEC-003)
- Add Secure flag to session cookie in production (SEC-004)
- Fix ISO build script to handle zstd compression errors gracefully
- Sync archipelago.service from live server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:00:28 +00:00
DorianandClaude Opus 4.6 1bb72dc87e feat: add AIUI node capabilities — file reading, log tailing, bitcoin/lnd deep data
Add readFileAsText() to filebrowser client, read-file and tail-logs action
handlers to context broker, bitcoin.getinfo and lnd.getinfo RPC enrichment
for context categories, and update AIUI protocol types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:50:40 +00:00
DorianandClaude Opus 4.6 36627ae1ac feat: wire context broker files category to FileBrowser, fix media state check
- sanitizeFiles() now fetches real data from FileBrowser (usage, folders, recent files)
- Fixed media state check to include 'running' and 'stopped' states, not just 'installed'
- Removed unused bottomPosition variable in CloudFolder.vue

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:09:45 +00:00
DorianandClaude Opus 4.6 7b56927c3c feat: complete AIUI integration — all 31 overnight tasks
- Protocol: 10 context categories (apps, system, network, bitcoin, media, files, notes, search, ai-local, wallet)
- ContextBroker: real data wiring for all categories with sanitization
- Permissions: user toggles for all categories in Settings
- Nginx: Claude API, OpenRouter, SearXNG proxy pass-through
- Actions: launch-app, search-web, install-app handlers
- Chat.vue: loading state + connection indicator
- Integration test page: test-aiui.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:34:02 +00:00
DorianandClaude Opus 4.6 584ce646e1 feat: AIUI chat mode integration with iframe, context broker, overnight loop
- Chat mode: AIUI loads in sandboxed iframe at /dashboard/chat with transparent bg
- Mode switcher: Easy + Pro tabs only, Chat is a launcher button
- Keyboard shortcuts: Cmd+1 (Easy), Cmd+2 (Pro), Cmd+3 (Chat), Cmd+M (cycle)
- Directional transitions: chat slides from/to left, dashboard from/to right
- Context broker: postMessage protocol for quarantined AIUI communication
- AI permissions store: user-controlled toggles for data access categories
- Settings UI: AI Data Access section with per-category toggles
- AIUI container manifest and nginx proxy config for /aiui/
- Deploy script builds AIUI with /aiui/ base path
- Overnight loop infrastructure (loop.sh, prepare.sh, plan.md, prompt.md)
- Security hooks for autonomous overnight runs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:06:20 +00:00