Commit Graph
2808 Commits
Author SHA1 Message Date
archipelagoandClaude Fable 5 a3521e5eeb feat(13-13): Routstr backend adapter — Nostr discovery, OpenAI chat, Cashu payment attach
Task 1 decision (proceed-docs-with-probe-first, operator-selected via
AskUserQuestion 2026-08-05): 13-ROUTSTR-FINDINGS.md observed 0 of 9 cited
protocol claims (no live provider was announcing on any of the 3 default
relays in a 30s window on 2026-08-03; relay reachability itself WAS
confirmed). This backend is written against docs.routstr.com's cited shape,
with the first live chat-completions call doubling as the capability probe:
a non-success HTTP status or a response missing the expected
choices[0].message shape fails loudly (bails with the real status/body)
rather than silently degrading.

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 01:26:59 -04:00
archipelago 8548544db9 docs(13-12): complete injection-boundary/egress-screen/rate-limit plan 2026-08-05 23:13:23 -04:00
archipelagoandClaude Fable 5 f1e50fbfd8 feat(13-12): G-B3 rate limit + read-only injection loop bound and owner notices
rate_limit.rs: assistant.chat gets its own request log keyed by
AUTHENTICATED SESSION (not client IP, per 13-AI-SPEC.md §6 G-B3's own
spec — an operator's session can roam across IPs within one sitting), on
the SAME EndpointRateLimiter struct rather than a second limiter type.
check_session/record_session_request enforce a hard ceiling (60/5min);
session_soft_threshold_reached (30/5min) is checked separately so the
call site can raise an owner notice before the hard refusal ever fires.
Wired into assistant_chat.rs's handle_assistant_chat (Rule 3 — the plan's
own declared intent, "assistant.chat is rate-limited per authenticated
session," has no other call site to reach the real RPC surface) and into
the existing 5-minute cleanup task in api/rpc/mod.rs.

loop_.rs: run_loop now tracks whether D-10-wrapped untrusted content is
present in context (seeded and re-checked as new tool results arrive
mid-loop), counts grant refusals split by that flag via
AssistantCounters::note_grant_refusal (a burst WITH untrusted content
raises a Security notice — something in shared content may be trying to
trigger actions; the same burst WITHOUT it raises a Ux/config notice
instead, so probing is never confused with misconfiguration, T-13-83),
counts turns-per-request, and counts MAX_TURNS-reached (3+ in one session
raises an owner notice) right before the loop's own bail — this is EV-13's
read-only injection loop, the one case the confirm gate structurally
cannot see because reads never confirm.

mod.rs: ToolExecCtx gains a `counters: Arc<AssistantCounters>` field
(defaulting to the process-wide global_counters(), overridable per-test via
with_confirm_gate_and_counters) so loop_.rs's counting has somewhere to
write and tests can assert against an isolated instance without polluting
concurrently-running tests.

read_only_injection_loop_terminates_and_is_counted (EV-13) and
grant_refusals_with_untrusted_content_are_a_security_signal (T-13-83) both
pass. Full `cargo test --package archipelago` (1211 tests) green — the
existing rate-limited RPC methods are unaffected by the new session-keyed
limiter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 23:09:51 -04:00
archipelagoandClaude Fable 5 fde7b1572d feat(13-12): G-B1/G-B2 cloud-egress secret scan and turn-minimality screen
assistant/egress.rs: screen_outbound(body, ctx) -> EgressVerdict runs on
every request body about to leave this node for a cloud backend. G-B1
scan_secret_shapes checks for macaroon-shaped hex runs, BIP39-length word
runs, ecash/Nostr-key-shaped strings, and the literal contents of files
under data_dir/secrets — a hit fails closed (BlockFallBackLocal), logging
only the match's kind, never the value. G-B2 assert_turn_minimal checks the
outbound body against a mechanical allowlist of this turn's own fields (the
user's turn, this turn's granted tool names, this turn's own tool results);
an unrelated earlier tool result or content wrapped for a different turn is
truncated out rather than eyeballed. An unparsable/ambiguous body also fails
closed. MAX_OUTBOUND_CONTEXT_CHARS caps body size independent of minimality.

Wired into backends/claude.rs's send() before the outbound HTTP request (on
a block, send() errors before anything is sent — Rule 3, outside this
task's originally-declared file list but structurally required to give
screen_outbound a real caller); never wired into ollama.rs — nothing leaves
the node on that leg, so paying the scan cost would be pointless.

