Skip to main content

kernex_adapter_core/
lib.rs

1//! Core trait and supporting types for kernex agent adapters.
2//!
3//! Workspace-internal. Concrete adapter implementations land in follow-up
4//! changes; this crate defines the shape they target.
5
6#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
7
8use std::sync::Arc;
9
10use thiserror::Error;
11
12/// Stable identifier for a supported agent.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14#[non_exhaustive]
15pub enum AdapterId {
16    ClaudeCode,
17    CodexCli,
18    OpenCode,
19    Cursor,
20    Cline,
21}
22
23/// Capability surface an adapter exposes. Sync default methods so adapter
24/// authors can override without dragging async machinery into capability
25/// reporting.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
27#[non_exhaustive]
28pub enum Capability {
29    Skills,
30    Memory,
31    Mcp,
32    OutputStyle,
33}
34
35/// Lightweight detection result surfaced by [`Adapter::detect`].
36///
37/// `config_root` is the adapter's home-rooted configuration directory
38/// (e.g., `~/.claude` for Claude Code, `~/.codex` for Codex CLI).
39/// `project_root` is the project-local allowlisted root for adapters that
40/// also write files in the current working directory (e.g., Codex's
41/// `<cwd>/AGENTS.md`, Cursor's `.cursorrules`). The Stage 5 APPLY sandbox
42/// check accepts writes inside EITHER `config_root` OR `project_root`.
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44#[non_exhaustive]
45pub struct Detection {
46    pub installed: bool,
47    pub config_root: Option<std::path::PathBuf>,
48    #[serde(default)]
49    pub project_root: Option<std::path::PathBuf>,
50    pub version: Option<String>,
51}
52
53impl Detection {
54    /// Construct a `Detection` for an adapter with no project-local writes.
55    ///
56    /// `project_root` is set to `None`. Adapters that write project-local
57    /// files (e.g., Codex `<cwd>/AGENTS.md`, Cursor `.cursorrules`) should
58    /// use [`Detection::with_project_root`] instead.
59    ///
60    /// The type is `#[non_exhaustive]`, so external crates cannot use a
61    /// struct literal. This constructor is the additive public surface that
62    /// lets downstream consumers build the value with a single call.
63    pub fn new(
64        installed: bool,
65        config_root: Option<std::path::PathBuf>,
66        version: Option<String>,
67    ) -> Self {
68        Self {
69            installed,
70            config_root,
71            project_root: None,
72            version,
73        }
74    }
75
76    /// Construct a `Detection` for an adapter that writes both home-rooted
77    /// and project-local files.
78    ///
79    /// Example: Codex writes `~/.codex/config.toml` (home) plus
80    /// `<cwd>/AGENTS.md` (project); Cursor writes `~/.cursor/mcp.json`
81    /// (home) plus `<cwd>/.cursorrules` (project). The Stage 5 APPLY
82    /// sandbox check accepts writes inside EITHER `config_root` OR
83    /// `project_root`.
84    pub fn with_project_root(
85        installed: bool,
86        config_root: Option<std::path::PathBuf>,
87        project_root: Option<std::path::PathBuf>,
88        version: Option<String>,
89    ) -> Self {
90        Self {
91            installed,
92            config_root,
93            project_root,
94            version,
95        }
96    }
97}
98
99/// Adapter error type. `#[non_exhaustive]` so future variants are non-breaking.
100#[derive(Debug, Error)]
101#[non_exhaustive]
102pub enum AdapterError {
103    #[error("adapter id {0:?} is not supported in this build")]
104    Unsupported(AdapterId),
105
106    #[error("config root unreadable: {0}")]
107    ConfigRootUnreadable(std::path::PathBuf),
108
109    #[error("io: {0}")]
110    Io(#[from] std::io::Error),
111
112    #[error("serialization: {0}")]
113    Serde(#[from] serde_json::Error),
114}
115
116/// Adapter trait. Object-safe; pin async to I/O methods only.
117#[async_trait::async_trait]
118pub trait Adapter: Send + Sync {
119    fn id(&self) -> AdapterId;
120
121    fn supports(&self, _cap: Capability) -> bool {
122        false
123    }
124
125    async fn detect(&self) -> Result<Detection, AdapterError>;
126
127    async fn install_command(&self) -> Result<String, AdapterError>;
128}
129
130/// Default adapter set known to this build. Empty in this scaffold; follow-up
131/// changes add entries as concrete adapter implementations land.
132pub const DEFAULT_ADAPTER_IDS: &[AdapterId] = &[];
133
134/// Switch-arm factory. Closed match; adding a new `AdapterId` variant breaks
135/// the build until this function is updated.
136pub fn new_adapter(id: AdapterId) -> Result<Arc<dyn Adapter>, AdapterError> {
137    match id {
138        AdapterId::ClaudeCode
139        | AdapterId::CodexCli
140        | AdapterId::OpenCode
141        | AdapterId::Cursor
142        | AdapterId::Cline => Err(AdapterError::Unsupported(id)),
143    }
144}
145
146/// Registry of adapter handles, keyed by [`AdapterId`].
147#[derive(Default)]
148pub struct AdapterRegistry {
149    inner: std::collections::HashMap<AdapterId, Arc<dyn Adapter>>,
150}
151
152impl AdapterRegistry {
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    /// Register an adapter handle keyed by its [`AdapterId`]. Returns the
158    /// previous handle for that id if one was already registered, mirroring
159    /// [`std::collections::HashMap::insert`]. Callers can detect duplicate
160    /// registrations by checking for `Some(_)`.
161    pub fn register(&mut self, adapter: Arc<dyn Adapter>) -> Option<Arc<dyn Adapter>> {
162        self.inner.insert(adapter.id(), adapter)
163    }
164
165    pub fn get(&self, id: AdapterId) -> Option<Arc<dyn Adapter>> {
166        self.inner.get(&id).cloned()
167    }
168}
169
170/// Build a registry pre-populated with [`DEFAULT_ADAPTER_IDS`]. Empty in this
171/// scaffold; follow-up changes populate it as adapter implementations land.
172pub fn default_registry() -> Result<AdapterRegistry, AdapterError> {
173    let mut registry = AdapterRegistry::new();
174    for id in DEFAULT_ADAPTER_IDS {
175        let _ = registry.register(new_adapter(*id)?);
176    }
177    Ok(registry)
178}