1use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 AcpRuntimeBackend, ClaudeCodeRuntimeBackend, CodexRuntimeBackend, HarnessId,
14 OpenCodeRuntimeBackend, PiRuntimeBackend, RuntimeBackend, RuntimeCapabilities,
15 RuntimeConnectLaunch, RuntimeLaunch,
16};
17
18pub const SUPPORT_REGISTRY_SCHEMA: &str = "supercode.support-registry.v1";
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ImplementationKind {
25 BuiltIn,
27 GenericProtocol,
29 Absent,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct NativeSupport {
36 pub discover: ImplementationKind,
38 pub load: ImplementationKind,
40 pub follow: ImplementationKind,
42 pub import: ImplementationKind,
44 pub export: ImplementationKind,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct RuntimeSupport {
51 pub implementation: ImplementationKind,
53 pub protocol: String,
55 pub default_launch: Option<RuntimeLaunch>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub connect_launch: Option<RuntimeConnectLaunch>,
63 pub capabilities: RuntimeCapabilities,
67}
68
69pub const ORCHESTRATION_CONCEPTS: &[&str] = &[
74 "scheduled_job",
75 "run",
76 "conversation",
77 "pending_request",
78 "profile",
79 "skills",
80 "memory",
81 "delivery_target",
82 "channel",
83 "routing",
84 "inbound_trigger",
85 "gateway_health",
86];
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct ConceptSupport {
91 pub concept: String,
93 pub observed: ImplementationKind,
95 pub controlled: ImplementationKind,
97 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub methods: Vec<String>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
105pub struct OrchestrationSupport {
106 pub concepts: Vec<ConceptSupport>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct HarnessSupportDescriptor {
112 pub id: HarnessId,
114 pub display_name: String,
116 pub native: NativeSupport,
118 pub runtime: RuntimeSupport,
120 #[serde(default)]
122 pub orchestration: OrchestrationSupport,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct SupportRegistryReport {
128 pub schema: String,
130 pub harnesses: Vec<HarnessSupportDescriptor>,
132}
133
134pub(crate) fn self_sandbox_supported() -> bool {
140 !cfg!(target_family = "wasm")
141}
142
143fn grok_arguments() -> Vec<String> {
145 let mut arguments: Vec<String> = Vec::new();
146 if self_sandbox_supported() {
147 arguments.push("--sandbox".into());
148 arguments.push("workspace".into());
149 }
150 arguments.push("agent".into());
151 arguments.push("--no-leader".into());
152 arguments.push("stdio".into());
153 arguments
154}
155
156fn built_in_native() -> NativeSupport {
157 NativeSupport {
158 discover: ImplementationKind::BuiltIn,
159 load: ImplementationKind::BuiltIn,
160 follow: ImplementationKind::BuiltIn,
161 import: ImplementationKind::BuiltIn,
162 export: ImplementationKind::BuiltIn,
163 }
164}
165
166fn built_in_runtime(
167 backend: &dyn RuntimeBackend,
168 protocol: &str,
169 launch: RuntimeLaunch,
170) -> RuntimeSupport {
171 RuntimeSupport {
172 implementation: ImplementationKind::BuiltIn,
173 protocol: protocol.into(),
174 default_launch: Some(launch),
175 connect_launch: None,
176 capabilities: backend.capabilities(),
177 }
178}
179
180pub fn orchestration_support(descriptor: &HarnessSupportDescriptor) -> OrchestrationSupport {
189 let concepts = ORCHESTRATION_CONCEPTS
190 .iter()
191 .map(|concept| {
192 let (observed, controlled, methods): (
193 ImplementationKind,
194 ImplementationKind,
195 Vec<&str>,
196 ) = match *concept {
197 "scheduled_job"
204 if crate::jobs_control::supports_job_control(descriptor.id.as_str()) =>
205 {
206 (
207 ImplementationKind::BuiltIn,
208 ImplementationKind::BuiltIn,
209 vec![
210 "harness.v1.jobs.list",
211 "harness.v1.jobs.get",
212 "harness.v1.jobs.create",
213 "harness.v1.jobs.update",
214 "harness.v1.jobs.pause",
215 "harness.v1.jobs.resume",
216 "harness.v1.jobs.run",
217 "harness.v1.jobs.delete",
218 ],
219 )
220 }
221 "scheduled_job" if crate::jobs::supports_jobs(descriptor.id.as_str()) => (
227 ImplementationKind::BuiltIn,
228 ImplementationKind::Absent,
229 vec!["harness.v1.jobs.list", "harness.v1.jobs.get"],
230 ),
231 "run" if crate::runs::supports_runs(descriptor.id.as_str()) => (
237 ImplementationKind::BuiltIn,
238 ImplementationKind::Absent,
239 vec!["harness.v1.runs.list", "harness.v1.runs.get"],
240 ),
241 "conversation"
252 if descriptor.native.load == ImplementationKind::BuiltIn
253 && !crate::sessions_control::controlled_methods(descriptor.id.as_str())
254 .is_empty() =>
255 {
256 let mut methods =
257 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"];
258 methods.extend(crate::sessions_control::controlled_methods(
259 descriptor.id.as_str(),
260 ));
261 (
262 ImplementationKind::BuiltIn,
263 ImplementationKind::BuiltIn,
264 methods,
265 )
266 }
267 "conversation" if descriptor.id.as_str() == HarnessId::ORCHESTRATOR => {
281 let mut methods = vec!["harness.v1.sessions.discover"];
282 methods.extend(crate::sessions_control::controlled_methods(
283 descriptor.id.as_str(),
284 ));
285 (
286 ImplementationKind::BuiltIn,
287 ImplementationKind::BuiltIn,
288 methods,
289 )
290 }
291 "conversation" if descriptor.native.load == ImplementationKind::BuiltIn => (
292 ImplementationKind::BuiltIn,
293 ImplementationKind::Absent,
294 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"],
295 ),
296 "pending_request" if descriptor.runtime.capabilities.respond_to_requests => (
305 ImplementationKind::BuiltIn,
306 ImplementationKind::BuiltIn,
307 vec![
308 "harness.v1.approvals.list",
309 "harness.v1.approvals.resolve",
310 "harness.v1.runtimes.respond",
311 ],
312 ),
313 "profile"
321 if crate::profiles_control::supports_profile_control(
322 descriptor.id.as_str(),
323 ) =>
324 {
325 (
326 ImplementationKind::BuiltIn,
327 ImplementationKind::BuiltIn,
328 vec![
329 "harness.v1.profiles.list",
330 "harness.v1.profiles.get",
331 "harness.v1.profiles.create",
332 "harness.v1.profiles.delete",
333 ],
334 )
335 }
336 "profile"
345 if crate::profiles::PROFILE_HARNESSES.contains(&descriptor.id.as_str()) =>
346 {
347 (
348 ImplementationKind::BuiltIn,
349 ImplementationKind::Absent,
350 vec!["harness.v1.profiles.list", "harness.v1.profiles.get"],
351 )
352 }
353 "skills"
363 if crate::skills_control::supports_skill_control(descriptor.id.as_str()) =>
364 {
365 (
366 ImplementationKind::BuiltIn,
367 ImplementationKind::BuiltIn,
368 vec![
369 "harness.v1.skills.list",
370 "harness.v1.skills.install",
371 "harness.v1.skills.remove",
372 ],
373 )
374 }
375 "delivery_target" if crate::jobs::supports_jobs(descriptor.id.as_str()) => {
381 let mut methods = vec!["harness.v1.jobs.list", "harness.v1.jobs.get"];
382 if crate::runs::supports_runs(descriptor.id.as_str()) {
383 methods.push("harness.v1.runs.list");
384 methods.push("harness.v1.runs.get");
385 }
386 (
387 ImplementationKind::BuiltIn,
388 ImplementationKind::Absent,
389 methods,
390 )
391 }
392 "gateway_health"
401 if matches!(
402 descriptor.id.as_str(),
403 HarnessId::HERMES | HarnessId::OPENCLAW | HarnessId::ORCHESTRATOR
408 ) =>
409 {
410 (
411 ImplementationKind::BuiltIn,
412 ImplementationKind::Absent,
413 vec!["harness.v1.harnesses.list"],
414 )
415 }
416 "inbound_trigger"
418 if crate::triggers::TRIGGER_HARNESSES.contains(&descriptor.id.as_str()) =>
419 {
420 (
421 ImplementationKind::BuiltIn,
422 ImplementationKind::Absent,
423 vec!["harness.v1.triggers.list"],
424 )
425 }
426 "routing" if crate::routes::ROUTE_HARNESSES.contains(&descriptor.id.as_str()) => (
428 ImplementationKind::BuiltIn,
429 ImplementationKind::Absent,
430 vec!["harness.v1.routes.list"],
431 ),
432 "channel"
433 if crate::channels::CHANNEL_HARNESSES.contains(&descriptor.id.as_str()) =>
434 {
435 (
436 ImplementationKind::BuiltIn,
437 ImplementationKind::Absent,
438 vec!["harness.v1.channels.list", "harness.v1.channels.status"],
439 )
440 }
441 "memory" if crate::memory::supports_memory(descriptor.id.as_str()) => (
448 ImplementationKind::BuiltIn,
449 ImplementationKind::Absent,
450 vec!["harness.v1.memory.show", "harness.v1.memory.search"],
451 ),
452 _ => (
453 ImplementationKind::Absent,
454 ImplementationKind::Absent,
455 vec![],
456 ),
457 };
458 ConceptSupport {
459 concept: (*concept).to_string(),
460 observed,
461 controlled,
462 methods: methods.into_iter().map(str::to_string).collect(),
463 }
464 })
465 .collect();
466 OrchestrationSupport { concepts }
467}
468
469pub fn harness_support_registry() -> SupportRegistryReport {
470 let claude = ClaudeCodeRuntimeBackend::new();
471 let codex = CodexRuntimeBackend::new();
472 let opencode = OpenCodeRuntimeBackend::new();
473 let pi = PiRuntimeBackend::new();
474 let grok_launch = RuntimeLaunch {
475 program: "grok".into(),
476 arguments: grok_arguments(),
477 env: BTreeMap::from([("GROK_AGENT_DASHBOARD".into(), "0".into())]),
478 };
479 let grok = AcpRuntimeBackend::new(HarnessId::from(HarnessId::GROK), grok_launch.clone())
480 .with_resume_support(true);
481 let gemini_launch = RuntimeLaunch {
482 program: "gemini".into(),
483 arguments: vec!["--experimental-acp".into()],
487 env: BTreeMap::new(),
488 };
489 let gemini = AcpRuntimeBackend::new(HarnessId::from(HarnessId::GEMINI), gemini_launch.clone())
490 .with_resume_support(true);
491 let goose_launch = RuntimeLaunch {
492 program: "goose".into(),
493 arguments: vec!["acp".into()],
494 env: BTreeMap::new(),
495 };
496 let goose = AcpRuntimeBackend::new(HarnessId::from(HarnessId::GOOSE), goose_launch.clone())
497 .with_resume_support(true);
498 let hermes_launch = RuntimeLaunch {
499 program: "hermes-acp".into(),
500 arguments: Vec::new(),
501 env: BTreeMap::new(),
502 };
503 let hermes = AcpRuntimeBackend::new(HarnessId::from(HarnessId::HERMES), hermes_launch.clone())
504 .with_resume_support(true);
505 let openclaw_launch = RuntimeLaunch {
506 program: "openclaw".into(),
511 arguments: vec!["acp".into()],
512 env: BTreeMap::new(),
513 };
514 let openclaw = AcpRuntimeBackend::new(
515 HarnessId::from(HarnessId::OPENCLAW),
516 openclaw_launch.clone(),
517 )
518 .with_resume_support(true);
519 let supercode_launch = RuntimeLaunch {
520 program: "supercode".into(),
521 arguments: vec!["acp".into()],
522 env: BTreeMap::new(),
523 };
524 let supercode = AcpRuntimeBackend::new(
525 HarnessId::from(HarnessId::SUPERCODE),
526 supercode_launch.clone(),
527 )
528 .with_resume_support(true);
529
530 let mut report = SupportRegistryReport {
531 schema: SUPPORT_REGISTRY_SCHEMA.into(),
532 harnesses: vec![
533 HarnessSupportDescriptor {
534 id: HarnessId::from(HarnessId::CLAUDE_CODE),
535 display_name: "Claude Code".into(),
536 orchestration: OrchestrationSupport::default(),
537 native: built_in_native(),
538 runtime: built_in_runtime(&claude, "claude-stream-json", claude.launch().clone()),
542 },
543 HarnessSupportDescriptor {
544 id: HarnessId::from(HarnessId::CODEX),
545 display_name: "Codex".into(),
546 orchestration: OrchestrationSupport::default(),
547 native: built_in_native(),
548 runtime: built_in_runtime(
549 &codex,
550 "codex-app-server-jsonl",
551 RuntimeLaunch {
552 program: "codex".into(),
553 arguments: vec!["app-server".into()],
554 env: BTreeMap::new(),
555 },
556 ),
557 },
558 HarnessSupportDescriptor {
559 id: HarnessId::from(HarnessId::OPENCODE),
560 display_name: "OpenCode".into(),
561 orchestration: OrchestrationSupport::default(),
562 native: built_in_native(),
563 runtime: built_in_runtime(
564 &opencode,
565 "opencode-http-sse",
566 RuntimeLaunch {
567 program: "opencode".into(),
568 arguments: vec!["serve".into()],
569 env: BTreeMap::new(),
570 },
571 ),
572 },
573 HarnessSupportDescriptor {
574 id: HarnessId::from(HarnessId::PI),
575 display_name: "Pi".into(),
576 orchestration: OrchestrationSupport::default(),
577 native: built_in_native(),
578 runtime: built_in_runtime(
579 &pi,
580 "pi-rpc-jsonl",
581 RuntimeLaunch {
582 program: "pi".into(),
583 arguments: vec!["--mode".into(), "rpc".into()],
584 env: BTreeMap::new(),
585 },
586 ),
587 },
588 HarnessSupportDescriptor {
589 id: HarnessId::from(HarnessId::GROK),
590 display_name: "Grok".into(),
591 orchestration: OrchestrationSupport::default(),
592 native: built_in_native(),
593 runtime: RuntimeSupport {
594 implementation: ImplementationKind::GenericProtocol,
595 protocol: "acp-v1-jsonrpc".into(),
596 default_launch: Some(grok_launch),
597 connect_launch: None,
598 capabilities: grok.capabilities(),
599 },
600 },
601 HarnessSupportDescriptor {
602 id: HarnessId::from(HarnessId::GEMINI),
603 display_name: "Gemini CLI".into(),
604 orchestration: OrchestrationSupport::default(),
605 native: built_in_native(),
606 runtime: RuntimeSupport {
607 implementation: ImplementationKind::GenericProtocol,
608 protocol: "acp-v1-jsonrpc".into(),
609 default_launch: Some(gemini_launch),
610 connect_launch: None,
611 capabilities: gemini.capabilities(),
612 },
613 },
614 HarnessSupportDescriptor {
615 id: HarnessId::from(HarnessId::GOOSE),
616 display_name: "Goose".into(),
617 orchestration: OrchestrationSupport::default(),
618 native: built_in_native(),
619 runtime: RuntimeSupport {
620 implementation: ImplementationKind::GenericProtocol,
621 protocol: "acp-v1-jsonrpc".into(),
622 default_launch: Some(goose_launch),
623 connect_launch: None,
624 capabilities: goose.capabilities(),
625 },
626 },
627 HarnessSupportDescriptor {
628 id: HarnessId::from(HarnessId::HERMES),
629 display_name: "Hermes Agent".into(),
630 orchestration: OrchestrationSupport::default(),
631 native: NativeSupport {
632 discover: ImplementationKind::BuiltIn,
640 load: ImplementationKind::BuiltIn,
641 follow: ImplementationKind::Absent,
642 import: ImplementationKind::Absent,
643 export: ImplementationKind::Absent,
644 },
645 runtime: RuntimeSupport {
646 implementation: ImplementationKind::GenericProtocol,
647 protocol: "acp-v1-jsonrpc".into(),
648 default_launch: Some(hermes_launch),
649 connect_launch: None,
650 capabilities: hermes.capabilities(),
651 },
652 },
653 HarnessSupportDescriptor {
654 id: HarnessId::from(HarnessId::OPENCLAW),
655 display_name: "OpenClaw".into(),
656 orchestration: OrchestrationSupport::default(),
657 native: NativeSupport {
658 discover: ImplementationKind::BuiltIn,
665 load: ImplementationKind::BuiltIn,
666 follow: ImplementationKind::Absent,
667 import: ImplementationKind::Absent,
668 export: ImplementationKind::Absent,
669 },
670 runtime: RuntimeSupport {
671 implementation: ImplementationKind::GenericProtocol,
672 protocol: "acp-v1-jsonrpc".into(),
673 default_launch: Some(openclaw_launch),
674 connect_launch: Some(RuntimeConnectLaunch {
681 config_path: "~/.openclaw/openclaw.json".into(),
682 address_pointer: "/gateway/remote/url".into(),
683 port_pointer: Some("/gateway/port".into()),
684 default_address: Some("ws://127.0.0.1:18789".into()),
685 auth_pointer: Some("/gateway/auth/token".into()),
686 protocol: "acp-v1-jsonrpc".into(),
687 }),
688 capabilities: openclaw.capabilities(),
689 },
690 },
691 HarnessSupportDescriptor {
701 id: HarnessId::from(HarnessId::ORCHESTRATOR),
702 display_name: "Orchestrator".into(),
703 orchestration: OrchestrationSupport::default(),
704 native: NativeSupport {
705 discover: ImplementationKind::Absent,
706 load: ImplementationKind::Absent,
707 follow: ImplementationKind::Absent,
708 import: ImplementationKind::Absent,
709 export: ImplementationKind::Absent,
710 },
711 runtime: RuntimeSupport {
712 implementation: ImplementationKind::Absent,
713 protocol: "none".into(),
714 default_launch: None,
715 connect_launch: None,
716 capabilities: RuntimeCapabilities {
717 start_session: false,
718 resume_session: false,
719 attach_existing_process: false,
720 send_input: false,
721 stream_events: false,
722 interrupt: false,
723 steer: false,
724 respond_to_requests: false,
725 },
726 },
727 },
728 HarnessSupportDescriptor {
729 id: HarnessId::from(HarnessId::SUPERCODE),
730 display_name: "Supercode".into(),
731 orchestration: OrchestrationSupport::default(),
732 native: built_in_native(),
733 runtime: RuntimeSupport {
734 implementation: ImplementationKind::GenericProtocol,
735 protocol: "acp-v1-jsonrpc".into(),
736 default_launch: Some(supercode_launch),
737 connect_launch: None,
738 capabilities: supercode.capabilities(),
739 },
740 },
741 ],
742 };
743 for descriptor in &mut report.harnesses {
744 descriptor.orchestration = orchestration_support(descriptor);
745 }
746 report
747}
748
749pub fn harness_support(id: &str) -> Option<HarnessSupportDescriptor> {
751 harness_support_registry()
752 .harnesses
753 .into_iter()
754 .find(|harness| harness.id.as_str() == id)
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760
761 #[test]
762 fn claude_code_default_launch_is_the_stream_json_prefix() {
763 let claude = harness_support_registry()
764 .harnesses
765 .into_iter()
766 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
767 .unwrap();
768 let launch = claude.runtime.default_launch.unwrap();
769 assert_eq!(launch.program, "claude");
770 let joined = launch.arguments.join(" ");
771 assert!(joined.contains("--input-format stream-json"), "{joined}");
772 assert!(joined.contains("--output-format stream-json"), "{joined}");
773 assert!(
774 joined.contains("--permission-prompt-tool stdio"),
775 "{joined}"
776 );
777 }
778
779 #[test]
780 fn grok_asks_for_a_self_sandbox_only_where_one_is_possible() {
781 let arguments = grok_arguments();
782 assert_eq!(
783 arguments.iter().any(|argument| argument == "--sandbox"),
784 self_sandbox_supported(),
785 );
786 assert!(
787 arguments.ends_with(&["agent".into(), "--no-leader".into(), "stdio".into()]),
788 "{arguments:?}",
789 );
790 }
791
792 #[test]
793 fn registry_is_unique_and_reports_all_native_support() {
794 let report = harness_support_registry();
795 assert_eq!(report.schema, SUPPORT_REGISTRY_SCHEMA);
796 assert_eq!(report.harnesses.len(), 11);
799 let ids = report
800 .harnesses
801 .iter()
802 .map(|harness| harness.id.as_str())
803 .collect::<std::collections::BTreeSet<_>>();
804 assert_eq!(ids.len(), report.harnesses.len());
805
806 let grok = report
807 .harnesses
808 .iter()
809 .find(|harness| harness.id.as_str() == HarnessId::GROK)
810 .unwrap();
811 assert_eq!(grok.native.discover, ImplementationKind::BuiltIn);
812 assert_eq!(grok.native.load, ImplementationKind::BuiltIn);
813 assert_eq!(grok.native.follow, ImplementationKind::BuiltIn);
814 assert_eq!(grok.native.import, ImplementationKind::BuiltIn);
815 for id in [HarnessId::GEMINI, HarnessId::SUPERCODE] {
816 let harness = report
817 .harnesses
818 .iter()
819 .find(|harness| harness.id.as_str() == id)
820 .unwrap();
821 assert_eq!(harness.native.discover, ImplementationKind::BuiltIn);
822 assert_eq!(harness.native.load, ImplementationKind::BuiltIn);
823 assert_eq!(harness.native.follow, ImplementationKind::BuiltIn);
824 }
825 assert_eq!(grok.native.export, ImplementationKind::BuiltIn);
826 assert_eq!(
827 grok.runtime.implementation,
828 ImplementationKind::GenericProtocol
829 );
830 let expected: Vec<&str> = if self_sandbox_supported() {
831 vec!["--sandbox", "workspace", "agent", "--no-leader", "stdio"]
832 } else {
833 vec!["agent", "--no-leader", "stdio"]
834 };
835 assert_eq!(
836 grok.runtime.default_launch.as_ref().unwrap().arguments,
837 expected
838 );
839 assert!(!grok
840 .runtime
841 .default_launch
842 .as_ref()
843 .unwrap()
844 .arguments
845 .iter()
846 .any(|argument| argument == "--always-approve"));
847 assert!(grok.runtime.capabilities.resume_session);
848 }
849
850 #[test]
856 fn openclaw_registers_gateway_mediated_with_no_store_access() {
857 let report = harness_support_registry();
858 let openclaw = report
859 .harnesses
860 .iter()
861 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
862 .expect("openclaw must be registered");
863 assert_eq!(openclaw.display_name, "OpenClaw");
864 assert_eq!(openclaw.native.discover, ImplementationKind::BuiltIn);
869 assert_eq!(openclaw.native.load, ImplementationKind::BuiltIn);
870 for kind in [
871 openclaw.native.follow,
872 openclaw.native.import,
873 openclaw.native.export,
874 ] {
875 assert_eq!(kind, ImplementationKind::Absent);
876 }
877 assert_eq!(
878 openclaw.runtime.implementation,
879 ImplementationKind::GenericProtocol
880 );
881 assert_eq!(openclaw.runtime.protocol, "acp-v1-jsonrpc");
882 let launch = openclaw.runtime.default_launch.as_ref().unwrap();
883 assert_eq!(launch.program, "openclaw");
884 assert_eq!(launch.arguments, ["acp"]);
885 let connect = openclaw.runtime.connect_launch.as_ref().unwrap();
886 assert_eq!(connect.config_path, "~/.openclaw/openclaw.json");
887 assert_eq!(connect.address_pointer, "/gateway/remote/url");
891 assert_eq!(connect.port_pointer.as_deref(), Some("/gateway/port"));
892 assert_eq!(
893 connect.default_address.as_deref(),
894 Some("ws://127.0.0.1:18789")
895 );
896 assert_eq!(connect.auth_pointer.as_deref(), Some("/gateway/auth/token"));
897 assert!(openclaw.runtime.capabilities.resume_session);
901 }
902
903 #[test]
904 fn hermes_registers_as_a_driven_tier_acp_entry_without_native_claims() {
905 let report = harness_support_registry();
906 let hermes = report
907 .harnesses
908 .iter()
909 .find(|harness| harness.id.as_str() == HarnessId::HERMES)
910 .expect("hermes must be registered");
911 assert_eq!(hermes.display_name, "Hermes Agent");
912 assert_eq!(hermes.native.discover, ImplementationKind::BuiltIn);
916 assert_eq!(hermes.native.load, ImplementationKind::BuiltIn);
917 for kind in [
918 hermes.native.follow,
919 hermes.native.import,
920 hermes.native.export,
921 ] {
922 assert_eq!(kind, ImplementationKind::Absent);
923 }
924 assert_eq!(
925 hermes.runtime.implementation,
926 ImplementationKind::GenericProtocol
927 );
928 assert_eq!(hermes.runtime.protocol, "acp-v1-jsonrpc");
929 let launch = hermes.runtime.default_launch.as_ref().unwrap();
930 assert_eq!(launch.program, "hermes-acp");
931 assert!(launch.arguments.is_empty());
932 assert!(hermes.runtime.connect_launch.is_none());
933 assert!(hermes.runtime.capabilities.resume_session);
937 assert!(!hermes.runtime.capabilities.attach_existing_process);
938 }
939
940 #[test]
941 fn connect_mode_descriptor_round_trips_the_registry_schema() {
942 let descriptor = HarnessSupportDescriptor {
943 id: HarnessId::from("openclaw"),
944 display_name: "OpenClaw".into(),
945 orchestration: OrchestrationSupport::default(),
946 native: NativeSupport {
947 discover: ImplementationKind::Absent,
948 load: ImplementationKind::Absent,
949 follow: ImplementationKind::Absent,
950 import: ImplementationKind::Absent,
951 export: ImplementationKind::Absent,
952 },
953 runtime: RuntimeSupport {
954 implementation: ImplementationKind::GenericProtocol,
955 protocol: "acp-v1-jsonrpc".into(),
956 default_launch: None,
957 connect_launch: Some(RuntimeConnectLaunch {
958 config_path: "~/.openclaw/openclaw.json".into(),
959 address_pointer: "/gateway/url".into(),
960 port_pointer: None,
961 default_address: None,
962 auth_pointer: Some("/gateway/token".into()),
963 protocol: "acp-v1-jsonrpc".into(),
964 }),
965 capabilities: RuntimeCapabilities {
966 start_session: true,
967 resume_session: false,
968 attach_existing_process: true,
969 send_input: true,
970 stream_events: true,
971 interrupt: false,
972 steer: false,
973 respond_to_requests: false,
974 },
975 },
976 };
977 let encoded = serde_json::to_value(&descriptor).unwrap();
978 assert_eq!(
979 encoded["runtime"]["connect_launch"]["address_pointer"],
980 "/gateway/url"
981 );
982 let decoded: HarnessSupportDescriptor = serde_json::from_value(encoded).unwrap();
983 assert_eq!(decoded, descriptor);
984 }
985
986 #[test]
987 fn spawn_only_registry_entries_do_not_serialize_a_connect_launch() {
988 let report = harness_support_registry();
989 for harness in &report.harnesses {
990 let encoded = serde_json::to_string(harness).unwrap();
991 if harness.id.as_str() == HarnessId::OPENCLAW {
992 assert!(encoded.contains("connect_launch"));
996 assert!(encoded.contains("/gateway/auth/token"));
997 assert!(encoded.contains("ws://127.0.0.1:18789"));
998 assert!(!encoded.contains("token\":\"ws"));
999 } else {
1000 assert!(
1001 !encoded.contains("connect_launch"),
1002 "{} must stay spawn-only",
1003 harness.id.as_str()
1004 );
1005 }
1006 }
1007 let encoded = serde_json::to_string(&report).unwrap();
1008 let decoded: SupportRegistryReport = serde_json::from_str(&encoded).unwrap();
1009 assert_eq!(decoded, report);
1010 }
1011
1012 #[test]
1015 fn orchestration_block_is_complete_and_its_methods_exist() {
1016 let report = harness_support_registry();
1017 for harness in &report.harnesses {
1018 let names: Vec<&str> = harness
1019 .orchestration
1020 .concepts
1021 .iter()
1022 .map(|c| c.concept.as_str())
1023 .collect();
1024 assert_eq!(names, ORCHESTRATION_CONCEPTS, "{}", harness.id.as_str());
1025 for concept in &harness.orchestration.concepts {
1026 let any_built_in = concept.observed == ImplementationKind::BuiltIn
1027 || concept.controlled == ImplementationKind::BuiltIn;
1028 assert_eq!(
1029 any_built_in,
1030 !concept.methods.is_empty(),
1031 "{}/{}: a BuiltIn tier must cite methods and an Absent one must not",
1032 harness.id.as_str(),
1033 concept.concept
1034 );
1035 for method in &concept.methods {
1036 assert!(
1037 crate::harness_service::HARNESS_SERVICE_METHODS.contains(&method.as_str()),
1038 "{}/{}: `{method}` is not a harness service method",
1039 harness.id.as_str(),
1040 concept.concept
1041 );
1042 }
1043 }
1044 }
1045 let hermes = report
1048 .harnesses
1049 .iter()
1050 .find(|h| h.id.as_str() == HarnessId::HERMES)
1051 .unwrap();
1052 let conv = &hermes.orchestration.concepts[2];
1053 assert_eq!(conv.concept, "conversation");
1054 assert_eq!(conv.observed, ImplementationKind::BuiltIn);
1055 assert_eq!(conv.controlled, ImplementationKind::BuiltIn);
1060 assert_eq!(
1061 conv.methods,
1062 vec![
1063 "harness.v1.sessions.discover",
1064 "harness.v1.sessions.load",
1065 "harness.v1.sessions.reset",
1066 "harness.v1.sessions.delete",
1067 ]
1068 );
1069 let openclaw_conv = &report
1072 .harnesses
1073 .iter()
1074 .find(|h| h.id.as_str() == HarnessId::OPENCLAW)
1075 .unwrap()
1076 .orchestration
1077 .concepts[2];
1078 assert_eq!(
1079 openclaw_conv.methods,
1080 vec![
1081 "harness.v1.sessions.discover",
1082 "harness.v1.sessions.load",
1083 "harness.v1.sessions.new",
1084 "harness.v1.sessions.reset",
1085 ]
1086 );
1087 let claude_conv = &report
1090 .harnesses
1091 .iter()
1092 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1093 .unwrap()
1094 .orchestration
1095 .concepts[2];
1096 assert_eq!(claude_conv.observed, ImplementationKind::BuiltIn);
1097 assert_eq!(claude_conv.controlled, ImplementationKind::Absent);
1098 assert_eq!(
1099 claude_conv.methods,
1100 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"]
1101 );
1102 for harness in &report.harnesses {
1103 let conversation = &harness.orchestration.concepts[2];
1104 let doors = crate::sessions_control::controlled_methods(harness.id.as_str());
1105 assert_eq!(
1106 conversation.controlled == ImplementationKind::BuiltIn,
1107 !doors.is_empty(),
1108 "{}: conversation controlled must track the sessions_control door table",
1109 harness.id.as_str()
1110 );
1111 for method in &doors {
1112 assert!(
1113 conversation.methods.iter().any(|listed| listed == method),
1114 "{}: `{method}` has a door but is not advertised",
1115 harness.id.as_str()
1116 );
1117 }
1118 for listed in &conversation.methods {
1119 assert!(
1120 listed.ends_with(".discover")
1121 || listed.ends_with(".load")
1122 || doors.contains(&listed.as_str()),
1123 "{}: `{listed}` is advertised with no door behind it",
1124 harness.id.as_str()
1125 );
1126 }
1127 }
1128 let pending = &hermes.orchestration.concepts[3];
1129 assert_eq!(pending.concept, "pending_request");
1130 assert_eq!(pending.controlled, ImplementationKind::BuiltIn);
1131 let job = &hermes.orchestration.concepts[0];
1135 assert_eq!(job.concept, "scheduled_job");
1136 assert_eq!(job.observed, ImplementationKind::BuiltIn);
1137 assert_eq!(job.controlled, ImplementationKind::BuiltIn);
1138 assert_eq!(
1139 job.methods,
1140 vec![
1141 "harness.v1.jobs.list",
1142 "harness.v1.jobs.get",
1143 "harness.v1.jobs.create",
1144 "harness.v1.jobs.update",
1145 "harness.v1.jobs.pause",
1146 "harness.v1.jobs.resume",
1147 "harness.v1.jobs.run",
1148 "harness.v1.jobs.delete",
1149 ]
1150 );
1151 let claude_job = &report
1153 .harnesses
1154 .iter()
1155 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1156 .unwrap()
1157 .orchestration
1158 .concepts[0];
1159 assert_eq!(claude_job.observed, ImplementationKind::BuiltIn);
1160 assert_eq!(claude_job.controlled, ImplementationKind::Absent);
1161 assert_eq!(
1162 claude_job.methods,
1163 vec!["harness.v1.jobs.list", "harness.v1.jobs.get"]
1164 );
1165 for harness in &report.harnesses {
1166 let job = &harness.orchestration.concepts[0];
1167 assert_eq!(
1168 job.observed == ImplementationKind::BuiltIn,
1169 crate::jobs::supports_jobs(harness.id.as_str()),
1170 "{}: scheduled_job observed must track JOB_HARNESSES",
1171 harness.id.as_str()
1172 );
1173 assert_eq!(
1174 job.controlled == ImplementationKind::BuiltIn,
1175 crate::jobs_control::supports_job_control(harness.id.as_str()),
1176 "{}: scheduled_job controlled must track CONTROLLED_JOB_HARNESSES",
1177 harness.id.as_str()
1178 );
1179 }
1180 let profile = &hermes.orchestration.concepts[4];
1184 assert_eq!(profile.concept, "profile");
1185 assert_eq!(profile.observed, ImplementationKind::BuiltIn);
1186 assert_eq!(profile.controlled, ImplementationKind::BuiltIn);
1187 assert_eq!(
1188 profile.methods,
1189 [
1190 "harness.v1.profiles.list",
1191 "harness.v1.profiles.get",
1192 "harness.v1.profiles.create",
1193 "harness.v1.profiles.delete",
1194 ]
1195 );
1196 let codex_profile = &report
1199 .harnesses
1200 .iter()
1201 .find(|h| h.id.as_str() == HarnessId::CODEX)
1202 .unwrap()
1203 .orchestration
1204 .concepts[4];
1205 assert_eq!(codex_profile.observed, ImplementationKind::BuiltIn);
1206 assert_eq!(codex_profile.controlled, ImplementationKind::Absent);
1207 assert_eq!(
1208 codex_profile.methods,
1209 ["harness.v1.profiles.list", "harness.v1.profiles.get"]
1210 );
1211 for harness in &report.harnesses {
1212 let profile = &harness.orchestration.concepts[4];
1213 assert_eq!(
1214 profile.observed == ImplementationKind::BuiltIn,
1215 crate::profiles::PROFILE_HARNESSES.contains(&harness.id.as_str()),
1216 "{}: profile observed tier disagrees with PROFILE_HARNESSES",
1217 harness.id.as_str()
1218 );
1219 assert_eq!(
1220 profile.controlled == ImplementationKind::BuiltIn,
1221 crate::profiles_control::supports_profile_control(harness.id.as_str()),
1222 "{}: profile controlled tier disagrees with CONTROLLED_PROFILE_HARNESSES",
1223 harness.id.as_str()
1224 );
1225 }
1226 }
1227
1228 #[test]
1232 fn memory_is_observed_only_for_the_harnesses_with_a_memory_store() {
1233 let report = harness_support_registry();
1234 for harness in &report.harnesses {
1235 let memory = harness
1236 .orchestration
1237 .concepts
1238 .iter()
1239 .find(|concept| concept.concept == "memory")
1240 .unwrap_or_else(|| panic!("{}: no memory concept row", harness.id.as_str()));
1241 if crate::memory::MEMORY_HARNESSES.contains(&harness.id.as_str()) {
1242 assert_eq!(
1243 memory.observed,
1244 ImplementationKind::BuiltIn,
1245 "{}: memory must be observed",
1246 harness.id.as_str()
1247 );
1248 assert_eq!(
1249 memory.methods,
1250 vec![
1251 "harness.v1.memory.show".to_string(),
1252 "harness.v1.memory.search".to_string()
1253 ]
1254 );
1255 } else {
1256 assert_eq!(
1257 memory.observed,
1258 ImplementationKind::Absent,
1259 "{}: memory must be absent",
1260 harness.id.as_str()
1261 );
1262 assert!(memory.methods.is_empty(), "{}", harness.id.as_str());
1263 }
1264 assert_eq!(memory.controlled, ImplementationKind::Absent);
1266 }
1267 }
1268
1269 #[test]
1273 fn skills_are_observed_and_controlled_for_the_harnesses_with_a_skills_loader() {
1274 let report = harness_support_registry();
1275 for harness in &report.harnesses {
1276 let skills = harness
1277 .orchestration
1278 .concepts
1279 .iter()
1280 .find(|concept| concept.concept == "skills")
1281 .unwrap();
1282 if crate::skills::SKILL_HARNESSES.contains(&harness.id.as_str()) {
1283 assert_eq!(
1284 skills.observed,
1285 ImplementationKind::BuiltIn,
1286 "{}",
1287 harness.id.as_str()
1288 );
1289 assert_eq!(
1293 skills.controlled,
1294 ImplementationKind::BuiltIn,
1295 "{}",
1296 harness.id.as_str()
1297 );
1298 assert_eq!(
1299 skills.methods,
1300 vec![
1301 "harness.v1.skills.list".to_string(),
1302 "harness.v1.skills.install".to_string(),
1303 "harness.v1.skills.remove".to_string(),
1304 ]
1305 );
1306 } else {
1307 assert_eq!(
1308 skills.observed,
1309 ImplementationKind::Absent,
1310 "{}",
1311 harness.id.as_str()
1312 );
1313 assert_eq!(
1314 skills.controlled,
1315 ImplementationKind::Absent,
1316 "{}",
1317 harness.id.as_str()
1318 );
1319 }
1320 }
1321 }
1322
1323 #[test]
1328 fn channels_are_observed_for_the_gateway_harnesses_only() {
1329 let report = harness_support_registry();
1330 for harness in &report.harnesses {
1331 let channel = harness
1332 .orchestration
1333 .concepts
1334 .iter()
1335 .find(|concept| concept.concept == "channel")
1336 .unwrap();
1337 if crate::channels::CHANNEL_HARNESSES.contains(&harness.id.as_str()) {
1338 assert_eq!(
1339 channel.observed,
1340 ImplementationKind::BuiltIn,
1341 "{}",
1342 harness.id.as_str()
1343 );
1344 assert_eq!(
1345 channel.methods,
1346 ["harness.v1.channels.list", "harness.v1.channels.status"]
1347 );
1348 } else {
1349 assert_eq!(
1350 channel.observed,
1351 ImplementationKind::Absent,
1352 "{}",
1353 harness.id.as_str()
1354 );
1355 assert!(channel.methods.is_empty(), "{}", harness.id.as_str());
1356 }
1357 assert_eq!(channel.controlled, ImplementationKind::Absent);
1359 }
1360 }
1361
1362 #[test]
1368 fn runs_are_observed_only_where_the_harness_keeps_a_fire_store() {
1369 let report = harness_support_registry();
1370 let concept = |harness: &HarnessSupportDescriptor, name: &str| {
1371 harness
1372 .orchestration
1373 .concepts
1374 .iter()
1375 .find(|concept| concept.concept == name)
1376 .unwrap_or_else(|| panic!("no `{name}` concept for {}", harness.id.as_str()))
1377 .clone()
1378 };
1379 let mut observed = Vec::new();
1380 for harness in &report.harnesses {
1381 let run = concept(harness, "run");
1382 if crate::runs::RUN_HARNESSES.contains(&harness.id.as_str()) {
1383 assert_eq!(
1384 run.observed,
1385 ImplementationKind::BuiltIn,
1386 "{}",
1387 harness.id.as_str()
1388 );
1389 assert_eq!(
1390 run.methods,
1391 vec![
1392 "harness.v1.runs.list".to_string(),
1393 "harness.v1.runs.get".to_string()
1394 ]
1395 );
1396 observed.push(harness.id.as_str().to_string());
1397 } else {
1398 assert_eq!(
1399 run.observed,
1400 ImplementationKind::Absent,
1401 "{}",
1402 harness.id.as_str()
1403 );
1404 assert!(run.methods.is_empty(), "{}", harness.id.as_str());
1405 }
1406 assert_eq!(run.controlled, ImplementationKind::Absent);
1409 }
1410 assert_eq!(observed, vec!["hermes", "openclaw", "orchestrator"]);
1414
1415 let claude = report
1416 .harnesses
1417 .iter()
1418 .find(|harness| harness.id.as_str() == HarnessId::CLAUDE_CODE)
1419 .expect("claude-code is in the registry");
1420 assert_eq!(
1421 concept(claude, "scheduled_job").observed,
1422 ImplementationKind::BuiltIn,
1423 );
1424 assert_eq!(concept(claude, "run").observed, ImplementationKind::Absent);
1425 }
1426
1427 #[test]
1429 fn orchestration_block_is_additive_on_the_wire() {
1430 let report = harness_support_registry();
1431 let mut value = serde_json::to_value(&report.harnesses[0]).unwrap();
1432 value.as_object_mut().unwrap().remove("orchestration");
1433 let back: HarnessSupportDescriptor = serde_json::from_value(value).unwrap();
1434 assert!(back.orchestration.concepts.is_empty());
1435 }
1436}