car-server-core 0.33.0

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
//! CAR Assistant — the flagship, general-purpose agent that ships in the `car`
//! binary and works out of the box (`car do`).
//!
//! Unlike the coder (coding-specific) or the create-car-agent skill (build your
//! own), this is a batteries-included assistant: files + a real shell + web +
//! durable memory, sandbox-first for safety, driven by CAR inference through a
//! full [`Runtime`] (validator, policy, permission tiers, event log). One core
//! backs three entry modes — one-shot, REPL, and the conversational
//! `agent.chat` surface.
//!
//! ## Module map
//! - [`executor`] — [`GeneralExecutor`], the substrate-bound tool executor
//!   (agent_basics + `calculate` + `shell` + network delegate).
//! - [`net_tools`] — host-side `http_request` / `web_search` (bypass the
//!   sandbox's `--network none`).
//! - [`substrate`] — sandbox-first environment selection ([`bind_default_substrate`]).
//! - [`policy`] — the assistant inspector chain (reuses the coder's footgun set).
//! - [`prompt`] — batch vs. conversational system prompts.
//! - [`agent_loop`] — the propose→validate→execute→observe loop.
//!
//! [`Runtime`]: car_engine::Runtime
//! [`GeneralExecutor`]: executor::GeneralExecutor
//! [`bind_default_substrate`]: substrate::bind_default_substrate

pub mod agent_loop;
pub mod chat;
pub mod executor;
pub mod memory;
pub mod net_tools;
pub mod policy;
pub mod prompt;
pub mod register;
pub mod substrate;

use std::path::PathBuf;
use std::sync::Arc;

use async_trait::async_trait;
use car_engine::{Runtime, ToolEntry, ToolExecutor, ToolSchema};
use car_eventlog::EventLog;
use car_inference::InferenceEngine;
use car_policy::permission::PermissionTier;
use serde_json::Value;

use memory::MemoryTools;

pub use agent_loop::{
    run_assistant_loop, run_assistant_loop_cancellable, ApprovalDecision, ApprovalGate,
    AssistantConfig, AssistantEvent, AssistantOutcome,
};
pub use chat::AssistantService;
pub use executor::GeneralExecutor;
pub use net_tools::NetTools;
pub use substrate::{bind_default_substrate, BoundEnvironment};

/// An assembled assistant runtime: the [`Runtime`] to drive, the model-visible
/// tool list, and the environment metadata for the system prompt.
pub struct AssistantRuntime {
    /// The configured runtime (validator + policy + tiers + event log), whose
    /// tool executor is the [`GeneralExecutor`].
    pub runtime: Runtime,
    /// The model-visible tools (from `GeneralExecutor::all_tool_defs()`).
    pub tools: Vec<Value>,
    /// One-line environment description for the system prompt.
    pub description: String,
    /// Whether execution is isolated in a container.
    pub sandboxed: bool,
    /// Tools that require human approval before running under the standing tier
    /// (writes/shell on the local host without `--full-access`); empty when the
    /// tier auto-allows everything. Feed into `AssistantConfig::gated_tools`.
    pub gated_tools: Vec<String>,
    /// If the sandbox was requested but unavailable, why we fell back to local.
    pub fallback_notice: Option<String>,
}

/// A delegate that tries each inner executor in turn, using the `unknown tool`
/// convention to fall through — so several tool families (network, memory) share
/// one `GeneralExecutor` delegate slot.
struct ChainedDelegate(Vec<Arc<dyn ToolExecutor>>);

#[async_trait]
impl ToolExecutor for ChainedDelegate {
    async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
        for ex in &self.0 {
            match ex.execute(tool, params).await {
                Err(e) if e.starts_with("unknown tool") => continue,
                other => return other,
            }
        }
        Err(format!("unknown tool: '{tool}'"))
    }
}

/// Build a registry [`ToolSchema`] from a model-facing `{name, description,
/// parameters}` def, so any advertised tool can be registered for validation.
fn schema_from_def(def: &Value) -> ToolSchema {
    ToolSchema {
        name: def["name"].as_str().unwrap_or_default().to_string(),
        description: def["description"].as_str().unwrap_or_default().to_string(),
        parameters: def["parameters"].clone(),
        returns: None,
        idempotent: false,
        cache_ttl_secs: None,
        rate_limit: None,
    }
}

/// Default durable-memory path: `~/.car/memory/assistant.json`.
fn default_memory_path() -> PathBuf {
    let home = std::env::var("HOME")
        .ok()
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    home.join(".car").join("memory").join("assistant.json")
}

/// Assemble an [`AssistantRuntime`] from an engine and a bound environment.
///
/// Registers the model-visible tools so the validator allows them (agent_basics
/// builtins + `shell` + `http_request` + `web_search`), binds the
/// [`GeneralExecutor`] as the executor and the environment's substrate, and
/// attaches an optional event-log journal.
pub async fn build_assistant_runtime(
    engine: Arc<InferenceEngine>,
    env: BoundEnvironment,
    eventlog: Option<PathBuf>,
) -> AssistantRuntime {
    // Mutations auto-allow unless the standing tier is ReadOnly (local host
    // without --full-access), in which case writes/shell need approval; clamp
    // file paths to root only off-sandbox.
    let gated_tools: Vec<String> = if matches!(env.tier, PermissionTier::ReadOnly) {
        ["write_file", "edit_file", "shell"]
            .iter()
            .map(|s| s.to_string())
            .collect()
    } else {
        Vec::new()
    };
    let clamp = !env.sandboxed;
    let description = env.description.clone();
    let sandboxed = env.sandboxed;
    let fallback_notice = env.fallback_notice.clone();

    // Delegate tools (host-side): network + durable memory. Both bypass the
    // substrate/path-clamp — network needs host egress, memory is CAR's graph.
    let net: Arc<dyn ToolExecutor> = Arc::new(NetTools::new());
    let mem: Arc<dyn ToolExecutor> = Arc::new(MemoryTools::open(default_memory_path()));
    let mut delegate_defs = net_tools::net_tool_defs();
    delegate_defs.extend(MemoryTools::tool_defs());
    let delegate: Arc<dyn ToolExecutor> = Arc::new(ChainedDelegate(vec![net, mem]));

    let executor = GeneralExecutor::new(env.substrate.clone(), env.root.clone(), clamp)
        .with_delegate(delegate, delegate_defs);
    let tools = executor.all_tool_defs();
    let executor: Arc<dyn ToolExecutor> = Arc::new(executor);

    let mut runtime = Runtime::new()
        .with_inference(engine)
        .with_executor(executor)
        .with_substrate(env.substrate.clone());
    if let Some(path) = eventlog {
        if let Some(parent) = path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        runtime = runtime.with_event_log(EventLog::with_journal(path));
    }

    // Register the model-visible tools so the validator admits them. Execution
    // is owned by the GeneralExecutor above; these registrations are for
    // validation + schema listing. agent_basics covers the file/calculate
    // builtins; everything else advertised (shell, http_request, web_search,
    // remember, recall) is registered from its advertised def.
    runtime.register_agent_basics().await;
    let builtin_names: std::collections::HashSet<String> = car_engine::agent_basic_entries()
        .into_iter()
        .map(|e| e.schema.name)
        .collect();
    for def in &tools {
        let name = def["name"].as_str().unwrap_or_default();
        if name.is_empty() || builtin_names.contains(name) {
            continue;
        }
        runtime
            .register_tool_entry(ToolEntry::new(schema_from_def(def)).with_side_effects(true))
            .await;
    }

    AssistantRuntime {
        runtime,
        tools,
        description,
        sandboxed,
        gated_tools,
        fallback_notice,
    }
}