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,
638 load: ImplementationKind::BuiltIn,
639 follow: ImplementationKind::Absent,
640 import: ImplementationKind::Absent,
641 export: ImplementationKind::Absent,
642 },
643 runtime: RuntimeSupport {
644 implementation: ImplementationKind::GenericProtocol,
645 protocol: "acp-v1-jsonrpc".into(),
646 default_launch: Some(hermes_launch),
647 connect_launch: None,
648 capabilities: hermes.capabilities(),
649 },
650 },
651 HarnessSupportDescriptor {
652 id: HarnessId::from(HarnessId::OPENCLAW),
653 display_name: "OpenClaw".into(),
654 orchestration: OrchestrationSupport::default(),
655 native: NativeSupport {
656 discover: ImplementationKind::BuiltIn,
663 load: ImplementationKind::BuiltIn,
664 follow: ImplementationKind::Absent,
665 import: ImplementationKind::Absent,
666 export: ImplementationKind::Absent,
667 },
668 runtime: RuntimeSupport {
669 implementation: ImplementationKind::GenericProtocol,
670 protocol: "acp-v1-jsonrpc".into(),
671 default_launch: Some(openclaw_launch),
672 connect_launch: Some(RuntimeConnectLaunch {
679 config_path: "~/.openclaw/openclaw.json".into(),
680 address_pointer: "/gateway/remote/url".into(),
681 port_pointer: Some("/gateway/port".into()),
682 default_address: Some("ws://127.0.0.1:18789".into()),
683 auth_pointer: Some("/gateway/auth/token".into()),
684 protocol: "acp-v1-jsonrpc".into(),
685 }),
686 capabilities: openclaw.capabilities(),
687 },
688 },
689 HarnessSupportDescriptor {
699 id: HarnessId::from(HarnessId::ORCHESTRATOR),
700 display_name: "Orchestrator".into(),
701 orchestration: OrchestrationSupport::default(),
702 native: NativeSupport {
703 discover: ImplementationKind::Absent,
704 load: ImplementationKind::Absent,
705 follow: ImplementationKind::Absent,
706 import: ImplementationKind::Absent,
707 export: ImplementationKind::Absent,
708 },
709 runtime: RuntimeSupport {
710 implementation: ImplementationKind::Absent,
711 protocol: "none".into(),
712 default_launch: None,
713 connect_launch: None,
714 capabilities: RuntimeCapabilities {
715 start_session: false,
716 resume_session: false,
717 attach_existing_process: false,
718 send_input: false,
719 stream_events: false,
720 interrupt: false,
721 steer: false,
722 respond_to_requests: false,
723 },
724 },
725 },
726 HarnessSupportDescriptor {
727 id: HarnessId::from(HarnessId::SUPERCODE),
728 display_name: "Supercode".into(),
729 orchestration: OrchestrationSupport::default(),
730 native: built_in_native(),
731 runtime: RuntimeSupport {
732 implementation: ImplementationKind::GenericProtocol,
733 protocol: "acp-v1-jsonrpc".into(),
734 default_launch: Some(supercode_launch),
735 connect_launch: None,
736 capabilities: supercode.capabilities(),
737 },
738 },
739 ],
740 };
741 for descriptor in &mut report.harnesses {
742 descriptor.orchestration = orchestration_support(descriptor);
743 }
744 report
745}
746
747pub fn harness_support(id: &str) -> Option<HarnessSupportDescriptor> {
749 harness_support_registry()
750 .harnesses
751 .into_iter()
752 .find(|harness| harness.id.as_str() == id)
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
760 fn claude_code_default_launch_is_the_stream_json_prefix() {
761 let claude = harness_support_registry()
762 .harnesses
763 .into_iter()
764 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
765 .unwrap();
766 let launch = claude.runtime.default_launch.unwrap();
767 assert_eq!(launch.program, "claude");
768 let joined = launch.arguments.join(" ");
769 assert!(joined.contains("--input-format stream-json"), "{joined}");
770 assert!(joined.contains("--output-format stream-json"), "{joined}");
771 assert!(
772 joined.contains("--permission-prompt-tool stdio"),
773 "{joined}"
774 );
775 }
776
777 #[test]
778 fn grok_asks_for_a_self_sandbox_only_where_one_is_possible() {
779 let arguments = grok_arguments();
780 assert_eq!(
781 arguments.iter().any(|argument| argument == "--sandbox"),
782 self_sandbox_supported(),
783 );
784 assert!(
785 arguments.ends_with(&["agent".into(), "--no-leader".into(), "stdio".into()]),
786 "{arguments:?}",
787 );
788 }
789
790 #[test]
791 fn registry_is_unique_and_reports_all_native_support() {
792 let report = harness_support_registry();
793 assert_eq!(report.schema, SUPPORT_REGISTRY_SCHEMA);
794 assert_eq!(report.harnesses.len(), 11);
797 let ids = report
798 .harnesses
799 .iter()
800 .map(|harness| harness.id.as_str())
801 .collect::<std::collections::BTreeSet<_>>();
802 assert_eq!(ids.len(), report.harnesses.len());
803
804 let grok = report
805 .harnesses
806 .iter()
807 .find(|harness| harness.id.as_str() == HarnessId::GROK)
808 .unwrap();
809 assert_eq!(grok.native.discover, ImplementationKind::BuiltIn);
810 assert_eq!(grok.native.load, ImplementationKind::BuiltIn);
811 assert_eq!(grok.native.follow, ImplementationKind::BuiltIn);
812 assert_eq!(grok.native.import, ImplementationKind::BuiltIn);
813 for id in [HarnessId::GEMINI, HarnessId::SUPERCODE] {
814 let harness = report
815 .harnesses
816 .iter()
817 .find(|harness| harness.id.as_str() == id)
818 .unwrap();
819 assert_eq!(harness.native.discover, ImplementationKind::BuiltIn);
820 assert_eq!(harness.native.load, ImplementationKind::BuiltIn);
821 assert_eq!(harness.native.follow, ImplementationKind::BuiltIn);
822 }
823 assert_eq!(grok.native.export, ImplementationKind::BuiltIn);
824 assert_eq!(
825 grok.runtime.implementation,
826 ImplementationKind::GenericProtocol
827 );
828 let expected: Vec<&str> = if self_sandbox_supported() {
829 vec!["--sandbox", "workspace", "agent", "--no-leader", "stdio"]
830 } else {
831 vec!["agent", "--no-leader", "stdio"]
832 };
833 assert_eq!(
834 grok.runtime.default_launch.as_ref().unwrap().arguments,
835 expected
836 );
837 assert!(!grok
838 .runtime
839 .default_launch
840 .as_ref()
841 .unwrap()
842 .arguments
843 .iter()
844 .any(|argument| argument == "--always-approve"));
845 assert!(grok.runtime.capabilities.resume_session);
846 }
847
848 #[test]
854 fn openclaw_registers_gateway_mediated_with_no_store_access() {
855 let report = harness_support_registry();
856 let openclaw = report
857 .harnesses
858 .iter()
859 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
860 .expect("openclaw must be registered");
861 assert_eq!(openclaw.display_name, "OpenClaw");
862 assert_eq!(openclaw.native.discover, ImplementationKind::BuiltIn);
867 assert_eq!(openclaw.native.load, ImplementationKind::BuiltIn);
868 for kind in [
869 openclaw.native.follow,
870 openclaw.native.import,
871 openclaw.native.export,
872 ] {
873 assert_eq!(kind, ImplementationKind::Absent);
874 }
875 assert_eq!(
876 openclaw.runtime.implementation,
877 ImplementationKind::GenericProtocol
878 );
879 assert_eq!(openclaw.runtime.protocol, "acp-v1-jsonrpc");
880 let launch = openclaw.runtime.default_launch.as_ref().unwrap();
881 assert_eq!(launch.program, "openclaw");
882 assert_eq!(launch.arguments, ["acp"]);
883 let connect = openclaw.runtime.connect_launch.as_ref().unwrap();
884 assert_eq!(connect.config_path, "~/.openclaw/openclaw.json");
885 assert_eq!(connect.address_pointer, "/gateway/remote/url");
889 assert_eq!(connect.port_pointer.as_deref(), Some("/gateway/port"));
890 assert_eq!(
891 connect.default_address.as_deref(),
892 Some("ws://127.0.0.1:18789")
893 );
894 assert_eq!(connect.auth_pointer.as_deref(), Some("/gateway/auth/token"));
895 assert!(openclaw.runtime.capabilities.resume_session);
899 }
900
901 #[test]
902 fn hermes_registers_as_a_driven_tier_acp_entry_without_native_claims() {
903 let report = harness_support_registry();
904 let hermes = report
905 .harnesses
906 .iter()
907 .find(|harness| harness.id.as_str() == HarnessId::HERMES)
908 .expect("hermes must be registered");
909 assert_eq!(hermes.display_name, "Hermes Agent");
910 assert_eq!(hermes.native.discover, ImplementationKind::BuiltIn);
914 assert_eq!(hermes.native.load, ImplementationKind::BuiltIn);
915 for kind in [
916 hermes.native.follow,
917 hermes.native.import,
918 hermes.native.export,
919 ] {
920 assert_eq!(kind, ImplementationKind::Absent);
921 }
922 assert_eq!(
923 hermes.runtime.implementation,
924 ImplementationKind::GenericProtocol
925 );
926 assert_eq!(hermes.runtime.protocol, "acp-v1-jsonrpc");
927 let launch = hermes.runtime.default_launch.as_ref().unwrap();
928 assert_eq!(launch.program, "hermes-acp");
929 assert!(launch.arguments.is_empty());
930 assert!(hermes.runtime.connect_launch.is_none());
931 assert!(hermes.runtime.capabilities.resume_session);
935 assert!(!hermes.runtime.capabilities.attach_existing_process);
936 }
937
938 #[test]
939 fn connect_mode_descriptor_round_trips_the_registry_schema() {
940 let descriptor = HarnessSupportDescriptor {
941 id: HarnessId::from("openclaw"),
942 display_name: "OpenClaw".into(),
943 orchestration: OrchestrationSupport::default(),
944 native: NativeSupport {
945 discover: ImplementationKind::Absent,
946 load: ImplementationKind::Absent,
947 follow: ImplementationKind::Absent,
948 import: ImplementationKind::Absent,
949 export: ImplementationKind::Absent,
950 },
951 runtime: RuntimeSupport {
952 implementation: ImplementationKind::GenericProtocol,
953 protocol: "acp-v1-jsonrpc".into(),
954 default_launch: None,
955 connect_launch: Some(RuntimeConnectLaunch {
956 config_path: "~/.openclaw/openclaw.json".into(),
957 address_pointer: "/gateway/url".into(),
958 port_pointer: None,
959 default_address: None,
960 auth_pointer: Some("/gateway/token".into()),
961 protocol: "acp-v1-jsonrpc".into(),
962 }),
963 capabilities: RuntimeCapabilities {
964 start_session: true,
965 resume_session: false,
966 attach_existing_process: true,
967 send_input: true,
968 stream_events: true,
969 interrupt: false,
970 steer: false,
971 respond_to_requests: false,
972 },
973 },
974 };
975 let encoded = serde_json::to_value(&descriptor).unwrap();
976 assert_eq!(
977 encoded["runtime"]["connect_launch"]["address_pointer"],
978 "/gateway/url"
979 );
980 let decoded: HarnessSupportDescriptor = serde_json::from_value(encoded).unwrap();
981 assert_eq!(decoded, descriptor);
982 }
983
984 #[test]
985 fn spawn_only_registry_entries_do_not_serialize_a_connect_launch() {
986 let report = harness_support_registry();
987 for harness in &report.harnesses {
988 let encoded = serde_json::to_string(harness).unwrap();
989 if harness.id.as_str() == HarnessId::OPENCLAW {
990 assert!(encoded.contains("connect_launch"));
994 assert!(encoded.contains("/gateway/auth/token"));
995 assert!(encoded.contains("ws://127.0.0.1:18789"));
996 assert!(!encoded.contains("token\":\"ws"));
997 } else {
998 assert!(
999 !encoded.contains("connect_launch"),
1000 "{} must stay spawn-only",
1001 harness.id.as_str()
1002 );
1003 }
1004 }
1005 let encoded = serde_json::to_string(&report).unwrap();
1006 let decoded: SupportRegistryReport = serde_json::from_str(&encoded).unwrap();
1007 assert_eq!(decoded, report);
1008 }
1009
1010 #[test]
1013 fn orchestration_block_is_complete_and_its_methods_exist() {
1014 let report = harness_support_registry();
1015 for harness in &report.harnesses {
1016 let names: Vec<&str> = harness
1017 .orchestration
1018 .concepts
1019 .iter()
1020 .map(|c| c.concept.as_str())
1021 .collect();
1022 assert_eq!(names, ORCHESTRATION_CONCEPTS, "{}", harness.id.as_str());
1023 for concept in &harness.orchestration.concepts {
1024 let any_built_in = concept.observed == ImplementationKind::BuiltIn
1025 || concept.controlled == ImplementationKind::BuiltIn;
1026 assert_eq!(
1027 any_built_in,
1028 !concept.methods.is_empty(),
1029 "{}/{}: a BuiltIn tier must cite methods and an Absent one must not",
1030 harness.id.as_str(),
1031 concept.concept
1032 );
1033 for method in &concept.methods {
1034 assert!(
1035 crate::harness_service::HARNESS_SERVICE_METHODS.contains(&method.as_str()),
1036 "{}/{}: `{method}` is not a harness service method",
1037 harness.id.as_str(),
1038 concept.concept
1039 );
1040 }
1041 }
1042 }
1043 let hermes = report
1046 .harnesses
1047 .iter()
1048 .find(|h| h.id.as_str() == HarnessId::HERMES)
1049 .unwrap();
1050 let conv = &hermes.orchestration.concepts[2];
1051 assert_eq!(conv.concept, "conversation");
1052 assert_eq!(conv.observed, ImplementationKind::BuiltIn);
1053 assert_eq!(conv.controlled, ImplementationKind::BuiltIn);
1058 assert_eq!(
1059 conv.methods,
1060 vec![
1061 "harness.v1.sessions.discover",
1062 "harness.v1.sessions.load",
1063 "harness.v1.sessions.reset",
1064 "harness.v1.sessions.delete",
1065 ]
1066 );
1067 let openclaw_conv = &report
1070 .harnesses
1071 .iter()
1072 .find(|h| h.id.as_str() == HarnessId::OPENCLAW)
1073 .unwrap()
1074 .orchestration
1075 .concepts[2];
1076 assert_eq!(
1077 openclaw_conv.methods,
1078 vec![
1079 "harness.v1.sessions.discover",
1080 "harness.v1.sessions.load",
1081 "harness.v1.sessions.new",
1082 "harness.v1.sessions.reset",
1083 ]
1084 );
1085 let claude_conv = &report
1088 .harnesses
1089 .iter()
1090 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1091 .unwrap()
1092 .orchestration
1093 .concepts[2];
1094 assert_eq!(claude_conv.observed, ImplementationKind::BuiltIn);
1095 assert_eq!(claude_conv.controlled, ImplementationKind::Absent);
1096 assert_eq!(
1097 claude_conv.methods,
1098 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"]
1099 );
1100 for harness in &report.harnesses {
1101 let conversation = &harness.orchestration.concepts[2];
1102 let doors = crate::sessions_control::controlled_methods(harness.id.as_str());
1103 assert_eq!(
1104 conversation.controlled == ImplementationKind::BuiltIn,
1105 !doors.is_empty(),
1106 "{}: conversation controlled must track the sessions_control door table",
1107 harness.id.as_str()
1108 );
1109 for method in &doors {
1110 assert!(
1111 conversation.methods.iter().any(|listed| listed == method),
1112 "{}: `{method}` has a door but is not advertised",
1113 harness.id.as_str()
1114 );
1115 }
1116 for listed in &conversation.methods {
1117 assert!(
1118 listed.ends_with(".discover")
1119 || listed.ends_with(".load")
1120 || doors.contains(&listed.as_str()),
1121 "{}: `{listed}` is advertised with no door behind it",
1122 harness.id.as_str()
1123 );
1124 }
1125 }
1126 let pending = &hermes.orchestration.concepts[3];
1127 assert_eq!(pending.concept, "pending_request");
1128 assert_eq!(pending.controlled, ImplementationKind::BuiltIn);
1129 let job = &hermes.orchestration.concepts[0];
1133 assert_eq!(job.concept, "scheduled_job");
1134 assert_eq!(job.observed, ImplementationKind::BuiltIn);
1135 assert_eq!(job.controlled, ImplementationKind::BuiltIn);
1136 assert_eq!(
1137 job.methods,
1138 vec![
1139 "harness.v1.jobs.list",
1140 "harness.v1.jobs.get",
1141 "harness.v1.jobs.create",
1142 "harness.v1.jobs.update",
1143 "harness.v1.jobs.pause",
1144 "harness.v1.jobs.resume",
1145 "harness.v1.jobs.run",
1146 "harness.v1.jobs.delete",
1147 ]
1148 );
1149 let claude_job = &report
1151 .harnesses
1152 .iter()
1153 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1154 .unwrap()
1155 .orchestration
1156 .concepts[0];
1157 assert_eq!(claude_job.observed, ImplementationKind::BuiltIn);
1158 assert_eq!(claude_job.controlled, ImplementationKind::Absent);
1159 assert_eq!(
1160 claude_job.methods,
1161 vec!["harness.v1.jobs.list", "harness.v1.jobs.get"]
1162 );
1163 for harness in &report.harnesses {
1164 let job = &harness.orchestration.concepts[0];
1165 assert_eq!(
1166 job.observed == ImplementationKind::BuiltIn,
1167 crate::jobs::supports_jobs(harness.id.as_str()),
1168 "{}: scheduled_job observed must track JOB_HARNESSES",
1169 harness.id.as_str()
1170 );
1171 assert_eq!(
1172 job.controlled == ImplementationKind::BuiltIn,
1173 crate::jobs_control::supports_job_control(harness.id.as_str()),
1174 "{}: scheduled_job controlled must track CONTROLLED_JOB_HARNESSES",
1175 harness.id.as_str()
1176 );
1177 }
1178 let profile = &hermes.orchestration.concepts[4];
1182 assert_eq!(profile.concept, "profile");
1183 assert_eq!(profile.observed, ImplementationKind::BuiltIn);
1184 assert_eq!(profile.controlled, ImplementationKind::BuiltIn);
1185 assert_eq!(
1186 profile.methods,
1187 [
1188 "harness.v1.profiles.list",
1189 "harness.v1.profiles.get",
1190 "harness.v1.profiles.create",
1191 "harness.v1.profiles.delete",
1192 ]
1193 );
1194 let codex_profile = &report
1197 .harnesses
1198 .iter()
1199 .find(|h| h.id.as_str() == HarnessId::CODEX)
1200 .unwrap()
1201 .orchestration
1202 .concepts[4];
1203 assert_eq!(codex_profile.observed, ImplementationKind::BuiltIn);
1204 assert_eq!(codex_profile.controlled, ImplementationKind::Absent);
1205 assert_eq!(
1206 codex_profile.methods,
1207 ["harness.v1.profiles.list", "harness.v1.profiles.get"]
1208 );
1209 for harness in &report.harnesses {
1210 let profile = &harness.orchestration.concepts[4];
1211 assert_eq!(
1212 profile.observed == ImplementationKind::BuiltIn,
1213 crate::profiles::PROFILE_HARNESSES.contains(&harness.id.as_str()),
1214 "{}: profile observed tier disagrees with PROFILE_HARNESSES",
1215 harness.id.as_str()
1216 );
1217 assert_eq!(
1218 profile.controlled == ImplementationKind::BuiltIn,
1219 crate::profiles_control::supports_profile_control(harness.id.as_str()),
1220 "{}: profile controlled tier disagrees with CONTROLLED_PROFILE_HARNESSES",
1221 harness.id.as_str()
1222 );
1223 }
1224 }
1225
1226 #[test]
1230 fn memory_is_observed_only_for_the_harnesses_with_a_memory_store() {
1231 let report = harness_support_registry();
1232 for harness in &report.harnesses {
1233 let memory = harness
1234 .orchestration
1235 .concepts
1236 .iter()
1237 .find(|concept| concept.concept == "memory")
1238 .unwrap_or_else(|| panic!("{}: no memory concept row", harness.id.as_str()));
1239 if crate::memory::MEMORY_HARNESSES.contains(&harness.id.as_str()) {
1240 assert_eq!(
1241 memory.observed,
1242 ImplementationKind::BuiltIn,
1243 "{}: memory must be observed",
1244 harness.id.as_str()
1245 );
1246 assert_eq!(
1247 memory.methods,
1248 vec![
1249 "harness.v1.memory.show".to_string(),
1250 "harness.v1.memory.search".to_string()
1251 ]
1252 );
1253 } else {
1254 assert_eq!(
1255 memory.observed,
1256 ImplementationKind::Absent,
1257 "{}: memory must be absent",
1258 harness.id.as_str()
1259 );
1260 assert!(memory.methods.is_empty(), "{}", harness.id.as_str());
1261 }
1262 assert_eq!(memory.controlled, ImplementationKind::Absent);
1264 }
1265 }
1266
1267 #[test]
1271 fn skills_are_observed_and_controlled_for_the_harnesses_with_a_skills_loader() {
1272 let report = harness_support_registry();
1273 for harness in &report.harnesses {
1274 let skills = harness
1275 .orchestration
1276 .concepts
1277 .iter()
1278 .find(|concept| concept.concept == "skills")
1279 .unwrap();
1280 if crate::skills::SKILL_HARNESSES.contains(&harness.id.as_str()) {
1281 assert_eq!(
1282 skills.observed,
1283 ImplementationKind::BuiltIn,
1284 "{}",
1285 harness.id.as_str()
1286 );
1287 assert_eq!(
1291 skills.controlled,
1292 ImplementationKind::BuiltIn,
1293 "{}",
1294 harness.id.as_str()
1295 );
1296 assert_eq!(
1297 skills.methods,
1298 vec![
1299 "harness.v1.skills.list".to_string(),
1300 "harness.v1.skills.install".to_string(),
1301 "harness.v1.skills.remove".to_string(),
1302 ]
1303 );
1304 } else {
1305 assert_eq!(
1306 skills.observed,
1307 ImplementationKind::Absent,
1308 "{}",
1309 harness.id.as_str()
1310 );
1311 assert_eq!(
1312 skills.controlled,
1313 ImplementationKind::Absent,
1314 "{}",
1315 harness.id.as_str()
1316 );
1317 }
1318 }
1319 }
1320
1321 #[test]
1326 fn channels_are_observed_for_the_gateway_harnesses_only() {
1327 let report = harness_support_registry();
1328 for harness in &report.harnesses {
1329 let channel = harness
1330 .orchestration
1331 .concepts
1332 .iter()
1333 .find(|concept| concept.concept == "channel")
1334 .unwrap();
1335 if crate::channels::CHANNEL_HARNESSES.contains(&harness.id.as_str()) {
1336 assert_eq!(
1337 channel.observed,
1338 ImplementationKind::BuiltIn,
1339 "{}",
1340 harness.id.as_str()
1341 );
1342 assert_eq!(
1343 channel.methods,
1344 ["harness.v1.channels.list", "harness.v1.channels.status"]
1345 );
1346 } else {
1347 assert_eq!(
1348 channel.observed,
1349 ImplementationKind::Absent,
1350 "{}",
1351 harness.id.as_str()
1352 );
1353 assert!(channel.methods.is_empty(), "{}", harness.id.as_str());
1354 }
1355 assert_eq!(channel.controlled, ImplementationKind::Absent);
1357 }
1358 }
1359
1360 #[test]
1366 fn runs_are_observed_only_where_the_harness_keeps_a_fire_store() {
1367 let report = harness_support_registry();
1368 let concept = |harness: &HarnessSupportDescriptor, name: &str| {
1369 harness
1370 .orchestration
1371 .concepts
1372 .iter()
1373 .find(|concept| concept.concept == name)
1374 .unwrap_or_else(|| panic!("no `{name}` concept for {}", harness.id.as_str()))
1375 .clone()
1376 };
1377 let mut observed = Vec::new();
1378 for harness in &report.harnesses {
1379 let run = concept(harness, "run");
1380 if crate::runs::RUN_HARNESSES.contains(&harness.id.as_str()) {
1381 assert_eq!(
1382 run.observed,
1383 ImplementationKind::BuiltIn,
1384 "{}",
1385 harness.id.as_str()
1386 );
1387 assert_eq!(
1388 run.methods,
1389 vec![
1390 "harness.v1.runs.list".to_string(),
1391 "harness.v1.runs.get".to_string()
1392 ]
1393 );
1394 observed.push(harness.id.as_str().to_string());
1395 } else {
1396 assert_eq!(
1397 run.observed,
1398 ImplementationKind::Absent,
1399 "{}",
1400 harness.id.as_str()
1401 );
1402 assert!(run.methods.is_empty(), "{}", harness.id.as_str());
1403 }
1404 assert_eq!(run.controlled, ImplementationKind::Absent);
1407 }
1408 assert_eq!(observed, vec!["hermes", "openclaw", "orchestrator"]);
1412
1413 let claude = report
1414 .harnesses
1415 .iter()
1416 .find(|harness| harness.id.as_str() == HarnessId::CLAUDE_CODE)
1417 .expect("claude-code is in the registry");
1418 assert_eq!(
1419 concept(claude, "scheduled_job").observed,
1420 ImplementationKind::BuiltIn,
1421 );
1422 assert_eq!(concept(claude, "run").observed, ImplementationKind::Absent);
1423 }
1424
1425 #[test]
1427 fn orchestration_block_is_additive_on_the_wire() {
1428 let report = harness_support_registry();
1429 let mut value = serde_json::to_value(&report.harnesses[0]).unwrap();
1430 value.as_object_mut().unwrap().remove("orchestration");
1431 let back: HarnessSupportDescriptor = serde_json::from_value(value).unwrap();
1432 assert!(back.orchestration.concepts.is_empty());
1433 }
1434}