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