car-external-agents 0.25.0

Detection of installed agentic CLIs (Claude Code, Codex, Gemini) for the Common Agent Runtime.
//! Wire-shape types for external-agent detection.
//!
//! Identical between the in-process FFI singleton and the daemon WS
//! surface, per the `car-ffi-common::supervisor` precedent — a host
//! can swap transports without reshaping payloads.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Stable identifier for each supported external agent. Mirrors the
/// canonical CLI binary name; downstream tooling keys on this string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AdapterId {
    /// Anthropic Claude Code (`claude` binary).
    ClaudeCode,
    /// OpenAI Codex CLI (`codex` binary).
    Codex,
    /// Google Gemini CLI (`gemini` binary).
    Gemini,
}

impl AdapterId {
    /// Stable string id used in JSON payloads and as the key downstream
    /// callers reference when invoking an external agent.
    pub fn as_str(self) -> &'static str {
        match self {
            AdapterId::ClaudeCode => "claude-code",
            AdapterId::Codex => "codex",
            AdapterId::Gemini => "gemini",
        }
    }

    /// Human-readable label for UI surfaces.
    pub fn display_name(self) -> &'static str {
        match self {
            AdapterId::ClaudeCode => "Claude Code",
            AdapterId::Codex => "Codex CLI",
            AdapterId::Gemini => "Gemini CLI",
        }
    }

    /// Every adapter the runtime knows about. Detection iterates this.
    pub fn all() -> &'static [AdapterId] {
        &[AdapterId::ClaudeCode, AdapterId::Codex, AdapterId::Gemini]
    }
}

/// How an installed CLI is authenticated (heuristic, never verified).
///
/// Detection inspects credential file *shape* — never contents — so
/// the result is advisory. Upstream tools change credential layouts
/// without notice; treat `Unknown` as the safe default and don't make
/// trust decisions on this field.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AuthKind {
    /// OAuth-backed login against the vendor's subscription product
    /// (Claude Pro/Max, ChatGPT Plus/Pro, etc.). Routing decisions
    /// can prefer this when capability fits — invocations don't burn
    /// API credits.
    Subscription,
    /// API key in the credential file. Per-request consumption billing.
    ApiKey,
    /// Cred file present but shape doesn't match either pattern.
    /// Common when the upstream tool ships a new auth flow.
    #[default]
    Unknown,
    /// No credential file found. Tool is installed but the user
    /// hasn't logged in yet.
    Unauthenticated,
}

/// Static capability set per adapter — what its JSON stdio protocol
/// advertises. Phase 1 ships these as advisory metadata; Phase 2's
/// invocation path uses them to decide which features are wired up.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Capabilities {
    /// Tool use (function calling) is available.
    #[serde(default)]
    pub tool_use: bool,
    /// MCP server registration is supported.
    #[serde(default)]
    pub mcp: bool,
    /// Hooks (pre/post-tool, pre-prompt, etc.) are supported.
    #[serde(default)]
    pub hooks: bool,
    /// Multi-turn sessions with stable id are supported.
    #[serde(default)]
    pub sessions: bool,
    /// Token-level streaming output is supported.
    #[serde(default)]
    pub streaming: bool,
}

/// Detection result for one installed adapter. Empty when the binary
/// isn't on `$PATH`. Wire shape is stable across FFI and WS surfaces.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalAgentSpec {
    /// Adapter identifier (`"claude-code"`, `"codex"`, `"gemini"`).
    pub id: String,
    /// Human-readable label.
    pub display_name: String,
    /// Resolved absolute path to the binary. Stored at detection time
    /// so future invocations don't consult `$PATH` again — closes the
    /// PATH-injection variant per the 2026-05 audit's reasoning.
    pub binary_path: PathBuf,
    /// Version string parsed from `<bin> --version`. `None` when the
    /// version probe failed or timed out — entry still useful, the
    /// binary is on disk.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    /// **Deprecated since Phase 2 stage 1.** Heuristic auth-kind
    /// derived from credential file shape — see [`AuthKind`]. Kept
    /// for backwards compatibility with consumers that haven't
    /// migrated to the `health` field. Modern macOS / Linux /
    /// Windows builds use OS keystores, so this field falls through
    /// to `Unknown` for the most common installs. Prefer `health`.
    #[serde(default)]
    pub auth_kind: AuthKind,
    /// Static capability advertisement.
    pub capabilities: Capabilities,
    /// UNIX seconds when this entry was last refreshed.
    #[serde(default)]
    pub detected_at: u64,
    /// Ground-truth health bucket from the tool's own auth-status
    /// command, populated when detection runs with health checks
    /// enabled. `None` when health wasn't requested — call
    /// `agents.health_external` (or `detect_with_health()`) to
    /// populate. Replaces `auth_kind` as the primary "is this tool
    /// ready to invoke" signal.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub health: Option<crate::health::ExternalAgentHealth>,
}