126 lines
5.1 KiB
Rust
126 lines
5.1 KiB
Rust
//! D-02: "one assistant, many front doors." A shared assistant service —
|
|||
|
|
//! one curated tool registry, one backend selector, one place the model key
|
||
|
|
//! lives — used today by AIUI chat (`CallerScope::LocalOperator`) and, by
|
||
|
|
//! design, extensible to mesh/LoRa callers (`CallerScope::Mesh`) and later
|
||
|
|
//! Pine voice without recreating a second, divergent security model.
|
||
|
|
//!
|
||
|
|
//! This is the tracer slice for Phase 13 (D-01, D-02, D-06): a typed
|
||
|
|
//! question reaches exactly one curated, read-only tool
|
||
|
|
//! (`tools::system_disk_status_tool`) via the Claude backend, dispatched
|
||
|
|
//! through the SAME `handle_system_disk_status` RPC handler every other
|
||
|
|
//! authenticated caller uses. See `13-01-PLAN.md` for the full spine.
|
||
|
|
|
||
|
|
pub mod backends;
|
||
|
|
pub mod loop_;
|
||
|
|
pub mod tools;
|
||
|
|
|
||
|
|
use std::collections::BTreeSet;
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use anyhow::Result;
|
||
|
|
|
||
|
|
use crate::api::rpc::RpcHandler;
|
||
|
|
|
||
|
|
/// D-16's ten permission categories. All default-closed on a fresh node —
|
||
|
|
/// nothing is shared with the model until deliberately granted.
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||
|
|
pub enum PermissionCategory {
|
||
|
|
Apps,
|
||
|
|
System,
|
||
|
|
Network,
|
||
|
|
Wallet,
|
||
|
|
Files,
|
||
|
|
Media,
|
||
|
|
Search,
|
||
|
|
AiLocal,
|
||
|
|
Notes,
|
||
|
|
Bitcoin,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// D-02's promoted primary noun: a caller identity carrying the permission
|
||
|
|
/// scope its tool calls resolve authority through. "A mesh peer" and "the
|
||
|
|
/// local operator in AIUI" are two variants of it; Pine voice will be a
|
||
|
|
/// third (not built in this phase — no `Voice` variant exists yet, by
|
||
|
|
/// design, until that phase actually needs one).
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub enum CallerScope {
|
||
|
|
/// A mesh/LoRa peer. Not exercised by this plan (mesh's existing
|
||
|
|
/// `!ai` path is Q&A-only, per `mesh/listener/assist.rs`'s own doc
|
||
|
|
/// comment) — the variant exists so the shape is right when a future
|
||
|
|
/// plan wires mesh callers into the shared loop.
|
||
|
|
Mesh { peer_id: String },
|
||
|
|
/// The authenticated operator using AIUI, identified by their neode-ui
|
||
|
|
/// session. This is the only variant this tracer's `assistant.chat`
|
||
|
|
/// RPC constructs.
|
||
|
|
LocalOperator { session_id: String },
|
||
|
|
}
|
||
|
|
|
||
|
|
impl CallerScope {
|
||
|
|
/// The sole source of tool authority `execute_tool` reads. No
|
||
|
|
/// `execute_tool` branch may read a caller-specific field directly
|
||
|
|
/// instead of going through this — that would reintroduce the
|
||
|
|
/// mesh-only assumption D-02 exists to retire.
|
||
|
|
pub fn granted_categories(&self) -> BTreeSet<PermissionCategory> {
|
||
|
|
match self {
|
||
|
|
// 13-05 replaces this hardcoded default with the persisted
|
||
|
|
// D-16 default-closed grants store — a data-source change, not
|
||
|
|
// an architectural one (per the plan's assumption-delta note).
|
||
|
|
CallerScope::LocalOperator { .. } => {
|
||
|
|
let mut set = BTreeSet::new();
|
||
|
|
set.insert(PermissionCategory::System);
|
||
|
|
set
|
||
|
|
}
|
||
|
|
// Intentionally conservative for this tracer: mesh has no
|
||
|
|
// tool-calling caller path wired up yet (today's mesh `!ai` is
|
||
|
|
// Q&A-only), so there is no real trusted_only/allowed_contacts
|
||
|
|
// grant to resolve. A future plan that wires the Mesh variant
|
||
|
|
// into the shared loop threads those existing per-caller
|
||
|
|
// controls through here — this is explicitly NOT the place a
|
||
|
|
// mesh-only field gets read directly by `execute_tool`.
|
||
|
|
CallerScope::Mesh { .. } => BTreeSet::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Bundles what `execute_tool` needs regardless of which backend produced
|
||
|
|
/// the tool call: the curated registry, the caller's resolved authority,
|
||
|
|
/// and a handle back to the SAME `RpcHandler` every other authenticated
|
||
|
|
/// caller dispatches through — never an AI-only backdoor.
|
||
|
|
pub struct ToolExecCtx {
|
||
|
|
pub registry: tools::ToolRegistry,
|
||
|
|
pub caller: CallerScope,
|
||
|
|
pub handler: Arc<RpcHandler>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Entry point: run one chat turn for `caller` through the shared loop.
|
||
|
|
/// Builds the visible-tool set from the caller's granted categories only
|
||
|
|
/// (D-16 — the model should never even see a tool it can't use), selects a
|
||
|
|
/// backend (Claude only, in this tracer), and runs it to a final answer.
|
||
|
|
pub async fn chat(handler: Arc<RpcHandler>, caller: CallerScope, user_text: String) -> Result<String> {
|
||
|
|
let registry = tools::registry();
|
||
|
|
let grants = caller.granted_categories();
|
||
|
|
let visible_tools = registry.visible_to(&grants);
|
||
|
|
|
||
|
|
let backend = backends::select_backend(handler.data_dir());
|
||
|
|
|
||
|
|
let system_prompt = "You are the Archipelago node's operator-control assistant. \
|
||
|
|
Only use the tools explicitly listed for this turn — never invent a tool name or call \
|
||
|
|
one that isn't listed. Every write requires human confirmation you cannot bypass or \
|
||
|
|
pre-approve on the user's behalf.";
|
||
|
|
|
||
|
|
let history = vec![tools::ChatMessage {
|
||
|
|
role: tools::Role::User,
|
||
|
|
text: Some(user_text),
|
||
|
|
tool_calls: vec![],
|
||
|
|
tool_results: vec![],
|
||
|
|
}];
|
||
|
|
|
||
|
|
let ctx = ToolExecCtx {
|
||
|
|
registry,
|
||
|
|
caller,
|
||
|
|
handler,
|
||
|
|
};
|
||
|
|
|
||
|
|
loop_::run_loop(backend.as_ref(), system_prompt, &visible_tools, history, &ctx).await
|
||
|
|
}
|