Skip to main content

gate4agent_adapters/
lib.rs

1//! Pure provider adapter contracts and implementations.
2//!
3//! Role: adapter shell logic without OS authority.
4//! Owns: revisioned adapter definitions and pure provider parsing/building.
5//! Exports: adapter registry and family-specific pure adapters.
6//! Forbidden: async, locks, channels, process, filesystem, database, network,
7//! credentials, and product presentation policy.
8
9mod capability;
10mod history;
11mod one_shot;
12mod pty_identity;
13mod resume;
14mod session_options;
15
16use gate4agent_types::{AdapterBinding, AdapterFamily, AdapterId, AdapterVerification, AgentId};
17use std::collections::BTreeMap;
18use std::sync::OnceLock;
19use thiserror::Error;
20
21pub use capability::{
22    capability_probe_plan, parse_capability_models, CapabilityProbeAdapterError,
23    CapabilityProbePlan, CAPABILITY_PROBE_OUTPUT_MAX_BYTES, CAPABILITY_PROBE_REVISION,
24};
25
26pub use history::{
27    history_source_variants, parse_history, HistoryAdapterError, HistoryDocument, HistoryMessage,
28    HistoryRole, HistorySession, HistorySourceLayout, HistorySourceVariant,
29    HISTORY_DOCUMENT_MAX_BYTES, HISTORY_MESSAGE_MAX_CHARS, HISTORY_METADATA_MAX_BYTES,
30    HISTORY_STORED_MESSAGES_MAX,
31};
32pub use one_shot::{
33    one_shot_spec, one_shot_specs, resolve_one_shot_plan,
34    resolve_one_shot_plan_with_persistence, OneShotAdapterError, OneShotAdapterSpec,
35    OneShotModelSource, OneShotModelSpec, OneShotPlan, OneShotPromptDelivery,
36    OneShotSessionPersistence, OneShotThinkingLevel, CLAUDE_CODE_INLINE_REVISION,
37    CODEX_CLI_INLINE_REVISION, KIMI_CODE_INLINE_REVISION, ONE_SHOT_OUTPUT_MAX_BYTES,
38    ONE_SHOT_REVISION, ONE_SHOT_THINKING_OPTION_ID, ONE_SHOT_TIMEOUT_SECONDS,
39};
40pub use pty_identity::{
41    CodexPtySessionIdentityExtractor, KimiPtySessionIdentityExtractor,
42    KIMI_PTY_SESSION_ID_MAX_BYTES,
43};
44pub use resume::{
45    build_resume_plan, build_resume_plan_for_identity, ResumeAdapterError, ResumePlan,
46    RESUME_SESSION_ID_MAX_BYTES,
47};
48pub use session_options::{
49    merge_session_option_models, parse_session_option_models, plan_mid_session_action,
50    plan_mid_session_option, resolve_session_option_launch, session_option_catalog,
51    AgentSessionOptionCatalog, ResolvedSessionOptionLaunch, SessionOption,
52    SessionOptionAdapterError, SessionOptionApply, SessionOptionArgumentOverride,
53    SessionOptionCategory, SessionOptionChoice, SessionOptionInteractionDetection,
54    SessionOptionKind, SessionOptionLaunchApplication, SessionOptionMidSessionApplication,
55    SessionOptionMidSessionPlan, SessionOptionModel, SessionOptionModelListSpec,
56    SESSION_OPTION_CATALOG_REVISION, SESSION_OPTION_MODELS_MAX,
57    SESSION_OPTION_MODEL_LIST_MAX_BYTES,
58};
59
60pub const BUILTIN_ADAPTER_REVISION: &str = "gate4agent-adapter/v1";
61
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct AdapterDescriptor {
64    pub family: AdapterFamily,
65    pub binding: AdapterBinding,
66    pub agents: Vec<AgentId>,
67}
68
69/// Validated registry keyed by adapter family and implementation identity.
70///
71/// The family is part of the key: one stable adapter ID may intentionally
72/// implement several independent families without turning them into a blanket
73/// provider-support claim.
74#[derive(Clone, Debug, Default)]
75pub struct AdapterRegistry {
76    descriptors: BTreeMap<(AdapterFamily, AdapterId), AdapterDescriptor>,
77}
78
79impl AdapterRegistry {
80    pub fn new(
81        descriptors: impl IntoIterator<Item = AdapterDescriptor>,
82    ) -> Result<Self, AdapterRegistryError> {
83        let mut registry = Self::default();
84        for descriptor in descriptors {
85            registry.insert(descriptor)?;
86        }
87        Ok(registry)
88    }
89
90    pub fn insert(&mut self, descriptor: AdapterDescriptor) -> Result<(), AdapterRegistryError> {
91        descriptor
92            .binding
93            .validate()
94            .map_err(|error| AdapterRegistryError::InvalidBinding {
95                family: descriptor.family,
96                adapter_id: descriptor.binding.id.clone(),
97                message: error.to_string(),
98            })?;
99        if descriptor.agents.is_empty() {
100            return Err(AdapterRegistryError::MissingAgents {
101                family: descriptor.family,
102                adapter_id: descriptor.binding.id,
103            });
104        }
105        let key = (descriptor.family, descriptor.binding.id.clone());
106        if self.descriptors.contains_key(&key) {
107            return Err(AdapterRegistryError::Duplicate {
108                family: key.0,
109                adapter_id: key.1,
110            });
111        }
112        self.descriptors.insert(key, descriptor);
113        Ok(())
114    }
115
116    pub fn get(&self, family: AdapterFamily, id: &AdapterId) -> Option<&AdapterDescriptor> {
117        self.descriptors.get(&(family, id.clone()))
118    }
119
120    pub fn binding(&self, family: AdapterFamily, id: &str) -> Option<&AdapterBinding> {
121        self.descriptors
122            .get(&(family, AdapterId::new(id).ok()?))
123            .map(|descriptor| &descriptor.binding)
124    }
125
126    pub fn supports(&self, family: AdapterFamily, binding: &AdapterBinding) -> bool {
127        self.get(family, &binding.id)
128            .is_some_and(|descriptor| descriptor.binding.revision == binding.revision)
129    }
130
131    pub fn iter(&self) -> impl ExactSizeIterator<Item = &AdapterDescriptor> {
132        self.descriptors.values()
133    }
134}
135
136/// Runtime implementations keyed by the same revisioned family binding used
137/// by the declarative adapter registry.
138///
139/// `T` is intentionally generic: native shells may store process/parser
140/// implementations while WASM consumers may store pure client handlers. The
141/// registry itself performs no I/O and does not prescribe an execution model.
142#[derive(Clone, Debug, Default)]
143pub struct AdapterRuntimeRegistry<T> {
144    runtimes: BTreeMap<(AdapterFamily, AdapterId), AdapterRuntime<T>>,
145}
146
147#[derive(Clone, Debug)]
148struct AdapterRuntime<T> {
149    binding: AdapterBinding,
150    implementation: T,
151}
152
153impl<T> AdapterRuntimeRegistry<T> {
154    pub fn insert(
155        &mut self,
156        family: AdapterFamily,
157        binding: AdapterBinding,
158        implementation: T,
159    ) -> Result<(), AdapterRuntimeRegistryError> {
160        binding
161            .validate()
162            .map_err(|error| AdapterRuntimeRegistryError::InvalidBinding {
163                family,
164                adapter_id: binding.id.clone(),
165                message: error.to_string(),
166            })?;
167        let key = (family, binding.id.clone());
168        if self.runtimes.contains_key(&key) {
169            return Err(AdapterRuntimeRegistryError::Duplicate {
170                family,
171                adapter_id: binding.id,
172            });
173        }
174        self.runtimes.insert(
175            key,
176            AdapterRuntime {
177                binding,
178                implementation,
179            },
180        );
181        Ok(())
182    }
183
184    pub fn resolve(
185        &self,
186        family: AdapterFamily,
187        binding: &AdapterBinding,
188    ) -> Result<&T, AdapterRuntimeRegistryError> {
189        let Some(runtime) = self.runtimes.get(&(family, binding.id.clone())) else {
190            return Err(AdapterRuntimeRegistryError::Unavailable {
191                family,
192                adapter_id: binding.id.clone(),
193                revision: binding.revision.clone(),
194            });
195        };
196        if runtime.binding.revision != binding.revision {
197            return Err(AdapterRuntimeRegistryError::RevisionMismatch {
198                family,
199                adapter_id: binding.id.clone(),
200                requested: binding.revision.clone(),
201                available: runtime.binding.revision.clone(),
202            });
203        }
204        Ok(&runtime.implementation)
205    }
206
207    pub fn len(&self) -> usize {
208        self.runtimes.len()
209    }
210
211    pub fn is_empty(&self) -> bool {
212        self.runtimes.is_empty()
213    }
214}
215
216#[derive(Clone, Debug, Error, Eq, PartialEq)]
217pub enum AdapterRegistryError {
218    #[error("duplicate {family:?} adapter ID: {adapter_id}")]
219    Duplicate {
220        family: AdapterFamily,
221        adapter_id: AdapterId,
222    },
223    #[error("{family:?} adapter {adapter_id} has no bound agents")]
224    MissingAgents {
225        family: AdapterFamily,
226        adapter_id: AdapterId,
227    },
228    #[error("invalid {family:?} adapter {adapter_id}: {message}")]
229    InvalidBinding {
230        family: AdapterFamily,
231        adapter_id: AdapterId,
232        message: String,
233    },
234}
235
236#[derive(Clone, Debug, Error, Eq, PartialEq)]
237pub enum AdapterRuntimeRegistryError {
238    #[error("duplicate runtime for {family:?} adapter {adapter_id}")]
239    Duplicate {
240        family: AdapterFamily,
241        adapter_id: AdapterId,
242    },
243    #[error("invalid runtime binding for {family:?} adapter {adapter_id}: {message}")]
244    InvalidBinding {
245        family: AdapterFamily,
246        adapter_id: AdapterId,
247        message: String,
248    },
249    #[error("runtime unavailable for {family:?} adapter {adapter_id} at revision {revision}")]
250    Unavailable {
251        family: AdapterFamily,
252        adapter_id: AdapterId,
253        revision: String,
254    },
255    #[error(
256        "runtime revision mismatch for {family:?} adapter {adapter_id}: requested {requested}, available {available}"
257    )]
258    RevisionMismatch {
259        family: AdapterFamily,
260        adapter_id: AdapterId,
261        requested: String,
262        available: String,
263    },
264}
265
266pub fn builtin_adapter_registry() -> &'static AdapterRegistry {
267    static REGISTRY: OnceLock<AdapterRegistry> = OnceLock::new();
268    REGISTRY.get_or_init(|| {
269        AdapterRegistry::new(builtin_descriptors())
270            .expect("built-in provider adapter registry must be valid")
271    })
272}
273
274fn builtin_descriptors() -> Vec<AdapterDescriptor> {
275    let mut descriptors = Vec::new();
276    for id in ["claude-code", "codex", "kimi"] {
277        descriptors.push(descriptor(AdapterFamily::PtySemantic, id));
278    }
279    for id in ["claude-code", "codex", "kimi"] {
280        descriptors.push(descriptor(AdapterFamily::Pipe, id));
281    }
282    for id in ["claude", "codex", "kimi"] {
283        let (revision, verification) = match id {
284            "claude" => (CLAUDE_CODE_INLINE_REVISION, AdapterVerification::VendorCanary),
285            "codex" => (CODEX_CLI_INLINE_REVISION, AdapterVerification::VendorCanary),
286            "kimi" => (KIMI_CODE_INLINE_REVISION, AdapterVerification::VendorCanary),
287            _ => (ONE_SHOT_REVISION, AdapterVerification::SyntheticFixture),
288        };
289        descriptors.push(descriptor_with_revision_and_verification(
290            AdapterFamily::OneShot,
291            id,
292            revision,
293            verification,
294        ));
295    }
296    for id in ["claude-code", "codex", "grok", "kimi"] {
297        descriptors.push(descriptor(AdapterFamily::Acp, id));
298    }
299    for id in ["claude-code", "codex", "grok", "kimi"] {
300        descriptors.push(descriptor(AdapterFamily::History, id));
301        descriptors.push(descriptor(AdapterFamily::Resume, id));
302    }
303    for id in ["claude-code", "codex"] {
304        descriptors.push(descriptor_with_revision(
305            AdapterFamily::SessionOptions,
306            id,
307            SESSION_OPTION_CATALOG_REVISION,
308        ));
309    }
310    descriptors
311}
312
313fn descriptor(family: AdapterFamily, id: &str) -> AdapterDescriptor {
314    descriptor_with_revision(family, id, BUILTIN_ADAPTER_REVISION)
315}
316
317fn descriptor_with_revision(family: AdapterFamily, id: &str, revision: &str) -> AdapterDescriptor {
318    descriptor_with_revision_and_verification(
319        family,
320        id,
321        revision,
322        AdapterVerification::SyntheticFixture,
323    )
324}
325
326fn descriptor_with_revision_and_verification(
327    family: AdapterFamily,
328    id: &str,
329    revision: &str,
330    verification: AdapterVerification,
331) -> AdapterDescriptor {
332    let agent_id = if id == "claude-code" { "claude" } else { id };
333    AdapterDescriptor {
334        family,
335        binding: AdapterBinding::new(
336            AdapterId::new(id).expect("hardcoded adapter ID"),
337            revision,
338            verification,
339        )
340        .expect("hardcoded adapter binding"),
341        agents: vec![AgentId::new(agent_id).expect("hardcoded agent ID")],
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn family_is_part_of_the_registry_key() {
351        let registry = builtin_adapter_registry();
352        let claude = AdapterId::new("claude-code").unwrap();
353        assert!(registry.get(AdapterFamily::PtySemantic, &claude).is_some());
354        assert!(registry.get(AdapterFamily::Pipe, &claude).is_some());
355        assert!(registry.get(AdapterFamily::Acp, &claude).is_some());
356        assert!(registry.get(AdapterFamily::History, &claude).is_some());
357        for id in ["codex", "kimi"] {
358            let id = AdapterId::new(id).unwrap();
359            assert!(registry.get(AdapterFamily::PtySemantic, &id).is_some());
360            assert!(registry.get(AdapterFamily::Pipe, &id).is_some());
361            assert!(registry.get(AdapterFamily::Acp, &id).is_some());
362            assert!(registry.get(AdapterFamily::History, &id).is_some());
363        }
364        let grok = AdapterId::new("grok").unwrap();
365        assert!(registry.get(AdapterFamily::PtySemantic, &grok).is_none());
366        assert!(registry.get(AdapterFamily::Pipe, &grok).is_none());
367        assert!(registry.get(AdapterFamily::Acp, &grok).is_some());
368        assert!(registry.get(AdapterFamily::History, &grok).is_some());
369    }
370
371    /// Lifecycle hooks are retired (owner ruling 2026-09-25): sessions are
372    /// observed through ACP where a provider has it, never through a global
373    /// hook install. Neither `Hook` nor `ManagedHook` is registered anymore.
374    #[test]
375    fn hook_families_are_retired_and_unregistered() {
376        let registry = builtin_adapter_registry();
377        assert_eq!(
378            registry
379                .iter()
380                .filter(|descriptor| descriptor.family == AdapterFamily::Hook
381                    || descriptor.family == AdapterFamily::ManagedHook)
382                .count(),
383            0
384        );
385    }
386
387    #[test]
388    fn history_registry_matches_the_fleet_source_inventory() {
389        let actual = builtin_adapter_registry()
390            .iter()
391            .filter(|descriptor| descriptor.family == AdapterFamily::History)
392            .map(|descriptor| descriptor.binding.id.as_str())
393            .collect::<std::collections::BTreeSet<_>>();
394        let expected = ["claude-code", "codex", "grok", "kimi"]
395            .into_iter()
396            .collect::<std::collections::BTreeSet<_>>();
397        assert_eq!(actual, expected);
398    }
399
400    #[test]
401    fn one_shot_registry_tracks_live_and_reference_contract_revisions() {
402        let actual = builtin_adapter_registry()
403            .iter()
404            .filter(|descriptor| descriptor.family == AdapterFamily::OneShot)
405            .map(|descriptor| {
406                (
407                    descriptor.binding.id.as_str(),
408                    descriptor.binding.revision.as_str(),
409                )
410            })
411            .collect::<std::collections::BTreeSet<_>>();
412        let expected = [
413            ("claude", CLAUDE_CODE_INLINE_REVISION),
414            ("codex", CODEX_CLI_INLINE_REVISION),
415            ("kimi", KIMI_CODE_INLINE_REVISION),
416        ]
417        .into_iter()
418        .collect();
419        assert_eq!(actual, expected);
420        for id in ["claude", "codex", "kimi"] {
421            assert_eq!(
422                builtin_adapter_registry()
423                    .binding(AdapterFamily::OneShot, id)
424                    .unwrap()
425                    .verification,
426                AdapterVerification::VendorCanary,
427                "{id}"
428            );
429        }
430    }
431
432    #[test]
433    fn resume_registry_matches_the_supported_live_inventory() {
434        let actual = builtin_adapter_registry()
435            .iter()
436            .filter(|descriptor| descriptor.family == AdapterFamily::Resume)
437            .map(|descriptor| descriptor.binding.id.as_str())
438            .collect::<std::collections::BTreeSet<_>>();
439        let expected = ["claude-code", "codex", "grok", "kimi"].into_iter().collect();
440        assert_eq!(actual, expected);
441    }
442
443    #[test]
444    fn session_option_registry_matches_the_fleet_catalog_inventory() {
445        let actual = builtin_adapter_registry()
446            .iter()
447            .filter(|descriptor| descriptor.family == AdapterFamily::SessionOptions)
448            .map(|descriptor| {
449                (
450                    descriptor.binding.id.as_str(),
451                    descriptor.binding.revision.as_str(),
452                )
453            })
454            .collect::<std::collections::BTreeSet<_>>();
455        let expected = ["claude-code", "codex"]
456            .into_iter()
457            .map(|id| (id, SESSION_OPTION_CATALOG_REVISION))
458            .collect();
459        assert_eq!(actual, expected);
460    }
461
462    #[test]
463    fn capability_probe_registry_is_empty_for_the_current_fleet() {
464        let actual = builtin_adapter_registry()
465            .iter()
466            .filter(|descriptor| descriptor.family == AdapterFamily::CapabilityProbe)
467            .count();
468        assert_eq!(actual, 0);
469    }
470
471    #[test]
472    fn binding_revision_must_match_the_registered_implementation() {
473        let registry = builtin_adapter_registry();
474        let binding = AdapterBinding::new(
475            AdapterId::new("codex").unwrap(),
476            "other-revision",
477            AdapterVerification::Reference,
478        )
479        .unwrap();
480        assert!(!registry.supports(AdapterFamily::Pipe, &binding));
481    }
482
483    #[test]
484    fn runtime_resolution_is_family_and_revision_exact() {
485        let binding = builtin_adapter_registry()
486            .binding(AdapterFamily::Pipe, "codex")
487            .unwrap()
488            .clone();
489        let mut registry = AdapterRuntimeRegistry::default();
490        registry
491            .insert(AdapterFamily::Pipe, binding.clone(), "pipe-runtime")
492            .unwrap();
493
494        assert_eq!(
495            registry.resolve(AdapterFamily::Pipe, &binding).unwrap(),
496            &"pipe-runtime"
497        );
498        assert!(matches!(
499            registry.resolve(AdapterFamily::PtySemantic, &binding),
500            Err(AdapterRuntimeRegistryError::Unavailable { .. })
501        ));
502
503        let other_revision =
504            AdapterBinding::new(binding.id, "other-revision", AdapterVerification::Reference)
505                .unwrap();
506        assert!(matches!(
507            registry.resolve(AdapterFamily::Pipe, &other_revision),
508            Err(AdapterRuntimeRegistryError::RevisionMismatch { .. })
509        ));
510    }
511}