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
143pub(crate) fn grok_home_env() -> BTreeMap<String, String> {
148 let mut env = BTreeMap::new();
149 if std::env::var_os("GROK_HOME").is_none() {
150 if let Some(home) = std::env::var_os("HOME").map(std::path::PathBuf::from) {
151 let grok = home.join(".grok");
152 let linked = std::fs::symlink_metadata(&grok)
153 .map(|metadata| metadata.file_type().is_symlink())
154 .unwrap_or(false);
155 if let (true, Ok(resolved)) = (linked, grok.canonicalize()) {
156 env.insert("GROK_HOME".into(), resolved.to_string_lossy().into_owned());
157 }
158 }
159 }
160 env
161}
162
163pub(crate) fn grok_env() -> BTreeMap<String, String> {
165 let mut env = grok_home_env();
166 env.insert("GROK_AGENT_DASHBOARD".into(), "0".into());
167 env
168}
169
170fn grok_arguments() -> Vec<String> {
172 let mut arguments: Vec<String> = Vec::new();
173 if self_sandbox_supported() {
174 arguments.push("--sandbox".into());
175 arguments.push("workspace".into());
176 }
177 arguments.push("agent".into());
178 arguments.push("--no-leader".into());
179 arguments.push("stdio".into());
180 arguments
181}
182
183fn built_in_native() -> NativeSupport {
184 NativeSupport {
185 discover: ImplementationKind::BuiltIn,
186 load: ImplementationKind::BuiltIn,
187 follow: ImplementationKind::BuiltIn,
188 import: ImplementationKind::BuiltIn,
189 export: ImplementationKind::BuiltIn,
190 }
191}
192
193fn built_in_runtime(
194 backend: &dyn RuntimeBackend,
195 protocol: &str,
196 launch: RuntimeLaunch,
197) -> RuntimeSupport {
198 RuntimeSupport {
199 implementation: ImplementationKind::BuiltIn,
200 protocol: protocol.into(),
201 default_launch: Some(launch),
202 connect_launch: None,
203 capabilities: backend.capabilities(),
204 }
205}
206
207pub fn orchestration_support(descriptor: &HarnessSupportDescriptor) -> OrchestrationSupport {
216 let concepts = ORCHESTRATION_CONCEPTS
217 .iter()
218 .map(|concept| {
219 let (observed, controlled, methods): (
220 ImplementationKind,
221 ImplementationKind,
222 Vec<&str>,
223 ) = match *concept {
224 "scheduled_job"
231 if crate::jobs_control::supports_job_control(descriptor.id.as_str()) =>
232 {
233 (
234 ImplementationKind::BuiltIn,
235 ImplementationKind::BuiltIn,
236 vec![
237 "harness.v1.jobs.list",
238 "harness.v1.jobs.get",
239 "harness.v1.jobs.create",
240 "harness.v1.jobs.update",
241 "harness.v1.jobs.pause",
242 "harness.v1.jobs.resume",
243 "harness.v1.jobs.run",
244 "harness.v1.jobs.delete",
245 ],
246 )
247 }
248 "scheduled_job" if crate::jobs::supports_jobs(descriptor.id.as_str()) => (
254 ImplementationKind::BuiltIn,
255 ImplementationKind::Absent,
256 vec!["harness.v1.jobs.list", "harness.v1.jobs.get"],
257 ),
258 "run" if crate::runs::supports_runs(descriptor.id.as_str()) => (
264 ImplementationKind::BuiltIn,
265 ImplementationKind::Absent,
266 vec!["harness.v1.runs.list", "harness.v1.runs.get"],
267 ),
268 "conversation"
279 if descriptor.native.load == ImplementationKind::BuiltIn
280 && !crate::sessions_control::controlled_methods(descriptor.id.as_str())
281 .is_empty() =>
282 {
283 let mut methods =
284 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"];
285 methods.extend(crate::sessions_control::controlled_methods(
286 descriptor.id.as_str(),
287 ));
288 (
289 ImplementationKind::BuiltIn,
290 ImplementationKind::BuiltIn,
291 methods,
292 )
293 }
294 "conversation" if descriptor.id.as_str() == HarnessId::ORCHESTRATOR => {
308 let mut methods = vec!["harness.v1.sessions.discover"];
309 methods.extend(crate::sessions_control::controlled_methods(
310 descriptor.id.as_str(),
311 ));
312 (
313 ImplementationKind::BuiltIn,
314 ImplementationKind::BuiltIn,
315 methods,
316 )
317 }
318 "conversation" if descriptor.native.load == ImplementationKind::BuiltIn => (
319 ImplementationKind::BuiltIn,
320 ImplementationKind::Absent,
321 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"],
322 ),
323 "pending_request" if descriptor.runtime.capabilities.respond_to_requests => (
332 ImplementationKind::BuiltIn,
333 ImplementationKind::BuiltIn,
334 vec![
335 "harness.v1.approvals.list",
336 "harness.v1.approvals.resolve",
337 "harness.v1.runtimes.respond",
338 ],
339 ),
340 "profile"
348 if crate::profiles_control::supports_profile_control(
349 descriptor.id.as_str(),
350 ) =>
351 {
352 (
353 ImplementationKind::BuiltIn,
354 ImplementationKind::BuiltIn,
355 vec![
356 "harness.v1.profiles.list",
357 "harness.v1.profiles.get",
358 "harness.v1.profiles.create",
359 "harness.v1.profiles.delete",
360 ],
361 )
362 }
363 "profile"
372 if crate::profiles::PROFILE_HARNESSES.contains(&descriptor.id.as_str()) =>
373 {
374 (
375 ImplementationKind::BuiltIn,
376 ImplementationKind::Absent,
377 vec!["harness.v1.profiles.list", "harness.v1.profiles.get"],
378 )
379 }
380 "skills"
390 if crate::skills_control::supports_skill_control(descriptor.id.as_str()) =>
391 {
392 (
393 ImplementationKind::BuiltIn,
394 ImplementationKind::BuiltIn,
395 vec![
396 "harness.v1.skills.list",
397 "harness.v1.skills.install",
398 "harness.v1.skills.remove",
399 ],
400 )
401 }
402 "delivery_target" if crate::jobs::supports_jobs(descriptor.id.as_str()) => {
408 let mut methods = vec!["harness.v1.jobs.list", "harness.v1.jobs.get"];
409 if crate::runs::supports_runs(descriptor.id.as_str()) {
410 methods.push("harness.v1.runs.list");
411 methods.push("harness.v1.runs.get");
412 }
413 (
414 ImplementationKind::BuiltIn,
415 ImplementationKind::Absent,
416 methods,
417 )
418 }
419 "gateway_health"
428 if matches!(
429 descriptor.id.as_str(),
430 HarnessId::HERMES | HarnessId::OPENCLAW | HarnessId::ORCHESTRATOR
435 ) =>
436 {
437 (
438 ImplementationKind::BuiltIn,
439 ImplementationKind::Absent,
440 vec!["harness.v1.harnesses.list"],
441 )
442 }
443 "inbound_trigger"
445 if crate::triggers::TRIGGER_HARNESSES.contains(&descriptor.id.as_str()) =>
446 {
447 (
448 ImplementationKind::BuiltIn,
449 ImplementationKind::Absent,
450 vec!["harness.v1.triggers.list"],
451 )
452 }
453 "routing" if crate::routes::ROUTE_HARNESSES.contains(&descriptor.id.as_str()) => (
455 ImplementationKind::BuiltIn,
456 ImplementationKind::Absent,
457 vec!["harness.v1.routes.list"],
458 ),
459 "channel"
460 if crate::channels::CHANNEL_HARNESSES.contains(&descriptor.id.as_str()) =>
461 {
462 (
463 ImplementationKind::BuiltIn,
464 ImplementationKind::Absent,
465 vec!["harness.v1.channels.list", "harness.v1.channels.status"],
466 )
467 }
468 "memory" if crate::memory::supports_memory(descriptor.id.as_str()) => (
475 ImplementationKind::BuiltIn,
476 ImplementationKind::Absent,
477 vec!["harness.v1.memory.show", "harness.v1.memory.search"],
478 ),
479 _ => (
480 ImplementationKind::Absent,
481 ImplementationKind::Absent,
482 vec![],
483 ),
484 };
485 ConceptSupport {
486 concept: (*concept).to_string(),
487 observed,
488 controlled,
489 methods: methods.into_iter().map(str::to_string).collect(),
490 }
491 })
492 .collect();
493 OrchestrationSupport { concepts }
494}
495
496pub fn harness_support_registry() -> SupportRegistryReport {
497 let claude = ClaudeCodeRuntimeBackend::new();
498 let codex = CodexRuntimeBackend::new();
499 let opencode = OpenCodeRuntimeBackend::new();
500 let pi = PiRuntimeBackend::new();
501 let grok_launch = RuntimeLaunch {
502 program: "grok".into(),
503 arguments: grok_arguments(),
504 env: grok_env(),
505 };
506 let grok = AcpRuntimeBackend::new(HarnessId::from(HarnessId::GROK), grok_launch.clone())
507 .with_resume_support(true);
508 let gemini_launch = RuntimeLaunch {
509 program: "gemini".into(),
510 arguments: vec!["--experimental-acp".into()],
514 env: BTreeMap::new(),
515 };
516 let gemini = AcpRuntimeBackend::new(HarnessId::from(HarnessId::GEMINI), gemini_launch.clone())
517 .with_resume_support(true);
518 let goose_launch = RuntimeLaunch {
519 program: "goose".into(),
520 arguments: vec!["acp".into()],
521 env: BTreeMap::new(),
522 };
523 let goose = AcpRuntimeBackend::new(HarnessId::from(HarnessId::GOOSE), goose_launch.clone())
524 .with_resume_support(true);
525 let hermes_launch = RuntimeLaunch {
526 program: "hermes-acp".into(),
527 arguments: Vec::new(),
528 env: BTreeMap::new(),
529 };
530 let hermes = AcpRuntimeBackend::new(HarnessId::from(HarnessId::HERMES), hermes_launch.clone())
531 .with_resume_support(true);
532 let openclaw_launch = RuntimeLaunch {
533 program: "openclaw".into(),
538 arguments: vec!["acp".into()],
539 env: BTreeMap::new(),
540 };
541 let openclaw = AcpRuntimeBackend::new(
542 HarnessId::from(HarnessId::OPENCLAW),
543 openclaw_launch.clone(),
544 )
545 .with_resume_support(true);
546 let supercode_launch = RuntimeLaunch {
547 program: "supercode".into(),
548 arguments: vec!["acp".into()],
549 env: BTreeMap::new(),
550 };
551 let supercode = AcpRuntimeBackend::new(
552 HarnessId::from(HarnessId::SUPERCODE),
553 supercode_launch.clone(),
554 )
555 .with_resume_support(true);
556
557 let mut report = SupportRegistryReport {
558 schema: SUPPORT_REGISTRY_SCHEMA.into(),
559 harnesses: vec![
560 HarnessSupportDescriptor {
561 id: HarnessId::from(HarnessId::CLAUDE_CODE),
562 display_name: "Claude Code".into(),
563 orchestration: OrchestrationSupport::default(),
564 native: built_in_native(),
565 runtime: built_in_runtime(&claude, "claude-stream-json", claude.launch().clone()),
569 },
570 HarnessSupportDescriptor {
571 id: HarnessId::from(HarnessId::CODEX),
572 display_name: "Codex".into(),
573 orchestration: OrchestrationSupport::default(),
574 native: built_in_native(),
575 runtime: built_in_runtime(
576 &codex,
577 "codex-app-server-jsonl",
578 RuntimeLaunch {
579 program: "codex".into(),
580 arguments: vec!["app-server".into()],
581 env: BTreeMap::new(),
582 },
583 ),
584 },
585 HarnessSupportDescriptor {
586 id: HarnessId::from(HarnessId::OPENCODE),
587 display_name: "OpenCode".into(),
588 orchestration: OrchestrationSupport::default(),
589 native: built_in_native(),
590 runtime: built_in_runtime(
591 &opencode,
592 "opencode-http-sse",
593 RuntimeLaunch {
594 program: "opencode".into(),
595 arguments: vec!["serve".into()],
596 env: BTreeMap::new(),
597 },
598 ),
599 },
600 HarnessSupportDescriptor {
601 id: HarnessId::from(HarnessId::PI),
602 display_name: "Pi".into(),
603 orchestration: OrchestrationSupport::default(),
604 native: built_in_native(),
605 runtime: built_in_runtime(
606 &pi,
607 "pi-rpc-jsonl",
608 RuntimeLaunch {
609 program: "pi".into(),
610 arguments: vec!["--mode".into(), "rpc".into()],
611 env: BTreeMap::new(),
612 },
613 ),
614 },
615 HarnessSupportDescriptor {
616 id: HarnessId::from(HarnessId::GROK),
617 display_name: "Grok".into(),
618 orchestration: OrchestrationSupport::default(),
619 native: built_in_native(),
620 runtime: RuntimeSupport {
621 implementation: ImplementationKind::GenericProtocol,
622 protocol: "acp-v1-jsonrpc".into(),
623 default_launch: Some(grok_launch),
624 connect_launch: None,
625 capabilities: grok.capabilities(),
626 },
627 },
628 HarnessSupportDescriptor {
629 id: HarnessId::from(HarnessId::GEMINI),
630 display_name: "Gemini CLI".into(),
631 orchestration: OrchestrationSupport::default(),
632 native: built_in_native(),
633 runtime: RuntimeSupport {
634 implementation: ImplementationKind::GenericProtocol,
635 protocol: "acp-v1-jsonrpc".into(),
636 default_launch: Some(gemini_launch),
637 connect_launch: None,
638 capabilities: gemini.capabilities(),
639 },
640 },
641 HarnessSupportDescriptor {
642 id: HarnessId::from(HarnessId::GOOSE),
643 display_name: "Goose".into(),
644 orchestration: OrchestrationSupport::default(),
645 native: built_in_native(),
646 runtime: RuntimeSupport {
647 implementation: ImplementationKind::GenericProtocol,
648 protocol: "acp-v1-jsonrpc".into(),
649 default_launch: Some(goose_launch),
650 connect_launch: None,
651 capabilities: goose.capabilities(),
652 },
653 },
654 HarnessSupportDescriptor {
655 id: HarnessId::from(HarnessId::HERMES),
656 display_name: "Hermes Agent".into(),
657 orchestration: OrchestrationSupport::default(),
658 native: NativeSupport {
659 discover: ImplementationKind::BuiltIn,
667 load: ImplementationKind::BuiltIn,
668 follow: ImplementationKind::Absent,
669 import: ImplementationKind::Absent,
670 export: ImplementationKind::GenericProtocol,
671 },
672 runtime: RuntimeSupport {
673 implementation: ImplementationKind::GenericProtocol,
674 protocol: "acp-v1-jsonrpc".into(),
675 default_launch: Some(hermes_launch),
676 connect_launch: None,
677 capabilities: hermes.capabilities(),
678 },
679 },
680 HarnessSupportDescriptor {
681 id: HarnessId::from(HarnessId::OPENCLAW),
682 display_name: "OpenClaw".into(),
683 orchestration: OrchestrationSupport::default(),
684 native: NativeSupport {
685 discover: ImplementationKind::BuiltIn,
692 load: ImplementationKind::BuiltIn,
693 follow: ImplementationKind::Absent,
694 import: ImplementationKind::Absent,
695 export: ImplementationKind::Absent,
696 },
697 runtime: RuntimeSupport {
698 implementation: ImplementationKind::GenericProtocol,
699 protocol: "acp-v1-jsonrpc".into(),
700 default_launch: Some(openclaw_launch),
701 connect_launch: Some(RuntimeConnectLaunch {
708 config_path: "~/.openclaw/openclaw.json".into(),
709 address_pointer: "/gateway/remote/url".into(),
710 port_pointer: Some("/gateway/port".into()),
711 default_address: Some("ws://127.0.0.1:18789".into()),
712 auth_pointer: Some("/gateway/auth/token".into()),
713 protocol: "acp-v1-jsonrpc".into(),
714 }),
715 capabilities: openclaw.capabilities(),
716 },
717 },
718 HarnessSupportDescriptor {
728 id: HarnessId::from(HarnessId::ORCHESTRATOR),
729 display_name: "Orchestrator".into(),
730 orchestration: OrchestrationSupport::default(),
731 native: NativeSupport {
732 discover: ImplementationKind::Absent,
733 load: ImplementationKind::Absent,
734 follow: ImplementationKind::Absent,
735 import: ImplementationKind::Absent,
736 export: ImplementationKind::Absent,
737 },
738 runtime: RuntimeSupport {
739 implementation: ImplementationKind::Absent,
740 protocol: "none".into(),
741 default_launch: None,
742 connect_launch: None,
743 capabilities: RuntimeCapabilities {
744 start_session: false,
745 resume_session: false,
746 attach_existing_process: false,
747 send_input: false,
748 stream_events: false,
749 interrupt: false,
750 steer: false,
751 respond_to_requests: false,
752 },
753 },
754 },
755 HarnessSupportDescriptor {
756 id: HarnessId::from(HarnessId::SUPERCODE),
757 display_name: "Supercode".into(),
758 orchestration: OrchestrationSupport::default(),
759 native: built_in_native(),
760 runtime: RuntimeSupport {
761 implementation: ImplementationKind::GenericProtocol,
762 protocol: "acp-v1-jsonrpc".into(),
763 default_launch: Some(supercode_launch),
764 connect_launch: None,
765 capabilities: supercode.capabilities(),
766 },
767 },
768 ],
769 };
770 for descriptor in &mut report.harnesses {
771 descriptor.orchestration = orchestration_support(descriptor);
772 }
773 report
774}
775
776pub fn harness_support(id: &str) -> Option<HarnessSupportDescriptor> {
778 harness_support_registry()
779 .harnesses
780 .into_iter()
781 .find(|harness| harness.id.as_str() == id)
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787
788 #[test]
789 fn claude_code_default_launch_is_the_stream_json_prefix() {
790 let claude = harness_support_registry()
791 .harnesses
792 .into_iter()
793 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
794 .unwrap();
795 let launch = claude.runtime.default_launch.unwrap();
796 assert_eq!(launch.program, "claude");
797 let joined = launch.arguments.join(" ");
798 assert!(joined.contains("--input-format stream-json"), "{joined}");
799 assert!(joined.contains("--output-format stream-json"), "{joined}");
800 assert!(
801 joined.contains("--permission-prompt-tool stdio"),
802 "{joined}"
803 );
804 }
805
806 #[test]
807 fn grok_asks_for_a_self_sandbox_only_where_one_is_possible() {
808 let arguments = grok_arguments();
809 assert_eq!(
810 arguments.iter().any(|argument| argument == "--sandbox"),
811 self_sandbox_supported(),
812 );
813 assert!(
814 arguments.ends_with(&["agent".into(), "--no-leader".into(), "stdio".into()]),
815 "{arguments:?}",
816 );
817 }
818
819 #[test]
820 fn registry_is_unique_and_reports_all_native_support() {
821 let report = harness_support_registry();
822 assert_eq!(report.schema, SUPPORT_REGISTRY_SCHEMA);
823 assert_eq!(report.harnesses.len(), 11);
826 let ids = report
827 .harnesses
828 .iter()
829 .map(|harness| harness.id.as_str())
830 .collect::<std::collections::BTreeSet<_>>();
831 assert_eq!(ids.len(), report.harnesses.len());
832
833 let grok = report
834 .harnesses
835 .iter()
836 .find(|harness| harness.id.as_str() == HarnessId::GROK)
837 .unwrap();
838 assert_eq!(grok.native.discover, ImplementationKind::BuiltIn);
839 assert_eq!(grok.native.load, ImplementationKind::BuiltIn);
840 assert_eq!(grok.native.follow, ImplementationKind::BuiltIn);
841 assert_eq!(grok.native.import, ImplementationKind::BuiltIn);
842 for id in [HarnessId::GEMINI, HarnessId::SUPERCODE] {
843 let harness = report
844 .harnesses
845 .iter()
846 .find(|harness| harness.id.as_str() == id)
847 .unwrap();
848 assert_eq!(harness.native.discover, ImplementationKind::BuiltIn);
849 assert_eq!(harness.native.load, ImplementationKind::BuiltIn);
850 assert_eq!(harness.native.follow, ImplementationKind::BuiltIn);
851 }
852 assert_eq!(grok.native.export, ImplementationKind::BuiltIn);
853 assert_eq!(
854 grok.runtime.implementation,
855 ImplementationKind::GenericProtocol
856 );
857 let expected: Vec<&str> = if self_sandbox_supported() {
858 vec!["--sandbox", "workspace", "agent", "--no-leader", "stdio"]
859 } else {
860 vec!["agent", "--no-leader", "stdio"]
861 };
862 assert_eq!(
863 grok.runtime.default_launch.as_ref().unwrap().arguments,
864 expected
865 );
866 assert!(!grok
867 .runtime
868 .default_launch
869 .as_ref()
870 .unwrap()
871 .arguments
872 .iter()
873 .any(|argument| argument == "--always-approve"));
874 assert!(grok.runtime.capabilities.resume_session);
875 }
876
877 #[test]
883 fn openclaw_registers_gateway_mediated_with_no_store_access() {
884 let report = harness_support_registry();
885 let openclaw = report
886 .harnesses
887 .iter()
888 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
889 .expect("openclaw must be registered");
890 assert_eq!(openclaw.display_name, "OpenClaw");
891 assert_eq!(openclaw.native.discover, ImplementationKind::BuiltIn);
896 assert_eq!(openclaw.native.load, ImplementationKind::BuiltIn);
897 for kind in [
898 openclaw.native.follow,
899 openclaw.native.import,
900 openclaw.native.export,
901 ] {
902 assert_eq!(kind, ImplementationKind::Absent);
903 }
904 assert_eq!(
905 openclaw.runtime.implementation,
906 ImplementationKind::GenericProtocol
907 );
908 assert_eq!(openclaw.runtime.protocol, "acp-v1-jsonrpc");
909 let launch = openclaw.runtime.default_launch.as_ref().unwrap();
910 assert_eq!(launch.program, "openclaw");
911 assert_eq!(launch.arguments, ["acp"]);
912 let connect = openclaw.runtime.connect_launch.as_ref().unwrap();
913 assert_eq!(connect.config_path, "~/.openclaw/openclaw.json");
914 assert_eq!(connect.address_pointer, "/gateway/remote/url");
918 assert_eq!(connect.port_pointer.as_deref(), Some("/gateway/port"));
919 assert_eq!(
920 connect.default_address.as_deref(),
921 Some("ws://127.0.0.1:18789")
922 );
923 assert_eq!(connect.auth_pointer.as_deref(), Some("/gateway/auth/token"));
924 assert!(openclaw.runtime.capabilities.resume_session);
928 }
929
930 #[test]
931 fn hermes_registers_as_a_driven_tier_acp_entry_without_native_claims() {
932 let report = harness_support_registry();
933 let hermes = report
934 .harnesses
935 .iter()
936 .find(|harness| harness.id.as_str() == HarnessId::HERMES)
937 .expect("hermes must be registered");
938 assert_eq!(hermes.display_name, "Hermes Agent");
939 assert_eq!(hermes.native.discover, ImplementationKind::BuiltIn);
942 assert_eq!(hermes.native.load, ImplementationKind::BuiltIn);
943 for kind in [hermes.native.follow, hermes.native.import] {
944 assert_eq!(kind, ImplementationKind::Absent);
945 }
946 assert_eq!(hermes.native.export, ImplementationKind::GenericProtocol);
947 assert_eq!(
948 hermes.runtime.implementation,
949 ImplementationKind::GenericProtocol
950 );
951 assert_eq!(hermes.runtime.protocol, "acp-v1-jsonrpc");
952 let launch = hermes.runtime.default_launch.as_ref().unwrap();
953 assert_eq!(launch.program, "hermes-acp");
954 assert!(launch.arguments.is_empty());
955 assert!(hermes.runtime.connect_launch.is_none());
956 assert!(hermes.runtime.capabilities.resume_session);
960 assert!(!hermes.runtime.capabilities.attach_existing_process);
961 }
962
963 #[test]
964 fn connect_mode_descriptor_round_trips_the_registry_schema() {
965 let descriptor = HarnessSupportDescriptor {
966 id: HarnessId::from("openclaw"),
967 display_name: "OpenClaw".into(),
968 orchestration: OrchestrationSupport::default(),
969 native: NativeSupport {
970 discover: ImplementationKind::Absent,
971 load: ImplementationKind::Absent,
972 follow: ImplementationKind::Absent,
973 import: ImplementationKind::Absent,
974 export: ImplementationKind::Absent,
975 },
976 runtime: RuntimeSupport {
977 implementation: ImplementationKind::GenericProtocol,
978 protocol: "acp-v1-jsonrpc".into(),
979 default_launch: None,
980 connect_launch: Some(RuntimeConnectLaunch {
981 config_path: "~/.openclaw/openclaw.json".into(),
982 address_pointer: "/gateway/url".into(),
983 port_pointer: None,
984 default_address: None,
985 auth_pointer: Some("/gateway/token".into()),
986 protocol: "acp-v1-jsonrpc".into(),
987 }),
988 capabilities: RuntimeCapabilities {
989 start_session: true,
990 resume_session: false,
991 attach_existing_process: true,
992 send_input: true,
993 stream_events: true,
994 interrupt: false,
995 steer: false,
996 respond_to_requests: false,
997 },
998 },
999 };
1000 let encoded = serde_json::to_value(&descriptor).unwrap();
1001 assert_eq!(
1002 encoded["runtime"]["connect_launch"]["address_pointer"],
1003 "/gateway/url"
1004 );
1005 let decoded: HarnessSupportDescriptor = serde_json::from_value(encoded).unwrap();
1006 assert_eq!(decoded, descriptor);
1007 }
1008
1009 #[test]
1010 fn spawn_only_registry_entries_do_not_serialize_a_connect_launch() {
1011 let report = harness_support_registry();
1012 for harness in &report.harnesses {
1013 let encoded = serde_json::to_string(harness).unwrap();
1014 if harness.id.as_str() == HarnessId::OPENCLAW {
1015 assert!(encoded.contains("connect_launch"));
1019 assert!(encoded.contains("/gateway/auth/token"));
1020 assert!(encoded.contains("ws://127.0.0.1:18789"));
1021 assert!(!encoded.contains("token\":\"ws"));
1022 } else {
1023 assert!(
1024 !encoded.contains("connect_launch"),
1025 "{} must stay spawn-only",
1026 harness.id.as_str()
1027 );
1028 }
1029 }
1030 let encoded = serde_json::to_string(&report).unwrap();
1031 let decoded: SupportRegistryReport = serde_json::from_str(&encoded).unwrap();
1032 assert_eq!(decoded, report);
1033 }
1034
1035 #[test]
1038 fn orchestration_block_is_complete_and_its_methods_exist() {
1039 let report = harness_support_registry();
1040 for harness in &report.harnesses {
1041 let names: Vec<&str> = harness
1042 .orchestration
1043 .concepts
1044 .iter()
1045 .map(|c| c.concept.as_str())
1046 .collect();
1047 assert_eq!(names, ORCHESTRATION_CONCEPTS, "{}", harness.id.as_str());
1048 for concept in &harness.orchestration.concepts {
1049 let any_built_in = concept.observed == ImplementationKind::BuiltIn
1050 || concept.controlled == ImplementationKind::BuiltIn;
1051 assert_eq!(
1052 any_built_in,
1053 !concept.methods.is_empty(),
1054 "{}/{}: a BuiltIn tier must cite methods and an Absent one must not",
1055 harness.id.as_str(),
1056 concept.concept
1057 );
1058 for method in &concept.methods {
1059 assert!(
1060 crate::harness_service::HARNESS_SERVICE_METHODS.contains(&method.as_str()),
1061 "{}/{}: `{method}` is not a harness service method",
1062 harness.id.as_str(),
1063 concept.concept
1064 );
1065 }
1066 }
1067 }
1068 let hermes = report
1071 .harnesses
1072 .iter()
1073 .find(|h| h.id.as_str() == HarnessId::HERMES)
1074 .unwrap();
1075 let conv = &hermes.orchestration.concepts[2];
1076 assert_eq!(conv.concept, "conversation");
1077 assert_eq!(conv.observed, ImplementationKind::BuiltIn);
1078 assert_eq!(conv.controlled, ImplementationKind::BuiltIn);
1083 assert_eq!(
1084 conv.methods,
1085 vec![
1086 "harness.v1.sessions.discover",
1087 "harness.v1.sessions.load",
1088 "harness.v1.sessions.reset",
1089 "harness.v1.sessions.delete",
1090 ]
1091 );
1092 let openclaw_conv = &report
1095 .harnesses
1096 .iter()
1097 .find(|h| h.id.as_str() == HarnessId::OPENCLAW)
1098 .unwrap()
1099 .orchestration
1100 .concepts[2];
1101 assert_eq!(
1102 openclaw_conv.methods,
1103 vec![
1104 "harness.v1.sessions.discover",
1105 "harness.v1.sessions.load",
1106 "harness.v1.sessions.new",
1107 "harness.v1.sessions.reset",
1108 ]
1109 );
1110 let claude_conv = &report
1113 .harnesses
1114 .iter()
1115 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1116 .unwrap()
1117 .orchestration
1118 .concepts[2];
1119 assert_eq!(claude_conv.observed, ImplementationKind::BuiltIn);
1120 assert_eq!(claude_conv.controlled, ImplementationKind::Absent);
1121 assert_eq!(
1122 claude_conv.methods,
1123 vec!["harness.v1.sessions.discover", "harness.v1.sessions.load"]
1124 );
1125 for harness in &report.harnesses {
1126 let conversation = &harness.orchestration.concepts[2];
1127 let doors = crate::sessions_control::controlled_methods(harness.id.as_str());
1128 assert_eq!(
1129 conversation.controlled == ImplementationKind::BuiltIn,
1130 !doors.is_empty(),
1131 "{}: conversation controlled must track the sessions_control door table",
1132 harness.id.as_str()
1133 );
1134 for method in &doors {
1135 assert!(
1136 conversation.methods.iter().any(|listed| listed == method),
1137 "{}: `{method}` has a door but is not advertised",
1138 harness.id.as_str()
1139 );
1140 }
1141 for listed in &conversation.methods {
1142 assert!(
1143 listed.ends_with(".discover")
1144 || listed.ends_with(".load")
1145 || doors.contains(&listed.as_str()),
1146 "{}: `{listed}` is advertised with no door behind it",
1147 harness.id.as_str()
1148 );
1149 }
1150 }
1151 let pending = &hermes.orchestration.concepts[3];
1152 assert_eq!(pending.concept, "pending_request");
1153 assert_eq!(pending.controlled, ImplementationKind::BuiltIn);
1154 let job = &hermes.orchestration.concepts[0];
1158 assert_eq!(job.concept, "scheduled_job");
1159 assert_eq!(job.observed, ImplementationKind::BuiltIn);
1160 assert_eq!(job.controlled, ImplementationKind::BuiltIn);
1161 assert_eq!(
1162 job.methods,
1163 vec![
1164 "harness.v1.jobs.list",
1165 "harness.v1.jobs.get",
1166 "harness.v1.jobs.create",
1167 "harness.v1.jobs.update",
1168 "harness.v1.jobs.pause",
1169 "harness.v1.jobs.resume",
1170 "harness.v1.jobs.run",
1171 "harness.v1.jobs.delete",
1172 ]
1173 );
1174 let claude_job = &report
1176 .harnesses
1177 .iter()
1178 .find(|h| h.id.as_str() == HarnessId::CLAUDE_CODE)
1179 .unwrap()
1180 .orchestration
1181 .concepts[0];
1182 assert_eq!(claude_job.observed, ImplementationKind::BuiltIn);
1183 assert_eq!(claude_job.controlled, ImplementationKind::Absent);
1184 assert_eq!(
1185 claude_job.methods,
1186 vec!["harness.v1.jobs.list", "harness.v1.jobs.get"]
1187 );
1188 for harness in &report.harnesses {
1189 let job = &harness.orchestration.concepts[0];
1190 assert_eq!(
1191 job.observed == ImplementationKind::BuiltIn,
1192 crate::jobs::supports_jobs(harness.id.as_str()),
1193 "{}: scheduled_job observed must track JOB_HARNESSES",
1194 harness.id.as_str()
1195 );
1196 assert_eq!(
1197 job.controlled == ImplementationKind::BuiltIn,
1198 crate::jobs_control::supports_job_control(harness.id.as_str()),
1199 "{}: scheduled_job controlled must track CONTROLLED_JOB_HARNESSES",
1200 harness.id.as_str()
1201 );
1202 }
1203 let profile = &hermes.orchestration.concepts[4];
1207 assert_eq!(profile.concept, "profile");
1208 assert_eq!(profile.observed, ImplementationKind::BuiltIn);
1209 assert_eq!(profile.controlled, ImplementationKind::BuiltIn);
1210 assert_eq!(
1211 profile.methods,
1212 [
1213 "harness.v1.profiles.list",
1214 "harness.v1.profiles.get",
1215 "harness.v1.profiles.create",
1216 "harness.v1.profiles.delete",
1217 ]
1218 );
1219 let codex_profile = &report
1222 .harnesses
1223 .iter()
1224 .find(|h| h.id.as_str() == HarnessId::CODEX)
1225 .unwrap()
1226 .orchestration
1227 .concepts[4];
1228 assert_eq!(codex_profile.observed, ImplementationKind::BuiltIn);
1229 assert_eq!(codex_profile.controlled, ImplementationKind::Absent);
1230 assert_eq!(
1231 codex_profile.methods,
1232 ["harness.v1.profiles.list", "harness.v1.profiles.get"]
1233 );
1234 for harness in &report.harnesses {
1235 let profile = &harness.orchestration.concepts[4];
1236 assert_eq!(
1237 profile.observed == ImplementationKind::BuiltIn,
1238 crate::profiles::PROFILE_HARNESSES.contains(&harness.id.as_str()),
1239 "{}: profile observed tier disagrees with PROFILE_HARNESSES",
1240 harness.id.as_str()
1241 );
1242 assert_eq!(
1243 profile.controlled == ImplementationKind::BuiltIn,
1244 crate::profiles_control::supports_profile_control(harness.id.as_str()),
1245 "{}: profile controlled tier disagrees with CONTROLLED_PROFILE_HARNESSES",
1246 harness.id.as_str()
1247 );
1248 }
1249 }
1250
1251 #[test]
1255 fn memory_is_observed_only_for_the_harnesses_with_a_memory_store() {
1256 let report = harness_support_registry();
1257 for harness in &report.harnesses {
1258 let memory = harness
1259 .orchestration
1260 .concepts
1261 .iter()
1262 .find(|concept| concept.concept == "memory")
1263 .unwrap_or_else(|| panic!("{}: no memory concept row", harness.id.as_str()));
1264 if crate::memory::MEMORY_HARNESSES.contains(&harness.id.as_str()) {
1265 assert_eq!(
1266 memory.observed,
1267 ImplementationKind::BuiltIn,
1268 "{}: memory must be observed",
1269 harness.id.as_str()
1270 );
1271 assert_eq!(
1272 memory.methods,
1273 vec![
1274 "harness.v1.memory.show".to_string(),
1275 "harness.v1.memory.search".to_string()
1276 ]
1277 );
1278 } else {
1279 assert_eq!(
1280 memory.observed,
1281 ImplementationKind::Absent,
1282 "{}: memory must be absent",
1283 harness.id.as_str()
1284 );
1285 assert!(memory.methods.is_empty(), "{}", harness.id.as_str());
1286 }
1287 assert_eq!(memory.controlled, ImplementationKind::Absent);
1289 }
1290 }
1291
1292 #[test]
1296 fn skills_are_observed_and_controlled_for_the_harnesses_with_a_skills_loader() {
1297 let report = harness_support_registry();
1298 for harness in &report.harnesses {
1299 let skills = harness
1300 .orchestration
1301 .concepts
1302 .iter()
1303 .find(|concept| concept.concept == "skills")
1304 .unwrap();
1305 if crate::skills::SKILL_HARNESSES.contains(&harness.id.as_str()) {
1306 assert_eq!(
1307 skills.observed,
1308 ImplementationKind::BuiltIn,
1309 "{}",
1310 harness.id.as_str()
1311 );
1312 assert_eq!(
1316 skills.controlled,
1317 ImplementationKind::BuiltIn,
1318 "{}",
1319 harness.id.as_str()
1320 );
1321 assert_eq!(
1322 skills.methods,
1323 vec![
1324 "harness.v1.skills.list".to_string(),
1325 "harness.v1.skills.install".to_string(),
1326 "harness.v1.skills.remove".to_string(),
1327 ]
1328 );
1329 } else {
1330 assert_eq!(
1331 skills.observed,
1332 ImplementationKind::Absent,
1333 "{}",
1334 harness.id.as_str()
1335 );
1336 assert_eq!(
1337 skills.controlled,
1338 ImplementationKind::Absent,
1339 "{}",
1340 harness.id.as_str()
1341 );
1342 }
1343 }
1344 }
1345
1346 #[test]
1351 fn channels_are_observed_for_the_gateway_harnesses_only() {
1352 let report = harness_support_registry();
1353 for harness in &report.harnesses {
1354 let channel = harness
1355 .orchestration
1356 .concepts
1357 .iter()
1358 .find(|concept| concept.concept == "channel")
1359 .unwrap();
1360 if crate::channels::CHANNEL_HARNESSES.contains(&harness.id.as_str()) {
1361 assert_eq!(
1362 channel.observed,
1363 ImplementationKind::BuiltIn,
1364 "{}",
1365 harness.id.as_str()
1366 );
1367 assert_eq!(
1368 channel.methods,
1369 ["harness.v1.channels.list", "harness.v1.channels.status"]
1370 );
1371 } else {
1372 assert_eq!(
1373 channel.observed,
1374 ImplementationKind::Absent,
1375 "{}",
1376 harness.id.as_str()
1377 );
1378 assert!(channel.methods.is_empty(), "{}", harness.id.as_str());
1379 }
1380 assert_eq!(channel.controlled, ImplementationKind::Absent);
1382 }
1383 }
1384
1385 #[test]
1391 fn runs_are_observed_only_where_the_harness_keeps_a_fire_store() {
1392 let report = harness_support_registry();
1393 let concept = |harness: &HarnessSupportDescriptor, name: &str| {
1394 harness
1395 .orchestration
1396 .concepts
1397 .iter()
1398 .find(|concept| concept.concept == name)
1399 .unwrap_or_else(|| panic!("no `{name}` concept for {}", harness.id.as_str()))
1400 .clone()
1401 };
1402 let mut observed = Vec::new();
1403 for harness in &report.harnesses {
1404 let run = concept(harness, "run");
1405 if crate::runs::RUN_HARNESSES.contains(&harness.id.as_str()) {
1406 assert_eq!(
1407 run.observed,
1408 ImplementationKind::BuiltIn,
1409 "{}",
1410 harness.id.as_str()
1411 );
1412 assert_eq!(
1413 run.methods,
1414 vec![
1415 "harness.v1.runs.list".to_string(),
1416 "harness.v1.runs.get".to_string()
1417 ]
1418 );
1419 observed.push(harness.id.as_str().to_string());
1420 } else {
1421 assert_eq!(
1422 run.observed,
1423 ImplementationKind::Absent,
1424 "{}",
1425 harness.id.as_str()
1426 );
1427 assert!(run.methods.is_empty(), "{}", harness.id.as_str());
1428 }
1429 assert_eq!(run.controlled, ImplementationKind::Absent);
1432 }
1433 assert_eq!(observed, vec!["hermes", "openclaw", "orchestrator"]);
1437
1438 let claude = report
1439 .harnesses
1440 .iter()
1441 .find(|harness| harness.id.as_str() == HarnessId::CLAUDE_CODE)
1442 .expect("claude-code is in the registry");
1443 assert_eq!(
1444 concept(claude, "scheduled_job").observed,
1445 ImplementationKind::BuiltIn,
1446 );
1447 assert_eq!(concept(claude, "run").observed, ImplementationKind::Absent);
1448 }
1449
1450 #[test]
1452 fn orchestration_block_is_additive_on_the_wire() {
1453 let report = harness_support_registry();
1454 let mut value = serde_json::to_value(&report.harnesses[0]).unwrap();
1455 value.as_object_mut().unwrap().remove("orchestration");
1456 let back: HarnessSupportDescriptor = serde_json::from_value(value).unwrap();
1457 assert!(back.orchestration.concepts.is_empty());
1458 }
1459}