Commit Graph
6 Commits
Author SHA1 Message Date
archipelagoandClaude Fable 5 8ba6041251 feat(13-13): D-05 prepaid budget — arithmetic ceiling, hard stop, D-04 chain complete
Completes D-04's backend chain: Ollama -> Claude -> Routstr (budget-gated),
and wires D-05's operator-set prepaid allowance as a hard, arithmetic stop
a prompt-injected model can never cross.

- assistant/mod.rs: `AssistantBudget` (allowance_sats/spent_sats,
  persisted 0600 under data_dir/assistant/budget.json, mirroring
  Grants::load/save exactly — a missing/corrupt file defaults to a ZERO
  allowance, D-16's "default closed" applied to money). `payment_policy()`
  builds a `PaymentPolicy` from ONLY these two persisted fields — no
  parameter accepts anything model/tool/provider-influenced, which is what
  makes the ceiling arithmetic rather than a policy an injected model
  could argue with. `record_spend()` persists a successful payment and
  raises a one-time 80%-threshold owner notice (AI-SPEC §7b). New typed
  `BudgetExhausted` error (downcastable via anyhow) is the signal
  `loop_.rs` distinguishes from an ordinary transport error.
- assistant/loop_.rs: `run_loop` downcasts a `BudgetExhausted` out of the
  backend's `Err` and returns `Ok` with a plain-language stop message —
  no retry, no re-price, no partial spend, no fall-through to a different
  provider at a different price. Verified to actually matter: temporarily
  replaced the terminating `return` with `continue` and confirmed
  `zero_budget_stops_loop_without_retry` goes red (the backend gets
  retried 8x to MAX_TURNS and the turn errors instead of stopping
  cleanly); restored and reconfirmed green (13-13-SUMMARY.md records the
  observed failure).
- assistant/backends/mod.rs: `select_backend` now takes `&RpcHandler`
  (was `&Path`) to also read the Tor-proxy config; completes the D-04
  chain — Routstr never selected when the operator's allowance is zero
  (Claude alone instead), otherwise chained as Claude's fallback
  (Ollama -> Claude -> Routstr, each leg reached only when the priors are
  unavailable). New `BackendId::Routstr` variant.
- assistant/backends/routstr.rs: the payment-decline arm now returns the
  typed `BudgetExhausted` (was a plain bail in Task 2's commit, per the
  plan's own "handled in Task 3" note); a successful payment records spend
  against the persisted budget immediately (the Cashu proofs are already
  committed at that point, regardless of whether the subsequent chat HTTP
  call itself succeeds).
- api/rpc/assistant_chat.rs: `assistant.budget-get`/`assistant.budget-set`
  RPCs (routed through the existing single `assistant.` dispatcher arm —
  dispatcher.rs untouched) and a `nostr_tor_proxy()` accessor for
  select_backend's onion-preference decision.

Named tests (assistant::tests::): zero_budget_stops_loop_without_retry (S-12),
zero_allowance_never_selects_routstr, ceiling_is_not_a_function_of_model_output,
injection_loop_against_low_budget_does_not_overspend (EV-17) — all pass.
Full assistant:: suite: 91/91. Full crate suite: 1235/1235 (2 pre-existing
ignored, unrelated). dispatcher.rs and Cargo.toml untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 05:33:27 -04:00
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
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 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 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 fe6ccff73c feat(13-01): Rust assistant spine — one curated tool, one backend, one RPC surface
D-01/D-02/D-06 tracer slice: a new crate::assistant module (CallerScope,
PermissionCategory, ToolExecCtx, chat()) runs a multi-turn tool-calling loop
(run_loop/execute_tool, MAX_TURNS=8) against a curated single-tool registry
(system_disk_status, hand-written JSON Schema — no schemars) via a Claude
Messages API backend. execute_tool is the single choke point: unknown tools
are refused not ignored, D-16 category grants are re-checked even though the
system prompt already omits ungranted tools, and every real tool dispatches
through the SAME handle_system_disk_status RPC handler every other
authenticated caller uses (assistant_dispatch_tool bridge in
api/rpc/assistant_chat.rs) — never an AI-only backdoor.

assistant.chat is registered in dispatcher.rs as a single guarded
`m if m.starts_with("assistant.")` arm reached only after the existing
session-cookie + CSRF + role.can_access() gate in api/rpc/mod.rs — asserted
directly by assistant_methods_require_session against the live
UNAUTHENTICATED_METHODS list (visibility only widened to pub(crate) for that
assertion; the list's contents are untouched, per the Phase-10 hard
constraint).

Key read from data_dir/secrets/claude-api-key — the same path
mesh/rpc/mesh/assistant.rs already probes — never a second key location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:37:16 -04:00