car-server-core 0.33.0

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
//! Zero-install registration of the flagship assistant into CarHost.
//!
//! Seeds one [`AgentSpec`] into `~/.car/agents.json` whose command is the `car`
//! binary invoked as `car do --serve`, so the assistant shows up in CarHost's
//! Agents list and auto-starts on daemon boot — nothing for the user to install.
//! Idempotent and non-destructive: if an entry with this id already exists we
//! leave it untouched (respecting any operator change, e.g. a disabled
//! `auto_start`).
//!
//! Two entry points, because the manifest is lock-guarded by whoever owns it:
//! - [`ensure_registered_in`] registers into an already-held [`Supervisor`] —
//!   the daemon calls this at boot with the sibling `car` binary path.
//! - [`ensure_registered`] opens the user-default supervisor itself — the `car`
//!   CLI calls this when no daemon owns the manifest.

use car_registry::supervisor::{AgentSpec, RestartPolicy, Supervisor};
use std::collections::BTreeMap;
use std::path::Path;

/// The reserved agent id for the built-in assistant.
pub const ASSISTANT_AGENT_ID: &str = "car-assistant";

/// Build the assistant's spec for a given `car` binary path.
fn spec(command: String) -> AgentSpec {
    AgentSpec {
        id: ASSISTANT_AGENT_ID.to_string(),
        name: "CAR Assistant".to_string(),
        command,
        args: vec!["do".to_string(), "--serve".to_string()],
        cwd: None,
        env: BTreeMap::new(),
        restart: RestartPolicy::OnFailure,
        max_restarts: 5,
        backoff_secs: 2,
        auto_start: true,
        // Empty → the supervisor mints and persists a per-agent token.
        token: String::new(),
        // Advertise chat so CarHost shows the Chat tab and routes agents.chat here.
        capabilities: vec!["chat".to_string()],
    }
}

/// Register the assistant into an already-held supervisor, using `car_binary`
/// as the command. Skips if already present. `Ok(true)` = newly created.
pub async fn ensure_registered_in(
    sup: &Supervisor,
    car_binary: &Path,
) -> Result<bool, String> {
    if sup
        .list()
        .await
        .iter()
        .any(|a| a.spec.id == ASSISTANT_AGENT_ID)
    {
        return Ok(false);
    }
    sup.upsert(spec(car_binary.to_string_lossy().into_owned()))
        .await
        .map(|_| true)
        .map_err(|e| e.to_string())
}

/// Register the assistant via the user-default supervisor, using the running
/// executable as the command (the `car` CLI calls this). Returns `Err` if the
/// manifest is locked by a running daemon — the caller should treat that as
/// "already handled by the daemon" and stay quiet.
pub async fn ensure_registered() -> Result<bool, String> {
    let exe = std::env::current_exe()
        .map_err(|e| format!("cannot resolve current executable: {e}"))?;
    let sup = Supervisor::user_default().map_err(|e| e.to_string())?;
    ensure_registered_in(&sup, &exe).await
}

/// True if `err` is the "another supervisor owns the manifest" lock message —
/// i.e. a daemon is running and will have registered the assistant itself.
pub fn is_manifest_locked(err: &str) -> bool {
    err.contains("another supervisor already owns this manifest")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn register_is_idempotent_and_uses_the_given_binary() {
        let dir = tempfile::tempdir().unwrap();
        let manifest = dir.path().join("agents.json");
        let logs = dir.path().join("logs");
        let sup = Supervisor::with_paths(manifest, logs).unwrap();

        // A real, absolute, non-world-writable executable (the upsert validator
        // rejects paths under /tmp, so we can't point at the tempdir).
        let car = Path::new("/bin/sh");

        // First registration creates it; second is a no-op (respects existing).
        assert!(ensure_registered_in(&sup, car).await.unwrap());
        assert!(!ensure_registered_in(&sup, car).await.unwrap());

        let listed = sup.list().await;
        let entry = listed
            .iter()
            .find(|a| a.spec.id == ASSISTANT_AGENT_ID)
            .expect("assistant registered");
        assert_eq!(entry.spec.command, "/bin/sh");
        assert_eq!(entry.spec.args, vec!["do".to_string(), "--serve".to_string()]);
        assert!(entry.spec.auto_start);
        assert!(
            entry.spec.capabilities.contains(&"chat".to_string()),
            "assistant must advertise the chat capability"
        );
        // The supervisor minted a token.
        assert!(!entry.spec.token.is_empty());
    }
}