supercode_harness/support.rs
1//! Canonical implementation inventory for external coding harnesses.
2//!
3//! This registry describes wiring that exists in the compiled core. It does
4//! not claim that a harness has passed a real executable smoke test; the
5//! support audit joins this inventory with behavioral probe receipts and
6//! tracker state before it calls anything verified.
7
8use 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
18/// Schema emitted by [`harness_support_registry`].
19pub const SUPPORT_REGISTRY_SCHEMA: &str = "supercode.support-registry.v1";
20
21/// How a primitive is wired into the compiled core.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ImplementationKind {
25 /// A harness-specific implementation is registered.
26 BuiltIn,
27 /// A protocol-generic implementation is usable with a known launch.
28 GenericProtocol,
29 /// No implementation is present.
30 Absent,
31}
32
33/// Persisted-session and translation implementation facts.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct NativeSupport {
36 /// Whether the catalog can discover this harness's sessions.
37 pub discover: ImplementationKind,
38 /// Whether the core can load this harness's native persisted format.
39 pub load: ImplementationKind,
40 /// Whether the generic follower can open this harness's native storage.
41 pub follow: ImplementationKind,
42 /// Whether the canonical session can import this native format.
43 pub import: ImplementationKind,
44 /// Whether the canonical session can export this native format.
45 pub export: ImplementationKind,
46}
47
48/// Live runtime wiring known without launching the real executable.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct RuntimeSupport {
51 /// Harness-specific or protocol-generic adapter registration.
52 pub implementation: ImplementationKind,
53 /// Protocol spoken by the adapter.
54 pub protocol: String,
55 /// Command used when callers do not provide an override.
56 pub default_launch: Option<RuntimeLaunch>,
57 /// Connect-mode launch for gateway harnesses: where a running endpoint's
58 /// address and credential live in the harness's own config file. `None`
59 /// for spawn-only harnesses; declaring one is an explicit registry
60 /// decision, never inferred.
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub connect_launch: Option<RuntimeConnectLaunch>,
63 /// Static adapter capabilities. Optional protocol features are only true
64 /// for known agents that advertise them; the adapter validates them again
65 /// during the live handshake.
66 pub capabilities: RuntimeCapabilities,
67}
68
69/// One compiled harness implementation descriptor.
70
71/// The twelve Domain 11 concepts, in plan order
72/// (`docs/plans/orchestration-domain-11-2026-09-02.md`).
73pub 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/// One orchestration concept's tiers for one harness (ORCH-4).
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct ConceptSupport {
91 /// One of [`ORCHESTRATION_CONCEPTS`].
92 pub concept: String,
93 /// Read tier: supercode lists/inspects the concept from the harness's own files or CLI.
94 pub observed: ImplementationKind,
95 /// Write tier: supercode mutates the concept through the harness's own verb.
96 pub controlled: ImplementationKind,
97 /// `harness.v1.<noun>.<verb>` methods backing the non-`Absent` tiers.
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub methods: Vec<String>,
100}
101
102/// Per-concept observed / controlled tiers for one harness (additive to the
103/// v1 registry schema, like `connect_launch`).
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
105pub struct OrchestrationSupport {
106 /// Exactly [`ORCHESTRATION_CONCEPTS`], in order.
107 pub concepts: Vec<ConceptSupport>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct HarnessSupportDescriptor {
112 /// Stable harness identifier.
113 pub id: HarnessId,
114 /// Human-readable name.
115 pub display_name: String,
116 /// Native persistence/translation implementation.
117 pub native: NativeSupport,
118 /// Live runtime implementation.
119 pub runtime: RuntimeSupport,
120 /// ORCH-4: orchestration concept tiers (defaults to all-`Absent`).
121 #[serde(default)]
122 pub orchestration: OrchestrationSupport,
123}
124
125/// Machine-readable compiled support inventory.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct SupportRegistryReport {
128 /// Report schema.
129 pub schema: String,
130 /// Harness descriptors, in stable product order.
131 pub harnesses: Vec<HarnessSupportDescriptor>,
132}
133
134/// Whether a harness can enter its own sandbox by replacing its process image.
135///
136/// WebAssembly has no process replacement, so a harness asked to sandbox itself
137/// there aborts before its handshake. Everything the loader starts on that
138/// target is already confined to the browser it runs in.
139pub(crate) fn self_sandbox_supported() -> bool {
140 !cfg!(target_family = "wasm")
141}
142
143/// Grok refuses its `workspace` sandbox when its home is a symlink ("symlinked GROK_HOME is
144/// not allowed under sandbox write-deny"), so a symlinked `~/.grok` is named by the path it
145/// resolves to: the same directory, which the sandbox can then protect. An explicit
146/// `GROK_HOME` is left to the caller.
147pub(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
166/// A headless Grok runtime's environment: no agent dashboard, and [`grok_home_env`].
167pub(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
173/// Builds Grok's stdio launch, asking it to sandbox itself where it can.
174fn 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
210/// Return the single compiled inventory used by product surfaces and audits.
211
212/// Derive a harness's orchestration tiers from what the registry already
213/// proves: a harness whose sessions load natively is `observed` for the
214/// conversation concept (`sessions.discover`/`load`), and a runtime door that
215/// answers protocol requests is `controlled` for pending requests
216/// (`runtimes.respond`). Every other cell is `Absent` until its ORCH item
217/// lands and adds its method here.
218pub 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 // ORCH-18: the two harnesses that publish a client-callable
228 // cron verb are also CONTROLLED — supercode runs `hermes cron
229 // …` / `openclaw cron …` on the caller's behalf and re-reads
230 // the row (`crate::jobs_control`). supercode still schedules
231 // nothing itself; the tier means "the harness's own verb is
232 // reachable through one uniform door".
233 "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 // ORCH-7: the three harnesses that HAVE scheduled jobs are read
252 // uniformly from their own stores. Claude Code stays read-only
253 // on purpose: its jobs are session-scoped runtime state created
254 // by the model inside a session (`CronCreate`), so there is no
255 // harness verb for a client to call.
256 "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 // ORCH-8: a harness is `observed` for runs when it KEEPS a
262 // fire store the loader in `crate::runs` opens. Claude Code
263 // has scheduled jobs but no run store — its fires are turns —
264 // so it is deliberately absent here while `scheduled_job`
265 // above is built-in for it.
266 "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 // ORCH-19: a harness is CONTROLLED for conversations when it
272 // publishes at least one lifecycle DOOR supercode can drive —
273 // Codex's `archive`/`delete` CLI verbs, OpenCode's HTTP session
274 // API, Hermes's and OpenClaw's `/new`/`/reset` slash commands
275 // typed into a live driven session, Hermes's `sessions delete`,
276 // and supercode's own store. The advertised methods come from
277 // `sessions_control`'s own door table, so a method can never be
278 // listed here without a door behind it. Claude Code stays
279 // read-only on purpose: it publishes no lifecycle verb at all
280 // (its sessions expire on a retention window it owns).
281 "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 // ORC-7: the orchestrator's conversations are its BINDINGS,
298 // discovered from every profile folder's `bindings` table
299 // (`supercode_interchange::catalog::discover_orchestrator`). They are
300 // observed, not loadable: the transcript belongs to the
301 // worker harness the binding addresses and is read through
302 // that harness's own `sessions.load`.
303 // ORC-13 makes them CONTROLLED: `/new` and `/reset` are the
304 // two chat commands the daemon's reducer applies to a binding
305 // (`docs/ORCHESTRATOR-IR.md` §4.5), and the operator door now
306 // reaches that reducer from outside a chat — the daemon's own
307 // socket, or its package's CLI when the daemon is down. The
308 // methods come from `sessions_control`'s door table, so a
309 // method can never be advertised without a door behind it.
310 "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 // ORCH-9: a harness is `observed` for pending requests when
327 // its runtime door can carry one — the same flag that makes
328 // it `controlled`, because at the pinned versions the LIVE
329 // request IS the only uniform source (no harness stores
330 // approvals; see `crate::approvals`).
331 // ORCH-20 adds `harness.v1.approvals.resolve` to the
332 // controlled tier: one uniform decision, translated onto the
333 // request's own options and sent through `runtimes.respond`.
334 "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 // ORCH-21: the two harnesses that publish a client-callable
344 // profile lifecycle verb are also CONTROLLED — supercode runs
345 // `hermes profile create|delete` / `openclaw agents
346 // add|delete` on the caller's behalf and re-reads the row
347 // (`crate::profiles_control`). supercode still owns no config
348 // plane; the tier means "the harness's own verb is reachable
349 // through one uniform door".
350 "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 // ORCH-10: the profile noun is read for the four harnesses
367 // that have one — supercode's presets, Codex's
368 // `[profiles.<name>]` tables, Hermes's profile homes, and
369 // OpenClaw's agent homes. Every other harness refuses. Codex
370 // and supercode stay read-only on purpose: a Codex profile is
371 // a table a human authors in `config.toml` and a supercode
372 // preset is compiled-in code, so neither publishes a verb a
373 // client could call.
374 "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 // ORCH-11: a harness is `observed` for skills when the loader
384 // in `crate::skills` opens its documented skill roots.
385 // ORCH-22 makes those same harnesses `controlled`: each one
386 // publishes a skills door supercode drives — `hermes skills
387 // install|uninstall`, `openclaw skills install`, and for the
388 // core four the loader's own directory, which IS their only
389 // skills door. supercode resolves no registry and unpacks no
390 // archive; a verb a harness lacks (OpenClaw has no `skills
391 // remove` at the pin) refuses with UnsupportedAction.
392 "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 // ORCH-13: a delivery target is a FIELD on a job or a run,
406 // not a noun with verbs of its own, so it is observed exactly
407 // where those rows are — `deliver` on every job harness, and
408 // the delivery record on the two that keep a fire store.
409 // Nothing is controlled: supercode never sends.
410 "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 // ORCH-14: the channel noun is read for the two gateway
423 // harnesses that HAVE install-scoped channels — Hermes's
424 // `platforms:` blocks and OpenClaw's `channels.<name>`
425 // entries. Claude Code's channels are MCP servers that
426 // declare the capability over the protocol, not in a config
427 // file, so it is refused rather than guessed at.
428 // ORCH-17: gateway state/endpoint on the inventory row, derived from the
429 // UNI-7 running-instance probe and the harness's own config.
430 "gateway_health"
431 if matches!(
432 descriptor.id.as_str(),
433 // ORC-7: the orchestrator's gateway state is its
434 // daemon lease (`<home>/orchestrator.lock` plus a
435 // liveness check on the pid it names), reported on
436 // the same `harnesses.list` row as the other two.
437 HarnessId::HERMES | HarnessId::OPENCLAW | HarnessId::ORCHESTRATOR
438 ) =>
439 {
440 (
441 ImplementationKind::BuiltIn,
442 ImplementationKind::Absent,
443 vec!["harness.v1.harnesses.list"],
444 )
445 }
446 // ORCH-16: inbound webhook routes / hook mappings from the same configs.
447 "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 // ORCH-15: routing entries read from the same gateway configs.
457 "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 // ORCH-12: a harness is `observed` for memory when
472 // `crate::memory` opens its own persistent memory documents —
473 // Claude Code's per-project auto-memory directory, Hermes's
474 // `memories/MEMORY.md`/`USER.md` per profile home, and
475 // OpenClaw memory-core's workspace files. Nothing is
476 // controlled: forget/reset stay the harness's own verb.
477 "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 // PARITY-24 drift 2026-08-31: gemini-cli 0.29.x renamed the ACP
514 // flag; `--acp` is rejected with "Unknown argument". Verified live:
515 // `--experimental-acp` completes the v1 initialize handshake.
516 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 // `openclaw acp` is a stdio ACP bridge that CONNECTS to a running
537 // Gateway (never spawns one); with no flags it resolves the gateway
538 // target from OpenClaw's own config. The explicit-endpoint variant is
539 // the connect_launch below.
540 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 // the backend's own prefix: a published launch that omitted the
569 // stream-json flags started the interactive TUI on a pipe when
570 // handed back through `RuntimeStart.launch`
571 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 // UNI-15 read-only tier: discovery + load over the
663 // state.db SQLite store. Follow/import stay Absent.
664 // EXPORT (UNI-18) goes through Hermes's own door:
665 // `hermes sessions import --from codex` (0.21.0), which
666 // writes the store with Hermes's own writer — supercode
667 // never writes a live Hermes store itself. The tier
668 // stays driven (matrix membership is UNI-17's flip).
669 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 // UNI-16 read-only tier: discovery over
689 // `agents/<id>/sessions/*.jsonl` and the pi-v3-dialect
690 // loader (`from_openclaw_str`). Import (translate IN),
691 // export (write OUT), and follow stay Absent — the write
692 // path is a permanent skip, and the TIER stays `driven`:
693 // matrix membership remains UNI-17's priced flip.
694 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 // Blind-walk finding 2026-08-31: `gateway.url` is NOT a
705 // key openclaw's config schema accepts (the gateway
706 // rejects the whole file as invalid config). The real
707 // shape: an optional full URL at `gateway.remote.url`, a
708 // bare `gateway.port` number, or nothing at all — the
709 // documented out-of-the-box endpoint is ws://127.0.0.1:18789.
710 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 // ORC-7: the orchestrator is a harness id so the EXISTING
722 // orchestration readers list its state — its folder is a
723 // Hermes-shaped home (`docs/ORCHESTRATOR-IR.md` §6) and each
724 // reader is pointed at it with no new reader code. It has no
725 // native session tier of its own: it keeps no transcripts, only
726 // BINDINGS that address a WORKER harness's session, which is read
727 // through that harness's own door. It has no runtime either — the
728 // daemon is a Node process the operator verbs start and stop, not
729 // an adapter supercode connects a turn to.
730 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
779/// Look up one harness in the compiled registry.
780pub 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}