1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ManifestPlacementClass {
23 PreSession,
25 PreFrameLeading,
27 PreFrameTrailing,
29 ToolResult,
31 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct ManifestLifecycleEventSupport {
49 pub support: SupportState,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub modes: Vec<IntegrationMode>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct ManifestPlacementSupport {
60 pub support: SupportState,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub max_bytes: Option<u64>,
64}
65
66#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(deny_unknown_fields)]
79pub struct ManifestReceipts {
80 pub native: bool,
82 pub lifeloop_synthesized: bool,
84 pub receipt_ledger: SupportState,
86}
87
88#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct ManifestSessionRename {
104 pub support: SupportState,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct ManifestRenewal {
114 pub reset: ManifestRenewalReset,
115 pub continuation: ManifestRenewalContinuation,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub evidence: Option<String>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct ManifestRenewalReset {
127 pub native: SupportState,
128 pub wrapper_mediated: SupportState,
129 pub manual: SupportState,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct ManifestRenewalContinuation {
139 pub observation: SupportState,
140 pub payload_delivery: SupportState,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(deny_unknown_fields)]
147pub struct ManifestApprovalSurface {
148 pub support: SupportState,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(deny_unknown_fields)]
154pub struct ManifestTelemetrySource {
155 pub source: String,
156 pub support: SupportState,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(deny_unknown_fields)]
165pub struct ManifestKnownDegradation {
166 pub capability: String,
167 pub previous_support: SupportState,
168 pub current_support: SupportState,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub evidence: Option<String>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(deny_unknown_fields)]
184pub struct AdapterManifest {
185 pub contract_version: String,
186 pub adapter_id: String,
187 pub adapter_version: String,
188 pub display_name: String,
189 pub role: AdapterRole,
190 pub integration_modes: Vec<IntegrationMode>,
191 pub lifecycle_events: BTreeMap<LifecycleEventKind, ManifestLifecycleEventSupport>,
192 pub placement: BTreeMap<ManifestPlacementClass, ManifestPlacementSupport>,
193 pub context_pressure: ManifestContextPressure,
194 pub receipts: ManifestReceipts,
195
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub session_identity: Option<ManifestSessionIdentity>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub session_rename: Option<ManifestSessionRename>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub renewal: Option<ManifestRenewal>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub approval_surface: Option<ManifestApprovalSurface>,
204 #[serde(default, skip_serializing_if = "Vec::is_empty")]
205 pub failure_modes: Vec<FailureClass>,
206 #[serde(default, skip_serializing_if = "Vec::is_empty")]
207 pub telemetry_sources: Vec<ManifestTelemetrySource>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
209 pub known_degradations: Vec<ManifestKnownDegradation>,
210}
211
212impl AdapterManifest {
213 pub fn validate(&self) -> Result<(), ValidationError> {
214 if self.contract_version != SCHEMA_VERSION {
215 return Err(ValidationError::SchemaVersionMismatch {
216 expected: SCHEMA_VERSION.to_string(),
217 found: self.contract_version.clone(),
218 });
219 }
220 require_non_empty(&self.adapter_id, "manifest.adapter_id")?;
221 require_non_empty(&self.adapter_version, "manifest.adapter_version")?;
222 require_non_empty(&self.display_name, "manifest.display_name")?;
223 if self.integration_modes.is_empty() {
224 return Err(ValidationError::InvalidManifest(
225 "manifest.integration_modes must declare at least one integration mode".into(),
226 ));
227 }
228 for deg in &self.known_degradations {
229 require_non_empty(°.capability, "manifest.known_degradations[].capability")?;
230 }
231 for src in &self.telemetry_sources {
232 require_non_empty(&src.source, "manifest.telemetry_sources[].source")?;
233 }
234 if let Some(renewal) = &self.renewal
235 && let Some(evidence) = &renewal.evidence
236 {
237 require_non_empty(evidence, "manifest.renewal.evidence")?;
238 }
239 Ok(())
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
249#[serde(rename_all = "snake_case")]
250pub enum ConformanceLevel {
251 V1Conformance,
255 PreConformance,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct RegisteredAdapter {
265 pub manifest: AdapterManifest,
266 pub conformance: ConformanceLevel,
267}
268
269pub fn manifest_registry() -> Vec<RegisteredAdapter> {
273 vec![
274 RegisteredAdapter {
275 manifest: codex_manifest(),
276 conformance: ConformanceLevel::V1Conformance,
277 },
278 RegisteredAdapter {
279 manifest: claude_manifest(),
280 conformance: ConformanceLevel::V1Conformance,
281 },
282 RegisteredAdapter {
283 manifest: hermes_manifest(),
284 conformance: ConformanceLevel::PreConformance,
285 },
286 RegisteredAdapter {
287 manifest: openclaw_manifest(),
288 conformance: ConformanceLevel::PreConformance,
289 },
290 RegisteredAdapter {
291 manifest: gemini_manifest(),
292 conformance: ConformanceLevel::PreConformance,
293 },
294 RegisteredAdapter {
295 manifest: opencode_manifest(),
296 conformance: ConformanceLevel::PreConformance,
297 },
298 ]
299}
300
301pub fn lookup_manifest(adapter_id: &str) -> Option<RegisteredAdapter> {
304 manifest_registry()
305 .into_iter()
306 .find(|entry| entry.manifest.adapter_id == adapter_id)
307}
308
309fn synthesized() -> SupportState {
310 SupportState::Synthesized
311}
312
313fn native() -> SupportState {
314 SupportState::Native
315}
316
317fn unavailable() -> SupportState {
318 SupportState::Unavailable
319}
320
321fn manual() -> SupportState {
322 SupportState::Manual
323}
324
325pub fn codex_manifest() -> AdapterManifest {
330 let lifecycle_events = BTreeMap::from([
331 (
332 LifecycleEventKind::SessionStarting,
333 ManifestLifecycleEventSupport {
334 support: native(),
335 modes: vec![IntegrationMode::NativeHook],
336 },
337 ),
338 (
339 LifecycleEventKind::SessionStarted,
340 ManifestLifecycleEventSupport {
341 support: native(),
342 modes: vec![IntegrationMode::NativeHook],
343 },
344 ),
345 (
346 LifecycleEventKind::FrameOpening,
347 ManifestLifecycleEventSupport {
348 support: native(),
349 modes: vec![IntegrationMode::NativeHook],
350 },
351 ),
352 (
353 LifecycleEventKind::FrameOpened,
354 ManifestLifecycleEventSupport {
355 support: synthesized(),
356 modes: vec![IntegrationMode::NativeHook],
357 },
358 ),
359 (
360 LifecycleEventKind::ContextPressureObserved,
361 ManifestLifecycleEventSupport {
362 support: native(),
363 modes: vec![IntegrationMode::NativeHook],
364 },
365 ),
366 (
367 LifecycleEventKind::ContextCompacted,
368 ManifestLifecycleEventSupport {
369 support: native(),
370 modes: vec![IntegrationMode::NativeHook],
371 },
372 ),
373 (
374 LifecycleEventKind::FrameEnding,
375 ManifestLifecycleEventSupport {
376 support: native(),
377 modes: vec![IntegrationMode::NativeHook],
378 },
379 ),
380 (
381 LifecycleEventKind::FrameEnded,
382 ManifestLifecycleEventSupport {
383 support: native(),
384 modes: vec![IntegrationMode::NativeHook],
385 },
386 ),
387 (
388 LifecycleEventKind::SessionEnding,
389 ManifestLifecycleEventSupport {
390 support: unavailable(),
391 modes: Vec::new(),
392 },
393 ),
394 (
395 LifecycleEventKind::SessionEnded,
396 ManifestLifecycleEventSupport {
397 support: unavailable(),
398 modes: Vec::new(),
399 },
400 ),
401 (
402 LifecycleEventKind::SupervisorTick,
403 ManifestLifecycleEventSupport {
404 support: unavailable(),
405 modes: Vec::new(),
406 },
407 ),
408 (
409 LifecycleEventKind::CapabilityDegraded,
410 ManifestLifecycleEventSupport {
411 support: synthesized(),
412 modes: vec![IntegrationMode::NativeHook],
413 },
414 ),
415 (
416 LifecycleEventKind::ReceiptEmitted,
417 ManifestLifecycleEventSupport {
418 support: synthesized(),
419 modes: vec![IntegrationMode::NativeHook],
420 },
421 ),
422 (
423 LifecycleEventKind::ReceiptGapDetected,
424 ManifestLifecycleEventSupport {
425 support: unavailable(),
426 modes: Vec::new(),
427 },
428 ),
429 ]);
430
431 let placement = BTreeMap::from([
432 (
433 ManifestPlacementClass::PreSession,
434 ManifestPlacementSupport {
435 support: native(),
436 max_bytes: Some(8192),
437 },
438 ),
439 (
440 ManifestPlacementClass::PreFrameLeading,
441 ManifestPlacementSupport {
442 support: native(),
443 max_bytes: Some(8192),
444 },
445 ),
446 (
447 ManifestPlacementClass::PreFrameTrailing,
448 ManifestPlacementSupport {
449 support: unavailable(),
450 max_bytes: None,
451 },
452 ),
453 (
454 ManifestPlacementClass::ToolResult,
455 ManifestPlacementSupport {
456 support: unavailable(),
457 max_bytes: None,
458 },
459 ),
460 (
461 ManifestPlacementClass::ManualOperator,
462 ManifestPlacementSupport {
463 support: manual(),
464 max_bytes: None,
465 },
466 ),
467 ]);
468
469 AdapterManifest {
470 contract_version: SCHEMA_VERSION.to_string(),
471 adapter_id: "codex".into(),
472 adapter_version: "0.1.0".into(),
473 display_name: "Codex".into(),
474 role: AdapterRole::PrimaryWorker,
475 integration_modes: vec![IntegrationMode::NativeHook, IntegrationMode::ManualSkill],
476 lifecycle_events,
477 placement,
478 context_pressure: ManifestContextPressure {
479 support: native(),
480 evidence: Some(
481 "Codex CLI 0.129 exposes PreCompact before context pressure handling and PostCompact after context compacts"
482 .into(),
483 ),
484 },
485 receipts: ManifestReceipts {
486 native: false,
487 lifeloop_synthesized: true,
488 receipt_ledger: unavailable(),
489 },
490 session_identity: Some(ManifestSessionIdentity {
491 harness_session_id: native(),
492 harness_run_id: synthesized(),
493 harness_task_id: unavailable(),
494 }),
495 session_rename: None,
496 renewal: None,
497 approval_surface: None,
498 failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
499 telemetry_sources: Vec::new(),
500 known_degradations: Vec::new(),
501 }
502}
503
504pub fn claude_manifest() -> AdapterManifest {
506 let lifecycle_events = BTreeMap::from([
507 (
508 LifecycleEventKind::SessionStarting,
509 ManifestLifecycleEventSupport {
510 support: native(),
511 modes: vec![IntegrationMode::NativeHook],
512 },
513 ),
514 (
515 LifecycleEventKind::SessionStarted,
516 ManifestLifecycleEventSupport {
517 support: native(),
518 modes: vec![IntegrationMode::NativeHook],
519 },
520 ),
521 (
522 LifecycleEventKind::FrameOpening,
523 ManifestLifecycleEventSupport {
524 support: native(),
525 modes: vec![IntegrationMode::NativeHook],
526 },
527 ),
528 (
529 LifecycleEventKind::FrameOpened,
530 ManifestLifecycleEventSupport {
531 support: native(),
532 modes: vec![IntegrationMode::NativeHook],
533 },
534 ),
535 (
536 LifecycleEventKind::ContextPressureObserved,
537 ManifestLifecycleEventSupport {
538 support: native(),
539 modes: vec![IntegrationMode::NativeHook],
540 },
541 ),
542 (
543 LifecycleEventKind::ContextCompacted,
544 ManifestLifecycleEventSupport {
545 support: unavailable(),
546 modes: Vec::new(),
547 },
548 ),
549 (
550 LifecycleEventKind::FrameEnding,
551 ManifestLifecycleEventSupport {
552 support: native(),
553 modes: vec![IntegrationMode::NativeHook],
554 },
555 ),
556 (
557 LifecycleEventKind::FrameEnded,
558 ManifestLifecycleEventSupport {
559 support: native(),
560 modes: vec![IntegrationMode::NativeHook],
561 },
562 ),
563 (
564 LifecycleEventKind::SessionEnding,
565 ManifestLifecycleEventSupport {
566 support: native(),
567 modes: vec![IntegrationMode::NativeHook],
568 },
569 ),
570 (
571 LifecycleEventKind::SessionEnded,
572 ManifestLifecycleEventSupport {
573 support: native(),
574 modes: vec![IntegrationMode::NativeHook],
575 },
576 ),
577 (
578 LifecycleEventKind::SupervisorTick,
579 ManifestLifecycleEventSupport {
580 support: unavailable(),
581 modes: Vec::new(),
582 },
583 ),
584 (
585 LifecycleEventKind::CapabilityDegraded,
586 ManifestLifecycleEventSupport {
587 support: synthesized(),
588 modes: vec![IntegrationMode::NativeHook],
589 },
590 ),
591 (
592 LifecycleEventKind::ReceiptEmitted,
593 ManifestLifecycleEventSupport {
594 support: synthesized(),
595 modes: vec![IntegrationMode::NativeHook],
596 },
597 ),
598 (
599 LifecycleEventKind::ReceiptGapDetected,
600 ManifestLifecycleEventSupport {
601 support: unavailable(),
602 modes: Vec::new(),
603 },
604 ),
605 ]);
606
607 let placement = BTreeMap::from([
608 (
609 ManifestPlacementClass::PreSession,
610 ManifestPlacementSupport {
611 support: native(),
612 max_bytes: Some(16_384),
613 },
614 ),
615 (
616 ManifestPlacementClass::PreFrameLeading,
617 ManifestPlacementSupport {
618 support: native(),
619 max_bytes: Some(16_384),
620 },
621 ),
622 (
623 ManifestPlacementClass::PreFrameTrailing,
624 ManifestPlacementSupport {
625 support: unavailable(),
626 max_bytes: None,
627 },
628 ),
629 (
630 ManifestPlacementClass::ToolResult,
631 ManifestPlacementSupport {
632 support: unavailable(),
633 max_bytes: None,
634 },
635 ),
636 (
637 ManifestPlacementClass::ManualOperator,
638 ManifestPlacementSupport {
639 support: manual(),
640 max_bytes: None,
641 },
642 ),
643 ]);
644
645 AdapterManifest {
646 contract_version: SCHEMA_VERSION.to_string(),
647 adapter_id: "claude".into(),
648 adapter_version: "0.1.0".into(),
649 display_name: "Claude".into(),
650 role: AdapterRole::PrimaryWorker,
651 integration_modes: vec![IntegrationMode::NativeHook],
652 lifecycle_events,
653 placement,
654 context_pressure: ManifestContextPressure {
655 support: native(),
656 evidence: Some(
657 "Claude emits PreCompact and SessionEnd events that map directly to context.pressure_observed"
658 .into(),
659 ),
660 },
661 receipts: ManifestReceipts {
662 native: false,
663 lifeloop_synthesized: true,
664 receipt_ledger: unavailable(),
665 },
666 session_identity: Some(ManifestSessionIdentity {
667 harness_session_id: native(),
668 harness_run_id: synthesized(),
669 harness_task_id: unavailable(),
670 }),
671 session_rename: None,
672 renewal: None,
673 approval_surface: None,
674 failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
675 telemetry_sources: Vec::new(),
676 known_degradations: Vec::new(),
677 }
678}
679
680pub fn hermes_manifest() -> AdapterManifest {
684 pre_conformance_reference_adapter_manifest("hermes", "Hermes")
685}
686
687pub fn openclaw_manifest() -> AdapterManifest {
689 pre_conformance_reference_adapter_manifest("openclaw", "OpenClaw")
690}
691
692pub fn gemini_manifest() -> AdapterManifest {
694 pre_conformance_telemetry_only_manifest("gemini", "Gemini")
695}
696
697pub fn opencode_manifest() -> AdapterManifest {
699 pre_conformance_telemetry_only_manifest("opencode", "OpenCode")
700}
701
702fn pre_conformance_reference_adapter_manifest(
703 adapter_id: &str,
704 display_name: &str,
705) -> AdapterManifest {
706 let lifecycle_events = BTreeMap::from([
707 (
708 LifecycleEventKind::SessionStarting,
709 ManifestLifecycleEventSupport {
710 support: SupportState::Partial,
711 modes: vec![IntegrationMode::ReferenceAdapter],
712 },
713 ),
714 (
715 LifecycleEventKind::SessionStarted,
716 ManifestLifecycleEventSupport {
717 support: SupportState::Partial,
718 modes: vec![IntegrationMode::ReferenceAdapter],
719 },
720 ),
721 (
722 LifecycleEventKind::FrameOpening,
723 ManifestLifecycleEventSupport {
724 support: SupportState::Partial,
725 modes: vec![IntegrationMode::ReferenceAdapter],
726 },
727 ),
728 (
729 LifecycleEventKind::FrameEnded,
730 ManifestLifecycleEventSupport {
731 support: SupportState::Partial,
732 modes: vec![IntegrationMode::ReferenceAdapter],
733 },
734 ),
735 (
736 LifecycleEventKind::SessionEnded,
737 ManifestLifecycleEventSupport {
738 support: SupportState::Partial,
739 modes: vec![IntegrationMode::ReferenceAdapter],
740 },
741 ),
742 ]);
743
744 let placement = BTreeMap::from([
745 (
746 ManifestPlacementClass::PreSession,
747 ManifestPlacementSupport {
748 support: SupportState::Partial,
749 max_bytes: None,
750 },
751 ),
752 (
753 ManifestPlacementClass::PreFrameLeading,
754 ManifestPlacementSupport {
755 support: SupportState::Partial,
756 max_bytes: None,
757 },
758 ),
759 (
760 ManifestPlacementClass::ManualOperator,
761 ManifestPlacementSupport {
762 support: SupportState::Manual,
763 max_bytes: None,
764 },
765 ),
766 ]);
767
768 AdapterManifest {
769 contract_version: SCHEMA_VERSION.to_string(),
770 adapter_id: adapter_id.to_string(),
771 adapter_version: "0.0.1-pre".into(),
772 display_name: display_name.to_string(),
773 role: AdapterRole::Worker,
774 integration_modes: vec![IntegrationMode::ReferenceAdapter],
775 lifecycle_events,
776 placement,
777 context_pressure: ManifestContextPressure {
778 support: SupportState::Partial,
779 evidence: None,
780 },
781 receipts: ManifestReceipts {
782 native: false,
783 lifeloop_synthesized: true,
784 receipt_ledger: SupportState::Unavailable,
785 },
786 session_identity: None,
787 session_rename: None,
788 renewal: None,
789 approval_surface: None,
790 failure_modes: Vec::new(),
791 telemetry_sources: Vec::new(),
792 known_degradations: Vec::new(),
793 }
794}
795
796fn pre_conformance_telemetry_only_manifest(
797 adapter_id: &str,
798 display_name: &str,
799) -> AdapterManifest {
800 let lifecycle_events = BTreeMap::from([
801 (
802 LifecycleEventKind::SessionStarting,
803 ManifestLifecycleEventSupport {
804 support: SupportState::Partial,
805 modes: vec![IntegrationMode::TelemetryOnly],
806 },
807 ),
808 (
809 LifecycleEventKind::ContextPressureObserved,
810 ManifestLifecycleEventSupport {
811 support: SupportState::Partial,
812 modes: vec![IntegrationMode::TelemetryOnly],
813 },
814 ),
815 (
816 LifecycleEventKind::SessionEnded,
817 ManifestLifecycleEventSupport {
818 support: SupportState::Partial,
819 modes: vec![IntegrationMode::TelemetryOnly],
820 },
821 ),
822 ]);
823
824 let placement = BTreeMap::from([(
825 ManifestPlacementClass::ManualOperator,
826 ManifestPlacementSupport {
827 support: SupportState::Manual,
828 max_bytes: None,
829 },
830 )]);
831
832 AdapterManifest {
833 contract_version: SCHEMA_VERSION.to_string(),
834 adapter_id: adapter_id.to_string(),
835 adapter_version: "0.0.1-pre".into(),
836 display_name: display_name.to_string(),
837 role: AdapterRole::Observer,
838 integration_modes: vec![IntegrationMode::TelemetryOnly],
839 lifecycle_events,
840 placement,
841 context_pressure: ManifestContextPressure {
842 support: SupportState::Partial,
843 evidence: None,
844 },
845 receipts: ManifestReceipts {
846 native: false,
847 lifeloop_synthesized: true,
848 receipt_ledger: SupportState::Unavailable,
849 },
850 session_identity: None,
851 session_rename: None,
852 renewal: None,
853 approval_surface: None,
854 failure_modes: Vec::new(),
855 telemetry_sources: Vec::new(),
856 known_degradations: Vec::new(),
857 }
858}