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>
This commit is contained in:
archipelago
2026-08-03 14:37:16 -04:00
co-authored by Claude Opus 5
parent 15774d266f
commit fe6ccff73c
11 changed files with 924 additions and 2 deletions
@@ -0,0 +1,35 @@
//! The `Backend` trait — the wire-format-agnostic seam every model backend
//! (Ollama, Claude, Routstr) implements once. The loop and every tool are
//! written against this trait only; wire-format differences live entirely
//! inside each adapter.
use std::path::Path;
use anyhow::Result;
use async_trait::async_trait;
use super::tools::{ChatMessage, ToolCall, ToolDef};
pub mod claude;
#[cfg(test)]
pub mod scripted;
pub enum BackendTurn {
Text(String),
ToolCalls(Vec<ToolCall>),
}
#[async_trait]
pub trait Backend: Send + Sync {
async fn send(&self, system: &str, tools: &[ToolDef], history: &[ChatMessage]) -> Result<BackendTurn>;
}
/// D-04's backend chain: local Ollama first (node data never leaves the
/// node when a local model is available), then Claude, then Routstr. Only
/// the Claude leg is implemented in this tracer — `backends/ollama.rs`
/// (13-10) and `backends/routstr.rs` (13-13) slot in ahead of and behind it
/// without changing the `Backend` trait; that is the architectural
/// commitment this tracer proves.
pub fn select_backend(data_dir: &Path) -> Box<dyn Backend> {
Box::new(claude::ClaudeBackend::new(data_dir.to_path_buf()))
}