Skip to main content

supercode_harness/
harness_service.rs

1//! Versioned, language-neutral service over persisted harness sessions.
2//!
3//! The service is transport-agnostic: [`HarnessSessionService::handle`] accepts
4//! one JSON-RPC value and [`HarnessSessionService::poll`] produces subscription
5//! notifications. The CLI exposes those primitives as NDJSON over stdio.
6
7use std::collections::{BTreeMap, BTreeSet};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13use serde_json::{json, Value};
14use tokio::sync::Notify;
15
16use crate::runtime::generated_session_id;
17#[cfg(feature = "adapter-api")]
18use crate::runtime::{HostedHarnessConnection, HostedHarnessRuntime};
19use crate::sdk::{
20    discover_session_page, load_session, load_session_with_fidelity, SdkCapabilities, SdkError,
21    SdkErrorCode, SdkEvent, SdkOperation, SdkRequest, SdkRuntimeEvent, SdkService,
22};
23use crate::watch::{bound_session_view, message_json, normalized_session_json};
24use crate::Fidelity;
25#[cfg(feature = "adapter-api")]
26use crate::SupercodeHttpRuntimeBackend;
27use crate::{
28    discover_live_runtime, harness_support_registry, AcpRuntimeBackend, ClaudeCodeRuntimeBackend,
29    CodexRuntimeBackend, DiscoveryQuery, HarnessCatalog, HarnessHomes, HarnessId,
30    ImplementationKind, LiveRuntimeEndpoint, LiveRuntimeSource, OpenCodeRuntimeBackend,
31    PiRuntimeBackend, Role, RuntimeAttachRequest, RuntimeBackend, RuntimeConnection, RuntimeInput,
32    RuntimeLaunch, RuntimeStartRequest, Session, SessionDescriptor, SessionFollower, SessionFormat,
33    SessionLocator, SessionSource,
34};
35use crate::{reduce, tokens};
36#[cfg(feature = "adapter-api")]
37use crate::{register_live_runtime, resolve_live_runtime, LiveRuntimeRegistration};
38
39/// Every JSON-RPC method the harness service dispatches (`harness.v1.capabilities`
40/// reports it; ORCH-4 registry tiers must cite entries of it).
41pub const HARNESS_SERVICE_METHODS: &[&str] = &[
42    "harness.v1.support.report",
43    "harness.v1.harnesses.list",
44    "harness.v1.harnesses.probe",
45    "harness.v1.harnesses.settings",
46    "harness.v1.harnesses.configure",
47    "harness.v1.harnesses.auth.methods",
48    "harness.v1.harnesses.auth.begin",
49    "harness.v1.harnesses.auth.verify",
50    "harness.v1.sessions.discover",
51    "harness.v1.sessions.load",
52    "harness.v1.sessions.follow",
53    "harness.v1.sessions.unfollow",
54    "harness.v1.sessions.activity.subscribe",
55    "harness.v1.sessions.activity.unsubscribe",
56    "harness.v1.sessions.index.subscribe",
57    "harness.v1.sessions.index.resize",
58    "harness.v1.sessions.index.unsubscribe",
59    "harness.v1.sessions.message",
60    "harness.v1.sessions.import",
61    "harness.v1.sessions.export",
62    "harness.v1.sessions.translate",
63    "harness.v1.sessions.reduce",
64    "harness.v1.sessions.branch",
65    "harness.v1.sessions.handoff",
66    "harness.v1.sessions.materialize",
67    "harness.v1.sessions.resume_instructions",
68    "harness.v1.skills.list",
69    "harness.v1.skills.install",
70    "harness.v1.skills.remove",
71    "harness.v1.memory.show",
72    "harness.v1.memory.search",
73    "harness.v1.jobs.list",
74    "harness.v1.jobs.get",
75    "harness.v1.jobs.create",
76    "harness.v1.jobs.update",
77    "harness.v1.jobs.pause",
78    "harness.v1.jobs.resume",
79    "harness.v1.jobs.run",
80    "harness.v1.jobs.delete",
81    "harness.v1.jobs.notepad",
82    "harness.v1.jobs.notepad_set",
83    "harness.v1.jobs.notepad_delete",
84    "harness.v1.sessions.new",
85    "harness.v1.sessions.reset",
86    "harness.v1.sessions.archive",
87    "harness.v1.sessions.delete",
88    "harness.v1.runs.list",
89    "harness.v1.runs.get",
90    "harness.v1.approvals.list",
91    "harness.v1.approvals.resolve",
92    "harness.v1.runtimes.capabilities",
93    "harness.v1.runtimes.start",
94    "harness.v1.runtimes.resume",
95    "harness.v1.runtimes.attach_existing",
96    "harness.v1.runtimes.attach",
97    "harness.v1.runtimes.send_input",
98    "harness.v1.runtimes.interrupt",
99    "harness.v1.runtimes.steer",
100    "harness.v1.runtimes.respond",
101    "harness.v1.runtimes.terminal_instructions",
102    "harness.v1.runtimes.acquire_control",
103    "harness.v1.runtimes.heartbeat",
104    "harness.v1.runtimes.detach",
105    "harness.v1.runtimes.close",
106    "harness.v1.profiles.list",
107    "harness.v1.profiles.get",
108    "harness.v1.profiles.create",
109    "harness.v1.profiles.delete",
110    "harness.v1.channels.list",
111    "harness.v1.routes.list",
112    "harness.v1.triggers.list",
113    "harness.v1.channels.status",
114    "harness.v1.orchestration.load",
115    "harness.v1.orchestration.save",
116    "harness.v1.orchestration.compile",
117    "harness.v1.orchestration.decompile",
118    "harness.v1.orchestration.import",
119    "harness.v1.orchestration.export",
120    "harness.v1.workflow.load",
121];
122
123/// Protocol namespace implemented by this service.
124pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
125/// Notification method emitted for followed-session changes.
126pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
127/// Notification method emitted for normalized session-activity transitions.
128pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
129/// Notification method emitted for revisioned session-list changes.
130pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
131/// Notification method emitted for live runtime events.
132pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
133
134/// Stateful persisted-session service. Each instance owns its follow
135/// subscriptions; discovery and loading remain read-only.
136pub struct HarnessSessionService {
137    catalog: HarnessCatalog,
138    followers: BTreeMap<String, SessionFollower>,
139    followed_sources: BTreeMap<String, FollowedSource>,
140    activity_subscriptions: BTreeMap<String, ActivitySubscription>,
141    index_subscriptions: BTreeMap<String, crate::session_index::SessionIndexSubscription>,
142    index_notifier: Arc<Notify>,
143    #[cfg(feature = "adapter-api")]
144    activity_monitor: crate::session_activity::SessionActivityMonitor,
145    next_subscription: u64,
146    runtimes: BTreeMap<String, Box<dyn RuntimeConnection>>,
147    /// Connections lent to a detached call that is running right now. The
148    /// runtime itself is OUT of `runtimes` for that whole call, and these
149    /// names are how a second caller is told the connection is busy rather
150    /// than unknown.
151    runtimes_in_flight: BTreeSet<String>,
152    terminal_launches: BTreeMap<String, StructuredLaunch>,
153    runtime_sequences: BTreeMap<String, u64>,
154    next_runtime: u64,
155    reduction_store_root: Option<PathBuf>,
156    /// ORCH-9: live permission/approval requests outstanding on the open
157    /// runtime connections above, fed by the same event pump that publishes
158    /// `harness.v1.runtimes.event`.
159    approvals: crate::approvals::ApprovalRegistry,
160    /// ORCH-9: supercode's own queued subagent approvals, when the host that
161    /// owns this service publishes its parent queue here.
162    subagent_approvals: Option<Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>>,
163}
164
165impl Default for HarnessSessionService {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171impl HarnessSessionService {
172    /// Create an empty service instance.
173    pub fn new() -> Self {
174        Self {
175            catalog: HarnessCatalog::new(),
176            followers: BTreeMap::new(),
177            followed_sources: BTreeMap::new(),
178            activity_subscriptions: BTreeMap::new(),
179            index_subscriptions: BTreeMap::new(),
180            index_notifier: Arc::new(Notify::new()),
181            #[cfg(feature = "adapter-api")]
182            activity_monitor: Default::default(),
183            next_subscription: 1,
184            runtimes: BTreeMap::new(),
185            runtimes_in_flight: BTreeSet::new(),
186            terminal_launches: BTreeMap::new(),
187            runtime_sequences: BTreeMap::new(),
188            next_runtime: 1,
189            reduction_store_root: None,
190            approvals: crate::approvals::ApprovalRegistry::new(),
191            subagent_approvals: None,
192        }
193    }
194
195    /// Override the trusted, service-owned store used for durable reduction
196    /// bundles. Embedders and tests use this to keep all writes inside an
197    /// explicitly selected root; the CLI otherwise uses the normal
198    /// `$SUPERCODE_HOME/sessions` location.
199    pub fn with_reduction_store_root(mut self, root: impl Into<PathBuf>) -> Self {
200        self.reduction_store_root = Some(root.into());
201        self
202    }
203
204    /// ORCH-9: publish the parent's own subagent-approval queue into
205    /// `harness.v1.approvals.list`.
206    ///
207    /// This is the SAME `Arc` an [`crate::Agent`] pushes into
208    /// (`Agent::pending_child_approvals`), so a host that runs supercode's own
209    /// loop beside this service surfaces those requests through the uniform
210    /// door without copying them anywhere.
211    pub fn observe_subagent_approvals(
212        &mut self,
213        queue: Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>,
214    ) {
215        self.subagent_approvals = Some(queue);
216    }
217
218    /// ORCH-9: every approval request this service can see, newest last.
219    ///
220    /// Two sources, both live: the requests outstanding on the open runtime
221    /// connections, and supercode's own queued subagent approvals. There is
222    /// no file or database source at the pinned harness versions (see
223    /// [`crate::approvals`]), so a stored or proposal row is never produced.
224    pub fn approvals(&self, query: &crate::approvals::ApprovalsQuery) -> Vec<crate::ApprovalRow> {
225        let now = crate::approvals::now_ms();
226        let mut rows = self.approvals.rows(now);
227        if let Some(queue) = self.subagent_approvals.as_ref() {
228            let queued = queue
229                .lock()
230                .unwrap_or_else(std::sync::PoisonError::into_inner)
231                .clone();
232            rows.extend(crate::approvals::subagent_rows(&queued, now));
233        }
234        rows.retain(|row| query.matches(row));
235        rows.sort_by(|left, right| {
236            left.requested_at_ms
237                .cmp(&right.requested_at_ms)
238                .then_with(|| left.id.cmp(&right.id))
239        });
240        rows
241    }
242
243    /// ORCH-20 (controlled tier): answer one listed approval request by its
244    /// row id and one uniform decision.
245    ///
246    /// The decision is translated into the option token and reply envelope
247    /// the door that raised the request already accepts
248    /// ([`crate::approvals::plan_reply`]), and the answer is then sent by
249    /// calling `harness.v1.runtimes.respond` itself — the same code path, the
250    /// same adapter, the same bookkeeping that drops the row. This verb adds
251    /// a translation and nothing else.
252    async fn approvals_resolve(
253        &mut self,
254        params: Value,
255    ) -> std::result::Result<Value, ServiceError> {
256        let params = decode::<crate::approvals::ApprovalsResolveParams>(params)?;
257        if params.id.trim().is_empty() {
258            return Err(ServiceError::InvalidParams(
259                "approvals resolve requires the `id` of a listed approval row".into(),
260            ));
261        }
262        let choice = match (params.decision, params.option_id.as_deref()) {
263            (Some(_), Some(_)) => {
264                return Err(ServiceError::InvalidParams(
265                    "approvals resolve takes either `decision` or `option_id`, not both".into(),
266                ))
267            }
268            (Some(decision), None) => crate::approvals::ApprovalChoice::Decision(decision),
269            (None, Some(option)) => crate::approvals::ApprovalChoice::Option(option.to_string()),
270            (None, None) => {
271                return Err(ServiceError::InvalidParams(format!(
272                    "approvals resolve requires `decision` ({}) or an explicit `option_id`",
273                    crate::approvals::ApprovalDecision::ALL
274                        .map(|decision| decision.as_str())
275                        .join(" | "),
276                )))
277            }
278        };
279        let resolution = self
280            .approvals
281            .resolution(&params.id, &choice)
282            .map_err(|error| ServiceError::InvalidParams(error.to_string()))?;
283        // The harness's own door, unchanged: this is the identical call
284        // `harness.v1.runtimes.respond` performs for a caller who built the
285        // envelope by hand, including dropping the answered row.
286        self.runtime_call(
287            "harness.v1.runtimes.respond",
288            json!({
289                "connection": resolution.connection,
290                "request_id": resolution.request_id,
291                "response": resolution.response,
292            }),
293        )
294        .await?;
295        Ok(json!({
296            "id": params.id,
297            "decision": params.decision.map(|decision| decision.as_str()),
298            "option_id": resolution.option_id,
299            "resolved": true,
300        }))
301    }
302
303    /// Return the edge-triggered wakeup used by session-index filesystem
304    /// subscriptions. Transports can await this instead of polling indexes.
305    #[cfg(feature = "adapter-api")]
306    pub fn session_index_notifier(&self) -> Arc<Notify> {
307        Arc::clone(&self.index_notifier)
308    }
309
310    /// Handle one JSON-RPC 2.0 request and return one JSON-RPC response.
311    #[cfg(feature = "adapter-api")]
312    pub fn handle(&mut self, request: Value) -> Value {
313        let id = request.get("id").cloned().unwrap_or(Value::Null);
314        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
315            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
316        }
317        let Some(method) = request.get("method").and_then(Value::as_str) else {
318            return rpc_error(id, -32600, "request is missing `method`");
319        };
320        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
321        match self.call(method, params) {
322            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
323            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
324            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
325            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
326            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
327            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
328        }
329    }
330
331    /// Handle either a persisted-session request or an asynchronous live
332    /// runtime request.
333    #[cfg(feature = "adapter-api")]
334    pub async fn handle_async(&mut self, request: Value) -> Value {
335        let method = request
336            .get("method")
337            .and_then(Value::as_str)
338            .unwrap_or_default();
339        if matches!(
340            method,
341            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe"
342        ) {
343            let id = request.get("id").cloned().unwrap_or(Value::Null);
344            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
345                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
346            }
347            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
348            return match self.inventory_call(method, params).await {
349                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
350                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
351                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
352                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
353                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
354                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
355            };
356        }
357        if matches!(
358            method,
359            "harness.v1.harnesses.auth.methods"
360                | "harness.v1.harnesses.auth.begin"
361                | "harness.v1.harnesses.auth.verify"
362        ) {
363            let id = request.get("id").cloned().unwrap_or(Value::Null);
364            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
365                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
366            }
367            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
368            return match self.harness_authentication_call(method, params).await {
369                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
370                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
371                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
372                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
373                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
374                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
375            };
376        }
377        // ORCH-19 controlled tier. Answered here rather than through the SDK
378        // operation dispatch below so the harness's OWN refusal reaches the
379        // caller: `sdk_error` collapses every `UnsupportedAction` to one
380        // generic sentence, and the whole point of this tier is that a
381        // refusal names which door the harness does have.
382        if matches!(
383            method,
384            "harness.v1.sessions.new"
385                | "harness.v1.sessions.reset"
386                | "harness.v1.sessions.archive"
387                | "harness.v1.sessions.delete"
388        ) {
389            let id = request.get("id").cloned().unwrap_or(Value::Null);
390            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
391                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
392            }
393            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
394            let verb = match method {
395                "harness.v1.sessions.new" => crate::SessionVerb::New,
396                "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
397                "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
398                _ => crate::SessionVerb::Delete,
399            };
400            return match self.mutate_session(verb, params).await {
401                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
402                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
403                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
404                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
405                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
406                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
407            };
408        }
409        if method == "harness.v1.sessions.message" {
410            let id = request.get("id").cloned().unwrap_or(Value::Null);
411            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
412                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
413            }
414            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
415            return match self.message_call(params).await {
416                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
417                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
418                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
419                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
420                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
421                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
422            };
423        }
424        if matches!(
425            method,
426            "harness.v1.harnesses.settings" | "harness.v1.harnesses.configure"
427        ) {
428            let id = request.get("id").cloned().unwrap_or(Value::Null);
429            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
430                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
431            }
432            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
433            return match self.harness_settings_call(method, params) {
434                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
435                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
436                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
437                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
438                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
439                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
440            };
441        }
442        if method == "harness.v1.sessions.activity.subscribe" {
443            let id = request.get("id").cloned().unwrap_or(Value::Null);
444            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
445                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
446            }
447            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
448            return match self.subscribe_session_activity(params).await {
449                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
450                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
451                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
452                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
453                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
454                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
455            };
456        }
457        if let Some(operation) = SdkOperation::from_method(method) {
458            let id = request.get("id").cloned().unwrap_or(Value::Null);
459            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
460                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
461            }
462            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
463            return match self.execute(SdkRequest { operation, params }).await {
464                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
465                Err(error) => sdk_rpc_error(id, &error),
466            };
467        }
468        if !method.starts_with("harness.v1.runtimes.") {
469            return self.handle(request);
470        }
471        let id = request.get("id").cloned().unwrap_or(Value::Null);
472        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
473            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
474        }
475        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
476        match self.runtime_call(method, params).await {
477            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
478            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
479            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
480            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
481            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
482            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
483        }
484    }
485
486    /// Poll all active subscriptions once and return zero or more JSON-RPC
487    /// notifications. Recoverable follower errors are delivered as events.
488    #[cfg(feature = "adapter-api")]
489    pub fn poll(&mut self) -> Vec<Value> {
490        let mut notifications = Vec::new();
491        for (subscription, follower) in &mut self.followers {
492            match follower.poll() {
493                Ok(Some(event)) => notifications.push(json!({
494                    "jsonrpc": "2.0",
495                    "method": SESSION_EVENT_METHOD,
496                    "params": {
497                        "subscription": subscription,
498                        "event": event.to_json(),
499                    }
500                })),
501                Ok(None) => {}
502                Err(error) => notifications.push(json!({
503                    "jsonrpc": "2.0",
504                    "method": SESSION_EVENT_METHOD,
505                    "params": {
506                        "subscription": subscription,
507                        "event": {
508                            "type": "watch_error",
509                            "recoverable": true,
510                            "message": error.to_string(),
511                        },
512                    }
513                })),
514            }
515        }
516        notifications
517    }
518
519    /// Report each followed session's live-runtime lifecycle state on that
520    /// session's own subscription, emitting only when the state changes.
521    ///
522    /// A growing transcript is not evidence that an agent is working, so the
523    /// state comes from the live-runtime registry and nowhere else. A followed
524    /// session with no registered Supercode runtime — a harness running outside
525    /// Supercode — reports `persisted`, which says plainly that its activity is
526    /// unknown rather than guessing at it. These events carry no sequence
527    /// number and no transcript content; they never interleave with the
528    /// content follower's sequenced stream.
529    #[cfg(feature = "adapter-api")]
530    pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
531        let registry = crate::LocalRuntimeRegistry::new();
532        let authorization = crate::RuntimeAuthorization::observer();
533        let mut notifications = Vec::new();
534        for (subscription, source) in &mut self.followed_sources {
535            let state = match registry
536                .source_state(&source.harness, &source.session_id, &authorization)
537                .await
538            {
539                Ok(Some(state)) => state,
540                Ok(None) => crate::RuntimeRegistryState::Persisted,
541                // A failed registry read is not evidence of a state change.
542                Err(_) => continue,
543            };
544            if source.reported.as_deref() == Some(state.as_str()) {
545                continue;
546            }
547            source.reported = Some(state.as_str().to_string());
548            notifications.push(json!({
549                "jsonrpc": "2.0",
550                "method": SESSION_EVENT_METHOD,
551                "params": {
552                    "subscription": subscription,
553                    "event": {"type": "runtime_state", "state": state.as_str()},
554                },
555            }));
556        }
557        notifications
558    }
559
560    /// Poll normalized activity subscriptions, emitting only proven state
561    /// transitions. Every subscription is bulk-sampled so stock-harness
562    /// process and registry discovery happens once per UI, not once per row.
563    #[cfg(feature = "adapter-api")]
564    pub async fn poll_session_activities(&mut self) -> Vec<Value> {
565        let subscriptions = self
566            .activity_subscriptions
567            .iter()
568            .map(|(id, subscription)| {
569                (
570                    id.clone(),
571                    subscription.locators.clone(),
572                    subscription.homes.clone(),
573                )
574            })
575            .collect::<Vec<_>>();
576        let mut notifications = Vec::new();
577        for (subscription_id, locators, homes) in subscriptions {
578            let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
579                // A failed evidence read proves no transition. Retain the last
580                // good state instead of flashing every row to persisted.
581                continue;
582            };
583            let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
584                continue;
585            };
586            let mut changed = Vec::new();
587            for activity in activities {
588                let key = activity.key();
589                if subscription
590                    .reported
591                    .get(&key)
592                    .is_some_and(|previous| previous.same_state(&activity))
593                {
594                    continue;
595                }
596                subscription.reported.insert(key, activity.clone());
597                changed.push(activity);
598            }
599            if !changed.is_empty() {
600                notifications.push(json!({
601                    "jsonrpc": "2.0",
602                    "method": SESSION_ACTIVITY_EVENT_METHOD,
603                    "params": {
604                        "subscription": subscription_id,
605                        "activities": changed,
606                    },
607                }));
608            }
609        }
610        notifications
611    }
612
613    /// Drain native-store invalidations and emit revisioned descriptor deltas.
614    /// An idle subscription performs no catalog or transcript reads between
615    /// its minute-scale recovery reconciliations.
616    #[cfg(feature = "adapter-api")]
617    pub fn poll_session_indexes(&mut self) -> Vec<Value> {
618        let mut notifications = Vec::new();
619        for (subscription, index) in &mut self.index_subscriptions {
620            let homes = index.homes().clone();
621            match index.poll() {
622                Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
623                    Ok(changes) => notifications.push(json!({
624                        "jsonrpc": "2.0",
625                        "method": SESSION_INDEX_EVENT_METHOD,
626                        "params": {
627                            "subscription": subscription,
628                            "revision": delta.revision,
629                            "changes": changes,
630                        },
631                    })),
632                    Err(error) => notifications.push(json!({
633                        "jsonrpc": "2.0",
634                        "method": SESSION_INDEX_EVENT_METHOD,
635                        "params": {
636                            "subscription": subscription,
637                            "error": {"recoverable": true, "message": error_message(error)},
638                        },
639                    })),
640                },
641                Ok(None) => {}
642                Err(error) => notifications.push(json!({
643                    "jsonrpc": "2.0",
644                    "method": SESSION_INDEX_EVENT_METHOD,
645                    "params": {
646                        "subscription": subscription,
647                        "error": {"recoverable": true, "message": error},
648                    },
649                })),
650            }
651        }
652        notifications
653    }
654
655    #[cfg(feature = "adapter-api")]
656    async fn subscribe_session_activity(
657        &mut self,
658        params: Value,
659    ) -> std::result::Result<Value, ServiceError> {
660        let params = decode::<ActivitySubscribeParams>(params)?;
661        if params.locators.is_empty() {
662            return Err(ServiceError::InvalidParams(
663                "sessions.activity.subscribe requires at least one locator".into(),
664            ));
665        }
666        if params.locators.len() > 2_048 {
667            return Err(ServiceError::InvalidParams(
668                "sessions.activity.subscribe accepts at most 2048 locators".into(),
669            ));
670        }
671        let initial = self
672            .activity_monitor
673            .resolve(&params.locators, &params.homes)
674            .await
675            .map_err(ServiceError::Sdk)?;
676        let subscription = format!("activity-sub-{}", self.next_subscription);
677        self.next_subscription += 1;
678        let reported = initial
679            .iter()
680            .cloned()
681            .map(|activity| (activity.key(), activity))
682            .collect();
683        self.activity_subscriptions.insert(
684            subscription.clone(),
685            ActivitySubscription {
686                locators: params.locators,
687                homes: params.homes,
688                reported,
689            },
690        );
691        Ok(json!({"subscription": subscription, "initial": initial}))
692    }
693
694    /// Non-blockingly sample one event from every connected live runtime.
695    #[cfg(feature = "adapter-api")]
696    pub async fn poll_runtimes(&mut self) -> Vec<Value> {
697        self.poll_sdk_events()
698            .await
699            .into_iter()
700            .map(|(connection, runtime_event)| {
701                json!({
702                    "jsonrpc": "2.0",
703                    "method": RUNTIME_EVENT_METHOD,
704                    "params": {
705                        "connection": connection,
706                        "session_id": runtime_event.session_id,
707                        "sequence": runtime_event.event.sequence,
708                        "event": {
709                            "kind": runtime_event.event.kind,
710                            "payload": runtime_event.event.payload,
711                        },
712                    },
713                })
714            })
715            .collect()
716    }
717
718    async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
719        let mut events = Vec::new();
720        let mut closed = Vec::new();
721        let now_ms = crate::approvals::now_ms();
722        for (connection, runtime) in &mut self.runtimes {
723            let session_id = runtime.handle().runtime_id.clone();
724            let harness = runtime.handle().harness.clone();
725            // Drain what the runtime already has: a turn is several events
726            // (updates, then the protocol's completion), and delivering one
727            // per poll would cost a poll interval each. A zero timeout takes
728            // only what is ready — an idle runtime costs nothing.
729            for _ in 0..256 {
730                match tokio::time::timeout(Duration::ZERO, runtime.next_event()).await {
731                    Ok(Ok(Some(event))) => {
732                        let terminal = event.kind == "transport_closed";
733                        // ORCH-9: a permission/approval request arrives as an
734                        // ordinary event; it becomes listable here and stops
735                        // being listable when `runtimes.respond` answers it.
736                        self.approvals
737                            .observe(connection, &harness, &session_id, &event, now_ms);
738                        let next_sequence = self
739                            .runtime_sequences
740                            .entry(session_id.clone())
741                            .or_insert(0);
742                        let sequence = event.sequence.unwrap_or_else(|| {
743                            *next_sequence = next_sequence.saturating_add(1);
744                            *next_sequence
745                        });
746                        *next_sequence = (*next_sequence).max(sequence);
747                        events.push((
748                            connection.clone(),
749                            SdkRuntimeEvent {
750                                session_id: session_id.clone(),
751                                event: SdkEvent {
752                                    sequence,
753                                    kind: event.kind,
754                                    payload: event.payload,
755                                },
756                            },
757                        ));
758                        if terminal {
759                            closed.push(connection.clone());
760                            break;
761                        }
762                    }
763                    Ok(Ok(None)) => {
764                        let sequence = self
765                            .runtime_sequences
766                            .entry(session_id.clone())
767                            .or_insert(0);
768                        *sequence = sequence.saturating_add(1);
769                        events.push((
770                        connection.clone(),
771                        SdkRuntimeEvent {
772                            session_id,
773                            event: SdkEvent {
774                                sequence: *sequence,
775                                kind: "transport_closed".into(),
776                                payload: json!({"message": "Harness runtime transport closed."}),
777                            },
778                        },
779                    ));
780                        closed.push(connection.clone());
781                        break;
782                    }
783                    Err(_) => break,
784                    Ok(Err(error)) => {
785                        let sequence = self
786                            .runtime_sequences
787                            .entry(session_id.clone())
788                            .or_insert(0);
789                        *sequence = sequence.saturating_add(1);
790                        events.push((
791                        connection.clone(),
792                        SdkRuntimeEvent {
793                            session_id,
794                            event: SdkEvent {
795                                sequence: *sequence,
796                                kind: "transport_error".into(),
797                                payload: json!({"message": error.to_string(), "terminal": true}),
798                            },
799                        },
800                    ));
801                        closed.push(connection.clone());
802                        break;
803                    }
804                }
805            }
806        }
807        for connection in closed {
808            if let Some(runtime) = self.runtimes.remove(&connection) {
809                self.runtime_sequences.remove(&runtime.handle().runtime_id);
810            }
811            self.terminal_launches.remove(&connection);
812            // A connection that is gone cannot answer anything it was
813            // holding; those requests stop being listable with it.
814            self.approvals.forget(&connection);
815        }
816        events
817    }
818
819    fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
820        match method {
821            "harness.v1.capabilities" => Ok(json!({
822                "version": HARNESS_SERVICE_VERSION,
823                "sdk": self.capabilities(),
824                "methods": HARNESS_SERVICE_METHODS,
825                "notifications": [
826                    SESSION_EVENT_METHOD,
827                    SESSION_ACTIVITY_EVENT_METHOD,
828                    SESSION_INDEX_EVENT_METHOD,
829                    RUNTIME_EVENT_METHOD
830                ],
831                "harnesses": harness_support_registry()
832                    .harnesses
833                    .into_iter()
834                    .map(|harness| harness.id)
835                    .collect::<Vec<_>>(),
836            })),
837            "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
838                .map_err(|error| ServiceError::Operation(error.to_string())),
839            "harness.v1.profiles.list" | "harness.v1.profiles.get" => profiles_call(method, params),
840            // ORCH-21 controlled tier. Each verb translates to the HARNESS'S
841            // OWN profile verb and runs it (`crate::profiles_control`);
842            // supercode makes and removes nothing itself. The row returned is
843            // re-read through the ORCH-10 loader afterwards, and `ran`
844            // narrates the exact command.
845            "harness.v1.profiles.create" => {
846                mutate_profile(crate::profiles_control::ProfileVerb::Create, params)
847            }
848            "harness.v1.profiles.delete" => {
849                mutate_profile(crate::profiles_control::ProfileVerb::Delete, params)
850            }
851            "harness.v1.channels.list" | "harness.v1.channels.status" => {
852                channels_call(method, params)
853            }
854            // ORCH-15 observed tier: which profile / agent a surface tuple
855            // resolves to, read from each gateway harness's own config.
856            "harness.v1.routes.list" => routes_call(params),
857            // ORCH-16 observed tier: inbound webhook routes / hook mappings.
858            "harness.v1.triggers.list" => triggers_call(params),
859            // ONT-4: the orchestration doors. One home folder in, one typed orchestration
860            // value out (and back). Every one of the four is
861            // `crate::orchestration_doors`, which the `supercode orchestration` verbs call
862            // too — the RPC adds nothing but the envelope. A vault VALUE
863            // never crosses this wire: a load or a compile answers with the
864            // `.env` KEY NAMES, and a caller that needs a value reads the
865            // home's own `.env`.
866            // the workflow layer's read door: a harness's board as one typed value,
867            // the same code the `supercode workflow load` verb calls
868            "harness.v1.workflow.load" => {
869                let params = decode::<WorkflowLoadParams>(params)?;
870                let read =
871                    crate::workflow_doors::load(params.from, &params.home).map_err(operation)?;
872                serde_json::to_value(read)
873                    .map_err(|error| ServiceError::Operation(error.to_string()))
874            }
875            "harness.v1.orchestration.load" => {
876                let params = decode::<OrchestrationLoadParams>(params)?;
877                let read = crate::orchestration_doors::load(&params.root, params.flavor)
878                    .map_err(operation)?;
879                serde_json::to_value(read)
880                    .map_err(|error| ServiceError::Operation(error.to_string()))
881            }
882            "harness.v1.orchestration.save" => {
883                let params = decode::<OrchestrationSaveParams>(params)?;
884                let saved = crate::orchestration_doors::save(
885                    &params.root,
886                    params.orchestration,
887                    params.vault,
888                )
889                .map_err(operation)?;
890                serde_json::to_value(saved)
891                    .map_err(|error| ServiceError::Operation(error.to_string()))
892            }
893            "harness.v1.orchestration.compile" => {
894                let params = decode::<OrchestrationCompileParams>(params)?;
895                let read = crate::orchestration_doors::compile(params.from, &params.home)
896                    .map_err(operation)?;
897                serde_json::to_value(read)
898                    .map_err(|error| ServiceError::Operation(error.to_string()))
899            }
900            "harness.v1.orchestration.decompile" => {
901                let params = decode::<OrchestrationDecompileParams>(params)?;
902                let report = crate::orchestration_doors::decompile(
903                    params.to,
904                    params.orchestration,
905                    &params.source,
906                    params.source_flavor,
907                    &params.dest,
908                    params.vault,
909                )
910                .map_err(operation)?;
911                serde_json::to_value(report)
912                    .map_err(|error| ServiceError::Operation(error.to_string()))
913            }
914            // a migration keeps the credential in this process: a compile and
915            // a save (import), a load and a decompile (export), composed here
916            // because composed by a client the secret would have to cross
917            // the wire
918            "harness.v1.orchestration.import" => {
919                let params = decode::<OrchestrationImportParams>(params)?;
920                let imported =
921                    crate::orchestration_doors::import(params.from, &params.home, &params.into)
922                        .map_err(operation)?;
923                serde_json::to_value(imported)
924                    .map_err(|error| ServiceError::Operation(error.to_string()))
925            }
926            "harness.v1.orchestration.export" => {
927                let params = decode::<OrchestrationExportParams>(params)?;
928                let report =
929                    crate::orchestration_doors::export(params.to, &params.root, &params.dest)
930                        .map_err(operation)?;
931                serde_json::to_value(report)
932                    .map_err(|error| ServiceError::Operation(error.to_string()))
933            }
934            // ORCH-12 observed tier: read and search the persistent memory
935            // documents a harness keeps on disk. Read-only — every write
936            // (`hermes memory off`, `openclaw memory forget|reset`, Claude
937            // Code's `/memory`) stays the harness's own verb. A harness with
938            // no memory store is refused with UnsupportedAction.
939            "harness.v1.memory.show" | "harness.v1.memory.search" => memory_call(method, params),
940            // ORCH-11 observed tier: read-only enumeration of every harness's
941            // installed skill packages. An unknown harness id is refused with
942            // UnsupportedAction — every harness supports skills, so a filter
943            // that matches nothing is a caller error, never an empty listing.
944            "harness.v1.skills.list" => {
945                let query = decode::<crate::skills::SkillsQuery>(params)?;
946                if let Some(harness) = query.harness.as_deref() {
947                    if !crate::skills::SKILL_HARNESSES.contains(&harness) {
948                        return Err(ServiceError::UnsupportedAction(format!(
949                            "`{harness}` has no skills root supercode reads"
950                        )));
951                    }
952                }
953                serde_json::to_value(crate::skills::list_skills(&query))
954                    .map_err(|error| ServiceError::Operation(error.to_string()))
955            }
956            // ORCH-22 controlled tier: each verb goes through the door the
957            // HARNESS publishes — `hermes skills install|uninstall`,
958            // `openclaw skills install`, and for the core four the loader's
959            // own directory, which is the only skills door those harnesses
960            // have. supercode resolves no registry and unpacks no archive.
961            // The row returned is re-read through the ORCH-11 loader
962            // afterwards, and `ran` narrates exactly what was performed.
963            "harness.v1.skills.install" => {
964                mutate_skill(crate::skills_control::SkillVerb::Install, params)
965            }
966            "harness.v1.skills.remove" => {
967                mutate_skill(crate::skills_control::SkillVerb::Remove, params)
968            }
969            // ORCH-9 observed tier: the approval requests waiting for an
970            // answer. At the pinned harness versions the only uniform source
971            // is a LIVE request held by an open runtime connection, plus
972            // supercode's own queued subagent approvals — neither Hermes
973            // 0.21.0 nor OpenClaw 2026.7.1-2 has an approvals door to read
974            // (see `crate::approvals`). A harness whose runtime cannot carry
975            // a protocol request at all is refused by name.
976            "harness.v1.approvals.list" => {
977                let query = decode::<crate::approvals::ApprovalsQuery>(params)?;
978                if let Some(harness) = query.harness.as_deref() {
979                    if !crate::approvals::lists_approvals(harness) {
980                        return Err(ServiceError::UnsupportedAction(format!(
981                            "`{harness}` has no runtime door that carries an approval request"
982                        )));
983                    }
984                }
985                serde_json::to_value(self.approvals(&query))
986                    .map_err(|error| ServiceError::Operation(error.to_string()))
987            }
988            "harness.v1.sessions.discover" => {
989                let query = decode::<DiscoveryQuery>(params)?;
990                let page = discover_session_page(&query).map_err(operation)?;
991                // Claude Code is the one harness that publishes its RUNNING
992                // sessions. The registry is read once per discovery and joined
993                // by session id; every record in it has already survived a
994                // `kill(pid, 0)` liveness check inside `read_registry`.
995                let peers = if page
996                    .sessions
997                    .iter()
998                    .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
999                {
1000                    crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(
1001                        &query.homes,
1002                    ))
1003                } else {
1004                    Vec::new()
1005                };
1006                let activities = crate::session_activity::resolve_stock_session_activities(
1007                    &page
1008                        .sessions
1009                        .iter()
1010                        .map(|session| session.locator.clone())
1011                        .collect::<Vec<_>>(),
1012                    &query.homes,
1013                )
1014                .into_iter()
1015                .map(|activity| (activity.key(), activity))
1016                .collect::<BTreeMap<_, _>>();
1017                let sessions = page
1018                    .sessions
1019                    .into_iter()
1020                    .map(|session| {
1021                        let mut value = live_descriptor_value(&session, &peers)?;
1022                        let activity_key = (
1023                            session.locator.harness.as_str().to_string(),
1024                            session.locator.session_id.clone(),
1025                        );
1026                        if let Some(activity) = activities.get(&activity_key) {
1027                            value["activity"] = serde_json::to_value(activity)
1028                                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1029                            if let Some(status) = legacy_live_status(activity) {
1030                                value["live_status"] = json!(status);
1031                            }
1032                        }
1033                        Ok(value)
1034                    })
1035                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1036                let mut result = json!({"sessions": sessions, "next_cursor": page.next_cursor});
1037                // Preserve the metadata-only wire shape, but carry the catalog's
1038                // proof/counts when the caller explicitly requests preview search.
1039                if query.search_previews {
1040                    result["receipt"] = serde_json::to_value(page.receipt)
1041                        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1042                }
1043                Ok(result)
1044            }
1045            "harness.v1.sessions.load" => {
1046                let params = decode::<LoadSessionParams>(params)?;
1047                if let Some(options) = &params.options {
1048                    options.validate()?;
1049                    if let Some(result) = indexed_claude_window(&params.read.locator, options)? {
1050                        return Ok(result);
1051                    }
1052                    return load_session(&params.read.locator)
1053                        .map(|session| projected_session_result(&session, options))
1054                        .map_err(operation);
1055                }
1056                let mut session = if params.read.display_history() {
1057                    self.catalog
1058                        .load_display_view(
1059                            &params.read.locator,
1060                            params.read.read_fidelity(),
1061                            params.read.tail_messages().unwrap_or(500),
1062                        )
1063                        .map_err(crate::Error::from)
1064                } else if params.read.include_subagents() {
1065                    load_session_with_fidelity(&params.read.locator, params.read.read_fidelity())
1066                } else {
1067                    self.catalog
1068                        .load_parent_with_fidelity(
1069                            &params.read.locator,
1070                            params.read.read_fidelity(),
1071                        )
1072                        .map_err(crate::Error::from)
1073                }
1074                .map_err(operation)?;
1075                params.read.bound_session(&mut session);
1076                Ok(json!({"session": normalized_session_json(&session)}))
1077            }
1078            "harness.v1.sessions.follow" => {
1079                let params = decode::<LocatorParams>(params)?;
1080                let mut follower = self
1081                    .catalog
1082                    .follow_read_view(
1083                        &params.locator,
1084                        params.read_fidelity(),
1085                        params.include_subagents(),
1086                        params.tail_messages(),
1087                        params.max_message_chars(),
1088                        params.display_history(),
1089                    )
1090                    .map_err(operation)?;
1091                let initial = follower
1092                    .poll()
1093                    .map_err(operation)?
1094                    .map(|event| event.to_json());
1095                let subscription = format!("sub-{}", self.next_subscription);
1096                self.next_subscription += 1;
1097                self.followers.insert(subscription.clone(), follower);
1098                self.followed_sources.insert(
1099                    subscription.clone(),
1100                    FollowedSource {
1101                        harness: params.locator.harness.as_str().to_string(),
1102                        session_id: params.locator.session_id.clone(),
1103                        reported: None,
1104                    },
1105                );
1106                Ok(json!({"subscription": subscription, "initial": initial}))
1107            }
1108            "harness.v1.sessions.unfollow" => {
1109                let params = decode::<UnfollowParams>(params)?;
1110                self.followed_sources.remove(&params.subscription);
1111                Ok(json!({
1112                    "removed": self.followers.remove(&params.subscription).is_some()
1113                }))
1114            }
1115            "harness.v1.sessions.activity.unsubscribe" => {
1116                let params = decode::<UnfollowParams>(params)?;
1117                Ok(json!({
1118                    "removed": self.activity_subscriptions.remove(&params.subscription).is_some()
1119                }))
1120            }
1121            "harness.v1.sessions.index.subscribe" => {
1122                let query = decode::<DiscoveryQuery>(params)?;
1123                crate::session_index::validate_query(&query)
1124                    .map_err(ServiceError::InvalidParams)?;
1125                let homes = query.homes.clone();
1126                let (index, initial) = crate::session_index::SessionIndexSubscription::open(
1127                    query,
1128                    Arc::clone(&self.index_notifier),
1129                )
1130                .map_err(ServiceError::Operation)?;
1131                let peers = peers_for_descriptors(&initial, &homes);
1132                let initial = initial
1133                    .iter()
1134                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1135                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1136                let subscription = format!("index-sub-{}", self.next_subscription);
1137                self.next_subscription += 1;
1138                self.index_subscriptions.insert(subscription.clone(), index);
1139                Ok(json!({
1140                    "subscription": subscription,
1141                    "revision": 1,
1142                    "initial": initial,
1143                }))
1144            }
1145            "harness.v1.sessions.index.resize" => {
1146                let params = decode::<IndexResizeParams>(params)?;
1147                crate::session_index::validate_limit(params.limit)
1148                    .map_err(ServiceError::InvalidParams)?;
1149                let index = self
1150                    .index_subscriptions
1151                    .get_mut(&params.subscription)
1152                    .ok_or_else(|| {
1153                        ServiceError::InvalidParams("unknown session index subscription".into())
1154                    })?;
1155                let prepared = index
1156                    .prepare_resize(params.limit)
1157                    .map_err(ServiceError::Operation)?;
1158                let peers = peers_for_descriptors(&prepared.page.sessions, index.homes());
1159                let initial = prepared
1160                    .page
1161                    .sessions
1162                    .iter()
1163                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1164                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1165                let response = json!({
1166                    "subscription": params.subscription,
1167                    "revision": prepared.revision,
1168                    "initial": initial,
1169                    "receipt": prepared.page.receipt,
1170                });
1171                index.commit_resize(prepared);
1172                Ok(response)
1173            }
1174            "harness.v1.sessions.index.unsubscribe" => {
1175                let params = decode::<UnfollowParams>(params)?;
1176                Ok(json!({
1177                    "removed": self.index_subscriptions.remove(&params.subscription).is_some()
1178                }))
1179            }
1180            "harness.v1.sessions.import" => {
1181                let params = decode::<ImportSessionParams>(params)?;
1182                let session = Session::load_str(&params.content, params.source_harness.into())
1183                    .map_err(operation)?;
1184                Ok(json!({"session": normalized_session_json(&session)}))
1185            }
1186            "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
1187                let params = decode::<ExportSessionParams>(params)?;
1188                let session = load_session(&params.locator).map_err(operation)?;
1189                let artifact = session_artifact(&params.locator, &session, params.target_harness)?;
1190                if method == "harness.v1.sessions.export"
1191                    && params.target_harness == TransferFormat::Hermes
1192                {
1193                    // UNI-18: write through Hermes's own door, never into its store
1194                    let imported = crate::hermes_import::import_into_hermes(&session, None)
1195                        .map_err(operation)?;
1196                    return Ok(json!({"artifact": artifact, "imported": imported}));
1197                }
1198                Ok(json!({"artifact": artifact}))
1199            }
1200            "harness.v1.sessions.reduce" => {
1201                let params = decode::<ReduceSessionParams>(params)?;
1202                self.reduce_session(params)
1203            }
1204            "harness.v1.sessions.branch" => {
1205                let params = decode::<BranchSessionParams>(params)?;
1206                let session = load_session(&params.locator).map_err(operation)?;
1207                let storage = params.locator.storage.path().display().to_string();
1208                let bootstrap_prompt = format!(
1209                    "Continue as a new branch from {} session {}. The frozen parent transcript is at {}. Read or load that parent for context, summarize the relevant state, then continue independently without mutating the parent session.",
1210                    params.locator.harness.as_str(), params.locator.session_id, storage
1211                );
1212                let artifact = params
1213                    .target_harness
1214                    .map(|target| session_artifact(&params.locator, &session, target))
1215                    .transpose()?;
1216                Ok(json!({
1217                    "parent": params.locator,
1218                    "session": normalized_session_json(&session),
1219                    "bootstrap_prompt": bootstrap_prompt,
1220                    "artifact": artifact,
1221                }))
1222            }
1223            "harness.v1.sessions.handoff" => {
1224                let params = decode::<HandoffSessionParams>(params)?;
1225                let session = load_session(&params.locator).map_err(operation)?;
1226                let cwd = params
1227                    .cwd
1228                    .or_else(|| session.meta.cwd.clone())
1229                    .unwrap_or_else(|| PathBuf::from("."));
1230                let artifact = handoff_artifact(&params.locator, &session, params.target_harness)?;
1231                let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
1232                    ServiceError::Operation(
1233                        "handoff artifact omitted target session identity".into(),
1234                    )
1235                })?;
1236                let instructions =
1237                    handoff_instructions(params.target_harness, target_session_id, &cwd);
1238                Ok(json!({
1239                    "artifact": artifact,
1240                    "launch": instructions.launch,
1241                    "materialize": instructions.materialize,
1242                    "requires_materialization": instructions.requires_materialization,
1243                    "note": instructions.note,
1244                }))
1245            }
1246            "harness.v1.sessions.materialize" => {
1247                let params = decode::<MaterializeSessionParams>(params)?;
1248                let locator = crate::native_materialize::materialize_native_artifact(
1249                    params.artifact,
1250                    &params.cwd,
1251                )
1252                .map_err(ServiceError::Operation)?;
1253                Ok(json!({"locator": locator}))
1254            }
1255            // ORCH-7 observed tier. Read-only: the handlers open the harness's
1256            // own job store (Claude Code's session JSONL, Hermes's and
1257            // OpenClaw's `cron/jobs.json`) and never write, fire, or schedule.
1258            "harness.v1.jobs.list" => {
1259                let query = decode::<crate::jobs::JobsQuery>(params)?;
1260                if let Some(harness) = query.harness.as_deref() {
1261                    refuse_harness_without_jobs(harness, "jobs.list")?;
1262                }
1263                let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1264                serde_json::to_value(listing)
1265                    .map_err(|error| ServiceError::Operation(error.to_string()))
1266            }
1267            "harness.v1.jobs.get" => {
1268                let params = decode::<JobsGetParams>(params)?;
1269                refuse_harness_without_jobs(&params.harness, "jobs.get")?;
1270                match crate::jobs::get_job(&params.harness, &params.id, &params.homes)
1271                    .map_err(operation)?
1272                {
1273                    Some((job, source)) => Ok(json!({"job": job, "source": source})),
1274                    None => Err(ServiceError::Operation(format!(
1275                        "`{}` has no scheduled job `{}`",
1276                        params.harness, params.id
1277                    ))),
1278                }
1279            }
1280            // ORCH-18 controlled tier. Each verb translates to the HARNESS'S
1281            // OWN cron verb and runs it (`crate::jobs_control`); supercode
1282            // schedules nothing. The row returned is re-read from the
1283            // harness's store afterwards, and `ran` narrates the exact command
1284            // with any credential redacted.
1285            "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1286            "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1287            "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1288            "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1289            "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1290            "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1291            "harness.v1.jobs.notepad"
1292            | "harness.v1.jobs.notepad_set"
1293            | "harness.v1.jobs.notepad_delete" => {
1294                let request = decode::<crate::jobs_notepad::JobNotepadRequest>(params)?;
1295                refuse_harness_without_jobs(&request.harness, "jobs.notepad")?;
1296                let answer = match method {
1297                    "harness.v1.jobs.notepad_set" => crate::jobs_notepad::set(&request),
1298                    "harness.v1.jobs.notepad_delete" => crate::jobs_notepad::delete(&request),
1299                    _ => crate::jobs_notepad::read(&request),
1300                }
1301                .map_err(job_control_error)?;
1302                serde_json::to_value(answer)
1303                    .map_err(|error| ServiceError::Operation(error.to_string()))
1304            }
1305            // ORCH-8 observed tier. Read-only: the handlers open the harness's
1306            // own run store (Hermes's `cron/executions.db`, OpenClaw's
1307            // `cron_run_logs`) and never claim, retry, or prune a fire.
1308            "harness.v1.runs.list" => {
1309                let query = decode::<crate::runs::RunsQuery>(params)?;
1310                if let Some(harness) = query.harness.as_deref() {
1311                    refuse_harness_without_runs(harness, "runs.list")?;
1312                }
1313                let listing = crate::runs::list_runs(&query).map_err(operation)?;
1314                serde_json::to_value(listing)
1315                    .map_err(|error| ServiceError::Operation(error.to_string()))
1316            }
1317            "harness.v1.runs.get" => {
1318                let params = decode::<RunsGetParams>(params)?;
1319                refuse_harness_without_runs(&params.harness, "runs.get")?;
1320                match crate::runs::get_run(&params.harness, &params.id, &params.homes)
1321                    .map_err(operation)?
1322                {
1323                    Some((run, source)) => Ok(json!({"run": run, "source": source})),
1324                    None => Err(ServiceError::Operation(format!(
1325                        "`{}` has no run `{}`",
1326                        params.harness, params.id
1327                    ))),
1328                }
1329            }
1330            "harness.v1.sessions.resume_instructions" => {
1331                let params = decode::<ResumeInstructionsParams>(params)?;
1332                let session = load_session(&params.locator).map_err(operation)?;
1333                let cwd = params
1334                    .cwd
1335                    .or(session.meta.cwd)
1336                    .unwrap_or_else(|| PathBuf::from("."));
1337                let launch = resume_launch(
1338                    params.locator.harness.as_str(),
1339                    &params.locator.session_id,
1340                    &cwd,
1341                    params.policy,
1342                )?;
1343                Ok(json!({"launch": launch}))
1344            }
1345            _ => Err(ServiceError::MethodNotFound),
1346        }
1347    }
1348
1349    fn reduce_session(
1350        &self,
1351        params: ReduceSessionParams,
1352    ) -> std::result::Result<Value, ServiceError> {
1353        let session = load_session(&params.locator).map_err(operation)?;
1354        if session.messages.is_empty() {
1355            return Err(ServiceError::InvalidParams(
1356                "cannot reduce an empty session".into(),
1357            ));
1358        }
1359        let keep_last = params.keep_last.clamp(1, 128);
1360        let policy = reduce::ReductionPolicy {
1361            clear_turns_older_than: Some(keep_last),
1362            ..Default::default()
1363        };
1364        let (view, log) =
1365            reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1366        if log.reductions.is_empty() {
1367            return Err(ServiceError::UnsupportedAction(format!(
1368                "session `{}` is already too small for a meaningful reversible reduction",
1369                params.locator.session_id
1370            )));
1371        }
1372        let source_tokens = tokens::estimate_view_tokens(&session.messages);
1373        let reduced_tokens = tokens::estimate_view_tokens(&view);
1374        if reduced_tokens >= source_tokens {
1375            return Err(ServiceError::UnsupportedAction(format!(
1376                "session `{}` has no token-reducing reversible projection",
1377                params.locator.session_id
1378            )));
1379        }
1380
1381        let store_root = self
1382            .reduction_store_root
1383            .clone()
1384            .unwrap_or_else(default_reduction_store_root);
1385        let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1386        let rescue_id = format!("rescue-{}", generated_session_id());
1387        let imported = session
1388            .imported_message_count
1389            .unwrap_or(session.messages.len())
1390            .min(session.messages.len());
1391        let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1392        let view_jsonl = messages_jsonl(&view)?;
1393        let title = format!(
1394            "Reduced {} continuation from {}",
1395            params.target_harness.id(),
1396            params.locator.session_id
1397        );
1398
1399        // Durability order is intentional: the full source of truth lands
1400        // before either object that can refer to it. A crash may leave an
1401        // unused sidecar, but can never leave a reduced view whose originals
1402        // were not durably written first.
1403        store
1404            .save_sidecar(&rescue_id, &sidecar_jsonl)
1405            .map_err(operation)?;
1406        store
1407            .save_reduction_log(&rescue_id, &log)
1408            .map_err(operation)?;
1409        store
1410            .save(&rescue_id, &title, &view_jsonl)
1411            .map_err(operation)?;
1412
1413        let source_bytes = serde_json::to_vec(&session.messages)
1414            .map_err(|error| ServiceError::Operation(error.to_string()))?
1415            .len() as u64;
1416        let reduced_bytes = serde_json::to_vec(&view)
1417            .map_err(|error| ServiceError::Operation(error.to_string()))?
1418            .len() as u64;
1419        store
1420            .set_reduction_stats(
1421                &rescue_id,
1422                &title,
1423                source_bytes,
1424                reduced_bytes,
1425                log.reductions.len() as u32,
1426            )
1427            .map_err(operation)?;
1428
1429        // The receipt is issued only after a real disk reload. This proves
1430        // the exact files another process will consume, not the convenient
1431        // in-memory values that produced them.
1432        let reloaded_sidecar = store
1433            .load_sidecar(&rescue_id)
1434            .map_err(operation)?
1435            .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1436        let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1437        let reloaded_log = store
1438            .load_reduction_log(&rescue_id)
1439            .map_err(operation)?
1440            .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1441        let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1442        reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1443        // `sc.reduction` is deliberately in-memory-only metadata: it must
1444        // never leak onto a provider-facing transcript. Reapplying the
1445        // durable log to the durable sidecar restores those ids. Comparing
1446        // its wire form with the transcript reloaded above proves that the
1447        // persisted view is exactly the deterministic projection before we
1448        // use the restamped form for inversion.
1449        let (restamped_view, restamped_log) =
1450            reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1451        if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1452            return Err(ServiceError::Operation(
1453                "persisted reduction view does not match its durable log and sidecar".into(),
1454            ));
1455        }
1456        if restamped_log != reloaded_log {
1457            return Err(ServiceError::Operation(
1458                "reapplying the durable reduction log changed its identity".into(),
1459            ));
1460        }
1461        let inverted =
1462            reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1463        if inverted != session.messages {
1464            return Err(ServiceError::Operation(
1465                "reduction inversion did not restore the source messages byte-exactly".into(),
1466            ));
1467        }
1468
1469        let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1470        let sidecar_path = store.sidecar_path(&rescue_id);
1471        let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1472        let bootstrap_prompt = reduced_bootstrap_prompt(
1473            &params.locator,
1474            params.target_harness,
1475            &view_jsonl,
1476            &sidecar_path,
1477            &reduction_log_path,
1478        );
1479        let mut reduced_session = session.clone();
1480        reduced_session.meta.session_id = Some(rescue_id.clone());
1481        reduced_session.messages = view;
1482
1483        Ok(json!({
1484            "session": normalized_session_json(&reduced_session),
1485            "bootstrap_prompt": bootstrap_prompt,
1486            "receipt": {
1487                "id": rescue_id,
1488                "sidecar_id": rescue_id,
1489                "source_harness": params.locator.harness,
1490                "target_harness": params.target_harness.id(),
1491                "source_tokens": source_tokens,
1492                "reduced_tokens": reduced_tokens,
1493                "ratio": ratio,
1494                "source_bytes": source_bytes,
1495                "reduced_bytes": reduced_bytes,
1496                "reductions": reloaded_log.reductions.len(),
1497                "sidecar_path": sidecar_path,
1498                "reduction_log_path": reduction_log_path,
1499                "verified": true,
1500                "reversible": true,
1501            }
1502        }))
1503    }
1504
1505    /// Recognize the one request family whose waiting happens entirely
1506    /// outside this service's state, and hand a transport the half it can run
1507    /// off the task that owns the service.
1508    ///
1509    /// Opening a runtime is the only door here that waits on a foreign
1510    /// program: it spawns the harness's own binary and completes that
1511    /// program's protocol handshake, which takes as long as the program takes
1512    /// to answer. A transport that awaited the whole request inline would
1513    /// stop reading its own input for that whole time, so ONE slow launch
1514    /// would queue every later request on the same server — including reads
1515    /// like `sessions.discover` that touch no runtime at all. Splitting the
1516    /// request lets the transport spawn [`RuntimeOpen::open`] and keep
1517    /// reading, then pay only the short bookkeeping half
1518    /// ([`Self::register_open_runtime`]) when the runtime is up.
1519    ///
1520    /// `None` for every other method: those are answered by
1521    /// [`Self::handle_async`] as before.
1522    pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1523        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1524            return None;
1525        }
1526        let method = request.get("method").and_then(Value::as_str)?;
1527        if !RUNTIME_OPEN_METHODS.contains(&method) {
1528            return None;
1529        }
1530        Some(RuntimeOpen {
1531            id: request.get("id").cloned().unwrap_or(Value::Null),
1532            method: method.to_string(),
1533            params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1534        })
1535    }
1536
1537    /// Recognize a [`DETACHED_METHODS`] request and hand a transport the
1538    /// whole of it: the service-state half is read here and now, and what
1539    /// remains waits on a foreign program with nothing of this service's in
1540    /// hand.
1541    ///
1542    /// Same reason as [`Self::runtime_open`], different doors. Probing a
1543    /// harness starts it and completes its handshake; couriering a message
1544    /// runs a `claude` process to completion; a conversation verb runs the
1545    /// harness's own CLI or calls its HTTP API. A transport that awaited any
1546    /// of those inline would stop reading its own input for that whole time,
1547    /// so one probe of an unhealthy harness would queue every later request
1548    /// on the same server.
1549    ///
1550    /// Unlike an opening runtime there is no bookkeeping half: the answer
1551    /// [`DetachedCall::run`] produces is the caller's complete response, so a
1552    /// transport writes it without coming back here.
1553    ///
1554    /// `None` for every other method — including the LIVE `sessions.new` /
1555    /// `sessions.reset` door and `runtimes.close`, which wait on a runtime
1556    /// connection this service owns and so are split off by
1557    /// [`Self::detach_runtime`] instead.
1558    pub fn detach(&self, request: &Value) -> Option<DetachedCall> {
1559        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1560            return None;
1561        }
1562        let method = request.get("method").and_then(Value::as_str)?;
1563        if !DETACHED_METHODS.contains(&method) {
1564            return None;
1565        }
1566        let id = request.get("id").cloned().unwrap_or(Value::Null);
1567        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1568        let work = match method {
1569            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe" => self
1570                .inventory_work(method, params)
1571                .map(DetachedWork::Inventory),
1572            "harness.v1.sessions.message" => {
1573                decode::<MessageSessionParams>(params).map(DetachedWork::Message)
1574            }
1575            _ => {
1576                let verb = match method {
1577                    "harness.v1.sessions.new" => crate::SessionVerb::New,
1578                    "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
1579                    "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
1580                    _ => crate::SessionVerb::Delete,
1581                };
1582                match decode::<crate::SessionMutation>(params) {
1583                    Ok(mutation) => {
1584                        match crate::sessions_control::door(&mutation.harness, verb) {
1585                            // The live door needs the open runtime connection
1586                            // this service owns; it stays inline.
1587                            Ok(crate::SessionDoor::Live(_)) => return None,
1588                            Ok(_) => Ok(DetachedWork::SessionMutation { verb, mutation }),
1589                            Err(error) => Err(session_control_error(error)),
1590                        }
1591                    }
1592                    Err(error) => Err(error),
1593                }
1594            }
1595        };
1596        Some(DetachedCall {
1597            id,
1598            method: method.to_string(),
1599            work: work.map(Work::Free),
1600        })
1601    }
1602
1603    /// Recognize the two doors that wait on a runtime THIS SERVICE OWNS, and
1604    /// hand a transport the whole of each by lending the connection out.
1605    ///
1606    /// `runtimes.close` surrenders its runtime for good; the LIVE
1607    /// `sessions.new` / `sessions.reset` door borrows one for the length of
1608    /// the slash command and gives it back through
1609    /// [`Self::finish_detached`]. Both are bounded by
1610    /// [`RUNTIME_CONTROL_DEADLINE`], and a wedged runtime spends all of it —
1611    /// which is exactly as long as a transport that awaited them inline would
1612    /// stop reading its own input.
1613    ///
1614    /// `None` for every other method, and for the `sessions.new` /
1615    /// `sessions.reset` doors that are not live: [`Self::detach`] owns those.
1616    pub fn detach_runtime(&mut self, request: &Value) -> Option<DetachedCall> {
1617        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1618            return None;
1619        }
1620        let method = request.get("method").and_then(Value::as_str)?;
1621        let id = request.get("id").cloned().unwrap_or(Value::Null);
1622        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1623        let work = match method {
1624            "harness.v1.runtimes.close" => decode::<RuntimeConnectionParams>(params)
1625                .and_then(|params| self.surrender_runtime(&params.connection))
1626                .map(|(runtime, process_group)| {
1627                    Work::Runtime(RuntimeWork::Close {
1628                        runtime,
1629                        process_group,
1630                    })
1631                }),
1632            "harness.v1.sessions.new" | "harness.v1.sessions.reset" => {
1633                let verb = if method == "harness.v1.sessions.new" {
1634                    crate::SessionVerb::New
1635                } else {
1636                    crate::SessionVerb::Reset
1637                };
1638                let mutation = decode::<crate::SessionMutation>(params).ok()?;
1639                // Everything but the live door — including a refusal and a
1640                // request naming no connection — is `detach`'s or
1641                // `handle_async`'s to answer.
1642                let Ok(crate::SessionDoor::Live(command)) =
1643                    crate::sessions_control::door(&mutation.harness, verb)
1644                else {
1645                    return None;
1646                };
1647                let connection = mutation
1648                    .connection
1649                    .clone()
1650                    .filter(|value| !value.trim().is_empty())?;
1651                self.lend_runtime(&connection).map(|runtime| {
1652                    let session = live_session_name(runtime.as_ref(), &mutation);
1653                    Work::Runtime(RuntimeWork::LiveCommand {
1654                        connection,
1655                        runtime,
1656                        verb,
1657                        mutation,
1658                        command,
1659                        session,
1660                    })
1661                })
1662            }
1663            _ => return None,
1664        };
1665        Some(DetachedCall {
1666            id,
1667            method: method.to_string(),
1668            work,
1669        })
1670    }
1671
1672    /// Take back whatever a detached call borrowed and hand over the caller's
1673    /// response. Every answer from [`DetachedCall::run`] comes through here,
1674    /// so a lent-out connection is back in the service before the response
1675    /// that used it is written.
1676    pub fn finish_detached(&mut self, answer: DetachedAnswer) -> Value {
1677        let DetachedAnswer { response, returned } = answer;
1678        if let Some(ReturnedRuntime {
1679            connection,
1680            runtime,
1681        }) = returned
1682        {
1683            self.runtimes_in_flight.remove(&connection);
1684            self.runtimes.insert(connection, runtime);
1685        }
1686        response
1687    }
1688
1689    /// Answer a request split out by [`Self::runtime_open`] and already
1690    /// awaited by [`RuntimeOpen::open`]: register the runtime this service now
1691    /// owns and build its JSON-RPC response.
1692    pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1693        let OpenedRuntime { id, outcome } = opened;
1694        let result = match outcome {
1695            Ok(open) => self.register_open_runtime(open).await,
1696            Err(error) => Err(error),
1697        };
1698        service_response(id, result)
1699    }
1700
1701    /// Take ownership of an opened runtime.
1702    async fn register_open_runtime(
1703        &mut self,
1704        open: OpenRuntime,
1705    ) -> std::result::Result<Value, ServiceError> {
1706        match open {
1707            OpenRuntime::Hosted {
1708                runtime,
1709                capabilities,
1710                workspace,
1711            } => {
1712                self.insert_hosted_runtime(runtime, capabilities, workspace)
1713                    .await
1714            }
1715            OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1716        }
1717    }
1718
1719    async fn runtime_call(
1720        &mut self,
1721        method: &str,
1722        params: Value,
1723    ) -> std::result::Result<Value, ServiceError> {
1724        match method {
1725            "harness.v1.runtimes.capabilities" => {
1726                let params = decode::<RuntimeBackendParams>(params)?;
1727                let backend = runtime_backend(&params)?;
1728                Ok(json!({
1729                    "harness": backend.harness(),
1730                    "capabilities": backend.capabilities(),
1731                }))
1732            }
1733            method if RUNTIME_OPEN_METHODS.contains(&method) => {
1734                self.register_open_runtime(open_runtime(method, params).await?)
1735                    .await
1736            }
1737            "harness.v1.runtimes.send_input" => {
1738                let params = decode::<RuntimeInputParams>(params)?;
1739                let image_urls = validate_runtime_image_urls(params.image_urls)?;
1740                let runtime = self.runtime_mut(&params.connection)?;
1741                let turn_id = within_control_deadline(
1742                    method,
1743                    runtime.send_input(RuntimeInput {
1744                        text: params.text,
1745                        image_urls,
1746                    }),
1747                )
1748                .await?
1749                .map_err(operation)?;
1750                Ok(json!({"turn_id": turn_id}))
1751            }
1752            "harness.v1.runtimes.interrupt" => {
1753                let params = decode::<RuntimeConnectionParams>(params)?;
1754                within_control_deadline(method, self.runtime_mut(&params.connection)?.interrupt())
1755                    .await?
1756                    .map_err(operation)?;
1757                Ok(json!({}))
1758            }
1759            "harness.v1.runtimes.steer" => {
1760                let params = decode::<RuntimeInputParams>(params)?;
1761                if !params.image_urls.is_empty() {
1762                    return Err(ServiceError::InvalidParams(
1763                        "runtime steering accepts text only".into(),
1764                    ));
1765                }
1766                let text = params.text.trim();
1767                if text.is_empty() || text.chars().count() > 50_000 {
1768                    return Err(ServiceError::InvalidParams(
1769                        "runtime steering requires 1 to 50,000 text characters".into(),
1770                    ));
1771                }
1772                within_control_deadline(
1773                    method,
1774                    self.runtime_mut(&params.connection)?
1775                        .steer(text.to_string()),
1776                )
1777                .await?
1778                .map_err(operation)?;
1779                Ok(json!({}))
1780            }
1781            "harness.v1.runtimes.respond" => {
1782                let params = decode::<RuntimeRespondParams>(params)?;
1783                let request_id = params.request_id.clone();
1784                within_control_deadline(
1785                    method,
1786                    self.runtime_mut(&params.connection)?
1787                        .respond(params.request_id, params.response),
1788                )
1789                .await?
1790                .map_err(operation)?;
1791                // ORCH-9: an answered request is no longer waiting for one.
1792                self.approvals.answered(&params.connection, &request_id);
1793                Ok(json!({}))
1794            }
1795            "harness.v1.runtimes.acquire_control" => {
1796                let params = decode::<RuntimeConnectionParams>(params)?;
1797                let snapshot = within_control_deadline(
1798                    method,
1799                    self.runtime_mut(&params.connection)?.acquire_control(),
1800                )
1801                .await?
1802                .map_err(operation)?;
1803                serde_json::to_value(snapshot)
1804                    .map_err(|error| ServiceError::Operation(error.to_string()))
1805            }
1806            "harness.v1.runtimes.heartbeat" => {
1807                let params = decode::<RuntimeConnectionParams>(params)?;
1808                let snapshot = within_control_deadline(
1809                    method,
1810                    self.runtime_mut(&params.connection)?.heartbeat(),
1811                )
1812                .await?
1813                .map_err(operation)?;
1814                serde_json::to_value(snapshot)
1815                    .map_err(|error| ServiceError::Operation(error.to_string()))
1816            }
1817            "harness.v1.runtimes.detach" => {
1818                let params = decode::<RuntimeConnectionParams>(params)?;
1819                let snapshot =
1820                    within_control_deadline(method, self.runtime_mut(&params.connection)?.detach())
1821                        .await?
1822                        .map_err(operation)?;
1823                serde_json::to_value(snapshot)
1824                    .map_err(|error| ServiceError::Operation(error.to_string()))
1825            }
1826            "harness.v1.runtimes.terminal_instructions" => {
1827                let params = decode::<RuntimeConnectionParams>(params)?;
1828                let launch = self
1829                    .terminal_launches
1830                    .get(&params.connection)
1831                    .ok_or_else(|| {
1832                        ServiceError::Operation(
1833                            "this runtime is not hosted for terminal attachment".into(),
1834                        )
1835                    })?;
1836                Ok(json!({"launch":launch}))
1837            }
1838            "harness.v1.runtimes.close" => {
1839                let params = decode::<RuntimeConnectionParams>(params)?;
1840                let (runtime, process_group) = self.surrender_runtime(&params.connection)?;
1841                close_runtime(runtime, process_group).await
1842            }
1843            _ => Err(ServiceError::MethodNotFound),
1844        }
1845    }
1846
1847    /// Deliver one message into a session that is running right now.
1848    #[cfg(feature = "adapter-api")]
1849    async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1850        let params = decode::<MessageSessionParams>(params)?;
1851        Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
1852    }
1853
1854    #[cfg(feature = "adapter-api")]
1855    fn harness_settings_call(
1856        &self,
1857        method: &str,
1858        params: Value,
1859    ) -> std::result::Result<Value, ServiceError> {
1860        let homes = crate::HarnessHomes::default();
1861        match method {
1862            "harness.v1.harnesses.settings" => {
1863                let params = decode::<HarnessSettingsParams>(params)?;
1864                let report = crate::inspect_harness_interop_settings(&homes, &params.harness)
1865                    .map_err(|error| ServiceError::Operation(error.to_string()))?;
1866                serde_json::to_value(report)
1867                    .map_err(|error| ServiceError::Operation(error.to_string()))
1868            }
1869            "harness.v1.harnesses.configure" => {
1870                let params = decode::<ConfigureHarnessParams>(params)?;
1871                let report = crate::configure_harness_interop_settings(
1872                    &homes,
1873                    &params.harness,
1874                    &params.changes,
1875                    params.expected_revision.as_deref(),
1876                )
1877                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1878                serde_json::to_value(report)
1879                    .map_err(|error| ServiceError::Operation(error.to_string()))
1880            }
1881            _ => Err(ServiceError::MethodNotFound),
1882        }
1883    }
1884
1885    fn insert_runtime(
1886        &mut self,
1887        runtime: Box<dyn RuntimeConnection>,
1888    ) -> std::result::Result<Value, ServiceError> {
1889        let connection = format!("runtime-{}", self.next_runtime);
1890        self.next_runtime += 1;
1891        let handle = runtime.handle().clone();
1892        self.runtime_sequences
1893            .entry(handle.runtime_id.clone())
1894            .or_insert(0);
1895        self.runtimes.insert(connection.clone(), runtime);
1896        Ok(json!({"connection": connection, "handle": handle}))
1897    }
1898
1899    #[cfg(feature = "adapter-api")]
1900    async fn insert_hosted_runtime(
1901        &mut self,
1902        runtime: Box<dyn RuntimeConnection>,
1903        capabilities: crate::RuntimeCapabilities,
1904        workspace: PathBuf,
1905    ) -> std::result::Result<Value, ServiceError> {
1906        let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1907        let token: std::sync::Arc<str> = crate::server::generate_token().into();
1908        let server = crate::server::run_frontend_http(
1909            host.clone(),
1910            host.frontend_sender(),
1911            "127.0.0.1:0",
1912            token.clone(),
1913            connection.handle().runtime_id.clone(),
1914        )
1915        .await
1916        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1917        let source = LiveRuntimeSource {
1918            harness: connection.handle().harness.as_str().to_string(),
1919            session_id: connection.handle().runtime_id.clone(),
1920            workspace: workspace.clone(),
1921        };
1922        let registration = register_live_runtime(
1923            connection.handle().runtime_id.clone(),
1924            source.clone(),
1925            format!("http://{}", server.address()),
1926            token.to_string(),
1927        )
1928        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1929        let endpoint = registration.endpoint().to_string();
1930        let launch = StructuredLaunch {
1931            cwd: workspace,
1932            // Pin attachment to the executable hosting this runtime. A bare
1933            // `supercode` could resolve to an older global install whose CLI
1934            // does not understand the receipt it is being asked to open.
1935            program: std::env::current_exe()
1936                .ok()
1937                .map(|path| path.to_string_lossy().into_owned())
1938                .unwrap_or_else(|| "supercode".into()),
1939            arguments: vec![
1940                "harness".into(),
1941                "attach".into(),
1942                "--endpoint".into(),
1943                endpoint,
1944                "--harness".into(),
1945                source.harness,
1946                "--session".into(),
1947                source.session_id,
1948            ],
1949            env: BTreeMap::new(),
1950        };
1951        let lease = HostedRuntimeLease {
1952            connection,
1953            _host: host,
1954            _registration: registration,
1955            _server: server,
1956        };
1957        let opened = self.insert_runtime(Box::new(lease))?;
1958        let connection_id = opened["connection"]
1959            .as_str()
1960            .expect("insert_runtime returns a connection id")
1961            .to_string();
1962        self.terminal_launches.insert(connection_id, launch);
1963        Ok(opened)
1964    }
1965
1966    #[cfg(not(feature = "adapter-api"))]
1967    async fn insert_hosted_runtime(
1968        &mut self,
1969        runtime: Box<dyn RuntimeConnection>,
1970        _capabilities: crate::RuntimeCapabilities,
1971        _workspace: PathBuf,
1972    ) -> std::result::Result<Value, ServiceError> {
1973        self.insert_runtime(runtime)
1974    }
1975
1976    fn runtime_mut(
1977        &mut self,
1978        connection: &str,
1979    ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1980        if self.runtimes_in_flight.contains(connection) {
1981            return Err(self.lent_out(connection));
1982        }
1983        self.runtimes.get_mut(connection).ok_or_else(|| {
1984            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1985        })
1986    }
1987
1988    /// What a caller is told about a connection that is out on a detached
1989    /// call. It is not gone and it is not free: it is mid-call, which is the
1990    /// same answer the runtime itself gives a second turn.
1991    fn lent_out(&self, connection: &str) -> ServiceError {
1992        ServiceError::Operation(format!(
1993            "runtime connection `{connection}`: a harness turn is already in progress"
1994        ))
1995    }
1996
1997    /// Take a runtime OUT of the service for the duration of one detached
1998    /// call, leaving its name marked as lent out.
1999    fn lend_runtime(
2000        &mut self,
2001        connection: &str,
2002    ) -> std::result::Result<Box<dyn RuntimeConnection>, ServiceError> {
2003        if self.runtimes_in_flight.contains(connection) {
2004            return Err(self.lent_out(connection));
2005        }
2006        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2007            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2008        })?;
2009        self.runtimes_in_flight.insert(connection.to_string());
2010        Ok(runtime)
2011    }
2012
2013    /// Surrender a runtime for good: the connection and everything the
2014    /// service hung off it are gone before its teardown is even attempted.
2015    ///
2016    /// `close` is what a caller reaches for when a runtime has stopped
2017    /// answering, and a runtime that has stopped answering is exactly the one
2018    /// whose graceful close cannot complete: a hosted runtime's own loop
2019    /// parks on the call the runtime never answered, so it never dequeues the
2020    /// shutdown either. Keeping the entry until teardown succeeded made a
2021    /// wedged runtime permanent — every later call on that connection, and
2022    /// every new turn, answered "a harness turn is already in progress" with
2023    /// no way to take the connection back.
2024    fn surrender_runtime(
2025        &mut self,
2026        connection: &str,
2027    ) -> std::result::Result<(Box<dyn RuntimeConnection>, Option<u32>), ServiceError> {
2028        if self.runtimes_in_flight.contains(connection) {
2029            return Err(self.lent_out(connection));
2030        }
2031        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2032            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2033        })?;
2034        let process_group = runtime_process_group(runtime.handle());
2035        let runtime_id = runtime.handle().runtime_id.clone();
2036        self.terminal_launches.remove(connection);
2037        self.runtime_sequences.remove(&runtime_id);
2038        self.approvals.forget(connection);
2039        Ok((runtime, process_group))
2040    }
2041
2042    /// SIGKILL the process group of every runtime this service owns, without
2043    /// waiting on any of them.
2044    ///
2045    /// A host leaving for good calls this BEFORE dropping the service. The
2046    /// handle this service holds is not the runtime's connection: a hosted
2047    /// runtime's real transport lives in the task driving it, so neither
2048    /// exiting the process nor dropping these handles reaches the harness
2049    /// process — while dropping them does remove each runtime's live-runtime
2050    /// receipt. Signalling first is what keeps a removed receipt from
2051    /// advertising a harness that is still running.
2052    pub fn kill_all_runtime_groups(&self) -> usize {
2053        self.runtimes
2054            .values()
2055            .filter(|runtime| kill_runtime_process_group(runtime_process_group(runtime.handle())))
2056            .count()
2057    }
2058
2059    /// ORCH-19: run one conversation-lifecycle verb through the harness's own
2060    /// door.
2061    ///
2062    /// Two doors, one shape. A CLI / HTTP / own-store door is self-contained
2063    /// in [`crate::sessions_control`]. A LIVE door (Hermes's and OpenClaw's
2064    /// `/new` and `/reset`, which are slash commands their gateway interprets
2065    /// INSIDE a session) is performed here, because only the service owns the
2066    /// open runtime connection — the command is typed through the very same
2067    /// `send_input` path a human's message takes, so supercode invents no
2068    /// private channel.
2069    async fn mutate_session(
2070        &mut self,
2071        verb: crate::SessionVerb,
2072        params: Value,
2073    ) -> std::result::Result<Value, ServiceError> {
2074        let mutation = decode::<crate::SessionMutation>(params)?;
2075        let door = crate::sessions_control::door(&mutation.harness, verb)
2076            .map_err(session_control_error)?;
2077        let outcome = match door {
2078            // The live door types the slash command through an open hosted
2079            // runtime, which only exists with the `adapter-api` feature; the
2080            // CLI / HTTP / own-store doors below need nothing extra.
2081            #[cfg(not(feature = "adapter-api"))]
2082            crate::SessionDoor::Live(command) => {
2083                return Err(ServiceError::Operation(format!(
2084                    "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
2085                     session, which needs this build's `adapter-api` feature",
2086                    mutation.harness,
2087                    verb.as_str()
2088                )));
2089            }
2090            #[cfg(feature = "adapter-api")]
2091            crate::SessionDoor::Live(command) => {
2092                let connection = mutation
2093                    .connection
2094                    .clone()
2095                    .filter(|value| !value.trim().is_empty())
2096                    .ok_or_else(|| {
2097                        ServiceError::InvalidParams(format!(
2098                            "`{}` performs `sessions.{}` by typing `{command}` into a live \
2099                             driven session: pass the `connection` of an open runtime \
2100                             (`harness.v1.runtimes.start`)",
2101                            mutation.harness,
2102                            verb.as_str()
2103                        ))
2104                    })?;
2105                let runtime = self.runtime_mut(&connection)?;
2106                let session = live_session_name(runtime.as_ref(), &mutation);
2107                // Typing into a live session is a control call on an open
2108                // runtime, and a wedged runtime never accepts one, so it is
2109                // bounded exactly like the other control verbs. A transport
2110                // with a loop of its own lends the connection out instead of
2111                // waiting here: see [`Self::detach_runtime`].
2112                return type_live_command(runtime.as_mut(), verb, &mutation, command, session)
2113                    .await;
2114            }
2115            _ => run_session_mutation(verb, &mutation).await?,
2116        };
2117        serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
2118    }
2119
2120    /// Answer an inventory request whole, for callers that have nowhere to
2121    /// put the waiting half. A transport with a loop of its own splits it
2122    /// instead: see [`Self::detach`].
2123    async fn inventory_call(
2124        &self,
2125        method: &str,
2126        params: Value,
2127    ) -> std::result::Result<Value, ServiceError> {
2128        run_inventory(self.inventory_work(method, params)?).await
2129    }
2130
2131    /// The half of an inventory request that reads this service's state:
2132    /// resolve the selection and count the persisted sessions each row
2133    /// reports. What remains — finding executables, asking them their
2134    /// version, and (at `probe: handshake`) starting each harness and
2135    /// completing its protocol handshake — touches no service state at all.
2136    fn inventory_work(
2137        &self,
2138        method: &str,
2139        params: Value,
2140    ) -> std::result::Result<InventoryWork, ServiceError> {
2141        let mut params = decode::<HarnessInventoryParams>(params)?;
2142        if method == "harness.v1.harnesses.probe" {
2143            let harness = params.harness.take().ok_or_else(|| {
2144                ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
2145            })?;
2146            params.harnesses = vec![harness];
2147        }
2148        let selected = params
2149            .harnesses
2150            .iter()
2151            .map(HarnessId::as_str)
2152            .collect::<std::collections::BTreeSet<_>>();
2153        let supported = harness_support_registry()
2154            .harnesses
2155            .into_iter()
2156            .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
2157            .collect::<Vec<_>>();
2158        if !params.harnesses.is_empty() && supported.len() != selected.len() {
2159            let known = supported
2160                .iter()
2161                .map(|harness| harness.id.as_str())
2162                .collect::<std::collections::BTreeSet<_>>();
2163            let missing = params
2164                .harnesses
2165                .iter()
2166                .filter(|id| !known.contains(id.as_str()))
2167                .map(HarnessId::as_str)
2168                .collect::<Vec<_>>();
2169            return Err(ServiceError::InvalidParams(format!(
2170                "unknown harness(es): {}",
2171                missing.join(", ")
2172            )));
2173        }
2174        let global_counts = params
2175            .include_sessions
2176            .then(|| self.session_counts(None, &params.harnesses));
2177        let workspace_counts = params
2178            .include_sessions
2179            .then(|| {
2180                params
2181                    .workspace
2182                    .as_deref()
2183                    .map(|workspace| self.session_counts(Some(workspace), &params.harnesses))
2184            })
2185            .flatten();
2186        Ok(InventoryWork {
2187            params,
2188            supported,
2189            global_counts,
2190            workspace_counts,
2191        })
2192    }
2193
2194    #[cfg(feature = "adapter-api")]
2195    async fn harness_authentication_call(
2196        &self,
2197        method: &str,
2198        params: Value,
2199    ) -> std::result::Result<Value, ServiceError> {
2200        match method {
2201            "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
2202                let params = decode::<HarnessAuthenticationParams>(params)?;
2203                serde_json::to_value(crate::inspect_harness_authentication(&params.harness).await)
2204                    .map_err(|error| ServiceError::Operation(error.to_string()))
2205            }
2206            "harness.v1.harnesses.auth.begin" => {
2207                let params = decode::<BeginHarnessAuthenticationParams>(params)?;
2208                let cwd = params
2209                    .cwd
2210                    .or_else(|| std::env::current_dir().ok())
2211                    .unwrap_or_else(|| PathBuf::from("."));
2212                let plan = crate::harness_authentication_plan(
2213                    &params.harness,
2214                    params.environment,
2215                    params.method,
2216                    &cwd,
2217                )
2218                .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
2219                serde_json::to_value(plan)
2220                    .map_err(|error| ServiceError::Operation(error.to_string()))
2221            }
2222            _ => Err(ServiceError::MethodNotFound),
2223        }
2224    }
2225
2226    fn session_counts(
2227        &self,
2228        workspace: Option<&Path>,
2229        harnesses: &[HarnessId],
2230    ) -> BTreeMap<String, usize> {
2231        let mut counts = BTreeMap::new();
2232        for session in self
2233            .catalog
2234            .discover(&DiscoveryQuery {
2235                workspace: workspace.map(Path::to_path_buf),
2236                harnesses: harnesses.to_vec(),
2237                ..DiscoveryQuery::default()
2238            })
2239            .unwrap_or_default()
2240        {
2241            *counts
2242                .entry(session.locator.harness.as_str().to_string())
2243                .or_insert(0) += 1;
2244        }
2245        counts
2246    }
2247}
2248
2249#[async_trait::async_trait]
2250impl SdkService for HarnessSessionService {
2251    fn capabilities(&self) -> SdkCapabilities {
2252        SdkCapabilities::default()
2253    }
2254
2255    async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2256        if request.operation == SdkOperation::Events {
2257            let events = self
2258                .poll_sdk_events()
2259                .await
2260                .into_iter()
2261                .map(|(_, event)| event)
2262                .collect::<Vec<_>>();
2263            return serde_json::to_value(events).map_err(|error| {
2264                SdkError::new(
2265                    SdkErrorCode::Execution,
2266                    request.operation,
2267                    error.to_string(),
2268                )
2269            });
2270        }
2271        if self.runtimes.is_empty()
2272            && matches!(
2273                request.operation,
2274                SdkOperation::Input
2275                    | SdkOperation::Interrupt
2276                    | SdkOperation::Steer
2277                    | SdkOperation::Respond
2278                    | SdkOperation::Close
2279            )
2280        {
2281            return Err(SdkError::unsupported(request.operation));
2282        }
2283        let method = request
2284            .operation
2285            .method()
2286            .ok_or_else(|| SdkError::unsupported(request.operation))?;
2287        let result = match request.operation {
2288            SdkOperation::Discover
2289            | SdkOperation::Load
2290            | SdkOperation::Export
2291            | SdkOperation::ProfilesList
2292            | SdkOperation::ProfilesGet
2293            | SdkOperation::ProfilesCreate
2294            | SdkOperation::ProfilesDelete
2295            | SdkOperation::SkillsList
2296            | SdkOperation::SkillsInstall
2297            | SdkOperation::SkillsRemove
2298            | SdkOperation::ChannelsList
2299            | SdkOperation::RoutesList
2300            | SdkOperation::TriggersList
2301            | SdkOperation::ChannelsStatus
2302            | SdkOperation::MemoryShow
2303            | SdkOperation::MemorySearch
2304            | SdkOperation::JobsList
2305            | SdkOperation::JobsGet
2306            | SdkOperation::JobsCreate
2307            | SdkOperation::JobsUpdate
2308            | SdkOperation::JobsPause
2309            | SdkOperation::JobsResume
2310            | SdkOperation::JobsRun
2311            | SdkOperation::JobsDelete
2312            | SdkOperation::JobsNotepad
2313            | SdkOperation::JobsNotepadSet
2314            | SdkOperation::JobsNotepadDelete
2315            | SdkOperation::RunsList
2316            | SdkOperation::RunsGet
2317            | SdkOperation::ApprovalsList
2318            | SdkOperation::OrchestrationLoad
2319            | SdkOperation::OrchestrationSave
2320            | SdkOperation::OrchestrationCompile
2321            | SdkOperation::OrchestrationDecompile
2322            | SdkOperation::OrchestrationImport
2323            | SdkOperation::OrchestrationExport
2324            | SdkOperation::WorkflowLoad => self.call(method, request.params),
2325            // ORCH-20: answering needs the live connection, so it takes the
2326            // async door and ends in `harness.v1.runtimes.respond`.
2327            SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2328            SdkOperation::Start
2329            | SdkOperation::Resume
2330            | SdkOperation::Input
2331            | SdkOperation::Interrupt
2332            | SdkOperation::Steer
2333            | SdkOperation::Respond
2334            | SdkOperation::Close => self.runtime_call(method, request.params).await,
2335            // ORCH-19 controlled tier. Every verb goes through the HARNESS'S
2336            // OWN door — its CLI, its HTTP API, or its slash command typed
2337            // into a live driven session — and returns the row re-read from
2338            // the harness's store afterwards.
2339            SdkOperation::SessionsNew => {
2340                self.mutate_session(crate::SessionVerb::New, request.params)
2341                    .await
2342            }
2343            SdkOperation::SessionsReset => {
2344                self.mutate_session(crate::SessionVerb::Reset, request.params)
2345                    .await
2346            }
2347            SdkOperation::SessionsArchive => {
2348                self.mutate_session(crate::SessionVerb::Archive, request.params)
2349                    .await
2350            }
2351            SdkOperation::SessionsDelete => {
2352                self.mutate_session(crate::SessionVerb::Delete, request.params)
2353                    .await
2354            }
2355            SdkOperation::Events => unreachable!("handled before method dispatch"),
2356        };
2357        result.map_err(|error| sdk_error(request.operation, error))
2358    }
2359
2360    async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2361        Ok(self
2362            .poll_sdk_events()
2363            .await
2364            .into_iter()
2365            .map(|(_, event)| event)
2366            .collect())
2367    }
2368}
2369
2370#[cfg(feature = "adapter-api")]
2371struct HostedRuntimeLease {
2372    connection: HostedHarnessConnection,
2373    _host: std::sync::Arc<HostedHarnessRuntime>,
2374    _registration: LiveRuntimeRegistration,
2375    _server: crate::server::FrontendHttpServer,
2376}
2377
2378#[async_trait::async_trait]
2379#[cfg(feature = "adapter-api")]
2380impl RuntimeConnection for HostedRuntimeLease {
2381    fn handle(&self) -> &crate::RuntimeHandle {
2382        self.connection.handle()
2383    }
2384
2385    async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2386        self.connection.send_input(input).await
2387    }
2388
2389    async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2390        self.connection.next_event().await
2391    }
2392
2393    async fn interrupt(&mut self) -> crate::Result<()> {
2394        self.connection.interrupt().await
2395    }
2396
2397    // the lease must forward every verb its capabilities advertise; without
2398    // this, steer fell to the trait default and refused a turn it claimed
2399    async fn steer(&mut self, text: String) -> crate::Result<()> {
2400        self.connection.steer(text).await
2401    }
2402
2403    async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2404        self.connection.respond(request_id, response).await
2405    }
2406
2407    async fn close(&mut self) -> crate::Result<()> {
2408        self.connection.close().await
2409    }
2410}
2411
2412/// One inventory request's waiting half, already separated from the service
2413/// state it reads. See [`HarnessSessionService::inventory_work`].
2414struct InventoryWork {
2415    params: HarnessInventoryParams,
2416    supported: Vec<crate::HarnessSupportDescriptor>,
2417    global_counts: Option<BTreeMap<String, usize>>,
2418    workspace_counts: Option<BTreeMap<String, usize>>,
2419}
2420
2421/// Perform one conversation-lifecycle verb through a door that is
2422/// self-contained in [`crate::sessions_control`]: the harness's own CLI, its
2423/// HTTP API, the orchestrator daemon's socket, or supercode's own store.
2424/// Touches no service state, so this runs on any task. The LIVE door is not
2425/// here — it types its slash command through a runtime connection the service
2426/// owns, and is performed by [`HarnessSessionService::mutate_session`].
2427async fn run_session_mutation(
2428    verb: crate::SessionVerb,
2429    mutation: &crate::SessionMutation,
2430) -> std::result::Result<crate::SessionMutationOutcome, ServiceError> {
2431    // Only the HTTP door actually awaits anything. The CLI, store and daemon
2432    // doors run the harness's own program, or its store, with calls that
2433    // block the calling THREAD from start to finish — a future that never
2434    // yields, which no timeout around it can interrupt and which would hold a
2435    // runtime worker for as long as the harness takes. They go to a blocking
2436    // task, where blocking is what the thread is for.
2437    let door =
2438        crate::sessions_control::door(&mutation.harness, verb).map_err(session_control_error)?;
2439    if let crate::SessionDoor::Http = door {
2440        return crate::sessions_control::mutate(verb, mutation)
2441            .await
2442            .map_err(session_control_error);
2443    }
2444    let mutation = mutation.clone();
2445    tokio::task::spawn_blocking(move || crate::sessions_control::mutate_blocking(verb, &mutation))
2446        .await
2447        .map_err(|error| {
2448            ServiceError::Operation(format!("the conversation verb could not be run: {error}"))
2449        })?
2450        .map_err(session_control_error)
2451}
2452
2453/// Probe every selected harness and assemble the report. Touches no service
2454/// state, so this runs on any task.
2455async fn run_inventory(work: InventoryWork) -> std::result::Result<Value, ServiceError> {
2456    let InventoryWork {
2457        params,
2458        supported,
2459        global_counts,
2460        workspace_counts,
2461    } = work;
2462    let probes = supported.into_iter().map(|descriptor| {
2463        let global = global_counts
2464            .as_ref()
2465            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2466        let workspace = workspace_counts
2467            .as_ref()
2468            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2469        probe_harness(descriptor, &params, global, workspace)
2470    });
2471    let harnesses = futures::future::join_all(probes).await;
2472    serde_json::to_value(HarnessInventoryReport {
2473        probe: params.probe,
2474        workspace: params.workspace,
2475        harnesses,
2476    })
2477    .map_err(|error| ServiceError::Operation(error.to_string()))
2478}
2479
2480async fn probe_harness(
2481    descriptor: crate::HarnessSupportDescriptor,
2482    params: &HarnessInventoryParams,
2483    global: Option<usize>,
2484    workspace: Option<usize>,
2485) -> LocalHarness {
2486    let launch = descriptor.runtime.default_launch.as_ref();
2487    // ORC-7: the orchestrator publishes no runtime launch — it is not an
2488    // adapter supercode connects a turn to. What "installed" means for it
2489    // is that its Node daemon entry is present, so the row answers from
2490    // that instead of from a PATH lookup it could never satisfy.
2491    let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
2492        .then(crate::orchestrator::daemon_entry)
2493        .and_then(Result::ok);
2494    let executable = match &orchestrator_entry {
2495        Some(entry) => Some(entry.clone()),
2496        None => launch.and_then(|launch| find_executable(&launch.program)),
2497    };
2498    let installed = executable.is_some();
2499    let version = if params.skip_versions || orchestrator_entry.is_some() {
2500        // The orchestrator's "executable" is a Node module, not a CLI
2501        // with a `--version` flag; running it to ask would start a daemon.
2502        None
2503    } else {
2504        match executable.as_deref() {
2505            Some(path) => executable_version(path).await,
2506            None => None,
2507        }
2508    };
2509    let configured = auth_evidence(descriptor.id.as_str());
2510    let mut auth = if configured {
2511        HarnessAuthState::Configured
2512    } else if matches!(
2513        descriptor.id.as_str(),
2514        HarnessId::CLAUDE_CODE | HarnessId::CODEX
2515    ) {
2516        // These two adapters have explicit native status/login contracts
2517        // and complete local evidence coverage (including Claude's macOS
2518        // Keychain-backed oauthAccount marker). Treating absent evidence
2519        // as unknown advertises a start that will only fail interactively.
2520        HarnessAuthState::Required
2521    } else {
2522        HarnessAuthState::Unknown
2523    };
2524    let mut runtime = if installed {
2525        HarnessRuntimeState::Degraded
2526    } else {
2527        HarnessRuntimeState::Unavailable
2528    };
2529    let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2530    let mut reason = (!installed).then(|| {
2531        if is_orchestrator {
2532            format!(
2533                "{} is supported but its daemon entry `{}` was not found",
2534                descriptor.display_name,
2535                crate::orchestrator::DAEMON_ENTRY
2536            )
2537        } else {
2538            format!(
2539                "{} is supported but `{}` was not found on PATH",
2540                descriptor.display_name,
2541                launch
2542                    .map(|launch| launch.program.as_str())
2543                    .unwrap_or("executable")
2544            )
2545        }
2546    });
2547    let mut repair = (!installed).then(|| {
2548        if is_orchestrator {
2549            format!(
2550                "Install the `supercode-orchestrator` package so `{}` resolves.",
2551                crate::orchestrator::DAEMON_ENTRY
2552            )
2553        } else {
2554            format!(
2555                "Install {} and ensure `{}` is on PATH.",
2556                descriptor.display_name,
2557                launch
2558                    .map(|launch| launch.program.as_str())
2559                    .unwrap_or("its executable")
2560            )
2561        }
2562    });
2563
2564    if installed && params.probe == HarnessProbeLevel::Handshake {
2565        let backend_params = RuntimeBackendParams {
2566            harness: descriptor.id.clone(),
2567            protocol: None,
2568            launch: None,
2569            base_url: None,
2570            policy: RuntimePolicy::Default,
2571        };
2572        match runtime_backend(&backend_params) {
2573            Ok(backend) => {
2574                let cwd = params
2575                    .workspace
2576                    .clone()
2577                    .or_else(|| std::env::current_dir().ok())
2578                    .unwrap_or_else(|| PathBuf::from("."));
2579                let isolated = descriptor
2580                    .runtime
2581                    .default_launch
2582                    .clone()
2583                    .and_then(|launch| IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok());
2584                let Some(isolated) = isolated else {
2585                    reason = Some(
2586                        "No-prompt runtime handshake could not create its isolated harness home."
2587                            .into(),
2588                    );
2589                    repair = Some(
2590                        "Check temporary-directory permissions, then run the handshake probe again."
2591                            .into(),
2592                    );
2593                    let running = probe_running_instance(descriptor.id.as_str());
2594                    return LocalHarness {
2595                        gateway: gateway_health(
2596                            descriptor.id.as_str(),
2597                            installed,
2598                            running.as_ref(),
2599                            version.as_deref(),
2600                        ),
2601                        id: descriptor.id,
2602                        display_name: descriptor.display_name,
2603                        supported: true,
2604                        installed,
2605                        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2606                        version,
2607                        auth,
2608                        runtime,
2609                        protocol: descriptor.runtime.protocol,
2610                        capabilities: descriptor.runtime.capabilities.clone(),
2611                        effective_capabilities: descriptor.runtime.capabilities,
2612                        sessions: HarnessSessionCounts { global, workspace },
2613                        running,
2614                        reason,
2615                        repair,
2616                    };
2617                };
2618                match tokio::time::timeout(
2619                    Duration::from_secs(30),
2620                    backend.start(RuntimeStartRequest {
2621                        cwd,
2622                        launch: Some(isolated.launch.clone()),
2623                        mcp_servers: Vec::new(),
2624                    }),
2625                )
2626                .await
2627                {
2628                    Ok(Ok(mut connection)) => {
2629                        match stabilize_handshake(connection.as_mut()).await {
2630                            Ok(()) => {
2631                                auth = HarnessAuthState::Ready;
2632                                runtime = HarnessRuntimeState::Ready;
2633                                reason = Some(
2634                                    "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2635                                        .into(),
2636                                );
2637                                repair = None;
2638                            }
2639                            Err(message) => {
2640                                auth = if looks_like_auth_error(&message) {
2641                                    HarnessAuthState::Required
2642                                } else if configured {
2643                                    HarnessAuthState::Configured
2644                                } else {
2645                                    HarnessAuthState::Unknown
2646                                };
2647                                reason = Some(format!(
2648                                    "No-prompt runtime handshake became unhealthy during startup: {message}"
2649                                ));
2650                                repair = Some(if auth == HarnessAuthState::Required {
2651                                    format!(
2652                                        "Run `{}` interactively once and complete sign-in, then probe again.",
2653                                        launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2654                                    )
2655                                } else {
2656                                    "Run the harness directly to inspect its startup failure, then probe again."
2657                                        .into()
2658                                });
2659                            }
2660                        }
2661                        let _ =
2662                            tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2663                    }
2664                    Ok(Err(error)) => {
2665                        let message = truncate_text(&error.to_string(), 500);
2666                        auth = if looks_like_auth_error(&message) {
2667                            HarnessAuthState::Required
2668                        } else if configured {
2669                            HarnessAuthState::Configured
2670                        } else {
2671                            HarnessAuthState::Unknown
2672                        };
2673                        reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2674                        repair = Some(if auth == HarnessAuthState::Required {
2675                            format!(
2676                                "Run `{}` interactively once and complete sign-in, then probe again.",
2677                                launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2678                            )
2679                        } else {
2680                            "Check the harness installation and run the handshake probe again."
2681                                .into()
2682                        });
2683                    }
2684                    Err(_) => {
2685                        reason =
2686                            Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2687                        repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2688                    }
2689                }
2690                // Keep the isolated home alive through process teardown.
2691                // Otherwise the compiler may release the last meaningful
2692                // use after cloning `launch`, and a still-starting CLI can
2693                // recreate its state directory after Drop removed it.
2694                // Some Node-based launchers finish a short asynchronous
2695                // installation-id write just after their parent process
2696                // is reaped. Remove once immediately, allow that bounded
2697                // writer to settle, then perform the authoritative pass.
2698                let _ = isolated.cleanup();
2699                tokio::time::sleep(Duration::from_millis(250)).await;
2700                if let Err(error) = isolated.cleanup() {
2701                    auth = if configured {
2702                        HarnessAuthState::Configured
2703                    } else {
2704                        HarnessAuthState::Unknown
2705                    };
2706                    runtime = HarnessRuntimeState::Degraded;
2707                    reason = Some(format!(
2708                        "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2709                    ));
2710                    repair = Some(
2711                        "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2712                            .into(),
2713                    );
2714                }
2715            }
2716            Err(error) => {
2717                reason = Some(error_message(error));
2718            }
2719        }
2720    } else if installed && configured {
2721        reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2722    } else if installed && auth == HarnessAuthState::Required {
2723        reason = Some("Executable found, but no native authentication evidence is present.".into());
2724        repair = Some(format!(
2725            "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2726            descriptor.id.as_str()
2727        ));
2728    } else if installed {
2729        reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2730        repair = Some(format!(
2731            "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2732            launch
2733                .map(|launch| launch.program.as_str())
2734                .unwrap_or("the harness")
2735        ));
2736    }
2737
2738    let effective_capabilities = if installed {
2739        descriptor.runtime.capabilities.clone()
2740    } else {
2741        unavailable_capabilities()
2742    };
2743    let running = probe_running_instance(descriptor.id.as_str());
2744    LocalHarness {
2745        gateway: gateway_health(
2746            descriptor.id.as_str(),
2747            installed,
2748            running.as_ref(),
2749            version.as_deref(),
2750        ),
2751        id: descriptor.id,
2752        display_name: descriptor.display_name,
2753        supported: true,
2754        installed,
2755        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2756        version,
2757        auth,
2758        runtime,
2759        protocol: descriptor.runtime.protocol,
2760        capabilities: descriptor.runtime.capabilities,
2761        effective_capabilities,
2762        sessions: HarnessSessionCounts { global, workspace },
2763        running,
2764        reason,
2765        repair,
2766    }
2767}
2768
2769async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2770    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2771    loop {
2772        let now = tokio::time::Instant::now();
2773        if now >= deadline {
2774            return Ok(());
2775        }
2776        match tokio::time::timeout(deadline - now, connection.next_event()).await {
2777            Err(_) => return Ok(()),
2778            Ok(Ok(Some(event))) => {
2779                if let Some(message) = handshake_event_failure(&event) {
2780                    return Err(truncate_text(&message, 500));
2781                }
2782            }
2783            Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2784            Ok(Err(error)) => return Err(error.to_string()),
2785        }
2786    }
2787}
2788
2789fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2790    let detail = event
2791        .payload
2792        .get("message")
2793        .or_else(|| event.payload.get("line"))
2794        .and_then(Value::as_str)
2795        .unwrap_or(event.kind.as_str());
2796    match event.kind.as_str() {
2797        "transport_closed" => Some("runtime transport closed during startup".into()),
2798        "transport_error" => Some(format!("runtime transport error: {detail}")),
2799        "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2800        // Stderr is retained as a runtime event, but is not transport health.
2801        // Grok, for example, can log an AuthorizationRequired error from an
2802        // optional background worker while its ACP session continues to send
2803        // updates and complete prompts normally.
2804        _ => None,
2805    }
2806}
2807
2808fn indexed_claude_window(
2809    locator: &SessionLocator,
2810    options: &SessionLoadOptions,
2811) -> std::result::Result<Option<Value>, ServiceError> {
2812    use supercode_interchange::session::ClaudeReadIndex;
2813    // Exact parent-only window: recursive/full-artifact requests retain the
2814    // existing owner. This is not a bounded display-history substitution.
2815    if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2816        || options.include_subagents != Some(false)
2817    {
2818        return Ok(None);
2819    }
2820    let crate::StorageLocator::File { path } = &locator.storage else {
2821        return Ok(None);
2822    };
2823    if !ClaudeReadIndex::supports(path)
2824        .map_err(|error| ServiceError::Operation(error.to_string()))?
2825    {
2826        return Ok(None);
2827    }
2828    let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2829        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2830    let total = index.len();
2831    let (offset, end) = projected_message_window(total, options);
2832    let session = index
2833        .read_messages(offset..end)
2834        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2835    let summary = index
2836        .read_summary()
2837        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2838    let selected_options = SessionLoadOptions {
2839        message_offset: None,
2840        message_limit: None,
2841        message_tail: None,
2842        ..options.clone()
2843    };
2844    let mut selected = projected_session_json(&session, &selected_options);
2845    selected["raw_record_count"] = json!(index.raw_record_count());
2846    Ok(Some(json!({
2847        "session": selected,
2848        "summary": projected_session_summary(&summary, options),
2849        "window": {
2850            "has_more": offset > 0 || end < total, "has_newer": end < total,
2851            "has_older": offset > 0, "newer_items": index.item_count(end..total),
2852            "offset": offset, "older_items": index.item_count(0..offset),
2853            "returned": end - offset, "total_messages": total,
2854        }
2855    })))
2856}
2857
2858fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2859    let total_messages = session.messages.len();
2860    let (offset, end) = projected_message_window(total_messages, options);
2861    json!({
2862        "session": projected_session_json(session, options),
2863        "summary": projected_session_summary(session, options),
2864        "window": {
2865            "has_more": offset > 0 || end < total_messages,
2866            "has_newer": end < total_messages,
2867            "has_older": offset > 0,
2868            "newer_items": normalized_item_count(&session.messages[end..]),
2869            "offset": offset,
2870            "older_items": normalized_item_count(&session.messages[..offset]),
2871            "returned": end.saturating_sub(offset),
2872            "total_messages": total_messages,
2873        }
2874    })
2875}
2876
2877fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2878    messages
2879        .iter()
2880        .map(|message| {
2881            let conversation = usize::from(
2882                matches!(message.role, Role::Assistant | Role::User)
2883                    && message_has_content(message),
2884            );
2885            let tool_result =
2886                usize::from(message.role == Role::Tool && message_has_content(message));
2887            conversation + tool_result + message.tool_calls().len()
2888        })
2889        .sum()
2890}
2891
2892fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2893    let mut conversational = session.messages.iter().filter(|message| {
2894        matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2895    });
2896    let first_message = conversational.clone().next();
2897    let last_message = conversational.next_back();
2898    let mut assistant = session
2899        .messages
2900        .iter()
2901        .filter(|message| message.role == Role::Assistant && message_has_content(message));
2902    let first_assistant_message = assistant.clone().next();
2903    let last_assistant_message = assistant.next_back();
2904    let end_of_turn = session
2905        .messages
2906        .iter()
2907        .rev()
2908        .find(|message| message.role != Role::System)
2909        .is_some_and(|message| {
2910            message.role == Role::Assistant
2911                && message_has_content(message)
2912                && message.tool_calls().is_empty()
2913        });
2914    let project = |message: Option<&crate::ChatMessage>| {
2915        message.map(|message| project_inline_media(message_json(message), options))
2916    };
2917    json!({
2918        "end_of_turn": end_of_turn,
2919        "first_assistant_message": project(first_assistant_message),
2920        "first_message": project(first_message),
2921        "last_assistant_message": project(last_assistant_message),
2922        "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2923        "last_message": project(last_message),
2924    })
2925}
2926
2927fn message_has_content(message: &crate::ChatMessage) -> bool {
2928    message
2929        .content
2930        .as_deref()
2931        .is_some_and(|content| !content.trim().is_empty())
2932        || message
2933            .content_parts
2934            .as_ref()
2935            .is_some_and(|parts| !parts.is_empty())
2936}
2937
2938fn message_text(message: &crate::ChatMessage) -> String {
2939    if let Some(content) = &message.content {
2940        return content.clone();
2941    }
2942    message
2943        .content_parts
2944        .as_ref()
2945        .into_iter()
2946        .flatten()
2947        .filter_map(|part| part.get("text").and_then(Value::as_str))
2948        .collect::<Vec<_>>()
2949        .join("\n")
2950}
2951
2952fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2953    let (offset, end) = projected_message_window(session.messages.len(), options);
2954    let messages = session.messages[offset..end]
2955        .iter()
2956        .map(|message| project_inline_media(message_json(message), options))
2957        .collect::<Vec<_>>();
2958    let subagents = if options.include_subagents.unwrap_or(true) {
2959        // The reported window describes the top-level transcript. Applying it
2960        // recursively would silently truncate subagents without returning a
2961        // window for each child. Keep their histories complete while carrying
2962        // the caller's media policy through the tree.
2963        let subagent_options = SessionLoadOptions {
2964            message_limit: None,
2965            message_offset: None,
2966            message_tail: None,
2967            ..options.clone()
2968        };
2969        session
2970            .subagents
2971            .iter()
2972            .map(|subagent| projected_session_json(subagent, &subagent_options))
2973            .collect::<Vec<_>>()
2974    } else {
2975        Vec::new()
2976    };
2977    json!({
2978        "source": match session.meta.source {
2979            SessionSource::ClaudeCode => "claude_code",
2980            SessionSource::Codex => "codex",
2981            SessionSource::Gemini => "gemini",
2982            SessionSource::Goose => "goose",
2983            SessionSource::Grok => "grok",
2984            SessionSource::Native => "native",
2985            SessionSource::OpenClaw => "openclaw",
2986            SessionSource::Hermes => "hermes",
2987            SessionSource::OpenCode => "opencode",
2988            SessionSource::Pi => "pi",
2989        },
2990        "session_id": session.meta.session_id,
2991        "ended_at": session.meta.ended_at,
2992        "end_reason": session.meta.end_reason,
2993        "model": session.meta.model,
2994        "cwd": session.meta.cwd,
2995        "system_prompt": session.meta.system_prompt,
2996        "agent_id": session.meta.agent_id,
2997        "parent_tool_use_id": session.meta.parent_tool_use_id,
2998        "lineage": session.meta.lineage,
2999        "messages": messages,
3000        "subagents": subagents,
3001        "raw_record_count": session.raw.len(),
3002        "parse_error_lines": session.parse_error_lines,
3003    })
3004}
3005
3006fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
3007    if let Some(tail) = options.message_tail {
3008        return (total.saturating_sub(tail), total);
3009    }
3010    let offset = options.message_offset.unwrap_or(0).min(total);
3011    let end = options
3012        .message_limit
3013        .map(|limit| offset.saturating_add(limit).min(total))
3014        .unwrap_or(total);
3015    (offset, end)
3016}
3017
3018fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
3019    let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
3020        return message;
3021    };
3022    for part in parts {
3023        let Some(url) = part
3024            .get("image_url")
3025            .and_then(|image| image.get("url"))
3026            .and_then(Value::as_str)
3027        else {
3028            continue;
3029        };
3030        let Some(rest) = url.strip_prefix("data:") else {
3031            continue;
3032        };
3033        let Some((media_type, encoded)) = rest.split_once(";base64,") else {
3034            continue;
3035        };
3036        let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
3037        let decoded_bytes = encoded.len().saturating_mul(3) / 4;
3038        let decoded_bytes = decoded_bytes.saturating_sub(padding);
3039        let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
3040            || options
3041                .max_inline_media_bytes
3042                .is_some_and(|limit| decoded_bytes > limit);
3043        if should_elide {
3044            *part = json!({
3045                "type": "media_reference",
3046                "media_type": media_type,
3047                "encoding": "base64",
3048                "encoded_bytes": encoded.len(),
3049                "decoded_bytes": decoded_bytes,
3050                "omitted": true,
3051            });
3052        }
3053    }
3054    message
3055}
3056
3057#[derive(Deserialize)]
3058struct LocatorParams {
3059    locator: SessionLocator,
3060    /// Optional fidelity for the READ surfaces (`sessions.load`,
3061    /// `sessions.follow`).
3062    ///
3063    /// Omitted means [`Fidelity::Semantic`]: these two methods only ever
3064    /// produce a read-only view, and a compacted or resumed-across-files
3065    /// transcript — the everyday shape of a long Claude Code session — has no
3066    /// losslessly reconstructable record graph, so refusing to render it made
3067    /// the mirror unusable rather than accurate. A caller that intends to
3068    /// CONTINUE from what it reads asks for a lossless level explicitly and
3069    /// gets the strict refusal back. Every other method (export, translate,
3070    /// branch, handoff, resume_instructions) is lossless-only and has no
3071    /// such knob.
3072    #[serde(default)]
3073    fidelity: Option<Fidelity>,
3074    /// Optional bounded frontend projection. Absent preserves the historical
3075    /// complete-session read contract.
3076    #[serde(default)]
3077    view: Option<SessionReadView>,
3078}
3079
3080#[derive(Deserialize)]
3081struct SessionReadView {
3082    /// Number of trailing normalized messages to return. Zero is treated as
3083    /// one so a caller cannot accidentally request an unbounded empty mode.
3084    #[serde(default)]
3085    tail_messages: Option<usize>,
3086    /// Whether Claude Code child transcripts belong in this view. The
3087    /// frontend default is false; the legacy no-view path remains true.
3088    #[serde(default)]
3089    include_subagents: bool,
3090    /// Preserve human-visible native history across model-context compaction.
3091    #[serde(default)]
3092    display_history: bool,
3093    /// Bound each individual text field so a single tool result cannot turn a
3094    /// small message window into a hundred-megabyte RPC response.
3095    #[serde(default)]
3096    max_message_chars: Option<usize>,
3097}
3098
3099impl LocatorParams {
3100    fn read_fidelity(&self) -> Fidelity {
3101        self.fidelity.unwrap_or(Fidelity::Semantic)
3102    }
3103
3104    fn include_subagents(&self) -> bool {
3105        self.view
3106            .as_ref()
3107            .map(|view| view.include_subagents)
3108            .unwrap_or(true)
3109    }
3110
3111    fn tail_messages(&self) -> Option<usize> {
3112        self.view
3113            .as_ref()
3114            .and_then(|view| view.tail_messages)
3115            .map(|limit| limit.clamp(1, 5_000))
3116    }
3117
3118    fn display_history(&self) -> bool {
3119        self.view.as_ref().is_some_and(|view| view.display_history)
3120    }
3121
3122    fn max_message_chars(&self) -> Option<usize> {
3123        self.view
3124            .as_ref()
3125            .and_then(|view| view.max_message_chars)
3126            .map(|limit| limit.clamp(256, 64_000))
3127    }
3128
3129    fn bound_session(&self, session: &mut Session) {
3130        bound_session_view(session, self.tail_messages(), self.max_message_chars());
3131    }
3132}
3133
3134#[derive(Debug, Clone, Copy, Default, Deserialize)]
3135#[serde(rename_all = "snake_case")]
3136enum InlineMediaMode {
3137    #[default]
3138    Full,
3139    Metadata,
3140}
3141
3142#[derive(Debug, Clone, Default, Deserialize)]
3143#[serde(default)]
3144struct SessionLoadOptions {
3145    include_subagents: Option<bool>,
3146    inline_media: InlineMediaMode,
3147    max_inline_media_bytes: Option<usize>,
3148    message_limit: Option<usize>,
3149    message_offset: Option<usize>,
3150    message_tail: Option<usize>,
3151}
3152
3153impl SessionLoadOptions {
3154    fn validate(&self) -> std::result::Result<(), ServiceError> {
3155        if self.message_tail.is_some()
3156            && (self.message_limit.is_some() || self.message_offset.is_some())
3157        {
3158            return Err(ServiceError::InvalidParams(
3159                "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
3160                    .into(),
3161            ));
3162        }
3163        Ok(())
3164    }
3165}
3166
3167#[derive(Deserialize)]
3168struct LoadSessionParams {
3169    #[serde(flatten)]
3170    read: LocatorParams,
3171    #[serde(default)]
3172    options: Option<SessionLoadOptions>,
3173}
3174
3175#[derive(Deserialize)]
3176struct UnfollowParams {
3177    subscription: String,
3178}
3179
3180#[derive(Debug, Deserialize)]
3181#[serde(deny_unknown_fields)]
3182struct IndexResizeParams {
3183    subscription: String,
3184    limit: usize,
3185}
3186
3187#[derive(Deserialize)]
3188struct ActivitySubscribeParams {
3189    locators: Vec<SessionLocator>,
3190    #[serde(default)]
3191    homes: crate::HarnessHomes,
3192}
3193
3194#[derive(Deserialize)]
3195struct MessageSessionParams {
3196    locator: SessionLocator,
3197    text: String,
3198    /// Same storage roots discovery accepts, so a caller (and a test) can
3199    /// point the live-session registry somewhere other than `$HOME`.
3200    #[serde(default)]
3201    homes: crate::HarnessHomes,
3202}
3203
3204#[derive(Deserialize)]
3205#[serde(deny_unknown_fields)]
3206struct HarnessSettingsParams {
3207    harness: String,
3208}
3209
3210#[derive(Deserialize)]
3211#[serde(deny_unknown_fields)]
3212struct ConfigureHarnessParams {
3213    harness: String,
3214    #[serde(default)]
3215    changes: Vec<crate::HarnessSettingChange>,
3216    #[serde(default)]
3217    expected_revision: Option<String>,
3218}
3219
3220fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3221    match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3222        Ok(report) => (
3223            serde_json::to_value(report).unwrap_or(Value::Null),
3224            Value::Null,
3225        ),
3226        Err(error) => (
3227            Value::Null,
3228            Value::String(format!(
3229                "Supercode could not inspect Claude Code inbound controls: {error}"
3230            )),
3231        ),
3232    }
3233}
3234
3235/// Deliver `text` into a session that is running right now, or say why not.
3236///
3237/// A refusal is a RESULT, not a JSON-RPC error: "that session is persisted
3238/// only" is an answer about the session, which a mirror renders next to the
3239/// transcript, and this service's error envelope carries no structured data
3240/// field a machine-readable reason could survive in.
3241///
3242/// `delivered_to_bus` is the honest ceiling of what the courier proves. The
3243/// message reached the receiving session's inbox; whether that session ever
3244/// reads it is governed by ITS OWN inbound controls (`crossSessionInbound`,
3245/// approval dialogs), which Supercode neither sees nor overrides.
3246async fn message_live_session(
3247    params: &MessageSessionParams,
3248    runner: &dyn crate::claude_peer::CourierRunner,
3249) -> Value {
3250    if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
3251        return json!({
3252            "delivered_to_bus": false,
3253            "refusal": {
3254                "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
3255                "message": format!(
3256                    "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
3257                    params.locator.harness.as_str()
3258                ),
3259            },
3260        });
3261    }
3262    let (inbound_controls, inbound_controls_error) =
3263        claude_inbound_controls_or_error(&params.homes);
3264    match crate::claude_peer::message_claude_peer(
3265        &params.homes,
3266        &params.locator.session_id,
3267        &params.text,
3268        runner,
3269    )
3270    .await
3271    {
3272        Ok(delivery) => json!({
3273            "delivered_to_bus": true,
3274            "target": {
3275                "session_id": delivery.target.session_id,
3276                "name": delivery.target.name,
3277                "pid": delivery.target.pid,
3278                "cwd": delivery.target.cwd,
3279                "status": delivery.target.status.map(|status| status.as_str()),
3280            },
3281            "courier": {
3282                "model": crate::claude_peer::COURIER_MODEL,
3283                "report": delivery.courier_report,
3284            },
3285            "inbound_controls": inbound_controls,
3286            "inbound_controls_error": inbound_controls_error,
3287        }),
3288        Err(refusal) => json!({
3289            "delivered_to_bus": false,
3290            "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
3291            "inbound_controls": inbound_controls,
3292            "inbound_controls_error": inbound_controls_error,
3293        }),
3294    }
3295}
3296
3297/// Source identity of one follow subscription, plus the last lifecycle state
3298/// already reported on it. The follower itself stays purely persistence-facing.
3299// Only the adapter-api poll reads these; the subscription bookkeeping itself is
3300// shared by both builds.
3301#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3302struct FollowedSource {
3303    harness: String,
3304    session_id: String,
3305    reported: Option<String>,
3306}
3307
3308#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3309struct ActivitySubscription {
3310    locators: Vec<SessionLocator>,
3311    homes: crate::HarnessHomes,
3312    reported: BTreeMap<(String, String), crate::SessionActivity>,
3313}
3314
3315fn peers_for_descriptors(
3316    descriptors: &[SessionDescriptor],
3317    homes: &HarnessHomes,
3318) -> Vec<crate::claude_peer::ClaudePeerSession> {
3319    if descriptors
3320        .iter()
3321        .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
3322    {
3323        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3324    } else {
3325        Vec::new()
3326    }
3327}
3328
3329/// Add the live address that makes an indexed row behaviorally equivalent to a discovered row.
3330///
3331/// The durable index owns only persistence metadata. Live endpoints remain projections: every
3332/// message/attach operation revalidates its authority, so publishing one here never trusts a stale
3333/// browser-held handle. Reading the Claude registry once per batch keeps this O(peers + rows).
3334fn live_descriptor_value(
3335    session: &SessionDescriptor,
3336    peers: &[crate::claude_peer::ClaudePeerSession],
3337) -> std::result::Result<Value, ServiceError> {
3338    let mut value = serde_json::to_value(session)
3339        .map_err(|error| ServiceError::Operation(error.to_string()))?;
3340    if let Some(workspace) = &session.cwd {
3341        let source = LiveRuntimeSource {
3342            harness: session.locator.harness.as_str().to_string(),
3343            session_id: session.locator.session_id.clone(),
3344            workspace: workspace.clone(),
3345        };
3346        if let Some(endpoint) = discover_live_runtime(&source)
3347            .map_err(|error| ServiceError::Operation(error.to_string()))?
3348        {
3349            value["live_endpoint"] = json!(endpoint.as_str());
3350        }
3351    }
3352    if value.get("live_endpoint").is_none() {
3353        if let Some(peer) = peers.iter().find(|peer| {
3354            session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3355                && peer.session_id == session.locator.session_id
3356        }) {
3357            value["live_endpoint"] = json!(peer.endpoint().as_str());
3358        }
3359    }
3360    Ok(value)
3361}
3362
3363fn live_index_changes(
3364    changes: Vec<crate::session_index::SessionIndexChange>,
3365    homes: &HarnessHomes,
3366) -> std::result::Result<Vec<Value>, ServiceError> {
3367    use crate::session_index::SessionIndexChange;
3368    let has_claude = changes.iter().any(|change| match change {
3369        SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3370            descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3371        }
3372        SessionIndexChange::Removed { .. } => false,
3373    });
3374    let peers = if has_claude {
3375        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3376    } else {
3377        Vec::new()
3378    };
3379    changes
3380        .into_iter()
3381        .map(|change| match change {
3382            SessionIndexChange::Added { descriptor } => Ok(json!({
3383                "kind": "added",
3384                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3385            })),
3386            SessionIndexChange::Updated { descriptor } => Ok(json!({
3387                "kind": "updated",
3388                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3389            })),
3390            SessionIndexChange::Removed { key } => Ok(json!({
3391                "kind": "removed",
3392                "key": key,
3393            })),
3394        })
3395        .collect()
3396}
3397
3398fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3399    use crate::{SessionPresence, SessionTurnState};
3400    match (activity.presence, activity.turn) {
3401        (SessionPresence::Persisted, _) => None,
3402        (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3403        (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3404        // The normalized activity object can honestly report a live owner even
3405        // when the stock harness never published a turn status. Preserve the
3406        // older field's stricter contract instead of guessing `running`.
3407        (SessionPresence::Running, SessionTurnState::Unknown)
3408            if activity.evidence.native_state.is_none() =>
3409        {
3410            None
3411        }
3412        (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3413    }
3414}
3415
3416#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3417#[serde(rename_all = "kebab-case")]
3418enum TransferFormat {
3419    ClaudeCode,
3420    Codex,
3421    #[serde(rename = "opencode", alias = "open-code")]
3422    OpenCode,
3423    Pi,
3424    Grok,
3425    Gemini,
3426    Goose,
3427    /// UNI-18: a Hermes target. Its artifact is the Codex rollout that
3428    /// `hermes sessions import --from codex` reads; `sessions.export` performs
3429    /// that import into the Hermes home.
3430    Hermes,
3431}
3432
3433impl TransferFormat {
3434    fn id(self) -> &'static str {
3435        match self {
3436            Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3437            Self::Codex => HarnessId::CODEX,
3438            Self::OpenCode => HarnessId::OPENCODE,
3439            Self::Pi => HarnessId::PI,
3440            Self::Grok => HarnessId::GROK,
3441            Self::Gemini => HarnessId::GEMINI,
3442            Self::Goose => HarnessId::GOOSE,
3443            Self::Hermes => HarnessId::HERMES,
3444        }
3445    }
3446}
3447
3448impl From<TransferFormat> for SessionFormat {
3449    fn from(value: TransferFormat) -> Self {
3450        match value {
3451            TransferFormat::ClaudeCode => Self::ClaudeCode,
3452            TransferFormat::Codex => Self::Codex,
3453            TransferFormat::OpenCode => Self::OpenCode,
3454            TransferFormat::Pi => Self::Pi,
3455            TransferFormat::Grok => Self::Grok,
3456            TransferFormat::Gemini => Self::Gemini,
3457            TransferFormat::Goose => Self::Goose,
3458            // a Hermes artifact is the Codex rollout Hermes imports
3459            TransferFormat::Hermes => Self::Codex,
3460        }
3461    }
3462}
3463
3464#[derive(Deserialize)]
3465struct ImportSessionParams {
3466    source_harness: TransferFormat,
3467    content: String,
3468}
3469
3470#[derive(Deserialize)]
3471struct ExportSessionParams {
3472    locator: SessionLocator,
3473    target_harness: TransferFormat,
3474}
3475
3476#[derive(Deserialize)]
3477struct ReduceSessionParams {
3478    locator: SessionLocator,
3479    target_harness: TransferFormat,
3480    #[serde(default = "default_keep_last")]
3481    keep_last: usize,
3482}
3483
3484fn default_keep_last() -> usize {
3485    6
3486}
3487
3488#[derive(Deserialize)]
3489struct BranchSessionParams {
3490    locator: SessionLocator,
3491    #[serde(default)]
3492    target_harness: Option<TransferFormat>,
3493}
3494
3495#[derive(Deserialize)]
3496struct HandoffSessionParams {
3497    locator: SessionLocator,
3498    target_harness: TransferFormat,
3499    #[serde(default)]
3500    cwd: Option<PathBuf>,
3501}
3502
3503#[derive(Deserialize)]
3504struct MaterializeSessionParams {
3505    artifact: crate::native_materialize::MaterializeArtifact,
3506    cwd: PathBuf,
3507}
3508
3509#[derive(Debug, Clone, Copy, Default, Deserialize)]
3510#[serde(rename_all = "snake_case")]
3511enum ResumePolicy {
3512    #[default]
3513    Default,
3514    Yolo,
3515}
3516
3517#[derive(Deserialize)]
3518struct ResumeInstructionsParams {
3519    locator: SessionLocator,
3520    #[serde(default)]
3521    cwd: Option<PathBuf>,
3522    #[serde(default)]
3523    policy: ResumePolicy,
3524}
3525
3526/// `harness.v1.workflow.load` parameters: which harness's board, and its home.
3527#[derive(Deserialize)]
3528struct WorkflowLoadParams {
3529    from: crate::workflow_doors::WorkflowHarness,
3530    home: PathBuf,
3531}
3532
3533/// ONT-4 `harness.v1.orchestration.load` parameters. `flavor` says which layout the
3534/// folder is read as; our own is the default.
3535#[derive(Deserialize)]
3536struct OrchestrationLoadParams {
3537    root: PathBuf,
3538    #[serde(default)]
3539    flavor: crate::orchestration_doors::HomeFlavor,
3540}
3541
3542/// ONT-4 `harness.v1.orchestration.save` parameters. `vault` is merged into the
3543/// home's own secrets; a caller that sends none keeps what is on disk.
3544#[derive(Deserialize)]
3545struct OrchestrationSaveParams {
3546    root: PathBuf,
3547    orchestration: crate::orchestration::Orchestration,
3548    #[serde(default)]
3549    vault: BTreeMap<String, String>,
3550}
3551
3552/// ONT-4 `harness.v1.orchestration.compile` parameters.
3553#[derive(Deserialize)]
3554struct OrchestrationCompileParams {
3555    from: crate::orchestration_doors::OrchestrationHarness,
3556    home: PathBuf,
3557}
3558
3559/// ONT-4 `harness.v1.orchestration.decompile` parameters. `source` is the home the
3560/// orchestration was compiled from: it is re-compiled to recover the io bookkeeping
3561/// that byte reuse and the live-store refusal (UNI-18) are decided from.
3562#[derive(Deserialize)]
3563struct OrchestrationDecompileParams {
3564    to: crate::orchestration_doors::OrchestrationHarness,
3565    orchestration: crate::orchestration::Orchestration,
3566    source: PathBuf,
3567    #[serde(default)]
3568    source_flavor: crate::orchestration_doors::SourceFlavor,
3569    dest: PathBuf,
3570    #[serde(default)]
3571    vault: BTreeMap<String, String>,
3572}
3573
3574/// `harness.v1.orchestration.import` parameters: another harness's home, and the
3575/// folder of ours it becomes.
3576#[derive(Deserialize)]
3577struct OrchestrationImportParams {
3578    from: crate::orchestration_doors::OrchestrationHarness,
3579    home: PathBuf,
3580    into: PathBuf,
3581}
3582
3583/// `harness.v1.orchestration.export` parameters: a folder of ours, and the home of
3584/// another harness it becomes.
3585#[derive(Deserialize)]
3586struct OrchestrationExportParams {
3587    to: crate::orchestration_doors::OrchestrationHarness,
3588    root: PathBuf,
3589    dest: PathBuf,
3590}
3591
3592/// `harness.v1.jobs.get` parameters.
3593#[derive(Deserialize)]
3594struct JobsGetParams {
3595    harness: String,
3596    id: String,
3597    #[serde(default)]
3598    homes: crate::HarnessHomes,
3599}
3600
3601/// ORCH-18: run one mutating job verb through the harness's own CLI.
3602///
3603/// The refusal ladder is deliberate: a harness with no scheduled-job concept
3604/// at all answers with the SAME sentence `jobs.list` gives it, and a harness
3605/// that has jobs but publishes no client-callable verb (Claude Code, whose
3606/// jobs are created by the model inside a session) answers with its own
3607/// reason. Neither is ever a silent no-op.
3608fn mutate_job(
3609    verb: crate::jobs_control::JobVerb,
3610    params: Value,
3611) -> std::result::Result<Value, ServiceError> {
3612    let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3613    refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3614    let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3615    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3616}
3617
3618/// ORCH-22: run one mutating skills verb through the harness's own door.
3619///
3620/// The refusal ladder mirrors `jobs.*`: a harness with no skills root at all
3621/// answers with the same sentence `skills.list` gives it, and a harness whose
3622/// door does not publish this verb (OpenClaw has no `skills remove` at the
3623/// pin) answers with its own reason. Neither is ever a silent no-op.
3624fn mutate_skill(
3625    verb: crate::skills_control::SkillVerb,
3626    params: Value,
3627) -> std::result::Result<Value, ServiceError> {
3628    let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3629    if !crate::skills_control::supports_skill_control(&mutation.harness) {
3630        return Err(ServiceError::UnsupportedAction(format!(
3631            "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3632            mutation.harness,
3633            verb.as_str(),
3634            crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3635        )));
3636    }
3637    let outcome =
3638        crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3639    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3640}
3641
3642/// The skills twin of [`job_control_error`], with the same mapping rule.
3643fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3644    match error {
3645        crate::skills_control::SkillControlError::Unsupported(message) => {
3646            ServiceError::UnsupportedAction(message)
3647        }
3648        crate::skills_control::SkillControlError::Invalid(message) => {
3649            ServiceError::InvalidParams(message)
3650        }
3651        crate::skills_control::SkillControlError::Failed(message) => {
3652            ServiceError::Operation(message)
3653        }
3654    }
3655}
3656
3657/// ORCH-21: run one mutating profile verb through the harness's own CLI.
3658///
3659/// The refusal ladder mirrors `mutate_job`'s: a harness with no profile
3660/// concept at all answers with the SAME sentence `profiles.list` gives it, and
3661/// a harness that HAS profiles but publishes no client-callable verb (Codex's
3662/// file-authored `[profiles.<name>]` tables, supercode's compiled-in presets)
3663/// answers with its own reason. Neither is ever a silent no-op.
3664fn mutate_profile(
3665    verb: crate::profiles_control::ProfileVerb,
3666    params: Value,
3667) -> std::result::Result<Value, ServiceError> {
3668    let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3669    let outcome =
3670        crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3671    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3672}
3673
3674/// The same mapping `job_control_error` applies, for the profile noun.
3675fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3676    match error {
3677        crate::profiles_control::ProfileControlError::Unsupported(message) => {
3678            ServiceError::UnsupportedAction(message)
3679        }
3680        crate::profiles_control::ProfileControlError::Invalid(message) => {
3681            ServiceError::InvalidParams(message)
3682        }
3683        crate::profiles_control::ProfileControlError::Failed(message) => {
3684            ServiceError::Operation(message)
3685        }
3686    }
3687}
3688
3689/// Map a controlled-tier failure onto the service's error vocabulary. A verb
3690/// the harness lacks is `UnsupportedAction`; a harness verb that RAN and
3691/// failed carries its own stderr through as the operation error.
3692fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3693    match error {
3694        crate::jobs_control::JobControlError::Unsupported(message) => {
3695            ServiceError::UnsupportedAction(message)
3696        }
3697        crate::jobs_control::JobControlError::Invalid(message) => {
3698            ServiceError::InvalidParams(message)
3699        }
3700        crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3701    }
3702}
3703
3704/// Map an ORCH-19 controlled-tier failure onto the service's error
3705/// vocabulary. A verb the harness has no door for is `UnsupportedAction`; a
3706/// door that RAN and failed carries the harness's own stderr / HTTP body
3707/// through as the operation error.
3708fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3709    match error {
3710        crate::SessionControlError::Unsupported(message) => {
3711            ServiceError::UnsupportedAction(message)
3712        }
3713        crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3714        crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3715    }
3716}
3717
3718/// A harness without a scheduled-job concept refuses the verb rather than
3719/// answering with an empty list — an absent capability and an empty inventory
3720/// are different answers (the same rule `runtimes.capabilities` applies to
3721/// `steer`).
3722fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3723    if crate::jobs::supports_jobs(harness) {
3724        return Ok(());
3725    }
3726    Err(ServiceError::UnsupportedAction(format!(
3727        "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3728        crate::jobs::JOB_HARNESSES.join(", ")
3729    )))
3730}
3731
3732/// `harness.v1.runs.get` parameters.
3733#[derive(Deserialize)]
3734struct RunsGetParams {
3735    harness: String,
3736    id: String,
3737    #[serde(default)]
3738    homes: crate::HarnessHomes,
3739}
3740
3741/// A harness with no run store refuses the verb rather than answering with an
3742/// empty history — the same rule `jobs.list` applies. Claude Code lands here
3743/// on purpose: its cron fires are ordinary turns inside the session that
3744/// created the job, so there is no fire record to list.
3745fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3746    if crate::runs::supports_runs(harness) {
3747        return Ok(());
3748    }
3749    Err(ServiceError::UnsupportedAction(format!(
3750        "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3751        crate::runs::RUN_HARNESSES.join(", ")
3752    )))
3753}
3754
3755#[derive(Serialize)]
3756struct SessionArtifact {
3757    source_harness: HarnessId,
3758    target_harness: &'static str,
3759    session_id: Option<String>,
3760    content: String,
3761    suggested_filename: String,
3762    files: Vec<SessionArtifactFile>,
3763    fidelity: Fidelity,
3764    residue: Vec<String>,
3765}
3766
3767#[derive(Serialize)]
3768struct SessionArtifactFile {
3769    path: String,
3770    content: String,
3771    role: ArtifactFileRole,
3772}
3773
3774#[derive(Serialize)]
3775#[serde(rename_all = "snake_case")]
3776enum ArtifactFileRole {
3777    Primary,
3778    Subagent,
3779    Bundle,
3780    SourceRecovery,
3781}
3782
3783#[derive(Serialize)]
3784struct StructuredLaunch {
3785    cwd: PathBuf,
3786    program: String,
3787    arguments: Vec<String>,
3788    env: BTreeMap<String, String>,
3789}
3790
3791struct HandoffInstructions {
3792    launch: StructuredLaunch,
3793    materialize: Option<StructuredLaunch>,
3794    requires_materialization: bool,
3795    note: String,
3796}
3797
3798#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3799#[serde(rename_all = "snake_case")]
3800enum HarnessProbeLevel {
3801    #[default]
3802    Passive,
3803    Handshake,
3804}
3805
3806#[derive(Default, Deserialize)]
3807#[serde(default)]
3808struct HarnessInventoryParams {
3809    harness: Option<HarnessId>,
3810    harnesses: Vec<HarnessId>,
3811    workspace: Option<PathBuf>,
3812    probe: HarnessProbeLevel,
3813    include_sessions: bool,
3814    /// Omit subprocess-based `--version` calls when a latency-sensitive UI only needs readiness.
3815    skip_versions: bool,
3816}
3817
3818#[derive(Deserialize)]
3819struct HarnessAuthenticationParams {
3820    harness: HarnessId,
3821}
3822
3823#[derive(Deserialize)]
3824struct BeginHarnessAuthenticationParams {
3825    harness: HarnessId,
3826    #[serde(default = "local_browser_authentication_environment")]
3827    environment: crate::HarnessAuthenticationEnvironment,
3828    #[serde(default)]
3829    method: Option<crate::HarnessAuthenticationMethodId>,
3830    #[serde(default)]
3831    cwd: Option<PathBuf>,
3832}
3833
3834fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3835    crate::HarnessAuthenticationEnvironment::LocalBrowser
3836}
3837
3838#[derive(Serialize)]
3839struct HarnessInventoryReport {
3840    probe: HarnessProbeLevel,
3841    workspace: Option<PathBuf>,
3842    harnesses: Vec<LocalHarness>,
3843}
3844
3845#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3846#[serde(rename_all = "snake_case")]
3847enum HarnessAuthState {
3848    Ready,
3849    Configured,
3850    Required,
3851    Unknown,
3852}
3853
3854#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3855#[serde(rename_all = "snake_case")]
3856enum HarnessRuntimeState {
3857    Ready,
3858    Degraded,
3859    Unavailable,
3860}
3861
3862#[derive(Serialize)]
3863struct HarnessSessionCounts {
3864    global: Option<usize>,
3865    workspace: Option<usize>,
3866}
3867
3868/// Receipt-backed evidence that a harness has a RUNNING instance right now,
3869/// distinct from being merely installed (UNI-7). Detection is passive and
3870/// default-on: a gateway liveness connect for daemon harnesses, a fresh
3871/// SQLite WAL stamp for store-writer harnesses (precedent: the opencode
3872/// follower's -wal/-shm freshness). Control stays behind per-connection
3873/// grants — this reports observations only.
3874/// ORCH-17: the gateway-health noun on an inventory row. Derived from the
3875/// UNI-7 running-instance probe (Hermes: `state.db-wal` freshness; OpenClaw:
3876/// a TCP connect to the gateway endpoint resolved from its OWN config) plus
3877/// the executable version — never by starting anything.
3878#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3879#[serde(rename_all = "snake_case")]
3880pub enum GatewayState {
3881    Up,
3882    Down,
3883    Unknown,
3884}
3885
3886/// ORCH-17: `gateway` on a `harness.v1.harnesses.list` row.
3887#[derive(Debug, Clone, Serialize)]
3888pub struct GatewayHealth {
3889    pub state: GatewayState,
3890    /// The endpoint supercode would connect to (OpenClaw: the gateway
3891    /// WebSocket resolved from `openclaw.json`; core harnesses: their
3892    /// declared connect address when one exists). `None` when the harness
3893    /// has no single endpoint (Hermes multiplexes platforms).
3894    #[serde(skip_serializing_if = "Option::is_none")]
3895    pub endpoint: Option<String>,
3896    #[serde(skip_serializing_if = "Option::is_none")]
3897    pub version: Option<String>,
3898    /// What the verdict rests on, or why it is `unknown`.
3899    pub evidence: String,
3900    pub checked_at_ms: u64,
3901}
3902
3903/// OpenClaw's gateway WebSocket endpoint, resolved from its own config the
3904/// way the registry's connect descriptor prescribes (`gateway.url`, else
3905/// `gateway.port`, else the documented default).
3906fn openclaw_gateway_endpoint(home: &Path) -> String {
3907    let config_path = home.join(".openclaw/openclaw.json");
3908    let gateway = std::fs::read_to_string(&config_path)
3909        .ok()
3910        .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3911        .and_then(|config| config.get("gateway").cloned());
3912    if let Some(url) = gateway
3913        .as_ref()
3914        .and_then(|gateway| gateway.get("url"))
3915        .and_then(serde_json::Value::as_str)
3916    {
3917        return url.to_string();
3918    }
3919    let port = gateway
3920        .as_ref()
3921        .and_then(|gateway| gateway.get("port"))
3922        .and_then(serde_json::Value::as_u64)
3923        .unwrap_or(18789);
3924    format!("ws://127.0.0.1:{port}")
3925}
3926
3927/// Ask Hermes itself (`hermes gateway status`, read-only, ~1 s) whether its
3928/// gateway is up. The command is per-host launchd/systemd text without a JSON
3929/// form at 0.19–0.21; the verdict is read from the lines it prints:
3930/// "supervised by launchd (PID …)" / "is running" → up, "not running" /
3931/// "not installed" → down, anything else → no verdict. `SUPERCODE_HERMES_BIN`
3932/// overrides the executable so a fake can stand in under test.
3933fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3934    let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3935    let output = std::process::Command::new(&program)
3936        .args(["gateway", "status"])
3937        .stdin(std::process::Stdio::null())
3938        .output()
3939        .ok()?;
3940    let text = format!(
3941        "{}{}",
3942        String::from_utf8_lossy(&output.stdout),
3943        String::from_utf8_lossy(&output.stderr)
3944    );
3945    let verdict = text.lines().find_map(|line| {
3946        let l = line.trim();
3947        if l.contains("supervised by launchd (PID")
3948            || l.contains("supervised by systemd (PID")
3949            || l.contains("Gateway is running")
3950            || l.contains("process is running")
3951        {
3952            Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3953        } else if l.contains("not running") || l.contains("not installed") {
3954            Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3955        } else {
3956            None
3957        }
3958    });
3959    verdict
3960}
3961
3962fn gateway_health(
3963    id: &str,
3964    installed: bool,
3965    running: Option<&RunningInstance>,
3966    version: Option<&str>,
3967) -> GatewayHealth {
3968    let checked_at_ms = now_epoch_ms();
3969    let home = std::env::var_os("HOME").map(PathBuf::from);
3970    match id {
3971        HarnessId::HERMES | HarnessId::OPENCLAW => {
3972            let endpoint = (id == HarnessId::OPENCLAW)
3973                .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3974                .flatten();
3975            let (state, evidence) = match running {
3976                Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3977                None if !installed => (
3978                    GatewayState::Unknown,
3979                    format!("`{id}` is not installed; no gateway to probe"),
3980                ),
3981                None if id == HarnessId::HERMES => match hermes_gateway_status() {
3982                    // The harness's own door outranks the WAL heuristic: an idle
3983                    // gateway writes nothing for minutes yet is up.
3984                    Some((state, evidence)) => (state, evidence),
3985                    None => (
3986                        GatewayState::Down,
3987                        "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
3988                    ),
3989                },
3990                None => (
3991                    GatewayState::Down,
3992                    format!(
3993                        "no TCP listener at {}",
3994                        endpoint.as_deref().unwrap_or("the gateway endpoint")
3995                    ),
3996                ),
3997            };
3998            GatewayHealth {
3999                state,
4000                endpoint,
4001                version: version.map(str::to_string),
4002                evidence,
4003                checked_at_ms,
4004            }
4005        }
4006        // ORC-7: the orchestrator's gateway IS its daemon, and the daemon's
4007        // own lease file is the record of it. A lease naming a live pid is
4008        // up; a lease whose process is gone is down and says so as a STALE
4009        // lease, never as "no lease"; no lease at all is down. Nothing is
4010        // started, and no port is guessed — the daemon multiplexes adapters
4011        // the way Hermes does, so it has no single endpoint either.
4012        HarnessId::ORCHESTRATOR => {
4013            let root = crate::HarnessHomes::default().orchestrator;
4014            let (state, evidence) = match crate::orchestrator::read_lease(&root) {
4015                Some(lease) if lease.is_live() => (
4016                    GatewayState::Up,
4017                    format!(
4018                        "`{}` names pid {} (started {}), which is live",
4019                        crate::orchestrator::lock_path(&root).display(),
4020                        lease.pid,
4021                        lease.started_at
4022                    ),
4023                ),
4024                Some(lease) => (
4025                    GatewayState::Down,
4026                    format!(
4027                        "stale lease `{}`: pid {} is gone",
4028                        crate::orchestrator::lock_path(&root).display(),
4029                        lease.pid
4030                    ),
4031                ),
4032                None => (
4033                    GatewayState::Down,
4034                    format!(
4035                        "no lease at `{}`; `supercode orchestrator start` writes one",
4036                        crate::orchestrator::lock_path(&root).display()
4037                    ),
4038                ),
4039            };
4040            GatewayHealth {
4041                state,
4042                endpoint: None,
4043                version: version.map(str::to_string),
4044                evidence,
4045                checked_at_ms,
4046            }
4047        }
4048        _ => GatewayHealth {
4049            state: GatewayState::Unknown,
4050            endpoint: None,
4051            version: version.map(str::to_string),
4052            evidence: format!("`{id}` runs per session, not as a gateway"),
4053            checked_at_ms,
4054        },
4055    }
4056}
4057
4058#[derive(Debug, Clone, Serialize)]
4059struct RunningInstance {
4060    /// How the instance was detected.
4061    method: RunningInstanceMethod,
4062    /// The evidence the verdict rests on (endpoint reached / WAL path+age).
4063    evidence: String,
4064    /// Epoch-ms instant the probe executed.
4065    checked_at_ms: u64,
4066}
4067
4068#[derive(Debug, Clone, Copy, Serialize)]
4069#[serde(rename_all = "snake_case")]
4070enum RunningInstanceMethod {
4071    /// A TCP connect to the harness's own configured gateway endpoint
4072    /// succeeded.
4073    GatewayConnect,
4074    /// The harness's session store has an active SQLite WAL (a live writer
4075    /// holds the store open and stamped it recently).
4076    StoreWalActivity,
4077}
4078
4079fn now_epoch_ms() -> u64 {
4080    std::time::SystemTime::now()
4081        .duration_since(std::time::UNIX_EPOCH)
4082        .map(|elapsed| elapsed.as_millis() as u64)
4083        .unwrap_or(0)
4084}
4085
4086/// OpenClaw: the gateway endpoint comes from the harness's OWN config
4087/// (`<home>/.openclaw/openclaw.json` — `gateway.url` or `gateway.port`,
4088/// default port 18789); a successful TCP connect is the running signal.
4089fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4090    let config_path = home.join(".openclaw/openclaw.json");
4091    let text = std::fs::read_to_string(&config_path).ok();
4092    let gateway = text
4093        .as_deref()
4094        .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4095        .and_then(|config| config.get("gateway").cloned());
4096    let address = gateway
4097        .as_ref()
4098        .and_then(|gateway| gateway.get("url"))
4099        .and_then(serde_json::Value::as_str)
4100        .and_then(|url| {
4101            url.split("://").nth(1).map(|rest| {
4102                rest.trim_end_matches('/')
4103                    .split('/')
4104                    .next()
4105                    .unwrap_or(rest)
4106                    .to_string()
4107            })
4108        })
4109        .unwrap_or_else(|| {
4110            let port = gateway
4111                .as_ref()
4112                .and_then(|gateway| gateway.get("port"))
4113                .and_then(serde_json::Value::as_u64)
4114                .unwrap_or(18789);
4115            format!("127.0.0.1:{port}")
4116        });
4117    let reachable = std::net::TcpStream::connect_timeout(
4118        &address.parse().ok()?,
4119        std::time::Duration::from_millis(400),
4120    )
4121    .is_ok();
4122    reachable.then(|| RunningInstance {
4123        method: RunningInstanceMethod::GatewayConnect,
4124        evidence: format!(
4125            "gateway endpoint {address} accepted a TCP connect (from {})",
4126            config_path.display()
4127        ),
4128        checked_at_ms: now_epoch_ms(),
4129    })
4130}
4131
4132/// Hermes: `<home>/.hermes/state.db-wal` freshly modified means a live writer
4133/// holds the store open (SQLite WAL exists only while a connection is open;
4134/// a recent stamp distinguishes an active instance from a stale crash
4135/// leftover).
4136fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4137    let wal = home.join(".hermes/state.db-wal");
4138    let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4139    let age_ms = std::time::SystemTime::now()
4140        .duration_since(modified)
4141        .map(|age| age.as_millis() as u64)
4142        .unwrap_or(u64::MAX);
4143    (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4144        method: RunningInstanceMethod::StoreWalActivity,
4145        evidence: format!(
4146            "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4147            wal.display()
4148        ),
4149        checked_at_ms: now_epoch_ms(),
4150    })
4151}
4152
4153/// Default-on running-instance detection for the harnesses that have one.
4154fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4155    let home = std::env::var_os("HOME").map(PathBuf::from)?;
4156    match id {
4157        HarnessId::OPENCLAW => probe_openclaw_running(&home),
4158        HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4159        _ => None,
4160    }
4161}
4162
4163#[derive(Serialize)]
4164struct LocalHarness {
4165    id: HarnessId,
4166    display_name: String,
4167    supported: bool,
4168    installed: bool,
4169    executable: Option<String>,
4170    version: Option<String>,
4171    auth: HarnessAuthState,
4172    runtime: HarnessRuntimeState,
4173    protocol: String,
4174    capabilities: crate::RuntimeCapabilities,
4175    effective_capabilities: crate::RuntimeCapabilities,
4176    sessions: HarnessSessionCounts,
4177    /// Receipt-backed running-instance detection (None = not detected or the
4178    /// harness has no running-instance concept). Distinct from `installed`.
4179    #[serde(skip_serializing_if = "Option::is_none")]
4180    running: Option<RunningInstance>,
4181    /// ORCH-17: gateway health derived from `running` + the harness's own config.
4182    gateway: GatewayHealth,
4183    reason: Option<String>,
4184    repair: Option<String>,
4185}
4186
4187#[derive(Clone, Deserialize)]
4188struct RuntimeBackendParams {
4189    harness: HarnessId,
4190    #[serde(default)]
4191    protocol: Option<String>,
4192    #[serde(default)]
4193    launch: Option<RuntimeLaunch>,
4194    #[serde(default)]
4195    base_url: Option<String>,
4196    #[serde(default)]
4197    policy: RuntimePolicy,
4198}
4199
4200#[derive(Debug, Clone, Copy, Default, Deserialize)]
4201#[serde(rename_all = "snake_case")]
4202enum RuntimePolicy {
4203    #[default]
4204    Default,
4205    Yolo,
4206}
4207
4208#[derive(Deserialize)]
4209struct RuntimeStartParams {
4210    #[serde(flatten)]
4211    backend: RuntimeBackendParams,
4212    cwd: PathBuf,
4213    /// MCP servers to mount into the new session through the harness's own
4214    /// start door (ORC-6). Backends without such a door ignore them.
4215    #[serde(default)]
4216    mcp_servers: Vec<crate::McpServerLaunch>,
4217}
4218
4219#[derive(Deserialize)]
4220struct RuntimeAttachParams {
4221    #[serde(flatten)]
4222    backend: RuntimeBackendParams,
4223    runtime_id: String,
4224    #[serde(default)]
4225    cwd: Option<PathBuf>,
4226    /// MCP servers to mount into the resumed session (the start door's own
4227    /// field, carried again because a session's tools die with its process).
4228    #[serde(default)]
4229    mcp_servers: Vec<crate::McpServerLaunch>,
4230}
4231
4232#[derive(Deserialize)]
4233struct RuntimeConnectionParams {
4234    connection: String,
4235}
4236
4237#[derive(Deserialize)]
4238struct RuntimeInputParams {
4239    connection: String,
4240    text: String,
4241    #[serde(default)]
4242    image_urls: Vec<String>,
4243}
4244
4245const MAX_RUNTIME_IMAGES: usize = 4;
4246const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4247const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4248
4249fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4250    if image_urls.len() > MAX_RUNTIME_IMAGES {
4251        return Err(ServiceError::InvalidParams(format!(
4252            "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4253        )));
4254    }
4255    let mut total = 0usize;
4256    for url in &image_urls {
4257        if !(url.starts_with("data:image/")
4258            || url.starts_with("https://")
4259            || url.starts_with("http://"))
4260        {
4261            return Err(ServiceError::InvalidParams(
4262                "runtime images must be image data URLs or HTTP(S) URLs".into(),
4263            ));
4264        }
4265        if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4266            return Err(ServiceError::InvalidParams(format!(
4267                "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4268            )));
4269        }
4270        total = total.saturating_add(url.len());
4271    }
4272    if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4273        return Err(ServiceError::InvalidParams(format!(
4274            "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4275        )));
4276    }
4277    Ok(image_urls)
4278}
4279
4280#[derive(Deserialize)]
4281struct RuntimeRespondParams {
4282    connection: String,
4283    request_id: Value,
4284    response: Value,
4285}
4286
4287fn default_reduction_store_root() -> PathBuf {
4288    if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4289        return PathBuf::from(root).join("sessions");
4290    }
4291    if let Some(home) = std::env::var_os("HOME") {
4292        return PathBuf::from(home).join(".supercode").join("sessions");
4293    }
4294    PathBuf::from(".supercode").join("sessions")
4295}
4296
4297fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4298    let mut output = String::new();
4299    for message in messages {
4300        output.push_str(
4301            &serde_json::to_string(message)
4302                .map_err(|error| ServiceError::Operation(error.to_string()))?,
4303        );
4304        output.push('\n');
4305    }
4306    Ok(output)
4307}
4308
4309fn parse_messages_jsonl(
4310    content: &str,
4311) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4312    content
4313        .lines()
4314        .enumerate()
4315        .filter(|(_, line)| !line.trim().is_empty())
4316        .map(|(index, line)| {
4317            serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4318                ServiceError::Operation(format!(
4319                    "reduced transcript line {} is invalid: {error}",
4320                    index + 1
4321                ))
4322            })
4323        })
4324        .collect()
4325}
4326
4327fn reduced_bootstrap_prompt(
4328    source: &SessionLocator,
4329    target: TransferFormat,
4330    view_jsonl: &str,
4331    sidecar_path: &Path,
4332    reduction_log_path: &Path,
4333) -> String {
4334    format!(
4335        "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4336         \n\
4337         The bounded working transcript is below. Treat reduction markers as transparent placeholders, not missing work. If a detail behind a marker is needed, use ordinary file-reading/search tools against the full Supercode sidecar at `{sidecar}` and its reduction index at `{log}`. Do not guess hidden content. Both files were reloaded and verified before this continuation was issued.\n\
4338         \n\
4339         <supercode-reduced-session source-session=\"{source_id}\">\n\
4340         {view_jsonl}\
4341         </supercode-reduced-session>\n\
4342         \n\
4343         Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4344        source_harness = source.harness.as_str(),
4345        target_harness = target.id(),
4346        sidecar = sidecar_path.display(),
4347        log = reduction_log_path.display(),
4348        source_id = source.session_id,
4349    )
4350}
4351
4352fn session_artifact(
4353    locator: &SessionLocator,
4354    session: &Session,
4355    target: TransferFormat,
4356) -> std::result::Result<SessionArtifact, ServiceError> {
4357    session_artifact_with_id(locator, session, target, None)
4358}
4359
4360fn session_artifact_with_id(
4361    locator: &SessionLocator,
4362    session: &Session,
4363    target: TransferFormat,
4364    target_session_id: Option<&str>,
4365) -> std::result::Result<SessionArtifact, ServiceError> {
4366    let format: SessionFormat = target.into();
4367    let diagonal = format.source() == session.meta.source;
4368    let has_appended_turns = session
4369        .imported_message_count
4370        .is_some_and(|imported| imported < session.messages.len());
4371    let content = if let Some(id) = target_session_id {
4372        if diagonal && format != SessionFormat::OpenCode {
4373            session
4374                .to_jsonl_spliced(format, Some(id))
4375                .map_err(operation)?
4376        } else {
4377            let mut rewritten = session.clone();
4378            rewritten.meta.session_id = Some(id.to_string());
4379            rewritten.to_jsonl(format).map_err(operation)?
4380        }
4381    } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4382        session.raw_verbatim()
4383    } else if diagonal {
4384        session.to_jsonl_spliced(format, None).map_err(operation)?
4385    } else {
4386        session.to_jsonl(format).map_err(operation)?
4387    };
4388    let stem = sanitize_filename(
4389        target_session_id
4390            .or(session.meta.session_id.as_deref())
4391            .unwrap_or(&locator.session_id),
4392    );
4393    let suggested_filename = if diagonal && target == TransferFormat::Grok {
4394        "chat_history.jsonl".to_string()
4395    } else if target == TransferFormat::Goose {
4396        format!("{stem}.goose.json")
4397    } else {
4398        format!("{stem}.{}.jsonl", target.id())
4399    };
4400    let mut files = vec![SessionArtifactFile {
4401        path: suggested_filename.clone(),
4402        content: content.clone(),
4403        role: ArtifactFileRole::Primary,
4404    }];
4405    if target == TransferFormat::ClaudeCode {
4406        let bundle_stem = Path::new(&suggested_filename)
4407            .file_stem()
4408            .and_then(|stem| stem.to_str())
4409            .unwrap_or(&stem);
4410        let mut child_paths = BTreeSet::new();
4411        for (index, subagent) in session.subagents.iter().enumerate() {
4412            let agent_id = subagent
4413                .meta
4414                .agent_id
4415                .as_deref()
4416                .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4417                .map(sanitize_filename)
4418                .filter(|id| !id.is_empty())
4419                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4420            let child_has_appended_turns = subagent
4421                .imported_message_count
4422                .is_some_and(|imported| imported < subagent.messages.len());
4423            let child_content = if target_session_id.is_none()
4424                && subagent.meta.source == SessionSource::ClaudeCode
4425                && subagent.raw_is_verbatim
4426                && !child_has_appended_turns
4427            {
4428                subagent.raw_verbatim()
4429            } else if subagent.meta.source == SessionSource::ClaudeCode {
4430                subagent
4431                    .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4432                    .map_err(operation)?
4433            } else {
4434                let mut child = subagent.clone();
4435                if let Some(id) = target_session_id {
4436                    child.meta.session_id = Some(id.to_string());
4437                }
4438                child
4439                    .to_jsonl(SessionFormat::ClaudeCode)
4440                    .map_err(operation)?
4441            };
4442            let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4443            if !child_paths.insert(path.clone()) {
4444                return Err(ServiceError::Operation(format!(
4445                    "Claude subagent ids collide at artifact path `{path}`"
4446                )));
4447            }
4448            files.push(SessionArtifactFile {
4449                path,
4450                content: child_content,
4451                role: ArtifactFileRole::Subagent,
4452            });
4453        }
4454    }
4455    if diagonal && target == TransferFormat::Grok {
4456        append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4457    }
4458    if !diagonal || !session.raw_is_verbatim {
4459        files.push(SessionArtifactFile {
4460            path: "recovery/source.supercode.jsonl".into(),
4461            content: session.to_native_jsonl(),
4462            role: ArtifactFileRole::SourceRecovery,
4463        });
4464        for (index, subagent) in session.subagents.iter().enumerate() {
4465            let id = subagent
4466                .meta
4467                .agent_id
4468                .as_deref()
4469                .map(sanitize_filename)
4470                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4471            files.push(SessionArtifactFile {
4472                path: format!("recovery/subagents/{id}.supercode.jsonl"),
4473                content: subagent.to_native_jsonl(),
4474                role: ArtifactFileRole::SourceRecovery,
4475            });
4476        }
4477    }
4478    if !diagonal && session.meta.source == SessionSource::Grok {
4479        append_grok_bundle_files(
4480            locator,
4481            "recovery/grok/",
4482            ArtifactFileRole::SourceRecovery,
4483            &mut files,
4484        )?;
4485    }
4486    let (fidelity, residue) = if diagonal
4487        && target_session_id.is_none()
4488        && session.raw_is_verbatim
4489        && !has_appended_turns
4490    {
4491        (Fidelity::ByteLossless, Vec::new())
4492    } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4493        (
4494            Fidelity::ValueLossless,
4495            vec![if target_session_id.is_some() {
4496                "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4497            } else {
4498                "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4499            }],
4500        )
4501    } else {
4502        (
4503            Fidelity::Semantic,
4504            vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4505        )
4506    };
4507    Ok(SessionArtifact {
4508        source_harness: locator.harness.clone(),
4509        target_harness: target.id(),
4510        session_id: target_session_id
4511            .map(str::to_string)
4512            .or_else(|| session.meta.session_id.clone()),
4513        content,
4514        suggested_filename,
4515        files,
4516        fidelity,
4517        residue,
4518    })
4519}
4520
4521fn append_grok_bundle_files(
4522    locator: &SessionLocator,
4523    prefix: &str,
4524    role: ArtifactFileRole,
4525    files: &mut Vec<SessionArtifactFile>,
4526) -> std::result::Result<(), ServiceError> {
4527    let primary = locator.storage.path();
4528    if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4529        return Err(ServiceError::Operation(format!(
4530            "Grok bundle locator must name chat_history.jsonl, got {}",
4531            primary.display()
4532        )));
4533    }
4534    let parent = primary.parent().ok_or_else(|| {
4535        ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4536    })?;
4537    for name in ["summary.json", "updates.jsonl"] {
4538        let path = parent.join(name);
4539        let metadata = match std::fs::symlink_metadata(&path) {
4540            Ok(metadata) => metadata,
4541            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4542            Err(error) => return Err(ServiceError::Operation(error.to_string())),
4543        };
4544        if metadata.file_type().is_symlink() || !metadata.is_file() {
4545            return Err(ServiceError::Operation(format!(
4546                "refusing non-regular Grok bundle member {}",
4547                path.display()
4548            )));
4549        }
4550        let content = std::fs::read_to_string(&path).map_err(|error| {
4551            ServiceError::Operation(format!(
4552                "Grok bundle member {} is not representable as UTF-8: {error}",
4553                path.display()
4554            ))
4555        })?;
4556        files.push(SessionArtifactFile {
4557            path: format!("{prefix}{name}"),
4558            content,
4559            role: match role {
4560                ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4561                _ => ArtifactFileRole::SourceRecovery,
4562            },
4563        });
4564    }
4565    Ok(())
4566}
4567
4568fn handoff_artifact(
4569    locator: &SessionLocator,
4570    session: &Session,
4571    target: TransferFormat,
4572) -> std::result::Result<SessionArtifact, ServiceError> {
4573    let target_session_id = target_session_id(target);
4574    session_artifact_with_id(locator, session, target, Some(&target_session_id))
4575}
4576
4577fn target_session_id(target: TransferFormat) -> String {
4578    let uuid = generated_session_id();
4579    match target {
4580        TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4581        TransferFormat::ClaudeCode
4582        | TransferFormat::Codex
4583        | TransferFormat::Pi
4584        | TransferFormat::Grok
4585        | TransferFormat::Gemini
4586        | TransferFormat::Goose
4587        | TransferFormat::Hermes => uuid,
4588    }
4589}
4590
4591fn sanitize_filename(value: &str) -> String {
4592    let value = value
4593        .chars()
4594        .map(|character| {
4595            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4596                character
4597            } else {
4598                '-'
4599            }
4600        })
4601        .collect::<String>();
4602    let value = value.trim_matches('-');
4603    if value.is_empty() {
4604        "session".into()
4605    } else {
4606        value.chars().take(100).collect()
4607    }
4608}
4609
4610fn handoff_instructions(
4611    target: TransferFormat,
4612    session_id: &str,
4613    cwd: &Path,
4614) -> HandoffInstructions {
4615    let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4616        cwd: cwd.to_path_buf(),
4617        program: program.into(),
4618        arguments,
4619        env: BTreeMap::new(),
4620    };
4621    match target {
4622        TransferFormat::ClaudeCode => HandoffInstructions {
4623            launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4624            materialize: None,
4625            requires_materialization: true,
4626            note: "Write the artifact into Claude Code's native project session store before running the resume launch; Claude Code has no general transcript-import command.".into(),
4627        },
4628        TransferFormat::Hermes => HandoffInstructions {
4629            launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4630            materialize: None,
4631            requires_materialization: true,
4632            note: "Hand the artifact (a Codex rollout) to `hermes sessions import --from codex <file>` — `sessions.export --to hermes` does exactly that — and resume the id Hermes prints: Hermes mints its own id and writes its own store.".into(),
4633        },
4634        TransferFormat::Codex => HandoffInstructions {
4635            launch: launch("codex", vec!["resume".into(), session_id.into()]),
4636            materialize: None,
4637            requires_materialization: true,
4638            note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4639        },
4640        TransferFormat::OpenCode => HandoffInstructions {
4641            launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4642            materialize: Some(launch(
4643                "opencode",
4644                vec!["import".into(), "{artifact_path}".into()],
4645            )),
4646            requires_materialization: true,
4647            note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4648        },
4649        TransferFormat::Pi => HandoffInstructions {
4650            launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4651            materialize: None,
4652            requires_materialization: true,
4653            note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4654        },
4655        TransferFormat::Grok => HandoffInstructions {
4656            launch: launch(
4657                "grok",
4658                vec!["--resume".into(), "{materialized_session_id}".into()],
4659            ),
4660            materialize: None,
4661            requires_materialization: true,
4662            note: "Grok has no import command. Materialize the artifact through `harness.v1.sessions.materialize` (target `grok`, `value_lossless`, the destination cwd): it writes Grok's store entry (`chat_history.jsonl` and the `summary.json` `--resume` requires) under a fresh id; replace {materialized_session_id} with the id it returns.".into(),
4663        },
4664        TransferFormat::Gemini => HandoffInstructions {
4665            launch: launch(
4666                "gemini",
4667                vec!["--session-file".into(), "{artifact_path}".into()],
4668            ),
4669            materialize: None,
4670            requires_materialization: true,
4671            note: "Write the Gemini JSONL artifact to a file and replace {artifact_path}; Gemini imports it into the current project's chat store before opening the continuation.".into(),
4672        },
4673        TransferFormat::Goose => HandoffInstructions {
4674            launch: launch(
4675                "goose",
4676                vec![
4677                    "session".into(),
4678                    "--resume".into(),
4679                    "--session-id".into(),
4680                    "{imported_session_id}".into(),
4681                ],
4682            ),
4683            materialize: Some(launch(
4684                "goose",
4685                vec!["session".into(), "import".into(), "{artifact_path}".into()],
4686            )),
4687            requires_materialization: true,
4688            note: "Write the Goose JSON artifact to a file, run the materialize command, read the imported session id from its output, replace {imported_session_id}, then resume that native Goose session.".into(),
4689        },
4690    }
4691}
4692
4693fn resume_launch(
4694    harness: &str,
4695    session_id: &str,
4696    cwd: &Path,
4697    policy: ResumePolicy,
4698) -> std::result::Result<StructuredLaunch, ServiceError> {
4699    let mut arguments = Vec::new();
4700    let program = match harness {
4701        HarnessId::GROK => {
4702            if matches!(policy, ResumePolicy::Yolo) {
4703                if crate::support::self_sandbox_supported() {
4704                    arguments.extend(["--sandbox".into(), "workspace".into()]);
4705                }
4706                arguments.push("--always-approve".into());
4707            }
4708            arguments.extend(["--resume".into(), session_id.into()]);
4709            "grok"
4710        }
4711        HarnessId::CODEX => {
4712            let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4713                .expect("a filesystem path always serializes as JSON text");
4714            arguments.extend([
4715                "-c".into(),
4716                "check_for_update_on_startup=false".into(),
4717                "-c".into(),
4718                format!("projects.{cwd_key}.trust_level=\"trusted\""),
4719            ]);
4720            if matches!(policy, ResumePolicy::Yolo) {
4721                arguments.extend([
4722                    "--dangerously-bypass-approvals-and-sandbox".into(),
4723                    "--dangerously-bypass-hook-trust".into(),
4724                ]);
4725            }
4726            arguments.extend(["resume".into(), session_id.into()]);
4727            "codex"
4728        }
4729        HarnessId::CLAUDE_CODE => {
4730            if matches!(policy, ResumePolicy::Yolo) {
4731                arguments.push("--dangerously-skip-permissions".into());
4732            }
4733            arguments.extend(["--resume".into(), session_id.into()]);
4734            "claude"
4735        }
4736        HarnessId::GEMINI => {
4737            if matches!(policy, ResumePolicy::Yolo) {
4738                arguments.push("--yolo".into());
4739            }
4740            arguments.extend(["--resume".into(), session_id.into()]);
4741            "gemini"
4742        }
4743        HarnessId::GOOSE => {
4744            arguments.extend([
4745                "session".into(),
4746                "--resume".into(),
4747                "--session-id".into(),
4748                session_id.into(),
4749            ]);
4750            "goose"
4751        }
4752        HarnessId::PI => {
4753            if matches!(policy, ResumePolicy::Yolo) {
4754                arguments.push("--approve".into());
4755            }
4756            arguments.extend(["--session".into(), session_id.into()]);
4757            "pi"
4758        }
4759        HarnessId::OPENCODE => {
4760            arguments.extend(["--session".into(), session_id.into()]);
4761            "opencode"
4762        }
4763        HarnessId::SUPERCODE => {
4764            if matches!(policy, ResumePolicy::Yolo) {
4765                arguments.push("--dangerous".into());
4766            }
4767            arguments.extend(["resume".into(), session_id.into()]);
4768            "supercode"
4769        }
4770        other => {
4771            return Err(ServiceError::InvalidParams(format!(
4772                "no structured resume launch is registered for harness `{other}`"
4773            )))
4774        }
4775    };
4776    Ok(StructuredLaunch {
4777        cwd: cwd.to_path_buf(),
4778        env: if program == "grok" {
4779            crate::support::grok_home_env()
4780        } else {
4781            BTreeMap::new()
4782        },
4783        program: program.into(),
4784        arguments,
4785    })
4786}
4787
4788/// Stage the resolved gateway credential in a private (0600) file so the
4789/// bridge can read it via `--token-file` — the delivery the real `openclaw
4790/// acp` accepts. One stable file per endpoint (keyed by an address digest,
4791/// no secret material in the name), overwritten on every connect so files
4792/// never accumulate and a rotated token never goes stale on disk.
4793fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4794    let digest = blake3::hash(address.as_bytes()).to_hex();
4795    let path = std::env::temp_dir().join(format!(
4796        "supercode-openclaw-gateway-token-{}",
4797        &digest.as_str()[..16]
4798    ));
4799    #[cfg(unix)]
4800    {
4801        use std::io::Write;
4802        use std::os::unix::fs::OpenOptionsExt;
4803        let mut file = std::fs::OpenOptions::new()
4804            .write(true)
4805            .create(true)
4806            .truncate(true)
4807            .mode(0o600)
4808            .open(&path)?;
4809        file.write_all(secret.as_bytes())?;
4810    }
4811    #[cfg(not(unix))]
4812    std::fs::write(&path, secret)?;
4813    Ok(path)
4814}
4815
4816/// Open a connect-mode descriptor: resolve the endpoint address and
4817/// credential from the harness's own config file and build the backend that
4818/// joins the already-running endpoint. Fails closed with a specific
4819/// diagnostic when the config cannot be resolved or the declared protocol has
4820/// no connect-capable client yet.
4821fn open_connect_descriptor(
4822    descriptor: &crate::HarnessSupportDescriptor,
4823    home: &Path,
4824) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4825    let Some(connect) = &descriptor.runtime.connect_launch else {
4826        return Err(ServiceError::InvalidParams(format!(
4827            "harness `{}` has no registered connect-mode launch",
4828            descriptor.id.as_str()
4829        )));
4830    };
4831    let resolved = connect
4832        .resolve(home)
4833        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4834    match (descriptor.id.as_str(), connect.protocol.as_str()) {
4835        (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4836            let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4837            if let Some(token) = resolved.auth {
4838                backend = backend.with_bearer(token);
4839            }
4840            Ok(Box::new(backend))
4841        }
4842        (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4843            // OpenClaw's own `openclaw acp` binary is the gateway client: a
4844            // stdio ACP bridge that joins the RUNNING gateway at the resolved
4845            // endpoint. Blind-walk finding 2026-08-31: the real bridge does
4846            // NOT honor OPENCLAW_GATEWAY_TOKEN from the environment — the
4847            // credential must arrive via `--token-file` (never bare `--token`
4848            // on argv, where process listings could read it). The env var is
4849            // still set for older bridges that did read it. Requires openclaw
4850            // >= 2026.7: the 2026.2 bridge drops its gateway socket
4851            // mid-prompt and advertises no session resume (executed finding,
4852            // docs/interop/research/openclaw-acp-dialect-2026-08-30.json).
4853            let mut env = BTreeMap::new();
4854            let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4855            if let Some(token) = resolved.auth {
4856                let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4857                    .map_err(|error| {
4858                        ServiceError::UnsupportedAction(format!(
4859                            "could not stage the gateway credential for the bridge: {error}"
4860                        ))
4861                    })?;
4862                arguments.push("--token-file".into());
4863                arguments.push(token_path.to_string_lossy().into_owned());
4864                env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4865            }
4866            // The bridge program comes from the descriptor's own default
4867            // launch (the compiled registry pins `openclaw`), so tests can
4868            // substitute an absolute mock-bridge path without touching
4869            // process-global state.
4870            let program = descriptor
4871                .runtime
4872                .default_launch
4873                .as_ref()
4874                .map(|launch| launch.program.clone())
4875                .unwrap_or_else(|| "openclaw".into());
4876            let launch = RuntimeLaunch {
4877                program,
4878                arguments,
4879                env,
4880            };
4881            Ok(Box::new(
4882                crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4883                    .with_resume_support(descriptor.runtime.capabilities.resume_session),
4884            ))
4885        }
4886        _ => Err(ServiceError::UnsupportedAction(format!(
4887            "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4888            descriptor.id.as_str(),
4889            connect.protocol
4890        ))),
4891    }
4892}
4893
4894/// The registry's connect-mode launch for this harness, honored only when the
4895/// caller supplied neither an explicit launch nor a base URL.
4896fn registry_connect_descriptor(
4897    params: &RuntimeBackendParams,
4898) -> Option<crate::HarnessSupportDescriptor> {
4899    if params.launch.is_some() || params.base_url.is_some() {
4900        return None;
4901    }
4902    harness_support_registry()
4903        .harnesses
4904        .into_iter()
4905        .find(|descriptor| descriptor.id == params.harness)
4906        .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4907}
4908
4909fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4910    std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4911        ServiceError::UnsupportedAction(
4912            "connect-mode launches need HOME to locate the harness config".into(),
4913        )
4914    })
4915}
4916
4917/// The doors that open a runtime: each spawns or joins a program and waits on
4918/// that program's protocol handshake before it can answer.
4919pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4920    "harness.v1.runtimes.start",
4921    "harness.v1.runtimes.resume",
4922    "harness.v1.runtimes.attach",
4923    "harness.v1.runtimes.attach_existing",
4924];
4925
4926/// How long a runtime gets to finish opening before its caller is answered an
4927/// error instead. A program that never speaks the protocol at all — the wrong
4928/// binary, a shim that prints usage and waits — never answers the handshake,
4929/// so the wait is unbounded without this.
4930pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
4931
4932/// How long a control call on an ALREADY-open runtime — send input, interrupt,
4933/// steer, respond, close — gets before its caller is answered an error
4934/// instead. A live runtime answers these in milliseconds; a wedged one never
4935/// answers at all, and `close` is exactly what a caller reaches for when it
4936/// suspects that.
4937pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
4938
4939/// The doors whose work happens entirely OUTSIDE this service's state once
4940/// its state has been read: probing harnesses, couriering a message into a
4941/// live session, and performing a conversation verb through a harness's own
4942/// CLI / HTTP / store door. Every one of them waits on a child process or a
4943/// network peer. See [`HarnessSessionService::detach`].
4944pub const DETACHED_METHODS: &[&str] = &[
4945    "harness.v1.harnesses.list",
4946    "harness.v1.harnesses.probe",
4947    "harness.v1.sessions.message",
4948    "harness.v1.sessions.new",
4949    "harness.v1.sessions.reset",
4950    "harness.v1.sessions.archive",
4951    "harness.v1.sessions.delete",
4952];
4953
4954/// How long a request moved off a transport's loop gets before its caller is
4955/// answered an error instead. Each of these already bounds its own inner
4956/// waits (a probe's handshake, the courier's run); this is the backstop for
4957/// the ones that do not — a harness CLI that never exits — so no caller waits
4958/// forever on a detached task no one is watching.
4959pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
4960
4961/// How long `sessions.discover` gets before its caller is answered an error
4962/// instead. Discovery reads each harness's own store, and a store on a cold
4963/// or unavailable mount answers at the filesystem's pace rather than its own.
4964///
4965/// Deliberately shorter than the clients' own request deadline (30s): the
4966/// server's answer names the store that did not answer, and it is only read
4967/// if it lands before the client stops listening.
4968pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
4969
4970/// Bound one control call on an open runtime by [`RUNTIME_CONTROL_DEADLINE`],
4971/// naming the method and the bound when it blows.
4972async fn within_control_deadline<F: std::future::Future>(
4973    method: &str,
4974    call: F,
4975) -> std::result::Result<F::Output, ServiceError> {
4976    tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
4977        .await
4978        .map_err(|_| {
4979            ServiceError::Operation(format!(
4980                "`{method}` gave up after {}s: the runtime did not answer",
4981                RUNTIME_CONTROL_DEADLINE.as_secs()
4982            ))
4983        })
4984}
4985
4986/// One [`RUNTIME_OPEN_METHODS`] request, parsed but not yet started. See
4987/// [`HarnessSessionService::runtime_open`] for why it exists apart from
4988/// [`HarnessSessionService::handle_async`].
4989pub struct RuntimeOpen {
4990    id: Value,
4991    method: String,
4992    params: Value,
4993}
4994
4995impl RuntimeOpen {
4996    /// Do the waiting: spawn or join the program and complete its handshake,
4997    /// bounded by [`RUNTIME_OPEN_DEADLINE`]. Touches no service state, so this
4998    /// runs on any task.
4999    pub async fn open(self) -> OpenedRuntime {
5000        let Self { id, method, params } = self;
5001        let outcome = open_runtime(&method, params).await;
5002        OpenedRuntime { id, outcome }
5003    }
5004}
5005
5006/// The result of [`RuntimeOpen::open`], ready for
5007/// [`HarnessSessionService::finish_runtime_open`].
5008pub struct OpenedRuntime {
5009    id: Value,
5010    outcome: std::result::Result<OpenRuntime, ServiceError>,
5011}
5012
5013/// One detached request: the half that reads this service's state already
5014/// done, and the half that waits not yet started. See
5015/// [`HarnessSessionService::detach`] and
5016/// [`HarnessSessionService::detach_runtime`].
5017pub struct DetachedCall {
5018    id: Value,
5019    method: String,
5020    work: std::result::Result<Work, ServiceError>,
5021}
5022
5023impl DetachedCall {
5024    /// Do the waiting and answer. Runs on any task: whatever this call needed
5025    /// from the service was taken before it left.
5026    pub async fn run(self) -> DetachedAnswer {
5027        let Self { id, method, work } = self;
5028        match work {
5029            // A call holding a runtime is already bounded by
5030            // RUNTIME_CONTROL_DEADLINE, and its future OWNS that connection:
5031            // a second timeout around it would drop the connection mid-call
5032            // and take down a runtime its caller still has.
5033            Ok(Work::Runtime(work)) => {
5034                let (result, returned) = work.run().await;
5035                DetachedAnswer {
5036                    response: service_response(id, result),
5037                    returned,
5038                }
5039            }
5040            Ok(Work::Free(work)) => {
5041                let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5042                    Ok(result) => result,
5043                    Err(_) => Err(ServiceError::Operation(format!(
5044                        "`{method}` gave up after {}s: the harness it waits on did not answer",
5045                        DETACHED_CALL_DEADLINE.as_secs()
5046                    ))),
5047                };
5048                DetachedAnswer {
5049                    response: service_response(id, result),
5050                    returned: None,
5051                }
5052            }
5053            Err(error) => DetachedAnswer {
5054                response: service_response(id, Err(error)),
5055                returned: None,
5056            },
5057        }
5058    }
5059}
5060
5061/// One detached call's complete answer, plus whatever it must hand back to
5062/// the service before that answer is written. See
5063/// [`HarnessSessionService::finish_detached`].
5064pub struct DetachedAnswer {
5065    response: Value,
5066    returned: Option<ReturnedRuntime>,
5067}
5068
5069impl DetachedAnswer {
5070    /// The caller's JSON-RPC response, for a transport that owns no service
5071    /// to give a borrowed connection back to.
5072    pub fn into_response(self) -> Value {
5073        self.response
5074    }
5075}
5076
5077/// A connection lent to a detached call, on its way back to the service that
5078/// owns it.
5079pub struct ReturnedRuntime {
5080    connection: String,
5081    runtime: Box<dyn RuntimeConnection>,
5082}
5083
5084/// The waiting half of one detached request: with nothing of the service's
5085/// in hand, or holding a connection the service lent out for the call.
5086enum Work {
5087    Free(DetachedWork),
5088    Runtime(RuntimeWork),
5089}
5090
5091/// The waiting half of one detached request that holds nothing of the
5092/// service's.
5093enum DetachedWork {
5094    /// Probe the selected harnesses: find their executables, ask each its
5095    /// version, and at `probe: handshake` start each one and complete its
5096    /// protocol handshake.
5097    Inventory(InventoryWork),
5098    /// Run the courier that delivers one message into a live session.
5099    Message(MessageSessionParams),
5100    /// Perform one conversation verb through the harness's own CLI, HTTP API,
5101    /// daemon socket, or supercode's own store.
5102    SessionMutation {
5103        verb: crate::SessionVerb,
5104        mutation: crate::SessionMutation,
5105    },
5106}
5107
5108impl DetachedWork {
5109    async fn run(self) -> std::result::Result<Value, ServiceError> {
5110        match self {
5111            Self::Inventory(work) => run_inventory(work).await,
5112            Self::Message(params) => {
5113                Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
5114            }
5115            Self::SessionMutation { verb, mutation } => {
5116                let outcome = run_session_mutation(verb, &mutation).await?;
5117                serde_json::to_value(outcome)
5118                    .map_err(|error| ServiceError::Operation(error.to_string()))
5119            }
5120        }
5121    }
5122}
5123
5124/// One detached call that holds a runtime connection for its whole run.
5125enum RuntimeWork {
5126    /// Tear down a runtime the service has already surrendered.
5127    Close {
5128        runtime: Box<dyn RuntimeConnection>,
5129        process_group: Option<u32>,
5130    },
5131    /// Type one live slash command through a borrowed connection, then give
5132    /// the connection back.
5133    LiveCommand {
5134        connection: String,
5135        runtime: Box<dyn RuntimeConnection>,
5136        verb: crate::SessionVerb,
5137        mutation: crate::SessionMutation,
5138        command: &'static str,
5139        session: String,
5140    },
5141}
5142
5143/// What one [`RuntimeWork`] answers with: the caller's result, and the
5144/// connection to give back when the call only borrowed one.
5145type RuntimeWorkAnswer = (
5146    std::result::Result<Value, ServiceError>,
5147    Option<ReturnedRuntime>,
5148);
5149
5150impl RuntimeWork {
5151    async fn run(self) -> RuntimeWorkAnswer {
5152        match self {
5153            Self::Close {
5154                runtime,
5155                process_group,
5156            } => (close_runtime(runtime, process_group).await, None),
5157            Self::LiveCommand {
5158                connection,
5159                mut runtime,
5160                verb,
5161                mutation,
5162                command,
5163                session,
5164            } => {
5165                let result =
5166                    type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5167                (
5168                    result,
5169                    Some(ReturnedRuntime {
5170                        connection,
5171                        runtime,
5172                    }),
5173                )
5174            }
5175        }
5176    }
5177}
5178
5179/// Tear down a runtime already out of the service, within
5180/// [`RUNTIME_CONTROL_DEADLINE`].
5181async fn close_runtime(
5182    mut runtime: Box<dyn RuntimeConnection>,
5183    process_group: Option<u32>,
5184) -> std::result::Result<Value, ServiceError> {
5185    match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5186        Ok(result) => {
5187            result.map_err(operation)?;
5188            Ok(json!({"closed": true}))
5189        }
5190        Err(deadline) => {
5191            // Dropping the handle is not enough: the process that stopped
5192            // answering is held by a task parked on it, so nothing here runs
5193            // its Drop. Signal the group the graceful path would have
5194            // signalled, then say so.
5195            let killed = kill_runtime_process_group(process_group);
5196            drop(runtime);
5197            Ok(json!({
5198                "closed": true,
5199                "killed": killed,
5200                "detail": error_message(deadline),
5201            }))
5202        }
5203    }
5204}
5205
5206/// The conversation a live `sessions.new` / `sessions.reset` acts on: the one
5207/// the request named, or the runtime's own session.
5208fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5209    mutation
5210        .session
5211        .clone()
5212        .filter(|value| !value.trim().is_empty())
5213        .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5214}
5215
5216/// Type one harness slash command into a live session through the very same
5217/// `send_input` path a human's message takes, within
5218/// [`RUNTIME_CONTROL_DEADLINE`].
5219async fn type_live_command(
5220    runtime: &mut dyn RuntimeConnection,
5221    verb: crate::SessionVerb,
5222    mutation: &crate::SessionMutation,
5223    command: &str,
5224    session: String,
5225) -> std::result::Result<Value, ServiceError> {
5226    within_control_deadline(
5227        &format!("sessions.{}", verb.as_str()),
5228        runtime.send_input(RuntimeInput {
5229            text: command.to_string(),
5230            image_urls: Vec::new(),
5231        }),
5232    )
5233    .await?
5234    .map_err(operation)?;
5235    let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5236        .map_err(session_control_error)?;
5237    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5238}
5239
5240/// A runtime that is up and whose handshake completed, with what the service
5241/// needs to take ownership of it.
5242enum OpenRuntime {
5243    /// supercode spawned this process, so it also hosts it: a frontend server,
5244    /// a live-runtime registration and a terminal launch of its own.
5245    Hosted {
5246        runtime: Box<dyn RuntimeConnection>,
5247        capabilities: crate::RuntimeCapabilities,
5248        workspace: PathBuf,
5249    },
5250    /// `attach_existing` joined a process supercode does not own. It is
5251    /// registered as a bare connection and hosts nothing.
5252    Joined { runtime: Box<dyn RuntimeConnection> },
5253}
5254
5255/// Open the runtime one [`RUNTIME_OPEN_METHODS`] request asks for, within
5256/// [`RUNTIME_OPEN_DEADLINE`]. The error a blown deadline answers names the
5257/// method and the bound, so a caller reads why it was cut loose instead of
5258/// waiting on a handshake that is never coming.
5259async fn open_runtime(
5260    method: &str,
5261    params: Value,
5262) -> std::result::Result<OpenRuntime, ServiceError> {
5263    match tokio::time::timeout(
5264        RUNTIME_OPEN_DEADLINE,
5265        open_runtime_unbounded(method, params),
5266    )
5267    .await
5268    {
5269        Ok(result) => result,
5270        Err(_) => Err(ServiceError::Operation(format!(
5271            "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5272            RUNTIME_OPEN_DEADLINE.as_secs()
5273        ))),
5274    }
5275}
5276
5277async fn open_runtime_unbounded(
5278    method: &str,
5279    params: Value,
5280) -> std::result::Result<OpenRuntime, ServiceError> {
5281    match method {
5282        "harness.v1.runtimes.start" => {
5283            let params = decode::<RuntimeStartParams>(params)?;
5284            let backend = runtime_backend(&params.backend)?;
5285            let capabilities = backend.capabilities();
5286            let workspace = params.cwd.clone();
5287            let runtime = backend
5288                .start(RuntimeStartRequest {
5289                    cwd: params.cwd,
5290                    launch: runtime_launch(&params.backend),
5291                    mcp_servers: params.mcp_servers,
5292                })
5293                .await
5294                .map_err(operation)?;
5295            Ok(OpenRuntime::Hosted {
5296                runtime,
5297                capabilities,
5298                workspace,
5299            })
5300        }
5301        "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5302            let params = decode::<RuntimeAttachParams>(params)?;
5303            let backend = runtime_backend(&params.backend)?;
5304            let capabilities = backend.capabilities();
5305            let workspace = params
5306                .cwd
5307                .clone()
5308                .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5309            let runtime = backend
5310                .attach(RuntimeAttachRequest {
5311                    runtime_id: params.runtime_id,
5312                    cwd: params.cwd,
5313                    launch: runtime_launch(&params.backend),
5314                    mcp_servers: params.mcp_servers,
5315                })
5316                .await
5317                .map_err(operation)?;
5318            Ok(OpenRuntime::Hosted {
5319                runtime,
5320                capabilities,
5321                workspace,
5322            })
5323        }
5324        "harness.v1.runtimes.attach_existing" => {
5325            let params = decode::<RuntimeAttachParams>(params)?;
5326            let backend: Box<dyn RuntimeBackend> = match params
5327                .backend
5328                .base_url
5329                .as_deref()
5330                .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5331            {
5332                Some(endpoint) => {
5333                    #[cfg(not(feature = "adapter-api"))]
5334                    {
5335                        let _ = endpoint;
5336                        return Err(ServiceError::UnsupportedAction(
5337                            "live HTTP attachment adapter is not compiled".into(),
5338                        ));
5339                    }
5340                    #[cfg(feature = "adapter-api")]
5341                    {
5342                        let workspace = params.cwd.clone().ok_or_else(|| {
5343                            ServiceError::InvalidParams(
5344                                "Supercode live attach requires the project cwd".into(),
5345                            )
5346                        })?;
5347                        let source = LiveRuntimeSource {
5348                            harness: params.backend.harness.as_str().to_string(),
5349                            session_id: params.runtime_id.clone(),
5350                            workspace,
5351                        };
5352                        let receipt = resolve_live_runtime(&endpoint, &source)
5353                            .map_err(|error| ServiceError::Operation(error.to_string()))?;
5354                        Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5355                    }
5356                }
5357                None => runtime_backend(&params.backend)?,
5358            };
5359            let capabilities = backend.capabilities();
5360            if !capabilities.attach_existing_process {
5361                return Err(ServiceError::Operation(format!(
5362                    "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5363                    backend.harness().as_str()
5364                )));
5365            }
5366            let runtime = backend
5367                .attach_existing(RuntimeAttachRequest {
5368                    runtime_id: params.runtime_id,
5369                    cwd: params.cwd,
5370                    launch: runtime_launch(&params.backend),
5371                    mcp_servers: params.mcp_servers,
5372                })
5373                .await
5374                .map_err(operation)?;
5375            Ok(OpenRuntime::Joined { runtime })
5376        }
5377        _ => Err(ServiceError::MethodNotFound),
5378    }
5379}
5380
5381/// Wrap one service outcome in its JSON-RPC 2.0 envelope.
5382fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5383    match result {
5384        Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5385        Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5386        Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5387        Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5388        Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5389        Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5390    }
5391}
5392
5393fn runtime_backend(
5394    params: &RuntimeBackendParams,
5395) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5396    if let Some(descriptor) = registry_connect_descriptor(params) {
5397        return open_connect_descriptor(&descriptor, &service_home()?);
5398    }
5399    if params.protocol.as_deref() == Some("acp") {
5400        let launch = params
5401            .launch
5402            .clone()
5403            .or_else(|| {
5404                harness_support_registry()
5405                    .harnesses
5406                    .into_iter()
5407                    .find(|harness| harness.id == params.harness)
5408                    .filter(|harness| {
5409                        harness.runtime.implementation == ImplementationKind::GenericProtocol
5410                            && harness.runtime.protocol.starts_with("acp")
5411                    })
5412                    .and_then(|harness| harness.runtime.default_launch)
5413            })
5414            .ok_or_else(|| {
5415                ServiceError::InvalidParams(
5416                    "an ACP runtime requires `launch` unless the harness has a registered default"
5417                        .into(),
5418                )
5419            })?;
5420        let resume_session = harness_support_registry()
5421            .harnesses
5422            .into_iter()
5423            .find(|harness| harness.id == params.harness)
5424            .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5425        return Ok(Box::new(
5426            AcpRuntimeBackend::new(params.harness.clone(), launch)
5427                .with_resume_support(resume_session),
5428        ));
5429    }
5430    let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5431        HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5432        HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5433        HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5434        HarnessId::OPENCODE => match &params.base_url {
5435            Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5436            None => Box::new(OpenCodeRuntimeBackend::new()),
5437        },
5438        harness => {
5439            let descriptor = harness_support_registry()
5440                .harnesses
5441                .into_iter()
5442                .find(|descriptor| descriptor.id.as_str() == harness)
5443                .filter(|descriptor| {
5444                    descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5445                        && descriptor.runtime.protocol.starts_with("acp")
5446                });
5447            let Some(descriptor) = descriptor else {
5448                return Err(ServiceError::InvalidParams(format!(
5449                    "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5450                )));
5451            };
5452            let resume = descriptor.runtime.capabilities.resume_session;
5453            Box::new(
5454                AcpRuntimeBackend::new(
5455                    descriptor.id,
5456                    descriptor
5457                        .runtime
5458                        .default_launch
5459                        .expect("generic ACP registry entry includes its launch"),
5460                )
5461                .with_resume_support(resume),
5462            )
5463        }
5464    };
5465    Ok(backend)
5466}
5467
5468fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5469    if let Some(launch) = &params.launch {
5470        return Some(launch.clone());
5471    }
5472    if !matches!(params.policy, RuntimePolicy::Yolo) {
5473        return None;
5474    }
5475    let launch = match params.harness.as_str() {
5476        HarnessId::GROK => RuntimeLaunch {
5477            program: "grok".into(),
5478            arguments: {
5479                let mut arguments: Vec<String> = Vec::new();
5480                if crate::support::self_sandbox_supported() {
5481                    arguments.extend(["--sandbox".into(), "workspace".into()]);
5482                }
5483                arguments.extend([
5484                    "--always-approve".into(),
5485                    "agent".into(),
5486                    "--no-leader".into(),
5487                    "stdio".into(),
5488                ]);
5489                arguments
5490            },
5491            env: crate::support::grok_env(),
5492        },
5493        HarnessId::CODEX => RuntimeLaunch {
5494            program: "codex".into(),
5495            arguments: vec![
5496                "--dangerously-bypass-approvals-and-sandbox".into(),
5497                "--dangerously-bypass-hook-trust".into(),
5498                "app-server".into(),
5499            ],
5500            env: BTreeMap::new(),
5501        },
5502        HarnessId::CLAUDE_CODE => RuntimeLaunch {
5503            program: "claude".into(),
5504            arguments: vec![
5505                "--dangerously-skip-permissions".into(),
5506                "--print".into(),
5507                "--input-format".into(),
5508                "stream-json".into(),
5509                "--output-format".into(),
5510                "stream-json".into(),
5511                "--verbose".into(),
5512            ],
5513            env: BTreeMap::new(),
5514        },
5515        HarnessId::PI => RuntimeLaunch {
5516            program: "pi".into(),
5517            arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5518            env: BTreeMap::new(),
5519        },
5520        HarnessId::OPENCODE => RuntimeLaunch {
5521            program: "opencode".into(),
5522            arguments: vec!["serve".into()],
5523            env: BTreeMap::new(),
5524        },
5525        HarnessId::GEMINI => RuntimeLaunch {
5526            program: "gemini".into(),
5527            arguments: vec!["--acp".into(), "--yolo".into()],
5528            env: BTreeMap::new(),
5529        },
5530        HarnessId::GOOSE => RuntimeLaunch {
5531            program: "goose".into(),
5532            arguments: vec!["acp".into()],
5533            env: BTreeMap::new(),
5534        },
5535        HarnessId::SUPERCODE => RuntimeLaunch {
5536            program: "supercode".into(),
5537            arguments: vec!["acp".into(), "--dangerous".into()],
5538            env: BTreeMap::new(),
5539        },
5540        _ => return None,
5541    };
5542    Some(launch)
5543}
5544
5545/// Disposable harness state for a no-prompt readiness probe. Merely opening
5546/// several stock CLIs writes a session header or migrates configuration, so a
5547/// handshake must never point at the user's real home. Authentication files
5548/// are copied into the private temporary home; all writes disappear with the
5549/// guard after the connection closes.
5550struct IsolatedProbeHome {
5551    launch: RuntimeLaunch,
5552    root: PathBuf,
5553}
5554
5555impl IsolatedProbeHome {
5556    fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5557        let root = std::env::temp_dir().join(format!(
5558            "supercode-harness-probe-{harness}-{}",
5559            generated_session_id()
5560        ));
5561        std::fs::create_dir_all(&root)?;
5562        set_private_dir_permissions(&root)?;
5563
5564        if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
5565            for relative in probe_auth_files(harness) {
5566                copy_probe_file(&source_home, &root, relative)?;
5567            }
5568        }
5569        // supercode reads its own config home ($SUPERCODE_HOME, else
5570        // $XDG_CONFIG_HOME/supercode, else ~/.config/supercode), not a fixed
5571        // place under HOME: a login kept under XDG_CONFIG_HOME probed as
5572        // "no API key found" while `supercode run` answered.
5573        if harness == HarnessId::SUPERCODE {
5574            let config_home = crate::agent::global_instructions_dir();
5575            for file in ["config.toml", "credentials.toml"] {
5576                copy_probe_path(
5577                    &config_home.join(file),
5578                    &root.join(".config/supercode").join(file),
5579                )?;
5580            }
5581        }
5582        configure_isolated_probe_auth(harness, &root)?;
5583
5584        let root_text = root.to_string_lossy().into_owned();
5585        for (key, value) in [
5586            ("HOME", root_text.clone()),
5587            (
5588                "XDG_CACHE_HOME",
5589                root.join(".cache").to_string_lossy().into_owned(),
5590            ),
5591            (
5592                "XDG_CONFIG_HOME",
5593                root.join(".config").to_string_lossy().into_owned(),
5594            ),
5595            (
5596                "XDG_DATA_HOME",
5597                root.join(".local/share").to_string_lossy().into_owned(),
5598            ),
5599        ] {
5600            launch.env.insert(key.into(), value);
5601        }
5602        let scoped = match harness {
5603            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5604            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5605            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5606            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5607            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5608            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5609            _ => None,
5610        };
5611        if let Some((key, value)) = scoped {
5612            launch
5613                .env
5614                .insert(key.into(), value.to_string_lossy().into_owned());
5615        }
5616        Ok(Self { launch, root })
5617    }
5618
5619    fn cleanup(&self) -> std::io::Result<()> {
5620        match std::fs::remove_dir_all(&self.root) {
5621            Ok(()) => Ok(()),
5622            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5623            Err(error) => Err(error),
5624        }
5625    }
5626}
5627
5628impl Drop for IsolatedProbeHome {
5629    fn drop(&mut self) {
5630        let _ = self.cleanup();
5631    }
5632}
5633
5634fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5635    match harness {
5636        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5637        // The gateway endpoint + token live in openclaw's own config; without
5638        // it the isolated probe dials the default endpoint unauthenticated
5639        // (PARITY-24 finding 2026-08-31).
5640        HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5641        HarnessId::CODEX => &[".codex/auth.json"],
5642        HarnessId::GEMINI => &[
5643            ".gemini/google_accounts.json",
5644            ".gemini/oauth_creds.json",
5645            ".gemini/settings.json",
5646        ],
5647        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5648        HarnessId::OPENCODE => &[
5649            ".config/opencode/auth.json",
5650            ".local/share/opencode/auth.json",
5651        ],
5652        HarnessId::PI => &[".pi/agent/auth.json"],
5653        // Hermes keeps its provider selection in config.yaml, its OAuth
5654        // credential pool in auth.json, and API keys in .env; without them
5655        // the isolated probe sees "No LLM provider configured" for a
5656        // hermes that answers fine from the user's real home.
5657        HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5658        _ => &[],
5659    }
5660}
5661
5662fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5663    copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5664}
5665
5666fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5667    if !source.is_file() {
5668        return Ok(());
5669    }
5670    if let Some(parent) = destination.parent() {
5671        std::fs::create_dir_all(parent)?;
5672        set_private_dir_permissions(parent)?;
5673    }
5674    std::fs::copy(source, destination)?;
5675    set_private_file_permissions(destination)
5676}
5677
5678fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5679    if harness != HarnessId::GEMINI {
5680        return Ok(());
5681    }
5682    let oauth = probe_home.join(".gemini/oauth_creds.json");
5683    if !oauth.is_file() {
5684        return Ok(());
5685    }
5686    let settings_path = probe_home.join(".gemini/settings.json");
5687    let mut settings = std::fs::read_to_string(&settings_path)
5688        .ok()
5689        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5690        .unwrap_or_else(|| json!({}));
5691    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5692    std::fs::write(
5693        &settings_path,
5694        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5695    )?;
5696    set_private_file_permissions(&settings_path)
5697}
5698
5699#[cfg(unix)]
5700fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5701    use std::os::unix::fs::PermissionsExt;
5702    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5703}
5704
5705#[cfg(not(unix))]
5706fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5707    Ok(())
5708}
5709
5710#[cfg(unix)]
5711fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5712    use std::os::unix::fs::PermissionsExt;
5713    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5714}
5715
5716#[cfg(not(unix))]
5717fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5718    Ok(())
5719}
5720
5721fn find_executable(program: &str) -> Option<PathBuf> {
5722    let candidate = PathBuf::from(program);
5723    if candidate.components().count() > 1 {
5724        return candidate.is_file().then_some(candidate);
5725    }
5726    let path = std::env::var_os("PATH")?;
5727    for directory in std::env::split_paths(&path) {
5728        let candidate = directory.join(program);
5729        if candidate.is_file() {
5730            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5731        }
5732        #[cfg(windows)]
5733        {
5734            for extension in ["exe", "cmd", "bat"] {
5735                let candidate = directory.join(format!("{program}.{extension}"));
5736                if candidate.is_file() {
5737                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5738                }
5739            }
5740        }
5741    }
5742    None
5743}
5744
5745async fn executable_version(executable: &Path) -> Option<String> {
5746    let mut command = tokio::process::Command::new(executable);
5747    command
5748        .arg("--version")
5749        .stdin(std::process::Stdio::null())
5750        .stdout(std::process::Stdio::piped())
5751        .stderr(std::process::Stdio::piped())
5752        .kill_on_drop(true);
5753    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5754        .await
5755        .ok()?
5756        .ok()?;
5757    let stdout = String::from_utf8_lossy(&output.stdout);
5758    let stderr = String::from_utf8_lossy(&output.stderr);
5759    stdout
5760        .lines()
5761        .chain(stderr.lines())
5762        .map(str::trim)
5763        .find(|line| !line.is_empty())
5764        .map(|line| truncate_text(line, 200))
5765}
5766
5767pub(crate) fn auth_evidence(harness: &str) -> bool {
5768    let env_names: &[&str] = match harness {
5769        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5770        HarnessId::CODEX => &["OPENAI_API_KEY"],
5771        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5772        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5773        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5774        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5775        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5776        _ => &[],
5777    };
5778    if env_names
5779        .iter()
5780        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5781    {
5782        return true;
5783    }
5784    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5785        return false;
5786    };
5787    let files: Vec<PathBuf> = match harness {
5788        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5789        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5790        HarnessId::OPENCODE => vec![
5791            home.join(".local/share/opencode/auth.json"),
5792            home.join(".config/opencode/auth.json"),
5793        ],
5794        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5795        HarnessId::GROK => vec![home.join(".grok/auth.json")],
5796        HarnessId::GEMINI => vec![
5797            home.join(".gemini/oauth_creds.json"),
5798            home.join(".gemini/google_accounts.json"),
5799        ],
5800        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5801        HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5802        _ => Vec::new(),
5803    };
5804    if files.into_iter().any(|path| {
5805        std::fs::metadata(path)
5806            .map(|metadata| metadata.is_file() && metadata.len() > 2)
5807            .unwrap_or(false)
5808    }) {
5809        return true;
5810    }
5811    // macOS keeps Claude Code's OAuth login in the Keychain, so
5812    // `.claude/.credentials.json` never exists there and the file probe above
5813    // reports a signed-in install as unauthenticated forever. A completed
5814    // login also writes an `oauthAccount` record into `~/.claude.json` on
5815    // every platform — file-based, prompt-free evidence (querying the
5816    // Keychain itself from an unsigned daemon can raise a UI prompt).
5817    if harness == HarnessId::CLAUDE_CODE {
5818        return std::fs::read_to_string(home.join(".claude.json"))
5819            .map(|text| text.contains("\"oauthAccount\""))
5820            .unwrap_or(false);
5821    }
5822    false
5823}
5824
5825fn looks_like_auth_error(message: &str) -> bool {
5826    let message = message.to_ascii_lowercase();
5827    [
5828        "auth",
5829        "login",
5830        "sign in",
5831        "sign-in",
5832        "credential",
5833        "unauthorized",
5834        "forbidden",
5835        "token",
5836    ]
5837    .iter()
5838    .any(|needle| message.contains(needle))
5839}
5840
5841fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5842    crate::RuntimeCapabilities {
5843        start_session: false,
5844        resume_session: false,
5845        attach_existing_process: false,
5846        send_input: false,
5847        stream_events: false,
5848        interrupt: false,
5849        steer: false,
5850        respond_to_requests: false,
5851    }
5852}
5853
5854fn truncate_text(text: &str, max_chars: usize) -> String {
5855    let mut chars = text.chars();
5856    let truncated = chars.by_ref().take(max_chars).collect::<String>();
5857    if chars.next().is_some() {
5858        format!("{truncated}…")
5859    } else {
5860        truncated
5861    }
5862}
5863
5864/// The process group a runtime's own handle names, when it names one.
5865///
5866/// Every adapter that spawns a local process spawns it as its own group
5867/// leader (`Command::process_group(0)`), so the endpoint's pid IS the group
5868/// id. A runtime reached over HTTP, or one supercode joined rather than
5869/// spawned, names no group here and is left alone.
5870fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5871    match &handle.endpoint {
5872        crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5873        crate::RuntimeEndpoint::Http { .. } => None,
5874    }
5875}
5876
5877/// SIGKILL a wedged runtime's whole process group, reporting whether there
5878/// was one to signal. This is the same group teardown a graceful `close`
5879/// performs; it runs here only when the graceful path blew its deadline,
5880/// because the task parked on the unanswered call still owns the process
5881/// handle and so no `Drop` of ours can reach it.
5882fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5883    match process_group {
5884        #[cfg(unix)]
5885        Some(pid) => {
5886            crate::lsp::kill_process_group(pid);
5887            true
5888        }
5889        #[cfg(not(unix))]
5890        Some(_) => false,
5891        None => false,
5892    }
5893}
5894
5895fn error_message(error: ServiceError) -> String {
5896    match error {
5897        ServiceError::InvalidParams(message)
5898        | ServiceError::Operation(message)
5899        | ServiceError::UnsupportedAction(message) => message,
5900        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5901        ServiceError::Sdk(error) => error.to_string(),
5902    }
5903}
5904
5905#[derive(Debug)]
5906enum ServiceError {
5907    InvalidParams(String),
5908    MethodNotFound,
5909    UnsupportedAction(String),
5910    Operation(String),
5911    Sdk(SdkError),
5912}
5913
5914fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5915    match error {
5916        ServiceError::InvalidParams(message) => {
5917            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5918        }
5919        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5920            SdkError::unsupported(operation)
5921        }
5922        ServiceError::Operation(message) => {
5923            let code = if message.contains("already in progress") {
5924                SdkErrorCode::Busy
5925            } else if message.contains("not supported by this runtime") {
5926                SdkErrorCode::UnsupportedAction
5927            } else if message.contains("unknown runtime connection") {
5928                SdkErrorCode::NotFound
5929            } else {
5930                SdkErrorCode::Execution
5931            };
5932            SdkError::new(code, operation, message)
5933        }
5934        ServiceError::Sdk(error) => error,
5935    }
5936}
5937
5938fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5939    let error_code = error.code();
5940    let code = match error_code {
5941        SdkErrorCode::Unauthenticated => -32030,
5942        SdkErrorCode::Unauthorized => -32031,
5943        SdkErrorCode::ControllerRequired => -32032,
5944        SdkErrorCode::LeaseExpired => -32033,
5945        SdkErrorCode::InvalidArgument => -32602,
5946        SdkErrorCode::NotFound => -32004,
5947        SdkErrorCode::Busy => -32000,
5948        SdkErrorCode::UnsupportedAction => -32020,
5949        SdkErrorCode::Execution => -32002,
5950        SdkErrorCode::Transport => -32003,
5951    };
5952    json!({
5953        "jsonrpc": "2.0",
5954        "id": id,
5955        "error": {
5956            "code": code,
5957            "name": error_code,
5958            "operation": error.operation(),
5959            "message": error.to_string(),
5960        },
5961    })
5962}
5963
5964fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
5965    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
5966}
5967
5968fn operation(error: impl Into<crate::Error>) -> ServiceError {
5969    let error = error.into();
5970    match error {
5971        crate::Error::Sdk(error) => ServiceError::Sdk(error),
5972        error => ServiceError::Operation(error.to_string()),
5973    }
5974}
5975
5976/// ORCH-12 `harness.v1.memory.show|search` params. `homes` is the same
5977/// storage-root override every read-only method accepts, so a caller can
5978/// point the read at a fixture home without touching the real ones.
5979#[derive(Debug, Clone, Deserialize, Default)]
5980#[serde(default)]
5981struct MemoryRequest {
5982    /// Harness whose store is read. Required.
5983    harness: Option<String>,
5984    /// The needle, required by `search`.
5985    query: Option<String>,
5986    /// Hermes profile, OpenClaw agent, or Claude Code project.
5987    profile: Option<String>,
5988    /// Claude Code session id selecting a project store (`show` only).
5989    session: Option<String>,
5990    /// Include each document's whole text (`show` only).
5991    full: bool,
5992    /// Treat `query` as a regular expression (`search` only).
5993    regex: bool,
5994    /// Working tree whose project store is read.
5995    cwd: Option<std::path::PathBuf>,
5996    /// Storage roots to read.
5997    homes: crate::HarnessHomes,
5998}
5999
6000/// Read the memory noun. A harness with no memory store fails with
6001/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6002fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6003    let request = decode::<MemoryRequest>(params)?;
6004    let harness = request
6005        .harness
6006        .clone()
6007        .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6008    let to_service = |error: crate::memory::MemoryError| match error {
6009        crate::memory::MemoryError::UnsupportedHarness { .. }
6010        | crate::memory::MemoryError::SessionNotScoped { .. } => {
6011            ServiceError::UnsupportedAction(error.to_string())
6012        }
6013        other => ServiceError::InvalidParams(other.to_string()),
6014    };
6015    match method {
6016        "harness.v1.memory.show" => {
6017            let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6018                harness,
6019                profile: request.profile,
6020                session: request.session,
6021                full: request.full,
6022                cwd: request.cwd,
6023                homes: request.homes,
6024            })
6025            .map_err(to_service)?;
6026            Ok(json!({
6027                "schema": crate::memory::MEMORY_SCHEMA,
6028                "documents": documents,
6029            }))
6030        }
6031        "harness.v1.memory.search" => {
6032            let query = request
6033                .query
6034                .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6035            let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6036                harness,
6037                query,
6038                profile: request.profile,
6039                regex: request.regex,
6040                cwd: request.cwd,
6041                homes: request.homes,
6042            })
6043            .map_err(to_service)?;
6044            Ok(json!({
6045                "schema": crate::memory::MEMORY_SCHEMA,
6046                "matches": matches,
6047            }))
6048        }
6049        _ => Err(ServiceError::MethodNotFound),
6050    }
6051}
6052
6053/// ORCH-10 `harness.v1.profiles.list|get` params. `homes` is the same
6054/// storage-root override every read-only method accepts, so a caller can
6055/// point the read at a fixture home without touching the real ones.
6056#[derive(Debug, Clone, Deserialize)]
6057#[serde(default)]
6058struct ProfilesQuery {
6059    /// Restrict the listing to one harness. `get` requires it.
6060    harness: Option<String>,
6061    /// Profile name, required by `get`.
6062    name: Option<String>,
6063    /// Storage roots to read.
6064    homes: crate::HarnessHomes,
6065}
6066
6067impl Default for ProfilesQuery {
6068    fn default() -> Self {
6069        Self {
6070            harness: None,
6071            name: None,
6072            homes: crate::HarnessHomes::default(),
6073        }
6074    }
6075}
6076
6077/// Read the profile noun. A harness with no profile concept fails with
6078/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6079fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6080    let query = decode::<ProfilesQuery>(params)?;
6081    let to_service = |error: crate::profiles::ProfileError| match error {
6082        crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6083            ServiceError::UnsupportedAction(error.to_string())
6084        }
6085        crate::profiles::ProfileError::NotFound { .. } => {
6086            ServiceError::InvalidParams(error.to_string())
6087        }
6088    };
6089    match method {
6090        "harness.v1.profiles.list" => {
6091            let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6092                .map_err(to_service)?;
6093            Ok(json!({
6094                "schema": crate::profiles::PROFILES_SCHEMA,
6095                "profiles": profiles,
6096            }))
6097        }
6098        "harness.v1.profiles.get" => {
6099            let harness = query
6100                .harness
6101                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6102            let name = query
6103                .name
6104                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6105            let profile =
6106                crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6107            Ok(json!({
6108                "schema": crate::profiles::PROFILES_SCHEMA,
6109                "profile": profile,
6110            }))
6111        }
6112        _ => Err(ServiceError::MethodNotFound),
6113    }
6114}
6115
6116/// ORCH-14 `harness.v1.channels.list|status` params, the same storage-root
6117/// override every read-only method accepts so a caller can point the read at
6118/// a fixture home without touching the real ones.
6119#[derive(Debug, Clone, Deserialize)]
6120#[serde(default)]
6121struct ChannelsQuery {
6122    /// Restrict the listing to one harness. `status` requires it.
6123    harness: Option<String>,
6124    /// Channel name, required by `status`.
6125    name: Option<String>,
6126    /// Storage roots to read.
6127    homes: crate::HarnessHomes,
6128}
6129
6130impl Default for ChannelsQuery {
6131    fn default() -> Self {
6132        Self {
6133            harness: None,
6134            name: None,
6135            homes: crate::HarnessHomes::default(),
6136        }
6137    }
6138}
6139
6140/// Read the channel noun. A harness with no channel concept fails with
6141/// `UnsupportedAction` (RPC `-32020`), never an empty list. No row carries a
6142/// token, key or secret — see `crate::channels` "Secrecy".
6143#[derive(Debug, Clone, Deserialize)]
6144#[serde(default)]
6145struct RoutesQuery {
6146    harness: Option<String>,
6147    /// Restrict to routes targeting one profile / agent.
6148    profile: Option<String>,
6149    homes: crate::HarnessHomes,
6150}
6151
6152impl Default for RoutesQuery {
6153    fn default() -> Self {
6154        Self {
6155            harness: None,
6156            profile: None,
6157            homes: crate::HarnessHomes::default(),
6158        }
6159    }
6160}
6161
6162#[derive(Debug, Clone, Deserialize)]
6163#[serde(default)]
6164struct TriggersQuery {
6165    harness: Option<String>,
6166    homes: crate::HarnessHomes,
6167}
6168
6169impl Default for TriggersQuery {
6170    fn default() -> Self {
6171        Self {
6172            harness: None,
6173            homes: crate::HarnessHomes::default(),
6174        }
6175    }
6176}
6177
6178fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6179    let query = decode::<TriggersQuery>(params)?;
6180    let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6181        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6182    Ok(json!({
6183        "schema": crate::triggers::TRIGGERS_SCHEMA,
6184        "triggers": triggers,
6185    }))
6186}
6187
6188fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6189    let query = decode::<RoutesQuery>(params)?;
6190    let routes = crate::routes::list_routes(
6191        &query.homes,
6192        query.harness.as_deref(),
6193        query.profile.as_deref(),
6194    )
6195    .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6196    Ok(json!({
6197        "schema": crate::routes::ROUTES_SCHEMA,
6198        "routes": routes,
6199    }))
6200}
6201
6202fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6203    let query = decode::<ChannelsQuery>(params)?;
6204    let to_service = |error: crate::channels::ChannelError| match error {
6205        crate::channels::ChannelError::UnsupportedHarness { .. } => {
6206            ServiceError::UnsupportedAction(error.to_string())
6207        }
6208        crate::channels::ChannelError::NotFound { .. } => {
6209            ServiceError::InvalidParams(error.to_string())
6210        }
6211    };
6212    match method {
6213        "harness.v1.channels.list" => {
6214            let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6215                .map_err(to_service)?;
6216            Ok(json!({
6217                "schema": crate::channels::CHANNELS_SCHEMA,
6218                "channels": channels,
6219            }))
6220        }
6221        "harness.v1.channels.status" => {
6222            let harness = query
6223                .harness
6224                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6225            let name = query
6226                .name
6227                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6228            let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6229                .map_err(to_service)?;
6230            Ok(json!({
6231                "schema": crate::channels::CHANNELS_SCHEMA,
6232                "channel": channel,
6233            }))
6234        }
6235        _ => Err(ServiceError::MethodNotFound),
6236    }
6237}
6238
6239fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6240    json!({
6241        "jsonrpc": "2.0",
6242        "id": id,
6243        "error": {"code": code, "message": message},
6244    })
6245}
6246
6247#[cfg(test)]
6248mod tests {
6249    use super::*;
6250    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6251    use async_trait::async_trait;
6252    use std::io::Write;
6253    use std::path::PathBuf;
6254    use std::time::Instant;
6255
6256    #[test]
6257    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6258        let descriptor = SessionDescriptor {
6259            locator: SessionLocator {
6260                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6261                session_id: "live-session".into(),
6262                storage: StorageLocator::File {
6263                    path: PathBuf::from("/tmp/live-session.jsonl"),
6264                },
6265            },
6266            cwd: Some(PathBuf::from("/project")),
6267            title: None,
6268            preview_candidates: Vec::new(),
6269            latest_message_candidates: Vec::new(),
6270            updated_at_ms: Some(1),
6271            message_count: None,
6272            model: None,
6273            parent_session_id: None,
6274            child_session_count: 0,
6275            nouns: Default::default(),
6276        };
6277        let peer = crate::claude_peer::ClaudePeerSession {
6278            pid: 42,
6279            session_id: "live-session".into(),
6280            cwd: Some(PathBuf::from("/project")),
6281            name: "peer".into(),
6282            socket_path: PathBuf::from("/tmp/peer.sock"),
6283            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6284            updated_at_ms: Some(1),
6285            version: Some("test".into()),
6286        };
6287
6288        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6289        assert!(value["live_endpoint"]
6290            .as_str()
6291            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6292    }
6293
6294    struct EndingRuntime {
6295        handle: RuntimeHandle,
6296        event: Option<HarnessEvent>,
6297        close_failures: usize,
6298    }
6299
6300    #[async_trait]
6301    impl RuntimeConnection for EndingRuntime {
6302        fn handle(&self) -> &RuntimeHandle {
6303            &self.handle
6304        }
6305
6306        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6307            unreachable!("ending runtime does not accept input")
6308        }
6309
6310        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6311            Ok(self.event.take())
6312        }
6313
6314        async fn interrupt(&mut self) -> crate::Result<()> {
6315            Ok(())
6316        }
6317
6318        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6319            Ok(())
6320        }
6321
6322        async fn close(&mut self) -> crate::Result<()> {
6323            if self.close_failures > 0 {
6324                self.close_failures -= 1;
6325                return Err(crate::Error::Other(
6326                    "cleanup temporarily unavailable".into(),
6327                ));
6328            }
6329            Ok(())
6330        }
6331    }
6332
6333    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6334        Box::new(EndingRuntime {
6335            handle: RuntimeHandle {
6336                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6337                runtime_id: "ending-session".into(),
6338                endpoint: RuntimeEndpoint::LocalProcess {
6339                    pid: None,
6340                    command: vec!["ending-runtime".into()],
6341                    protocol: "test".into(),
6342                },
6343            },
6344            event,
6345            close_failures: 0,
6346        })
6347    }
6348
6349    #[tokio::test]
6350    async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6351        let mut service = HarnessSessionService::new();
6352        let handle = ending_runtime(None).handle().clone();
6353        let runtime_id = handle.runtime_id.clone();
6354        let opened = service
6355            .insert_runtime(Box::new(EndingRuntime {
6356                handle,
6357                event: None,
6358                close_failures: 1,
6359            }))
6360            .unwrap();
6361        let connection = opened["connection"].as_str().unwrap().to_string();
6362        service.terminal_launches.insert(
6363            connection.clone(),
6364            StructuredLaunch {
6365                cwd: PathBuf::from("/fixture"),
6366                program: "fixture".into(),
6367                arguments: Vec::new(),
6368                env: BTreeMap::new(),
6369            },
6370        );
6371        let first = service
6372            .handle_async(request(
6373                1,
6374                "harness.v1.runtimes.close",
6375                json!({"connection": connection}),
6376            ))
6377            .await;
6378        // The harness's own teardown failed and the caller is told so...
6379        assert!(first.get("error").is_some(), "{first}");
6380        // ...but the connection is gone all the same. A connection whose close
6381        // cannot complete is exactly the one that must not stay registered:
6382        // holding it would answer every later call on this node with a turn
6383        // that is never going to end.
6384        assert!(!service.runtimes.contains_key(&connection));
6385        assert!(!service.terminal_launches.contains_key(&connection));
6386        assert!(!service.runtime_sequences.contains_key(&runtime_id));
6387        let again = service
6388            .handle_async(request(
6389                2,
6390                "harness.v1.runtimes.close",
6391                json!({"connection": connection}),
6392            ))
6393            .await;
6394        assert_eq!(again["error"]["code"], -32602, "{again}");
6395    }
6396
6397    fn request(id: u64, method: &str, params: Value) -> Value {
6398        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6399    }
6400
6401    // ---- ORCH-6: conversation nouns on `sessions.*` ----------------------
6402
6403    fn hermes_store() -> PathBuf {
6404        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6405    }
6406
6407    /// The discovery response for the Hermes fixture home, with the one
6408    /// machine-specific value (the absolute store path) replaced so the exact
6409    /// same JSON can be committed and replayed by the UI story.
6410    fn hermes_discovery(params: Value) -> Value {
6411        let mut response =
6412            HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6413        let store = hermes_store().display().to_string();
6414        for session in response["result"]["sessions"]
6415            .as_array_mut()
6416            .expect("sessions array")
6417        {
6418            if session["locator"]["storage"]["path"] == json!(store) {
6419                session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6420            }
6421            // `activity` reports a wall-clock observation instant, not a fact
6422            // about the session; it would make this response differ on every
6423            // call. The nouns under test are all session facts.
6424            session.as_object_mut().unwrap().remove("activity");
6425        }
6426        response["result"].take()
6427    }
6428
6429    fn hermes_query() -> Value {
6430        json!({
6431            "harnesses": ["hermes"],
6432            "homes": {"hermes": hermes_store()},
6433        })
6434    }
6435
6436    fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6437        result["sessions"]
6438            .as_array()
6439            .expect("sessions array")
6440            .iter()
6441            .find(|session| session["locator"]["session_id"] == json!(id))
6442            .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6443    }
6444
6445    #[test]
6446    fn orch6_discover_rows_carry_the_conversation_nouns() {
6447        let result = hermes_discovery(hermes_query());
6448
6449        // A Telegram DM: reached on a channel, no repo — the workspace IS the
6450        // channel (D2 precedence), and `main` is not a profile.
6451        let dm = row(&result, "tg-dm-1");
6452        assert_eq!(dm["trigger"], json!("channel"));
6453        assert_eq!(dm["surface"]["platform"], json!("telegram"));
6454        assert_eq!(dm["surface"]["kind"], json!("dm"));
6455        assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6456        assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6457        assert_eq!(
6458            dm["workspace"],
6459            json!({"kind": "channel", "value": "telegram:123456"})
6460        );
6461        assert!(dm.get("profile").is_none(), "{dm:#}");
6462
6463        // A cron fire: recurring, with the job recovered from the minted id.
6464        let fire = row(&result, "cron_job42_20260902_120000");
6465        assert_eq!(fire["trigger"], json!("cron"));
6466        assert_eq!(
6467            fire["recurrence"],
6468            json!({"job_id": "job42", "kind": "cron"})
6469        );
6470        assert_eq!(fire["workspace"]["kind"], json!("repo"));
6471
6472        // A profiled group session with a pending handoff: repo workspace
6473        // wins over the channel, and the chat stays on the surface key.
6474        let coder = row(&result, "tg-coder-1");
6475        assert_eq!(coder["trigger"], json!("channel"));
6476        assert_eq!(coder["profile"], json!("coder"));
6477        assert_eq!(coder["surface"]["thread_id"], json!("55"));
6478        assert_eq!(
6479            coder["surface"]["key"],
6480            json!("agent:coder:telegram:group:-100777:55")
6481        );
6482        assert_eq!(
6483            coder["workspace"],
6484            json!({"kind": "repo", "value": "/workspace/project"})
6485        );
6486        assert_eq!(
6487            coder["cross_surface"],
6488            json!({"state": "pending", "platform": "discord"})
6489        );
6490
6491        // A plain ACP session stays human-triggered with no surface at all.
6492        let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6493        assert_eq!(acp["trigger"], json!("human"));
6494        assert!(acp.get("surface").is_none(), "{acp:#}");
6495        assert_eq!(acp["workspace"], json!({"kind": "none"}));
6496    }
6497
6498    #[test]
6499    fn orch6_discover_filters_by_harness_and_profile() {
6500        let mut params = hermes_query();
6501        params["profile"] = json!("coder");
6502        let result = hermes_discovery(params);
6503        let ids: Vec<&str> = result["sessions"]
6504            .as_array()
6505            .expect("sessions array")
6506            .iter()
6507            .map(|session| session["locator"]["session_id"].as_str().unwrap())
6508            .collect();
6509        assert_eq!(ids, vec!["tg-coder-1"]);
6510
6511        // A profile no session is routed through returns nothing rather than
6512        // silently ignoring the filter.
6513        let mut missing = hermes_query();
6514        missing["profile"] = json!("nobody");
6515        assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6516
6517        // The harness filter is `harnesses`; an id no harness answers to is
6518        // an empty page, never every store on the box.
6519        let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6520        assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6521    }
6522
6523    #[test]
6524    fn orch6_load_reports_the_same_nouns_as_discovery() {
6525        let mut service = HarnessSessionService::new();
6526        let loaded = service.handle(request(
6527            1,
6528            "harness.v1.sessions.load",
6529            json!({"locator": {
6530                "harness": "hermes",
6531                "session_id": "tg-coder-1",
6532                "storage": {"kind": "file", "path": hermes_store()},
6533            }}),
6534        ));
6535        let session = &loaded["result"]["session"];
6536        let discovered = hermes_discovery(hermes_query());
6537        let row = row(&discovered, "tg-coder-1");
6538        for noun in [
6539            "trigger",
6540            "surface",
6541            "profile",
6542            "recurrence",
6543            "cross_surface",
6544            "workspace",
6545        ] {
6546            assert_eq!(
6547                session[noun],
6548                row.get(noun).cloned().unwrap_or(Value::Null),
6549                "`{noun}` disagrees between sessions.load and sessions.discover"
6550            );
6551        }
6552    }
6553
6554    /// ORCH-10: the fixture homes, as the RPC's `homes` override. Hermes's
6555    /// home is named by its `state.db`; OpenClaw's is the state directory.
6556    fn profile_fixture_homes() -> Value {
6557        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6558        json!({
6559            "hermes": fixtures.join("hermes_home/state.db"),
6560            "openclaw": fixtures.join("openclaw_home"),
6561        })
6562    }
6563
6564    fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6565        response["result"]["profiles"]
6566            .as_array()
6567            .unwrap_or_else(|| panic!("no profiles array in {response}"))
6568            .iter()
6569            .find(|row| row["harness"] == harness && row["name"] == name)
6570            .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6571    }
6572
6573    /// dev/01: every source answers in one row shape, over the committed
6574    /// fixture homes — the Hermes profile directory and its `state.db`
6575    /// partition, the OpenClaw agent directories and `openclaw.json`, and
6576    /// supercode's own presets.
6577    #[test]
6578    fn profiles_list_reads_every_source_uniformly() {
6579        let mut service = HarnessSessionService::new();
6580        let response = service.handle(request(
6581            1,
6582            "harness.v1.profiles.list",
6583            json!({"homes": profile_fixture_homes()}),
6584        ));
6585        assert_eq!(
6586            response["result"]["schema"],
6587            crate::profiles::PROFILES_SCHEMA
6588        );
6589
6590        let default = profile_row(&response, "hermes", "default");
6591        assert_eq!(default["kind"], "hermes_profile");
6592        assert_eq!(default["default"], true);
6593        assert_eq!(default["routes"], 0);
6594        assert_eq!(default["sessions"], 11);
6595        assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6596
6597        let coder = profile_row(&response, "hermes", "coder");
6598        assert_eq!(coder["kind"], "hermes_profile");
6599        assert_eq!(coder["default"], false);
6600        assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6601        assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6602        assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6603        assert!(coder["home"]
6604            .as_str()
6605            .unwrap()
6606            .ends_with("hermes_home/profiles/coder"));
6607
6608        let main = profile_row(&response, "openclaw", "main");
6609        assert_eq!(main["kind"], "openclaw_agent");
6610        // No entry declares `default: true` (real configs do not), so `main`
6611        // wins on OpenClaw's own convention rather than alphabetically.
6612        assert_eq!(main["default"], true);
6613        assert_eq!(main["routes"], 0);
6614        assert_eq!(main["sessions"], 4);
6615        assert_eq!(
6616            main["model"],
6617            Value::Null,
6618            "`agents.defaults.model` is an install default, not this agent's pin"
6619        );
6620
6621        let design = profile_row(&response, "openclaw", "design");
6622        assert_eq!(design["default"], false);
6623        assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6624        assert_eq!(design["sessions"], 0);
6625        assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6626
6627        let preset = profile_row(&response, "supercode", "supercode-default");
6628        assert_eq!(preset["kind"], "preset");
6629        assert_eq!(preset["default"], true);
6630        assert_eq!(preset["home"], Value::Null);
6631        assert_eq!(preset["routes"], Value::Null);
6632    }
6633
6634    /// Codex's own profiles are `[profiles.<name>]` tables, with the
6635    /// top-level `profile` key naming the default.
6636    #[test]
6637    fn profiles_list_reads_codex_profile_tables() {
6638        let codex_home = std::env::temp_dir().join(format!(
6639            "supercode-orch10-codex-{}-{}",
6640            std::process::id(),
6641            std::time::SystemTime::now()
6642                .duration_since(std::time::UNIX_EPOCH)
6643                .unwrap()
6644                .as_nanos()
6645        ));
6646        std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6647        std::fs::write(
6648            codex_home.join("config.toml"),
6649            "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6650        )
6651        .unwrap();
6652
6653        let mut service = HarnessSessionService::new();
6654        let response = service.handle(request(
6655            1,
6656            "harness.v1.profiles.list",
6657            json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6658        ));
6659        let rows = response["result"]["profiles"].as_array().unwrap();
6660        assert_eq!(rows.len(), 2, "{response}");
6661        let review = profile_row(&response, "codex", "review");
6662        assert_eq!(review["kind"], "codex_profile");
6663        assert_eq!(review["default"], true);
6664        assert_eq!(review["model"], "gpt-5.1-codex");
6665        assert_eq!(review["home"], Value::Null);
6666        assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6667
6668        let got = service.handle(request(
6669            2,
6670            "harness.v1.profiles.get",
6671            json!({
6672                "harness": "codex",
6673                "name": "fast",
6674                "homes": {"codex": codex_home.join("sessions")},
6675            }),
6676        ));
6677        assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6678        std::fs::remove_dir_all(&codex_home).ok();
6679    }
6680
6681    /// A verb a harness lacks fails with `UnsupportedAction`, never a silent
6682    /// empty list; an unknown name is an invalid argument, not an empty row.
6683    #[test]
6684    fn profiles_refuse_harnesses_without_the_concept() {
6685        let mut service = HarnessSessionService::new();
6686        let response = service.handle(request(
6687            1,
6688            "harness.v1.profiles.list",
6689            json!({"harness": "claude-code"}),
6690        ));
6691        assert_eq!(response["error"]["code"], -32020, "{response}");
6692
6693        let missing = service.handle(request(
6694            2,
6695            "harness.v1.profiles.get",
6696            json!({
6697                "harness": "hermes",
6698                "name": "no-such-profile",
6699                "homes": profile_fixture_homes(),
6700            }),
6701        ));
6702        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6703    }
6704
6705    /// The two methods are advertised, so a client discovers them from
6706    /// `harness.v1.capabilities` rather than from documentation.
6707    #[test]
6708    fn profiles_methods_are_advertised() {
6709        let mut service = HarnessSessionService::new();
6710        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6711        let methods = response["result"]["methods"].as_array().unwrap();
6712        for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6713            assert!(
6714                methods.iter().any(|entry| entry == method),
6715                "{method} is not advertised"
6716            );
6717        }
6718    }
6719
6720    // -----------------------------------------------------------------
6721    // ORCH-14 — channels
6722    // -----------------------------------------------------------------
6723
6724    fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6725        response["result"]["channels"]
6726            .as_array()
6727            .unwrap_or_else(|| panic!("no channels array in {response}"))
6728            .iter()
6729            .find(|row| row["harness"] == harness && row["name"] == name)
6730            .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6731    }
6732
6733    fn channels_list(harness: Option<&str>) -> Value {
6734        let mut params = json!({"homes": profile_fixture_homes()});
6735        if let Some(harness) = harness {
6736            params["harness"] = json!(harness);
6737        }
6738        HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6739    }
6740
6741    /// dev/01: both sources answer in one row shape over the committed
6742    /// fixture homes — Hermes's `platforms:` blocks with their `extra` maps,
6743    /// and OpenClaw's `channels.<name>` entries split per account.
6744    #[test]
6745    fn channels_list_reads_both_gateway_harnesses_uniformly() {
6746        let response = channels_list(None);
6747        assert_eq!(
6748            response["result"]["schema"],
6749            crate::channels::CHANNELS_SCHEMA
6750        );
6751
6752        // Hermes: a credentialed platform, a bridged `extra.key` platform,
6753        // and one the config explicitly disables.
6754        let telegram = channel_row(&response, "hermes", "telegram");
6755        assert_eq!(telegram["kind"], "telegram");
6756        assert_eq!(telegram["enabled"], true);
6757        assert_eq!(telegram["configured"], true);
6758        // The `sessions` count is the discovery rows whose surface platform
6759        // is telegram: the fixture's `agent:main:telegram:…` DM and the
6760        // `agent:coder:telegram:…` group.
6761        assert_eq!(telegram["sessions"], 2);
6762        let api = channel_row(&response, "hermes", "api_server");
6763        assert_eq!(api["configured"], true, "extra.key is a credential key");
6764        assert_eq!(api["sessions"], 0);
6765        let webhook = channel_row(&response, "hermes", "webhook");
6766        assert_eq!(webhook["enabled"], false);
6767        // Hermes lists no credential for `webhook`: declaring it is all it
6768        // needs, so a credential-less entry is still `configured`.
6769        assert_eq!(webhook["configured"], true);
6770
6771        // OpenClaw: one row per account, named `<channel>/<accountId>`.
6772        let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6773        assert_eq!(linked["kind"], "slack");
6774        assert_eq!(linked["account"], "T0FIXTURE");
6775        assert_eq!(linked["enabled"], true);
6776        assert_eq!(linked["configured"], true);
6777        let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6778        assert_eq!(unlinked["enabled"], false);
6779        assert_eq!(
6780            unlinked["configured"], false,
6781            "an account with no credential key is not configured"
6782        );
6783        // A single-account channel keeps its own name and names its account
6784        // inline.
6785        let telegram = channel_row(&response, "openclaw", "telegram");
6786        assert_eq!(telegram["account"], "hermes-fixture-bot");
6787        assert_eq!(telegram["configured"], true);
6788
6789        // `status` is never claimed from a config file.
6790        for row in response["result"]["channels"].as_array().unwrap() {
6791            assert_eq!(row["status"], "unknown", "{row}");
6792        }
6793    }
6794
6795    /// dev/01: no field of any emitted row carries a credential. The fixture
6796    /// homes hold four FAKE credential strings; a row that leaked one — as a
6797    /// value, an account label, or a name — fails here.
6798    #[test]
6799    fn channels_rows_never_carry_a_fixture_secret() {
6800        let secrets = [
6801            "FAKE-TOKEN-DO-NOT-EMIT",
6802            "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6803            "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6804            "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6805            "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6806        ];
6807        // The strings really are in the fixtures, so this test can fail.
6808        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6809        let raw = format!(
6810            "{}{}",
6811            std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6812            std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6813        );
6814        for secret in secrets {
6815            assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6816        }
6817
6818        let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6819        for secret in secrets {
6820            assert!(
6821                !emitted.contains(secret),
6822                "`{secret}` leaked into a channel row: {emitted}"
6823            );
6824        }
6825        // Belt and braces: no row FIELD is credential-shaped either, so a
6826        // future field cannot smuggle one past the literal scan.
6827        for row in channels_list(None)["result"]["channels"]
6828            .as_array()
6829            .unwrap()
6830        {
6831            for key in row.as_object().unwrap().keys() {
6832                let key = key.to_ascii_lowercase();
6833                assert!(
6834                    !["token", "key", "secret", "password", "credential"]
6835                        .iter()
6836                        .any(|marker| key.ends_with(marker)),
6837                    "`{key}` is a credential-shaped field on a channel row"
6838                );
6839            }
6840        }
6841    }
6842
6843    /// `status` answers one row by name, and refuses an unknown one.
6844    #[test]
6845    fn channels_status_reads_one_row_by_name() {
6846        let mut service = HarnessSessionService::new();
6847        let got = service.handle(request(
6848            1,
6849            "harness.v1.channels.status",
6850            json!({
6851                "harness": "openclaw",
6852                "name": "slack/T0FIXTURE",
6853                "homes": profile_fixture_homes(),
6854            }),
6855        ));
6856        assert_eq!(got["result"]["channel"]["kind"], "slack");
6857        assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6858        assert_eq!(got["result"]["channel"]["status"], "unknown");
6859
6860        let missing = service.handle(request(
6861            2,
6862            "harness.v1.channels.status",
6863            json!({
6864                "harness": "openclaw",
6865                "name": "no-such-channel",
6866                "homes": profile_fixture_homes(),
6867            }),
6868        ));
6869        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6870    }
6871
6872    /// A harness with no channel concept fails with `UnsupportedAction`,
6873    /// never a silent empty list — Claude Code included, because its channels
6874    /// are MCP-protocol declarations no config file names.
6875    #[test]
6876    fn channels_refuse_harnesses_without_the_concept() {
6877        let response = channels_list(Some("claude-code"));
6878        assert_eq!(response["error"]["code"], -32020, "{response}");
6879        let codex = channels_list(Some("codex"));
6880        assert_eq!(codex["error"]["code"], -32020, "{codex}");
6881    }
6882
6883    /// The harness filter restricts the rows rather than being ignored.
6884    #[test]
6885    fn channels_list_filters_by_harness() {
6886        let response = channels_list(Some("openclaw"));
6887        let rows = response["result"]["channels"].as_array().unwrap();
6888        assert!(!rows.is_empty(), "{response}");
6889        assert!(
6890            rows.iter().all(|row| row["harness"] == "openclaw"),
6891            "harness filter leaked: {response}"
6892        );
6893    }
6894
6895    /// Both methods are advertised, so a client discovers them from
6896    /// `harness.v1.capabilities` rather than from documentation.
6897    #[test]
6898    fn channels_methods_are_advertised() {
6899        let mut service = HarnessSessionService::new();
6900        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6901        let methods = response["result"]["methods"].as_array().unwrap();
6902        for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6903            assert!(
6904                methods.iter().any(|entry| entry == method),
6905                "{method} is not advertised"
6906            );
6907        }
6908    }
6909
6910    /// The UI story renders REAL rows: this writes the discovery response the
6911    /// two assertions above pin into the fixture the Storybook
6912    /// `Compositions/Universal nouns` stories import, and fails when the
6913    /// committed copy has drifted from what the service now answers.
6914    #[test]
6915    fn orch6_story_fixture_matches_the_live_discovery_response() {
6916        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6917            .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6918        let mut result = hermes_discovery(hermes_query());
6919        // `updated_at_ms` is derived from the fixture's own stored timestamps,
6920        // so the whole response is deterministic; drop only the cursor, which
6921        // is pagination state rather than a session fact.
6922        result.as_object_mut().unwrap().remove("next_cursor");
6923        let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6924        if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6925            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6926            std::fs::write(&path, &rendered).unwrap();
6927        }
6928        let committed = std::fs::read_to_string(&path).unwrap_or_default();
6929        assert_eq!(
6930            committed, rendered,
6931            "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6932             re-run with SUPERCODE_UPDATE_FIXTURES=1"
6933        );
6934    }
6935
6936    fn pi_locator() -> SessionLocator {
6937        SessionLocator {
6938            harness: HarnessId::from(HarnessId::PI),
6939            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6940            storage: StorageLocator::File {
6941                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6942                    .join("tests/fixtures/pi_session.jsonl"),
6943            },
6944        }
6945    }
6946
6947    fn opencode_locator() -> SessionLocator {
6948        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
6949        SessionLocator {
6950            harness: HarnessId::from(HarnessId::OPENCODE),
6951            session_id: session_id.into(),
6952            storage: StorageLocator::Sqlite {
6953                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6954                    .join("tests/fixtures/opencode_fixture/opencode.db"),
6955                selector: session_id.into(),
6956            },
6957        }
6958    }
6959
6960    fn grok_locator() -> SessionLocator {
6961        SessionLocator {
6962            harness: HarnessId::from(HarnessId::GROK),
6963            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
6964            storage: StorageLocator::File {
6965                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6966                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
6967            },
6968        }
6969    }
6970
6971    // ---- ORCH-11: `harness.v1.skills.list` -------------------------------
6972
6973    fn fixture_homes() -> Value {
6974        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6975        json!({
6976            "claude_code": fixtures.join("__absent__"),
6977            "codex": fixtures.join("__absent__"),
6978            "opencode": fixtures.join("__absent__"),
6979            "pi": fixtures.join("__absent__"),
6980            "agents": fixtures.join("__absent__"),
6981            "hermes": fixtures.join("hermes_home"),
6982            "openclaw": fixtures.join("openclaw_home"),
6983        })
6984    }
6985
6986    #[test]
6987    fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
6988        let root = std::env::temp_dir().join(format!(
6989            "supercode-preview-rpc-{}-{}",
6990            std::process::id(),
6991            std::time::SystemTime::now()
6992                .duration_since(std::time::UNIX_EPOCH)
6993                .unwrap()
6994                .as_nanos()
6995        ));
6996        std::fs::create_dir_all(&root).unwrap();
6997        for id in ["first", "second"] {
6998            std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
6999                json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7000                json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7001            )).unwrap();
7002        }
7003        let mut service = HarnessSessionService::new();
7004        let query = json!({
7005            "harnesses": ["codex"], "homes": {"codex": root},
7006            "query": "nebula", "search_previews": true, "limit": 1
7007        });
7008        let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7009        assert!(first.get("error").is_none(), "{first}");
7010        assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7011        assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7012        let mut next_query = query.clone();
7013        next_query["cursor"] = first["result"]["next_cursor"].clone();
7014        let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7015        assert_eq!(next["result"]["receipt"]["returned"], 1);
7016        assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7017        assert_eq!(next["result"]["receipt"]["truncated"], false);
7018        assert_ne!(
7019            first["result"]["sessions"][0]["locator"],
7020            next["result"]["sessions"][0]["locator"]
7021        );
7022        let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7023        assert!(
7024            refused["error"]["message"]
7025                .as_str()
7026                .unwrap()
7027                .contains("use sessions.discover"),
7028            "{refused}"
7029        );
7030        std::fs::remove_dir_all(root).unwrap();
7031    }
7032
7033    #[test]
7034    fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7035        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7036        let root = std::env::temp_dir().join(format!(
7037            "supercode-index-rpc-{}-{}",
7038            std::process::id(),
7039            std::time::SystemTime::now()
7040                .duration_since(std::time::UNIX_EPOCH)
7041                .unwrap()
7042                .as_nanos()
7043        ));
7044        std::fs::create_dir_all(&root).unwrap();
7045        for id in ["first", "second"] {
7046            std::fs::write(root.join(format!("{id}.jsonl")), format!(
7047                "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7048            )).unwrap();
7049        }
7050        let mut service = HarnessSessionService::new();
7051        let opened = service.handle(request(
7052            1,
7053            "harness.v1.sessions.index.subscribe",
7054            json!({
7055                "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7056            }),
7057        ));
7058        assert!(opened.get("error").is_none(), "{opened:#}");
7059        let subscription = opened["result"]["subscription"]
7060            .as_str()
7061            .unwrap()
7062            .to_owned();
7063        assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7064        for params in [
7065            json!({"subscription": subscription, "limit": 0}),
7066            json!({"subscription": subscription, "limit": 2049}),
7067            json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7068            json!({"subscription": "unknown", "limit": 2}),
7069        ] {
7070            let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7071            assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7072        }
7073        for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7074            let response = service.handle(request(
7075                3,
7076                "harness.v1.sessions.index.resize",
7077                json!({
7078                    "subscription": subscription, "limit": limit
7079                }),
7080            ));
7081            assert!(response.get("error").is_none(), "{response:#}");
7082            assert_eq!(response["result"]["subscription"], subscription);
7083            assert_eq!(response["result"]["revision"], revision);
7084            assert_eq!(
7085                response["result"]["initial"].as_array().unwrap().len(),
7086                limit
7087            );
7088            assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7089            assert_eq!(service.index_subscriptions.len(), 1);
7090        }
7091        let removed = service.handle(request(
7092            4,
7093            "harness.v1.sessions.index.unsubscribe",
7094            json!({
7095                "subscription": subscription
7096            }),
7097        ));
7098        assert_eq!(removed["result"]["removed"], true);
7099        let stale = service.handle(request(
7100            5,
7101            "harness.v1.sessions.index.resize",
7102            json!({
7103                "subscription": subscription, "limit": 1
7104            }),
7105        ));
7106        assert_eq!(stale["error"]["code"], -32602);
7107        drop(service);
7108        std::fs::remove_dir_all(root).unwrap();
7109    }
7110
7111    fn skills_rows(params: Value) -> Vec<Value> {
7112        let response =
7113            HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7114        assert!(response.get("error").is_none(), "{response:#}");
7115        response["result"].as_array().cloned().unwrap_or_default()
7116    }
7117
7118    /// The uniform row over two harnesses at once, from the harnesses' own
7119    /// skill roots: name, harness, scope, location, description, version.
7120    #[test]
7121    fn skills_list_reads_the_hermes_and_openclaw_roots() {
7122        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7123        let rows = skills_rows(json!({
7124            "homes": fixture_homes(),
7125            "cwd": fixtures.join("hermes_home"),
7126        }));
7127        let arxiv = rows
7128            .iter()
7129            .find(|row| row["name"] == json!("arxiv-search"))
7130            .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7131        assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7132        assert_eq!(arxiv["scope"], json!("user"));
7133        assert_eq!(arxiv["version"], json!("1.4.0"));
7134        assert!(arxiv["location"]
7135            .as_str()
7136            .unwrap()
7137            .ends_with("hermes_home/skills/research/arxiv"));
7138
7139        // A directory with no SKILL.md still lists, by directory name.
7140        let bare = rows
7141            .iter()
7142            .find(|row| row["name"] == json!("bare-skill"))
7143            .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7144        assert_eq!(bare["enabled"], json!(null));
7145        assert!(bare.get("description").is_none());
7146
7147        let demo = rows
7148            .iter()
7149            .find(|row| row["name"] == json!("clawhub-demo"))
7150            .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7151        assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7152        assert_eq!(demo["scope"], json!("managed"));
7153        assert_eq!(demo["enabled"], json!(false));
7154    }
7155
7156    /// Both filters select against the same rows.
7157    #[test]
7158    fn skills_list_filters_by_harness_and_scope() {
7159        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7160        let hermes = skills_rows(json!({
7161            "homes": fixture_homes(),
7162            "cwd": fixtures.join("hermes_home"),
7163            "harness": HarnessId::HERMES,
7164        }));
7165        assert!(!hermes.is_empty());
7166        assert!(hermes
7167            .iter()
7168            .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7169
7170        let managed = skills_rows(json!({
7171            "homes": fixture_homes(),
7172            "cwd": fixtures.join("openclaw_home"),
7173            "harness": HarnessId::OPENCLAW,
7174            "scope": "managed",
7175        }));
7176        assert_eq!(managed.len(), 1, "{managed:#?}");
7177        assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7178
7179        let bundled = skills_rows(json!({
7180            "homes": fixture_homes(),
7181            "cwd": fixtures.join("openclaw_home"),
7182            "harness": HarnessId::OPENCLAW,
7183            "scope": "bundled",
7184        }));
7185        assert!(bundled.is_empty(), "{bundled:#?}");
7186    }
7187
7188    /// A harness supercode has no skills root for is refused by name, not
7189    /// answered with an empty list.
7190    #[test]
7191    fn skills_list_refuses_an_unknown_harness() {
7192        let response = HarnessSessionService::new().handle(request(
7193            1,
7194            "harness.v1.skills.list",
7195            json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7196        ));
7197        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7198        assert!(response["error"]["message"]
7199            .as_str()
7200            .unwrap()
7201            .contains("not-a-harness"));
7202    }
7203
7204    /// The method is advertised, and its SDK operation resolves it.
7205    #[test]
7206    fn skills_list_is_an_advertised_method_and_sdk_operation() {
7207        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7208        assert_eq!(
7209            SdkOperation::from_method("harness.v1.skills.list"),
7210            Some(SdkOperation::SkillsList)
7211        );
7212    }
7213
7214    // ---- ORCH-22: `harness.v1.skills.install|remove` ----------------------
7215
7216    /// Both controlled verbs are advertised and resolve to their operation.
7217    #[test]
7218    fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7219        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7220        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7221        assert_eq!(
7222            SdkOperation::from_method("harness.v1.skills.install"),
7223            Some(SdkOperation::SkillsInstall)
7224        );
7225        assert_eq!(
7226            SdkOperation::from_method("harness.v1.skills.remove"),
7227            Some(SdkOperation::SkillsRemove)
7228        );
7229    }
7230
7231    /// The directory door, end to end over the RPC: a local package lands in
7232    /// Claude Code's own user root and the outcome carries the operation and
7233    /// the row the ORCH-11 loader reads back.
7234    #[test]
7235    fn skills_install_and_remove_drive_the_directory_door() {
7236        let root = std::env::temp_dir().join(format!(
7237            "supercode-orch22-rpc-{}-{}",
7238            std::process::id(),
7239            std::time::SystemTime::now()
7240                .duration_since(std::time::UNIX_EPOCH)
7241                .unwrap()
7242                .as_nanos()
7243        ));
7244        let source = root.join("probe-src");
7245        std::fs::create_dir_all(&source).unwrap();
7246        std::fs::write(
7247            source.join("SKILL.md"),
7248            "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7249        )
7250        .unwrap();
7251        let homes = json!({
7252            "claude_code": root.join("claude_home"),
7253            "codex": root.join("__absent__"),
7254            "opencode": root.join("__absent__"),
7255            "pi": root.join("__absent__"),
7256            "hermes": root.join("__absent__"),
7257            "openclaw": root.join("__absent__"),
7258            "agents": root.join("__absent__"),
7259        });
7260
7261        let mut service = HarnessSessionService::new();
7262        let installed = service.handle(request(
7263            1,
7264            "harness.v1.skills.install",
7265            json!({
7266                "harness": HarnessId::CLAUDE_CODE,
7267                "source": source,
7268                "scope": "user",
7269                "cwd": root,
7270                "homes": homes,
7271            }),
7272        ));
7273        let result = &installed["result"];
7274        assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7275        assert_eq!(result["verb"], json!("install"));
7276        assert!(result["ran"]
7277            .as_str()
7278            .is_some_and(|ran| ran.starts_with("cp -R ")));
7279        assert_eq!(result["skill"]["scope"], json!("user"));
7280
7281        let removed = service.handle(request(
7282            2,
7283            "harness.v1.skills.remove",
7284            json!({
7285                "harness": HarnessId::CLAUDE_CODE,
7286                "name": "orch22-rpc",
7287                "scope": "user",
7288                "cwd": root,
7289                "homes": homes,
7290            }),
7291        ));
7292        assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7293        assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7294        std::fs::remove_dir_all(&root).ok();
7295    }
7296
7297    /// OpenClaw publishes no `skills remove` at the pin, so the uniform verb
7298    /// refuses with UnsupportedAction instead of deleting files itself.
7299    #[test]
7300    fn skills_remove_refuses_openclaw_at_the_pin() {
7301        let response = HarnessSessionService::new().handle(request(
7302            1,
7303            "harness.v1.skills.remove",
7304            json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7305        ));
7306        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7307        assert!(response["error"]["message"]
7308            .as_str()
7309            .unwrap()
7310            .contains("no `skills remove` verb"));
7311    }
7312
7313    /// A harness with no skills root at all is refused by name, with the
7314    /// same sentence `skills.list` gives it.
7315    #[test]
7316    fn skills_install_refuses_a_harness_without_a_skills_root() {
7317        let response = HarnessSessionService::new().handle(request(
7318            1,
7319            "harness.v1.skills.install",
7320            json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7321        ));
7322        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7323        assert!(response["error"]["message"]
7324            .as_str()
7325            .unwrap()
7326            .contains("not-a-harness"));
7327    }
7328
7329    // ---- ORCH-12: `harness.v1.memory.show|search` ------------------------
7330
7331    /// `HarnessHomes` for the committed fixture homes. Every root a test does
7332    /// not name is pinned at an absent path, so a read can never fall through
7333    /// to this machine's real harness homes. Note `hermes` is the `state.db`
7334    /// PATH (its parent is HERMES_HOME) and `claude_code` is the `projects`
7335    /// directory — the same contract discovery uses.
7336    fn memory_homes() -> Value {
7337        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7338        json!({
7339            "claude_code": fixtures.join("__absent__"),
7340            "codex": fixtures.join("__absent__"),
7341            "opencode": fixtures.join("__absent__"),
7342            "pi": fixtures.join("__absent__"),
7343            "grok": fixtures.join("__absent__"),
7344            "gemini": fixtures.join("__absent__"),
7345            "goose": fixtures.join("__absent__"),
7346            "supercode": fixtures.join("__absent__"),
7347            "hermes": fixtures.join("hermes_home/state.db"),
7348            "openclaw": fixtures.join("openclaw_home"),
7349        })
7350    }
7351
7352    fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7353        let response = HarnessSessionService::new().handle(request(1, method, params));
7354        assert!(response.get("error").is_none(), "{response:#}");
7355        assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7356        response["result"][key]
7357            .as_array()
7358            .cloned()
7359            .unwrap_or_default()
7360    }
7361
7362    fn memory_documents(params: Value) -> Vec<Value> {
7363        memory_call_ok("harness.v1.memory.show", params, "documents")
7364    }
7365
7366    fn memory_matches(params: Value) -> Vec<Value> {
7367        memory_call_ok("harness.v1.memory.search", params, "matches")
7368    }
7369
7370    fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7371        rows.iter()
7372            .find(|row| row["profile"] == profile && row["name"] == name)
7373            .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7374    }
7375
7376    /// Hermes: the built-in `MEMORY.md`/`USER.md` pair and the `memories/`
7377    /// topic files, for HERMES_HOME itself and for every profile home.
7378    #[test]
7379    fn memory_show_reads_the_hermes_profile_homes() {
7380        let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7381
7382        let notes = find_document(&rows, "default", "MEMORY.md");
7383        assert_eq!(notes["harness"], "hermes");
7384        assert_eq!(notes["scope"], "user");
7385        assert!(notes["size"].as_u64().unwrap() > 0);
7386        assert!(notes["updated_at"].is_string(), "{notes:#?}");
7387        // The default answer previews the head and never the whole body.
7388        assert!(notes.get("content").is_none(), "{notes:#?}");
7389        assert_eq!(notes["truncated"], true);
7390        assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7391
7392        let user = find_document(&rows, "default", "USER.md");
7393        assert_eq!(user["scope"], "user");
7394        assert!(user["preview"]
7395            .as_array()
7396            .unwrap()
7397            .iter()
7398            .any(|line| line.as_str().unwrap().contains("neovim")));
7399
7400        let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7401        assert!(topic["path"]
7402            .as_str()
7403            .unwrap()
7404            .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7405
7406        // Profile mode points HERMES_HOME at `<root>/profiles/<name>`.
7407        let coder = find_document(&rows, "coder", "MEMORY.md");
7408        assert_eq!(coder["scope"], "profile");
7409        assert!(coder["path"]
7410            .as_str()
7411            .unwrap()
7412            .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7413    }
7414
7415    /// `full` is the only way a body crosses the wire, and `profile` narrows
7416    /// the read to one home.
7417    #[test]
7418    fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7419        let rows = memory_documents(json!({
7420            "harness": "hermes",
7421            "profile": "coder",
7422            "full": true,
7423            "homes": memory_homes(),
7424        }));
7425        assert!(
7426            rows.iter().all(|row| row["profile"] == "coder"),
7427            "{rows:#?}"
7428        );
7429        let coder = find_document(&rows, "coder", "MEMORY.md");
7430        assert!(coder["content"]
7431            .as_str()
7432            .expect("full returns the body")
7433            .contains("anthropic/claude-opus-4-8"));
7434    }
7435
7436    /// OpenClaw: memory-core's files under each agent's workspace —
7437    /// `<state>/workspace` for the default agent, `<state>/workspace-<id>`
7438    /// for any other.
7439    #[test]
7440    fn memory_show_reads_the_openclaw_agent_workspaces() {
7441        let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7442
7443        let main = find_document(&rows, "main", "MEMORY.md");
7444        assert_eq!(main["scope"], "agent");
7445        assert!(main["path"]
7446            .as_str()
7447            .unwrap()
7448            .ends_with("openclaw_home/workspace/MEMORY.md"));
7449
7450        let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7451        assert!(topic["path"]
7452            .as_str()
7453            .unwrap()
7454            .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7455
7456        let design = find_document(&rows, "design", "MEMORY.md");
7457        assert!(design["path"]
7458            .as_str()
7459            .unwrap()
7460            .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7461    }
7462
7463    /// Claude Code: the auto-memory directory of the project the working tree
7464    /// belongs to, keyed by the enclosing git repository.
7465    #[test]
7466    fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7467        let scratch = std::env::temp_dir().join(format!(
7468            "supercode-orch12-cc-{}-{}",
7469            std::process::id(),
7470            std::time::SystemTime::now()
7471                .duration_since(std::time::UNIX_EPOCH)
7472                .unwrap()
7473                .as_nanos()
7474        ));
7475        let project = scratch.join("repo");
7476        std::fs::create_dir_all(project.join(".git")).unwrap();
7477        // Auto-memory is shared across a repo's worktrees, so a nested
7478        // working directory must resolve to the repo's own project dir.
7479        let worktree = project.join("crates/harness");
7480        std::fs::create_dir_all(&worktree).unwrap();
7481        let slug: String = project
7482            .to_string_lossy()
7483            .chars()
7484            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7485            .collect();
7486        let projects = scratch.join("claude/projects");
7487        let memory = projects.join(&slug).join("memory");
7488        std::fs::create_dir_all(&memory).unwrap();
7489        std::fs::write(
7490            memory.join("MEMORY.md"),
7491            "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7492        )
7493        .unwrap();
7494        std::fs::write(
7495            memory.join("build-box.md"),
7496            "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7497        )
7498        .unwrap();
7499
7500        let mut homes = memory_homes();
7501        homes["claude_code"] = json!(projects);
7502        let rows = memory_documents(json!({
7503            "harness": "claude-code",
7504            "cwd": worktree,
7505            "homes": homes,
7506        }));
7507        let index = find_document(&rows, &slug, "MEMORY.md");
7508        assert_eq!(index["harness"], "claude-code");
7509        assert_eq!(index["scope"], "project");
7510        let topic = find_document(&rows, &slug, "build-box.md");
7511        assert!(topic["preview"]
7512            .as_array()
7513            .unwrap()
7514            .iter()
7515            .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7516
7517        let hits = memory_matches(json!({
7518            "harness": "claude-code",
7519            "query": "pinned harnesses",
7520            "cwd": worktree,
7521            "homes": homes,
7522        }));
7523        assert_eq!(hits.len(), 1, "{hits:#?}");
7524        assert_eq!(hits[0]["name"], "MEMORY.md");
7525        assert_eq!(hits[0]["line"], 2);
7526
7527        let _ = std::fs::remove_dir_all(&scratch);
7528    }
7529
7530    /// A config-less OpenClaw install declares no default agent, but
7531    /// memory-core still resolves ONE agent to the default `workspace`
7532    /// directory — the same `main`-then-first convention the profile rows
7533    /// use. Measured against `openclaw memory status` on the pinned CLI
7534    /// (`docs/interop/research/orch12-memory-receipt-2026-09-03.json`).
7535    #[test]
7536    fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7537        let state = std::env::temp_dir().join(format!(
7538            "supercode-orch12-oc-{}-{}",
7539            std::process::id(),
7540            std::time::SystemTime::now()
7541                .duration_since(std::time::UNIX_EPOCH)
7542                .unwrap()
7543                .as_nanos()
7544        ));
7545        // No `openclaw.json`: only the agent home the gateway creates.
7546        std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7547        std::fs::create_dir_all(state.join("workspace")).unwrap();
7548        std::fs::write(
7549            state.join("workspace/MEMORY.md"),
7550            "the gateway websocket needs credentials\n",
7551        )
7552        .unwrap();
7553
7554        let mut homes = memory_homes();
7555        homes["openclaw"] = json!(state);
7556        let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7557        assert_eq!(rows.len(), 1, "{rows:#?}");
7558        let row = find_document(&rows, "main", "MEMORY.md");
7559        assert_eq!(row["scope"], "agent");
7560        assert!(row["path"]
7561            .as_str()
7562            .unwrap()
7563            .ends_with("workspace/MEMORY.md"));
7564
7565        let _ = std::fs::remove_dir_all(&state);
7566    }
7567
7568    /// Search is a plain scan over the same documents: a hit carries the
7569    /// path, line and excerpt; a miss is an empty list, not an error.
7570    #[test]
7571    fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7572        let hit = memory_matches(json!({
7573            "harness": "hermes",
7574            "query": "NEOVIM",
7575            "homes": memory_homes(),
7576        }));
7577        assert_eq!(hit.len(), 1, "{hit:#?}");
7578        assert_eq!(hit[0]["harness"], "hermes");
7579        assert_eq!(hit[0]["name"], "USER.md");
7580        assert_eq!(hit[0]["scope"], "user");
7581        assert_eq!(hit[0]["line"], 5);
7582        assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7583
7584        // A regular expression reaches the same lines.
7585        let regex = memory_matches(json!({
7586            "harness": "hermes",
7587            "query": "neo(vim|vi)",
7588            "regex": true,
7589            "homes": memory_homes(),
7590        }));
7591        assert_eq!(regex.len(), 1, "{regex:#?}");
7592
7593        let miss = memory_matches(json!({
7594            "harness": "hermes",
7595            "query": "no-memory-line-says-this",
7596            "homes": memory_homes(),
7597        }));
7598        assert!(miss.is_empty(), "{miss:#?}");
7599    }
7600
7601    /// The uniform-verb contract: a harness with no memory store at the pin
7602    /// is refused by name, and `session` only selects a Claude Code project.
7603    #[test]
7604    fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7605        for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7606            let response = HarnessSessionService::new().handle(request(
7607                1,
7608                method,
7609                json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7610            ));
7611            assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7612            assert!(response["error"]["message"]
7613                .as_str()
7614                .unwrap()
7615                .contains("codex"));
7616        }
7617
7618        let response = HarnessSessionService::new().handle(request(
7619            1,
7620            "harness.v1.memory.show",
7621            json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7622        ));
7623        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7624
7625        // `harness` is not optional: memory documents are the user's prose.
7626        let response = HarnessSessionService::new().handle(request(
7627            1,
7628            "harness.v1.memory.show",
7629            json!({"homes": memory_homes()}),
7630        ));
7631        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7632    }
7633
7634    /// Both methods are advertised, and their SDK operations resolve them.
7635    #[test]
7636    fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7637        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7638        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7639        assert_eq!(
7640            SdkOperation::from_method("harness.v1.memory.show"),
7641            Some(SdkOperation::MemoryShow)
7642        );
7643        assert_eq!(
7644            SdkOperation::from_method("harness.v1.memory.search"),
7645            Some(SdkOperation::MemorySearch)
7646        );
7647    }
7648
7649    // ---- ORCH-9: `harness.v1.approvals.list` -----------------------------
7650
7651    /// A runtime that raises one protocol request and then goes quiet, so a
7652    /// single poll delivers the request without closing the connection.
7653    struct RequestingRuntime {
7654        handle: RuntimeHandle,
7655        events: std::collections::VecDeque<HarnessEvent>,
7656        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7657    }
7658
7659    #[async_trait]
7660    impl RuntimeConnection for RequestingRuntime {
7661        fn handle(&self) -> &RuntimeHandle {
7662            &self.handle
7663        }
7664
7665        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7666            unreachable!("this runtime only raises requests")
7667        }
7668
7669        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7670            match self.events.pop_front() {
7671                Some(event) => Ok(Some(event)),
7672                // Quiet, not closed: `poll_sdk_events` times out and leaves
7673                // the connection open, the way a runtime blocked on a
7674                // permission request behaves.
7675                None => std::future::pending().await,
7676            }
7677        }
7678
7679        async fn interrupt(&mut self) -> crate::Result<()> {
7680            Ok(())
7681        }
7682
7683        async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7684            // Both halves are recorded: ORCH-20 has to prove not just that the
7685            // right request was answered but that the door received its own
7686            // reply envelope.
7687            self.answered
7688                .lock()
7689                .unwrap_or_else(std::sync::PoisonError::into_inner)
7690                .push(json!({"request_id": request_id, "response": response}));
7691            Ok(())
7692        }
7693
7694        async fn close(&mut self) -> crate::Result<()> {
7695            Ok(())
7696        }
7697    }
7698
7699    fn requesting_runtime(
7700        harness: &str,
7701        events: Vec<HarnessEvent>,
7702        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7703    ) -> Box<dyn RuntimeConnection> {
7704        requesting_runtime_named(harness, "hermes-live-session", events, answered)
7705    }
7706
7707    fn requesting_runtime_named(
7708        harness: &str,
7709        runtime_id: &str,
7710        events: Vec<HarnessEvent>,
7711        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7712    ) -> Box<dyn RuntimeConnection> {
7713        Box::new(RequestingRuntime {
7714            handle: RuntimeHandle {
7715                harness: HarnessId::from(harness),
7716                runtime_id: runtime_id.into(),
7717                endpoint: RuntimeEndpoint::LocalProcess {
7718                    pid: None,
7719                    command: vec!["hermes-acp".into()],
7720                    protocol: "acp".into(),
7721                },
7722            },
7723            events: events.into(),
7724            answered,
7725        })
7726    }
7727
7728    fn permission_event(id: u64, title: &str) -> HarnessEvent {
7729        HarnessEvent {
7730            sequence: None,
7731            kind: "session/request_permission".into(),
7732            payload: json!({
7733                "jsonrpc": "2.0",
7734                "id": id,
7735                "method": "session/request_permission",
7736                "params": {
7737                    "sessionId": "hermes-live-session",
7738                    "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7739                    "options": [
7740                        {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7741                        {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7742                        {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7743                    ],
7744                },
7745            }),
7746        }
7747    }
7748
7749    fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7750        let response = service.handle(request(1, "harness.v1.approvals.list", params));
7751        assert!(response.get("error").is_none(), "{response:#}");
7752        response["result"].clone()
7753    }
7754
7755    /// ORC-2 dev/01: the same uniform loop over the CLAUDE CODE door. The
7756    /// `can_use_tool` control request the CLI raises to its registered
7757    /// permission handler lists as one pending row, `approvals.resolve <id>
7758    /// allow_once` sends the `{behavior}` result the CLI accepts through
7759    /// `runtimes.respond`, and the row is gone. The frame is the one claude
7760    /// 2.1.258 wrote, transcribed from
7761    /// `docs/interop/research/orc2-claude-respond-receipt-2026-09-04.json`.
7762    #[tokio::test]
7763    async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7764        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7765        let mut service = HarnessSessionService::new();
7766        service.runtimes.insert(
7767            "runtime-cc".into(),
7768            requesting_runtime_named(
7769                HarnessId::CLAUDE_CODE,
7770                "claude-live-session",
7771                vec![HarnessEvent {
7772                    sequence: None,
7773                    kind: "control_request".into(),
7774                    payload: json!({
7775                        "type": "control_request",
7776                        "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7777                        "request": {
7778                            "subtype": "can_use_tool",
7779                            "tool_name": "Bash",
7780                            "display_name": "Bash",
7781                            "input": {"command": "touch probe-artifact.txt"},
7782                            "tool_use_id": "toolu_mock_1",
7783                        },
7784                    }),
7785                }],
7786                answered.clone(),
7787            ),
7788        );
7789
7790        let notifications = service.poll_runtimes().await;
7791        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7792
7793        let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7794        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7795        let row = &rows[0];
7796        assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7797        assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7798        assert_eq!(row["status"], "pending");
7799        assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7800        assert_eq!(row["runtime_id"], "claude-live-session");
7801        assert_eq!(
7802            row["options"]
7803                .as_array()
7804                .unwrap()
7805                .iter()
7806                .map(|option| option["id"].as_str().unwrap())
7807                .collect::<Vec<_>>(),
7808            vec!["allow", "deny"],
7809        );
7810
7811        let response = resolve(
7812            &mut service,
7813            json!({"id": row["id"], "decision": "allow_once"}),
7814        )
7815        .await;
7816        assert!(response.get("error").is_none(), "{response:#}");
7817        assert_eq!(response["result"]["option_id"], "allow");
7818        assert_eq!(
7819            answered
7820                .lock()
7821                .unwrap_or_else(std::sync::PoisonError::into_inner)
7822                .as_slice(),
7823            &[json!({
7824                "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7825                "response": {"behavior": "allow"},
7826            })],
7827        );
7828        assert_eq!(
7829            approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7830                .as_array()
7831                .map(Vec::len),
7832            Some(0),
7833        );
7834    }
7835
7836    /// dev/01: a live ACP permission request raised on a driven runtime is
7837    /// listable while the turn is blocked on it, and stops being listable
7838    /// the moment `runtimes.respond` answers it.
7839    #[tokio::test]
7840    async fn a_live_permission_request_lists_until_it_is_answered() {
7841        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7842        let mut service = HarnessSessionService::new();
7843        service.runtimes.insert(
7844            "runtime-1".into(),
7845            requesting_runtime(
7846                HarnessId::HERMES,
7847                vec![permission_event(7, "rm -rf build")],
7848                answered.clone(),
7849            ),
7850        );
7851
7852        let notifications = service.poll_runtimes().await;
7853        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7854
7855        let rows = approvals(&mut service, json!({}));
7856        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7857        let row = &rows[0];
7858        assert_eq!(row["id"], "runtime-1/7");
7859        assert_eq!(row["harness"], HarnessId::HERMES);
7860        assert_eq!(row["kind"], "live");
7861        assert_eq!(row["status"], "pending");
7862        assert_eq!(row["subject"], "rm -rf build");
7863        assert_eq!(row["session_id"], "hermes-live-session");
7864        assert_eq!(row["runtime_id"], "hermes-live-session");
7865        assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7866        assert!(
7867            row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7868            "{row:#}"
7869        );
7870        assert_eq!(
7871            row["options"]
7872                .as_array()
7873                .unwrap()
7874                .iter()
7875                .map(|option| option["id"].as_str().unwrap())
7876                .collect::<Vec<_>>(),
7877            vec!["allow_once", "allow_for_session", "deny"],
7878        );
7879
7880        // The filters select against the same rows.
7881        assert_eq!(
7882            approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7883                .as_array()
7884                .map(Vec::len),
7885            Some(1),
7886        );
7887        assert_eq!(
7888            approvals(&mut service, json!({"session": "some-other-session"}))
7889                .as_array()
7890                .map(Vec::len),
7891            Some(0),
7892        );
7893
7894        let response = service
7895            .handle_async(request(
7896                2,
7897                "harness.v1.runtimes.respond",
7898                json!({
7899                    "connection": "runtime-1",
7900                    "request_id": 7,
7901                    "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7902                }),
7903            ))
7904            .await;
7905        assert!(response.get("error").is_none(), "{response:#}");
7906        assert_eq!(
7907            answered
7908                .lock()
7909                .unwrap_or_else(std::sync::PoisonError::into_inner)
7910                .as_slice(),
7911            &[json!({
7912                "request_id": 7,
7913                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7914            })],
7915        );
7916
7917        let rows = approvals(&mut service, json!({}));
7918        assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7919    }
7920
7921    /// dev/01: supercode's own queued subagent approvals list through the
7922    /// same door, carrying the outcome the record holds.
7923    #[test]
7924    fn queued_subagent_approvals_list_through_the_same_door() {
7925        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7926            crate::subagents::QueuedApproval {
7927                child_agent_id: "child-7".into(),
7928                tool: "shell".into(),
7929                subject: Some("cargo publish --dry-run".into()),
7930                queued_at_ms: 1,
7931                outcome: None,
7932            },
7933            crate::subagents::QueuedApproval {
7934                child_agent_id: "child-8".into(),
7935                tool: "write_file".into(),
7936                subject: None,
7937                queued_at_ms: 2,
7938                outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7939            },
7940        ]));
7941        let mut service = HarnessSessionService::new();
7942        service.observe_subagent_approvals(queue);
7943
7944        let rows = approvals(&mut service, json!({}));
7945        assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
7946        assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
7947        assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
7948        assert_eq!(rows[0]["status"], "pending");
7949        assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
7950        assert_eq!(rows[1]["status"], "denied");
7951        assert!(rows[1]["options"].as_array().unwrap().is_empty());
7952
7953        // `--session` addresses a subagent row by its child agent id.
7954        let only = approvals(&mut service, json!({"session": "child-8"}));
7955        assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
7956        assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
7957    }
7958
7959    /// The uniform-verb contract: an id whose runtime door cannot carry a
7960    /// protocol request is refused BY NAME rather than answered with an empty
7961    /// list. Since ORC-2 gave Claude Code a permission-response primitive
7962    /// every registered harness can carry one, so the refusal is exercised on
7963    /// an unknown id — and the registered ids are asserted to be accepted.
7964    #[test]
7965    fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
7966        let response = HarnessSessionService::new().handle(request(
7967            1,
7968            "harness.v1.approvals.list",
7969            json!({"harness": "not-a-harness"}),
7970        ));
7971        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7972        assert!(response["error"]["message"]
7973            .as_str()
7974            .unwrap()
7975            .contains("not-a-harness"));
7976        for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
7977            let response = HarnessSessionService::new().handle(request(
7978                1,
7979                "harness.v1.approvals.list",
7980                json!({"harness": harness}),
7981            ));
7982            assert!(response.get("error").is_none(), "{harness}: {response:#}");
7983        }
7984    }
7985
7986    /// The method is advertised, its SDK operation resolves it, and the
7987    /// registry reports the concept as observed for every harness whose
7988    /// runtime door can carry a request.
7989    #[test]
7990    fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
7991        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
7992        assert_eq!(
7993            SdkOperation::from_method("harness.v1.approvals.list"),
7994            Some(SdkOperation::ApprovalsList)
7995        );
7996        let registry = harness_support_registry();
7997        for id in [
7998            HarnessId::HERMES,
7999            HarnessId::OPENCLAW,
8000            HarnessId::CODEX,
8001            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8002            // pending_request concept joins the other driven doors.
8003            HarnessId::CLAUDE_CODE,
8004        ] {
8005            let concept = registry
8006                .harnesses
8007                .iter()
8008                .find(|harness| harness.id.as_str() == id)
8009                .unwrap()
8010                .orchestration
8011                .concepts
8012                .iter()
8013                .find(|concept| concept.concept == "pending_request")
8014                .unwrap();
8015            assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8016            assert!(concept
8017                .methods
8018                .iter()
8019                .any(|method| method == "harness.v1.approvals.list"));
8020        }
8021    }
8022
8023    // ---- ORCH-20: `harness.v1.approvals.resolve` -------------------------
8024
8025    async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8026        service
8027            .handle_async(request(3, "harness.v1.approvals.resolve", params))
8028            .await
8029    }
8030
8031    /// dev/01: the whole loop on a driven runtime — list one pending row,
8032    /// answer it by ROW ID with one uniform decision, and see it gone. The
8033    /// door receives its own ACP envelope carrying the option it enumerated.
8034    #[tokio::test]
8035    async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8036        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8037        let mut service = HarnessSessionService::new();
8038        service.runtimes.insert(
8039            "runtime-1".into(),
8040            requesting_runtime(
8041                HarnessId::HERMES,
8042                vec![permission_event(7, "rm -rf build")],
8043                answered.clone(),
8044            ),
8045        );
8046        service.poll_runtimes().await;
8047
8048        let rows = approvals(&mut service, json!({}));
8049        assert_eq!(rows[0]["id"], "runtime-1/7");
8050
8051        let response = resolve(
8052            &mut service,
8053            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8054        )
8055        .await;
8056        assert!(response.get("error").is_none(), "{response:#}");
8057        assert_eq!(
8058            response["result"],
8059            json!({
8060                "id": "runtime-1/7",
8061                "decision": "allow_once",
8062                "option_id": "allow_once",
8063                "resolved": true,
8064            }),
8065        );
8066        // The harness's own door was called with its own envelope.
8067        assert_eq!(
8068            answered
8069                .lock()
8070                .unwrap_or_else(std::sync::PoisonError::into_inner)
8071                .as_slice(),
8072            &[json!({
8073                "request_id": 7,
8074                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8075            })],
8076        );
8077        // And the row is gone, the same way `runtimes.respond` drops it.
8078        assert_eq!(
8079            approvals(&mut service, json!({})).as_array().map(Vec::len),
8080            Some(0),
8081        );
8082        // Answering it twice is an honest miss, not a silent success.
8083        let response = resolve(
8084            &mut service,
8085            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8086        )
8087        .await;
8088        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8089    }
8090
8091    /// dev/01: deny travels the same path and picks the option the request
8092    /// itself classified as a refusal.
8093    #[tokio::test]
8094    async fn deny_selects_the_requests_own_reject_option() {
8095        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8096        let mut service = HarnessSessionService::new();
8097        service.runtimes.insert(
8098            "runtime-1".into(),
8099            requesting_runtime(
8100                HarnessId::HERMES,
8101                vec![permission_event(11, "git push --force")],
8102                answered.clone(),
8103            ),
8104        );
8105        service.poll_runtimes().await;
8106
8107        let response = resolve(
8108            &mut service,
8109            json!({"id": "runtime-1/11", "decision": "deny"}),
8110        )
8111        .await;
8112        assert!(response.get("error").is_none(), "{response:#}");
8113        // `deny` is the optionId whose ACP `kind` is `reject_once`.
8114        assert_eq!(response["result"]["option_id"], "deny");
8115        assert_eq!(
8116            answered
8117                .lock()
8118                .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8119            json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8120        );
8121        assert_eq!(
8122            approvals(&mut service, json!({})).as_array().map(Vec::len),
8123            Some(0),
8124        );
8125    }
8126
8127    /// dev/01: a decision this request does not offer is refused by name,
8128    /// listing the ones it does — never silently downgraded to a neighbour.
8129    #[tokio::test]
8130    async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8131        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8132        let mut service = HarnessSessionService::new();
8133        let mut event = permission_event(3, "rm -rf build");
8134        // A request offering only allow-once and deny, as hermes 0.21.0's
8135        // edit-approval layer raises one.
8136        event.payload["params"]["options"] = json!([
8137            {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8138            {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8139        ]);
8140        service.runtimes.insert(
8141            "runtime-1".into(),
8142            requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8143        );
8144        service.poll_runtimes().await;
8145
8146        let response = resolve(
8147            &mut service,
8148            json!({"id": "runtime-1/3", "decision": "allow_always"}),
8149        )
8150        .await;
8151        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8152        let message = response["error"]["message"].as_str().unwrap();
8153        assert!(message.contains("allow_always"), "{message}");
8154        assert!(message.contains("allow_once, deny"), "{message}");
8155        // Nothing was sent, and the request is still waiting for an answer.
8156        assert!(answered
8157            .lock()
8158            .unwrap_or_else(std::sync::PoisonError::into_inner)
8159            .is_empty());
8160        assert_eq!(
8161            approvals(&mut service, json!({})).as_array().map(Vec::len),
8162            Some(1),
8163        );
8164    }
8165
8166    /// dev/01: supercode's own queued subagent row is addressable but not
8167    /// answerable through this door — it is the parent's audit copy of a
8168    /// request its own handler answers. Refused by name, never a no-op.
8169    #[tokio::test]
8170    async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8171        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8172            crate::subagents::QueuedApproval {
8173                child_agent_id: "child-7".into(),
8174                tool: "shell".into(),
8175                subject: Some("cargo publish --dry-run".into()),
8176                queued_at_ms: 1,
8177                outcome: None,
8178            },
8179        ]));
8180        let mut service = HarnessSessionService::new();
8181        service.observe_subagent_approvals(queue.clone());
8182        let row = approvals(&mut service, json!({}))[0]["id"]
8183            .as_str()
8184            .unwrap()
8185            .to_string();
8186        assert_eq!(row, "supercode/subagent/child-7/1/0");
8187
8188        let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8189        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8190        let message = response["error"]["message"].as_str().unwrap();
8191        assert!(message.contains("queued subagent record"), "{message}");
8192        assert!(message.contains("request"), "{message}");
8193        // The audit record is untouched: nothing pretended to answer it.
8194        assert!(queue
8195            .lock()
8196            .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8197            .outcome
8198            .is_none());
8199    }
8200
8201    /// An id nobody is holding, and a call that names no decision at all,
8202    /// both fail with a message that says why.
8203    #[tokio::test]
8204    async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8205        let mut service = HarnessSessionService::new();
8206        let response = resolve(
8207            &mut service,
8208            json!({"id": "runtime-9/4", "decision": "deny"}),
8209        )
8210        .await;
8211        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8212        assert!(response["error"]["message"]
8213            .as_str()
8214            .unwrap()
8215            .contains("runtime-9/4"));
8216
8217        let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8218        let message = response["error"]["message"].as_str().unwrap();
8219        assert!(
8220            message.contains("allow_once | allow_always | deny"),
8221            "{message}"
8222        );
8223
8224        let response = resolve(
8225            &mut service,
8226            json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8227        )
8228        .await;
8229        assert!(response["error"]["message"]
8230            .as_str()
8231            .unwrap()
8232            .contains("not both"));
8233    }
8234
8235    /// The method is advertised, its SDK operation resolves it, and every
8236    /// harness whose runtime door can carry a request reports it on the
8237    /// CONTROLLED tier beside `runtimes.respond`.
8238    #[test]
8239    fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8240        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8241        assert_eq!(
8242            SdkOperation::from_method("harness.v1.approvals.resolve"),
8243            Some(SdkOperation::ApprovalsResolve)
8244        );
8245        assert_eq!(
8246            SdkOperation::ApprovalsResolve.action_name(),
8247            "approvals_resolve"
8248        );
8249        let registry = harness_support_registry();
8250        for id in [
8251            HarnessId::HERMES,
8252            HarnessId::OPENCLAW,
8253            HarnessId::CODEX,
8254            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8255            // pending_request concept joins the other driven doors.
8256            HarnessId::CLAUDE_CODE,
8257        ] {
8258            let concept = registry
8259                .harnesses
8260                .iter()
8261                .find(|harness| harness.id.as_str() == id)
8262                .unwrap()
8263                .orchestration
8264                .concepts
8265                .iter()
8266                .find(|concept| concept.concept == "pending_request")
8267                .unwrap();
8268            assert_eq!(
8269                concept.controlled,
8270                crate::ImplementationKind::BuiltIn,
8271                "{id}"
8272            );
8273            assert!(
8274                concept
8275                    .methods
8276                    .iter()
8277                    .any(|method| method == "harness.v1.approvals.resolve"),
8278                "{id}"
8279            );
8280        }
8281    }
8282
8283    #[test]
8284    fn capabilities_are_explicit_and_versioned() {
8285        let mut service = HarnessSessionService::new();
8286        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8287        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8288        assert_eq!(
8289            response["result"]["sdk"]["schema_version"],
8290            crate::SDK_SCHEMA_VERSION
8291        );
8292        assert_eq!(
8293            response["result"]["sdk"]["operations"]
8294                .as_array()
8295                .unwrap()
8296                .len(),
8297            SdkOperation::ALL.len()
8298        );
8299        assert_eq!(
8300            response["result"]["harnesses"].as_array().unwrap().len(),
8301            11
8302        );
8303        assert!(response["result"]["harnesses"]
8304            .as_array()
8305            .unwrap()
8306            .iter()
8307            .any(|harness| harness == HarnessId::GROK));
8308        assert!(response["result"]["harnesses"]
8309            .as_array()
8310            .unwrap()
8311            .iter()
8312            .any(|harness| harness == HarnessId::GOOSE));
8313    }
8314
8315    #[test]
8316    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8317        let noisy_stderr = crate::HarnessEvent {
8318            sequence: None,
8319            kind: "transport_stderr".into(),
8320            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8321        };
8322        assert_eq!(handshake_event_failure(&noisy_stderr), None);
8323
8324        let closed = crate::HarnessEvent {
8325            sequence: None,
8326            kind: "transport_closed".into(),
8327            payload: json!({}),
8328        };
8329        assert!(handshake_event_failure(&closed).is_some());
8330    }
8331
8332    #[tokio::test]
8333    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8334        let mut service = HarnessSessionService::new();
8335        service
8336            .runtimes
8337            .insert("raw-eof".into(), ending_runtime(None));
8338        service.runtimes.insert(
8339            "explicit-close".into(),
8340            ending_runtime(Some(HarnessEvent {
8341                sequence: None,
8342                kind: "transport_closed".into(),
8343                payload: json!({"message": "native transport exited"}),
8344            })),
8345        );
8346
8347        let notifications = service.poll_runtimes().await;
8348
8349        assert_eq!(notifications.len(), 2);
8350        assert!(notifications
8351            .iter()
8352            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8353        assert!(notifications.iter().all(|notification| {
8354            notification["params"]["session_id"] == "ending-session"
8355                && notification["params"]["connection"].is_string()
8356        }));
8357        let mut sequences = notifications
8358            .iter()
8359            .filter_map(|notification| notification["params"]["sequence"].as_u64())
8360            .collect::<Vec<_>>();
8361        sequences.sort_unstable();
8362        assert_eq!(sequences, vec![1, 2]);
8363        assert!(service.runtimes.is_empty());
8364    }
8365
8366    #[test]
8367    fn support_report_and_grok_default_binding_share_the_registry() {
8368        let mut service = HarnessSessionService::new();
8369        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8370        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8371        let params = RuntimeBackendParams {
8372            harness: HarnessId::from(HarnessId::GROK),
8373            protocol: None,
8374            launch: None,
8375            base_url: None,
8376            policy: RuntimePolicy::Default,
8377        };
8378        let backend = match runtime_backend(&params) {
8379            Ok(backend) => backend,
8380            Err(_) => panic!("Grok should bind through its registered ACP launch"),
8381        };
8382        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8383        assert!(backend.capabilities().start_session);
8384        let registered = harness_support_registry()
8385            .harnesses
8386            .into_iter()
8387            .find(|harness| harness.id.as_str() == HarnessId::GROK)
8388            .and_then(|harness| harness.runtime.default_launch)
8389            .unwrap();
8390        assert!(!registered
8391            .arguments
8392            .iter()
8393            .any(|argument| argument == "--always-approve"));
8394        assert!(runtime_launch(&params).is_none());
8395
8396        let yolo = RuntimeBackendParams {
8397            policy: RuntimePolicy::Yolo,
8398            ..params
8399        };
8400        assert!(runtime_launch(&yolo)
8401            .unwrap()
8402            .arguments
8403            .iter()
8404            .any(|argument| argument == "--always-approve"));
8405
8406        let mismatched_protocol = RuntimeBackendParams {
8407            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8408            protocol: Some("acp".into()),
8409            launch: None,
8410            base_url: None,
8411            policy: RuntimePolicy::Default,
8412        };
8413        assert!(runtime_backend(&mismatched_protocol).is_err());
8414    }
8415
8416    #[test]
8417    fn load_follow_and_unfollow_share_the_same_locator() {
8418        let mut service = HarnessSessionService::new();
8419        let locator = pi_locator();
8420        let loaded = service.handle(request(
8421            1,
8422            "harness.v1.sessions.load",
8423            json!({"locator": locator}),
8424        ));
8425        assert_eq!(
8426            loaded["result"]["session"]["session_id"],
8427            locator.session_id
8428        );
8429
8430        let followed = service.handle(request(
8431            2,
8432            "harness.v1.sessions.follow",
8433            json!({"locator": locator}),
8434        ));
8435        assert_eq!(followed["result"]["subscription"], "sub-1");
8436        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8437        assert!(service.poll().is_empty());
8438
8439        let unfollowed = service.handle(request(
8440            3,
8441            "harness.v1.sessions.unfollow",
8442            json!({"subscription": "sub-1"}),
8443        ));
8444        assert_eq!(unfollowed["result"]["removed"], true);
8445    }
8446
8447    #[test]
8448    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8449        let temp = std::env::temp_dir().join(format!(
8450            "supercode-bounded-view-{}-{}",
8451            std::process::id(),
8452            generated_session_id()
8453        ));
8454        let path = temp.join("parent.jsonl");
8455        let subagents = temp.join("parent/subagents");
8456        std::fs::create_dir_all(&subagents).unwrap();
8457        let long_last = "x".repeat(300);
8458        let parent_records = [
8459            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8460            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8461            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8462        ];
8463        std::fs::write(
8464            &path,
8465            format!(
8466                "{}\n",
8467                parent_records
8468                    .iter()
8469                    .map(Value::to_string)
8470                    .collect::<Vec<_>>()
8471                    .join("\n")
8472            ),
8473        )
8474        .unwrap();
8475        std::fs::write(
8476            subagents.join("agent-child.jsonl"),
8477            concat!(
8478                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8479                "\n",
8480            ),
8481        )
8482        .unwrap();
8483        let locator = SessionLocator {
8484            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8485            session_id: "parent".into(),
8486            storage: StorageLocator::File { path },
8487        };
8488        let mut service = HarnessSessionService::new();
8489
8490        let complete = service.handle(request(
8491            1,
8492            "harness.v1.sessions.load",
8493            json!({"locator": locator}),
8494        ));
8495        assert_eq!(
8496            complete["result"]["session"]["subagents"]
8497                .as_array()
8498                .unwrap()
8499                .len(),
8500            1
8501        );
8502
8503        let bounded = service.handle(request(
8504            2,
8505            "harness.v1.sessions.load",
8506            json!({
8507                "locator": locator,
8508                "view": {
8509                    "tail_messages": 1,
8510                    "max_message_chars": 256,
8511                    "include_subagents": false
8512                },
8513            }),
8514        ));
8515        let session = &bounded["result"]["session"];
8516        assert!(session["subagents"].as_array().unwrap().is_empty());
8517        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8518        assert_eq!(
8519            session["messages"][0]["content"],
8520            format!("{}\n…", "x".repeat(256))
8521        );
8522
8523        let followed = service.handle(request(
8524            3,
8525            "harness.v1.sessions.follow",
8526            json!({
8527                "locator": locator,
8528                "view": {
8529                    "tail_messages": 1,
8530                    "max_message_chars": 256,
8531                    "include_subagents": false
8532                },
8533            }),
8534        ));
8535        let initial = &followed["result"]["initial"]["session"];
8536        assert!(initial["subagents"].as_array().unwrap().is_empty());
8537        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8538
8539        let _ = std::fs::remove_dir_all(&temp);
8540    }
8541
8542    #[test]
8543    fn forty_megabyte_display_load_is_bounded_and_prompt() {
8544        let temp = std::env::temp_dir().join(format!(
8545            "supercode-large-display-view-{}-{}",
8546            std::process::id(),
8547            generated_session_id()
8548        ));
8549        std::fs::create_dir_all(&temp).unwrap();
8550        let path = temp.join("rollout.jsonl");
8551        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8552        writeln!(
8553            file,
8554            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8555        )
8556        .unwrap();
8557        let padding = "x".repeat(80 * 1024);
8558        for index in 0..512 {
8559            let marker = if index == 0 {
8560                "OLDEST-SHOULD-NOT-LOAD"
8561            } else if index == 511 {
8562                "LATEST-MUST-LOAD"
8563            } else {
8564                "bulk"
8565            };
8566            writeln!(
8567                file,
8568                "{}",
8569                json!({
8570                    "timestamp": "2026-01-01T00:00:01Z",
8571                    "type": "response_item",
8572                    "payload": {
8573                        "type": "message",
8574                        "role": "assistant",
8575                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8576                    },
8577                })
8578            )
8579            .unwrap();
8580        }
8581        file.flush().unwrap();
8582        drop(file);
8583        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8584
8585        let locator = SessionLocator {
8586            harness: HarnessId::from(HarnessId::CODEX),
8587            session_id: "large-display".into(),
8588            storage: StorageLocator::File { path },
8589        };
8590        let started = Instant::now();
8591        let response = HarnessSessionService::new().handle(request(
8592            1,
8593            "harness.v1.sessions.load",
8594            json!({
8595                "locator": locator,
8596                "view": {
8597                    "tail_messages": 500,
8598                    "max_message_chars": 1024,
8599                    "include_subagents": false,
8600                    "display_history": true,
8601                },
8602            }),
8603        ));
8604        let elapsed = started.elapsed();
8605        let wire = response.to_string();
8606        eprintln!(
8607            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8608            wire.len()
8609        );
8610        assert!(response.get("error").is_none(), "{response:#}");
8611        assert!(wire.contains("LATEST-MUST-LOAD"));
8612        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8613        assert!(
8614            wire.len() < 2 * 1024 * 1024,
8615            "bounded wire was {} bytes",
8616            wire.len()
8617        );
8618        assert!(
8619            elapsed.as_secs_f64() < 3.0,
8620            "bounded 40 MiB load took {elapsed:?}"
8621        );
8622
8623        let _ = std::fs::remove_dir_all(&temp);
8624    }
8625
8626    #[test]
8627    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8628        let temp = std::env::temp_dir().join(format!(
8629            "supercode-large-goose-view-{}-{}",
8630            std::process::id(),
8631            generated_session_id()
8632        ));
8633        std::fs::create_dir_all(&temp).unwrap();
8634        let path = temp.join("sessions.db");
8635        let connection = rusqlite::Connection::open(&path).unwrap();
8636        connection
8637            .execute_batch(
8638                "CREATE TABLE sessions (
8639                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8640                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8641                    session_type TEXT NOT NULL, extension_data TEXT,
8642                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8643                    archived_at TEXT
8644                 );
8645                 CREATE TABLE messages (
8646                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8647                    role TEXT NOT NULL, content_json TEXT NOT NULL,
8648                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
8649                 );",
8650            )
8651            .unwrap();
8652        connection
8653            .execute(
8654                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8655                rusqlite::params![
8656                    "goose-large",
8657                    "Large Goose session",
8658                    "/tmp",
8659                    "2026-01-01 00:00:00",
8660                    "2026-01-01 00:00:02",
8661                    "user",
8662                    "{}",
8663                    "auto",
8664                    "anthropic",
8665                    r#"{"model_name":"claude-sonnet"}"#,
8666                ],
8667            )
8668            .unwrap();
8669        let old_content = serde_json::to_string(&vec![json!({
8670            "type": "text",
8671            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8672        })])
8673        .unwrap();
8674        connection
8675            .execute(
8676                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8677                rusqlite::params!["goose-large", old_content],
8678            )
8679            .unwrap();
8680        connection
8681            .execute(
8682                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8683                rusqlite::params![
8684                    "goose-large",
8685                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8686                ],
8687            )
8688            .unwrap();
8689        drop(connection);
8690        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8691
8692        let locator = SessionLocator {
8693            harness: HarnessId::from(HarnessId::GOOSE),
8694            session_id: "goose-large".into(),
8695            storage: StorageLocator::Sqlite {
8696                path,
8697                selector: "goose-large".into(),
8698            },
8699        };
8700        let started = Instant::now();
8701        let response = HarnessSessionService::new().handle(request(
8702            1,
8703            "harness.v1.sessions.load",
8704            json!({
8705                "locator": locator,
8706                "view": {
8707                    "tail_messages": 1,
8708                    "max_message_chars": 1024,
8709                    "include_subagents": false,
8710                    "display_history": true,
8711                },
8712            }),
8713        ));
8714        let elapsed = started.elapsed();
8715        let wire = response.to_string();
8716        eprintln!(
8717            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8718            wire.len()
8719        );
8720        assert!(response.get("error").is_none(), "{response:#}");
8721        assert!(wire.contains("LATEST-MUST-LOAD"));
8722        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8723        assert!(
8724            wire.len() < 64 * 1024,
8725            "bounded wire was {} bytes",
8726            wire.len()
8727        );
8728        assert!(
8729            elapsed.as_secs_f64() < 1.0,
8730            "bounded Goose load took {elapsed:?}"
8731        );
8732
8733        let _ = std::fs::remove_dir_all(&temp);
8734    }
8735
8736    #[test]
8737    fn display_view_keeps_codex_assistant_history_across_compaction() {
8738        let temp = std::env::temp_dir().join(format!(
8739            "supercode-codex-display-view-{}-{}",
8740            std::process::id(),
8741            generated_session_id()
8742        ));
8743        std::fs::create_dir_all(&temp).unwrap();
8744        let path = temp.join("rollout.jsonl");
8745        std::fs::write(
8746            &path,
8747            concat!(
8748                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8749                "\n",
8750                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8751                "\n",
8752                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8753                "\n",
8754                r#"{"timestamp":"2026-01-01T00:00:03Z","type":"compacted","payload":{"replacement_history":[{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]},{"type":"compaction","encrypted_content":"opaque"}]}}"#,
8755                "\n",
8756                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8757                "\n",
8758                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8759                "\n",
8760            ),
8761        )
8762        .unwrap();
8763        let locator = SessionLocator {
8764            harness: HarnessId::from(HarnessId::CODEX),
8765            session_id: "codex-display".into(),
8766            storage: StorageLocator::File { path },
8767        };
8768        let mut service = HarnessSessionService::new();
8769
8770        let continuation = service.handle(request(
8771            1,
8772            "harness.v1.sessions.load",
8773            json!({"locator": locator}),
8774        ));
8775        let continuation_text = continuation["result"]["session"]["messages"].to_string();
8776        assert!(!continuation_text.contains("old answer"));
8777
8778        let display = service.handle(request(
8779            2,
8780            "harness.v1.sessions.load",
8781            json!({
8782                "locator": locator,
8783                "view": {
8784                    "tail_messages": 10,
8785                    "include_subagents": false,
8786                    "display_history": true,
8787                },
8788            }),
8789        ));
8790        let display_text = display["result"]["session"]["messages"].to_string();
8791        assert!(display_text.contains("old prompt"));
8792        assert!(display_text.contains("old answer"));
8793        assert!(display_text.contains("new prompt"));
8794        assert!(display_text.contains("new answer"));
8795
8796        let _ = std::fs::remove_dir_all(&temp);
8797    }
8798
8799    #[test]
8800    fn indexed_claude_windows_match_the_existing_wire_projection() {
8801        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8802            .join("tests/fixtures/claude_code_session.jsonl");
8803        let locator = SessionLocator {
8804            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8805            session_id: "fixture".into(),
8806            storage: StorageLocator::File { path },
8807        };
8808        let full = load_session(&locator).unwrap();
8809        for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8810            for offset in [0, 1, full.messages.len(), usize::MAX] {
8811                for limit in [0, 1, 3, usize::MAX] {
8812                    let options = SessionLoadOptions {
8813                        include_subagents: Some(false),
8814                        inline_media,
8815                        message_offset: Some(offset),
8816                        message_limit: Some(limit),
8817                        ..Default::default()
8818                    };
8819                    let expected = projected_session_result(&full, &options);
8820                    assert_eq!(
8821                        indexed_claude_window(&locator, &options).unwrap().unwrap(),
8822                        expected
8823                    );
8824                }
8825            }
8826            for tail in [0, 1, 3, usize::MAX] {
8827                let options = SessionLoadOptions {
8828                    include_subagents: Some(false),
8829                    inline_media,
8830                    message_tail: Some(tail),
8831                    ..Default::default()
8832                };
8833                assert_eq!(
8834                    indexed_claude_window(&locator, &options).unwrap().unwrap(),
8835                    projected_session_result(&full, &options)
8836                );
8837            }
8838        }
8839    }
8840
8841    #[test]
8842    fn load_supports_bounded_windows_and_media_metadata() {
8843        let mut service = HarnessSessionService::new();
8844        let locator = pi_locator();
8845        let bounded = service.handle(request(
8846            1,
8847            "harness.v1.sessions.load",
8848            json!({
8849                "locator": locator,
8850                "options": {
8851                    "include_subagents": false,
8852                    "message_limit": 2,
8853                    "message_offset": 1
8854                }
8855            }),
8856        ));
8857        assert_eq!(bounded["result"]["window"]["offset"], 1);
8858        assert_eq!(bounded["result"]["window"]["returned"], 2);
8859        assert!(bounded["result"]["summary"]["first_message"].is_object());
8860        assert!(bounded["result"]["summary"]["last_message"].is_object());
8861        assert_eq!(
8862            bounded["result"]["session"]["messages"]
8863                .as_array()
8864                .unwrap()
8865                .len(),
8866            2
8867        );
8868        assert!(bounded["result"]["session"]["subagents"]
8869            .as_array()
8870            .unwrap()
8871            .is_empty());
8872
8873        let tail = service.handle(request(
8874            2,
8875            "harness.v1.sessions.load",
8876            json!({"locator": locator, "options": {"message_tail": 1}}),
8877        ));
8878        assert_eq!(tail["result"]["window"]["returned"], 1);
8879        assert_eq!(tail["result"]["window"]["has_more"], true);
8880        assert_eq!(tail["result"]["window"]["has_older"], true);
8881        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8882        assert!(tail["result"]["summary"]["first_message"].is_object());
8883
8884        let metadata_only = service.handle(request(
8885            3,
8886            "harness.v1.sessions.load",
8887            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8888        ));
8889        assert!(metadata_only["result"]["session"]
8890            .to_string()
8891            .contains("media_reference"));
8892        assert!(!metadata_only["result"]["session"]
8893            .to_string()
8894            .contains("data:image/"));
8895    }
8896
8897    #[test]
8898    fn import_translate_branch_and_handoff_use_typed_artifacts() {
8899        let mut service = HarnessSessionService::new();
8900        let locator = pi_locator();
8901        let translated = service.handle(request(
8902            1,
8903            "harness.v1.sessions.translate",
8904            json!({"locator": locator, "target_harness": "grok"}),
8905        ));
8906        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8907        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8908        assert!(translated["result"]["artifact"]["content"]
8909            .as_str()
8910            .is_some_and(|content| !content.is_empty()));
8911
8912        for target in ["opencode", "open-code"] {
8913            let opencode = service.handle(request(
8914                6,
8915                "harness.v1.sessions.translate",
8916                json!({"locator": locator, "target_harness": target}),
8917            ));
8918            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8919        }
8920        let goose = service.handle(request(
8921            7,
8922            "harness.v1.sessions.translate",
8923            json!({"locator": locator, "target_harness": "goose"}),
8924        ));
8925        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8926        assert!(serde_json::from_str::<Value>(
8927            goose["result"]["artifact"]["content"].as_str().unwrap()
8928        )
8929        .unwrap()["conversation"]
8930            .is_array());
8931
8932        let imported = service.handle(request(
8933            2,
8934            "harness.v1.sessions.import",
8935            json!({
8936                "source_harness": "grok",
8937                "content": translated["result"]["artifact"]["content"],
8938            }),
8939        ));
8940        assert_eq!(imported["result"]["session"]["source"], "grok");
8941
8942        let branched = service.handle(request(
8943            3,
8944            "harness.v1.sessions.branch",
8945            json!({"locator": locator, "target_harness": "codex"}),
8946        ));
8947        assert_eq!(branched["result"]["parent"]["harness"], "pi");
8948        assert!(branched["result"]["bootstrap_prompt"]
8949            .as_str()
8950            .unwrap()
8951            .contains("frozen parent transcript"));
8952        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
8953
8954        let handoff = service.handle(request(
8955            4,
8956            "harness.v1.sessions.handoff",
8957            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
8958        ));
8959        assert_eq!(handoff["result"]["launch"]["program"], "pi");
8960        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
8961        assert_eq!(handoff["result"]["requires_materialization"], true);
8962
8963        let goose_handoff = service.handle(request(
8964            8,
8965            "harness.v1.sessions.handoff",
8966            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
8967        ));
8968        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
8969        assert_eq!(
8970            goose_handoff["result"]["materialize"]["arguments"],
8971            json!(["session", "import", "{artifact_path}"])
8972        );
8973
8974        let resumed = service.handle(request(
8975            5,
8976            "harness.v1.sessions.resume_instructions",
8977            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
8978        ));
8979        assert_eq!(resumed["result"]["launch"]["program"], "pi");
8980        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
8981    }
8982
8983    #[test]
8984    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
8985        let temp = std::env::temp_dir().join(format!(
8986            "supercode-service-reduce-{}-{}",
8987            std::process::id(),
8988            generated_session_id()
8989        ));
8990        let source_path = temp.join("source.jsonl");
8991        let store_root = temp.join("store");
8992        std::fs::create_dir_all(&temp).unwrap();
8993
8994        let mut records = vec![json!({
8995            "timestamp": "2026-01-01T00:00:00Z",
8996            "type": "session_meta",
8997            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
8998        })];
8999        for turn in 0..16 {
9000            records.push(json!({
9001                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9002                "type": "response_item",
9003                "payload": {
9004                    "type": "message",
9005                    "role": "user",
9006                    "content": [{
9007                        "type": "input_text",
9008                        "text": format!("request {turn}: {}", "context ".repeat(80)),
9009                    }],
9010                },
9011            }));
9012            records.push(json!({
9013                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9014                "type": "response_item",
9015                "payload": {
9016                    "type": "message",
9017                    "role": "assistant",
9018                    "content": [{
9019                        "type": "output_text",
9020                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9021                    }],
9022                },
9023            }));
9024        }
9025        let source = format!(
9026            "{}\n",
9027            records
9028                .iter()
9029                .map(Value::to_string)
9030                .collect::<Vec<_>>()
9031                .join("\n")
9032        );
9033        std::fs::write(&source_path, &source).unwrap();
9034        let locator = SessionLocator {
9035            harness: HarnessId::from(HarnessId::CODEX),
9036            session_id: "codex-reduce".into(),
9037            storage: StorageLocator::File {
9038                path: source_path.clone(),
9039            },
9040        };
9041        let original = load_session(&locator).unwrap();
9042        let mut service =
9043            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9044
9045        let response = service.handle(request(
9046            1,
9047            "harness.v1.sessions.reduce",
9048            json!({
9049                "locator": locator,
9050                "target_harness": "claude-code",
9051                "keep_last": 4,
9052            }),
9053        ));
9054        assert!(response.get("error").is_none(), "{response:#}");
9055        let receipt = &response["result"]["receipt"];
9056        assert_eq!(receipt["source_harness"], "codex");
9057        assert_eq!(receipt["target_harness"], "claude-code");
9058        assert_eq!(receipt["verified"], true);
9059        assert_eq!(receipt["reversible"], true);
9060        assert!(receipt["reductions"].as_u64().unwrap() > 0);
9061        assert!(
9062            receipt["source_tokens"].as_u64().unwrap()
9063                > receipt["reduced_tokens"].as_u64().unwrap()
9064        );
9065        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9066        assert!(response["result"]["bootstrap_prompt"]
9067            .as_str()
9068            .unwrap()
9069            .contains("Do not guess hidden content"));
9070
9071        let rescue_id = receipt["id"].as_str().unwrap();
9072        let store = crate::SessionStore::open(&store_root).unwrap();
9073        let sidecar =
9074            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9075        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9076        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9077        let policy = reduce::ReductionPolicy {
9078            clear_turns_older_than: Some(4),
9079            ..Default::default()
9080        };
9081        let (restamped_view, reapplied_log) =
9082            reduce::project_messages(&sidecar.messages, &policy, &log);
9083        assert_eq!(
9084            messages_jsonl(&persisted_view).unwrap(),
9085            messages_jsonl(&restamped_view).unwrap()
9086        );
9087        assert_eq!(reapplied_log, log);
9088        reduce::verify_log(&log, &sidecar).unwrap();
9089        assert_eq!(
9090            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9091            original.messages
9092        );
9093        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9094
9095        std::fs::remove_dir_all(temp).ok();
9096    }
9097
9098    #[test]
9099    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9100        let temp = std::env::temp_dir().join(format!(
9101            "supercode-severed-view-{}-{}",
9102            std::process::id(),
9103            generated_session_id()
9104        ));
9105        std::fs::create_dir_all(&temp).unwrap();
9106        let path = temp.join("severed.jsonl");
9107        // A live record whose parent was pruned — what a compacted or
9108        // resumed-across-files Claude Code session looks like on disk.
9109        std::fs::write(
9110            &path,
9111            concat!(
9112                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9113                "\n",
9114                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9115                "\n",
9116            ),
9117        )
9118        .unwrap();
9119        let locator = SessionLocator {
9120            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9121            session_id: "severed".into(),
9122            storage: StorageLocator::File { path },
9123        };
9124        let mut service = HarnessSessionService::new();
9125
9126        let viewed = service.handle(request(
9127            1,
9128            "harness.v1.sessions.load",
9129            json!({"locator": locator}),
9130        ));
9131        let session = &viewed["result"]["session"];
9132        assert_eq!(session["fidelity"], "semantic");
9133        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9134        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9135            entry
9136                .as_str()
9137                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9138        }));
9139
9140        // Asking a READ surface for a lossless reconstruction gets the strict
9141        // refusal back, unchanged.
9142        let strict = service.handle(request(
9143            2,
9144            "harness.v1.sessions.load",
9145            json!({"locator": locator, "fidelity": "byte_lossless"}),
9146        ));
9147        assert!(strict["error"]["message"]
9148            .as_str()
9149            .unwrap()
9150            .contains("cannot reconstruct lossless Claude continuation"));
9151
9152        // Transfer/continuation surfaces have no view mode at all.
9153        let translated = service.handle(request(
9154            3,
9155            "harness.v1.sessions.translate",
9156            json!({"locator": locator, "target_harness": "codex"}),
9157        ));
9158        assert!(translated["error"]["message"]
9159            .as_str()
9160            .unwrap()
9161            .contains("cannot reconstruct lossless Claude continuation"));
9162        let resumed = service.handle(request(
9163            4,
9164            "harness.v1.sessions.resume_instructions",
9165            json!({"locator": locator}),
9166        ));
9167        assert!(resumed["error"]["message"]
9168            .as_str()
9169            .unwrap()
9170            .contains("cannot reconstruct lossless Claude continuation"));
9171
9172        let _ = std::fs::remove_dir_all(&temp);
9173    }
9174
9175    #[test]
9176    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9177        let codex = resume_launch(
9178            HarnessId::CODEX,
9179            "codex-session",
9180            Path::new("/tmp/project"),
9181            ResumePolicy::Yolo,
9182        )
9183        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9184        assert_eq!(codex.program, "codex");
9185        assert_eq!(
9186            codex.arguments,
9187            [
9188                "-c",
9189                "check_for_update_on_startup=false",
9190                "-c",
9191                "projects.\"/tmp/project\".trust_level=\"trusted\"",
9192                "--dangerously-bypass-approvals-and-sandbox",
9193                "--dangerously-bypass-hook-trust",
9194                "resume",
9195                "codex-session",
9196            ]
9197        );
9198
9199        let gemini = resume_launch(
9200            HarnessId::GEMINI,
9201            "gemini-session",
9202            Path::new("/tmp/project"),
9203            ResumePolicy::Yolo,
9204        )
9205        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9206        assert_eq!(gemini.program, "gemini");
9207        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9208
9209        let goose = resume_launch(
9210            HarnessId::GOOSE,
9211            "goose-session",
9212            Path::new("/tmp/project"),
9213            ResumePolicy::Yolo,
9214        )
9215        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9216        assert_eq!(goose.program, "goose");
9217        assert_eq!(
9218            goose.arguments,
9219            ["session", "--resume", "--session-id", "goose-session"]
9220        );
9221
9222        let supercode = resume_launch(
9223            HarnessId::SUPERCODE,
9224            "supercode-session",
9225            Path::new("/tmp/project"),
9226            ResumePolicy::Yolo,
9227        )
9228        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9229        assert_eq!(supercode.program, "supercode");
9230        assert_eq!(
9231            supercode.arguments,
9232            ["--dangerous", "resume", "supercode-session"]
9233        );
9234    }
9235
9236    #[test]
9237    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9238        let temp = std::env::temp_dir().join(format!(
9239            "supercode-harness-artifact-{}-{}",
9240            std::process::id(),
9241            generated_session_id()
9242        ));
9243        let main_path = temp.join("parent.jsonl");
9244        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9245        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9246        let fixture = std::fs::read_to_string(
9247            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9248                .join("tests/fixtures/claude_code_session.jsonl"),
9249        )
9250        .unwrap();
9251        let parent = fixture.trim_end_matches('\n');
9252        let child = fixture.trim_end_matches('\n');
9253        std::fs::write(&main_path, parent).unwrap();
9254        std::fs::write(&subagent_path, child).unwrap();
9255        let locator = SessionLocator {
9256            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9257            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9258            storage: StorageLocator::File {
9259                path: main_path.clone(),
9260            },
9261        };
9262        let mut service = HarnessSessionService::new();
9263        let claude = service.handle(request(
9264            1,
9265            "harness.v1.sessions.translate",
9266            json!({"locator": locator, "target_harness": "claude-code"}),
9267        ));
9268        let artifact = &claude["result"]["artifact"];
9269        assert_eq!(artifact["fidelity"], "byte_lossless");
9270        assert_eq!(artifact["content"], parent);
9271        let files = artifact["files"].as_array().unwrap();
9272        assert!(files.iter().any(|file| {
9273            file["role"] == "subagent"
9274                && file["path"]
9275                    .as_str()
9276                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9277                && file["content"] == child
9278        }));
9279        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9280
9281        let grok = service.handle(request(
9282            2,
9283            "harness.v1.sessions.translate",
9284            json!({"locator": grok_locator(), "target_harness": "grok"}),
9285        ));
9286        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9287        for name in ["summary.json", "updates.jsonl"] {
9288            let expected = std::fs::read_to_string(
9289                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9290                    .join("tests/fixtures/grok_session")
9291                    .join(name),
9292            )
9293            .unwrap();
9294            assert!(files.iter().any(|file| {
9295                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9296            }));
9297        }
9298        std::fs::remove_dir_all(temp).ok();
9299    }
9300
9301    #[test]
9302    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9303        let mut service = HarnessSessionService::new();
9304        let source = pi_locator();
9305        for (target, format) in [
9306            ("claude-code", SessionFormat::ClaudeCode),
9307            ("codex", SessionFormat::Codex),
9308            ("opencode", SessionFormat::OpenCode),
9309            ("pi", SessionFormat::Pi),
9310        ] {
9311            let result = service.handle(request(
9312                1,
9313                "harness.v1.sessions.handoff",
9314                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9315            ));
9316            let artifact = &result["result"]["artifact"];
9317            let target_id = artifact["session_id"].as_str().unwrap();
9318            assert_ne!(target_id, source.session_id, "{target}");
9319            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9320            assert_eq!(
9321                parsed.meta.session_id.as_deref(),
9322                Some(target_id),
9323                "{target}"
9324            );
9325            if target != "pi" {
9326                assert!(result["result"]["launch"]["arguments"]
9327                    .as_array()
9328                    .unwrap()
9329                    .iter()
9330                    .any(|argument| argument == target_id));
9331            }
9332            if target == "opencode" {
9333                assert!(target_id.starts_with("ses_"));
9334                fn assert_session_ids(value: &Value, target_id: &str) {
9335                    match value {
9336                        Value::Object(fields) => {
9337                            if let Some(session_id) = fields.get("sessionID") {
9338                                assert_eq!(session_id, target_id);
9339                            }
9340                            for child in fields.values() {
9341                                assert_session_ids(child, target_id);
9342                            }
9343                        }
9344                        Value::Array(values) => {
9345                            for child in values {
9346                                assert_session_ids(child, target_id);
9347                            }
9348                        }
9349                        _ => {}
9350                    }
9351                }
9352                let document: Value =
9353                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9354                assert_session_ids(&document, target_id);
9355            }
9356        }
9357
9358        let first = service.handle(request(
9359            2,
9360            "harness.v1.sessions.handoff",
9361            json!({"locator": source, "target_harness": "codex"}),
9362        ));
9363        let second = service.handle(request(
9364            3,
9365            "harness.v1.sessions.handoff",
9366            json!({"locator": source, "target_harness": "codex"}),
9367        ));
9368        assert_ne!(
9369            first["result"]["artifact"]["session_id"],
9370            second["result"]["artifact"]["session_id"]
9371        );
9372    }
9373
9374    #[test]
9375    fn grok_handoff_materializes_through_the_core_door() {
9376        let mut service = HarnessSessionService::new();
9377        let source = opencode_locator();
9378        let response = service.handle(request(
9379            1,
9380            "harness.v1.sessions.handoff",
9381            json!({
9382                "locator": source,
9383                "target_harness": "grok",
9384                "cwd": "/tmp/grok-handoff-project",
9385            }),
9386        ));
9387        let result = &response["result"];
9388
9389        // Grok has no import command: the artifact is Grok's own transcript under a fresh
9390        // identity, and `harness.v1.sessions.materialize` writes its store entry.
9391        assert_eq!(result["artifact"]["target_harness"], "grok");
9392        let artifact = Session::load_str(
9393            result["artifact"]["content"].as_str().unwrap(),
9394            SessionFormat::Grok,
9395        )
9396        .unwrap();
9397        assert!(!artifact.messages.is_empty());
9398        let target_session_id = result["artifact"]["session_id"].as_str().unwrap();
9399        assert_eq!(target_session_id.len(), 36);
9400        assert_ne!(target_session_id, opencode_locator().session_id);
9401        assert!(result["materialize"].is_null());
9402        assert_eq!(
9403            result["launch"]["arguments"],
9404            json!(["--resume", "{materialized_session_id}"])
9405        );
9406        assert!(result["note"]
9407            .as_str()
9408            .unwrap()
9409            .contains("harness.v1.sessions.materialize"));
9410    }
9411
9412    #[tokio::test]
9413    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9414        let mut service = HarnessSessionService::new();
9415        let inventory = service
9416            .handle_async(request(
9417                1,
9418                "harness.v1.harnesses.list",
9419                json!({"harnesses": ["missing"]}),
9420            ))
9421            .await;
9422        assert_eq!(inventory["error"]["code"], -32602);
9423
9424        let attached = service
9425            .handle_async(request(
9426                2,
9427                "harness.v1.runtimes.attach_existing",
9428                json!({"harness": "codex", "runtime_id": "thread-1"}),
9429            ))
9430            .await;
9431        assert_eq!(attached["error"]["code"], -32000);
9432        assert!(attached["error"]["message"]
9433            .as_str()
9434            .unwrap()
9435            .contains("runtimes.resume"));
9436    }
9437
9438    #[test]
9439    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9440        let mut service = HarnessSessionService::new();
9441        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9442        assert_eq!(invalid["error"]["code"], -32602);
9443        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9444        assert_eq!(unknown["error"]["code"], -32601);
9445    }
9446
9447    #[cfg(unix)]
9448    #[tokio::test]
9449    // The test mutates process-wide harness environment and deliberately
9450    // holds the global test lock until every async runtime operation ends.
9451    #[allow(clippy::await_holding_lock)]
9452    async fn async_service_drives_a_generic_acp_runtime() {
9453        let _environment_guard = crate::live_runtime::test_environment_lock();
9454        let script = r#"
9455            i=0
9456            while IFS= read -r line; do
9457              i=$((i + 1))
9458              case "$i" in
9459                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9460                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9461                3)
9462                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9463                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9464                  ;;
9465                4)
9466                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9467                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9468                  ;;
9469              esac
9470            done
9471        "#;
9472        let mut service = HarnessSessionService::new();
9473        let started = service
9474            .handle_async(request(
9475                1,
9476                "harness.v1.runtimes.start",
9477                json!({
9478                    "harness": "codex",
9479                    "protocol": "acp",
9480                    "cwd": std::env::current_dir().unwrap(),
9481                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9482                }),
9483            ))
9484            .await;
9485        assert_eq!(started["result"]["connection"], "runtime-1");
9486        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9487
9488        let terminal = service
9489            .handle_async(request(
9490                9,
9491                "harness.v1.runtimes.terminal_instructions",
9492                json!({"connection":"runtime-1"}),
9493            ))
9494            .await;
9495        let arguments = terminal["result"]["launch"]["arguments"]
9496            .as_array()
9497            .expect("hosted runtime should return terminal arguments");
9498        let endpoint_index = arguments
9499            .iter()
9500            .position(|value| value == "--endpoint")
9501            .expect("terminal command should use an opaque endpoint");
9502        let endpoint = LiveRuntimeEndpoint::parse(
9503            arguments[endpoint_index + 1]
9504                .as_str()
9505                .expect("endpoint argument should be text"),
9506        )
9507        .unwrap();
9508        assert!(!terminal.to_string().contains("Bearer"));
9509        let workspace = std::env::current_dir().unwrap();
9510        let receipt = resolve_live_runtime(
9511            &endpoint,
9512            &LiveRuntimeSource {
9513                harness: "codex".into(),
9514                session_id: "svc_acp".into(),
9515                workspace,
9516            },
9517        )
9518        .unwrap();
9519        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9520            .await
9521            .unwrap();
9522        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9523            .await
9524            .unwrap();
9525
9526        let sent = service
9527            .handle_async(request(
9528                2,
9529                "harness.v1.runtimes.send_input",
9530                json!({"connection": "runtime-1", "text": "hi"}),
9531            ))
9532            .await;
9533        assert_eq!(sent["result"]["turn_id"], "3");
9534
9535        let mut events = Vec::new();
9536        for _ in 0..20 {
9537            events.extend(service.poll_runtimes().await);
9538            if events.len() >= 2 {
9539                break;
9540            }
9541            tokio::time::sleep(Duration::from_millis(2)).await;
9542        }
9543        assert!(events
9544            .iter()
9545            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9546        assert!(events.iter().any(|event| {
9547            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9548        }));
9549
9550        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9551            loop {
9552                let event = attachment.next_event().await.unwrap();
9553                if event.kind == "text_delta" && event.payload["text"] == "ok" {
9554                    break;
9555                }
9556            }
9557        })
9558        .await;
9559        assert!(
9560            saw_editor_reply.is_ok(),
9561            "terminal should observe the editor-driven turn"
9562        );
9563
9564        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9565            .await
9566            .unwrap();
9567        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9568            loop {
9569                let event = attachment.next_event().await.unwrap();
9570                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9571                    break;
9572                }
9573            }
9574        })
9575        .await;
9576        assert!(
9577            saw_terminal_reply.is_ok(),
9578            "terminal should drive the same runtime"
9579        );
9580
9581        let closed = service
9582            .handle_async(request(
9583                3,
9584                "harness.v1.runtimes.close",
9585                json!({"connection": "runtime-1"}),
9586            ))
9587            .await;
9588        assert_eq!(closed["result"]["closed"], true);
9589    }
9590
9591    /// UNI-7 dev/02: a RUNNING mock gateway is detected through the real
9592    /// openclaw probe (config-declared endpoint, TCP connect), and an ACTIVE
9593    /// hermes WAL is detected through the real WAL-freshness probe; the
9594    /// negative sides (no listener, stale WAL, no config) stay undetected.
9595    #[test]
9596    fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9597        let home = connect_scratch_home("uni7-running");
9598
9599        // No config at all: hermes has no default endpoint, so no detection.
9600        // (openclaw's no-config behavior now probes its DOCUMENTED default
9601        // endpoint ws://127.0.0.1:18789 — see the connect launch's
9602        // `default_address` — which is real box state a hermetic test must
9603        // not assert either way; the closed-port negative below covers the
9604        // no-listener side deterministically.)
9605        assert!(probe_hermes_running(&home, 300_000).is_none());
9606
9607        // Mock gateway: a real TCP listener on an ephemeral port, declared in
9608        // the harness's own config file.
9609        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9610        let port = listener.local_addr().unwrap().port();
9611        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9612        std::fs::write(
9613            home.join(".openclaw/openclaw.json"),
9614            format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9615        )
9616        .unwrap();
9617        let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9618        assert!(matches!(
9619            running.method,
9620            RunningInstanceMethod::GatewayConnect
9621        ));
9622        assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9623        drop(listener);
9624        // Parallel tests also bind ephemeral loopback ports, so a just-freed
9625        // port can be re-bound by a NEIGHBORING test between drop and probe.
9626        // Detection on a closed port must fail — retry on a fresh port when
9627        // the freed one was recycled by someone else.
9628        let mut closed_detected = probe_openclaw_running(&home).is_some();
9629        for _ in 0..3 {
9630            if !closed_detected {
9631                break;
9632            }
9633            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9634            let port = listener.local_addr().unwrap().port();
9635            drop(listener);
9636            std::fs::write(
9637                home.join(".openclaw/openclaw.json"),
9638                format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9639            )
9640            .unwrap();
9641            closed_detected = probe_openclaw_running(&home).is_some();
9642        }
9643        assert!(
9644            !closed_detected,
9645            "a closed gateway must not read as running"
9646        );
9647
9648        // gateway.url form takes precedence over port.
9649        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9650        let port = listener.local_addr().unwrap().port();
9651        std::fs::write(
9652            home.join(".openclaw/openclaw.json"),
9653            format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9654        )
9655        .unwrap();
9656        assert!(probe_openclaw_running(&home).is_some());
9657        drop(listener);
9658
9659        // Hermes: an ACTIVE WAL (fresh stamp) is detected; a stale one is not.
9660        std::fs::create_dir_all(home.join(".hermes")).unwrap();
9661        let wal = home.join(".hermes/state.db-wal");
9662        std::fs::write(&wal, b"wal").unwrap();
9663        let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9664        assert!(matches!(
9665            running.method,
9666            RunningInstanceMethod::StoreWalActivity
9667        ));
9668        assert!(running.evidence.contains("state.db-wal"));
9669        let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9670        std::fs::File::options()
9671            .append(true)
9672            .open(&wal)
9673            .unwrap()
9674            .set_modified(stale)
9675            .unwrap();
9676        assert!(
9677            probe_hermes_running(&home, 300_000).is_none(),
9678            "a stale WAL (crash leftover) must not read as running"
9679        );
9680    }
9681
9682    fn connect_scratch_home(tag: &str) -> PathBuf {
9683        let dir = std::env::temp_dir().join(format!(
9684            "supercode-connect-service-{tag}-{}-{}",
9685            std::process::id(),
9686            std::time::SystemTime::now()
9687                .duration_since(std::time::UNIX_EPOCH)
9688                .unwrap()
9689                .as_nanos()
9690        ));
9691        std::fs::create_dir_all(&dir).unwrap();
9692        dir
9693    }
9694
9695    /// Minimal HTTP responder that speaks just enough OpenCode server to
9696    /// accept a health check, create a session, and hold an SSE stream open,
9697    /// while recording each request line with its Authorization header.
9698    async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9699        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9700        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9701        let address = listener.local_addr().unwrap();
9702        let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9703        tokio::spawn(async move {
9704            loop {
9705                let Ok((mut stream, _)) = listener.accept().await else {
9706                    break;
9707                };
9708                let request_sender = request_sender.clone();
9709                tokio::spawn(async move {
9710                    let (reader, mut writer) = stream.split();
9711                    let mut reader = BufReader::new(reader);
9712                    let mut request_line = String::new();
9713                    if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9714                        return;
9715                    }
9716                    let request_line = request_line.trim_end().to_string();
9717                    let mut authorization = String::new();
9718                    let mut content_length = 0usize;
9719                    loop {
9720                        let mut line = String::new();
9721                        if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9722                            return;
9723                        }
9724                        let line = line.trim_end();
9725                        if line.is_empty() {
9726                            break;
9727                        }
9728                        let lower = line.to_ascii_lowercase();
9729                        if let Some(value) = lower.strip_prefix("authorization:") {
9730                            authorization = value.trim().to_string();
9731                        }
9732                        if let Some(value) = lower.strip_prefix("content-length:") {
9733                            content_length = value.trim().parse().unwrap_or(0);
9734                        }
9735                    }
9736                    if content_length > 0 {
9737                        let mut body = vec![0u8; content_length];
9738                        let _ = reader.read_exact(&mut body).await;
9739                    }
9740                    let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9741                    if request_line.starts_with("GET /event") {
9742                        let _ = writer
9743                            .write_all(
9744                                b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9745                            )
9746                            .await;
9747                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9748                        return;
9749                    }
9750                    let body = if request_line.starts_with("POST /session") {
9751                        r#"{"id":"mock-session"}"#
9752                    } else {
9753                        r#"{"status":"ok"}"#
9754                    };
9755                    let response = format!(
9756                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9757                        body.len(),
9758                        body
9759                    );
9760                    let _ = writer.write_all(response.as_bytes()).await;
9761                });
9762            }
9763        });
9764        (format!("http://{address}"), request_receiver)
9765    }
9766
9767    fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9768        crate::HarnessSupportDescriptor {
9769            orchestration: Default::default(),
9770            id: HarnessId::from(HarnessId::OPENCODE),
9771            display_name: "OpenCode".into(),
9772            native: crate::NativeSupport {
9773                discover: crate::ImplementationKind::Absent,
9774                load: crate::ImplementationKind::Absent,
9775                follow: crate::ImplementationKind::Absent,
9776                import: crate::ImplementationKind::Absent,
9777                export: crate::ImplementationKind::Absent,
9778            },
9779            runtime: crate::RuntimeSupport {
9780                implementation: crate::ImplementationKind::BuiltIn,
9781                protocol: protocol.into(),
9782                default_launch: None,
9783                connect_launch: Some(crate::RuntimeConnectLaunch {
9784                    config_path: "~/opencode-tui.json".into(),
9785                    address_pointer: "/server/url".into(),
9786                    port_pointer: None,
9787                    default_address: None,
9788                    auth_pointer: Some("/server/token".into()),
9789                    protocol: protocol.into(),
9790                }),
9791                capabilities: crate::RuntimeCapabilities {
9792                    start_session: true,
9793                    resume_session: true,
9794                    attach_existing_process: true,
9795                    send_input: true,
9796                    stream_events: true,
9797                    interrupt: true,
9798                    steer: false,
9799                    respond_to_requests: true,
9800                },
9801            },
9802        }
9803    }
9804
9805    #[tokio::test]
9806    async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9807        let (base_url, mut requests) = mock_opencode_endpoint().await;
9808        let home = connect_scratch_home("open");
9809        std::fs::write(
9810            home.join("opencode-tui.json"),
9811            format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9812        )
9813        .unwrap();
9814
9815        let descriptor = connect_descriptor("opencode-http-sse");
9816        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9817        assert!(backend.capabilities().attach_existing_process);
9818
9819        let connection = backend
9820            .start(crate::RuntimeStartRequest {
9821                cwd: home.clone(),
9822                launch: None,
9823                mcp_servers: Vec::new(),
9824            })
9825            .await
9826            .unwrap();
9827        let handle = connection.handle();
9828        assert_eq!(handle.runtime_id, "mock-session");
9829        match &handle.endpoint {
9830            crate::RuntimeEndpoint::Http {
9831                base_url: endpoint, ..
9832            } => assert_eq!(endpoint, &base_url),
9833            other => panic!("connect mode must join the running endpoint, got {other:?}"),
9834        }
9835
9836        let mut seen = Vec::new();
9837        while let Ok(line) = requests.try_recv() {
9838            seen.push(line);
9839        }
9840        assert!(seen
9841            .iter()
9842            .any(|line| line.starts_with("GET /global/health")
9843                && line.contains("bearer connect-secret")));
9844        assert!(seen.iter().any(
9845            |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9846        ));
9847    }
9848
9849    /// UNI-5 dev/02, contract corrected by the 2026-08-31 blind walk: the
9850    /// full connect-mode attach path against a MOCK gateway bridge — no live
9851    /// gateway, no model spend. A scripted fake `openclaw` binary (a)
9852    /// asserts the REAL bridge contract — the resolved --url on argv and the
9853    /// credential via --token-file (the real bridge ignores the env var; the
9854    /// endpoint comes from openclaw-native `gateway.remote.url`, never the
9855    /// schema-invalid `gateway.url`) — then (b) speaks scripted ACP:
9856    /// initialize advertising sessionCapabilities.{list,resume},
9857    /// session/resume rebinding the requested session (join), and a
9858    /// prompted turn.
9859    #[tokio::test]
9860    async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9861        let home = connect_scratch_home("openclaw");
9862        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9863        std::fs::write(
9864            home.join(".openclaw/openclaw.json"),
9865            r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9866        )
9867        .unwrap();
9868        let script = home.join("openclaw");
9869        std::fs::write(
9870            &script,
9871            r#"#!/bin/sh
9872# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9873[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9874[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9875[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9876[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9877while IFS= read -r line; do
9878  case "$line" in
9879    *'"initialize"'*)
9880      printf '%s
9881' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9882    *'"session/resume"'*)
9883      printf '%s
9884' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9885    *'"session/new"'*)
9886      printf '%s
9887' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9888    *'"session/prompt"'*)
9889      printf '%s
9890' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9891      printf '%s
9892' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9893  esac
9894done
9895"#,
9896        )
9897        .unwrap();
9898        use std::os::unix::fs::PermissionsExt;
9899        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9900
9901        let mut descriptor = crate::harness_support_registry()
9902            .harnesses
9903            .into_iter()
9904            .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9905            .expect("openclaw must be registered");
9906        descriptor
9907            .runtime
9908            .connect_launch
9909            .as_mut()
9910            .unwrap()
9911            .config_path = "~/.openclaw/openclaw.json".into();
9912        descriptor.runtime.default_launch.as_mut().unwrap().program =
9913            script.to_string_lossy().into_owned();
9914        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9915        assert!(backend.capabilities().resume_session);
9916
9917        let joined = backend
9918            .attach(crate::RuntimeAttachRequest {
9919                runtime_id: "agent:main:main".into(),
9920                cwd: Some(home.clone()),
9921                launch: None,
9922                mcp_servers: Vec::new(),
9923            })
9924            .await;
9925        let mut connection = joined.expect("mock bridge attach must succeed");
9926        assert_eq!(connection.handle().runtime_id, "agent:main:main");
9927        let turn = connection
9928            .send_input(crate::RuntimeInput {
9929                text: "hello".into(),
9930                image_urls: Vec::new(),
9931            })
9932            .await;
9933        assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
9934        connection.close().await.unwrap();
9935    }
9936
9937    #[tokio::test]
9938    async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
9939        let home = connect_scratch_home("fail");
9940        std::fs::write(
9941            home.join("opencode-tui.json"),
9942            r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
9943        )
9944        .unwrap();
9945
9946        let gateway_only = connect_descriptor("acp-v1-jsonrpc");
9947        let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
9948            panic!("an ACP connect endpoint has no gateway client yet");
9949        };
9950        let message = format!("{error:?}");
9951        assert!(message.contains("acp-v1-jsonrpc"));
9952        assert!(!message.contains("connect-secret"));
9953
9954        let unreadable = connect_descriptor("opencode-http-sse");
9955        let missing_home = connect_scratch_home("missing");
9956        let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
9957            panic!("an unreadable connect config must fail closed");
9958        };
9959        let message = format!("{error:?}");
9960        assert!(message.contains("opencode-tui.json"));
9961        assert!(!message.contains("connect-secret"));
9962    }
9963
9964    // ---------------------------------------------------------------------
9965    // ORCH-7 — `harness.v1.jobs.list` / `jobs.get` over the committed fixtures
9966    // ---------------------------------------------------------------------
9967
9968    fn jobs_fixture_root() -> PathBuf {
9969        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
9970    }
9971
9972    /// Point only the three job-bearing homes at the fixtures. Nothing else is
9973    /// read, so the host machine's own harness homes cannot leak into a row.
9974    fn jobs_fixture_homes() -> Value {
9975        let root = jobs_fixture_root();
9976        json!({
9977            "claude_code": root.join("claude_jobs_home/projects"),
9978            "hermes": root.join("hermes_home/state.db"),
9979            "openclaw": root.join("openclaw_home"),
9980        })
9981    }
9982
9983    fn jobs_list(params: Value) -> Value {
9984        let mut service = HarnessSessionService::new();
9985        service.handle(request(1, "harness.v1.jobs.list", params))
9986    }
9987
9988    fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
9989        result["jobs"]
9990            .as_array()
9991            .expect("jobs is an array")
9992            .iter()
9993            .find(|job| job["id"] == id)
9994            .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
9995    }
9996
9997    #[test]
9998    fn gateway_health_derives_from_running_probe_and_install_state() {
9999        let running = RunningInstance {
10000            method: RunningInstanceMethod::GatewayConnect,
10001            evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10002            checked_at_ms: 1,
10003        };
10004        let up = gateway_health(
10005            HarnessId::OPENCLAW,
10006            true,
10007            Some(&running),
10008            Some("2026.7.1-2"),
10009        );
10010        assert_eq!(up.state, GatewayState::Up);
10011        assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10012        assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10013        // Hermes consults its own `gateway status` when the WAL heuristic says
10014        // nothing; a fake binary decides the verdict (the env var is global, so
10015        // the up/down cases run inside this one test, never in parallel).
10016        let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10017        std::fs::create_dir_all(&dir).unwrap();
10018        let fake = dir.join("hermes");
10019        let write_fake = |body: &str| {
10020            std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10021            #[cfg(unix)]
10022            {
10023                use std::os::unix::fs::PermissionsExt;
10024                std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10025            }
10026        };
10027        write_fake("echo '✗ Gateway service is not installed'");
10028        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10029            *slot.borrow_mut() = Some((
10030                HarnessId::HERMES.to_string(),
10031                fake.to_string_lossy().into_owned(),
10032            ))
10033        });
10034        let down = gateway_health(HarnessId::HERMES, true, None, None);
10035        assert_eq!(down.state, GatewayState::Down, "{down:?}");
10036        assert!(down.endpoint.is_none());
10037        assert!(down.evidence.contains("not installed"));
10038        write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10039        let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10040        assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10041        assert!(idle_but_up.evidence.contains("PID 4242"));
10042        write_fake("echo 'something unparseable'");
10043        let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10044        assert_eq!(no_verdict.state, GatewayState::Down);
10045        assert!(no_verdict.evidence.contains("no verdict"));
10046        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10047        let absent = gateway_health(HarnessId::HERMES, false, None, None);
10048        assert_eq!(absent.state, GatewayState::Unknown);
10049        let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10050        assert_eq!(core.state, GatewayState::Unknown);
10051        assert!(core.evidence.contains("per session"));
10052    }
10053
10054    #[test]
10055    fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10056        let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10057        let rows = response["result"]["triggers"]
10058            .as_array()
10059            .expect("triggers")
10060            .clone();
10061        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10062        assert!(
10063            hermes.iter().any(|r| r["name"] == "deploys"
10064                && r["route"] == "/webhooks/deploys"
10065                && r["kind"] == "webhook"),
10066            "{rows:#?}"
10067        );
10068        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10069        assert!(openclaw
10070            .iter()
10071            .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10072        assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10073            && r["kind"] == "hook_mapping"
10074            && r["target"]["action"] == "agent"));
10075        let rendered = response.to_string();
10076        for secret in [
10077            "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10078            "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10079        ] {
10080            assert!(!rendered.contains(secret), "{rendered}");
10081        }
10082        let refused =
10083            triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10084        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10085    }
10086
10087    fn triggers_list(params: Value) -> Value {
10088        let mut service = HarnessSessionService::new();
10089        service.handle(request(1, "harness.v1.triggers.list", params))
10090    }
10091
10092    #[test]
10093    fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10094        let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10095        let rows = response["result"]["routes"]
10096            .as_array()
10097            .expect("routes")
10098            .clone();
10099        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10100        assert_eq!(hermes.len(), 2, "{rows:#?}");
10101        assert_eq!(hermes[0]["target"], "coder");
10102        assert_eq!(hermes[0]["match"]["platform"], "slack");
10103        assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10104        assert_eq!(hermes[0]["specificity"], 4);
10105        assert_eq!(hermes[1]["default"], true);
10106        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10107        assert!(
10108            openclaw.iter().any(|r| r["target"] == "design"
10109                && r["match"]["platform"] == "slack"
10110                && r["specificity"] == 1),
10111            "{openclaw:#?}"
10112        );
10113        assert!(openclaw.iter().any(|r| r["default"] == true));
10114        // A core harness has no routing concept and is refused, never an empty list.
10115        let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10116        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10117    }
10118
10119    fn routes_list(params: Value) -> Value {
10120        let mut service = HarnessSessionService::new();
10121        service.handle(request(1, "harness.v1.routes.list", params))
10122    }
10123
10124    #[test]
10125    fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10126        let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10127        let result = &response["result"];
10128        let ids: Vec<&str> = result["jobs"]
10129            .as_array()
10130            .unwrap()
10131            .iter()
10132            .map(|job| job["id"].as_str().unwrap())
10133            .collect();
10134        assert_eq!(
10135            ids,
10136            vec![
10137                "release-watch",
10138                "toolu_wake_recheck",
10139                "digest-15m",
10140                "nightly-audit",
10141                "coder-standup",
10142                "ops-once-boot",
10143                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10144                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10145                "cron_standup",
10146                "cron_reindex",
10147            ],
10148            "{result}"
10149        );
10150
10151        // OpenClaw, pinned shape: rows come from `state/openclaw.sqlite`
10152        // (`cron_jobs.job_json` + runtime columns), captured from a real
10153        // 2026.7.1-2 gateway.
10154        let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10155        assert_eq!(health["harness"], "openclaw");
10156        assert_eq!(health["schedule"]["kind"], "interval");
10157        assert_eq!(health["schedule"]["minutes"], 10.0);
10158        assert_eq!(health["session_target"], "isolated");
10159        assert_eq!(health["payload"]["kind"], "prompt");
10160        assert_eq!(health["payload"]["text"], "nightly health check");
10161        // ORCH-13: the mode word (`announce`) and the channel it announces on
10162        // (`last`) are separate facts, and the store keeps both — in
10163        // `job_json.delivery` and in the `delivery_*` columns beside it.
10164        assert_eq!(health["deliver"]["mode"], "announce");
10165        assert_eq!(health["deliver"]["target"], "last");
10166        assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10167        let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10168        assert_eq!(digest["schedule"]["kind"], "cron");
10169        assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10170        assert_eq!(digest["session_target"], "main");
10171        assert_eq!(digest["payload"]["kind"], "system_event");
10172
10173        // Claude Code: session-scoped, one recurring cron and one one-shot wakeup.
10174        let cron = job_row(result, "release-watch");
10175        assert_eq!(cron["harness"], "claude-code");
10176        assert_eq!(cron["scope"], "session");
10177        assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10178        assert_eq!(cron["schedule"]["kind"], "cron");
10179        assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10180        assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10181        assert_eq!(cron["payload"]["kind"], "prompt");
10182        assert_eq!(cron["recurring"], true);
10183        assert_eq!(cron["deliver"]["target"], "session");
10184        let wakeup = job_row(result, "toolu_wake_recheck");
10185        assert_eq!(wakeup["payload"]["kind"], "wakeup");
10186        assert_eq!(wakeup["schedule"]["kind"], "once");
10187        assert_eq!(wakeup["recurring"], false);
10188        assert_eq!(wakeup["state"], "pending");
10189
10190        // Hermes: install-scoped, interval + origin delivery, and a paused cron.
10191        let interval = job_row(result, "digest-15m");
10192        assert_eq!(interval["harness"], "hermes");
10193        assert_eq!(interval["scope"], "install");
10194        assert_eq!(interval["profile"], Value::Null);
10195        assert_eq!(interval["schedule"]["kind"], "interval");
10196        assert_eq!(interval["schedule"]["minutes"], 15.0);
10197        assert_eq!(interval["schedule"]["display"], "every 15 min");
10198        assert_eq!(interval["deliver"]["target"], "origin");
10199        assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10200        assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10201        assert_eq!(interval["last_status"], "ok");
10202        let nightly = job_row(result, "nightly-audit");
10203        assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10204        assert_eq!(nightly["deliver"]["target"], "local");
10205        assert_eq!(nightly["enabled"], false);
10206        assert_eq!(nightly["state"], "paused");
10207        // The per-profile store carries the profile name from its own path.
10208        let profiled = job_row(result, "ops-once-boot");
10209        assert_eq!(profiled["profile"], "ops");
10210        assert_eq!(profiled["schedule"]["kind"], "once");
10211        assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10212        assert_eq!(profiled["payload"]["kind"], "script");
10213        // An explicit `<platform>:<chat>` target carries the chat itself.
10214        assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10215        assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10216        assert_eq!(profiled["recurring"], false);
10217
10218        // ORCH-13: a job delivering to its creating conversation carries that
10219        // conversation's whole surface — platform word, chat AND thread.
10220        let standup_to_group = job_row(result, "coder-standup");
10221        assert_eq!(standup_to_group["deliver"]["target"], "origin");
10222        assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10223        assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10224        // Hermes has no mode word and routes by adapter profile, not account.
10225        assert!(standup_to_group["deliver"]["mode"].is_null());
10226        assert!(standup_to_group["deliver"]["account"].is_null());
10227
10228        // OpenClaw: the session target and the delivery mode are the row's own
10229        // columns, not a footnote.
10230        let standup = job_row(result, "cron_standup");
10231        assert_eq!(standup["harness"], "openclaw");
10232        assert_eq!(standup["session_target"], "isolated");
10233        assert_eq!(standup["deliver"]["mode"], "announce");
10234        assert_eq!(standup["deliver"]["target"], "slack");
10235        assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10236        assert_eq!(standup["payload"]["kind"], "prompt");
10237        assert_eq!(standup["profile"], "main");
10238        let reindex = job_row(result, "cron_reindex");
10239        assert_eq!(reindex["session_target"], "main");
10240        assert_eq!(reindex["payload"]["kind"], "system_event");
10241        assert_eq!(reindex["schedule"]["kind"], "interval");
10242        assert_eq!(reindex["schedule"]["display"], "every 240 min");
10243        assert_eq!(reindex["enabled"], false);
10244
10245        // Every store consulted is named, so an empty answer is never silent.
10246        let states: Vec<(&str, &str)> = result["sources"]
10247            .as_array()
10248            .unwrap()
10249            .iter()
10250            .map(|source| {
10251                (
10252                    source["harness"].as_str().unwrap(),
10253                    source["state"].as_str().unwrap(),
10254                )
10255            })
10256            .collect();
10257        // The `coder` profile home has no cron store at all: it is named as
10258        // `absent_store`, not skipped, so "this profile schedules nothing" and
10259        // "this profile was never looked at" stay distinguishable.
10260        assert_eq!(
10261            states,
10262            vec![
10263                ("claude-code", "scanned"),
10264                ("hermes", "read"),
10265                ("hermes", "absent_store"),
10266                ("hermes", "read"),
10267                ("openclaw", "read"),
10268                ("openclaw", "read"),
10269            ],
10270            "{result}"
10271        );
10272    }
10273
10274    #[test]
10275    fn jobs_list_filters_by_harness_session_and_profile() {
10276        let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10277        let ids: Vec<&str> = by_harness["result"]["jobs"]
10278            .as_array()
10279            .unwrap()
10280            .iter()
10281            .map(|job| job["id"].as_str().unwrap())
10282            .collect();
10283        assert_eq!(
10284            ids,
10285            vec![
10286                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10287                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10288                "cron_standup",
10289                "cron_reindex",
10290            ]
10291        );
10292
10293        let by_session = jobs_list(json!({
10294            "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10295            "homes": jobs_fixture_homes(),
10296        }));
10297        let jobs = by_session["result"]["jobs"].as_array().unwrap();
10298        assert_eq!(jobs.len(), 2, "{by_session}");
10299        assert!(jobs
10300            .iter()
10301            .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10302
10303        let by_profile = jobs_list(json!({
10304            "harness": "hermes",
10305            "profile": "ops",
10306            "homes": jobs_fixture_homes(),
10307        }));
10308        let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10309        assert_eq!(jobs.len(), 1, "{by_profile}");
10310        assert_eq!(jobs[0]["id"], "ops-once-boot");
10311    }
10312
10313    #[test]
10314    fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10315        let mut service = HarnessSessionService::new();
10316        let hermes = service.handle(request(
10317            1,
10318            "harness.v1.jobs.get",
10319            json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10320        ));
10321        assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10322        // Native fields the uniform row does not carry survive on `source`.
10323        assert_eq!(hermes["result"]["source"]["provider"], "nous");
10324        assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10325
10326        let claude = service.handle(request(
10327            2,
10328            "harness.v1.jobs.get",
10329            json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10330        ));
10331        assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10332        assert_eq!(
10333            claude["result"]["source"]["tool_use_id"],
10334            "toolu_cron_release_watch"
10335        );
10336
10337        let missing = service.handle(request(
10338            3,
10339            "harness.v1.jobs.get",
10340            json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10341        ));
10342        assert!(missing["error"]["message"]
10343            .as_str()
10344            .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10345    }
10346
10347    #[test]
10348    fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10349        let mut service = HarnessSessionService::new();
10350        for (id, method, params) in [
10351            (
10352                1,
10353                "harness.v1.jobs.list",
10354                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10355            ),
10356            (
10357                2,
10358                "harness.v1.jobs.get",
10359                json!({"harness": "codex", "id": "anything"}),
10360            ),
10361        ] {
10362            let response = service.handle(request(id, method, params));
10363            assert_eq!(response["error"]["code"], -32020, "{response}");
10364            assert!(response["error"]["message"]
10365                .as_str()
10366                .is_some_and(|message| message.contains("has no scheduled jobs")));
10367            assert!(response.get("result").is_none());
10368        }
10369    }
10370
10371    #[test]
10372    fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10373        let scratch = std::env::temp_dir().join(format!(
10374            "supercode-jobs-migrated-{}-{}",
10375            std::process::id(),
10376            generated_session_id()
10377        ));
10378        std::fs::create_dir_all(&scratch).unwrap();
10379        let response = jobs_list(json!({
10380            "harness": "openclaw",
10381            "homes": {"openclaw": scratch.clone()},
10382        }));
10383        let result = &response["result"];
10384        assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10385        assert_eq!(result["sources"][0]["state"], "absent_store");
10386        assert_eq!(result["sources"][0]["harness"], "openclaw");
10387        std::fs::remove_dir_all(&scratch).ok();
10388    }
10389
10390    // ---------------------------------------------------------------------
10391    // ORCH-8 — `harness.v1.runs.list` / `runs.get` over the committed fire
10392    // stores: Hermes's `cron/executions.db` (root home + profile home) and
10393    // OpenClaw's `cron_run_logs`. Every fixture row is written by
10394    // `tests/fixtures/gen_runs_fixtures.py` against the harnesses' own DDL.
10395    // ---------------------------------------------------------------------
10396
10397    /// The health job in the committed OpenClaw fixture, which fired twice.
10398    const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10399    /// The digest job, whose single fire predates run ids.
10400    const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10401
10402    fn runs_list(params: Value) -> Value {
10403        let mut service = HarnessSessionService::new();
10404        service.handle(request(1, "harness.v1.runs.list", params))
10405    }
10406
10407    fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10408        result["runs"]
10409            .as_array()
10410            .expect("runs is an array")
10411            .iter()
10412            .find(|run| run["id"] == id)
10413            .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10414    }
10415
10416    #[test]
10417    fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10418        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10419        let result = &response["result"];
10420        let ids: Vec<&str> = result["runs"]
10421            .as_array()
10422            .expect("runs is an array")
10423            .iter()
10424            .map(|run| run["id"].as_str().unwrap())
10425            .collect();
10426        let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10427        assert_eq!(
10428            ids,
10429            vec![
10430                // Hermes, newest claim first, root ledger then profile ledger.
10431                "b2c3d4e5f60718293a4b5c6d7e8f9012",
10432                "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10433                "c3d4e5f60718293a4b5c6d7e8f901234",
10434                "f60718293a4b5c6d7e8f901234567890",
10435                "e5f60718293a4b5c6d7e8f9012345678",
10436                "d4e5f60718293a4b5c6d7e8f90123456",
10437                // OpenClaw, newest `ts` first.
10438                "run_health_0002",
10439                digest_fire.as_str(),
10440                "run_health_0001",
10441            ],
10442            "{result}"
10443        );
10444
10445        // The harness's OWN outcome word survives; nothing is renamed onto a
10446        // shared vocabulary.
10447        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10448        assert_eq!(failed["harness"], "hermes");
10449        assert_eq!(failed["job_id"], "job42");
10450        assert_eq!(failed["status"], "failed");
10451        assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10452        assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10453
10454        // Hermes's `unknown` — an attempt whose owner died before writing a
10455        // terminal state — is a fourth status, not folded into `failed`.
10456        let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10457        assert_eq!(abandoned["status"], "unknown");
10458        assert_eq!(abandoned["job_id"], "ops-once-boot");
10459
10460        // An unterminated fire has no finish, and no session is invented.
10461        let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10462        assert_eq!(running["status"], "running");
10463        assert!(running["finished_at"].is_null(), "{running}");
10464        assert!(running["session_id"].is_null(), "{running}");
10465
10466        // OpenClaw records the session on the row itself, and epoch-ms
10467        // timestamps are rendered as RFC 3339.
10468        let ok = run_row(result, "run_health_0001");
10469        assert_eq!(ok["harness"], "openclaw");
10470        assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10471        assert_eq!(ok["status"], "ok");
10472        assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10473        assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10474        assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10475        // OpenClaw's run log is written once, at finish: there is no claim.
10476        assert!(ok["claimed_at"].is_null(), "{ok}");
10477
10478        // A run-log row with no `run_id` falls back to the store's own
10479        // `(job_id, seq)` key rather than being dropped.
10480        assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10481
10482        // ORCH-13: a fire whose delivery nothing recorded says so, rather than
10483        // borrowing a neighbouring fire's outcome. Both of these ran on jobs
10484        // that deliver `local` (or have no job record at all), so no
10485        // obligation is addressed to a surface they could match.
10486        for id in [
10487            "b2c3d4e5f60718293a4b5c6d7e8f9012",
10488            "d4e5f60718293a4b5c6d7e8f90123456",
10489        ] {
10490            assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10491        }
10492
10493        // Every store consulted is named, including the profile home that has
10494        // no ledger — an empty history and an absent store are different.
10495        let sources = result["sources"].as_array().unwrap();
10496        let states: Vec<(&str, &str)> = sources
10497            .iter()
10498            .map(|source| {
10499                (
10500                    source["harness"].as_str().unwrap(),
10501                    source["state"].as_str().unwrap(),
10502                )
10503            })
10504            .collect();
10505        assert_eq!(
10506            states,
10507            vec![
10508                ("hermes", "read"),
10509                ("hermes", "absent_store"),
10510                ("hermes", "read"),
10511                ("openclaw", "read"),
10512            ],
10513            "{result}"
10514        );
10515        assert_eq!(sources[2]["profile"], "ops");
10516        assert!(sources[3]["path"]
10517            .as_str()
10518            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10519    }
10520
10521    #[test]
10522    fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10523        let response = runs_list(json!({
10524            "harness": "hermes",
10525            "job": "job42",
10526            "homes": jobs_fixture_homes(),
10527        }));
10528        let result = &response["result"];
10529        assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10530
10531        // Hermes writes NO link from an execution to its session. The fire
10532        // that ran the agent is joined to `cron_job42_<stamp>` because that
10533        // id's instant falls inside its [claimed_at, finished_at] window.
10534        let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10535        assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10536
10537        // The later fire failed before opening one. Its window holds no
10538        // session, so the row says so instead of re-using the earlier fire's
10539        // — the join is per-FIRE, not per-job.
10540        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10541        assert!(failed["session_id"].is_null(), "{failed}");
10542    }
10543
10544    /// ORCH-13: where a fire's output went, read from each harness's own
10545    /// delivery record — Hermes's `delivery_obligations` ledger inside
10546    /// `state.db`, OpenClaw's `delivery_*` run-log columns.
10547    #[test]
10548    fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10549        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10550        let result = &response["result"];
10551
10552        // Hermes: the ledger is the GATEWAY's, keyed by conversation and
10553        // surface, so the fire's own [claimed_at, finished_at] window picks
10554        // the obligation. The fire succeeded and so did the send.
10555        let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10556        assert_eq!(delivered["status"], "completed");
10557        assert_eq!(delivered["delivery"]["state"], "delivered");
10558        assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10559        assert_eq!(delivered["delivery"]["attempts"], 1);
10560        assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10561        assert_eq!(
10562            delivered["delivery"]["delivered_at"],
10563            "2026-09-02T09:00:30.400Z"
10564        );
10565
10566        // The next fire of the same job ALSO succeeded — and its output never
10567        // arrived. That is the fact `status` alone cannot carry.
10568        let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10569        assert_eq!(undelivered["status"], "completed");
10570        assert_eq!(undelivered["delivery"]["state"], "failed");
10571        assert_eq!(undelivered["delivery"]["attempts"], 3);
10572        assert_eq!(
10573            undelivered["delivery"]["last_error"],
10574            "telegram send failed: Bad Request: chat not found"
10575        );
10576        // Only a delivered obligation carries an instant of delivery; the
10577        // ledger's `updated_at` on a failed row dates the failure.
10578        assert!(
10579            undelivered["delivery"]["delivered_at"].is_null(),
10580            "{undelivered}"
10581        );
10582
10583        // OpenClaw writes the outcome onto the run-log row and declares the
10584        // address on the job, so the row's target is joined from `cron_jobs`.
10585        let announced = run_row(result, "run_health_0001");
10586        assert_eq!(announced["delivery"]["state"], "delivered");
10587        assert_eq!(announced["delivery"]["target"], "last");
10588        // Its run log counts no attempts and stamps no delivered-at.
10589        assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10590        assert!(
10591            announced["delivery"]["delivered_at"].is_null(),
10592            "{announced}"
10593        );
10594        let refused = run_row(result, "run_health_0002");
10595        assert_eq!(refused["delivery"]["state"], "not-delivered");
10596        assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10597
10598        // A run-log row with no delivery columns at all recorded no delivery:
10599        // the job's declared target is not evidence that anything was sent.
10600        let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10601        assert!(skipped["delivery"].is_null(), "{skipped}");
10602    }
10603
10604    /// A Hermes fire whose session carries a `session_key` is matched on that
10605    /// key FIRST — the most specific question the ledger can answer. Proven by
10606    /// moving the obligations off the job's surface on a COPY of the fixture,
10607    /// so only the session-key question can still find them.
10608    #[test]
10609    fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10610        let scratch = std::env::temp_dir().join(format!(
10611            "supercode-runs-delivery-{}-{}",
10612            std::process::id(),
10613            generated_session_id()
10614        ));
10615        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10616        let fixture = jobs_fixture_root().join("hermes_home");
10617        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10618        for name in ["cron/executions.db", "cron/jobs.json"] {
10619            std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10620        }
10621        {
10622            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10623            // The obligations now sit on a surface no job in this store
10624            // delivers to, so the surface question cannot match them.
10625            connection
10626                .execute(
10627                    "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10628                    [],
10629                )
10630                .unwrap();
10631            // A cron fire that ran inside a keyed conversation: the session
10632            // the window recovers carries `tg-coder-1`'s key.
10633            connection
10634                .execute(
10635                    "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10636                     ('cron_coder-standup_20260902_090010', 'cron', \
10637                      'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10638                    [],
10639                )
10640                .unwrap();
10641        }
10642        let response = runs_list(json!({
10643            "harness": "hermes",
10644            "job": "coder-standup",
10645            "homes": {"hermes": scratch.join("state.db")},
10646        }));
10647        let result = &response["result"];
10648        let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10649        assert_eq!(
10650            matched["session_id"], "cron_coder-standup_20260902_090010",
10651            "{result}"
10652        );
10653        assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10654        assert_eq!(
10655            matched["delivery"]["target"], "slack:C0FALLBACK:55",
10656            "{result}"
10657        );
10658        std::fs::remove_dir_all(&scratch).ok();
10659    }
10660
10661    #[test]
10662    fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10663        // A fire whose session was compressed mid-run is only readable at the
10664        // continuation, so that is what the row must report. Built on a COPY
10665        // of the committed fixture: no test writes to a fixture or to a real
10666        // harness home.
10667        let scratch = std::env::temp_dir().join(format!(
10668            "supercode-runs-compressed-{}-{}",
10669            std::process::id(),
10670            generated_session_id()
10671        ));
10672        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10673        let fixture = jobs_fixture_root().join("hermes_home");
10674        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10675        std::fs::copy(
10676            fixture.join("cron/executions.db"),
10677            scratch.join("cron/executions.db"),
10678        )
10679        .unwrap();
10680        {
10681            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10682            connection
10683                .execute(
10684                    "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10685                    ["cron_job42_20260902_120000"],
10686                )
10687                .unwrap();
10688            connection
10689                .execute(
10690                    "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10691                     VALUES ('job42-after-compaction', 'cron', \
10692                             'cron_job42_20260902_120000', 1788350000.0)",
10693                    [],
10694                )
10695                .unwrap();
10696        }
10697        let response = runs_list(json!({
10698            "harness": "hermes",
10699            "job": "job42",
10700            "homes": {"hermes": scratch.join("state.db")},
10701        }));
10702        let result = &response["result"];
10703        assert_eq!(
10704            run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10705            "job42-after-compaction",
10706            "{result}"
10707        );
10708        std::fs::remove_dir_all(&scratch).ok();
10709    }
10710
10711    #[test]
10712    fn runs_list_filters_by_job_and_caps_by_limit() {
10713        let by_job = runs_list(json!({
10714            "harness": "openclaw",
10715            "job": OPENCLAW_HEALTH_JOB,
10716            "homes": jobs_fixture_homes(),
10717        }));
10718        let ids: Vec<&str> = by_job["result"]["runs"]
10719            .as_array()
10720            .unwrap()
10721            .iter()
10722            .map(|run| run["id"].as_str().unwrap())
10723            .collect();
10724        assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10725
10726        let capped = runs_list(json!({
10727            "harness": "openclaw",
10728            "limit": 1,
10729            "homes": jobs_fixture_homes(),
10730        }));
10731        let runs = capped["result"]["runs"].as_array().unwrap();
10732        assert_eq!(runs.len(), 1, "{capped}");
10733        // Newest first, so the cap keeps the recent fire.
10734        assert_eq!(runs[0]["id"], "run_health_0002");
10735    }
10736
10737    #[test]
10738    fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10739        let mut service = HarnessSessionService::new();
10740        let hermes = service.handle(request(
10741            1,
10742            "harness.v1.runs.get",
10743            json!({
10744                "harness": "hermes",
10745                "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10746                "homes": jobs_fixture_homes(),
10747            }),
10748        ));
10749        assert_eq!(hermes["result"]["run"]["status"], "completed");
10750        assert_eq!(
10751            hermes["result"]["run"]["session_id"],
10752            "cron_job42_20260902_120000"
10753        );
10754        // Ledger columns the uniform row does not carry survive on `source`.
10755        assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10756        assert_eq!(hermes["result"]["source"]["pid"], 4242);
10757        assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10758
10759        let openclaw = service.handle(request(
10760            2,
10761            "harness.v1.runs.get",
10762            json!({
10763                "harness": "openclaw",
10764                "id": "run_health_0002",
10765                "homes": jobs_fixture_homes(),
10766            }),
10767        ));
10768        assert_eq!(openclaw["result"]["run"]["status"], "error");
10769        // ORCH-13: the run's delivery is projected AND the store's own columns
10770        // stay verbatim on `source`, so nothing about the fire is lost.
10771        assert_eq!(
10772            openclaw["result"]["source"]["delivery_status"],
10773            "not-delivered"
10774        );
10775        assert_eq!(
10776            openclaw["result"]["source"]["delivery_error"],
10777            "channel_not_found"
10778        );
10779        assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10780        assert_eq!(
10781            openclaw["result"]["run"]["delivery"]["state"],
10782            "not-delivered"
10783        );
10784        assert_eq!(
10785            openclaw["result"]["run"]["delivery"]["last_error"],
10786            "channel_not_found"
10787        );
10788
10789        let missing = service.handle(request(
10790            3,
10791            "harness.v1.runs.get",
10792            json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10793        ));
10794        assert!(missing["error"]["message"]
10795            .as_str()
10796            .is_some_and(|message| message.contains("no run `no-such-run`")));
10797    }
10798
10799    #[test]
10800    fn runs_refuse_a_harness_that_keeps_no_run_store() {
10801        let mut service = HarnessSessionService::new();
10802        for (id, method, params) in [
10803            // Claude Code HAS scheduled jobs but no fire store: its fires are
10804            // ordinary turns. It must refuse, not answer with an empty list.
10805            (
10806                1,
10807                "harness.v1.runs.list",
10808                json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10809            ),
10810            (
10811                2,
10812                "harness.v1.runs.get",
10813                json!({"harness": "claude-code", "id": "anything"}),
10814            ),
10815            (
10816                3,
10817                "harness.v1.runs.list",
10818                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10819            ),
10820        ] {
10821            let response = service.handle(request(id, method, params));
10822            assert_eq!(response["error"]["code"], -32020, "{response}");
10823            assert!(response["error"]["message"]
10824                .as_str()
10825                .is_some_and(|message| message.contains("keeps no run store")));
10826            assert!(response.get("result").is_none());
10827        }
10828    }
10829
10830    #[test]
10831    fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10832        let scratch = std::env::temp_dir().join(format!(
10833            "supercode-runs-empty-{}-{}",
10834            std::process::id(),
10835            generated_session_id()
10836        ));
10837        std::fs::create_dir_all(&scratch).unwrap();
10838        let response = runs_list(json!({
10839            "harness": "openclaw",
10840            "homes": {"openclaw": scratch.clone()},
10841        }));
10842        let result = &response["result"];
10843        assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10844        assert_eq!(result["sources"][0]["state"], "absent_store");
10845        assert!(result["sources"][0]["path"]
10846            .as_str()
10847            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10848        std::fs::remove_dir_all(&scratch).ok();
10849    }
10850}