car-server-core 0.36.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";

const ASSISTANT_CAPABILITIES: &[&str] = &[
    "chat",
    "verified-answers",
    "safety-checks",
    "memory-routines",
    "background-work",
    "trust-control",
    "approvals",
    "approval-channels",
    "a2ui",
    "continuity",
    "notifications",
    "connected-services",
    "conversation-channels",
    "workspace-files",
    "personal-context",
    "documents",
    "creative-output",
];

/// Build the assistant's spec for a given `car` binary path.
fn spec(command: String) -> AgentSpec {
    AgentSpec {
        id: ASSISTANT_AGENT_ID.to_string(),
        name: "Parslee Core".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 the consumer-visible Parslee Core strengths so hosts do not
        // mistake the flagship assistant for a narrow local chat agent.
        capabilities: assistant_capabilities(),
    }
}

fn assistant_capabilities() -> Vec<String> {
    ASSISTANT_CAPABILITIES
        .iter()
        .map(|capability| capability.to_string())
        .collect()
}

fn merge_assistant_capabilities(existing: &[String]) -> Vec<String> {
    let mut merged = existing.to_vec();
    for capability in ASSISTANT_CAPABILITIES {
        if !merged.iter().any(|value| value == capability) {
            merged.push((*capability).to_string());
        }
    }
    merged
}

/// 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 let Some(existing) = sup
        .list()
        .await
        .iter()
        .find(|a| a.spec.id == ASSISTANT_AGENT_ID)
    {
        let merged_capabilities = merge_assistant_capabilities(&existing.spec.capabilities);
        if merged_capabilities != existing.spec.capabilities {
            let mut updated = existing.spec.clone();
            updated.name = "Parslee Core".to_string();
            updated.capabilities = merged_capabilities;
            sup.upsert(updated).await.map_err(|e| e.to_string())?;
        }
        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::*;
    use std::path::PathBuf;

    /// A real, absolute, non-world-writable executable to stand in for the `car`
    /// binary. The upsert validator requires an absolute path to an existing file
    /// and rejects world-writable scratch dirs (`/tmp`, `%TEMP%`, …) — so we
    /// can't point at the tempdir, and `/bin/sh` doesn't exist on Windows.
    /// `%SystemRoot%\System32` is the Windows analogue of `/bin`: real, absolute,
    /// and not on the scratch denylist.
    fn stand_in_binary() -> PathBuf {
        if cfg!(windows) {
            let root = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
            PathBuf::from(format!(r"{root}\System32\cmd.exe"))
        } else {
            PathBuf::from("/bin/sh")
        }
    }

    /// A *different* real executable, for asserting that an existing operator-set
    /// command is not overwritten.
    fn other_binary() -> PathBuf {
        if cfg!(windows) {
            let root = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
            PathBuf::from(format!(r"{root}\System32\where.exe"))
        } else {
            PathBuf::from("/bin/echo")
        }
    }

    #[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();

        let car = stand_in_binary();

        // 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, car.to_string_lossy());
        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"
        );
        assert!(
            entry.spec.capabilities.len() > 10,
            "assistant must advertise the full Parslee Core capability surface"
        );
        for expected in [
            "verified-answers",
            "memory-routines",
            "approvals",
            "approval-channels",
            "a2ui",
            "continuity",
            "notifications",
            "connected-services",
            "documents",
            "creative-output",
        ] {
            assert!(
                entry.spec.capabilities.contains(&expected.to_string()),
                "assistant capability metadata missing {expected}"
            );
        }
        // The supervisor minted a token.
        assert!(!entry.spec.token.is_empty());
    }

    #[tokio::test]
    async fn register_refreshes_stale_builtin_capabilities_without_overwriting_operator_settings() {
        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();

        // The operator's own command/args, which registration must not clobber.
        // Never executed (auto_start = false), so the args are inert data.
        let operator_cmd = stand_in_binary();
        sup.upsert(AgentSpec {
            id: ASSISTANT_AGENT_ID.to_string(),
            name: "CAR Assistant".to_string(),
            command: operator_cmd.to_string_lossy().into_owned(),
            args: vec!["-c".to_string(), "sleep 1".to_string()],
            cwd: None,
            env: BTreeMap::new(),
            restart: RestartPolicy::Never,
            max_restarts: 1,
            backoff_secs: 9,
            auto_start: false,
            token: "operator-token".to_string(),
            capabilities: vec!["chat".to_string()],
        })
        .await
        .unwrap();

        assert!(!ensure_registered_in(&sup, &other_binary()).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.name, "Parslee Core");
        // Still the operator's binary, not the one we just passed in.
        assert_eq!(entry.spec.command, operator_cmd.to_string_lossy());
        assert_eq!(
            entry.spec.args,
            vec!["-c".to_string(), "sleep 1".to_string()]
        );
        assert_eq!(entry.spec.restart, RestartPolicy::Never);
        assert_eq!(entry.spec.max_restarts, 1);
        assert_eq!(entry.spec.backoff_secs, 9);
        assert!(!entry.spec.auto_start);
        assert_eq!(entry.spec.token, "operator-token");
        assert!(entry
            .spec
            .capabilities
            .contains(&"verified-answers".to_string()));
        assert!(entry.spec.capabilities.contains(&"a2ui".to_string()));
        assert!(entry
            .spec
            .capabilities
            .contains(&"connected-services".to_string()));
        assert!(entry
            .spec
            .capabilities
            .contains(&"creative-output".to_string()));
    }
}