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::GenericProtocol,
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);
915 assert_eq!(hermes.native.load, ImplementationKind::BuiltIn);
916 for kind in [hermes.native.follow, hermes.native.import] {
917 assert_eq!(kind, ImplementationKind::Absent);
918 }
919 assert_eq!(hermes.native.export, ImplementationKind::GenericProtocol);
920 assert_eq!(
921 hermes.runtime.implementation,
922 ImplementationKind::GenericProtocol
923 );
924 assert_eq!(hermes.runtime.protocol, "acp-v1-jsonrpc");
925 let launch = hermes.runtime.default_launch.as_ref().unwrap();
926 assert_eq!(launch.program, "hermes-acp");
927 assert!(launch.arguments.is_empty());
928 assert!(hermes.runtime.connect_launch.is_none());
929 assert!(hermes.runtime.capabilities.resume_session);
933 assert!(!hermes.runtime.capabilities.attach_existing_process);
934 }
935
936 #[test]
937 fn connect_mode_descriptor_round_trips_the_registry_schema() {
938 let descriptor = HarnessSupportDescriptor {
939 id: HarnessId::from("openclaw"),
940 display_name: "OpenClaw".into(),
941 orchestration: OrchestrationSupport::default(),
942 native: NativeSupport {
943 discover: ImplementationKind::Absent,
944 load: ImplementationKind::Absent,
945 follow: ImplementationKind::Absent,
946 import: ImplementationKind::Absent,
947 export: ImplementationKind::Absent,
948 },
949 runtime: RuntimeSupport {
950 implementation: ImplementationKind::GenericProtocol,
951 protocol: "acp-v1-jsonrpc".into(),
952 default_launch: None,
953 connect_launch: Some(RuntimeConnectLaunch {
954 config_path: "~/.openclaw/openclaw.json".into(),
955 address_pointer: "/gateway/url".into(),
956 port_pointer: None,
957 default_address: None,
958 auth_pointer: Some("/gateway/token".into()),
959 protocol: "acp-v1-jsonrpc".into(),
960 }),
961 capabilities: RuntimeCapabilities {
962 start_session: true,
963 resume_session: false,
964 attach_existing_process: true,
965 send_input: true,
966 stream_events: true,
967 interrupt: false,
968 steer: false,
969 respond_to_requests: false,
970 },
971 },
972 };
973 let encoded = serde_json::to_value(&descriptor).unwrap();
974 assert_eq!(
975 encoded["runtime"]["connect_launch"]["address_pointer"],
976 "/gateway/url"
977 );
978 let decoded: HarnessSupportDescriptor = serde_json::from_value(encoded).unwrap();
979 assert_eq!(decoded, descriptor);
980 }
981
982 #[test]
983 fn spawn_only_registry_entries_do_not_serialize_a_connect_launch() {
984 let report = harness_support_registry();
985 for harness in &report.harnesses {
986 let encoded = serde_json::to_string(harness).unwrap();
987 if harness.id.as_str() == HarnessId::OPENCLAW {
988 assert!(encoded.contains("connect_launch"));
992 assert!(encoded.contains("/gateway/auth/token"));
993 assert!(encoded.contains("ws://127.0.0.1:18789"));
994 assert!(!encoded.contains("token\":\"ws"));
995 } else {
996 assert!(
997 !encoded.contains("connect_launch"),
998 "{} must stay spawn-only",
999 harness.id.as_str()
1000 );
1001 }
1002 }
1003 let encoded = serde_json::to_string(&report).unwrap();
1004 let decoded: SupportRegistryReport = serde_json::from_str(&encoded).unwrap();
1005 assert_eq!(decoded, report);
1006 }
1007
1008 #[test]
1011 fn orchestration_block_is_complete_and_its_methods_exist() {
1012 let report = harness_support_registry();
1013 for harness in &report.harnesses {
1014 let names: Vec<&str> = harness
1015 .orchestration
1016 .concepts
1017 .iter()
1018 .map(|c| c.concept.as_str())
1019 .collect();
1020 assert_eq!(names, ORCHESTRATION_CONCEPTS, "{}", harness.id.as_str());
1021 for concept in &harness.orchestration.concepts {
1022 let any_built_in = concept.observed == ImplementationKind::BuiltIn
1023 || concept.controlled == ImplementationKind::BuiltIn;
1024 assert_eq!(
1025 any_built_in,
1026 !concept.methods.is_empty(),
1027 "{}/{}: a BuiltIn tier must cite methods and an Absent one must not",
1028 harness.id.as_str(),
1029 concept.concept
1030 );
1031 for method in &concept.methods {
1032 assert!(
1033 crate::harness_service::HARNESS_SERVICE_METHODS.contains(&method.as_str()),
1034 "{}/{}: `{method}` is not a harness service method",
1035 harness.id.as_str(),
1036 concept.concept
1037 );
1038 }
1039 }
1040 }
1041 let hermes = report
1044 .harnesses
1045 .iter()
1046 .find(|h| h.id.as_str() == HarnessId::HERMES)
1047 .unwrap();
1048 let conv = &hermes.orchestration.concepts[2];
1049 assert_eq!(conv.concept, "conversation");
1050 assert_eq!(conv.observed, ImplementationKind::BuiltIn);
1051 assert_eq!(conv.controlled, ImplementationKind::BuiltIn);
1056 assert_eq!(
1057 conv.methods,
1058 vec![
1059 "harness.v1.sessions.discover",
1060 "harness.v1.sessions.load",
1061 "harness.v1.sessions.reset",
1062 "harness.v1.sessions.delete",
1063 ]
1064 );
1065 let openclaw_conv = &report
1068 .harnesses
1069 .iter()
1070 .find(|h| h.id.as_str() == HarnessId::OPENCLAW)
1071 .unwrap()
1072 .orchestration
1073 .concepts[2];
1074 assert_eq!(
1075 openclaw_conv.methods,
1076 vec![
1077 "harness.v1.sessions.discover",
1078 "harness.v1.sessions.load",
1079 "harness.v1.sessions.new",
1080 "harness.v1.sessions.reset",
1081 ]
1082 );
1083 let claude_conv = &report
1086 .harnesses
1087 .iter()
1088 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1089 .unwrap()
1090 .orchestration
1091 .concepts[2];
1092 assert_eq!(claude_conv.observed, ImplementationKind::BuiltIn);
1093 assert_eq!(claude_conv.controlled, ImplementationKind::Absent);
1094 assert_eq!(
1095 claude_conv.methods,
1096 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"]
1097 );
1098 for harness in &report.harnesses {
1099 let conversation = &harness.orchestration.concepts[2];
1100 let doors = crate::sessions_control::controlled_methods(harness.id.as_str());
1101 assert_eq!(
1102 conversation.controlled == ImplementationKind::BuiltIn,
1103 !doors.is_empty(),
1104 "{}: conversation controlled must track the sessions_control door table",
1105 harness.id.as_str()
1106 );
1107 for method in &doors {
1108 assert!(
1109 conversation.methods.iter().any(|listed| listed == method),
1110 "{}: `{method}` has a door but is not advertised",
1111 harness.id.as_str()
1112 );
1113 }
1114 for listed in &conversation.methods {
1115 assert!(
1116 listed.ends_with(".discover")
1117 || listed.ends_with(".load")
1118 || doors.contains(&listed.as_str()),
1119 "{}: `{listed}` is advertised with no door behind it",
1120 harness.id.as_str()
1121 );
1122 }
1123 }
1124 let pending = &hermes.orchestration.concepts[3];
1125 assert_eq!(pending.concept, "pending_request");
1126 assert_eq!(pending.controlled, ImplementationKind::BuiltIn);
1127 let job = &hermes.orchestration.concepts[0];
1131 assert_eq!(job.concept, "scheduled_job");
1132 assert_eq!(job.observed, ImplementationKind::BuiltIn);
1133 assert_eq!(job.controlled, ImplementationKind::BuiltIn);
1134 assert_eq!(
1135 job.methods,
1136 vec![
1137 "harness.v1.jobs.list",
1138 "harness.v1.jobs.get",
1139 "harness.v1.jobs.create",
1140 "harness.v1.jobs.update",
1141 "harness.v1.jobs.pause",
1142 "harness.v1.jobs.resume",
1143 "harness.v1.jobs.run",
1144 "harness.v1.jobs.delete",
1145 ]
1146 );
1147 let claude_job = &report
1149 .harnesses
1150 .iter()
1151 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1152 .unwrap()
1153 .orchestration
1154 .concepts[0];
1155 assert_eq!(claude_job.observed, ImplementationKind::BuiltIn);
1156 assert_eq!(claude_job.controlled, ImplementationKind::Absent);
1157 assert_eq!(
1158 claude_job.methods,
1159 vec!["harness.v1.jobs.list", "harness.v1.jobs.get"]
1160 );
1161 for harness in &report.harnesses {
1162 let job = &harness.orchestration.concepts[0];
1163 assert_eq!(
1164 job.observed == ImplementationKind::BuiltIn,
1165 crate::jobs::supports_jobs(harness.id.as_str()),
1166 "{}: scheduled_job observed must track JOB_HARNESSES",
1167 harness.id.as_str()
1168 );
1169 assert_eq!(
1170 job.controlled == ImplementationKind::BuiltIn,
1171 crate::jobs_control::supports_job_control(harness.id.as_str()),
1172 "{}: scheduled_job controlled must track CONTROLLED_JOB_HARNESSES",
1173 harness.id.as_str()
1174 );
1175 }
1176 let profile = &hermes.orchestration.concepts[4];
1180 assert_eq!(profile.concept, "profile");
1181 assert_eq!(profile.observed, ImplementationKind::BuiltIn);
1182 assert_eq!(profile.controlled, ImplementationKind::BuiltIn);
1183 assert_eq!(
1184 profile.methods,
1185 [
1186 "harness.v1.profiles.list",
1187 "harness.v1.profiles.get",
1188 "harness.v1.profiles.create",
1189 "harness.v1.profiles.delete",
1190 ]
1191 );
1192 let codex_profile = &report
1195 .harnesses
1196 .iter()
1197 .find(|h| h.id.as_str() == HarnessId::CODEX)
1198 .unwrap()
1199 .orchestration
1200 .concepts[4];
1201 assert_eq!(codex_profile.observed, ImplementationKind::BuiltIn);
1202 assert_eq!(codex_profile.controlled, ImplementationKind::Absent);
1203 assert_eq!(
1204 codex_profile.methods,
1205 ["harness.v1.profiles.list", "harness.v1.profiles.get"]
1206 );
1207 for harness in &report.harnesses {
1208 let profile = &harness.orchestration.concepts[4];
1209 assert_eq!(
1210 profile.observed == ImplementationKind::BuiltIn,
1211 crate::profiles::PROFILE_HARNESSES.contains(&harness.id.as_str()),
1212 "{}: profile observed tier disagrees with PROFILE_HARNESSES",
1213 harness.id.as_str()
1214 );
1215 assert_eq!(
1216 profile.controlled == ImplementationKind::BuiltIn,
1217 crate::profiles_control::supports_profile_control(harness.id.as_str()),
1218 "{}: profile controlled tier disagrees with CONTROLLED_PROFILE_HARNESSES",
1219 harness.id.as_str()
1220 );
1221 }
1222 }
1223
1224 #[test]
1228 fn memory_is_observed_only_for_the_harnesses_with_a_memory_store() {
1229 let report = harness_support_registry();
1230 for harness in &report.harnesses {
1231 let memory = harness
1232 .orchestration
1233 .concepts
1234 .iter()
1235 .find(|concept| concept.concept == "memory")
1236 .unwrap_or_else(|| panic!("{}: no memory concept row", harness.id.as_str()));
1237 if crate::memory::MEMORY_HARNESSES.contains(&harness.id.as_str()) {
1238 assert_eq!(
1239 memory.observed,
1240 ImplementationKind::BuiltIn,
1241 "{}: memory must be observed",
1242 harness.id.as_str()
1243 );
1244 assert_eq!(
1245 memory.methods,
1246 vec![
1247 "harness.v1.memory.show".to_string(),
1248 "harness.v1.memory.search".to_string()
1249 ]
1250 );
1251 } else {
1252 assert_eq!(
1253 memory.observed,
1254 ImplementationKind::Absent,
1255 "{}: memory must be absent",
1256 harness.id.as_str()
1257 );
1258 assert!(memory.methods.is_empty(), "{}", harness.id.as_str());
1259 }
1260 assert_eq!(memory.controlled, ImplementationKind::Absent);
1262 }
1263 }
1264
1265 #[test]
1269 fn skills_are_observed_and_controlled_for_the_harnesses_with_a_skills_loader() {
1270 let report = harness_support_registry();
1271 for harness in &report.harnesses {
1272 let skills = harness
1273 .orchestration
1274 .concepts
1275 .iter()
1276 .find(|concept| concept.concept == "skills")
1277 .unwrap();
1278 if crate::skills::SKILL_HARNESSES.contains(&harness.id.as_str()) {
1279 assert_eq!(
1280 skills.observed,
1281 ImplementationKind::BuiltIn,
1282 "{}",
1283 harness.id.as_str()
1284 );
1285 assert_eq!(
1289 skills.controlled,
1290 ImplementationKind::BuiltIn,
1291 "{}",
1292 harness.id.as_str()
1293 );
1294 assert_eq!(
1295 skills.methods,
1296 vec![
1297 "harness.v1.skills.list".to_string(),
1298 "harness.v1.skills.install".to_string(),
1299 "harness.v1.skills.remove".to_string(),
1300 ]
1301 );
1302 } else {
1303 assert_eq!(
1304 skills.observed,
1305 ImplementationKind::Absent,
1306 "{}",
1307 harness.id.as_str()
1308 );
1309 assert_eq!(
1310 skills.controlled,
1311 ImplementationKind::Absent,
1312 "{}",
1313 harness.id.as_str()
1314 );
1315 }
1316 }
1317 }
1318
1319 #[test]
1324 fn channels_are_observed_for_the_gateway_harnesses_only() {
1325 let report = harness_support_registry();
1326 for harness in &report.harnesses {
1327 let channel = harness
1328 .orchestration
1329 .concepts
1330 .iter()
1331 .find(|concept| concept.concept == "channel")
1332 .unwrap();
1333 if crate::channels::CHANNEL_HARNESSES.contains(&harness.id.as_str()) {
1334 assert_eq!(
1335 channel.observed,
1336 ImplementationKind::BuiltIn,
1337 "{}",
1338 harness.id.as_str()
1339 );
1340 assert_eq!(
1341 channel.methods,
1342 ["harness.v1.channels.list", "harness.v1.channels.status"]
1343 );
1344 } else {
1345 assert_eq!(
1346 channel.observed,
1347 ImplementationKind::Absent,
1348 "{}",
1349 harness.id.as_str()
1350 );
1351 assert!(channel.methods.is_empty(), "{}", harness.id.as_str());
1352 }
1353 assert_eq!(channel.controlled, ImplementationKind::Absent);
1355 }
1356 }
1357
1358 #[test]
1364 fn runs_are_observed_only_where_the_harness_keeps_a_fire_store() {
1365 let report = harness_support_registry();
1366 let concept = |harness: &HarnessSupportDescriptor, name: &str| {
1367 harness
1368 .orchestration
1369 .concepts
1370 .iter()
1371 .find(|concept| concept.concept == name)
1372 .unwrap_or_else(|| panic!("no `{name}` concept for {}", harness.id.as_str()))
1373 .clone()
1374 };
1375 let mut observed = Vec::new();
1376 for harness in &report.harnesses {
1377 let run = concept(harness, "run");
1378 if crate::runs::RUN_HARNESSES.contains(&harness.id.as_str()) {
1379 assert_eq!(
1380 run.observed,
1381 ImplementationKind::BuiltIn,
1382 "{}",
1383 harness.id.as_str()
1384 );
1385 assert_eq!(
1386 run.methods,
1387 vec![
1388 "harness.v1.runs.list".to_string(),
1389 "harness.v1.runs.get".to_string()
1390 ]
1391 );
1392 observed.push(harness.id.as_str().to_string());
1393 } else {
1394 assert_eq!(
1395 run.observed,
1396 ImplementationKind::Absent,
1397 "{}",
1398 harness.id.as_str()
1399 );
1400 assert!(run.methods.is_empty(), "{}", harness.id.as_str());
1401 }
1402 assert_eq!(run.controlled, ImplementationKind::Absent);
1405 }
1406 assert_eq!(observed, vec!["hermes", "openclaw", "orchestrator"]);
1410
1411 let claude = report
1412 .harnesses
1413 .iter()
1414 .find(|harness| harness.id.as_str() == HarnessId::CLAUDE_CODE)
1415 .expect("claude-code is in the registry");
1416 assert_eq!(
1417 concept(claude, "scheduled_job").observed,
1418 ImplementationKind::BuiltIn,
1419 );
1420 assert_eq!(concept(claude, "run").observed, ImplementationKind::Absent);
1421 }
1422
1423 #[test]
1425 fn orchestration_block_is_additive_on_the_wire() {
1426 let report = harness_support_registry();
1427 let mut value = serde_json::to_value(&report.harnesses[0]).unwrap();
1428 value.as_object_mut().unwrap().remove("orchestration");
1429 let back: HarnessSupportDescriptor = serde_json::from_value(value).unwrap();
1430 assert!(back.orchestration.concepts.is_empty());
1431 }
1432}