Skip to main content

lifeloop/
manifest_contract.rs

1//! Adapter manifest types and built-in manifest registry.
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use crate::{
7    AdapterRole, FailureClass, IntegrationMode, LifecycleEventKind, SCHEMA_VERSION, SupportState,
8    ValidationError, require_non_empty,
9};
10
11/// Manifest placement classes — the trust-neutral, lifecycle-timing
12/// vocabulary the adapter manifest uses to declare placement support.
13///
14/// **Distinct from [`crate::PlacementClass`]**, which is the routing
15/// vocabulary the runtime uses on `acceptable_placements` for
16/// concrete payload delivery. The manifest declares *capability*;
17/// the payload envelope declares *routing intent*. A future
18/// revision may unify them; the current contract keeps them
19/// separate so manifest evolution does not churn payload routing.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ManifestPlacementClass {
23    /// Before any frame opens; e.g. session-init context.
24    PreSession,
25    /// Leading edge of a frame, before user/task input arrives.
26    PreFrameLeading,
27    /// Trailing edge of a frame, after input but before model execution.
28    PreFrameTrailing,
29    /// Inside a tool-result envelope returned to the model.
30    ToolResult,
31    /// Through an operator or manual surface (skill, command, wrapper).
32    ManualOperator,
33}
34
35impl ManifestPlacementClass {
36    pub const ALL: &'static [Self] = &[
37        Self::PreSession,
38        Self::PreFrameLeading,
39        Self::PreFrameTrailing,
40        Self::ToolResult,
41        Self::ManualOperator,
42    ];
43}
44
45/// Per-event capability claim inside a manifest.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct ManifestLifecycleEventSupport {
49    pub support: SupportState,
50    /// Integration modes through which the adapter delivers this event.
51    /// May be empty when `support` is `unavailable`.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub modes: Vec<IntegrationMode>,
54}
55
56/// Per-placement capability claim inside a manifest.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct ManifestPlacementSupport {
60    pub support: SupportState,
61    /// Placement size limit in bytes when the adapter declares one.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub max_bytes: Option<u64>,
64}
65
66/// Capability claim describing how the adapter surfaces
67/// `context.pressure_observed` lifecycle evidence.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct ManifestContextPressure {
71    pub support: SupportState,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub evidence: Option<String>,
74}
75
76/// Capability claim describing receipt emission and ledger support.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(deny_unknown_fields)]
79pub struct ManifestReceipts {
80    /// Adapter emits its own native receipts.
81    pub native: bool,
82    /// Lifeloop synthesizes receipts on the adapter's behalf.
83    pub lifeloop_synthesized: bool,
84    /// Durable cross-invocation receipt ledger.
85    pub receipt_ledger: SupportState,
86}
87
88/// Per-id support claims for harness identity correlation. Optional
89/// on the manifest because a telemetry-only adapter may not expose
90/// any of these.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct ManifestSessionIdentity {
94    pub harness_session_id: SupportState,
95    pub harness_run_id: SupportState,
96    pub harness_task_id: SupportState,
97}
98
99/// Capability claim for the adapter's session-rename surface.
100/// Optional on the manifest; absent means "no rename concept."
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct ManifestSessionRename {
104    pub support: SupportState,
105}
106
107/// Capability claim for reset/continuation renewal across a harness
108/// boundary. Lifeloop reports whether the adapter can prove the
109/// lifecycle path and delivery support; clients own renewal leases,
110/// continuation-token policy, and thread/session binding.
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct ManifestRenewal {
114    pub reset: ManifestRenewalReset,
115    pub continuation: ManifestRenewalContinuation,
116    /// Host integration profile ids required for this renewal path
117    /// when the claim is not available through every install shape.
118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
119    pub profiles: Vec<String>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub evidence: Option<String>,
122}
123
124/// Reset-side renewal capability. `native`, `wrapper_mediated`, and
125/// `manual` are separate so a manifest can distinguish a real harness
126/// reset surface, a launcher/wrapper path, an operator-only path, and
127/// the all-`unavailable` "no safe reset path" case.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(deny_unknown_fields)]
130pub struct ManifestRenewalReset {
131    pub native: SupportState,
132    pub wrapper_mediated: SupportState,
133    pub manual: SupportState,
134}
135
136/// Continuation-side renewal capability. Observation means the
137/// adapter can prove a continuation boundary happened; payload
138/// delivery means it can carry client-provided continuation facts
139/// across that boundary.
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct ManifestRenewalContinuation {
143    pub observation: SupportState,
144    pub payload_delivery: SupportState,
145}
146
147/// Capability claim for operator approval/intervention surfaces.
148/// Optional; absent means "no operator surface."
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(deny_unknown_fields)]
151pub struct ManifestApprovalSurface {
152    pub support: SupportState,
153}
154
155/// One telemetry source the adapter exposes for lifecycle evidence.
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(deny_unknown_fields)]
158pub struct ManifestTelemetrySource {
159    pub source: String,
160    pub support: SupportState,
161}
162
163/// One pre-declared capability degradation the adapter ships with.
164/// Lets a manifest say "this build's `context_pressure` was native
165/// upstream but is currently `unavailable` here" without firing a
166/// runtime `capability.degraded` event.
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(deny_unknown_fields)]
169pub struct ManifestKnownDegradation {
170    pub capability: String,
171    pub previous_support: SupportState,
172    pub current_support: SupportState,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub evidence: Option<String>,
175}
176
177/// Adapter manifest. Issue #6 lands the full shape; pre-issue-#6
178/// drafts shipped a stub with only schema_version, adapter_id,
179/// adapter_version, display_name, roles, integration_modes, and
180/// lifecycle_events.
181///
182/// `contract_version` (this struct's first field) carries the
183/// Lifeloop contract version label (e.g. `lifeloop.v0.2`),
184/// independent of `adapter_version`. The two are separate so
185/// adapters can iterate without bumping the contract.
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct AdapterManifest {
189    pub contract_version: String,
190    pub adapter_id: String,
191    pub adapter_version: String,
192    pub display_name: String,
193    pub role: AdapterRole,
194    pub integration_modes: Vec<IntegrationMode>,
195    pub lifecycle_events: BTreeMap<LifecycleEventKind, ManifestLifecycleEventSupport>,
196    pub placement: BTreeMap<ManifestPlacementClass, ManifestPlacementSupport>,
197    pub context_pressure: ManifestContextPressure,
198    pub receipts: ManifestReceipts,
199
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub session_identity: Option<ManifestSessionIdentity>,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub session_rename: Option<ManifestSessionRename>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub renewal: Option<ManifestRenewal>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub approval_surface: Option<ManifestApprovalSurface>,
208    #[serde(default, skip_serializing_if = "Vec::is_empty")]
209    pub failure_modes: Vec<FailureClass>,
210    #[serde(default, skip_serializing_if = "Vec::is_empty")]
211    pub telemetry_sources: Vec<ManifestTelemetrySource>,
212    #[serde(default, skip_serializing_if = "Vec::is_empty")]
213    pub known_degradations: Vec<ManifestKnownDegradation>,
214}
215
216impl AdapterManifest {
217    pub fn validate(&self) -> Result<(), ValidationError> {
218        if self.contract_version != SCHEMA_VERSION {
219            return Err(ValidationError::SchemaVersionMismatch {
220                expected: SCHEMA_VERSION.to_string(),
221                found: self.contract_version.clone(),
222            });
223        }
224        require_non_empty(&self.adapter_id, "manifest.adapter_id")?;
225        require_non_empty(&self.adapter_version, "manifest.adapter_version")?;
226        require_non_empty(&self.display_name, "manifest.display_name")?;
227        if self.integration_modes.is_empty() {
228            return Err(ValidationError::InvalidManifest(
229                "manifest.integration_modes must declare at least one integration mode".into(),
230            ));
231        }
232        for deg in &self.known_degradations {
233            require_non_empty(&deg.capability, "manifest.known_degradations[].capability")?;
234        }
235        for src in &self.telemetry_sources {
236            require_non_empty(&src.source, "manifest.telemetry_sources[].source")?;
237        }
238        if let Some(renewal) = &self.renewal
239            && let Some(evidence) = &renewal.evidence
240        {
241            require_non_empty(evidence, "manifest.renewal.evidence")?;
242        }
243        if let Some(renewal) = &self.renewal {
244            for profile in &renewal.profiles {
245                require_non_empty(profile, "manifest.renewal.profiles[]")?;
246            }
247        }
248        Ok(())
249    }
250}
251
252// ----------------------------------------------------------------------------
253// Manifest registry
254// ----------------------------------------------------------------------------
255
256/// Conformance posture of a registered adapter.
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
258#[serde(rename_all = "snake_case")]
259pub enum ConformanceLevel {
260    /// v1-conformance adapter: every capability claim that depends on
261    /// extracted code (asset rendering, telemetry, placement) is
262    /// paired with a test that verifies the claim.
263    V1Conformance,
264    /// Initial manifest shipped without full claim verification. The
265    /// claims describe expected behavior so clients can negotiate, but
266    /// the registry does not yet run capability-claim tests for them.
267    PreConformance,
268}
269
270/// Registry entry pairing an [`AdapterManifest`] with its
271/// [`ConformanceLevel`].
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct RegisteredAdapter {
274    pub manifest: AdapterManifest,
275    pub conformance: ConformanceLevel,
276}
277
278/// Built-in adapter manifest registry. Order is stable so callers
279/// that render a `lifeloop adapters` listing get a predictable
280/// output without sorting client-side.
281pub fn manifest_registry() -> Vec<RegisteredAdapter> {
282    vec![
283        RegisteredAdapter {
284            manifest: codex_manifest(),
285            conformance: ConformanceLevel::V1Conformance,
286        },
287        RegisteredAdapter {
288            manifest: claude_manifest(),
289            conformance: ConformanceLevel::V1Conformance,
290        },
291        RegisteredAdapter {
292            manifest: hermes_manifest(),
293            conformance: ConformanceLevel::PreConformance,
294        },
295        RegisteredAdapter {
296            manifest: openclaw_manifest(),
297            conformance: ConformanceLevel::PreConformance,
298        },
299        RegisteredAdapter {
300            manifest: gemini_manifest(),
301            conformance: ConformanceLevel::PreConformance,
302        },
303        RegisteredAdapter {
304            manifest: opencode_manifest(),
305            conformance: ConformanceLevel::PreConformance,
306        },
307    ]
308}
309
310/// Resolve a registered adapter by `adapter_id`. Returns `None` for
311/// unknown ids.
312pub fn lookup_manifest(adapter_id: &str) -> Option<RegisteredAdapter> {
313    manifest_registry()
314        .into_iter()
315        .find(|entry| entry.manifest.adapter_id == adapter_id)
316}
317
318fn synthesized() -> SupportState {
319    SupportState::Synthesized
320}
321
322fn native() -> SupportState {
323    SupportState::Native
324}
325
326fn unavailable() -> SupportState {
327    SupportState::Unavailable
328}
329
330fn manual() -> SupportState {
331    SupportState::Manual
332}
333
334/// Codex manifest. Native-hook integration covers Codex's stable hook
335/// surface, including `PreCompact` in Codex CLI 0.129+. Capability
336/// claims here are paired with verification tests in
337/// `tests/manifest_claims.rs`.
338pub fn codex_manifest() -> AdapterManifest {
339    let lifecycle_events = BTreeMap::from([
340        (
341            LifecycleEventKind::SessionStarting,
342            ManifestLifecycleEventSupport {
343                support: native(),
344                modes: vec![IntegrationMode::NativeHook],
345            },
346        ),
347        (
348            LifecycleEventKind::SessionStarted,
349            ManifestLifecycleEventSupport {
350                support: native(),
351                modes: vec![IntegrationMode::NativeHook],
352            },
353        ),
354        (
355            LifecycleEventKind::FrameOpening,
356            ManifestLifecycleEventSupport {
357                support: native(),
358                modes: vec![IntegrationMode::NativeHook],
359            },
360        ),
361        (
362            LifecycleEventKind::FrameOpened,
363            ManifestLifecycleEventSupport {
364                support: synthesized(),
365                modes: vec![IntegrationMode::NativeHook],
366            },
367        ),
368        (
369            LifecycleEventKind::ContextPressureObserved,
370            ManifestLifecycleEventSupport {
371                support: native(),
372                modes: vec![IntegrationMode::NativeHook],
373            },
374        ),
375        (
376            LifecycleEventKind::ContextCompacted,
377            ManifestLifecycleEventSupport {
378                support: native(),
379                modes: vec![IntegrationMode::NativeHook],
380            },
381        ),
382        (
383            LifecycleEventKind::FrameEnding,
384            ManifestLifecycleEventSupport {
385                support: native(),
386                modes: vec![IntegrationMode::NativeHook],
387            },
388        ),
389        (
390            LifecycleEventKind::FrameEnded,
391            ManifestLifecycleEventSupport {
392                support: native(),
393                modes: vec![IntegrationMode::NativeHook],
394            },
395        ),
396        (
397            LifecycleEventKind::SessionEnding,
398            ManifestLifecycleEventSupport {
399                support: unavailable(),
400                modes: Vec::new(),
401            },
402        ),
403        (
404            LifecycleEventKind::SessionEnded,
405            ManifestLifecycleEventSupport {
406                support: unavailable(),
407                modes: Vec::new(),
408            },
409        ),
410        (
411            LifecycleEventKind::SupervisorTick,
412            ManifestLifecycleEventSupport {
413                support: unavailable(),
414                modes: Vec::new(),
415            },
416        ),
417        (
418            LifecycleEventKind::CapabilityDegraded,
419            ManifestLifecycleEventSupport {
420                support: synthesized(),
421                modes: vec![IntegrationMode::NativeHook],
422            },
423        ),
424        (
425            LifecycleEventKind::ReceiptEmitted,
426            ManifestLifecycleEventSupport {
427                support: synthesized(),
428                modes: vec![IntegrationMode::NativeHook],
429            },
430        ),
431        (
432            LifecycleEventKind::ReceiptGapDetected,
433            ManifestLifecycleEventSupport {
434                support: unavailable(),
435                modes: Vec::new(),
436            },
437        ),
438    ]);
439
440    let placement = BTreeMap::from([
441        (
442            ManifestPlacementClass::PreSession,
443            ManifestPlacementSupport {
444                support: native(),
445                max_bytes: Some(8192),
446            },
447        ),
448        (
449            ManifestPlacementClass::PreFrameLeading,
450            ManifestPlacementSupport {
451                support: native(),
452                max_bytes: Some(8192),
453            },
454        ),
455        (
456            ManifestPlacementClass::PreFrameTrailing,
457            ManifestPlacementSupport {
458                support: unavailable(),
459                max_bytes: None,
460            },
461        ),
462        (
463            ManifestPlacementClass::ToolResult,
464            ManifestPlacementSupport {
465                support: unavailable(),
466                max_bytes: None,
467            },
468        ),
469        (
470            ManifestPlacementClass::ManualOperator,
471            ManifestPlacementSupport {
472                support: manual(),
473                max_bytes: None,
474            },
475        ),
476    ]);
477
478    AdapterManifest {
479        contract_version: SCHEMA_VERSION.to_string(),
480        adapter_id: "codex".into(),
481        adapter_version: "0.1.0".into(),
482        display_name: "Codex".into(),
483        role: AdapterRole::PrimaryWorker,
484        integration_modes: vec![IntegrationMode::NativeHook, IntegrationMode::ManualSkill],
485        lifecycle_events,
486        placement,
487        context_pressure: ManifestContextPressure {
488            support: native(),
489            evidence: Some(
490                "Codex CLI 0.129 exposes PreCompact before context pressure handling and PostCompact after context compacts"
491                    .into(),
492            ),
493        },
494        receipts: ManifestReceipts {
495            native: false,
496            lifeloop_synthesized: true,
497            receipt_ledger: unavailable(),
498        },
499        session_identity: Some(ManifestSessionIdentity {
500            harness_session_id: native(),
501            harness_run_id: synthesized(),
502            harness_task_id: unavailable(),
503        }),
504        session_rename: None,
505        renewal: Some(ManifestRenewal {
506            reset: ManifestRenewalReset {
507                native: unavailable(),
508                wrapper_mediated: synthesized(),
509                manual: manual(),
510            },
511            continuation: ManifestRenewalContinuation {
512                observation: native(),
513                payload_delivery: synthesized(),
514            },
515            profiles: vec![crate::host_assets::CCD_RENEWAL_PROFILE.id.into()],
516            evidence: Some(
517                "Codex Stop block-as-continuation evidence plus the opt-in renewal host-hook profile proves wrapper-mediated reset prepare and out-of-band continuation-token delivery"
518                    .into(),
519            ),
520        }),
521        approval_surface: None,
522        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
523        telemetry_sources: Vec::new(),
524        known_degradations: Vec::new(),
525    }
526}
527
528/// Claude manifest. Native-hook integration via `.claude/settings.json`.
529pub fn claude_manifest() -> AdapterManifest {
530    let lifecycle_events = BTreeMap::from([
531        (
532            LifecycleEventKind::SessionStarting,
533            ManifestLifecycleEventSupport {
534                support: native(),
535                modes: vec![IntegrationMode::NativeHook],
536            },
537        ),
538        (
539            LifecycleEventKind::SessionStarted,
540            ManifestLifecycleEventSupport {
541                support: native(),
542                modes: vec![IntegrationMode::NativeHook],
543            },
544        ),
545        (
546            LifecycleEventKind::FrameOpening,
547            ManifestLifecycleEventSupport {
548                support: native(),
549                modes: vec![IntegrationMode::NativeHook],
550            },
551        ),
552        (
553            LifecycleEventKind::FrameOpened,
554            ManifestLifecycleEventSupport {
555                support: native(),
556                modes: vec![IntegrationMode::NativeHook],
557            },
558        ),
559        (
560            LifecycleEventKind::ContextPressureObserved,
561            ManifestLifecycleEventSupport {
562                support: native(),
563                modes: vec![IntegrationMode::NativeHook],
564            },
565        ),
566        (
567            LifecycleEventKind::ContextCompacted,
568            ManifestLifecycleEventSupport {
569                support: unavailable(),
570                modes: Vec::new(),
571            },
572        ),
573        (
574            LifecycleEventKind::FrameEnding,
575            ManifestLifecycleEventSupport {
576                support: native(),
577                modes: vec![IntegrationMode::NativeHook],
578            },
579        ),
580        (
581            LifecycleEventKind::FrameEnded,
582            ManifestLifecycleEventSupport {
583                support: native(),
584                modes: vec![IntegrationMode::NativeHook],
585            },
586        ),
587        (
588            LifecycleEventKind::SessionEnding,
589            ManifestLifecycleEventSupport {
590                support: native(),
591                modes: vec![IntegrationMode::NativeHook],
592            },
593        ),
594        (
595            LifecycleEventKind::SessionEnded,
596            ManifestLifecycleEventSupport {
597                support: native(),
598                modes: vec![IntegrationMode::NativeHook],
599            },
600        ),
601        (
602            LifecycleEventKind::SupervisorTick,
603            ManifestLifecycleEventSupport {
604                support: unavailable(),
605                modes: Vec::new(),
606            },
607        ),
608        (
609            LifecycleEventKind::CapabilityDegraded,
610            ManifestLifecycleEventSupport {
611                support: synthesized(),
612                modes: vec![IntegrationMode::NativeHook],
613            },
614        ),
615        (
616            LifecycleEventKind::ReceiptEmitted,
617            ManifestLifecycleEventSupport {
618                support: synthesized(),
619                modes: vec![IntegrationMode::NativeHook],
620            },
621        ),
622        (
623            LifecycleEventKind::ReceiptGapDetected,
624            ManifestLifecycleEventSupport {
625                support: unavailable(),
626                modes: Vec::new(),
627            },
628        ),
629    ]);
630
631    let placement = BTreeMap::from([
632        (
633            ManifestPlacementClass::PreSession,
634            ManifestPlacementSupport {
635                support: native(),
636                max_bytes: Some(16_384),
637            },
638        ),
639        (
640            ManifestPlacementClass::PreFrameLeading,
641            ManifestPlacementSupport {
642                support: native(),
643                max_bytes: Some(16_384),
644            },
645        ),
646        (
647            ManifestPlacementClass::PreFrameTrailing,
648            ManifestPlacementSupport {
649                support: unavailable(),
650                max_bytes: None,
651            },
652        ),
653        (
654            ManifestPlacementClass::ToolResult,
655            ManifestPlacementSupport {
656                support: unavailable(),
657                max_bytes: None,
658            },
659        ),
660        (
661            ManifestPlacementClass::ManualOperator,
662            ManifestPlacementSupport {
663                support: manual(),
664                max_bytes: None,
665            },
666        ),
667    ]);
668
669    AdapterManifest {
670        contract_version: SCHEMA_VERSION.to_string(),
671        adapter_id: "claude".into(),
672        adapter_version: "0.1.0".into(),
673        display_name: "Claude".into(),
674        role: AdapterRole::PrimaryWorker,
675        integration_modes: vec![IntegrationMode::NativeHook],
676        lifecycle_events,
677        placement,
678        context_pressure: ManifestContextPressure {
679            support: native(),
680            evidence: Some(
681                "Claude emits PreCompact and SessionEnd events that map directly to context.pressure_observed"
682                    .into(),
683            ),
684        },
685        receipts: ManifestReceipts {
686            native: false,
687            lifeloop_synthesized: true,
688            receipt_ledger: unavailable(),
689        },
690        session_identity: Some(ManifestSessionIdentity {
691            harness_session_id: native(),
692            harness_run_id: synthesized(),
693            harness_task_id: unavailable(),
694        }),
695        session_rename: None,
696        renewal: None,
697        approval_surface: None,
698        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
699        telemetry_sources: Vec::new(),
700        known_degradations: Vec::new(),
701    }
702}
703
704/// Hermes pre-conformance manifest. Reference-adapter integration
705/// supplied as a JSON descriptor at the path declared in
706/// [`crate::host_assets::HERMES_TARGET_ADAPTER`].
707pub fn hermes_manifest() -> AdapterManifest {
708    pre_conformance_reference_adapter_manifest("hermes", "Hermes")
709}
710
711/// OpenClaw pre-conformance manifest.
712pub fn openclaw_manifest() -> AdapterManifest {
713    pre_conformance_reference_adapter_manifest("openclaw", "OpenClaw")
714}
715
716/// Gemini pre-conformance manifest.
717pub fn gemini_manifest() -> AdapterManifest {
718    pre_conformance_telemetry_only_manifest("gemini", "Gemini")
719}
720
721/// OpenCode pre-conformance manifest.
722pub fn opencode_manifest() -> AdapterManifest {
723    pre_conformance_telemetry_only_manifest("opencode", "OpenCode")
724}
725
726fn pre_conformance_reference_adapter_manifest(
727    adapter_id: &str,
728    display_name: &str,
729) -> AdapterManifest {
730    let lifecycle_events = BTreeMap::from([
731        (
732            LifecycleEventKind::SessionStarting,
733            ManifestLifecycleEventSupport {
734                support: SupportState::Partial,
735                modes: vec![IntegrationMode::ReferenceAdapter],
736            },
737        ),
738        (
739            LifecycleEventKind::SessionStarted,
740            ManifestLifecycleEventSupport {
741                support: SupportState::Partial,
742                modes: vec![IntegrationMode::ReferenceAdapter],
743            },
744        ),
745        (
746            LifecycleEventKind::FrameOpening,
747            ManifestLifecycleEventSupport {
748                support: SupportState::Partial,
749                modes: vec![IntegrationMode::ReferenceAdapter],
750            },
751        ),
752        (
753            LifecycleEventKind::FrameEnded,
754            ManifestLifecycleEventSupport {
755                support: SupportState::Partial,
756                modes: vec![IntegrationMode::ReferenceAdapter],
757            },
758        ),
759        (
760            LifecycleEventKind::SessionEnded,
761            ManifestLifecycleEventSupport {
762                support: SupportState::Partial,
763                modes: vec![IntegrationMode::ReferenceAdapter],
764            },
765        ),
766    ]);
767
768    let placement = BTreeMap::from([
769        (
770            ManifestPlacementClass::PreSession,
771            ManifestPlacementSupport {
772                support: SupportState::Partial,
773                max_bytes: None,
774            },
775        ),
776        (
777            ManifestPlacementClass::PreFrameLeading,
778            ManifestPlacementSupport {
779                support: SupportState::Partial,
780                max_bytes: None,
781            },
782        ),
783        (
784            ManifestPlacementClass::ManualOperator,
785            ManifestPlacementSupport {
786                support: SupportState::Manual,
787                max_bytes: None,
788            },
789        ),
790    ]);
791
792    AdapterManifest {
793        contract_version: SCHEMA_VERSION.to_string(),
794        adapter_id: adapter_id.to_string(),
795        adapter_version: "0.0.1-pre".into(),
796        display_name: display_name.to_string(),
797        role: AdapterRole::Worker,
798        integration_modes: vec![IntegrationMode::ReferenceAdapter],
799        lifecycle_events,
800        placement,
801        context_pressure: ManifestContextPressure {
802            support: SupportState::Partial,
803            evidence: None,
804        },
805        receipts: ManifestReceipts {
806            native: false,
807            lifeloop_synthesized: true,
808            receipt_ledger: SupportState::Unavailable,
809        },
810        session_identity: None,
811        session_rename: None,
812        renewal: None,
813        approval_surface: None,
814        failure_modes: Vec::new(),
815        telemetry_sources: Vec::new(),
816        known_degradations: Vec::new(),
817    }
818}
819
820fn pre_conformance_telemetry_only_manifest(
821    adapter_id: &str,
822    display_name: &str,
823) -> AdapterManifest {
824    let lifecycle_events = BTreeMap::from([
825        (
826            LifecycleEventKind::SessionStarting,
827            ManifestLifecycleEventSupport {
828                support: SupportState::Partial,
829                modes: vec![IntegrationMode::TelemetryOnly],
830            },
831        ),
832        (
833            LifecycleEventKind::ContextPressureObserved,
834            ManifestLifecycleEventSupport {
835                support: SupportState::Partial,
836                modes: vec![IntegrationMode::TelemetryOnly],
837            },
838        ),
839        (
840            LifecycleEventKind::SessionEnded,
841            ManifestLifecycleEventSupport {
842                support: SupportState::Partial,
843                modes: vec![IntegrationMode::TelemetryOnly],
844            },
845        ),
846    ]);
847
848    let placement = BTreeMap::from([(
849        ManifestPlacementClass::ManualOperator,
850        ManifestPlacementSupport {
851            support: SupportState::Manual,
852            max_bytes: None,
853        },
854    )]);
855
856    AdapterManifest {
857        contract_version: SCHEMA_VERSION.to_string(),
858        adapter_id: adapter_id.to_string(),
859        adapter_version: "0.0.1-pre".into(),
860        display_name: display_name.to_string(),
861        role: AdapterRole::Observer,
862        integration_modes: vec![IntegrationMode::TelemetryOnly],
863        lifecycle_events,
864        placement,
865        context_pressure: ManifestContextPressure {
866            support: SupportState::Partial,
867            evidence: None,
868        },
869        receipts: ManifestReceipts {
870            native: false,
871            lifeloop_synthesized: true,
872            receipt_ledger: SupportState::Unavailable,
873        },
874        session_identity: None,
875        session_rename: None,
876        renewal: None,
877        approval_surface: None,
878        failure_modes: Vec::new(),
879        telemetry_sources: Vec::new(),
880        known_degradations: Vec::new(),
881    }
882}