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 binding to their own state.
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/// Adapter manifest. Issue #6 lands the full shape; pre-issue-#6
164/// drafts shipped a stub with only schema_version, adapter_id,
165/// adapter_version, display_name, roles, integration_modes, and
166/// lifecycle_events.
167///
168/// `contract_version` (this struct's first field) carries the
169/// Lifeloop contract version label (e.g. `lifeloop.v0.3`),
170/// independent of `adapter_version`. The two are separate so
171/// adapters can iterate without bumping the contract.
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(deny_unknown_fields)]
174pub struct AdapterManifest {
175    pub contract_version: String,
176    pub adapter_id: String,
177    pub adapter_version: String,
178    pub display_name: String,
179    pub role: AdapterRole,
180    pub integration_modes: Vec<IntegrationMode>,
181    pub lifecycle_events: BTreeMap<LifecycleEventKind, ManifestLifecycleEventSupport>,
182    pub placement: BTreeMap<ManifestPlacementClass, ManifestPlacementSupport>,
183    pub context_pressure: ManifestContextPressure,
184    pub receipts: ManifestReceipts,
185
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub session_identity: Option<ManifestSessionIdentity>,
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub session_rename: Option<ManifestSessionRename>,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub renewal: Option<ManifestRenewal>,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub approval_surface: Option<ManifestApprovalSurface>,
194    #[serde(default, skip_serializing_if = "Vec::is_empty")]
195    pub failure_modes: Vec<FailureClass>,
196    #[serde(default, skip_serializing_if = "Vec::is_empty")]
197    pub telemetry_sources: Vec<ManifestTelemetrySource>,
198}
199
200impl AdapterManifest {
201    pub fn validate(&self) -> Result<(), ValidationError> {
202        if self.contract_version != SCHEMA_VERSION {
203            return Err(ValidationError::SchemaVersionMismatch {
204                expected: SCHEMA_VERSION.to_string(),
205                found: self.contract_version.clone(),
206            });
207        }
208        require_non_empty(&self.adapter_id, "manifest.adapter_id")?;
209        require_non_empty(&self.adapter_version, "manifest.adapter_version")?;
210        require_non_empty(&self.display_name, "manifest.display_name")?;
211        if self.integration_modes.is_empty() {
212            return Err(ValidationError::InvalidManifest(
213                "manifest.integration_modes must declare at least one integration mode".into(),
214            ));
215        }
216        if let Some(evidence) = &self.context_pressure.evidence {
217            require_non_empty(evidence, "manifest.context_pressure.evidence")?;
218        }
219        for src in &self.telemetry_sources {
220            require_non_empty(&src.source, "manifest.telemetry_sources[].source")?;
221        }
222        if let Some(renewal) = &self.renewal
223            && let Some(evidence) = &renewal.evidence
224        {
225            require_non_empty(evidence, "manifest.renewal.evidence")?;
226        }
227        if let Some(renewal) = &self.renewal {
228            for profile in &renewal.profiles {
229                require_non_empty(profile, "manifest.renewal.profiles[]")?;
230            }
231        }
232        Ok(())
233    }
234}
235
236// ----------------------------------------------------------------------------
237// Manifest registry
238// ----------------------------------------------------------------------------
239
240/// Conformance posture of a registered adapter.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243pub enum ConformanceLevel {
244    /// v1-conformance adapter: every capability claim that depends on
245    /// extracted code (asset rendering, telemetry, placement) is
246    /// paired with a test that verifies the claim.
247    V1Conformance,
248    /// Initial manifest shipped without full claim verification. The
249    /// claims describe expected behavior so clients can negotiate, but
250    /// the registry does not yet run capability-claim tests for them.
251    PreConformance,
252}
253
254/// Registry entry pairing an [`AdapterManifest`] with its
255/// [`ConformanceLevel`].
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct RegisteredAdapter {
258    pub manifest: AdapterManifest,
259    pub conformance: ConformanceLevel,
260}
261
262/// Built-in adapter manifest registry. Order is stable so callers
263/// that render a `lifeloop adapters` listing get a predictable
264/// output without sorting client-side.
265pub fn manifest_registry() -> Vec<RegisteredAdapter> {
266    vec![
267        RegisteredAdapter {
268            manifest: codex_manifest(),
269            conformance: ConformanceLevel::V1Conformance,
270        },
271        RegisteredAdapter {
272            manifest: claude_manifest(),
273            conformance: ConformanceLevel::V1Conformance,
274        },
275        RegisteredAdapter {
276            manifest: hermes_manifest(),
277            conformance: ConformanceLevel::PreConformance,
278        },
279        RegisteredAdapter {
280            manifest: openclaw_manifest(),
281            conformance: ConformanceLevel::PreConformance,
282        },
283        RegisteredAdapter {
284            manifest: gemini_manifest(),
285            conformance: ConformanceLevel::PreConformance,
286        },
287        RegisteredAdapter {
288            manifest: opencode_manifest(),
289            conformance: ConformanceLevel::PreConformance,
290        },
291    ]
292}
293
294/// Resolve a registered adapter by `adapter_id`. Returns `None` for
295/// unknown ids.
296pub fn lookup_manifest(adapter_id: &str) -> Option<RegisteredAdapter> {
297    manifest_registry()
298        .into_iter()
299        .find(|entry| entry.manifest.adapter_id == adapter_id)
300}
301
302fn synthesized() -> SupportState {
303    SupportState::Synthesized
304}
305
306fn native() -> SupportState {
307    SupportState::Native
308}
309
310fn unavailable() -> SupportState {
311    SupportState::Unavailable
312}
313
314fn manual() -> SupportState {
315    SupportState::Manual
316}
317
318/// Codex manifest. Native-hook integration covers Codex's stable hook
319/// surface, including `PreCompact` in Codex CLI 0.129+. Capability
320/// claims here are paired with verification tests in
321/// `tests/manifest_claims.rs`.
322pub fn codex_manifest() -> AdapterManifest {
323    let lifecycle_events = BTreeMap::from([
324        (
325            LifecycleEventKind::SessionStarting,
326            ManifestLifecycleEventSupport {
327                support: native(),
328                modes: vec![IntegrationMode::NativeHook],
329            },
330        ),
331        (
332            LifecycleEventKind::SessionStarted,
333            ManifestLifecycleEventSupport {
334                support: native(),
335                modes: vec![IntegrationMode::NativeHook],
336            },
337        ),
338        (
339            LifecycleEventKind::FrameOpening,
340            ManifestLifecycleEventSupport {
341                support: native(),
342                modes: vec![IntegrationMode::NativeHook],
343            },
344        ),
345        (
346            LifecycleEventKind::FrameOpened,
347            ManifestLifecycleEventSupport {
348                support: synthesized(),
349                modes: vec![IntegrationMode::NativeHook],
350            },
351        ),
352        (
353            LifecycleEventKind::ContextPressureObserved,
354            ManifestLifecycleEventSupport {
355                support: native(),
356                modes: vec![IntegrationMode::NativeHook],
357            },
358        ),
359        (
360            LifecycleEventKind::ContextCompacted,
361            ManifestLifecycleEventSupport {
362                support: native(),
363                modes: vec![IntegrationMode::NativeHook],
364            },
365        ),
366        (
367            LifecycleEventKind::FrameEnding,
368            ManifestLifecycleEventSupport {
369                support: native(),
370                modes: vec![IntegrationMode::NativeHook],
371            },
372        ),
373        (
374            LifecycleEventKind::FrameEnded,
375            ManifestLifecycleEventSupport {
376                support: native(),
377                modes: vec![IntegrationMode::NativeHook],
378            },
379        ),
380        (
381            LifecycleEventKind::SessionEnding,
382            ManifestLifecycleEventSupport {
383                support: unavailable(),
384                modes: Vec::new(),
385            },
386        ),
387        (
388            LifecycleEventKind::SessionEnded,
389            ManifestLifecycleEventSupport {
390                support: unavailable(),
391                modes: Vec::new(),
392            },
393        ),
394        (
395            LifecycleEventKind::SupervisorTick,
396            ManifestLifecycleEventSupport {
397                support: unavailable(),
398                modes: Vec::new(),
399            },
400        ),
401        (
402            LifecycleEventKind::CapabilityDegraded,
403            ManifestLifecycleEventSupport {
404                support: synthesized(),
405                modes: vec![IntegrationMode::NativeHook],
406            },
407        ),
408        (
409            LifecycleEventKind::ReceiptEmitted,
410            ManifestLifecycleEventSupport {
411                support: synthesized(),
412                modes: vec![IntegrationMode::NativeHook],
413            },
414        ),
415        (
416            LifecycleEventKind::ReceiptGapDetected,
417            ManifestLifecycleEventSupport {
418                support: unavailable(),
419                modes: Vec::new(),
420            },
421        ),
422    ]);
423
424    let placement = BTreeMap::from([
425        (
426            ManifestPlacementClass::PreSession,
427            ManifestPlacementSupport {
428                support: native(),
429                max_bytes: Some(8192),
430            },
431        ),
432        (
433            ManifestPlacementClass::PreFrameLeading,
434            ManifestPlacementSupport {
435                support: native(),
436                max_bytes: Some(8192),
437            },
438        ),
439        (
440            ManifestPlacementClass::PreFrameTrailing,
441            ManifestPlacementSupport {
442                support: unavailable(),
443                max_bytes: None,
444            },
445        ),
446        (
447            ManifestPlacementClass::ToolResult,
448            ManifestPlacementSupport {
449                support: unavailable(),
450                max_bytes: None,
451            },
452        ),
453        (
454            ManifestPlacementClass::ManualOperator,
455            ManifestPlacementSupport {
456                support: manual(),
457                max_bytes: None,
458            },
459        ),
460    ]);
461
462    AdapterManifest {
463        contract_version: SCHEMA_VERSION.to_string(),
464        adapter_id: "codex".into(),
465        adapter_version: "0.1.0".into(),
466        display_name: "Codex".into(),
467        role: AdapterRole::PrimaryWorker,
468        integration_modes: vec![IntegrationMode::NativeHook, IntegrationMode::ManualSkill],
469        lifecycle_events,
470        placement,
471        context_pressure: ManifestContextPressure {
472            support: native(),
473            evidence: Some(
474                "Codex CLI 0.129 exposes PreCompact before context pressure handling and PostCompact after context compacts"
475                    .into(),
476            ),
477        },
478        receipts: ManifestReceipts {
479            native: false,
480            lifeloop_synthesized: true,
481            receipt_ledger: unavailable(),
482        },
483        session_identity: Some(ManifestSessionIdentity {
484            harness_session_id: native(),
485            harness_run_id: synthesized(),
486            harness_task_id: unavailable(),
487        }),
488        session_rename: None,
489        renewal: Some(ManifestRenewal {
490            reset: ManifestRenewalReset {
491                native: unavailable(),
492                wrapper_mediated: synthesized(),
493                manual: manual(),
494            },
495            continuation: ManifestRenewalContinuation {
496                observation: native(),
497                payload_delivery: synthesized(),
498            },
499            profiles: Vec::new(),
500            evidence: Some(
501                "Codex SessionStart/Stop lifecycle hooks plus the subprocess client callback prove continuation-boundary observation and continuation payload delivery"
502                    .into(),
503            ),
504        }),
505        approval_surface: None,
506        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
507        telemetry_sources: Vec::new(),
508    }
509}
510
511/// Claude manifest. Native-hook integration via `.claude/settings.json`.
512pub fn claude_manifest() -> AdapterManifest {
513    let lifecycle_events = BTreeMap::from([
514        (
515            LifecycleEventKind::SessionStarting,
516            ManifestLifecycleEventSupport {
517                support: native(),
518                modes: vec![IntegrationMode::NativeHook],
519            },
520        ),
521        (
522            LifecycleEventKind::SessionStarted,
523            ManifestLifecycleEventSupport {
524                support: native(),
525                modes: vec![IntegrationMode::NativeHook],
526            },
527        ),
528        (
529            LifecycleEventKind::FrameOpening,
530            ManifestLifecycleEventSupport {
531                support: native(),
532                modes: vec![IntegrationMode::NativeHook],
533            },
534        ),
535        (
536            LifecycleEventKind::FrameOpened,
537            ManifestLifecycleEventSupport {
538                support: native(),
539                modes: vec![IntegrationMode::NativeHook],
540            },
541        ),
542        (
543            LifecycleEventKind::ContextPressureObserved,
544            ManifestLifecycleEventSupport {
545                support: native(),
546                modes: vec![IntegrationMode::NativeHook],
547            },
548        ),
549        (
550            LifecycleEventKind::ContextCompacted,
551            ManifestLifecycleEventSupport {
552                support: unavailable(),
553                modes: Vec::new(),
554            },
555        ),
556        (
557            LifecycleEventKind::FrameEnding,
558            ManifestLifecycleEventSupport {
559                support: native(),
560                modes: vec![IntegrationMode::NativeHook],
561            },
562        ),
563        (
564            LifecycleEventKind::FrameEnded,
565            ManifestLifecycleEventSupport {
566                support: native(),
567                modes: vec![IntegrationMode::NativeHook],
568            },
569        ),
570        (
571            LifecycleEventKind::SessionEnding,
572            ManifestLifecycleEventSupport {
573                support: native(),
574                modes: vec![IntegrationMode::NativeHook],
575            },
576        ),
577        (
578            LifecycleEventKind::SessionEnded,
579            ManifestLifecycleEventSupport {
580                support: native(),
581                modes: vec![IntegrationMode::NativeHook],
582            },
583        ),
584        (
585            LifecycleEventKind::SupervisorTick,
586            ManifestLifecycleEventSupport {
587                support: unavailable(),
588                modes: Vec::new(),
589            },
590        ),
591        (
592            LifecycleEventKind::CapabilityDegraded,
593            ManifestLifecycleEventSupport {
594                support: synthesized(),
595                modes: vec![IntegrationMode::NativeHook],
596            },
597        ),
598        (
599            LifecycleEventKind::ReceiptEmitted,
600            ManifestLifecycleEventSupport {
601                support: synthesized(),
602                modes: vec![IntegrationMode::NativeHook],
603            },
604        ),
605        (
606            LifecycleEventKind::ReceiptGapDetected,
607            ManifestLifecycleEventSupport {
608                support: unavailable(),
609                modes: Vec::new(),
610            },
611        ),
612    ]);
613
614    let placement = BTreeMap::from([
615        (
616            ManifestPlacementClass::PreSession,
617            ManifestPlacementSupport {
618                support: native(),
619                max_bytes: Some(16_384),
620            },
621        ),
622        (
623            ManifestPlacementClass::PreFrameLeading,
624            ManifestPlacementSupport {
625                support: native(),
626                max_bytes: Some(16_384),
627            },
628        ),
629        (
630            ManifestPlacementClass::PreFrameTrailing,
631            ManifestPlacementSupport {
632                support: unavailable(),
633                max_bytes: None,
634            },
635        ),
636        (
637            ManifestPlacementClass::ToolResult,
638            ManifestPlacementSupport {
639                support: unavailable(),
640                max_bytes: None,
641            },
642        ),
643        (
644            ManifestPlacementClass::ManualOperator,
645            ManifestPlacementSupport {
646                support: manual(),
647                max_bytes: None,
648            },
649        ),
650    ]);
651
652    AdapterManifest {
653        contract_version: SCHEMA_VERSION.to_string(),
654        adapter_id: "claude".into(),
655        adapter_version: "0.1.0".into(),
656        display_name: "Claude".into(),
657        role: AdapterRole::PrimaryWorker,
658        integration_modes: vec![IntegrationMode::NativeHook],
659        lifecycle_events,
660        placement,
661        context_pressure: ManifestContextPressure {
662            support: native(),
663            evidence: Some(
664                "Claude emits PreCompact and SessionEnd events that map directly to context.pressure_observed"
665                    .into(),
666            ),
667        },
668        receipts: ManifestReceipts {
669            native: false,
670            lifeloop_synthesized: true,
671            receipt_ledger: unavailable(),
672        },
673        session_identity: Some(ManifestSessionIdentity {
674            harness_session_id: native(),
675            harness_run_id: synthesized(),
676            harness_task_id: unavailable(),
677        }),
678        session_rename: None,
679        renewal: None,
680        approval_surface: None,
681        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
682        telemetry_sources: Vec::new(),
683    }
684}
685
686/// Hermes pre-conformance manifest. Reference-adapter integration
687/// supplied as a JSON descriptor at the path declared in
688/// [`crate::host_assets::HERMES_TARGET_ADAPTER`].
689pub fn hermes_manifest() -> AdapterManifest {
690    pre_conformance_reference_adapter_manifest("hermes", "Hermes")
691}
692
693/// OpenClaw pre-conformance manifest.
694pub fn openclaw_manifest() -> AdapterManifest {
695    pre_conformance_reference_adapter_manifest("openclaw", "OpenClaw")
696}
697
698/// Gemini pre-conformance manifest.
699pub fn gemini_manifest() -> AdapterManifest {
700    pre_conformance_telemetry_only_manifest("gemini", "Gemini")
701}
702
703/// OpenCode pre-conformance manifest.
704pub fn opencode_manifest() -> AdapterManifest {
705    pre_conformance_telemetry_only_manifest("opencode", "OpenCode")
706}
707
708fn pre_conformance_reference_adapter_manifest(
709    adapter_id: &str,
710    display_name: &str,
711) -> AdapterManifest {
712    let lifecycle_events = BTreeMap::from([
713        (
714            LifecycleEventKind::SessionStarting,
715            ManifestLifecycleEventSupport {
716                support: SupportState::Partial,
717                modes: vec![IntegrationMode::ReferenceAdapter],
718            },
719        ),
720        (
721            LifecycleEventKind::SessionStarted,
722            ManifestLifecycleEventSupport {
723                support: SupportState::Partial,
724                modes: vec![IntegrationMode::ReferenceAdapter],
725            },
726        ),
727        (
728            LifecycleEventKind::FrameOpening,
729            ManifestLifecycleEventSupport {
730                support: SupportState::Partial,
731                modes: vec![IntegrationMode::ReferenceAdapter],
732            },
733        ),
734        (
735            LifecycleEventKind::FrameEnded,
736            ManifestLifecycleEventSupport {
737                support: SupportState::Partial,
738                modes: vec![IntegrationMode::ReferenceAdapter],
739            },
740        ),
741        (
742            LifecycleEventKind::SessionEnded,
743            ManifestLifecycleEventSupport {
744                support: SupportState::Partial,
745                modes: vec![IntegrationMode::ReferenceAdapter],
746            },
747        ),
748    ]);
749
750    let placement = BTreeMap::from([
751        (
752            ManifestPlacementClass::PreSession,
753            ManifestPlacementSupport {
754                support: SupportState::Partial,
755                max_bytes: None,
756            },
757        ),
758        (
759            ManifestPlacementClass::PreFrameLeading,
760            ManifestPlacementSupport {
761                support: SupportState::Partial,
762                max_bytes: None,
763            },
764        ),
765        (
766            ManifestPlacementClass::ManualOperator,
767            ManifestPlacementSupport {
768                support: SupportState::Manual,
769                max_bytes: None,
770            },
771        ),
772    ]);
773
774    AdapterManifest {
775        contract_version: SCHEMA_VERSION.to_string(),
776        adapter_id: adapter_id.to_string(),
777        adapter_version: "0.0.1-pre".into(),
778        display_name: display_name.to_string(),
779        role: AdapterRole::Worker,
780        integration_modes: vec![IntegrationMode::ReferenceAdapter],
781        lifecycle_events,
782        placement,
783        context_pressure: ManifestContextPressure {
784            support: SupportState::Partial,
785            evidence: None,
786        },
787        receipts: ManifestReceipts {
788            native: false,
789            lifeloop_synthesized: true,
790            receipt_ledger: SupportState::Unavailable,
791        },
792        session_identity: None,
793        session_rename: None,
794        renewal: None,
795        approval_surface: None,
796        failure_modes: Vec::new(),
797        telemetry_sources: Vec::new(),
798    }
799}
800
801fn pre_conformance_telemetry_only_manifest(
802    adapter_id: &str,
803    display_name: &str,
804) -> AdapterManifest {
805    let lifecycle_events = BTreeMap::from([
806        (
807            LifecycleEventKind::SessionStarting,
808            ManifestLifecycleEventSupport {
809                support: SupportState::Partial,
810                modes: vec![IntegrationMode::TelemetryOnly],
811            },
812        ),
813        (
814            LifecycleEventKind::ContextPressureObserved,
815            ManifestLifecycleEventSupport {
816                support: SupportState::Partial,
817                modes: vec![IntegrationMode::TelemetryOnly],
818            },
819        ),
820        (
821            LifecycleEventKind::SessionEnded,
822            ManifestLifecycleEventSupport {
823                support: SupportState::Partial,
824                modes: vec![IntegrationMode::TelemetryOnly],
825            },
826        ),
827    ]);
828
829    let placement = BTreeMap::from([(
830        ManifestPlacementClass::ManualOperator,
831        ManifestPlacementSupport {
832            support: SupportState::Manual,
833            max_bytes: None,
834        },
835    )]);
836
837    AdapterManifest {
838        contract_version: SCHEMA_VERSION.to_string(),
839        adapter_id: adapter_id.to_string(),
840        adapter_version: "0.0.1-pre".into(),
841        display_name: display_name.to_string(),
842        role: AdapterRole::Observer,
843        integration_modes: vec![IntegrationMode::TelemetryOnly],
844        lifecycle_events,
845        placement,
846        context_pressure: ManifestContextPressure {
847            support: SupportState::Partial,
848            evidence: None,
849        },
850        receipts: ManifestReceipts {
851            native: false,
852            lifeloop_synthesized: true,
853            receipt_ledger: SupportState::Unavailable,
854        },
855        session_identity: None,
856        session_rename: None,
857        renewal: None,
858        approval_surface: None,
859        failure_modes: Vec::new(),
860        telemetry_sources: Vec::new(),
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867
868    fn valid_manifest() -> AdapterManifest {
869        manifest_registry()
870            .into_iter()
871            .next()
872            .expect("registry has at least one adapter")
873            .manifest
874    }
875
876    #[test]
877    fn empty_context_pressure_evidence_is_rejected() {
878        let mut manifest = valid_manifest();
879        manifest.validate().expect("baseline manifest validates");
880        manifest.context_pressure.evidence = Some(String::new());
881        assert!(matches!(
882            manifest.validate(),
883            Err(ValidationError::EmptyField(field)) if field == "manifest.context_pressure.evidence"
884        ));
885    }
886}