From b1df79cad01fdaef4818875fe04949615c691c9f Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 8 Oct 2026 12:55:59 -0400 Subject: [PATCH] fix: stop unfunded default Routstr before provider discovery --- .../src/assistant/backends/routstr.rs | 3 ++ core/archipelago/src/assistant/mod.rs | 47 +++++++++++++------ docs/external-access-and-websites.md | 8 ++++ 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/core/archipelago/src/assistant/backends/routstr.rs b/core/archipelago/src/assistant/backends/routstr.rs index f751d6f0..1831c77c 100644 --- a/core/archipelago/src/assistant/backends/routstr.rs +++ b/core/archipelago/src/assistant/backends/routstr.rs @@ -377,6 +377,9 @@ impl Backend for RoutstrBackend { tools: &[ToolDef], history: &[ChatMessage], ) -> Result { + if self.policy.budget_sats == 0 { + anyhow::bail!("Set a Routstr spending allowance before using AI."); + } let providers = discover_providers(self.tor_proxy.as_deref()).await; self.send_with_providers(&providers, system, tools, history) .await diff --git a/core/archipelago/src/assistant/mod.rs b/core/archipelago/src/assistant/mod.rs index 800e9325..617c1ad5 100644 --- a/core/archipelago/src/assistant/mod.rs +++ b/core/archipelago/src/assistant/mod.rs @@ -1800,25 +1800,42 @@ mod tests { ); } - /// D-05: a fresh node's `AssistantBudget` defaults to a zero - /// allowance, and `select_backend` must never select Routstr in that - /// case — the operator sees Claude alone (or Claude's own error) - /// rather than a paid backend chosen and then declined at the payment - /// step. + /// Routstr is the default provider, but a fresh node cannot discover, + /// infer or pay until the operator explicitly sets an allowance. #[tokio::test] - async fn zero_allowance_never_selects_routstr() { + async fn default_routstr_with_zero_allowance_stops_before_network_or_payment() { let (handler, _tmp) = test_rpc_handler().await; let budget = AssistantBudget::load(handler.data_dir()).await; - assert_eq!( - budget.allowance_sats, 0, - "a fresh node must default to a zero allowance" - ); + assert_eq!(budget.allowance_sats, 0); + let (backend, id) = backends::select_backend(&handler).await; + assert_eq!(id, backends::BackendId::Routstr); + let result = tokio::time::timeout( + std::time::Duration::from_secs(1), + backend.send("synthetic", &[], &[]), + ) + .await + .expect("zero allowance must stop before provider discovery"); + let error = result.err().expect("zero allowance cannot run inference"); + assert!(error.to_string().contains("spending allowance")); + let after = AssistantBudget::load(handler.data_dir()).await; + assert_eq!(after.allowance_sats, 0); + assert_eq!(after.spent_sats, 0); + } + + /// Keep the saved legacy automatic selection behavior: zero allowance + /// must not enable the paid fallback. + #[tokio::test] + async fn automatic_selection_with_zero_allowance_never_selects_routstr() { + let (handler, _tmp) = test_rpc_handler().await; + crate::settings::model_provider::ModelProvider { + provider: crate::settings::model_provider::Provider::Auto, + openai_model: String::new(), + } + .save(handler.data_dir()) + .await + .unwrap(); let (_backend, id) = backends::select_backend(&handler).await; - assert_ne!( - id, - backends::BackendId::Routstr, - "a zero allowance must never select Routstr" - ); + assert_ne!(id, backends::BackendId::Routstr); } /// D-05: `payment_policy()`'s ceiling is computed ONLY from the diff --git a/docs/external-access-and-websites.md b/docs/external-access-and-websites.md index 25c22e77..2562cc87 100644 --- a/docs/external-access-and-websites.md +++ b/docs/external-access-and-websites.md @@ -397,3 +397,11 @@ The funding modal's Scan action now opens the existing wallet scanner and return to funding on close; six focused connection-modal tests pass after that wiring. The first dashboard production build passed; it will be rebuilt for this final scanner wiring before deployment. AIUI and isolated backend builds are ongoing. + +The first full isolated backend run passed 1,717 tests with one outdated default +selection assertion failing (four explicit ignores). The assertion expected an +unconfigured node to choose Claude. Updated coverage distinguishes the new Routstr +default from a saved legacy Auto choice, and the Routstr adapter now rejects zero +allowance before even discovering providers. A rerun is required; no passing full +suite or deployment is claimed yet. Final dashboard and AIUI production builds +have both passed.