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