mod.rs: AssistantCounters/OwnerNotice — grant refusals, validation
failures, turns-per-request, untrusted-content-present,
cloud-escalation-while-local-up, blocked-egress and MAX_TURNS-reached
counters, each raising an owner_notice() at its own AI-SPEC §7b threshold.
Local and owner-facing only: no exporter, no /metrics, no OTLP anywhere in
assistant/ or rate_limit.rs. backends/mod.rs's select_backend raises a
cloud-escalation-while-local-up notice when Ollama is reachable but its
configured model isn't tool-capable (Rule 3, same file-scope reasoning).

9/9 assistant::egress:: tests pass in this task's own isolated state
(Task 1's 56 plus these 9 — ToolExecCtx's counters field and its loop_.rs
call sites are Task 3's own commit, since nothing in this task's behavior
needs them yet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:22:15 -04:00
archipelagoandClaude Fable 5 265ba5ab19 feat(13-12): D-10 untrusted-content boundary — wrap_untrusted, per-call random token
assistant/untrusted.rs: wrap_untrusted(label, text) wraps peer-supplied text
(filenames, log lines, mesh/peer status) in a delimiter block whose token is
freshly randomized on every call via the in-tree rand crate — never a module
constant, never derived from content. A forged closing boundary using a
guessed/fixed token cannot terminate the real block early (EV-11).

tools.rs: wrap_tool_result_if_untrusted wires this in for content_list,
app_logs and mesh_status (the tools whose results carry peer-authored text);
every other tool result passes through unwrapped. loop_.rs's execute_tool
calls it at the exact point a successful ToolResult is constructed, before
that content ever becomes part of a ChatMessage.

No pattern-stripping or keyword-blocklist filter was added (D-10 rejects
that approach by name) — the delimiter and D-11's confirm gate are two
independent layers. Four scripted-worst-case tests in mod.rs prove the gate
still holds even when a compromised model acts on an injected imperative
(injected_instruction_does_not_grant_authority), a forged closing delimiter
plus fake operator turn (forged_closing_delimiter_does_not_escape_block), or
an injected mislabel attempting to hide the real action from the human
(injected_mislabel_still_confirms_real_action) — plus
wrap_untrusted_token_is_per_call (tools.rs) asserting the per-call token
itself. Zero packages added — rand 0.8.5 already in-tree.

56/56 assistant:: tests pass in this task's own isolated state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:00:20 -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 c08f8f0e83 docs(13-11): complete music library + share MIME fix plan 2026-08-05 18:35:10 -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
archipelago abe77ebe7e fix(13-11): ShareModal's MIME map stops filing m4a/aac/opus/wma as Documents
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).
2026-08-05 18:20:51 -04:00
archipelago 7bea8f6ba4 feat(13-11): map music.list-tracks records onto AIUI's Song shape
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.
2026-08-05 18:17:54 -04:00
archipelagoandClaude Fable 5 dba3aecf26 docs(13-10): complete Ollama backend + node-side history plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 18:06:50 -04:00
archipelagoandClaude Fable 5 3da9928cc9 feat(13-10): node-side chat history, scoped by caller, compacted not truncated
history.rs persists the ChatMessage transcript under data_dir (D-08),
keyed by a HistoryKey derived from CallerScope so an operator's AIUI
session and a mesh peer's transcript are structurally distinct files, not
two rows a filter could forget. Writes are atomic (temp sibling + rename,
matching music/index.rs::save_atomic's precedent) and 0600, following
grants.rs's convention.

Tool results longer than MAX_TOOL_RESULT_CHARS are truncated with a
visible marker before entering history -- a new, assistant-scoped
constant, never assist.rs's LoRa-airtime-tuned reply cap. Once the
transcript exceeds KEEP_VERBATIM_TURNS, older turns fold into a running
summary extended incrementally as turns age out, never regenerated from
the full transcript. Wallet/files-category tool-call arguments are never
persisted (AI-SPEC §7b's field policy applied to storage, not only
tracing) -- categories are resolved by the caller from the same tools
registry execute_tool uses, so history.rs never re-derives a second,
driftable category list. Nothing reachable from confirm.rs's pending-
confirmation state has a parameter path into this module at all (S-09
stays true structurally).

assistant.history / assistant.clear-history route through 13-01's
existing assistant.* dispatcher arm (dispatcher.rs untouched), each
scoped to the calling session's own HistoryKey.

run_loop (loop_.rs) now returns (answer, full_history) instead of just
the answer string -- structurally necessary so chat() (mod.rs) can
persist the tool-call/tool-result messages the loop built internally, not
only the user question and final answer (Rule 3, mirroring 13-05's
precedent of touching a file outside its own plan's files_modified list
when the plan's own intent requires it). chat() persists this turn after
run_loop returns; it does not yet feed prior persisted turns back into
live model context -- a documented, deliberately scoped follow-up (see
mod.rs's chat() doc comment and the plan SUMMARY).

8 new tests under assistant::history::tests::, including
operator_and_mesh_transcripts_are_separate and
wallet_tool_arguments_never_reach_the_transcript (asserted against both
the deserialized struct and the raw on-disk bytes). Full assistant::
suite: 50/50 (42 baseline-after-Task-1 + 8 new); confirm::tests::
restart_drops_pending_not_executes still passes -- S-09 not weakened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 18:03:55 -04:00
archipelagoandClaude Fable 5 821d8700ce feat(13-10): Ollama tool-calling backend, first in the D-04 chain
backends/ollama.rs implements the Backend trait against Ollama's
POST /api/chat (messages + tools arrays, message.tool_calls response) --
never mesh/listener/assist.rs::call_ollama's older single-shot prompt
endpoint, which has no tool-calling support at all. Ollama's per-call
tool-call ids (absent on the wire) are synthesized; its already-parsed
function.arguments object is passed through without a second string-parse
(the OpenAI-shape normalization would be wrong here). Every request sets
an explicit generation-length cap and runs non-streaming.

model_supports_tools queries Ollama's /api/show and caches the answer for
the process lifetime, turning AI-SPEC's [ASSUMED] note about
qwen2.5-coder's tool capability into a runtime fact: a non-tool-capable or
unreachable Ollama falls through to Claude with a logged reason, never a
silent tools-free degrade.

select_backend (backends/mod.rs) is now async and reuses the existing
detect_ollama() probe (mesh::assistant, bumped to pub(crate) for this
reuse) rather than re-probing. A new FallbackChain wraps the Ollama leg so
a transport error mid-turn falls through to Claude for that same call
instead of failing the turn outright.

13 new tests under assistant::backends::{ollama,}::tests::, exercised
against a local hyper-based HTTP stub (no mock-HTTP crate exists in this
workspace). Full assistant:: suite: 42/42 (29 baseline + 13 new).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 17:42:24 -04:00
archipelagoandClaude Fable 5 a245e5d29f docs(13-08): summary — confirm gate + trusted chrome complete, checkpoint approved
Task 3 (checkpoint:human-verify, blocking) approved by the operator after a
full on-device pass on archi-dev-box: deny/approve/read-only/fail-safe-timeout
all verified with a real Claude 4.5 Haiku backend against a real container.
cargo assistant:: 29/29 green (incl. declined_action_never_reprompts_same_turn),
vitest toolConfirm/contextBroker/chatAiuiEmbed 40/40 green. STATE.md/ROADMAP.md/
REQUIREMENTS.md updated (9/15 plans, AIUI-01/AIUI-04 marked complete for this
plan's contribution). Next: wave 4 (13-10, then 13-11).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:18:34 -04:00
archipelagoandClaude Fable 5 077a098dbf docs(13-08): Task 3 on-device evidence — deny/approve transcript + read-only no-dialog
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 14:58:12 -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 44c864ac14 docs(13-08): defer AIUI-over-host background regression on mobile/companion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 14:15:05 -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 44f552cc3d fix(13-08): system prompt — call tools directly, never text-ask for confirmation
On-device UAT hit an infinite politeness loop: 'every write requires a human
confirmation you cannot bypass' read to the model as 'collect consent in text
first', so it never called restart_app, the confirm gate never engaged, and
each stateless turn (history is 13-10) dropped the user's 'confirmed' into a
void. The preamble now states the intended contract: the node presents the
trusted dialog the moment the tool is called; a text pre-ask stalls the action
and trains rubber-stamping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 13:34:23 -04:00
archipelagoandClaude Fable 5 830b77af18 fix(13-09): verify-aiui-deploy false-negative — pipefail EPIPEs curl on grep -q early exit
curl | grep -q under set -o pipefail: grep's first-match exit EPIPEs curl
(exit 23) whenever the marker precedes the tail of a >64KB chunk, so a
genuine deploy read as FAIL (bit during 13-08's AIUI redeploy — marker at
27% of a 416KB chunk failed 3/3 runs). Fetch to a temp file, then grep.
Negative control still fails as it should.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 13:05:42 -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 8a19e8d3f8 docs(13): STATE frontmatter — 13-08 at Task 3 blocking human-verify gate
Tasks 1+2 verified complete on HEAD (ae042db9, record commit fc09d7a2);
plan closes only after operator's on-device dialog inspection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 11:49:00 -04:00
archipelagoandClaude Fable 5 ae042db951 docs(13-08): verify Task 1 + Task 2 complete on continuation, checkpoint at Task 3
Continuation of the operator-restarted 13-08 session. Verified rather than
reshaped, per the pushed-history constraint on fc09d7a2/1a664be1:

- fc09d7a2's tools.rs/grants.rs/backends/mod.rs diffs confirmed rustfmt-only
  (line-wrap reformatting), no behavior change.
- Task 1 re-verified green on current HEAD: 28/28 assistant:: tests pass,
  approval_nonce_binds_to_exact_action passes individually, dispatcher.rs
  untouched (git diff --exit-code clean).
- Task 2 was already complete in fc09d7a2's uncommitted-state snapshot: all
  10 toolConfirm.test.ts cases pass (one per <behavior> bullet including
  iframe_message_cannot_open_or_resolve_confirmation), pre-existing
  contextBroker.test.ts + chatAiuiEmbed.test.ts (28 tests) still green,
  vue-tsc --noEmit clean, and every acceptance-criteria grep passes
  (Teleport to="body", zero postMessage/v-html in the modal, distinct
  aiui:tool-confirm-request event pair not reusing aiui:install-request,
  assistant.pending RPC-fetch, ToolConfirmModal mounted in Chat.vue).

fc09d7a2 stands as the commit of record for both Task 1 and Task 2 — no new
source changes were needed. STOPPING at Task 3 (checkpoint:human-verify,
gate=blocking): the anti-spoofing and clear-signing properties are visual/
judgement calls that require a human on archi-dev-box, not cargo test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 11:47:10 -04:00
archipelagoandClaude Fable 5 1a664be113 docs(13): checkpoint 13-08 mid-Task-1 for operator session restart
Task 1 test-green (28/28) at fc09d7a2; resume via continuation executor,
Task 3 remains a blocking human-verify gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 11:39:44 -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 Fable 5 db11c625c8 test(13-08): failing tests for the D-07/D-11 confirm gate (RED)
- confirm.rs: ConfirmGate/PendingConfirmation/Confirmed/PendingSnapshot/
  ResolveRefusal API skeleton (request/resolve/mint_nonce/build_description
  still todo!()) plus the five named confirm tests: S-02 nonce binding,
  S-03 no-model-text, S-08 distinct resources, S-09 restart drops pending,
  timeout declines, and the no-shared-lock-across-the-wait case
- mod.rs: ToolExecCtx gains the confirm gate (global by default, injectable
  for tests) and the S-01 destructive_tool_requires_confirm test with a
  seeded installed-app snapshot
- verified RED: 7 new tests fail (todo! cores + unfilled destructive branch)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 07:12:12 -04:00
archipelagoandClaude Fable 5 7025c5f26f docs(13-07): state + roadmap — 13-07 complete, next 13-08
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:34:26 -04:00
archipelagoandClaude Fable 5 17da7a7233 docs(13-07): summary — music index + music.* surface complete, 23/23 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:15:41 -04:00
archipelagoandClaude Fable 5 d3b0ed8a1d feat(13-07): music.* RPC surface behind one dispatcher arm
- api/rpc/music.rs: handle_music prefix sub-dispatcher (assistant_chat.rs
  shape) with list-albums / list-artists / list-tracks / status / reindex
- one guarded dispatcher.rs arm for the whole music. prefix, adjacent to
  the content.* block; the only registration point for the surface
- list-tracks: optional album_id filter, limit/offset pagination, limit
  clamped to [1,500] (default 100) — out-of-range degrades, never errors
  (T-13-41)
- reindex spawns the scan and returns immediately; second call while one
  runs reports already-running with the last stats; optional
  incremental:true routes to refresh_incremental so a changed library is
  reflected without a full re-extraction
- newer-schema index served as an empty library, never overwritten or
  reinterpreted by readers (13-MUSIC-MODEL.md downgrade contract)
- nothing music.* in UNAUTHENTICATED_METHODS — the surface rides the
  session/CSRF/RBAC gate; asserted by music_methods_require_session
  (T-13-40)
- 7 tests, one per Task 2 behavior bullet plus the incremental mode

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:09:18 -04:00
archipelagoandClaude Fable 5 49687f7ebd feat(13-07): persisted music index — scan, group, atomic save, incremental refresh
- music/index.rs: reindex + refresh_incremental over the media roots,
  (path, mtime, size) diffing so unchanged files never re-extract, rows
  removed when files disappear (derived albums vanish with their last
  track), per-file extraction errors counted in ScanStats.skipped
- save_atomic: temp sibling + fsync + rename — a concurrent read sees a
  complete index or the previous one, never a partial file (T-13-42)
- load refuses schema_version > MUSIC_SCHEMA_VERSION with a distinct
  NewerSchema error and never overwrites the newer file (T-13-43)
- symlinks whose canonical target escapes the media roots are skipped,
  not followed (T-13-39); confinement enforced here and in tags.rs
- reindex guard: AtomicBool + RAII release; a second concurrent scan
  reports already-running instead of duplicating the walk (T-13-41)
- music/mod.rs: media_roots(Config) (filebrowser/Music +
  purchased-content) and LibrarySnapshot (tracks + derived albums/artists)
- 9 tests, one per 13-07 Task 1 behavior bullet, programmatic FLAC
  fixtures into tempdirs (no committed binaries)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 08:18:06 -04:00
archipelagoandClaude Fable 5 589cbb030f docs(13-04): complete music model + tag extraction plan — summary, state, config
- 13-04-SUMMARY.md: all 3 tasks, broken-pipe recovery (verbatim wip
  checkpoint be8f24b4), MP3 fixture off-by-one deviation, 7/7 tests green
- STATE.md: 13-04 complete (7/15 phase-13 plans), D-13 decision + lofty
  gate recorded in accumulated context, next = waves 3+
- config.json: fold in pre-existing use_worktrees=false from the broken
  session (wave-continue bookkeeping, intentionally kept)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 07:06:10 -04:00
archipelagoandClaude Fable 5 4b577493b1 feat(13-04): tag extraction across mp3/flac/m4a/ogg with media-root confinement
Completes Task 3 on top of the recovered wip checkpoint (be8f24b4):

- fix the programmatically-generated MP3 fixture's frame length: lofty's
  Header::read computes samples*bitrate*125/sample_rate with truncating
  integer division BEFORE adding the padding byte, so the FF FB 52 C4
  frame is 209 bytes, not 210 — the off-by-one made cmp_header miss the
  second frame sync and reject the whole file as containing an invalid
  frame (mp3_id3v24_yields_full_record now passes; fixture-only fix,
  production code untouched)
- all 7 music::tags tests green; no binary audio fixtures committed
  (fixtures are built byte-by-byte into tempdirs at test run time)
- extract_tags canonicalizes and confines to caller-supplied media_roots
  before opening any file (T-13-20); non-audio is a distinct NotAudio
  error vs the Ok/has_tags=false untagged fallback (T-13-21)
- entity types in music/mod.rs implement 13-MUSIC-MODEL.md exactly:
  hybrid-identity TrackId, derived albums/artists, MUSIC_SCHEMA_VERSION=1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:42:40 -04:00
archipelagoandClaude Fable 5 be8f24b4e3 wip(13-04): checkpoint Task 3 tag-extraction work recovered after broken pipe
Verbatim checkpoint of uncommitted executor work (music/mod.rs, music/tags.rs,
mod music; in main.rs) before verification. Tests not yet run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:05:12 -04:00
archipelagoandClaude Fable 5 6156444004 chore(13-04): add lofty 0.24.0 for music tag extraction
Task 2 checkpoint:human-verify resolved by operator approval
2026-08-04: crates.io lofty — 811,138 total downloads, 248,280
recent, 56 versions spanning 2021-04-23 to 0.24.0 (2026-04-12);
repo github.com/Serial-ATA/lofty-rs — 348 stars, 1,962 commits,
fuzzing + benchmark infra, MIT/Apache-2.0 dual license, active CI,
no squatting indicators. Dependency tree reviewed via `cargo tree -p
lofty`: byteorder, data-encoding, flate2, log, ogg_pager, paste — no
networking or process-spawning crates. `cargo build --package
archipelago` exits 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 05:13:30 -04:00
archipelagoandClaude Fable 5 bca4660a95 docs(13-04): record D-13 one-way music entity model decision
Task 1 checkpoint:decision resolved by operator: hybrid-identity (path
row key, lazily-backfilled content-hash dedupe column), derived-albums
(computed at read time from track tags, not stored rows), a single
JSON index at data_dir/music/index.json matching content_server.rs's
load_catalog precedent, and both own-library + peer sources indexed.
MUSIC_SCHEMA_VERSION starts at 1; a newer-version index on an older
binary is treated as absent rather than reinterpreted or overwritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 05:08:29 -04:00
archipelagoandClaude Fable 5 bf9b4e6ad3 docs(13): 13-09 complete — nginx sync merged live, CSP boundary browser-verified
Recovered after a broken-pipe session cut off right after Task 4 finished:
the summary was fully written (Self-Check PASSED) but never committed.
Re-verified on resume before committing: /aiui/-scoped CSP header live on
archi-dev-box, build/verify scripts present+executable, render screenshot
intact. STATE.md advanced: 6/15 plans done, next is 13-04.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 04:57:30 -04:00
archipelagoandClaude Opus 5 b3b580aab5 docs(13): 13-09 checkpoint pause point — Tasks 1-3 done, Task 4 needs nginx sync + human browser verify
Continuation executor (post-reboot) ground-truthed the 30b2e02f WIP
build-aiui.sh checkpoint as complete/correct, finished Tasks 2-3 and
scripts/verify-aiui-deploy.sh, and proved the build+deploy+verify cycle
end-to-end on the real archi-dev-box node (this machine). Paused at Task
4's remaining human/browser-required steps because the live node's nginx
config predates even 13-02 — syncing it is a bigger diff than this plan's
own CSP addition and belongs to a human-supervised deploy, not an
unsupervised executor push to a live node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:25:35 -04:00
archipelagoandClaude Opus 5 3756ebffc0 feat(13-09): verify-aiui-deploy.sh fetches live chunks via sw.js, never disk
Post-deploy check for AIUI: resolves the LIVE chunk set by fetching the
service worker's precache manifest (sw.js — vite-plugin-pwa's
generateSW-mode workbox.precacheAndRoute([{url:...}]) array) over HTTP,
fetches each live chunk, and greps the fetched bytes for a marker string.
Exits non-zero when the marker is absent from every live chunk.

This exists because the node's assets/ directory is a never-pruned
graveyard (feedback_node_side_frontend_verify_stale_chunks): a disk grep
reports "deployed" before the deploy actually happened, because a dead
chunk from an old build still contains the old string. Never opens a
remote shell onto the node and never greps the node's filesystem directly
— every check is an HTTP fetch, exactly what a browser session would do.

Verified locally against a real AIUI build served over HTTP: a marker
actually present in a live-precached chunk (index.html) passes (exit 0,
2 chunks checked before the match); a nonexistent marker correctly fails
(exit 1) as the negative control — not a check that always passes.

Wired into deploy-to-target.sh's primary AIUI deploy path in the prior
commit (073bf6f3), which already calls this script by name after the copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:22:52 -04:00
archipelagoandClaude Opus 5 c3bffbd557 feat(13-09): widen same-host deploy guard from containment-only to any mismatch
Adds assert_safe_same_host_deploy(local_src, remote_dst) to lib/common.sh:
pure, no SSH inside it, callable directly from a test with fixed inputs.
Returns 0 only when the two already-resolved paths are equal; refuses
(non-zero, message naming both paths + the 2026-07-31 incident) on any
other same-host mismatch.

This closes a real gap in the 2026-07-31 incident's original fix: the old
guard's two `case` blocks refused only containment (source-in-destination
or destination-in-source). A SIBLING directory — for example this very
worktree, archy-phase13, deploying onto TARGET_DIR's resolved symlink
target (archy, the main checkout) — is neither contained by nor containing
of the destination, so the old guard let it through and `rsync --delete`
would have mirrored the sibling onto the main checkout, deleting everything
the sibling lacks. Found while retargeting deploy-to-target.sh for D-19,
not a D-19 effect itself.

deploy-to-target.sh's guard block now calls assert_safe_same_host_deploy
instead of the two inline containment-only case blocks (old logic removed,
not left dead alongside the new call).

tests/production-quality/deploy-guard-same-host.sh pins all five
<behavior> cases (identical/contained/containing/sibling/unrelated)
against the function with no SSH, no rsync, no real deploy — including the
exact archy-phase13-vs-archy pair as the sibling-directory regression pin.
Manually confirmed non-vacuous: flipping the sibling fixture's expectation
to "allow" makes the test fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:19:49 -04:00
archipelagoandClaude Opus 5 073bf6f33d feat(13-09): retarget AIUI build/deploy to the in-repo aiui/ tree (D-19)
Finishes the 13-09 build-aiui.sh WIP checkpoint (30b2e02f) that survived the
operator reboot ground-truthed and confirmed correct: require_base_path,
frozen-lockfile install, verify_dist's asset-href/commit-attribution checks
all verified working against a real build. Fixed one grep-forbidden leftover
(a comment mentioning the retired scripts/aiui.pin path).

Rewires both AIUI sections of deploy-to-target.sh (primary --live path and
the --both/secondary path) and setup-aiui-server.sh to build/deploy from
aiui/packages/app/dist instead of the retired ../AIUI sibling checkout:

- Primary section now calls scripts/build-aiui.sh instead of an inline
  `pnpm build`, then scripts/verify-aiui-deploy.sh after the copy. The
  demo/aiui/ fallback now prints a loud, unmissable warning naming that it
  is shipping a checked-in dist rather than a fresh build.
- Secondary/--both section retargeted to the in-repo dist path; its
  fallback-to-.228-streaming behavior is otherwise unchanged.
- setup-aiui-server.sh calls scripts/build-aiui.sh automatically when the
  dist is missing or stale, instead of printing a manual `cd ../AIUI/...`
  command and exiting (D-15: enforced, not remembered).

No remaining `../AIUI` reference in either script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 04:19:21 -04:00
archipelagoandClaude Opus 5 442122083f docs(13): reboot pause point — full resume state in stopped_at
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:29:21 -04:00
archipelagoandClaude Opus 5 30b2e02f4a wip(13-09): checkpoint in-progress executor work before operator reboot
build-aiui.sh (mid-write) + aiui/.gitignore, committed verbatim and UNVERIFIED
— not a task completion. The 13-09 executor will be killed by the reboot; its
continuation should read this checkpoint, judge it against the plan's
must_haves, and reset --soft / build forward as appropriate (same recovery
pattern as the 6ba52b22/13b576da broken-pipe rescue at the start of this phase).
Already committed by 13-09 before this: 6ac0ebbf (CSP sandbox task).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:28:54 -04:00
archipelagoandClaude Opus 5 57628a2aa4 fix(aiui)+docs: keyboard-resize parity for standalone AIUI; companion handover note
Operator-reported: keyboard opening in chat pads the tab bar and scrolls the
page instead of scaling the chat window. Triage: neode-ui already ships
interactive-widget=resizes-content + the --visual-viewport-height var, so
mobile-web Chrome resizes correctly — but an Android WebView ignores that meta
entirely, and the described pan-plus-padding is the adjustPan/edge-to-edge-
without-IME-insets signature. Companion-side fix documented for handover in
docs/companion-keyboard-viewport.md (manifest adjustResize, or IME insets when
edge-to-edge, plus a chrome://inspect verification recipe). Web side gets the
one real parity gap: AIUI's standalone index.html lacked the meta neode-ui has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:21:53 -04:00
archipelagoandClaude Opus 5 86129d5da2 fix(aiui): banner follows the selected item — reset fallback state on identity change
Operator-reported: selecting a different item in the content window left the
context-surface banner showing the previous item's image. Root cause: detail
views are reused, not remounted, and useBannerFallback kept primaryIndex/
stage/apiUrl alive across the prop change — once stage hit 'api' or 'done' it
never re-evaluated. Reset is keyed on title + the primary URL set, with a
generation guard so an in-flight fetch for the old item cannot stamp its
artwork onto the new one. Heals Film/TVSeries/Book detail at once; 3
regression tests pin it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:16:26 -04:00
archipelagoandClaude Opus 5 6ac0ebbf0a feat(13-09): enforce the /aiui/-scoped CSP sandbox boundary (AIUI-04)
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>
2026-08-04 03:11:36 -04:00
archipelagoandClaude Opus 5 5e3aab30a2 fix(aiui): header overlay panels get readable opacity — scoped, not blanket
Operator-reported: the model-picker and conversation-menu overlays are hard to
read over busy chat content at path-glass-card's shared rgba(0,0,0,0.65).
Scoped .header-overlay-panel (0.88) in ChatHeader.vue only — path-glass-card
itself is untouched, so BookDetail/ArticleDetail/TVSeriesDetail/WebsiteDetail/
ContentPanel/ChatWindow keep their existing glass. Unlayered scoped rule beats
the @layer components class without !important, and reaches the panels through
their Teleport to body.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:11:02 -04:00
archipelagoandClaude Opus 5 06c934bd59 docs(todos): capture two operator-reported AIUI issues
Mobile load speed and the 'how to use AIUI' brief not opening. Both noted with
the caveat that the deployed AIUI bundle is stale (pre-D-14), so they must be
reproduced against a fresh in-repo build before being chased.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:00:06 -04:00
archipelagoandClaude Opus 5 484b1209a8 docs(13): close windows 16 and 19 — assistant tests observed passing
21 passed / 0 failed across assistant::, plus assistant_methods_require_session
run explicitly. Both windows had non-defect root causes: window 19 was lane
staleness (missing 0de67ca6's PortMapping test-constructor fix, which made the
whole crate's test build fail), and window 16's repeated kills were the
orchestrator's own too-short timeout sending SIGTERM on a cold build, which I
had wrongly attributed to memory contention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:57:49 -04:00
archipelago 7cc58b7ac6 merge(13): bring main forward — unblocks the crate's test build
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
2026-08-04 01:50:39 -04:00
archipelago 0a0a2fde43 docs(13-05): complete curated tool registry and default-closed grants plan 2026-08-04 01:39:28 -04:00