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                    &params.homes,
1252                )
1253                .map_err(ServiceError::Operation)?;
1254                Ok(json!({"locator": locator}))
1255            }
1256            // ORCH-7 observed tier. Read-only: the handlers open the harness's
1257            // own job store (Claude Code's session JSONL, Hermes's and
1258            // OpenClaw's `cron/jobs.json`) and never write, fire, or schedule.
1259            "harness.v1.jobs.list" => {
1260                let query = decode::<crate::jobs::JobsQuery>(params)?;
1261                if let Some(harness) = query.harness.as_deref() {
1262                    refuse_harness_without_jobs(harness, "jobs.list")?;
1263                }
1264                let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1265                serde_json::to_value(listing)
1266                    .map_err(|error| ServiceError::Operation(error.to_string()))
1267            }
1268            "harness.v1.jobs.get" => {
1269                let params = decode::<JobsGetParams>(params)?;
1270                refuse_harness_without_jobs(&params.harness, "jobs.get")?;
1271                match crate::jobs::get_job(&params.harness, &params.id, &params.homes)
1272                    .map_err(operation)?
1273                {
1274                    Some((job, source)) => Ok(json!({"job": job, "source": source})),
1275                    None => Err(ServiceError::Operation(format!(
1276                        "`{}` has no scheduled job `{}`",
1277                        params.harness, params.id
1278                    ))),
1279                }
1280            }
1281            // ORCH-18 controlled tier. Each verb translates to the HARNESS'S
1282            // OWN cron verb and runs it (`crate::jobs_control`); supercode
1283            // schedules nothing. The row returned is re-read from the
1284            // harness's store afterwards, and `ran` narrates the exact command
1285            // with any credential redacted.
1286            "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1287            "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1288            "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1289            "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1290            "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1291            "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1292            "harness.v1.jobs.notepad"
1293            | "harness.v1.jobs.notepad_set"
1294            | "harness.v1.jobs.notepad_delete" => {
1295                let request = decode::<crate::jobs_notepad::JobNotepadRequest>(params)?;
1296                refuse_harness_without_jobs(&request.harness, "jobs.notepad")?;
1297                let answer = match method {
1298                    "harness.v1.jobs.notepad_set" => crate::jobs_notepad::set(&request),
1299                    "harness.v1.jobs.notepad_delete" => crate::jobs_notepad::delete(&request),
1300                    _ => crate::jobs_notepad::read(&request),
1301                }
1302                .map_err(job_control_error)?;
1303                serde_json::to_value(answer)
1304                    .map_err(|error| ServiceError::Operation(error.to_string()))
1305            }
1306            // ORCH-8 observed tier. Read-only: the handlers open the harness's
1307            // own run store (Hermes's `cron/executions.db`, OpenClaw's
1308            // `cron_run_logs`) and never claim, retry, or prune a fire.
1309            "harness.v1.runs.list" => {
1310                let query = decode::<crate::runs::RunsQuery>(params)?;
1311                if let Some(harness) = query.harness.as_deref() {
1312                    refuse_harness_without_runs(harness, "runs.list")?;
1313                }
1314                let listing = crate::runs::list_runs(&query).map_err(operation)?;
1315                serde_json::to_value(listing)
1316                    .map_err(|error| ServiceError::Operation(error.to_string()))
1317            }
1318            "harness.v1.runs.get" => {
1319                let params = decode::<RunsGetParams>(params)?;
1320                refuse_harness_without_runs(&params.harness, "runs.get")?;
1321                match crate::runs::get_run(&params.harness, &params.id, &params.homes)
1322                    .map_err(operation)?
1323                {
1324                    Some((run, source)) => Ok(json!({"run": run, "source": source})),
1325                    None => Err(ServiceError::Operation(format!(
1326                        "`{}` has no run `{}`",
1327                        params.harness, params.id
1328                    ))),
1329                }
1330            }
1331            "harness.v1.sessions.resume_instructions" => {
1332                let params = decode::<ResumeInstructionsParams>(params)?;
1333                let session = load_session(&params.locator).map_err(operation)?;
1334                let cwd = params
1335                    .cwd
1336                    .or(session.meta.cwd)
1337                    .unwrap_or_else(|| PathBuf::from("."));
1338                let launch = resume_launch(
1339                    params.locator.harness.as_str(),
1340                    &params.locator.session_id,
1341                    &cwd,
1342                    params.policy,
1343                )?;
1344                Ok(json!({"launch": launch}))
1345            }
1346            _ => Err(ServiceError::MethodNotFound),
1347        }
1348    }
1349
1350    fn reduce_session(
1351        &self,
1352        params: ReduceSessionParams,
1353    ) -> std::result::Result<Value, ServiceError> {
1354        let session = load_session(&params.locator).map_err(operation)?;
1355        if session.messages.is_empty() {
1356            return Err(ServiceError::InvalidParams(
1357                "cannot reduce an empty session".into(),
1358            ));
1359        }
1360        let keep_last = params.keep_last.clamp(1, 128);
1361        let policy = reduce::ReductionPolicy {
1362            clear_turns_older_than: Some(keep_last),
1363            ..Default::default()
1364        };
1365        let (view, log) =
1366            reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1367        if log.reductions.is_empty() {
1368            return Err(ServiceError::UnsupportedAction(format!(
1369                "session `{}` is already too small for a meaningful reversible reduction",
1370                params.locator.session_id
1371            )));
1372        }
1373        let source_tokens = tokens::estimate_view_tokens(&session.messages);
1374        let reduced_tokens = tokens::estimate_view_tokens(&view);
1375        if reduced_tokens >= source_tokens {
1376            return Err(ServiceError::UnsupportedAction(format!(
1377                "session `{}` has no token-reducing reversible projection",
1378                params.locator.session_id
1379            )));
1380        }
1381
1382        let store_root = self
1383            .reduction_store_root
1384            .clone()
1385            .unwrap_or_else(default_reduction_store_root);
1386        let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1387        let rescue_id = format!("rescue-{}", generated_session_id());
1388        let imported = session
1389            .imported_message_count
1390            .unwrap_or(session.messages.len())
1391            .min(session.messages.len());
1392        let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1393        let view_jsonl = messages_jsonl(&view)?;
1394        let title = format!(
1395            "Reduced {} continuation from {}",
1396            params.target_harness.id(),
1397            params.locator.session_id
1398        );
1399
1400        // Durability order is intentional: the full source of truth lands
1401        // before either object that can refer to it. A crash may leave an
1402        // unused sidecar, but can never leave a reduced view whose originals
1403        // were not durably written first.
1404        store
1405            .save_sidecar(&rescue_id, &sidecar_jsonl)
1406            .map_err(operation)?;
1407        store
1408            .save_reduction_log(&rescue_id, &log)
1409            .map_err(operation)?;
1410        store
1411            .save(&rescue_id, &title, &view_jsonl)
1412            .map_err(operation)?;
1413
1414        let source_bytes = serde_json::to_vec(&session.messages)
1415            .map_err(|error| ServiceError::Operation(error.to_string()))?
1416            .len() as u64;
1417        let reduced_bytes = serde_json::to_vec(&view)
1418            .map_err(|error| ServiceError::Operation(error.to_string()))?
1419            .len() as u64;
1420        store
1421            .set_reduction_stats(
1422                &rescue_id,
1423                &title,
1424                source_bytes,
1425                reduced_bytes,
1426                log.reductions.len() as u32,
1427            )
1428            .map_err(operation)?;
1429
1430        // The receipt is issued only after a real disk reload. This proves
1431        // the exact files another process will consume, not the convenient
1432        // in-memory values that produced them.
1433        let reloaded_sidecar = store
1434            .load_sidecar(&rescue_id)
1435            .map_err(operation)?
1436            .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1437        let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1438        let reloaded_log = store
1439            .load_reduction_log(&rescue_id)
1440            .map_err(operation)?
1441            .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1442        let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1443        reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1444        // `sc.reduction` is deliberately in-memory-only metadata: it must
1445        // never leak onto a provider-facing transcript. Reapplying the
1446        // durable log to the durable sidecar restores those ids. Comparing
1447        // its wire form with the transcript reloaded above proves that the
1448        // persisted view is exactly the deterministic projection before we
1449        // use the restamped form for inversion.
1450        let (restamped_view, restamped_log) =
1451            reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1452        if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1453            return Err(ServiceError::Operation(
1454                "persisted reduction view does not match its durable log and sidecar".into(),
1455            ));
1456        }
1457        if restamped_log != reloaded_log {
1458            return Err(ServiceError::Operation(
1459                "reapplying the durable reduction log changed its identity".into(),
1460            ));
1461        }
1462        let inverted =
1463            reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1464        if inverted != session.messages {
1465            return Err(ServiceError::Operation(
1466                "reduction inversion did not restore the source messages byte-exactly".into(),
1467            ));
1468        }
1469
1470        let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1471        let sidecar_path = store.sidecar_path(&rescue_id);
1472        let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1473        let bootstrap_prompt = reduced_bootstrap_prompt(
1474            &params.locator,
1475            params.target_harness,
1476            &view_jsonl,
1477            &sidecar_path,
1478            &reduction_log_path,
1479        );
1480        let mut reduced_session = session.clone();
1481        reduced_session.meta.session_id = Some(rescue_id.clone());
1482        reduced_session.messages = view;
1483
1484        Ok(json!({
1485            "session": normalized_session_json(&reduced_session),
1486            "bootstrap_prompt": bootstrap_prompt,
1487            "receipt": {
1488                "id": rescue_id,
1489                "sidecar_id": rescue_id,
1490                "source_harness": params.locator.harness,
1491                "target_harness": params.target_harness.id(),
1492                "source_tokens": source_tokens,
1493                "reduced_tokens": reduced_tokens,
1494                "ratio": ratio,
1495                "source_bytes": source_bytes,
1496                "reduced_bytes": reduced_bytes,
1497                "reductions": reloaded_log.reductions.len(),
1498                "sidecar_path": sidecar_path,
1499                "reduction_log_path": reduction_log_path,
1500                "verified": true,
1501                "reversible": true,
1502            }
1503        }))
1504    }
1505
1506    /// Recognize the one request family whose waiting happens entirely
1507    /// outside this service's state, and hand a transport the half it can run
1508    /// off the task that owns the service.
1509    ///
1510    /// Opening a runtime is the only door here that waits on a foreign
1511    /// program: it spawns the harness's own binary and completes that
1512    /// program's protocol handshake, which takes as long as the program takes
1513    /// to answer. A transport that awaited the whole request inline would
1514    /// stop reading its own input for that whole time, so ONE slow launch
1515    /// would queue every later request on the same server — including reads
1516    /// like `sessions.discover` that touch no runtime at all. Splitting the
1517    /// request lets the transport spawn [`RuntimeOpen::open`] and keep
1518    /// reading, then pay only the short bookkeeping half
1519    /// ([`Self::register_open_runtime`]) when the runtime is up.
1520    ///
1521    /// `None` for every other method: those are answered by
1522    /// [`Self::handle_async`] as before.
1523    pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1524        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1525            return None;
1526        }
1527        let method = request.get("method").and_then(Value::as_str)?;
1528        if !RUNTIME_OPEN_METHODS.contains(&method) {
1529            return None;
1530        }
1531        Some(RuntimeOpen {
1532            id: request.get("id").cloned().unwrap_or(Value::Null),
1533            method: method.to_string(),
1534            params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1535        })
1536    }
1537
1538    /// Recognize a [`DETACHED_METHODS`] request and hand a transport the
1539    /// whole of it: the service-state half is read here and now, and what
1540    /// remains waits on a foreign program with nothing of this service's in
1541    /// hand.
1542    ///
1543    /// Same reason as [`Self::runtime_open`], different doors. Probing a
1544    /// harness starts it and completes its handshake; couriering a message
1545    /// runs a `claude` process to completion; a conversation verb runs the
1546    /// harness's own CLI or calls its HTTP API. A transport that awaited any
1547    /// of those inline would stop reading its own input for that whole time,
1548    /// so one probe of an unhealthy harness would queue every later request
1549    /// on the same server.
1550    ///
1551    /// Unlike an opening runtime there is no bookkeeping half: the answer
1552    /// [`DetachedCall::run`] produces is the caller's complete response, so a
1553    /// transport writes it without coming back here.
1554    ///
1555    /// `None` for every other method — including the LIVE `sessions.new` /
1556    /// `sessions.reset` door and `runtimes.close`, which wait on a runtime
1557    /// connection this service owns and so are split off by
1558    /// [`Self::detach_runtime`] instead.
1559    pub fn detach(&self, request: &Value) -> Option<DetachedCall> {
1560        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1561            return None;
1562        }
1563        let method = request.get("method").and_then(Value::as_str)?;
1564        if !DETACHED_METHODS.contains(&method) {
1565            return None;
1566        }
1567        let id = request.get("id").cloned().unwrap_or(Value::Null);
1568        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1569        let work = match method {
1570            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe" => self
1571                .inventory_work(method, params)
1572                .map(DetachedWork::Inventory),
1573            "harness.v1.sessions.message" => {
1574                decode::<MessageSessionParams>(params).map(DetachedWork::Message)
1575            }
1576            _ => {
1577                let verb = match method {
1578                    "harness.v1.sessions.new" => crate::SessionVerb::New,
1579                    "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
1580                    "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
1581                    _ => crate::SessionVerb::Delete,
1582                };
1583                match decode::<crate::SessionMutation>(params) {
1584                    Ok(mutation) => {
1585                        match crate::sessions_control::door(&mutation.harness, verb) {
1586                            // The live door needs the open runtime connection
1587                            // this service owns; it stays inline.
1588                            Ok(crate::SessionDoor::Live(_)) => return None,
1589                            Ok(_) => Ok(DetachedWork::SessionMutation { verb, mutation }),
1590                            Err(error) => Err(session_control_error(error)),
1591                        }
1592                    }
1593                    Err(error) => Err(error),
1594                }
1595            }
1596        };
1597        Some(DetachedCall {
1598            id,
1599            method: method.to_string(),
1600            work: work.map(Work::Free),
1601        })
1602    }
1603
1604    /// Recognize the two doors that wait on a runtime THIS SERVICE OWNS, and
1605    /// hand a transport the whole of each by lending the connection out.
1606    ///
1607    /// `runtimes.close` surrenders its runtime for good; the LIVE
1608    /// `sessions.new` / `sessions.reset` door borrows one for the length of
1609    /// the slash command and gives it back through
1610    /// [`Self::finish_detached`]. Both are bounded by
1611    /// [`RUNTIME_CONTROL_DEADLINE`], and a wedged runtime spends all of it —
1612    /// which is exactly as long as a transport that awaited them inline would
1613    /// stop reading its own input.
1614    ///
1615    /// `None` for every other method, and for the `sessions.new` /
1616    /// `sessions.reset` doors that are not live: [`Self::detach`] owns those.
1617    pub fn detach_runtime(&mut self, request: &Value) -> Option<DetachedCall> {
1618        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1619            return None;
1620        }
1621        let method = request.get("method").and_then(Value::as_str)?;
1622        let id = request.get("id").cloned().unwrap_or(Value::Null);
1623        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1624        let work = match method {
1625            "harness.v1.runtimes.close" => decode::<RuntimeConnectionParams>(params)
1626                .and_then(|params| self.surrender_runtime(&params.connection))
1627                .map(|(runtime, process_group)| {
1628                    Work::Runtime(RuntimeWork::Close {
1629                        runtime,
1630                        process_group,
1631                    })
1632                }),
1633            "harness.v1.sessions.new" | "harness.v1.sessions.reset" => {
1634                let verb = if method == "harness.v1.sessions.new" {
1635                    crate::SessionVerb::New
1636                } else {
1637                    crate::SessionVerb::Reset
1638                };
1639                let mutation = decode::<crate::SessionMutation>(params).ok()?;
1640                // Everything but the live door — including a refusal and a
1641                // request naming no connection — is `detach`'s or
1642                // `handle_async`'s to answer.
1643                let Ok(crate::SessionDoor::Live(command)) =
1644                    crate::sessions_control::door(&mutation.harness, verb)
1645                else {
1646                    return None;
1647                };
1648                let connection = mutation
1649                    .connection
1650                    .clone()
1651                    .filter(|value| !value.trim().is_empty())?;
1652                self.lend_runtime(&connection).map(|runtime| {
1653                    let session = live_session_name(runtime.as_ref(), &mutation);
1654                    Work::Runtime(RuntimeWork::LiveCommand {
1655                        connection,
1656                        runtime,
1657                        verb,
1658                        mutation,
1659                        command,
1660                        session,
1661                    })
1662                })
1663            }
1664            _ => return None,
1665        };
1666        Some(DetachedCall {
1667            id,
1668            method: method.to_string(),
1669            work,
1670        })
1671    }
1672
1673    /// Take back whatever a detached call borrowed and hand over the caller's
1674    /// response. Every answer from [`DetachedCall::run`] comes through here,
1675    /// so a lent-out connection is back in the service before the response
1676    /// that used it is written.
1677    pub fn finish_detached(&mut self, answer: DetachedAnswer) -> Value {
1678        let DetachedAnswer { response, returned } = answer;
1679        if let Some(ReturnedRuntime {
1680            connection,
1681            runtime,
1682        }) = returned
1683        {
1684            self.runtimes_in_flight.remove(&connection);
1685            self.runtimes.insert(connection, runtime);
1686        }
1687        response
1688    }
1689
1690    /// Answer a request split out by [`Self::runtime_open`] and already
1691    /// awaited by [`RuntimeOpen::open`]: register the runtime this service now
1692    /// owns and build its JSON-RPC response.
1693    pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1694        let OpenedRuntime { id, outcome } = opened;
1695        let result = match outcome {
1696            Ok(open) => self.register_open_runtime(open).await,
1697            Err(error) => Err(error),
1698        };
1699        service_response(id, result)
1700    }
1701
1702    /// Take ownership of an opened runtime.
1703    async fn register_open_runtime(
1704        &mut self,
1705        open: OpenRuntime,
1706    ) -> std::result::Result<Value, ServiceError> {
1707        match open {
1708            OpenRuntime::Hosted {
1709                runtime,
1710                capabilities,
1711                workspace,
1712            } => {
1713                self.insert_hosted_runtime(runtime, capabilities, workspace)
1714                    .await
1715            }
1716            OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1717        }
1718    }
1719
1720    async fn runtime_call(
1721        &mut self,
1722        method: &str,
1723        params: Value,
1724    ) -> std::result::Result<Value, ServiceError> {
1725        match method {
1726            "harness.v1.runtimes.capabilities" => {
1727                let params = decode::<RuntimeBackendParams>(params)?;
1728                let backend = runtime_backend(&params)?;
1729                Ok(json!({
1730                    "harness": backend.harness(),
1731                    "capabilities": backend.capabilities(),
1732                }))
1733            }
1734            method if RUNTIME_OPEN_METHODS.contains(&method) => {
1735                self.register_open_runtime(open_runtime(method, params).await?)
1736                    .await
1737            }
1738            "harness.v1.runtimes.send_input" => {
1739                let params = decode::<RuntimeInputParams>(params)?;
1740                let image_urls = validate_runtime_image_urls(params.image_urls)?;
1741                let runtime = self.runtime_mut(&params.connection)?;
1742                let turn_id = within_control_deadline(
1743                    method,
1744                    runtime.send_input(RuntimeInput {
1745                        text: params.text,
1746                        image_urls,
1747                    }),
1748                )
1749                .await?
1750                .map_err(operation)?;
1751                Ok(json!({"turn_id": turn_id}))
1752            }
1753            "harness.v1.runtimes.interrupt" => {
1754                let params = decode::<RuntimeConnectionParams>(params)?;
1755                within_control_deadline(method, self.runtime_mut(&params.connection)?.interrupt())
1756                    .await?
1757                    .map_err(operation)?;
1758                Ok(json!({}))
1759            }
1760            "harness.v1.runtimes.steer" => {
1761                let params = decode::<RuntimeInputParams>(params)?;
1762                if !params.image_urls.is_empty() {
1763                    return Err(ServiceError::InvalidParams(
1764                        "runtime steering accepts text only".into(),
1765                    ));
1766                }
1767                let text = params.text.trim();
1768                if text.is_empty() || text.chars().count() > 50_000 {
1769                    return Err(ServiceError::InvalidParams(
1770                        "runtime steering requires 1 to 50,000 text characters".into(),
1771                    ));
1772                }
1773                within_control_deadline(
1774                    method,
1775                    self.runtime_mut(&params.connection)?
1776                        .steer(text.to_string()),
1777                )
1778                .await?
1779                .map_err(operation)?;
1780                Ok(json!({}))
1781            }
1782            "harness.v1.runtimes.respond" => {
1783                let params = decode::<RuntimeRespondParams>(params)?;
1784                let request_id = params.request_id.clone();
1785                within_control_deadline(
1786                    method,
1787                    self.runtime_mut(&params.connection)?
1788                        .respond(params.request_id, params.response),
1789                )
1790                .await?
1791                .map_err(operation)?;
1792                // ORCH-9: an answered request is no longer waiting for one.
1793                self.approvals.answered(&params.connection, &request_id);
1794                Ok(json!({}))
1795            }
1796            "harness.v1.runtimes.acquire_control" => {
1797                let params = decode::<RuntimeConnectionParams>(params)?;
1798                let snapshot = within_control_deadline(
1799                    method,
1800                    self.runtime_mut(&params.connection)?.acquire_control(),
1801                )
1802                .await?
1803                .map_err(operation)?;
1804                serde_json::to_value(snapshot)
1805                    .map_err(|error| ServiceError::Operation(error.to_string()))
1806            }
1807            "harness.v1.runtimes.heartbeat" => {
1808                let params = decode::<RuntimeConnectionParams>(params)?;
1809                let snapshot = within_control_deadline(
1810                    method,
1811                    self.runtime_mut(&params.connection)?.heartbeat(),
1812                )
1813                .await?
1814                .map_err(operation)?;
1815                serde_json::to_value(snapshot)
1816                    .map_err(|error| ServiceError::Operation(error.to_string()))
1817            }
1818            "harness.v1.runtimes.detach" => {
1819                let params = decode::<RuntimeConnectionParams>(params)?;
1820                let snapshot =
1821                    within_control_deadline(method, self.runtime_mut(&params.connection)?.detach())
1822                        .await?
1823                        .map_err(operation)?;
1824                serde_json::to_value(snapshot)
1825                    .map_err(|error| ServiceError::Operation(error.to_string()))
1826            }
1827            "harness.v1.runtimes.terminal_instructions" => {
1828                let params = decode::<RuntimeConnectionParams>(params)?;
1829                let launch = self
1830                    .terminal_launches
1831                    .get(&params.connection)
1832                    .ok_or_else(|| {
1833                        ServiceError::Operation(
1834                            "this runtime is not hosted for terminal attachment".into(),
1835                        )
1836                    })?;
1837                Ok(json!({"launch":launch}))
1838            }
1839            "harness.v1.runtimes.close" => {
1840                let params = decode::<RuntimeConnectionParams>(params)?;
1841                let (runtime, process_group) = self.surrender_runtime(&params.connection)?;
1842                close_runtime(runtime, process_group).await
1843            }
1844            _ => Err(ServiceError::MethodNotFound),
1845        }
1846    }
1847
1848    /// Deliver one message into a session that is running right now.
1849    #[cfg(feature = "adapter-api")]
1850    async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1851        let params = decode::<MessageSessionParams>(params)?;
1852        Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
1853    }
1854
1855    #[cfg(feature = "adapter-api")]
1856    fn harness_settings_call(
1857        &self,
1858        method: &str,
1859        params: Value,
1860    ) -> std::result::Result<Value, ServiceError> {
1861        let homes = crate::HarnessHomes::default();
1862        match method {
1863            "harness.v1.harnesses.settings" => {
1864                let params = decode::<HarnessSettingsParams>(params)?;
1865                let report = crate::inspect_harness_interop_settings(&homes, &params.harness)
1866                    .map_err(|error| ServiceError::Operation(error.to_string()))?;
1867                serde_json::to_value(report)
1868                    .map_err(|error| ServiceError::Operation(error.to_string()))
1869            }
1870            "harness.v1.harnesses.configure" => {
1871                let params = decode::<ConfigureHarnessParams>(params)?;
1872                let report = crate::configure_harness_interop_settings(
1873                    &homes,
1874                    &params.harness,
1875                    &params.changes,
1876                    params.expected_revision.as_deref(),
1877                )
1878                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1879                serde_json::to_value(report)
1880                    .map_err(|error| ServiceError::Operation(error.to_string()))
1881            }
1882            _ => Err(ServiceError::MethodNotFound),
1883        }
1884    }
1885
1886    fn insert_runtime(
1887        &mut self,
1888        runtime: Box<dyn RuntimeConnection>,
1889    ) -> std::result::Result<Value, ServiceError> {
1890        let connection = format!("runtime-{}", self.next_runtime);
1891        self.next_runtime += 1;
1892        let handle = runtime.handle().clone();
1893        self.runtime_sequences
1894            .entry(handle.runtime_id.clone())
1895            .or_insert(0);
1896        self.runtimes.insert(connection.clone(), runtime);
1897        Ok(json!({"connection": connection, "handle": handle}))
1898    }
1899
1900    #[cfg(feature = "adapter-api")]
1901    async fn insert_hosted_runtime(
1902        &mut self,
1903        runtime: Box<dyn RuntimeConnection>,
1904        capabilities: crate::RuntimeCapabilities,
1905        workspace: PathBuf,
1906    ) -> std::result::Result<Value, ServiceError> {
1907        let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1908        let token: std::sync::Arc<str> = crate::server::generate_token().into();
1909        let server = crate::server::run_frontend_http(
1910            host.clone(),
1911            host.frontend_sender(),
1912            "127.0.0.1:0",
1913            token.clone(),
1914            connection.handle().runtime_id.clone(),
1915        )
1916        .await
1917        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1918        let source = LiveRuntimeSource {
1919            harness: connection.handle().harness.as_str().to_string(),
1920            session_id: connection.handle().runtime_id.clone(),
1921            workspace: workspace.clone(),
1922        };
1923        let registration = register_live_runtime(
1924            connection.handle().runtime_id.clone(),
1925            source.clone(),
1926            format!("http://{}", server.address()),
1927            token.to_string(),
1928        )
1929        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1930        let endpoint = registration.endpoint().to_string();
1931        let launch = StructuredLaunch {
1932            cwd: workspace,
1933            // Pin attachment to the executable hosting this runtime. A bare
1934            // `supercode` could resolve to an older global install whose CLI
1935            // does not understand the receipt it is being asked to open.
1936            program: std::env::current_exe()
1937                .ok()
1938                .map(|path| path.to_string_lossy().into_owned())
1939                .unwrap_or_else(|| "supercode".into()),
1940            arguments: vec![
1941                "harness".into(),
1942                "attach".into(),
1943                "--endpoint".into(),
1944                endpoint,
1945                "--harness".into(),
1946                source.harness,
1947                "--session".into(),
1948                source.session_id,
1949            ],
1950            env: BTreeMap::new(),
1951        };
1952        let lease = HostedRuntimeLease {
1953            connection,
1954            _host: host,
1955            _registration: registration,
1956            _server: server,
1957        };
1958        let opened = self.insert_runtime(Box::new(lease))?;
1959        let connection_id = opened["connection"]
1960            .as_str()
1961            .expect("insert_runtime returns a connection id")
1962            .to_string();
1963        self.terminal_launches.insert(connection_id, launch);
1964        Ok(opened)
1965    }
1966
1967    #[cfg(not(feature = "adapter-api"))]
1968    async fn insert_hosted_runtime(
1969        &mut self,
1970        runtime: Box<dyn RuntimeConnection>,
1971        _capabilities: crate::RuntimeCapabilities,
1972        _workspace: PathBuf,
1973    ) -> std::result::Result<Value, ServiceError> {
1974        self.insert_runtime(runtime)
1975    }
1976
1977    fn runtime_mut(
1978        &mut self,
1979        connection: &str,
1980    ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1981        if self.runtimes_in_flight.contains(connection) {
1982            return Err(self.lent_out(connection));
1983        }
1984        self.runtimes.get_mut(connection).ok_or_else(|| {
1985            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1986        })
1987    }
1988
1989    /// What a caller is told about a connection that is out on a detached
1990    /// call. It is not gone and it is not free: it is mid-call, which is the
1991    /// same answer the runtime itself gives a second turn.
1992    fn lent_out(&self, connection: &str) -> ServiceError {
1993        ServiceError::Operation(format!(
1994            "runtime connection `{connection}`: a harness turn is already in progress"
1995        ))
1996    }
1997
1998    /// Take a runtime OUT of the service for the duration of one detached
1999    /// call, leaving its name marked as lent out.
2000    fn lend_runtime(
2001        &mut self,
2002        connection: &str,
2003    ) -> std::result::Result<Box<dyn RuntimeConnection>, ServiceError> {
2004        if self.runtimes_in_flight.contains(connection) {
2005            return Err(self.lent_out(connection));
2006        }
2007        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2008            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2009        })?;
2010        self.runtimes_in_flight.insert(connection.to_string());
2011        Ok(runtime)
2012    }
2013
2014    /// Surrender a runtime for good: the connection and everything the
2015    /// service hung off it are gone before its teardown is even attempted.
2016    ///
2017    /// `close` is what a caller reaches for when a runtime has stopped
2018    /// answering, and a runtime that has stopped answering is exactly the one
2019    /// whose graceful close cannot complete: a hosted runtime's own loop
2020    /// parks on the call the runtime never answered, so it never dequeues the
2021    /// shutdown either. Keeping the entry until teardown succeeded made a
2022    /// wedged runtime permanent — every later call on that connection, and
2023    /// every new turn, answered "a harness turn is already in progress" with
2024    /// no way to take the connection back.
2025    fn surrender_runtime(
2026        &mut self,
2027        connection: &str,
2028    ) -> std::result::Result<(Box<dyn RuntimeConnection>, Option<u32>), ServiceError> {
2029        if self.runtimes_in_flight.contains(connection) {
2030            return Err(self.lent_out(connection));
2031        }
2032        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2033            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2034        })?;
2035        let process_group = runtime_process_group(runtime.handle());
2036        let runtime_id = runtime.handle().runtime_id.clone();
2037        self.terminal_launches.remove(connection);
2038        self.runtime_sequences.remove(&runtime_id);
2039        self.approvals.forget(connection);
2040        Ok((runtime, process_group))
2041    }
2042
2043    /// SIGKILL the process group of every runtime this service owns, without
2044    /// waiting on any of them.
2045    ///
2046    /// A host leaving for good calls this BEFORE dropping the service. The
2047    /// handle this service holds is not the runtime's connection: a hosted
2048    /// runtime's real transport lives in the task driving it, so neither
2049    /// exiting the process nor dropping these handles reaches the harness
2050    /// process — while dropping them does remove each runtime's live-runtime
2051    /// receipt. Signalling first is what keeps a removed receipt from
2052    /// advertising a harness that is still running.
2053    pub fn kill_all_runtime_groups(&self) -> usize {
2054        self.runtimes
2055            .values()
2056            .filter(|runtime| kill_runtime_process_group(runtime_process_group(runtime.handle())))
2057            .count()
2058    }
2059
2060    /// ORCH-19: run one conversation-lifecycle verb through the harness's own
2061    /// door.
2062    ///
2063    /// Two doors, one shape. A CLI / HTTP / own-store door is self-contained
2064    /// in [`crate::sessions_control`]. A LIVE door (Hermes's and OpenClaw's
2065    /// `/new` and `/reset`, which are slash commands their gateway interprets
2066    /// INSIDE a session) is performed here, because only the service owns the
2067    /// open runtime connection — the command is typed through the very same
2068    /// `send_input` path a human's message takes, so supercode invents no
2069    /// private channel.
2070    async fn mutate_session(
2071        &mut self,
2072        verb: crate::SessionVerb,
2073        params: Value,
2074    ) -> std::result::Result<Value, ServiceError> {
2075        let mutation = decode::<crate::SessionMutation>(params)?;
2076        let door = crate::sessions_control::door(&mutation.harness, verb)
2077            .map_err(session_control_error)?;
2078        let outcome = match door {
2079            // The live door types the slash command through an open hosted
2080            // runtime, which only exists with the `adapter-api` feature; the
2081            // CLI / HTTP / own-store doors below need nothing extra.
2082            #[cfg(not(feature = "adapter-api"))]
2083            crate::SessionDoor::Live(command) => {
2084                return Err(ServiceError::Operation(format!(
2085                    "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
2086                     session, which needs this build's `adapter-api` feature",
2087                    mutation.harness,
2088                    verb.as_str()
2089                )));
2090            }
2091            #[cfg(feature = "adapter-api")]
2092            crate::SessionDoor::Live(command) => {
2093                let connection = mutation
2094                    .connection
2095                    .clone()
2096                    .filter(|value| !value.trim().is_empty())
2097                    .ok_or_else(|| {
2098                        ServiceError::InvalidParams(format!(
2099                            "`{}` performs `sessions.{}` by typing `{command}` into a live \
2100                             driven session: pass the `connection` of an open runtime \
2101                             (`harness.v1.runtimes.start`)",
2102                            mutation.harness,
2103                            verb.as_str()
2104                        ))
2105                    })?;
2106                let runtime = self.runtime_mut(&connection)?;
2107                let session = live_session_name(runtime.as_ref(), &mutation);
2108                // Typing into a live session is a control call on an open
2109                // runtime, and a wedged runtime never accepts one, so it is
2110                // bounded exactly like the other control verbs. A transport
2111                // with a loop of its own lends the connection out instead of
2112                // waiting here: see [`Self::detach_runtime`].
2113                return type_live_command(runtime.as_mut(), verb, &mutation, command, session)
2114                    .await;
2115            }
2116            _ => run_session_mutation(verb, &mutation).await?,
2117        };
2118        serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
2119    }
2120
2121    /// Answer an inventory request whole, for callers that have nowhere to
2122    /// put the waiting half. A transport with a loop of its own splits it
2123    /// instead: see [`Self::detach`].
2124    async fn inventory_call(
2125        &self,
2126        method: &str,
2127        params: Value,
2128    ) -> std::result::Result<Value, ServiceError> {
2129        run_inventory(self.inventory_work(method, params)?).await
2130    }
2131
2132    /// The half of an inventory request that reads this service's state:
2133    /// resolve the selection and count the persisted sessions each row
2134    /// reports. What remains — finding executables, asking them their
2135    /// version, and (at `probe: handshake`) starting each harness and
2136    /// completing its protocol handshake — touches no service state at all.
2137    fn inventory_work(
2138        &self,
2139        method: &str,
2140        params: Value,
2141    ) -> std::result::Result<InventoryWork, ServiceError> {
2142        let mut params = decode::<HarnessInventoryParams>(params)?;
2143        if method == "harness.v1.harnesses.probe" {
2144            let harness = params.harness.take().ok_or_else(|| {
2145                ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
2146            })?;
2147            params.harnesses = vec![harness];
2148        }
2149        let selected = params
2150            .harnesses
2151            .iter()
2152            .map(HarnessId::as_str)
2153            .collect::<std::collections::BTreeSet<_>>();
2154        let supported = harness_support_registry()
2155            .harnesses
2156            .into_iter()
2157            .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
2158            .collect::<Vec<_>>();
2159        if !params.harnesses.is_empty() && supported.len() != selected.len() {
2160            let known = supported
2161                .iter()
2162                .map(|harness| harness.id.as_str())
2163                .collect::<std::collections::BTreeSet<_>>();
2164            let missing = params
2165                .harnesses
2166                .iter()
2167                .filter(|id| !known.contains(id.as_str()))
2168                .map(HarnessId::as_str)
2169                .collect::<Vec<_>>();
2170            return Err(ServiceError::InvalidParams(format!(
2171                "unknown harness(es): {}",
2172                missing.join(", ")
2173            )));
2174        }
2175        let global_counts = params
2176            .include_sessions
2177            .then(|| self.session_counts(None, &params.harnesses));
2178        let workspace_counts = params
2179            .include_sessions
2180            .then(|| {
2181                params
2182                    .workspace
2183                    .as_deref()
2184                    .map(|workspace| self.session_counts(Some(workspace), &params.harnesses))
2185            })
2186            .flatten();
2187        Ok(InventoryWork {
2188            params,
2189            supported,
2190            global_counts,
2191            workspace_counts,
2192        })
2193    }
2194
2195    #[cfg(feature = "adapter-api")]
2196    async fn harness_authentication_call(
2197        &self,
2198        method: &str,
2199        params: Value,
2200    ) -> std::result::Result<Value, ServiceError> {
2201        match method {
2202            "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
2203                let params = decode::<HarnessAuthenticationParams>(params)?;
2204                serde_json::to_value(crate::inspect_harness_authentication(&params.harness).await)
2205                    .map_err(|error| ServiceError::Operation(error.to_string()))
2206            }
2207            "harness.v1.harnesses.auth.begin" => {
2208                let params = decode::<BeginHarnessAuthenticationParams>(params)?;
2209                let cwd = params
2210                    .cwd
2211                    .or_else(|| std::env::current_dir().ok())
2212                    .unwrap_or_else(|| PathBuf::from("."));
2213                let plan = crate::harness_authentication_plan(
2214                    &params.harness,
2215                    params.environment,
2216                    params.method,
2217                    &cwd,
2218                )
2219                .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
2220                serde_json::to_value(plan)
2221                    .map_err(|error| ServiceError::Operation(error.to_string()))
2222            }
2223            _ => Err(ServiceError::MethodNotFound),
2224        }
2225    }
2226
2227    fn session_counts(
2228        &self,
2229        workspace: Option<&Path>,
2230        harnesses: &[HarnessId],
2231    ) -> BTreeMap<String, usize> {
2232        let mut counts = BTreeMap::new();
2233        for session in self
2234            .catalog
2235            .discover(&DiscoveryQuery {
2236                workspace: workspace.map(Path::to_path_buf),
2237                harnesses: harnesses.to_vec(),
2238                ..DiscoveryQuery::default()
2239            })
2240            .unwrap_or_default()
2241        {
2242            *counts
2243                .entry(session.locator.harness.as_str().to_string())
2244                .or_insert(0) += 1;
2245        }
2246        counts
2247    }
2248}
2249
2250#[async_trait::async_trait]
2251impl SdkService for HarnessSessionService {
2252    fn capabilities(&self) -> SdkCapabilities {
2253        SdkCapabilities::default()
2254    }
2255
2256    async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2257        if request.operation == SdkOperation::Events {
2258            let events = self
2259                .poll_sdk_events()
2260                .await
2261                .into_iter()
2262                .map(|(_, event)| event)
2263                .collect::<Vec<_>>();
2264            return serde_json::to_value(events).map_err(|error| {
2265                SdkError::new(
2266                    SdkErrorCode::Execution,
2267                    request.operation,
2268                    error.to_string(),
2269                )
2270            });
2271        }
2272        if self.runtimes.is_empty()
2273            && matches!(
2274                request.operation,
2275                SdkOperation::Input
2276                    | SdkOperation::Interrupt
2277                    | SdkOperation::Steer
2278                    | SdkOperation::Respond
2279                    | SdkOperation::Close
2280            )
2281        {
2282            return Err(SdkError::unsupported(request.operation));
2283        }
2284        let method = request
2285            .operation
2286            .method()
2287            .ok_or_else(|| SdkError::unsupported(request.operation))?;
2288        let result = match request.operation {
2289            SdkOperation::Discover
2290            | SdkOperation::Load
2291            | SdkOperation::Export
2292            | SdkOperation::ProfilesList
2293            | SdkOperation::ProfilesGet
2294            | SdkOperation::ProfilesCreate
2295            | SdkOperation::ProfilesDelete
2296            | SdkOperation::SkillsList
2297            | SdkOperation::SkillsInstall
2298            | SdkOperation::SkillsRemove
2299            | SdkOperation::ChannelsList
2300            | SdkOperation::RoutesList
2301            | SdkOperation::TriggersList
2302            | SdkOperation::ChannelsStatus
2303            | SdkOperation::MemoryShow
2304            | SdkOperation::MemorySearch
2305            | SdkOperation::JobsList
2306            | SdkOperation::JobsGet
2307            | SdkOperation::JobsCreate
2308            | SdkOperation::JobsUpdate
2309            | SdkOperation::JobsPause
2310            | SdkOperation::JobsResume
2311            | SdkOperation::JobsRun
2312            | SdkOperation::JobsDelete
2313            | SdkOperation::JobsNotepad
2314            | SdkOperation::JobsNotepadSet
2315            | SdkOperation::JobsNotepadDelete
2316            | SdkOperation::RunsList
2317            | SdkOperation::RunsGet
2318            | SdkOperation::ApprovalsList
2319            | SdkOperation::OrchestrationLoad
2320            | SdkOperation::OrchestrationSave
2321            | SdkOperation::OrchestrationCompile
2322            | SdkOperation::OrchestrationDecompile
2323            | SdkOperation::OrchestrationImport
2324            | SdkOperation::OrchestrationExport
2325            | SdkOperation::WorkflowLoad => self.call(method, request.params),
2326            // ORCH-20: answering needs the live connection, so it takes the
2327            // async door and ends in `harness.v1.runtimes.respond`.
2328            SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2329            SdkOperation::Start
2330            | SdkOperation::Resume
2331            | SdkOperation::Input
2332            | SdkOperation::Interrupt
2333            | SdkOperation::Steer
2334            | SdkOperation::Respond
2335            | SdkOperation::Close => self.runtime_call(method, request.params).await,
2336            // ORCH-19 controlled tier. Every verb goes through the HARNESS'S
2337            // OWN door — its CLI, its HTTP API, or its slash command typed
2338            // into a live driven session — and returns the row re-read from
2339            // the harness's store afterwards.
2340            SdkOperation::SessionsNew => {
2341                self.mutate_session(crate::SessionVerb::New, request.params)
2342                    .await
2343            }
2344            SdkOperation::SessionsReset => {
2345                self.mutate_session(crate::SessionVerb::Reset, request.params)
2346                    .await
2347            }
2348            SdkOperation::SessionsArchive => {
2349                self.mutate_session(crate::SessionVerb::Archive, request.params)
2350                    .await
2351            }
2352            SdkOperation::SessionsDelete => {
2353                self.mutate_session(crate::SessionVerb::Delete, request.params)
2354                    .await
2355            }
2356            SdkOperation::Events => unreachable!("handled before method dispatch"),
2357        };
2358        result.map_err(|error| sdk_error(request.operation, error))
2359    }
2360
2361    async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2362        Ok(self
2363            .poll_sdk_events()
2364            .await
2365            .into_iter()
2366            .map(|(_, event)| event)
2367            .collect())
2368    }
2369}
2370
2371#[cfg(feature = "adapter-api")]
2372struct HostedRuntimeLease {
2373    connection: HostedHarnessConnection,
2374    _host: std::sync::Arc<HostedHarnessRuntime>,
2375    _registration: LiveRuntimeRegistration,
2376    _server: crate::server::FrontendHttpServer,
2377}
2378
2379#[async_trait::async_trait]
2380#[cfg(feature = "adapter-api")]
2381impl RuntimeConnection for HostedRuntimeLease {
2382    fn handle(&self) -> &crate::RuntimeHandle {
2383        self.connection.handle()
2384    }
2385
2386    async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2387        self.connection.send_input(input).await
2388    }
2389
2390    async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2391        self.connection.next_event().await
2392    }
2393
2394    async fn interrupt(&mut self) -> crate::Result<()> {
2395        self.connection.interrupt().await
2396    }
2397
2398    // the lease must forward every verb its capabilities advertise; without
2399    // this, steer fell to the trait default and refused a turn it claimed
2400    async fn steer(&mut self, text: String) -> crate::Result<()> {
2401        self.connection.steer(text).await
2402    }
2403
2404    async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2405        self.connection.respond(request_id, response).await
2406    }
2407
2408    async fn close(&mut self) -> crate::Result<()> {
2409        self.connection.close().await
2410    }
2411}
2412
2413/// One inventory request's waiting half, already separated from the service
2414/// state it reads. See [`HarnessSessionService::inventory_work`].
2415struct InventoryWork {
2416    params: HarnessInventoryParams,
2417    supported: Vec<crate::HarnessSupportDescriptor>,
2418    global_counts: Option<BTreeMap<String, usize>>,
2419    workspace_counts: Option<BTreeMap<String, usize>>,
2420}
2421
2422/// Perform one conversation-lifecycle verb through a door that is
2423/// self-contained in [`crate::sessions_control`]: the harness's own CLI, its
2424/// HTTP API, the orchestrator daemon's socket, or supercode's own store.
2425/// Touches no service state, so this runs on any task. The LIVE door is not
2426/// here — it types its slash command through a runtime connection the service
2427/// owns, and is performed by [`HarnessSessionService::mutate_session`].
2428async fn run_session_mutation(
2429    verb: crate::SessionVerb,
2430    mutation: &crate::SessionMutation,
2431) -> std::result::Result<crate::SessionMutationOutcome, ServiceError> {
2432    // Only the HTTP door actually awaits anything. The CLI, store and daemon
2433    // doors run the harness's own program, or its store, with calls that
2434    // block the calling THREAD from start to finish — a future that never
2435    // yields, which no timeout around it can interrupt and which would hold a
2436    // runtime worker for as long as the harness takes. They go to a blocking
2437    // task, where blocking is what the thread is for.
2438    let door =
2439        crate::sessions_control::door(&mutation.harness, verb).map_err(session_control_error)?;
2440    if let crate::SessionDoor::Http = door {
2441        return crate::sessions_control::mutate(verb, mutation)
2442            .await
2443            .map_err(session_control_error);
2444    }
2445    let mutation = mutation.clone();
2446    tokio::task::spawn_blocking(move || crate::sessions_control::mutate_blocking(verb, &mutation))
2447        .await
2448        .map_err(|error| {
2449            ServiceError::Operation(format!("the conversation verb could not be run: {error}"))
2450        })?
2451        .map_err(session_control_error)
2452}
2453
2454/// Probe every selected harness and assemble the report. Touches no service
2455/// state, so this runs on any task.
2456async fn run_inventory(work: InventoryWork) -> std::result::Result<Value, ServiceError> {
2457    let InventoryWork {
2458        params,
2459        supported,
2460        global_counts,
2461        workspace_counts,
2462    } = work;
2463    let probes = supported.into_iter().map(|descriptor| {
2464        let global = global_counts
2465            .as_ref()
2466            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2467        let workspace = workspace_counts
2468            .as_ref()
2469            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2470        probe_harness(descriptor, &params, global, workspace)
2471    });
2472    let harnesses = futures::future::join_all(probes).await;
2473    serde_json::to_value(HarnessInventoryReport {
2474        probe: params.probe,
2475        workspace: params.workspace,
2476        harnesses,
2477    })
2478    .map_err(|error| ServiceError::Operation(error.to_string()))
2479}
2480
2481async fn probe_harness(
2482    descriptor: crate::HarnessSupportDescriptor,
2483    params: &HarnessInventoryParams,
2484    global: Option<usize>,
2485    workspace: Option<usize>,
2486) -> LocalHarness {
2487    let launch = descriptor.runtime.default_launch.as_ref();
2488    // ORC-7: the orchestrator publishes no runtime launch — it is not an
2489    // adapter supercode connects a turn to. What "installed" means for it
2490    // is that its Node daemon entry is present, so the row answers from
2491    // that instead of from a PATH lookup it could never satisfy.
2492    let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
2493        .then(crate::orchestrator::daemon_entry)
2494        .and_then(Result::ok);
2495    let executable = match &orchestrator_entry {
2496        Some(entry) => Some(entry.clone()),
2497        None => launch.and_then(|launch| find_executable(&launch.program)),
2498    };
2499    let installed = executable.is_some();
2500    let version = if params.skip_versions || orchestrator_entry.is_some() {
2501        // The orchestrator's "executable" is a Node module, not a CLI
2502        // with a `--version` flag; running it to ask would start a daemon.
2503        None
2504    } else {
2505        match executable.as_deref() {
2506            Some(path) => executable_version(path).await,
2507            None => None,
2508        }
2509    };
2510    let configured = auth_evidence(descriptor.id.as_str());
2511    let mut auth = if configured {
2512        HarnessAuthState::Configured
2513    } else if matches!(
2514        descriptor.id.as_str(),
2515        HarnessId::CLAUDE_CODE | HarnessId::CODEX
2516    ) {
2517        // These two adapters have explicit native status/login contracts
2518        // and complete local evidence coverage (including Claude's macOS
2519        // Keychain-backed oauthAccount marker). Treating absent evidence
2520        // as unknown advertises a start that will only fail interactively.
2521        HarnessAuthState::Required
2522    } else {
2523        HarnessAuthState::Unknown
2524    };
2525    let mut runtime = if installed {
2526        HarnessRuntimeState::Degraded
2527    } else {
2528        HarnessRuntimeState::Unavailable
2529    };
2530    let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2531    let mut reason = (!installed).then(|| {
2532        if is_orchestrator {
2533            format!(
2534                "{} is supported but its daemon entry `{}` was not found",
2535                descriptor.display_name,
2536                crate::orchestrator::DAEMON_ENTRY
2537            )
2538        } else {
2539            format!(
2540                "{} is supported but `{}` was not found on PATH",
2541                descriptor.display_name,
2542                launch
2543                    .map(|launch| launch.program.as_str())
2544                    .unwrap_or("executable")
2545            )
2546        }
2547    });
2548    let mut repair = (!installed).then(|| {
2549        if is_orchestrator {
2550            format!(
2551                "Install the `supercode-orchestrator` package so `{}` resolves.",
2552                crate::orchestrator::DAEMON_ENTRY
2553            )
2554        } else {
2555            format!(
2556                "Install {} and ensure `{}` is on PATH.",
2557                descriptor.display_name,
2558                launch
2559                    .map(|launch| launch.program.as_str())
2560                    .unwrap_or("its executable")
2561            )
2562        }
2563    });
2564
2565    if installed && params.probe == HarnessProbeLevel::Handshake {
2566        let backend_params = RuntimeBackendParams {
2567            harness: descriptor.id.clone(),
2568            protocol: None,
2569            launch: None,
2570            base_url: None,
2571            policy: RuntimePolicy::Default,
2572        };
2573        match runtime_backend(&backend_params) {
2574            Ok(backend) => {
2575                let cwd = params
2576                    .workspace
2577                    .clone()
2578                    .or_else(|| std::env::current_dir().ok())
2579                    .unwrap_or_else(|| PathBuf::from("."));
2580                let isolated = descriptor
2581                    .runtime
2582                    .default_launch
2583                    .clone()
2584                    .and_then(|launch| IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok());
2585                let Some(isolated) = isolated else {
2586                    reason = Some(
2587                        "No-prompt runtime handshake could not create its isolated harness home."
2588                            .into(),
2589                    );
2590                    repair = Some(
2591                        "Check temporary-directory permissions, then run the handshake probe again."
2592                            .into(),
2593                    );
2594                    let running = probe_running_instance(descriptor.id.as_str());
2595                    return LocalHarness {
2596                        gateway: gateway_health(
2597                            descriptor.id.as_str(),
2598                            installed,
2599                            running.as_ref(),
2600                            version.as_deref(),
2601                        ),
2602                        id: descriptor.id,
2603                        display_name: descriptor.display_name,
2604                        supported: true,
2605                        installed,
2606                        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2607                        version,
2608                        auth,
2609                        runtime,
2610                        protocol: descriptor.runtime.protocol,
2611                        capabilities: descriptor.runtime.capabilities.clone(),
2612                        effective_capabilities: descriptor.runtime.capabilities,
2613                        sessions: HarnessSessionCounts { global, workspace },
2614                        running,
2615                        reason,
2616                        repair,
2617                    };
2618                };
2619                match tokio::time::timeout(
2620                    Duration::from_secs(30),
2621                    backend.start(RuntimeStartRequest {
2622                        cwd,
2623                        launch: Some(isolated.launch.clone()),
2624                        mcp_servers: Vec::new(),
2625                    }),
2626                )
2627                .await
2628                {
2629                    Ok(Ok(mut connection)) => {
2630                        match stabilize_handshake(connection.as_mut()).await {
2631                            Ok(()) => {
2632                                auth = HarnessAuthState::Ready;
2633                                runtime = HarnessRuntimeState::Ready;
2634                                reason = Some(
2635                                    "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2636                                        .into(),
2637                                );
2638                                repair = None;
2639                            }
2640                            Err(message) => {
2641                                auth = if looks_like_auth_error(&message) {
2642                                    HarnessAuthState::Required
2643                                } else if configured {
2644                                    HarnessAuthState::Configured
2645                                } else {
2646                                    HarnessAuthState::Unknown
2647                                };
2648                                reason = Some(format!(
2649                                    "No-prompt runtime handshake became unhealthy during startup: {message}"
2650                                ));
2651                                repair = Some(if auth == HarnessAuthState::Required {
2652                                    format!(
2653                                        "Run `{}` interactively once and complete sign-in, then probe again.",
2654                                        launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2655                                    )
2656                                } else {
2657                                    "Run the harness directly to inspect its startup failure, then probe again."
2658                                        .into()
2659                                });
2660                            }
2661                        }
2662                        let _ =
2663                            tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2664                    }
2665                    Ok(Err(error)) => {
2666                        let message = truncate_text(&error.to_string(), 500);
2667                        auth = if looks_like_auth_error(&message) {
2668                            HarnessAuthState::Required
2669                        } else if configured {
2670                            HarnessAuthState::Configured
2671                        } else {
2672                            HarnessAuthState::Unknown
2673                        };
2674                        reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2675                        repair = Some(if auth == HarnessAuthState::Required {
2676                            format!(
2677                                "Run `{}` interactively once and complete sign-in, then probe again.",
2678                                launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2679                            )
2680                        } else {
2681                            "Check the harness installation and run the handshake probe again."
2682                                .into()
2683                        });
2684                    }
2685                    Err(_) => {
2686                        reason =
2687                            Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2688                        repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2689                    }
2690                }
2691                // Keep the isolated home alive through process teardown.
2692                // Otherwise the compiler may release the last meaningful
2693                // use after cloning `launch`, and a still-starting CLI can
2694                // recreate its state directory after Drop removed it.
2695                // Some Node-based launchers finish a short asynchronous
2696                // installation-id write just after their parent process
2697                // is reaped. Remove once immediately, allow that bounded
2698                // writer to settle, then perform the authoritative pass.
2699                let _ = isolated.cleanup();
2700                tokio::time::sleep(Duration::from_millis(250)).await;
2701                if let Err(error) = isolated.cleanup() {
2702                    auth = if configured {
2703                        HarnessAuthState::Configured
2704                    } else {
2705                        HarnessAuthState::Unknown
2706                    };
2707                    runtime = HarnessRuntimeState::Degraded;
2708                    reason = Some(format!(
2709                        "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2710                    ));
2711                    repair = Some(
2712                        "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2713                            .into(),
2714                    );
2715                }
2716            }
2717            Err(error) => {
2718                reason = Some(error_message(error));
2719            }
2720        }
2721    } else if installed && configured {
2722        reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2723    } else if installed && auth == HarnessAuthState::Required {
2724        reason = Some("Executable found, but no native authentication evidence is present.".into());
2725        repair = Some(format!(
2726            "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2727            descriptor.id.as_str()
2728        ));
2729    } else if installed {
2730        reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2731        repair = Some(format!(
2732            "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2733            launch
2734                .map(|launch| launch.program.as_str())
2735                .unwrap_or("the harness")
2736        ));
2737    }
2738
2739    let effective_capabilities = if installed {
2740        descriptor.runtime.capabilities.clone()
2741    } else {
2742        unavailable_capabilities()
2743    };
2744    let running = probe_running_instance(descriptor.id.as_str());
2745    LocalHarness {
2746        gateway: gateway_health(
2747            descriptor.id.as_str(),
2748            installed,
2749            running.as_ref(),
2750            version.as_deref(),
2751        ),
2752        id: descriptor.id,
2753        display_name: descriptor.display_name,
2754        supported: true,
2755        installed,
2756        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2757        version,
2758        auth,
2759        runtime,
2760        protocol: descriptor.runtime.protocol,
2761        capabilities: descriptor.runtime.capabilities,
2762        effective_capabilities,
2763        sessions: HarnessSessionCounts { global, workspace },
2764        running,
2765        reason,
2766        repair,
2767    }
2768}
2769
2770async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2771    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2772    loop {
2773        let now = tokio::time::Instant::now();
2774        if now >= deadline {
2775            return Ok(());
2776        }
2777        match tokio::time::timeout(deadline - now, connection.next_event()).await {
2778            Err(_) => return Ok(()),
2779            Ok(Ok(Some(event))) => {
2780                if let Some(message) = handshake_event_failure(&event) {
2781                    return Err(truncate_text(&message, 500));
2782                }
2783            }
2784            Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2785            Ok(Err(error)) => return Err(error.to_string()),
2786        }
2787    }
2788}
2789
2790fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2791    let detail = event
2792        .payload
2793        .get("message")
2794        .or_else(|| event.payload.get("line"))
2795        .and_then(Value::as_str)
2796        .unwrap_or(event.kind.as_str());
2797    match event.kind.as_str() {
2798        "transport_closed" => Some("runtime transport closed during startup".into()),
2799        "transport_error" => Some(format!("runtime transport error: {detail}")),
2800        "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2801        // Stderr is retained as a runtime event, but is not transport health.
2802        // Grok, for example, can log an AuthorizationRequired error from an
2803        // optional background worker while its ACP session continues to send
2804        // updates and complete prompts normally.
2805        _ => None,
2806    }
2807}
2808
2809fn indexed_claude_window(
2810    locator: &SessionLocator,
2811    options: &SessionLoadOptions,
2812) -> std::result::Result<Option<Value>, ServiceError> {
2813    use supercode_interchange::session::ClaudeReadIndex;
2814    // Exact parent-only window: recursive/full-artifact requests retain the
2815    // existing owner. This is not a bounded display-history substitution.
2816    if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2817        || options.include_subagents != Some(false)
2818    {
2819        return Ok(None);
2820    }
2821    let crate::StorageLocator::File { path } = &locator.storage else {
2822        return Ok(None);
2823    };
2824    if !ClaudeReadIndex::supports(path)
2825        .map_err(|error| ServiceError::Operation(error.to_string()))?
2826    {
2827        return Ok(None);
2828    }
2829    let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2830        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2831    let total = index.len();
2832    let (offset, end) = projected_message_window(total, options);
2833    let session = index
2834        .read_messages(offset..end)
2835        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2836    let summary = index
2837        .read_summary()
2838        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2839    let selected_options = SessionLoadOptions {
2840        message_offset: None,
2841        message_limit: None,
2842        message_tail: None,
2843        ..options.clone()
2844    };
2845    let mut selected = projected_session_json(&session, &selected_options);
2846    selected["raw_record_count"] = json!(index.raw_record_count());
2847    Ok(Some(json!({
2848        "session": selected,
2849        "summary": projected_session_summary(&summary, options),
2850        "window": {
2851            "has_more": offset > 0 || end < total, "has_newer": end < total,
2852            "has_older": offset > 0, "newer_items": index.item_count(end..total),
2853            "offset": offset, "older_items": index.item_count(0..offset),
2854            "returned": end - offset, "total_messages": total,
2855        }
2856    })))
2857}
2858
2859fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2860    let total_messages = session.messages.len();
2861    let (offset, end) = projected_message_window(total_messages, options);
2862    json!({
2863        "session": projected_session_json(session, options),
2864        "summary": projected_session_summary(session, options),
2865        "window": {
2866            "has_more": offset > 0 || end < total_messages,
2867            "has_newer": end < total_messages,
2868            "has_older": offset > 0,
2869            "newer_items": normalized_item_count(&session.messages[end..]),
2870            "offset": offset,
2871            "older_items": normalized_item_count(&session.messages[..offset]),
2872            "returned": end.saturating_sub(offset),
2873            "total_messages": total_messages,
2874        }
2875    })
2876}
2877
2878fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2879    messages
2880        .iter()
2881        .map(|message| {
2882            let conversation = usize::from(
2883                matches!(message.role, Role::Assistant | Role::User)
2884                    && message_has_content(message),
2885            );
2886            let tool_result =
2887                usize::from(message.role == Role::Tool && message_has_content(message));
2888            conversation + tool_result + message.tool_calls().len()
2889        })
2890        .sum()
2891}
2892
2893fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2894    let mut conversational = session.messages.iter().filter(|message| {
2895        matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2896    });
2897    let first_message = conversational.clone().next();
2898    let last_message = conversational.next_back();
2899    let mut assistant = session
2900        .messages
2901        .iter()
2902        .filter(|message| message.role == Role::Assistant && message_has_content(message));
2903    let first_assistant_message = assistant.clone().next();
2904    let last_assistant_message = assistant.next_back();
2905    let end_of_turn = session
2906        .messages
2907        .iter()
2908        .rev()
2909        .find(|message| message.role != Role::System)
2910        .is_some_and(|message| {
2911            message.role == Role::Assistant
2912                && message_has_content(message)
2913                && message.tool_calls().is_empty()
2914        });
2915    let project = |message: Option<&crate::ChatMessage>| {
2916        message.map(|message| project_inline_media(message_json(message), options))
2917    };
2918    json!({
2919        "end_of_turn": end_of_turn,
2920        "first_assistant_message": project(first_assistant_message),
2921        "first_message": project(first_message),
2922        "last_assistant_message": project(last_assistant_message),
2923        "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2924        "last_message": project(last_message),
2925    })
2926}
2927
2928fn message_has_content(message: &crate::ChatMessage) -> bool {
2929    message
2930        .content
2931        .as_deref()
2932        .is_some_and(|content| !content.trim().is_empty())
2933        || message
2934            .content_parts
2935            .as_ref()
2936            .is_some_and(|parts| !parts.is_empty())
2937}
2938
2939fn message_text(message: &crate::ChatMessage) -> String {
2940    if let Some(content) = &message.content {
2941        return content.clone();
2942    }
2943    message
2944        .content_parts
2945        .as_ref()
2946        .into_iter()
2947        .flatten()
2948        .filter_map(|part| part.get("text").and_then(Value::as_str))
2949        .collect::<Vec<_>>()
2950        .join("\n")
2951}
2952
2953fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2954    let (offset, end) = projected_message_window(session.messages.len(), options);
2955    let messages = session.messages[offset..end]
2956        .iter()
2957        .map(|message| project_inline_media(message_json(message), options))
2958        .collect::<Vec<_>>();
2959    let subagents = if options.include_subagents.unwrap_or(true) {
2960        // The reported window describes the top-level transcript. Applying it
2961        // recursively would silently truncate subagents without returning a
2962        // window for each child. Keep their histories complete while carrying
2963        // the caller's media policy through the tree.
2964        let subagent_options = SessionLoadOptions {
2965            message_limit: None,
2966            message_offset: None,
2967            message_tail: None,
2968            ..options.clone()
2969        };
2970        session
2971            .subagents
2972            .iter()
2973            .map(|subagent| projected_session_json(subagent, &subagent_options))
2974            .collect::<Vec<_>>()
2975    } else {
2976        Vec::new()
2977    };
2978    json!({
2979        "source": match session.meta.source {
2980            SessionSource::ClaudeCode => "claude_code",
2981            SessionSource::Codex => "codex",
2982            SessionSource::Gemini => "gemini",
2983            SessionSource::Goose => "goose",
2984            SessionSource::Grok => "grok",
2985            SessionSource::Native => "native",
2986            SessionSource::OpenClaw => "openclaw",
2987            SessionSource::Hermes => "hermes",
2988            SessionSource::OpenCode => "opencode",
2989            SessionSource::Pi => "pi",
2990        },
2991        "session_id": session.meta.session_id,
2992        "ended_at": session.meta.ended_at,
2993        "end_reason": session.meta.end_reason,
2994        "model": session.meta.model,
2995        "cwd": session.meta.cwd,
2996        "system_prompt": session.meta.system_prompt,
2997        "agent_id": session.meta.agent_id,
2998        "parent_tool_use_id": session.meta.parent_tool_use_id,
2999        "lineage": session.meta.lineage,
3000        "messages": messages,
3001        "subagents": subagents,
3002        "raw_record_count": session.raw.len(),
3003        "parse_error_lines": session.parse_error_lines,
3004    })
3005}
3006
3007fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
3008    if let Some(tail) = options.message_tail {
3009        return (total.saturating_sub(tail), total);
3010    }
3011    let offset = options.message_offset.unwrap_or(0).min(total);
3012    let end = options
3013        .message_limit
3014        .map(|limit| offset.saturating_add(limit).min(total))
3015        .unwrap_or(total);
3016    (offset, end)
3017}
3018
3019fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
3020    let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
3021        return message;
3022    };
3023    for part in parts {
3024        let Some(url) = part
3025            .get("image_url")
3026            .and_then(|image| image.get("url"))
3027            .and_then(Value::as_str)
3028        else {
3029            continue;
3030        };
3031        let Some(rest) = url.strip_prefix("data:") else {
3032            continue;
3033        };
3034        let Some((media_type, encoded)) = rest.split_once(";base64,") else {
3035            continue;
3036        };
3037        let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
3038        let decoded_bytes = encoded.len().saturating_mul(3) / 4;
3039        let decoded_bytes = decoded_bytes.saturating_sub(padding);
3040        let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
3041            || options
3042                .max_inline_media_bytes
3043                .is_some_and(|limit| decoded_bytes > limit);
3044        if should_elide {
3045            *part = json!({
3046                "type": "media_reference",
3047                "media_type": media_type,
3048                "encoding": "base64",
3049                "encoded_bytes": encoded.len(),
3050                "decoded_bytes": decoded_bytes,
3051                "omitted": true,
3052            });
3053        }
3054    }
3055    message
3056}
3057
3058#[derive(Deserialize)]
3059struct LocatorParams {
3060    locator: SessionLocator,
3061    /// Optional fidelity for the READ surfaces (`sessions.load`,
3062    /// `sessions.follow`).
3063    ///
3064    /// Omitted means [`Fidelity::Semantic`]: these two methods only ever
3065    /// produce a read-only view, and a compacted or resumed-across-files
3066    /// transcript — the everyday shape of a long Claude Code session — has no
3067    /// losslessly reconstructable record graph, so refusing to render it made
3068    /// the mirror unusable rather than accurate. A caller that intends to
3069    /// CONTINUE from what it reads asks for a lossless level explicitly and
3070    /// gets the strict refusal back. Every other method (export, translate,
3071    /// branch, handoff, resume_instructions) is lossless-only and has no
3072    /// such knob.
3073    #[serde(default)]
3074    fidelity: Option<Fidelity>,
3075    /// Optional bounded frontend projection. Absent preserves the historical
3076    /// complete-session read contract.
3077    #[serde(default)]
3078    view: Option<SessionReadView>,
3079}
3080
3081#[derive(Deserialize)]
3082struct SessionReadView {
3083    /// Number of trailing normalized messages to return. Zero is treated as
3084    /// one so a caller cannot accidentally request an unbounded empty mode.
3085    #[serde(default)]
3086    tail_messages: Option<usize>,
3087    /// Whether Claude Code child transcripts belong in this view. The
3088    /// frontend default is false; the legacy no-view path remains true.
3089    #[serde(default)]
3090    include_subagents: bool,
3091    /// Preserve human-visible native history across model-context compaction.
3092    #[serde(default)]
3093    display_history: bool,
3094    /// Bound each individual text field so a single tool result cannot turn a
3095    /// small message window into a hundred-megabyte RPC response.
3096    #[serde(default)]
3097    max_message_chars: Option<usize>,
3098}
3099
3100impl LocatorParams {
3101    fn read_fidelity(&self) -> Fidelity {
3102        self.fidelity.unwrap_or(Fidelity::Semantic)
3103    }
3104
3105    fn include_subagents(&self) -> bool {
3106        self.view
3107            .as_ref()
3108            .map(|view| view.include_subagents)
3109            .unwrap_or(true)
3110    }
3111
3112    fn tail_messages(&self) -> Option<usize> {
3113        self.view
3114            .as_ref()
3115            .and_then(|view| view.tail_messages)
3116            .map(|limit| limit.clamp(1, 5_000))
3117    }
3118
3119    fn display_history(&self) -> bool {
3120        self.view.as_ref().is_some_and(|view| view.display_history)
3121    }
3122
3123    fn max_message_chars(&self) -> Option<usize> {
3124        self.view
3125            .as_ref()
3126            .and_then(|view| view.max_message_chars)
3127            .map(|limit| limit.clamp(256, 64_000))
3128    }
3129
3130    fn bound_session(&self, session: &mut Session) {
3131        bound_session_view(session, self.tail_messages(), self.max_message_chars());
3132    }
3133}
3134
3135#[derive(Debug, Clone, Copy, Default, Deserialize)]
3136#[serde(rename_all = "snake_case")]
3137enum InlineMediaMode {
3138    #[default]
3139    Full,
3140    Metadata,
3141}
3142
3143#[derive(Debug, Clone, Default, Deserialize)]
3144#[serde(default)]
3145struct SessionLoadOptions {
3146    include_subagents: Option<bool>,
3147    inline_media: InlineMediaMode,
3148    max_inline_media_bytes: Option<usize>,
3149    message_limit: Option<usize>,
3150    message_offset: Option<usize>,
3151    message_tail: Option<usize>,
3152}
3153
3154impl SessionLoadOptions {
3155    fn validate(&self) -> std::result::Result<(), ServiceError> {
3156        if self.message_tail.is_some()
3157            && (self.message_limit.is_some() || self.message_offset.is_some())
3158        {
3159            return Err(ServiceError::InvalidParams(
3160                "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
3161                    .into(),
3162            ));
3163        }
3164        Ok(())
3165    }
3166}
3167
3168#[derive(Deserialize)]
3169struct LoadSessionParams {
3170    #[serde(flatten)]
3171    read: LocatorParams,
3172    #[serde(default)]
3173    options: Option<SessionLoadOptions>,
3174}
3175
3176#[derive(Deserialize)]
3177struct UnfollowParams {
3178    subscription: String,
3179}
3180
3181#[derive(Debug, Deserialize)]
3182#[serde(deny_unknown_fields)]
3183struct IndexResizeParams {
3184    subscription: String,
3185    limit: usize,
3186}
3187
3188#[derive(Deserialize)]
3189struct ActivitySubscribeParams {
3190    locators: Vec<SessionLocator>,
3191    #[serde(default)]
3192    homes: crate::HarnessHomes,
3193}
3194
3195#[derive(Deserialize)]
3196struct MessageSessionParams {
3197    locator: SessionLocator,
3198    text: String,
3199    /// Same storage roots discovery accepts, so a caller (and a test) can
3200    /// point the live-session registry somewhere other than `$HOME`.
3201    #[serde(default)]
3202    homes: crate::HarnessHomes,
3203}
3204
3205#[derive(Deserialize)]
3206#[serde(deny_unknown_fields)]
3207struct HarnessSettingsParams {
3208    harness: String,
3209}
3210
3211#[derive(Deserialize)]
3212#[serde(deny_unknown_fields)]
3213struct ConfigureHarnessParams {
3214    harness: String,
3215    #[serde(default)]
3216    changes: Vec<crate::HarnessSettingChange>,
3217    #[serde(default)]
3218    expected_revision: Option<String>,
3219}
3220
3221fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3222    match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3223        Ok(report) => (
3224            serde_json::to_value(report).unwrap_or(Value::Null),
3225            Value::Null,
3226        ),
3227        Err(error) => (
3228            Value::Null,
3229            Value::String(format!(
3230                "Supercode could not inspect Claude Code inbound controls: {error}"
3231            )),
3232        ),
3233    }
3234}
3235
3236/// Deliver `text` into a session that is running right now, or say why not.
3237///
3238/// A refusal is a RESULT, not a JSON-RPC error: "that session is persisted
3239/// only" is an answer about the session, which a mirror renders next to the
3240/// transcript, and this service's error envelope carries no structured data
3241/// field a machine-readable reason could survive in.
3242///
3243/// `delivered_to_bus` is the honest ceiling of what the courier proves. The
3244/// message reached the receiving session's inbox; whether that session ever
3245/// reads it is governed by ITS OWN inbound controls (`crossSessionInbound`,
3246/// approval dialogs), which Supercode neither sees nor overrides.
3247async fn message_live_session(
3248    params: &MessageSessionParams,
3249    runner: &dyn crate::claude_peer::CourierRunner,
3250) -> Value {
3251    if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
3252        return json!({
3253            "delivered_to_bus": false,
3254            "refusal": {
3255                "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
3256                "message": format!(
3257                    "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
3258                    params.locator.harness.as_str()
3259                ),
3260            },
3261        });
3262    }
3263    let (inbound_controls, inbound_controls_error) =
3264        claude_inbound_controls_or_error(&params.homes);
3265    match crate::claude_peer::message_claude_peer(
3266        &params.homes,
3267        &params.locator.session_id,
3268        &params.text,
3269        runner,
3270    )
3271    .await
3272    {
3273        Ok(delivery) => json!({
3274            "delivered_to_bus": true,
3275            "target": {
3276                "session_id": delivery.target.session_id,
3277                "name": delivery.target.name,
3278                "pid": delivery.target.pid,
3279                "cwd": delivery.target.cwd,
3280                "status": delivery.target.status.map(|status| status.as_str()),
3281            },
3282            "courier": {
3283                "model": crate::claude_peer::COURIER_MODEL,
3284                "report": delivery.courier_report,
3285            },
3286            "inbound_controls": inbound_controls,
3287            "inbound_controls_error": inbound_controls_error,
3288        }),
3289        Err(refusal) => json!({
3290            "delivered_to_bus": false,
3291            "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
3292            "inbound_controls": inbound_controls,
3293            "inbound_controls_error": inbound_controls_error,
3294        }),
3295    }
3296}
3297
3298/// Source identity of one follow subscription, plus the last lifecycle state
3299/// already reported on it. The follower itself stays purely persistence-facing.
3300// Only the adapter-api poll reads these; the subscription bookkeeping itself is
3301// shared by both builds.
3302#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3303struct FollowedSource {
3304    harness: String,
3305    session_id: String,
3306    reported: Option<String>,
3307}
3308
3309#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3310struct ActivitySubscription {
3311    locators: Vec<SessionLocator>,
3312    homes: crate::HarnessHomes,
3313    reported: BTreeMap<(String, String), crate::SessionActivity>,
3314}
3315
3316fn peers_for_descriptors(
3317    descriptors: &[SessionDescriptor],
3318    homes: &HarnessHomes,
3319) -> Vec<crate::claude_peer::ClaudePeerSession> {
3320    if descriptors
3321        .iter()
3322        .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
3323    {
3324        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3325    } else {
3326        Vec::new()
3327    }
3328}
3329
3330/// Add the live address that makes an indexed row behaviorally equivalent to a discovered row.
3331///
3332/// The durable index owns only persistence metadata. Live endpoints remain projections: every
3333/// message/attach operation revalidates its authority, so publishing one here never trusts a stale
3334/// browser-held handle. Reading the Claude registry once per batch keeps this O(peers + rows).
3335fn live_descriptor_value(
3336    session: &SessionDescriptor,
3337    peers: &[crate::claude_peer::ClaudePeerSession],
3338) -> std::result::Result<Value, ServiceError> {
3339    let mut value = serde_json::to_value(session)
3340        .map_err(|error| ServiceError::Operation(error.to_string()))?;
3341    if let Some(workspace) = &session.cwd {
3342        let source = LiveRuntimeSource {
3343            harness: session.locator.harness.as_str().to_string(),
3344            session_id: session.locator.session_id.clone(),
3345            workspace: workspace.clone(),
3346        };
3347        if let Some(endpoint) = discover_live_runtime(&source)
3348            .map_err(|error| ServiceError::Operation(error.to_string()))?
3349        {
3350            value["live_endpoint"] = json!(endpoint.as_str());
3351        }
3352    }
3353    if value.get("live_endpoint").is_none() {
3354        if let Some(peer) = peers.iter().find(|peer| {
3355            session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3356                && peer.session_id == session.locator.session_id
3357        }) {
3358            value["live_endpoint"] = json!(peer.endpoint().as_str());
3359        }
3360    }
3361    Ok(value)
3362}
3363
3364fn live_index_changes(
3365    changes: Vec<crate::session_index::SessionIndexChange>,
3366    homes: &HarnessHomes,
3367) -> std::result::Result<Vec<Value>, ServiceError> {
3368    use crate::session_index::SessionIndexChange;
3369    let has_claude = changes.iter().any(|change| match change {
3370        SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3371            descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3372        }
3373        SessionIndexChange::Removed { .. } => false,
3374    });
3375    let peers = if has_claude {
3376        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3377    } else {
3378        Vec::new()
3379    };
3380    changes
3381        .into_iter()
3382        .map(|change| match change {
3383            SessionIndexChange::Added { descriptor } => Ok(json!({
3384                "kind": "added",
3385                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3386            })),
3387            SessionIndexChange::Updated { descriptor } => Ok(json!({
3388                "kind": "updated",
3389                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3390            })),
3391            SessionIndexChange::Removed { key } => Ok(json!({
3392                "kind": "removed",
3393                "key": key,
3394            })),
3395        })
3396        .collect()
3397}
3398
3399fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3400    use crate::{SessionPresence, SessionTurnState};
3401    match (activity.presence, activity.turn) {
3402        (SessionPresence::Persisted, _) => None,
3403        (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3404        (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3405        // The normalized activity object can honestly report a live owner even
3406        // when the stock harness never published a turn status. Preserve the
3407        // older field's stricter contract instead of guessing `running`.
3408        (SessionPresence::Running, SessionTurnState::Unknown)
3409            if activity.evidence.native_state.is_none() =>
3410        {
3411            None
3412        }
3413        (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3414    }
3415}
3416
3417#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3418#[serde(rename_all = "kebab-case")]
3419enum TransferFormat {
3420    ClaudeCode,
3421    Codex,
3422    #[serde(rename = "opencode", alias = "open-code")]
3423    OpenCode,
3424    Pi,
3425    Grok,
3426    Gemini,
3427    Goose,
3428    /// UNI-18: a Hermes target. Its artifact is the Codex rollout that
3429    /// `hermes sessions import --from codex` reads; `sessions.export` performs
3430    /// that import into the Hermes home.
3431    Hermes,
3432}
3433
3434impl TransferFormat {
3435    fn id(self) -> &'static str {
3436        match self {
3437            Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3438            Self::Codex => HarnessId::CODEX,
3439            Self::OpenCode => HarnessId::OPENCODE,
3440            Self::Pi => HarnessId::PI,
3441            Self::Grok => HarnessId::GROK,
3442            Self::Gemini => HarnessId::GEMINI,
3443            Self::Goose => HarnessId::GOOSE,
3444            Self::Hermes => HarnessId::HERMES,
3445        }
3446    }
3447}
3448
3449impl From<TransferFormat> for SessionFormat {
3450    fn from(value: TransferFormat) -> Self {
3451        match value {
3452            TransferFormat::ClaudeCode => Self::ClaudeCode,
3453            TransferFormat::Codex => Self::Codex,
3454            TransferFormat::OpenCode => Self::OpenCode,
3455            TransferFormat::Pi => Self::Pi,
3456            TransferFormat::Grok => Self::Grok,
3457            TransferFormat::Gemini => Self::Gemini,
3458            TransferFormat::Goose => Self::Goose,
3459            // a Hermes artifact is the Codex rollout Hermes imports
3460            TransferFormat::Hermes => Self::Codex,
3461        }
3462    }
3463}
3464
3465#[derive(Deserialize)]
3466struct ImportSessionParams {
3467    source_harness: TransferFormat,
3468    content: String,
3469}
3470
3471#[derive(Deserialize)]
3472struct ExportSessionParams {
3473    locator: SessionLocator,
3474    target_harness: TransferFormat,
3475}
3476
3477#[derive(Deserialize)]
3478struct ReduceSessionParams {
3479    locator: SessionLocator,
3480    target_harness: TransferFormat,
3481    #[serde(default = "default_keep_last")]
3482    keep_last: usize,
3483}
3484
3485fn default_keep_last() -> usize {
3486    6
3487}
3488
3489#[derive(Deserialize)]
3490struct BranchSessionParams {
3491    locator: SessionLocator,
3492    #[serde(default)]
3493    target_harness: Option<TransferFormat>,
3494}
3495
3496#[derive(Deserialize)]
3497struct HandoffSessionParams {
3498    locator: SessionLocator,
3499    target_harness: TransferFormat,
3500    #[serde(default)]
3501    cwd: Option<PathBuf>,
3502}
3503
3504#[derive(Deserialize)]
3505struct MaterializeSessionParams {
3506    artifact: crate::native_materialize::MaterializeArtifact,
3507    cwd: PathBuf,
3508    /// Where the continuation is written; unset roots are the environment's own, as discovery reads them.
3509    #[serde(default)]
3510    homes: HarnessHomes,
3511}
3512
3513#[derive(Debug, Clone, Copy, Default, Deserialize)]
3514#[serde(rename_all = "snake_case")]
3515enum ResumePolicy {
3516    #[default]
3517    Default,
3518    Yolo,
3519}
3520
3521#[derive(Deserialize)]
3522struct ResumeInstructionsParams {
3523    locator: SessionLocator,
3524    #[serde(default)]
3525    cwd: Option<PathBuf>,
3526    #[serde(default)]
3527    policy: ResumePolicy,
3528}
3529
3530/// `harness.v1.workflow.load` parameters: which harness's board, and its home.
3531#[derive(Deserialize)]
3532struct WorkflowLoadParams {
3533    from: crate::workflow_doors::WorkflowHarness,
3534    home: PathBuf,
3535}
3536
3537/// ONT-4 `harness.v1.orchestration.load` parameters. `flavor` says which layout the
3538/// folder is read as; our own is the default.
3539#[derive(Deserialize)]
3540struct OrchestrationLoadParams {
3541    root: PathBuf,
3542    #[serde(default)]
3543    flavor: crate::orchestration_doors::HomeFlavor,
3544}
3545
3546/// ONT-4 `harness.v1.orchestration.save` parameters. `vault` is merged into the
3547/// home's own secrets; a caller that sends none keeps what is on disk.
3548#[derive(Deserialize)]
3549struct OrchestrationSaveParams {
3550    root: PathBuf,
3551    orchestration: crate::orchestration::Orchestration,
3552    #[serde(default)]
3553    vault: BTreeMap<String, String>,
3554}
3555
3556/// ONT-4 `harness.v1.orchestration.compile` parameters.
3557#[derive(Deserialize)]
3558struct OrchestrationCompileParams {
3559    from: crate::orchestration_doors::OrchestrationHarness,
3560    home: PathBuf,
3561}
3562
3563/// ONT-4 `harness.v1.orchestration.decompile` parameters. `source` is the home the
3564/// orchestration was compiled from: it is re-compiled to recover the io bookkeeping
3565/// that byte reuse and the live-store refusal (UNI-18) are decided from.
3566#[derive(Deserialize)]
3567struct OrchestrationDecompileParams {
3568    to: crate::orchestration_doors::OrchestrationHarness,
3569    orchestration: crate::orchestration::Orchestration,
3570    source: PathBuf,
3571    #[serde(default)]
3572    source_flavor: crate::orchestration_doors::SourceFlavor,
3573    dest: PathBuf,
3574    #[serde(default)]
3575    vault: BTreeMap<String, String>,
3576}
3577
3578/// `harness.v1.orchestration.import` parameters: another harness's home, and the
3579/// folder of ours it becomes.
3580#[derive(Deserialize)]
3581struct OrchestrationImportParams {
3582    from: crate::orchestration_doors::OrchestrationHarness,
3583    home: PathBuf,
3584    into: PathBuf,
3585}
3586
3587/// `harness.v1.orchestration.export` parameters: a folder of ours, and the home of
3588/// another harness it becomes.
3589#[derive(Deserialize)]
3590struct OrchestrationExportParams {
3591    to: crate::orchestration_doors::OrchestrationHarness,
3592    root: PathBuf,
3593    dest: PathBuf,
3594}
3595
3596/// `harness.v1.jobs.get` parameters.
3597#[derive(Deserialize)]
3598struct JobsGetParams {
3599    harness: String,
3600    id: String,
3601    #[serde(default)]
3602    homes: crate::HarnessHomes,
3603}
3604
3605/// ORCH-18: run one mutating job verb through the harness's own CLI.
3606///
3607/// The refusal ladder is deliberate: a harness with no scheduled-job concept
3608/// at all answers with the SAME sentence `jobs.list` gives it, and a harness
3609/// that has jobs but publishes no client-callable verb (Claude Code, whose
3610/// jobs are created by the model inside a session) answers with its own
3611/// reason. Neither is ever a silent no-op.
3612fn mutate_job(
3613    verb: crate::jobs_control::JobVerb,
3614    params: Value,
3615) -> std::result::Result<Value, ServiceError> {
3616    let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3617    refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3618    let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3619    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3620}
3621
3622/// ORCH-22: run one mutating skills verb through the harness's own door.
3623///
3624/// The refusal ladder mirrors `jobs.*`: a harness with no skills root at all
3625/// answers with the same sentence `skills.list` gives it, and a harness whose
3626/// door does not publish this verb (OpenClaw has no `skills remove` at the
3627/// pin) answers with its own reason. Neither is ever a silent no-op.
3628fn mutate_skill(
3629    verb: crate::skills_control::SkillVerb,
3630    params: Value,
3631) -> std::result::Result<Value, ServiceError> {
3632    let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3633    if !crate::skills_control::supports_skill_control(&mutation.harness) {
3634        return Err(ServiceError::UnsupportedAction(format!(
3635            "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3636            mutation.harness,
3637            verb.as_str(),
3638            crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3639        )));
3640    }
3641    let outcome =
3642        crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3643    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3644}
3645
3646/// The skills twin of [`job_control_error`], with the same mapping rule.
3647fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3648    match error {
3649        crate::skills_control::SkillControlError::Unsupported(message) => {
3650            ServiceError::UnsupportedAction(message)
3651        }
3652        crate::skills_control::SkillControlError::Invalid(message) => {
3653            ServiceError::InvalidParams(message)
3654        }
3655        crate::skills_control::SkillControlError::Failed(message) => {
3656            ServiceError::Operation(message)
3657        }
3658    }
3659}
3660
3661/// ORCH-21: run one mutating profile verb through the harness's own CLI.
3662///
3663/// The refusal ladder mirrors `mutate_job`'s: a harness with no profile
3664/// concept at all answers with the SAME sentence `profiles.list` gives it, and
3665/// a harness that HAS profiles but publishes no client-callable verb (Codex's
3666/// file-authored `[profiles.<name>]` tables, supercode's compiled-in presets)
3667/// answers with its own reason. Neither is ever a silent no-op.
3668fn mutate_profile(
3669    verb: crate::profiles_control::ProfileVerb,
3670    params: Value,
3671) -> std::result::Result<Value, ServiceError> {
3672    let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3673    let outcome =
3674        crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3675    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3676}
3677
3678/// The same mapping `job_control_error` applies, for the profile noun.
3679fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3680    match error {
3681        crate::profiles_control::ProfileControlError::Unsupported(message) => {
3682            ServiceError::UnsupportedAction(message)
3683        }
3684        crate::profiles_control::ProfileControlError::Invalid(message) => {
3685            ServiceError::InvalidParams(message)
3686        }
3687        crate::profiles_control::ProfileControlError::Failed(message) => {
3688            ServiceError::Operation(message)
3689        }
3690    }
3691}
3692
3693/// Map a controlled-tier failure onto the service's error vocabulary. A verb
3694/// the harness lacks is `UnsupportedAction`; a harness verb that RAN and
3695/// failed carries its own stderr through as the operation error.
3696fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3697    match error {
3698        crate::jobs_control::JobControlError::Unsupported(message) => {
3699            ServiceError::UnsupportedAction(message)
3700        }
3701        crate::jobs_control::JobControlError::Invalid(message) => {
3702            ServiceError::InvalidParams(message)
3703        }
3704        crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3705    }
3706}
3707
3708/// Map an ORCH-19 controlled-tier failure onto the service's error
3709/// vocabulary. A verb the harness has no door for is `UnsupportedAction`; a
3710/// door that RAN and failed carries the harness's own stderr / HTTP body
3711/// through as the operation error.
3712fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3713    match error {
3714        crate::SessionControlError::Unsupported(message) => {
3715            ServiceError::UnsupportedAction(message)
3716        }
3717        crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3718        crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3719    }
3720}
3721
3722/// A harness without a scheduled-job concept refuses the verb rather than
3723/// answering with an empty list — an absent capability and an empty inventory
3724/// are different answers (the same rule `runtimes.capabilities` applies to
3725/// `steer`).
3726fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3727    if crate::jobs::supports_jobs(harness) {
3728        return Ok(());
3729    }
3730    Err(ServiceError::UnsupportedAction(format!(
3731        "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3732        crate::jobs::JOB_HARNESSES.join(", ")
3733    )))
3734}
3735
3736/// `harness.v1.runs.get` parameters.
3737#[derive(Deserialize)]
3738struct RunsGetParams {
3739    harness: String,
3740    id: String,
3741    #[serde(default)]
3742    homes: crate::HarnessHomes,
3743}
3744
3745/// A harness with no run store refuses the verb rather than answering with an
3746/// empty history — the same rule `jobs.list` applies. Claude Code lands here
3747/// on purpose: its cron fires are ordinary turns inside the session that
3748/// created the job, so there is no fire record to list.
3749fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3750    if crate::runs::supports_runs(harness) {
3751        return Ok(());
3752    }
3753    Err(ServiceError::UnsupportedAction(format!(
3754        "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3755        crate::runs::RUN_HARNESSES.join(", ")
3756    )))
3757}
3758
3759#[derive(Serialize)]
3760struct SessionArtifact {
3761    source_harness: HarnessId,
3762    target_harness: &'static str,
3763    session_id: Option<String>,
3764    content: String,
3765    suggested_filename: String,
3766    files: Vec<SessionArtifactFile>,
3767    fidelity: Fidelity,
3768    residue: Vec<String>,
3769}
3770
3771#[derive(Serialize)]
3772struct SessionArtifactFile {
3773    path: String,
3774    content: String,
3775    role: ArtifactFileRole,
3776}
3777
3778#[derive(Serialize)]
3779#[serde(rename_all = "snake_case")]
3780enum ArtifactFileRole {
3781    Primary,
3782    Subagent,
3783    Bundle,
3784    SourceRecovery,
3785}
3786
3787#[derive(Serialize)]
3788struct StructuredLaunch {
3789    cwd: PathBuf,
3790    program: String,
3791    arguments: Vec<String>,
3792    env: BTreeMap<String, String>,
3793}
3794
3795struct HandoffInstructions {
3796    launch: StructuredLaunch,
3797    materialize: Option<StructuredLaunch>,
3798    requires_materialization: bool,
3799    note: String,
3800}
3801
3802#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3803#[serde(rename_all = "snake_case")]
3804enum HarnessProbeLevel {
3805    #[default]
3806    Passive,
3807    Handshake,
3808}
3809
3810#[derive(Default, Deserialize)]
3811#[serde(default)]
3812struct HarnessInventoryParams {
3813    harness: Option<HarnessId>,
3814    harnesses: Vec<HarnessId>,
3815    workspace: Option<PathBuf>,
3816    probe: HarnessProbeLevel,
3817    include_sessions: bool,
3818    /// Omit subprocess-based `--version` calls when a latency-sensitive UI only needs readiness.
3819    skip_versions: bool,
3820}
3821
3822#[derive(Deserialize)]
3823struct HarnessAuthenticationParams {
3824    harness: HarnessId,
3825}
3826
3827#[derive(Deserialize)]
3828struct BeginHarnessAuthenticationParams {
3829    harness: HarnessId,
3830    #[serde(default = "local_browser_authentication_environment")]
3831    environment: crate::HarnessAuthenticationEnvironment,
3832    #[serde(default)]
3833    method: Option<crate::HarnessAuthenticationMethodId>,
3834    #[serde(default)]
3835    cwd: Option<PathBuf>,
3836}
3837
3838fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3839    crate::HarnessAuthenticationEnvironment::LocalBrowser
3840}
3841
3842#[derive(Serialize)]
3843struct HarnessInventoryReport {
3844    probe: HarnessProbeLevel,
3845    workspace: Option<PathBuf>,
3846    harnesses: Vec<LocalHarness>,
3847}
3848
3849#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3850#[serde(rename_all = "snake_case")]
3851enum HarnessAuthState {
3852    Ready,
3853    Configured,
3854    Required,
3855    Unknown,
3856}
3857
3858#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3859#[serde(rename_all = "snake_case")]
3860enum HarnessRuntimeState {
3861    Ready,
3862    Degraded,
3863    Unavailable,
3864}
3865
3866#[derive(Serialize)]
3867struct HarnessSessionCounts {
3868    global: Option<usize>,
3869    workspace: Option<usize>,
3870}
3871
3872/// Receipt-backed evidence that a harness has a RUNNING instance right now,
3873/// distinct from being merely installed (UNI-7). Detection is passive and
3874/// default-on: a gateway liveness connect for daemon harnesses, a fresh
3875/// SQLite WAL stamp for store-writer harnesses (precedent: the opencode
3876/// follower's -wal/-shm freshness). Control stays behind per-connection
3877/// grants — this reports observations only.
3878/// ORCH-17: the gateway-health noun on an inventory row. Derived from the
3879/// UNI-7 running-instance probe (Hermes: `state.db-wal` freshness; OpenClaw:
3880/// a TCP connect to the gateway endpoint resolved from its OWN config) plus
3881/// the executable version — never by starting anything.
3882#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3883#[serde(rename_all = "snake_case")]
3884pub enum GatewayState {
3885    Up,
3886    Down,
3887    Unknown,
3888}
3889
3890/// ORCH-17: `gateway` on a `harness.v1.harnesses.list` row.
3891#[derive(Debug, Clone, Serialize)]
3892pub struct GatewayHealth {
3893    pub state: GatewayState,
3894    /// The endpoint supercode would connect to (OpenClaw: the gateway
3895    /// WebSocket resolved from `openclaw.json`; core harnesses: their
3896    /// declared connect address when one exists). `None` when the harness
3897    /// has no single endpoint (Hermes multiplexes platforms).
3898    #[serde(skip_serializing_if = "Option::is_none")]
3899    pub endpoint: Option<String>,
3900    #[serde(skip_serializing_if = "Option::is_none")]
3901    pub version: Option<String>,
3902    /// What the verdict rests on, or why it is `unknown`.
3903    pub evidence: String,
3904    pub checked_at_ms: u64,
3905}
3906
3907/// OpenClaw's gateway WebSocket endpoint, resolved from its own config the
3908/// way the registry's connect descriptor prescribes (`gateway.url`, else
3909/// `gateway.port`, else the documented default).
3910fn openclaw_gateway_endpoint(home: &Path) -> String {
3911    let config_path = home.join(".openclaw/openclaw.json");
3912    let gateway = std::fs::read_to_string(&config_path)
3913        .ok()
3914        .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3915        .and_then(|config| config.get("gateway").cloned());
3916    if let Some(url) = gateway
3917        .as_ref()
3918        .and_then(|gateway| gateway.get("url"))
3919        .and_then(serde_json::Value::as_str)
3920    {
3921        return url.to_string();
3922    }
3923    let port = gateway
3924        .as_ref()
3925        .and_then(|gateway| gateway.get("port"))
3926        .and_then(serde_json::Value::as_u64)
3927        .unwrap_or(18789);
3928    format!("ws://127.0.0.1:{port}")
3929}
3930
3931/// Ask Hermes itself (`hermes gateway status`, read-only, ~1 s) whether its
3932/// gateway is up. The command is per-host launchd/systemd text without a JSON
3933/// form at 0.19–0.21; the verdict is read from the lines it prints:
3934/// "supervised by launchd (PID …)" / "is running" → up, "not running" /
3935/// "not installed" → down, anything else → no verdict. `SUPERCODE_HERMES_BIN`
3936/// overrides the executable so a fake can stand in under test.
3937fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3938    let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3939    let output = std::process::Command::new(&program)
3940        .args(["gateway", "status"])
3941        .stdin(std::process::Stdio::null())
3942        .output()
3943        .ok()?;
3944    let text = format!(
3945        "{}{}",
3946        String::from_utf8_lossy(&output.stdout),
3947        String::from_utf8_lossy(&output.stderr)
3948    );
3949    let verdict = text.lines().find_map(|line| {
3950        let l = line.trim();
3951        if l.contains("supervised by launchd (PID")
3952            || l.contains("supervised by systemd (PID")
3953            || l.contains("Gateway is running")
3954            || l.contains("process is running")
3955        {
3956            Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3957        } else if l.contains("not running") || l.contains("not installed") {
3958            Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3959        } else {
3960            None
3961        }
3962    });
3963    verdict
3964}
3965
3966fn gateway_health(
3967    id: &str,
3968    installed: bool,
3969    running: Option<&RunningInstance>,
3970    version: Option<&str>,
3971) -> GatewayHealth {
3972    let checked_at_ms = now_epoch_ms();
3973    let home = std::env::var_os("HOME").map(PathBuf::from);
3974    match id {
3975        HarnessId::HERMES | HarnessId::OPENCLAW => {
3976            let endpoint = (id == HarnessId::OPENCLAW)
3977                .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3978                .flatten();
3979            let (state, evidence) = match running {
3980                Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3981                None if !installed => (
3982                    GatewayState::Unknown,
3983                    format!("`{id}` is not installed; no gateway to probe"),
3984                ),
3985                None if id == HarnessId::HERMES => match hermes_gateway_status() {
3986                    // The harness's own door outranks the WAL heuristic: an idle
3987                    // gateway writes nothing for minutes yet is up.
3988                    Some((state, evidence)) => (state, evidence),
3989                    None => (
3990                        GatewayState::Down,
3991                        "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
3992                    ),
3993                },
3994                None => (
3995                    GatewayState::Down,
3996                    format!(
3997                        "no TCP listener at {}",
3998                        endpoint.as_deref().unwrap_or("the gateway endpoint")
3999                    ),
4000                ),
4001            };
4002            GatewayHealth {
4003                state,
4004                endpoint,
4005                version: version.map(str::to_string),
4006                evidence,
4007                checked_at_ms,
4008            }
4009        }
4010        // ORC-7: the orchestrator's gateway IS its daemon, and the daemon's
4011        // own lease file is the record of it. A lease naming a live pid is
4012        // up; a lease whose process is gone is down and says so as a STALE
4013        // lease, never as "no lease"; no lease at all is down. Nothing is
4014        // started, and no port is guessed — the daemon multiplexes adapters
4015        // the way Hermes does, so it has no single endpoint either.
4016        HarnessId::ORCHESTRATOR => {
4017            let root = crate::HarnessHomes::default().orchestrator;
4018            let (state, evidence) = match crate::orchestrator::read_lease(&root) {
4019                Some(lease) if lease.is_live() => (
4020                    GatewayState::Up,
4021                    format!(
4022                        "`{}` names pid {} (started {}), which is live",
4023                        crate::orchestrator::lock_path(&root).display(),
4024                        lease.pid,
4025                        lease.started_at
4026                    ),
4027                ),
4028                Some(lease) => (
4029                    GatewayState::Down,
4030                    format!(
4031                        "stale lease `{}`: pid {} is gone",
4032                        crate::orchestrator::lock_path(&root).display(),
4033                        lease.pid
4034                    ),
4035                ),
4036                None => (
4037                    GatewayState::Down,
4038                    format!(
4039                        "no lease at `{}`; `supercode orchestrator start` writes one",
4040                        crate::orchestrator::lock_path(&root).display()
4041                    ),
4042                ),
4043            };
4044            GatewayHealth {
4045                state,
4046                endpoint: None,
4047                version: version.map(str::to_string),
4048                evidence,
4049                checked_at_ms,
4050            }
4051        }
4052        _ => GatewayHealth {
4053            state: GatewayState::Unknown,
4054            endpoint: None,
4055            version: version.map(str::to_string),
4056            evidence: format!("`{id}` runs per session, not as a gateway"),
4057            checked_at_ms,
4058        },
4059    }
4060}
4061
4062#[derive(Debug, Clone, Serialize)]
4063struct RunningInstance {
4064    /// How the instance was detected.
4065    method: RunningInstanceMethod,
4066    /// The evidence the verdict rests on (endpoint reached / WAL path+age).
4067    evidence: String,
4068    /// Epoch-ms instant the probe executed.
4069    checked_at_ms: u64,
4070}
4071
4072#[derive(Debug, Clone, Copy, Serialize)]
4073#[serde(rename_all = "snake_case")]
4074enum RunningInstanceMethod {
4075    /// A TCP connect to the harness's own configured gateway endpoint
4076    /// succeeded.
4077    GatewayConnect,
4078    /// The harness's session store has an active SQLite WAL (a live writer
4079    /// holds the store open and stamped it recently).
4080    StoreWalActivity,
4081}
4082
4083fn now_epoch_ms() -> u64 {
4084    std::time::SystemTime::now()
4085        .duration_since(std::time::UNIX_EPOCH)
4086        .map(|elapsed| elapsed.as_millis() as u64)
4087        .unwrap_or(0)
4088}
4089
4090/// OpenClaw: the gateway endpoint comes from the harness's OWN config
4091/// (`<home>/.openclaw/openclaw.json` — `gateway.url` or `gateway.port`,
4092/// default port 18789); a successful TCP connect is the running signal.
4093fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4094    let config_path = home.join(".openclaw/openclaw.json");
4095    let text = std::fs::read_to_string(&config_path).ok();
4096    let gateway = text
4097        .as_deref()
4098        .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4099        .and_then(|config| config.get("gateway").cloned());
4100    let address = gateway
4101        .as_ref()
4102        .and_then(|gateway| gateway.get("url"))
4103        .and_then(serde_json::Value::as_str)
4104        .and_then(|url| {
4105            url.split("://").nth(1).map(|rest| {
4106                rest.trim_end_matches('/')
4107                    .split('/')
4108                    .next()
4109                    .unwrap_or(rest)
4110                    .to_string()
4111            })
4112        })
4113        .unwrap_or_else(|| {
4114            let port = gateway
4115                .as_ref()
4116                .and_then(|gateway| gateway.get("port"))
4117                .and_then(serde_json::Value::as_u64)
4118                .unwrap_or(18789);
4119            format!("127.0.0.1:{port}")
4120        });
4121    let reachable = std::net::TcpStream::connect_timeout(
4122        &address.parse().ok()?,
4123        std::time::Duration::from_millis(400),
4124    )
4125    .is_ok();
4126    reachable.then(|| RunningInstance {
4127        method: RunningInstanceMethod::GatewayConnect,
4128        evidence: format!(
4129            "gateway endpoint {address} accepted a TCP connect (from {})",
4130            config_path.display()
4131        ),
4132        checked_at_ms: now_epoch_ms(),
4133    })
4134}
4135
4136/// Hermes: `<home>/.hermes/state.db-wal` freshly modified means a live writer
4137/// holds the store open (SQLite WAL exists only while a connection is open;
4138/// a recent stamp distinguishes an active instance from a stale crash
4139/// leftover).
4140fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4141    let wal = home.join(".hermes/state.db-wal");
4142    let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4143    let age_ms = std::time::SystemTime::now()
4144        .duration_since(modified)
4145        .map(|age| age.as_millis() as u64)
4146        .unwrap_or(u64::MAX);
4147    (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4148        method: RunningInstanceMethod::StoreWalActivity,
4149        evidence: format!(
4150            "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4151            wal.display()
4152        ),
4153        checked_at_ms: now_epoch_ms(),
4154    })
4155}
4156
4157/// Default-on running-instance detection for the harnesses that have one.
4158fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4159    let home = std::env::var_os("HOME").map(PathBuf::from)?;
4160    match id {
4161        HarnessId::OPENCLAW => probe_openclaw_running(&home),
4162        HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4163        _ => None,
4164    }
4165}
4166
4167#[derive(Serialize)]
4168struct LocalHarness {
4169    id: HarnessId,
4170    display_name: String,
4171    supported: bool,
4172    installed: bool,
4173    executable: Option<String>,
4174    version: Option<String>,
4175    auth: HarnessAuthState,
4176    runtime: HarnessRuntimeState,
4177    protocol: String,
4178    capabilities: crate::RuntimeCapabilities,
4179    effective_capabilities: crate::RuntimeCapabilities,
4180    sessions: HarnessSessionCounts,
4181    /// Receipt-backed running-instance detection (None = not detected or the
4182    /// harness has no running-instance concept). Distinct from `installed`.
4183    #[serde(skip_serializing_if = "Option::is_none")]
4184    running: Option<RunningInstance>,
4185    /// ORCH-17: gateway health derived from `running` + the harness's own config.
4186    gateway: GatewayHealth,
4187    reason: Option<String>,
4188    repair: Option<String>,
4189}
4190
4191#[derive(Clone, Deserialize)]
4192struct RuntimeBackendParams {
4193    harness: HarnessId,
4194    #[serde(default)]
4195    protocol: Option<String>,
4196    #[serde(default)]
4197    launch: Option<RuntimeLaunch>,
4198    #[serde(default)]
4199    base_url: Option<String>,
4200    #[serde(default)]
4201    policy: RuntimePolicy,
4202}
4203
4204#[derive(Debug, Clone, Copy, Default, Deserialize)]
4205#[serde(rename_all = "snake_case")]
4206enum RuntimePolicy {
4207    #[default]
4208    Default,
4209    Yolo,
4210}
4211
4212#[derive(Deserialize)]
4213struct RuntimeStartParams {
4214    #[serde(flatten)]
4215    backend: RuntimeBackendParams,
4216    cwd: PathBuf,
4217    /// MCP servers to mount into the new session through the harness's own
4218    /// start door (ORC-6). Backends without such a door ignore them.
4219    #[serde(default)]
4220    mcp_servers: Vec<crate::McpServerLaunch>,
4221}
4222
4223#[derive(Deserialize)]
4224struct RuntimeAttachParams {
4225    #[serde(flatten)]
4226    backend: RuntimeBackendParams,
4227    runtime_id: String,
4228    #[serde(default)]
4229    cwd: Option<PathBuf>,
4230    /// MCP servers to mount into the resumed session (the start door's own
4231    /// field, carried again because a session's tools die with its process).
4232    #[serde(default)]
4233    mcp_servers: Vec<crate::McpServerLaunch>,
4234}
4235
4236#[derive(Deserialize)]
4237struct RuntimeConnectionParams {
4238    connection: String,
4239}
4240
4241#[derive(Deserialize)]
4242struct RuntimeInputParams {
4243    connection: String,
4244    text: String,
4245    #[serde(default)]
4246    image_urls: Vec<String>,
4247}
4248
4249const MAX_RUNTIME_IMAGES: usize = 4;
4250const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4251const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4252
4253fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4254    if image_urls.len() > MAX_RUNTIME_IMAGES {
4255        return Err(ServiceError::InvalidParams(format!(
4256            "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4257        )));
4258    }
4259    let mut total = 0usize;
4260    for url in &image_urls {
4261        if !(url.starts_with("data:image/")
4262            || url.starts_with("https://")
4263            || url.starts_with("http://"))
4264        {
4265            return Err(ServiceError::InvalidParams(
4266                "runtime images must be image data URLs or HTTP(S) URLs".into(),
4267            ));
4268        }
4269        if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4270            return Err(ServiceError::InvalidParams(format!(
4271                "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4272            )));
4273        }
4274        total = total.saturating_add(url.len());
4275    }
4276    if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4277        return Err(ServiceError::InvalidParams(format!(
4278            "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4279        )));
4280    }
4281    Ok(image_urls)
4282}
4283
4284#[derive(Deserialize)]
4285struct RuntimeRespondParams {
4286    connection: String,
4287    request_id: Value,
4288    response: Value,
4289}
4290
4291fn default_reduction_store_root() -> PathBuf {
4292    if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4293        return PathBuf::from(root).join("sessions");
4294    }
4295    if let Some(home) = std::env::var_os("HOME") {
4296        return PathBuf::from(home).join(".supercode").join("sessions");
4297    }
4298    PathBuf::from(".supercode").join("sessions")
4299}
4300
4301fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4302    let mut output = String::new();
4303    for message in messages {
4304        output.push_str(
4305            &serde_json::to_string(message)
4306                .map_err(|error| ServiceError::Operation(error.to_string()))?,
4307        );
4308        output.push('\n');
4309    }
4310    Ok(output)
4311}
4312
4313fn parse_messages_jsonl(
4314    content: &str,
4315) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4316    content
4317        .lines()
4318        .enumerate()
4319        .filter(|(_, line)| !line.trim().is_empty())
4320        .map(|(index, line)| {
4321            serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4322                ServiceError::Operation(format!(
4323                    "reduced transcript line {} is invalid: {error}",
4324                    index + 1
4325                ))
4326            })
4327        })
4328        .collect()
4329}
4330
4331fn reduced_bootstrap_prompt(
4332    source: &SessionLocator,
4333    target: TransferFormat,
4334    view_jsonl: &str,
4335    sidecar_path: &Path,
4336    reduction_log_path: &Path,
4337) -> String {
4338    format!(
4339        "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4340         \n\
4341         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\
4342         \n\
4343         <supercode-reduced-session source-session=\"{source_id}\">\n\
4344         {view_jsonl}\
4345         </supercode-reduced-session>\n\
4346         \n\
4347         Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4348        source_harness = source.harness.as_str(),
4349        target_harness = target.id(),
4350        sidecar = sidecar_path.display(),
4351        log = reduction_log_path.display(),
4352        source_id = source.session_id,
4353    )
4354}
4355
4356fn session_artifact(
4357    locator: &SessionLocator,
4358    session: &Session,
4359    target: TransferFormat,
4360) -> std::result::Result<SessionArtifact, ServiceError> {
4361    session_artifact_with_id(locator, session, target, None)
4362}
4363
4364fn session_artifact_with_id(
4365    locator: &SessionLocator,
4366    session: &Session,
4367    target: TransferFormat,
4368    target_session_id: Option<&str>,
4369) -> std::result::Result<SessionArtifact, ServiceError> {
4370    let format: SessionFormat = target.into();
4371    let diagonal = format.source() == session.meta.source;
4372    let has_appended_turns = session
4373        .imported_message_count
4374        .is_some_and(|imported| imported < session.messages.len());
4375    let content = if let Some(id) = target_session_id {
4376        if diagonal && format != SessionFormat::OpenCode {
4377            session
4378                .to_jsonl_spliced(format, Some(id))
4379                .map_err(operation)?
4380        } else {
4381            let mut rewritten = session.clone();
4382            rewritten.meta.session_id = Some(id.to_string());
4383            rewritten.to_jsonl(format).map_err(operation)?
4384        }
4385    } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4386        session.raw_verbatim()
4387    } else if diagonal {
4388        session.to_jsonl_spliced(format, None).map_err(operation)?
4389    } else {
4390        session.to_jsonl(format).map_err(operation)?
4391    };
4392    let stem = sanitize_filename(
4393        target_session_id
4394            .or(session.meta.session_id.as_deref())
4395            .unwrap_or(&locator.session_id),
4396    );
4397    let suggested_filename = if diagonal && target == TransferFormat::Grok {
4398        "chat_history.jsonl".to_string()
4399    } else if target == TransferFormat::Goose {
4400        format!("{stem}.goose.json")
4401    } else {
4402        format!("{stem}.{}.jsonl", target.id())
4403    };
4404    let mut files = vec![SessionArtifactFile {
4405        path: suggested_filename.clone(),
4406        content: content.clone(),
4407        role: ArtifactFileRole::Primary,
4408    }];
4409    if target == TransferFormat::ClaudeCode {
4410        let bundle_stem = Path::new(&suggested_filename)
4411            .file_stem()
4412            .and_then(|stem| stem.to_str())
4413            .unwrap_or(&stem);
4414        let mut child_paths = BTreeSet::new();
4415        for (index, subagent) in session.subagents.iter().enumerate() {
4416            let agent_id = subagent
4417                .meta
4418                .agent_id
4419                .as_deref()
4420                .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4421                .map(sanitize_filename)
4422                .filter(|id| !id.is_empty())
4423                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4424            let child_has_appended_turns = subagent
4425                .imported_message_count
4426                .is_some_and(|imported| imported < subagent.messages.len());
4427            let child_content = if target_session_id.is_none()
4428                && subagent.meta.source == SessionSource::ClaudeCode
4429                && subagent.raw_is_verbatim
4430                && !child_has_appended_turns
4431            {
4432                subagent.raw_verbatim()
4433            } else if subagent.meta.source == SessionSource::ClaudeCode {
4434                subagent
4435                    .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4436                    .map_err(operation)?
4437            } else {
4438                let mut child = subagent.clone();
4439                if let Some(id) = target_session_id {
4440                    child.meta.session_id = Some(id.to_string());
4441                }
4442                child
4443                    .to_jsonl(SessionFormat::ClaudeCode)
4444                    .map_err(operation)?
4445            };
4446            let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4447            if !child_paths.insert(path.clone()) {
4448                return Err(ServiceError::Operation(format!(
4449                    "Claude subagent ids collide at artifact path `{path}`"
4450                )));
4451            }
4452            files.push(SessionArtifactFile {
4453                path,
4454                content: child_content,
4455                role: ArtifactFileRole::Subagent,
4456            });
4457        }
4458    }
4459    if diagonal && target == TransferFormat::Grok {
4460        append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4461    }
4462    if !diagonal || !session.raw_is_verbatim {
4463        files.push(SessionArtifactFile {
4464            path: "recovery/source.supercode.jsonl".into(),
4465            content: session.to_native_jsonl(),
4466            role: ArtifactFileRole::SourceRecovery,
4467        });
4468        for (index, subagent) in session.subagents.iter().enumerate() {
4469            let id = subagent
4470                .meta
4471                .agent_id
4472                .as_deref()
4473                .map(sanitize_filename)
4474                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4475            files.push(SessionArtifactFile {
4476                path: format!("recovery/subagents/{id}.supercode.jsonl"),
4477                content: subagent.to_native_jsonl(),
4478                role: ArtifactFileRole::SourceRecovery,
4479            });
4480        }
4481    }
4482    if !diagonal && session.meta.source == SessionSource::Grok {
4483        append_grok_bundle_files(
4484            locator,
4485            "recovery/grok/",
4486            ArtifactFileRole::SourceRecovery,
4487            &mut files,
4488        )?;
4489    }
4490    let (fidelity, residue) = if diagonal
4491        && target_session_id.is_none()
4492        && session.raw_is_verbatim
4493        && !has_appended_turns
4494    {
4495        (Fidelity::ByteLossless, Vec::new())
4496    } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4497        (
4498            Fidelity::ValueLossless,
4499            vec![if target_session_id.is_some() {
4500                "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4501            } else {
4502                "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4503            }],
4504        )
4505    } else {
4506        (
4507            Fidelity::Semantic,
4508            vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4509        )
4510    };
4511    Ok(SessionArtifact {
4512        source_harness: locator.harness.clone(),
4513        target_harness: target.id(),
4514        session_id: target_session_id
4515            .map(str::to_string)
4516            .or_else(|| session.meta.session_id.clone()),
4517        content,
4518        suggested_filename,
4519        files,
4520        fidelity,
4521        residue,
4522    })
4523}
4524
4525fn append_grok_bundle_files(
4526    locator: &SessionLocator,
4527    prefix: &str,
4528    role: ArtifactFileRole,
4529    files: &mut Vec<SessionArtifactFile>,
4530) -> std::result::Result<(), ServiceError> {
4531    let primary = locator.storage.path();
4532    if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4533        return Err(ServiceError::Operation(format!(
4534            "Grok bundle locator must name chat_history.jsonl, got {}",
4535            primary.display()
4536        )));
4537    }
4538    let parent = primary.parent().ok_or_else(|| {
4539        ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4540    })?;
4541    for name in ["summary.json", "updates.jsonl"] {
4542        let path = parent.join(name);
4543        let metadata = match std::fs::symlink_metadata(&path) {
4544            Ok(metadata) => metadata,
4545            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4546            Err(error) => return Err(ServiceError::Operation(error.to_string())),
4547        };
4548        if metadata.file_type().is_symlink() || !metadata.is_file() {
4549            return Err(ServiceError::Operation(format!(
4550                "refusing non-regular Grok bundle member {}",
4551                path.display()
4552            )));
4553        }
4554        let content = std::fs::read_to_string(&path).map_err(|error| {
4555            ServiceError::Operation(format!(
4556                "Grok bundle member {} is not representable as UTF-8: {error}",
4557                path.display()
4558            ))
4559        })?;
4560        files.push(SessionArtifactFile {
4561            path: format!("{prefix}{name}"),
4562            content,
4563            role: match role {
4564                ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4565                _ => ArtifactFileRole::SourceRecovery,
4566            },
4567        });
4568    }
4569    Ok(())
4570}
4571
4572fn handoff_artifact(
4573    locator: &SessionLocator,
4574    session: &Session,
4575    target: TransferFormat,
4576) -> std::result::Result<SessionArtifact, ServiceError> {
4577    let target_session_id = target_session_id(target);
4578    session_artifact_with_id(locator, session, target, Some(&target_session_id))
4579}
4580
4581fn target_session_id(target: TransferFormat) -> String {
4582    let uuid = generated_session_id();
4583    match target {
4584        TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4585        TransferFormat::ClaudeCode
4586        | TransferFormat::Codex
4587        | TransferFormat::Pi
4588        | TransferFormat::Grok
4589        | TransferFormat::Gemini
4590        | TransferFormat::Goose
4591        | TransferFormat::Hermes => uuid,
4592    }
4593}
4594
4595fn sanitize_filename(value: &str) -> String {
4596    let value = value
4597        .chars()
4598        .map(|character| {
4599            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4600                character
4601            } else {
4602                '-'
4603            }
4604        })
4605        .collect::<String>();
4606    let value = value.trim_matches('-');
4607    if value.is_empty() {
4608        "session".into()
4609    } else {
4610        value.chars().take(100).collect()
4611    }
4612}
4613
4614fn handoff_instructions(
4615    target: TransferFormat,
4616    session_id: &str,
4617    cwd: &Path,
4618) -> HandoffInstructions {
4619    let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4620        cwd: cwd.to_path_buf(),
4621        program: program.into(),
4622        arguments,
4623        env: BTreeMap::new(),
4624    };
4625    match target {
4626        TransferFormat::ClaudeCode => HandoffInstructions {
4627            launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4628            materialize: None,
4629            requires_materialization: true,
4630            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(),
4631        },
4632        TransferFormat::Hermes => HandoffInstructions {
4633            launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4634            materialize: None,
4635            requires_materialization: true,
4636            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(),
4637        },
4638        TransferFormat::Codex => HandoffInstructions {
4639            launch: launch("codex", vec!["resume".into(), session_id.into()]),
4640            materialize: None,
4641            requires_materialization: true,
4642            note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4643        },
4644        TransferFormat::OpenCode => HandoffInstructions {
4645            launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4646            materialize: Some(launch(
4647                "opencode",
4648                vec!["import".into(), "{artifact_path}".into()],
4649            )),
4650            requires_materialization: true,
4651            note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4652        },
4653        TransferFormat::Pi => HandoffInstructions {
4654            launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4655            materialize: None,
4656            requires_materialization: true,
4657            note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4658        },
4659        TransferFormat::Grok => HandoffInstructions {
4660            launch: launch(
4661                "grok",
4662                vec!["--resume".into(), "{materialized_session_id}".into()],
4663            ),
4664            materialize: None,
4665            requires_materialization: true,
4666            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(),
4667        },
4668        TransferFormat::Gemini => HandoffInstructions {
4669            launch: launch(
4670                "gemini",
4671                vec!["--session-file".into(), "{artifact_path}".into()],
4672            ),
4673            materialize: None,
4674            requires_materialization: true,
4675            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(),
4676        },
4677        TransferFormat::Goose => HandoffInstructions {
4678            launch: launch(
4679                "goose",
4680                vec![
4681                    "session".into(),
4682                    "--resume".into(),
4683                    "--session-id".into(),
4684                    "{imported_session_id}".into(),
4685                ],
4686            ),
4687            materialize: Some(launch(
4688                "goose",
4689                vec!["session".into(), "import".into(), "{artifact_path}".into()],
4690            )),
4691            requires_materialization: true,
4692            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(),
4693        },
4694    }
4695}
4696
4697fn resume_launch(
4698    harness: &str,
4699    session_id: &str,
4700    cwd: &Path,
4701    policy: ResumePolicy,
4702) -> std::result::Result<StructuredLaunch, ServiceError> {
4703    let mut arguments = Vec::new();
4704    let program = match harness {
4705        HarnessId::GROK => {
4706            if matches!(policy, ResumePolicy::Yolo) {
4707                if crate::support::self_sandbox_supported() {
4708                    arguments.extend(["--sandbox".into(), "workspace".into()]);
4709                }
4710                arguments.push("--always-approve".into());
4711            }
4712            arguments.extend(["--resume".into(), session_id.into()]);
4713            "grok"
4714        }
4715        HarnessId::CODEX => {
4716            let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4717                .expect("a filesystem path always serializes as JSON text");
4718            arguments.extend([
4719                "-c".into(),
4720                "check_for_update_on_startup=false".into(),
4721                "-c".into(),
4722                format!("projects.{cwd_key}.trust_level=\"trusted\""),
4723            ]);
4724            if matches!(policy, ResumePolicy::Yolo) {
4725                arguments.extend([
4726                    "--dangerously-bypass-approvals-and-sandbox".into(),
4727                    "--dangerously-bypass-hook-trust".into(),
4728                ]);
4729            }
4730            arguments.extend(["resume".into(), session_id.into()]);
4731            "codex"
4732        }
4733        HarnessId::CLAUDE_CODE => {
4734            if matches!(policy, ResumePolicy::Yolo) {
4735                arguments.push("--dangerously-skip-permissions".into());
4736            }
4737            arguments.extend(["--resume".into(), session_id.into()]);
4738            "claude"
4739        }
4740        HarnessId::GEMINI => {
4741            if matches!(policy, ResumePolicy::Yolo) {
4742                arguments.push("--yolo".into());
4743            }
4744            arguments.extend(["--resume".into(), session_id.into()]);
4745            "gemini"
4746        }
4747        HarnessId::GOOSE => {
4748            arguments.extend([
4749                "session".into(),
4750                "--resume".into(),
4751                "--session-id".into(),
4752                session_id.into(),
4753            ]);
4754            "goose"
4755        }
4756        HarnessId::PI => {
4757            if matches!(policy, ResumePolicy::Yolo) {
4758                arguments.push("--approve".into());
4759            }
4760            arguments.extend(["--session".into(), session_id.into()]);
4761            "pi"
4762        }
4763        HarnessId::OPENCODE => {
4764            arguments.extend(["--session".into(), session_id.into()]);
4765            "opencode"
4766        }
4767        HarnessId::SUPERCODE => {
4768            if matches!(policy, ResumePolicy::Yolo) {
4769                arguments.push("--dangerous".into());
4770            }
4771            arguments.extend(["resume".into(), session_id.into()]);
4772            "supercode"
4773        }
4774        other => {
4775            return Err(ServiceError::InvalidParams(format!(
4776                "no structured resume launch is registered for harness `{other}`"
4777            )))
4778        }
4779    };
4780    Ok(StructuredLaunch {
4781        cwd: cwd.to_path_buf(),
4782        env: if program == "grok" {
4783            crate::support::grok_home_env()
4784        } else {
4785            BTreeMap::new()
4786        },
4787        program: program.into(),
4788        arguments,
4789    })
4790}
4791
4792/// Stage the resolved gateway credential in a private (0600) file so the
4793/// bridge can read it via `--token-file` — the delivery the real `openclaw
4794/// acp` accepts. One stable file per endpoint (keyed by an address digest,
4795/// no secret material in the name), overwritten on every connect so files
4796/// never accumulate and a rotated token never goes stale on disk.
4797fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4798    let digest = blake3::hash(address.as_bytes()).to_hex();
4799    let path = std::env::temp_dir().join(format!(
4800        "supercode-openclaw-gateway-token-{}",
4801        &digest.as_str()[..16]
4802    ));
4803    #[cfg(unix)]
4804    {
4805        use std::io::Write;
4806        use std::os::unix::fs::OpenOptionsExt;
4807        let mut file = std::fs::OpenOptions::new()
4808            .write(true)
4809            .create(true)
4810            .truncate(true)
4811            .mode(0o600)
4812            .open(&path)?;
4813        file.write_all(secret.as_bytes())?;
4814    }
4815    #[cfg(not(unix))]
4816    std::fs::write(&path, secret)?;
4817    Ok(path)
4818}
4819
4820/// Open a connect-mode descriptor: resolve the endpoint address and
4821/// credential from the harness's own config file and build the backend that
4822/// joins the already-running endpoint. Fails closed with a specific
4823/// diagnostic when the config cannot be resolved or the declared protocol has
4824/// no connect-capable client yet.
4825fn open_connect_descriptor(
4826    descriptor: &crate::HarnessSupportDescriptor,
4827    home: &Path,
4828) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4829    let Some(connect) = &descriptor.runtime.connect_launch else {
4830        return Err(ServiceError::InvalidParams(format!(
4831            "harness `{}` has no registered connect-mode launch",
4832            descriptor.id.as_str()
4833        )));
4834    };
4835    let resolved = connect
4836        .resolve(home)
4837        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4838    match (descriptor.id.as_str(), connect.protocol.as_str()) {
4839        (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4840            let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4841            if let Some(token) = resolved.auth {
4842                backend = backend.with_bearer(token);
4843            }
4844            Ok(Box::new(backend))
4845        }
4846        (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4847            // OpenClaw's own `openclaw acp` binary is the gateway client: a
4848            // stdio ACP bridge that joins the RUNNING gateway at the resolved
4849            // endpoint. Blind-walk finding 2026-08-31: the real bridge does
4850            // NOT honor OPENCLAW_GATEWAY_TOKEN from the environment — the
4851            // credential must arrive via `--token-file` (never bare `--token`
4852            // on argv, where process listings could read it). The env var is
4853            // still set for older bridges that did read it. Requires openclaw
4854            // >= 2026.7: the 2026.2 bridge drops its gateway socket
4855            // mid-prompt and advertises no session resume (executed finding,
4856            // docs/interop/research/openclaw-acp-dialect-2026-08-30.json).
4857            let mut env = BTreeMap::new();
4858            let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4859            if let Some(token) = resolved.auth {
4860                let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4861                    .map_err(|error| {
4862                        ServiceError::UnsupportedAction(format!(
4863                            "could not stage the gateway credential for the bridge: {error}"
4864                        ))
4865                    })?;
4866                arguments.push("--token-file".into());
4867                arguments.push(token_path.to_string_lossy().into_owned());
4868                env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4869            }
4870            // The bridge program comes from the descriptor's own default
4871            // launch (the compiled registry pins `openclaw`), so tests can
4872            // substitute an absolute mock-bridge path without touching
4873            // process-global state.
4874            let program = descriptor
4875                .runtime
4876                .default_launch
4877                .as_ref()
4878                .map(|launch| launch.program.clone())
4879                .unwrap_or_else(|| "openclaw".into());
4880            let launch = RuntimeLaunch {
4881                program,
4882                arguments,
4883                env,
4884            };
4885            Ok(Box::new(
4886                crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4887                    .with_resume_support(descriptor.runtime.capabilities.resume_session),
4888            ))
4889        }
4890        _ => Err(ServiceError::UnsupportedAction(format!(
4891            "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4892            descriptor.id.as_str(),
4893            connect.protocol
4894        ))),
4895    }
4896}
4897
4898/// The registry's connect-mode launch for this harness, honored only when the
4899/// caller supplied neither an explicit launch nor a base URL.
4900fn registry_connect_descriptor(
4901    params: &RuntimeBackendParams,
4902) -> Option<crate::HarnessSupportDescriptor> {
4903    if params.launch.is_some() || params.base_url.is_some() {
4904        return None;
4905    }
4906    harness_support_registry()
4907        .harnesses
4908        .into_iter()
4909        .find(|descriptor| descriptor.id == params.harness)
4910        .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4911}
4912
4913fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4914    std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4915        ServiceError::UnsupportedAction(
4916            "connect-mode launches need HOME to locate the harness config".into(),
4917        )
4918    })
4919}
4920
4921/// The doors that open a runtime: each spawns or joins a program and waits on
4922/// that program's protocol handshake before it can answer.
4923pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4924    "harness.v1.runtimes.start",
4925    "harness.v1.runtimes.resume",
4926    "harness.v1.runtimes.attach",
4927    "harness.v1.runtimes.attach_existing",
4928];
4929
4930/// How long a runtime gets to finish opening before its caller is answered an
4931/// error instead. A program that never speaks the protocol at all — the wrong
4932/// binary, a shim that prints usage and waits — never answers the handshake,
4933/// so the wait is unbounded without this.
4934pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
4935
4936/// How long a control call on an ALREADY-open runtime — send input, interrupt,
4937/// steer, respond, close — gets before its caller is answered an error
4938/// instead. A live runtime answers these in milliseconds; a wedged one never
4939/// answers at all, and `close` is exactly what a caller reaches for when it
4940/// suspects that.
4941pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
4942
4943/// The doors whose work happens entirely OUTSIDE this service's state once
4944/// its state has been read: probing harnesses, couriering a message into a
4945/// live session, and performing a conversation verb through a harness's own
4946/// CLI / HTTP / store door. Every one of them waits on a child process or a
4947/// network peer. See [`HarnessSessionService::detach`].
4948pub const DETACHED_METHODS: &[&str] = &[
4949    "harness.v1.harnesses.list",
4950    "harness.v1.harnesses.probe",
4951    "harness.v1.sessions.message",
4952    "harness.v1.sessions.new",
4953    "harness.v1.sessions.reset",
4954    "harness.v1.sessions.archive",
4955    "harness.v1.sessions.delete",
4956];
4957
4958/// How long a request moved off a transport's loop gets before its caller is
4959/// answered an error instead. Each of these already bounds its own inner
4960/// waits (a probe's handshake, the courier's run); this is the backstop for
4961/// the ones that do not — a harness CLI that never exits — so no caller waits
4962/// forever on a detached task no one is watching.
4963pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
4964
4965/// How long `sessions.discover` gets before its caller is answered an error
4966/// instead. Discovery reads each harness's own store, and a store on a cold
4967/// or unavailable mount answers at the filesystem's pace rather than its own.
4968///
4969/// Deliberately shorter than the clients' own request deadline (30s): the
4970/// server's answer names the store that did not answer, and it is only read
4971/// if it lands before the client stops listening.
4972pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
4973
4974/// Bound one control call on an open runtime by [`RUNTIME_CONTROL_DEADLINE`],
4975/// naming the method and the bound when it blows.
4976async fn within_control_deadline<F: std::future::Future>(
4977    method: &str,
4978    call: F,
4979) -> std::result::Result<F::Output, ServiceError> {
4980    tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
4981        .await
4982        .map_err(|_| {
4983            ServiceError::Operation(format!(
4984                "`{method}` gave up after {}s: the runtime did not answer",
4985                RUNTIME_CONTROL_DEADLINE.as_secs()
4986            ))
4987        })
4988}
4989
4990/// One [`RUNTIME_OPEN_METHODS`] request, parsed but not yet started. See
4991/// [`HarnessSessionService::runtime_open`] for why it exists apart from
4992/// [`HarnessSessionService::handle_async`].
4993pub struct RuntimeOpen {
4994    id: Value,
4995    method: String,
4996    params: Value,
4997}
4998
4999impl RuntimeOpen {
5000    /// Do the waiting: spawn or join the program and complete its handshake,
5001    /// bounded by [`RUNTIME_OPEN_DEADLINE`]. Touches no service state, so this
5002    /// runs on any task.
5003    pub async fn open(self) -> OpenedRuntime {
5004        let Self { id, method, params } = self;
5005        let outcome = open_runtime(&method, params).await;
5006        OpenedRuntime { id, outcome }
5007    }
5008}
5009
5010/// The result of [`RuntimeOpen::open`], ready for
5011/// [`HarnessSessionService::finish_runtime_open`].
5012pub struct OpenedRuntime {
5013    id: Value,
5014    outcome: std::result::Result<OpenRuntime, ServiceError>,
5015}
5016
5017/// One detached request: the half that reads this service's state already
5018/// done, and the half that waits not yet started. See
5019/// [`HarnessSessionService::detach`] and
5020/// [`HarnessSessionService::detach_runtime`].
5021pub struct DetachedCall {
5022    id: Value,
5023    method: String,
5024    work: std::result::Result<Work, ServiceError>,
5025}
5026
5027impl DetachedCall {
5028    /// Do the waiting and answer. Runs on any task: whatever this call needed
5029    /// from the service was taken before it left.
5030    pub async fn run(self) -> DetachedAnswer {
5031        let Self { id, method, work } = self;
5032        match work {
5033            // A call holding a runtime is already bounded by
5034            // RUNTIME_CONTROL_DEADLINE, and its future OWNS that connection:
5035            // a second timeout around it would drop the connection mid-call
5036            // and take down a runtime its caller still has.
5037            Ok(Work::Runtime(work)) => {
5038                let (result, returned) = work.run().await;
5039                DetachedAnswer {
5040                    response: service_response(id, result),
5041                    returned,
5042                }
5043            }
5044            Ok(Work::Free(work)) => {
5045                let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5046                    Ok(result) => result,
5047                    Err(_) => Err(ServiceError::Operation(format!(
5048                        "`{method}` gave up after {}s: the harness it waits on did not answer",
5049                        DETACHED_CALL_DEADLINE.as_secs()
5050                    ))),
5051                };
5052                DetachedAnswer {
5053                    response: service_response(id, result),
5054                    returned: None,
5055                }
5056            }
5057            Err(error) => DetachedAnswer {
5058                response: service_response(id, Err(error)),
5059                returned: None,
5060            },
5061        }
5062    }
5063}
5064
5065/// One detached call's complete answer, plus whatever it must hand back to
5066/// the service before that answer is written. See
5067/// [`HarnessSessionService::finish_detached`].
5068pub struct DetachedAnswer {
5069    response: Value,
5070    returned: Option<ReturnedRuntime>,
5071}
5072
5073impl DetachedAnswer {
5074    /// The caller's JSON-RPC response, for a transport that owns no service
5075    /// to give a borrowed connection back to.
5076    pub fn into_response(self) -> Value {
5077        self.response
5078    }
5079}
5080
5081/// A connection lent to a detached call, on its way back to the service that
5082/// owns it.
5083pub struct ReturnedRuntime {
5084    connection: String,
5085    runtime: Box<dyn RuntimeConnection>,
5086}
5087
5088/// The waiting half of one detached request: with nothing of the service's
5089/// in hand, or holding a connection the service lent out for the call.
5090enum Work {
5091    Free(DetachedWork),
5092    Runtime(RuntimeWork),
5093}
5094
5095/// The waiting half of one detached request that holds nothing of the
5096/// service's.
5097enum DetachedWork {
5098    /// Probe the selected harnesses: find their executables, ask each its
5099    /// version, and at `probe: handshake` start each one and complete its
5100    /// protocol handshake.
5101    Inventory(InventoryWork),
5102    /// Run the courier that delivers one message into a live session.
5103    Message(MessageSessionParams),
5104    /// Perform one conversation verb through the harness's own CLI, HTTP API,
5105    /// daemon socket, or supercode's own store.
5106    SessionMutation {
5107        verb: crate::SessionVerb,
5108        mutation: crate::SessionMutation,
5109    },
5110}
5111
5112impl DetachedWork {
5113    async fn run(self) -> std::result::Result<Value, ServiceError> {
5114        match self {
5115            Self::Inventory(work) => run_inventory(work).await,
5116            Self::Message(params) => {
5117                Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
5118            }
5119            Self::SessionMutation { verb, mutation } => {
5120                let outcome = run_session_mutation(verb, &mutation).await?;
5121                serde_json::to_value(outcome)
5122                    .map_err(|error| ServiceError::Operation(error.to_string()))
5123            }
5124        }
5125    }
5126}
5127
5128/// One detached call that holds a runtime connection for its whole run.
5129enum RuntimeWork {
5130    /// Tear down a runtime the service has already surrendered.
5131    Close {
5132        runtime: Box<dyn RuntimeConnection>,
5133        process_group: Option<u32>,
5134    },
5135    /// Type one live slash command through a borrowed connection, then give
5136    /// the connection back.
5137    LiveCommand {
5138        connection: String,
5139        runtime: Box<dyn RuntimeConnection>,
5140        verb: crate::SessionVerb,
5141        mutation: crate::SessionMutation,
5142        command: &'static str,
5143        session: String,
5144    },
5145}
5146
5147/// What one [`RuntimeWork`] answers with: the caller's result, and the
5148/// connection to give back when the call only borrowed one.
5149type RuntimeWorkAnswer = (
5150    std::result::Result<Value, ServiceError>,
5151    Option<ReturnedRuntime>,
5152);
5153
5154impl RuntimeWork {
5155    async fn run(self) -> RuntimeWorkAnswer {
5156        match self {
5157            Self::Close {
5158                runtime,
5159                process_group,
5160            } => (close_runtime(runtime, process_group).await, None),
5161            Self::LiveCommand {
5162                connection,
5163                mut runtime,
5164                verb,
5165                mutation,
5166                command,
5167                session,
5168            } => {
5169                let result =
5170                    type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5171                (
5172                    result,
5173                    Some(ReturnedRuntime {
5174                        connection,
5175                        runtime,
5176                    }),
5177                )
5178            }
5179        }
5180    }
5181}
5182
5183/// Tear down a runtime already out of the service, within
5184/// [`RUNTIME_CONTROL_DEADLINE`].
5185async fn close_runtime(
5186    mut runtime: Box<dyn RuntimeConnection>,
5187    process_group: Option<u32>,
5188) -> std::result::Result<Value, ServiceError> {
5189    match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5190        Ok(result) => {
5191            result.map_err(operation)?;
5192            Ok(json!({"closed": true}))
5193        }
5194        Err(deadline) => {
5195            // Dropping the handle is not enough: the process that stopped
5196            // answering is held by a task parked on it, so nothing here runs
5197            // its Drop. Signal the group the graceful path would have
5198            // signalled, then say so.
5199            let killed = kill_runtime_process_group(process_group);
5200            drop(runtime);
5201            Ok(json!({
5202                "closed": true,
5203                "killed": killed,
5204                "detail": error_message(deadline),
5205            }))
5206        }
5207    }
5208}
5209
5210/// The conversation a live `sessions.new` / `sessions.reset` acts on: the one
5211/// the request named, or the runtime's own session.
5212fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5213    mutation
5214        .session
5215        .clone()
5216        .filter(|value| !value.trim().is_empty())
5217        .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5218}
5219
5220/// Type one harness slash command into a live session through the very same
5221/// `send_input` path a human's message takes, within
5222/// [`RUNTIME_CONTROL_DEADLINE`].
5223async fn type_live_command(
5224    runtime: &mut dyn RuntimeConnection,
5225    verb: crate::SessionVerb,
5226    mutation: &crate::SessionMutation,
5227    command: &str,
5228    session: String,
5229) -> std::result::Result<Value, ServiceError> {
5230    within_control_deadline(
5231        &format!("sessions.{}", verb.as_str()),
5232        runtime.send_input(RuntimeInput {
5233            text: command.to_string(),
5234            image_urls: Vec::new(),
5235        }),
5236    )
5237    .await?
5238    .map_err(operation)?;
5239    let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5240        .map_err(session_control_error)?;
5241    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5242}
5243
5244/// A runtime that is up and whose handshake completed, with what the service
5245/// needs to take ownership of it.
5246enum OpenRuntime {
5247    /// supercode spawned this process, so it also hosts it: a frontend server,
5248    /// a live-runtime registration and a terminal launch of its own.
5249    Hosted {
5250        runtime: Box<dyn RuntimeConnection>,
5251        capabilities: crate::RuntimeCapabilities,
5252        workspace: PathBuf,
5253    },
5254    /// `attach_existing` joined a process supercode does not own. It is
5255    /// registered as a bare connection and hosts nothing.
5256    Joined { runtime: Box<dyn RuntimeConnection> },
5257}
5258
5259/// Open the runtime one [`RUNTIME_OPEN_METHODS`] request asks for, within
5260/// [`RUNTIME_OPEN_DEADLINE`]. The error a blown deadline answers names the
5261/// method and the bound, so a caller reads why it was cut loose instead of
5262/// waiting on a handshake that is never coming.
5263async fn open_runtime(
5264    method: &str,
5265    params: Value,
5266) -> std::result::Result<OpenRuntime, ServiceError> {
5267    match tokio::time::timeout(
5268        RUNTIME_OPEN_DEADLINE,
5269        open_runtime_unbounded(method, params),
5270    )
5271    .await
5272    {
5273        Ok(result) => result,
5274        Err(_) => Err(ServiceError::Operation(format!(
5275            "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5276            RUNTIME_OPEN_DEADLINE.as_secs()
5277        ))),
5278    }
5279}
5280
5281async fn open_runtime_unbounded(
5282    method: &str,
5283    params: Value,
5284) -> std::result::Result<OpenRuntime, ServiceError> {
5285    match method {
5286        "harness.v1.runtimes.start" => {
5287            let params = decode::<RuntimeStartParams>(params)?;
5288            let backend = runtime_backend(&params.backend)?;
5289            let capabilities = backend.capabilities();
5290            let workspace = params.cwd.clone();
5291            let runtime = backend
5292                .start(RuntimeStartRequest {
5293                    cwd: params.cwd,
5294                    launch: runtime_launch(&params.backend),
5295                    mcp_servers: params.mcp_servers,
5296                })
5297                .await
5298                .map_err(operation)?;
5299            Ok(OpenRuntime::Hosted {
5300                runtime,
5301                capabilities,
5302                workspace,
5303            })
5304        }
5305        "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5306            let params = decode::<RuntimeAttachParams>(params)?;
5307            let backend = runtime_backend(&params.backend)?;
5308            let capabilities = backend.capabilities();
5309            let workspace = params
5310                .cwd
5311                .clone()
5312                .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5313            let runtime = backend
5314                .attach(RuntimeAttachRequest {
5315                    runtime_id: params.runtime_id,
5316                    cwd: params.cwd,
5317                    launch: runtime_launch(&params.backend),
5318                    mcp_servers: params.mcp_servers,
5319                })
5320                .await
5321                .map_err(operation)?;
5322            Ok(OpenRuntime::Hosted {
5323                runtime,
5324                capabilities,
5325                workspace,
5326            })
5327        }
5328        "harness.v1.runtimes.attach_existing" => {
5329            let params = decode::<RuntimeAttachParams>(params)?;
5330            let backend: Box<dyn RuntimeBackend> = match params
5331                .backend
5332                .base_url
5333                .as_deref()
5334                .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5335            {
5336                Some(endpoint) => {
5337                    #[cfg(not(feature = "adapter-api"))]
5338                    {
5339                        let _ = endpoint;
5340                        return Err(ServiceError::UnsupportedAction(
5341                            "live HTTP attachment adapter is not compiled".into(),
5342                        ));
5343                    }
5344                    #[cfg(feature = "adapter-api")]
5345                    {
5346                        let workspace = params.cwd.clone().ok_or_else(|| {
5347                            ServiceError::InvalidParams(
5348                                "Supercode live attach requires the project cwd".into(),
5349                            )
5350                        })?;
5351                        let source = LiveRuntimeSource {
5352                            harness: params.backend.harness.as_str().to_string(),
5353                            session_id: params.runtime_id.clone(),
5354                            workspace,
5355                        };
5356                        let receipt = resolve_live_runtime(&endpoint, &source)
5357                            .map_err(|error| ServiceError::Operation(error.to_string()))?;
5358                        Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5359                    }
5360                }
5361                None => runtime_backend(&params.backend)?,
5362            };
5363            let capabilities = backend.capabilities();
5364            if !capabilities.attach_existing_process {
5365                return Err(ServiceError::Operation(format!(
5366                    "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5367                    backend.harness().as_str()
5368                )));
5369            }
5370            let runtime = backend
5371                .attach_existing(RuntimeAttachRequest {
5372                    runtime_id: params.runtime_id,
5373                    cwd: params.cwd,
5374                    launch: runtime_launch(&params.backend),
5375                    mcp_servers: params.mcp_servers,
5376                })
5377                .await
5378                .map_err(operation)?;
5379            Ok(OpenRuntime::Joined { runtime })
5380        }
5381        _ => Err(ServiceError::MethodNotFound),
5382    }
5383}
5384
5385/// Wrap one service outcome in its JSON-RPC 2.0 envelope.
5386fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5387    match result {
5388        Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5389        Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5390        Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5391        Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5392        Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5393        Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5394    }
5395}
5396
5397fn runtime_backend(
5398    params: &RuntimeBackendParams,
5399) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5400    if let Some(descriptor) = registry_connect_descriptor(params) {
5401        return open_connect_descriptor(&descriptor, &service_home()?);
5402    }
5403    if params.protocol.as_deref() == Some("acp") {
5404        let launch = params
5405            .launch
5406            .clone()
5407            .or_else(|| {
5408                harness_support_registry()
5409                    .harnesses
5410                    .into_iter()
5411                    .find(|harness| harness.id == params.harness)
5412                    .filter(|harness| {
5413                        harness.runtime.implementation == ImplementationKind::GenericProtocol
5414                            && harness.runtime.protocol.starts_with("acp")
5415                    })
5416                    .and_then(|harness| harness.runtime.default_launch)
5417            })
5418            .ok_or_else(|| {
5419                ServiceError::InvalidParams(
5420                    "an ACP runtime requires `launch` unless the harness has a registered default"
5421                        .into(),
5422                )
5423            })?;
5424        let resume_session = harness_support_registry()
5425            .harnesses
5426            .into_iter()
5427            .find(|harness| harness.id == params.harness)
5428            .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5429        return Ok(Box::new(
5430            AcpRuntimeBackend::new(params.harness.clone(), launch)
5431                .with_resume_support(resume_session),
5432        ));
5433    }
5434    let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5435        HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5436        HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5437        HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5438        HarnessId::OPENCODE => match &params.base_url {
5439            Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5440            None => Box::new(OpenCodeRuntimeBackend::new()),
5441        },
5442        harness => {
5443            let descriptor = harness_support_registry()
5444                .harnesses
5445                .into_iter()
5446                .find(|descriptor| descriptor.id.as_str() == harness)
5447                .filter(|descriptor| {
5448                    descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5449                        && descriptor.runtime.protocol.starts_with("acp")
5450                });
5451            let Some(descriptor) = descriptor else {
5452                return Err(ServiceError::InvalidParams(format!(
5453                    "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5454                )));
5455            };
5456            let resume = descriptor.runtime.capabilities.resume_session;
5457            Box::new(
5458                AcpRuntimeBackend::new(
5459                    descriptor.id,
5460                    descriptor
5461                        .runtime
5462                        .default_launch
5463                        .expect("generic ACP registry entry includes its launch"),
5464                )
5465                .with_resume_support(resume),
5466            )
5467        }
5468    };
5469    Ok(backend)
5470}
5471
5472fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5473    if let Some(launch) = &params.launch {
5474        return Some(launch.clone());
5475    }
5476    if !matches!(params.policy, RuntimePolicy::Yolo) {
5477        return None;
5478    }
5479    let launch = match params.harness.as_str() {
5480        HarnessId::GROK => RuntimeLaunch {
5481            program: "grok".into(),
5482            arguments: {
5483                let mut arguments: Vec<String> = Vec::new();
5484                if crate::support::self_sandbox_supported() {
5485                    arguments.extend(["--sandbox".into(), "workspace".into()]);
5486                }
5487                arguments.extend([
5488                    "--always-approve".into(),
5489                    "agent".into(),
5490                    "--no-leader".into(),
5491                    "stdio".into(),
5492                ]);
5493                arguments
5494            },
5495            env: crate::support::grok_env(),
5496        },
5497        HarnessId::CODEX => RuntimeLaunch {
5498            program: "codex".into(),
5499            arguments: vec![
5500                "--dangerously-bypass-approvals-and-sandbox".into(),
5501                "--dangerously-bypass-hook-trust".into(),
5502                "app-server".into(),
5503            ],
5504            env: BTreeMap::new(),
5505        },
5506        HarnessId::CLAUDE_CODE => RuntimeLaunch {
5507            program: "claude".into(),
5508            arguments: vec![
5509                "--dangerously-skip-permissions".into(),
5510                "--print".into(),
5511                "--input-format".into(),
5512                "stream-json".into(),
5513                "--output-format".into(),
5514                "stream-json".into(),
5515                "--verbose".into(),
5516            ],
5517            env: BTreeMap::new(),
5518        },
5519        HarnessId::PI => RuntimeLaunch {
5520            program: "pi".into(),
5521            arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5522            env: BTreeMap::new(),
5523        },
5524        HarnessId::OPENCODE => RuntimeLaunch {
5525            program: "opencode".into(),
5526            arguments: vec!["serve".into()],
5527            env: BTreeMap::new(),
5528        },
5529        HarnessId::GEMINI => RuntimeLaunch {
5530            program: "gemini".into(),
5531            arguments: vec!["--acp".into(), "--yolo".into()],
5532            env: BTreeMap::new(),
5533        },
5534        HarnessId::GOOSE => RuntimeLaunch {
5535            program: "goose".into(),
5536            arguments: vec!["acp".into()],
5537            env: BTreeMap::new(),
5538        },
5539        HarnessId::SUPERCODE => RuntimeLaunch {
5540            program: "supercode".into(),
5541            arguments: vec!["acp".into(), "--dangerous".into()],
5542            env: BTreeMap::new(),
5543        },
5544        _ => return None,
5545    };
5546    Some(launch)
5547}
5548
5549/// Disposable harness state for a no-prompt readiness probe. Merely opening
5550/// several stock CLIs writes a session header or migrates configuration, so a
5551/// handshake must never point at the user's real home. Authentication files
5552/// are copied into the private temporary home; all writes disappear with the
5553/// guard after the connection closes.
5554struct IsolatedProbeHome {
5555    launch: RuntimeLaunch,
5556    root: PathBuf,
5557}
5558
5559impl IsolatedProbeHome {
5560    fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5561        let root = std::env::temp_dir().join(format!(
5562            "supercode-harness-probe-{harness}-{}",
5563            generated_session_id()
5564        ));
5565        std::fs::create_dir_all(&root)?;
5566        set_private_dir_permissions(&root)?;
5567
5568        if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
5569            for relative in probe_auth_files(harness) {
5570                copy_probe_file(&source_home, &root, relative)?;
5571            }
5572        }
5573        // supercode reads its own config home ($SUPERCODE_HOME, else
5574        // $XDG_CONFIG_HOME/supercode, else ~/.config/supercode), not a fixed
5575        // place under HOME: a login kept under XDG_CONFIG_HOME probed as
5576        // "no API key found" while `supercode run` answered.
5577        if harness == HarnessId::SUPERCODE {
5578            let config_home = crate::agent::global_instructions_dir();
5579            for file in ["config.toml", "credentials.toml"] {
5580                copy_probe_path(
5581                    &config_home.join(file),
5582                    &root.join(".config/supercode").join(file),
5583                )?;
5584            }
5585        }
5586        configure_isolated_probe_auth(harness, &root)?;
5587
5588        let root_text = root.to_string_lossy().into_owned();
5589        for (key, value) in [
5590            ("HOME", root_text.clone()),
5591            (
5592                "XDG_CACHE_HOME",
5593                root.join(".cache").to_string_lossy().into_owned(),
5594            ),
5595            (
5596                "XDG_CONFIG_HOME",
5597                root.join(".config").to_string_lossy().into_owned(),
5598            ),
5599            (
5600                "XDG_DATA_HOME",
5601                root.join(".local/share").to_string_lossy().into_owned(),
5602            ),
5603        ] {
5604            launch.env.insert(key.into(), value);
5605        }
5606        let scoped = match harness {
5607            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5608            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5609            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5610            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5611            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5612            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5613            _ => None,
5614        };
5615        if let Some((key, value)) = scoped {
5616            launch
5617                .env
5618                .insert(key.into(), value.to_string_lossy().into_owned());
5619        }
5620        Ok(Self { launch, root })
5621    }
5622
5623    fn cleanup(&self) -> std::io::Result<()> {
5624        match std::fs::remove_dir_all(&self.root) {
5625            Ok(()) => Ok(()),
5626            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5627            Err(error) => Err(error),
5628        }
5629    }
5630}
5631
5632impl Drop for IsolatedProbeHome {
5633    fn drop(&mut self) {
5634        let _ = self.cleanup();
5635    }
5636}
5637
5638fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5639    match harness {
5640        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5641        // The gateway endpoint + token live in openclaw's own config; without
5642        // it the isolated probe dials the default endpoint unauthenticated
5643        // (PARITY-24 finding 2026-08-31).
5644        HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5645        HarnessId::CODEX => &[".codex/auth.json"],
5646        HarnessId::GEMINI => &[
5647            ".gemini/google_accounts.json",
5648            ".gemini/oauth_creds.json",
5649            ".gemini/settings.json",
5650        ],
5651        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5652        HarnessId::OPENCODE => &[
5653            ".config/opencode/auth.json",
5654            ".local/share/opencode/auth.json",
5655        ],
5656        HarnessId::PI => &[".pi/agent/auth.json"],
5657        // Hermes keeps its provider selection in config.yaml, its OAuth
5658        // credential pool in auth.json, and API keys in .env; without them
5659        // the isolated probe sees "No LLM provider configured" for a
5660        // hermes that answers fine from the user's real home.
5661        HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5662        _ => &[],
5663    }
5664}
5665
5666fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5667    copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5668}
5669
5670fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5671    if !source.is_file() {
5672        return Ok(());
5673    }
5674    if let Some(parent) = destination.parent() {
5675        std::fs::create_dir_all(parent)?;
5676        set_private_dir_permissions(parent)?;
5677    }
5678    std::fs::copy(source, destination)?;
5679    set_private_file_permissions(destination)
5680}
5681
5682fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5683    if harness != HarnessId::GEMINI {
5684        return Ok(());
5685    }
5686    let oauth = probe_home.join(".gemini/oauth_creds.json");
5687    if !oauth.is_file() {
5688        return Ok(());
5689    }
5690    let settings_path = probe_home.join(".gemini/settings.json");
5691    let mut settings = std::fs::read_to_string(&settings_path)
5692        .ok()
5693        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5694        .unwrap_or_else(|| json!({}));
5695    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5696    std::fs::write(
5697        &settings_path,
5698        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5699    )?;
5700    set_private_file_permissions(&settings_path)
5701}
5702
5703#[cfg(unix)]
5704fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5705    use std::os::unix::fs::PermissionsExt;
5706    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5707}
5708
5709#[cfg(not(unix))]
5710fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5711    Ok(())
5712}
5713
5714#[cfg(unix)]
5715fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5716    use std::os::unix::fs::PermissionsExt;
5717    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5718}
5719
5720#[cfg(not(unix))]
5721fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5722    Ok(())
5723}
5724
5725fn find_executable(program: &str) -> Option<PathBuf> {
5726    let candidate = PathBuf::from(program);
5727    if candidate.components().count() > 1 {
5728        return candidate.is_file().then_some(candidate);
5729    }
5730    let path = std::env::var_os("PATH")?;
5731    for directory in std::env::split_paths(&path) {
5732        let candidate = directory.join(program);
5733        if candidate.is_file() {
5734            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5735        }
5736        #[cfg(windows)]
5737        {
5738            for extension in ["exe", "cmd", "bat"] {
5739                let candidate = directory.join(format!("{program}.{extension}"));
5740                if candidate.is_file() {
5741                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5742                }
5743            }
5744        }
5745    }
5746    None
5747}
5748
5749async fn executable_version(executable: &Path) -> Option<String> {
5750    let mut command = tokio::process::Command::new(executable);
5751    command
5752        .arg("--version")
5753        .stdin(std::process::Stdio::null())
5754        .stdout(std::process::Stdio::piped())
5755        .stderr(std::process::Stdio::piped())
5756        .kill_on_drop(true);
5757    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5758        .await
5759        .ok()?
5760        .ok()?;
5761    let stdout = String::from_utf8_lossy(&output.stdout);
5762    let stderr = String::from_utf8_lossy(&output.stderr);
5763    stdout
5764        .lines()
5765        .chain(stderr.lines())
5766        .map(str::trim)
5767        .find(|line| !line.is_empty())
5768        .map(|line| truncate_text(line, 200))
5769}
5770
5771pub(crate) fn auth_evidence(harness: &str) -> bool {
5772    let env_names: &[&str] = match harness {
5773        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5774        HarnessId::CODEX => &["OPENAI_API_KEY"],
5775        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5776        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5777        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5778        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5779        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5780        _ => &[],
5781    };
5782    if env_names
5783        .iter()
5784        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5785    {
5786        return true;
5787    }
5788    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5789        return false;
5790    };
5791    let files: Vec<PathBuf> = match harness {
5792        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5793        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5794        HarnessId::OPENCODE => vec![
5795            home.join(".local/share/opencode/auth.json"),
5796            home.join(".config/opencode/auth.json"),
5797        ],
5798        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5799        HarnessId::GROK => vec![home.join(".grok/auth.json")],
5800        HarnessId::GEMINI => vec![
5801            home.join(".gemini/oauth_creds.json"),
5802            home.join(".gemini/google_accounts.json"),
5803        ],
5804        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5805        HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5806        _ => Vec::new(),
5807    };
5808    if files.into_iter().any(|path| {
5809        std::fs::metadata(path)
5810            .map(|metadata| metadata.is_file() && metadata.len() > 2)
5811            .unwrap_or(false)
5812    }) {
5813        return true;
5814    }
5815    // macOS keeps Claude Code's OAuth login in the Keychain, so
5816    // `.claude/.credentials.json` never exists there and the file probe above
5817    // reports a signed-in install as unauthenticated forever. A completed
5818    // login also writes an `oauthAccount` record into `~/.claude.json` on
5819    // every platform — file-based, prompt-free evidence (querying the
5820    // Keychain itself from an unsigned daemon can raise a UI prompt).
5821    if harness == HarnessId::CLAUDE_CODE {
5822        return std::fs::read_to_string(home.join(".claude.json"))
5823            .map(|text| text.contains("\"oauthAccount\""))
5824            .unwrap_or(false);
5825    }
5826    false
5827}
5828
5829fn looks_like_auth_error(message: &str) -> bool {
5830    let message = message.to_ascii_lowercase();
5831    [
5832        "auth",
5833        "login",
5834        "sign in",
5835        "sign-in",
5836        "credential",
5837        "unauthorized",
5838        "forbidden",
5839        "token",
5840    ]
5841    .iter()
5842    .any(|needle| message.contains(needle))
5843}
5844
5845fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5846    crate::RuntimeCapabilities {
5847        start_session: false,
5848        resume_session: false,
5849        attach_existing_process: false,
5850        send_input: false,
5851        stream_events: false,
5852        interrupt: false,
5853        steer: false,
5854        respond_to_requests: false,
5855    }
5856}
5857
5858fn truncate_text(text: &str, max_chars: usize) -> String {
5859    let mut chars = text.chars();
5860    let truncated = chars.by_ref().take(max_chars).collect::<String>();
5861    if chars.next().is_some() {
5862        format!("{truncated}…")
5863    } else {
5864        truncated
5865    }
5866}
5867
5868/// The process group a runtime's own handle names, when it names one.
5869///
5870/// Every adapter that spawns a local process spawns it as its own group
5871/// leader (`Command::process_group(0)`), so the endpoint's pid IS the group
5872/// id. A runtime reached over HTTP, or one supercode joined rather than
5873/// spawned, names no group here and is left alone.
5874fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5875    match &handle.endpoint {
5876        crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5877        crate::RuntimeEndpoint::Http { .. } => None,
5878    }
5879}
5880
5881/// SIGKILL a wedged runtime's whole process group, reporting whether there
5882/// was one to signal. This is the same group teardown a graceful `close`
5883/// performs; it runs here only when the graceful path blew its deadline,
5884/// because the task parked on the unanswered call still owns the process
5885/// handle and so no `Drop` of ours can reach it.
5886fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5887    match process_group {
5888        #[cfg(unix)]
5889        Some(pid) => {
5890            crate::lsp::kill_process_group(pid);
5891            true
5892        }
5893        #[cfg(not(unix))]
5894        Some(_) => false,
5895        None => false,
5896    }
5897}
5898
5899fn error_message(error: ServiceError) -> String {
5900    match error {
5901        ServiceError::InvalidParams(message)
5902        | ServiceError::Operation(message)
5903        | ServiceError::UnsupportedAction(message) => message,
5904        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5905        ServiceError::Sdk(error) => error.to_string(),
5906    }
5907}
5908
5909#[derive(Debug)]
5910enum ServiceError {
5911    InvalidParams(String),
5912    MethodNotFound,
5913    UnsupportedAction(String),
5914    Operation(String),
5915    Sdk(SdkError),
5916}
5917
5918fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5919    match error {
5920        ServiceError::InvalidParams(message) => {
5921            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5922        }
5923        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5924            SdkError::unsupported(operation)
5925        }
5926        ServiceError::Operation(message) => {
5927            let code = if message.contains("already in progress") {
5928                SdkErrorCode::Busy
5929            } else if message.contains("not supported by this runtime") {
5930                SdkErrorCode::UnsupportedAction
5931            } else if message.contains("unknown runtime connection") {
5932                SdkErrorCode::NotFound
5933            } else {
5934                SdkErrorCode::Execution
5935            };
5936            SdkError::new(code, operation, message)
5937        }
5938        ServiceError::Sdk(error) => error,
5939    }
5940}
5941
5942fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5943    let error_code = error.code();
5944    let code = match error_code {
5945        SdkErrorCode::Unauthenticated => -32030,
5946        SdkErrorCode::Unauthorized => -32031,
5947        SdkErrorCode::ControllerRequired => -32032,
5948        SdkErrorCode::LeaseExpired => -32033,
5949        SdkErrorCode::InvalidArgument => -32602,
5950        SdkErrorCode::NotFound => -32004,
5951        SdkErrorCode::Busy => -32000,
5952        SdkErrorCode::UnsupportedAction => -32020,
5953        SdkErrorCode::Execution => -32002,
5954        SdkErrorCode::Transport => -32003,
5955    };
5956    json!({
5957        "jsonrpc": "2.0",
5958        "id": id,
5959        "error": {
5960            "code": code,
5961            "name": error_code,
5962            "operation": error.operation(),
5963            "message": error.to_string(),
5964        },
5965    })
5966}
5967
5968fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
5969    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
5970}
5971
5972fn operation(error: impl Into<crate::Error>) -> ServiceError {
5973    let error = error.into();
5974    match error {
5975        crate::Error::Sdk(error) => ServiceError::Sdk(error),
5976        error => ServiceError::Operation(error.to_string()),
5977    }
5978}
5979
5980/// ORCH-12 `harness.v1.memory.show|search` params. `homes` is the same
5981/// storage-root override every read-only method accepts, so a caller can
5982/// point the read at a fixture home without touching the real ones.
5983#[derive(Debug, Clone, Deserialize, Default)]
5984#[serde(default)]
5985struct MemoryRequest {
5986    /// Harness whose store is read. Required.
5987    harness: Option<String>,
5988    /// The needle, required by `search`.
5989    query: Option<String>,
5990    /// Hermes profile, OpenClaw agent, or Claude Code project.
5991    profile: Option<String>,
5992    /// Claude Code session id selecting a project store (`show` only).
5993    session: Option<String>,
5994    /// Include each document's whole text (`show` only).
5995    full: bool,
5996    /// Treat `query` as a regular expression (`search` only).
5997    regex: bool,
5998    /// Working tree whose project store is read.
5999    cwd: Option<std::path::PathBuf>,
6000    /// Storage roots to read.
6001    homes: crate::HarnessHomes,
6002}
6003
6004/// Read the memory noun. A harness with no memory store fails with
6005/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6006fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6007    let request = decode::<MemoryRequest>(params)?;
6008    let harness = request
6009        .harness
6010        .clone()
6011        .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6012    let to_service = |error: crate::memory::MemoryError| match error {
6013        crate::memory::MemoryError::UnsupportedHarness { .. }
6014        | crate::memory::MemoryError::SessionNotScoped { .. } => {
6015            ServiceError::UnsupportedAction(error.to_string())
6016        }
6017        other => ServiceError::InvalidParams(other.to_string()),
6018    };
6019    match method {
6020        "harness.v1.memory.show" => {
6021            let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6022                harness,
6023                profile: request.profile,
6024                session: request.session,
6025                full: request.full,
6026                cwd: request.cwd,
6027                homes: request.homes,
6028            })
6029            .map_err(to_service)?;
6030            Ok(json!({
6031                "schema": crate::memory::MEMORY_SCHEMA,
6032                "documents": documents,
6033            }))
6034        }
6035        "harness.v1.memory.search" => {
6036            let query = request
6037                .query
6038                .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6039            let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6040                harness,
6041                query,
6042                profile: request.profile,
6043                regex: request.regex,
6044                cwd: request.cwd,
6045                homes: request.homes,
6046            })
6047            .map_err(to_service)?;
6048            Ok(json!({
6049                "schema": crate::memory::MEMORY_SCHEMA,
6050                "matches": matches,
6051            }))
6052        }
6053        _ => Err(ServiceError::MethodNotFound),
6054    }
6055}
6056
6057/// ORCH-10 `harness.v1.profiles.list|get` params. `homes` is the same
6058/// storage-root override every read-only method accepts, so a caller can
6059/// point the read at a fixture home without touching the real ones.
6060#[derive(Debug, Clone, Deserialize)]
6061#[serde(default)]
6062struct ProfilesQuery {
6063    /// Restrict the listing to one harness. `get` requires it.
6064    harness: Option<String>,
6065    /// Profile name, required by `get`.
6066    name: Option<String>,
6067    /// Storage roots to read.
6068    homes: crate::HarnessHomes,
6069}
6070
6071impl Default for ProfilesQuery {
6072    fn default() -> Self {
6073        Self {
6074            harness: None,
6075            name: None,
6076            homes: crate::HarnessHomes::default(),
6077        }
6078    }
6079}
6080
6081/// Read the profile noun. A harness with no profile concept fails with
6082/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6083fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6084    let query = decode::<ProfilesQuery>(params)?;
6085    let to_service = |error: crate::profiles::ProfileError| match error {
6086        crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6087            ServiceError::UnsupportedAction(error.to_string())
6088        }
6089        crate::profiles::ProfileError::NotFound { .. } => {
6090            ServiceError::InvalidParams(error.to_string())
6091        }
6092    };
6093    match method {
6094        "harness.v1.profiles.list" => {
6095            let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6096                .map_err(to_service)?;
6097            Ok(json!({
6098                "schema": crate::profiles::PROFILES_SCHEMA,
6099                "profiles": profiles,
6100            }))
6101        }
6102        "harness.v1.profiles.get" => {
6103            let harness = query
6104                .harness
6105                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6106            let name = query
6107                .name
6108                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6109            let profile =
6110                crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6111            Ok(json!({
6112                "schema": crate::profiles::PROFILES_SCHEMA,
6113                "profile": profile,
6114            }))
6115        }
6116        _ => Err(ServiceError::MethodNotFound),
6117    }
6118}
6119
6120/// ORCH-14 `harness.v1.channels.list|status` params, the same storage-root
6121/// override every read-only method accepts so a caller can point the read at
6122/// a fixture home without touching the real ones.
6123#[derive(Debug, Clone, Deserialize)]
6124#[serde(default)]
6125struct ChannelsQuery {
6126    /// Restrict the listing to one harness. `status` requires it.
6127    harness: Option<String>,
6128    /// Channel name, required by `status`.
6129    name: Option<String>,
6130    /// Storage roots to read.
6131    homes: crate::HarnessHomes,
6132}
6133
6134impl Default for ChannelsQuery {
6135    fn default() -> Self {
6136        Self {
6137            harness: None,
6138            name: None,
6139            homes: crate::HarnessHomes::default(),
6140        }
6141    }
6142}
6143
6144/// Read the channel noun. A harness with no channel concept fails with
6145/// `UnsupportedAction` (RPC `-32020`), never an empty list. No row carries a
6146/// token, key or secret — see `crate::channels` "Secrecy".
6147#[derive(Debug, Clone, Deserialize)]
6148#[serde(default)]
6149struct RoutesQuery {
6150    harness: Option<String>,
6151    /// Restrict to routes targeting one profile / agent.
6152    profile: Option<String>,
6153    homes: crate::HarnessHomes,
6154}
6155
6156impl Default for RoutesQuery {
6157    fn default() -> Self {
6158        Self {
6159            harness: None,
6160            profile: None,
6161            homes: crate::HarnessHomes::default(),
6162        }
6163    }
6164}
6165
6166#[derive(Debug, Clone, Deserialize)]
6167#[serde(default)]
6168struct TriggersQuery {
6169    harness: Option<String>,
6170    homes: crate::HarnessHomes,
6171}
6172
6173impl Default for TriggersQuery {
6174    fn default() -> Self {
6175        Self {
6176            harness: None,
6177            homes: crate::HarnessHomes::default(),
6178        }
6179    }
6180}
6181
6182fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6183    let query = decode::<TriggersQuery>(params)?;
6184    let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6185        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6186    Ok(json!({
6187        "schema": crate::triggers::TRIGGERS_SCHEMA,
6188        "triggers": triggers,
6189    }))
6190}
6191
6192fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6193    let query = decode::<RoutesQuery>(params)?;
6194    let routes = crate::routes::list_routes(
6195        &query.homes,
6196        query.harness.as_deref(),
6197        query.profile.as_deref(),
6198    )
6199    .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6200    Ok(json!({
6201        "schema": crate::routes::ROUTES_SCHEMA,
6202        "routes": routes,
6203    }))
6204}
6205
6206fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6207    let query = decode::<ChannelsQuery>(params)?;
6208    let to_service = |error: crate::channels::ChannelError| match error {
6209        crate::channels::ChannelError::UnsupportedHarness { .. } => {
6210            ServiceError::UnsupportedAction(error.to_string())
6211        }
6212        crate::channels::ChannelError::NotFound { .. } => {
6213            ServiceError::InvalidParams(error.to_string())
6214        }
6215    };
6216    match method {
6217        "harness.v1.channels.list" => {
6218            let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6219                .map_err(to_service)?;
6220            Ok(json!({
6221                "schema": crate::channels::CHANNELS_SCHEMA,
6222                "channels": channels,
6223            }))
6224        }
6225        "harness.v1.channels.status" => {
6226            let harness = query
6227                .harness
6228                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6229            let name = query
6230                .name
6231                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6232            let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6233                .map_err(to_service)?;
6234            Ok(json!({
6235                "schema": crate::channels::CHANNELS_SCHEMA,
6236                "channel": channel,
6237            }))
6238        }
6239        _ => Err(ServiceError::MethodNotFound),
6240    }
6241}
6242
6243fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6244    json!({
6245        "jsonrpc": "2.0",
6246        "id": id,
6247        "error": {"code": code, "message": message},
6248    })
6249}
6250
6251#[cfg(test)]
6252mod tests {
6253    use super::*;
6254    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6255    use async_trait::async_trait;
6256    use std::io::Write;
6257    use std::path::PathBuf;
6258    use std::time::Instant;
6259
6260    #[test]
6261    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6262        let descriptor = SessionDescriptor {
6263            locator: SessionLocator {
6264                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6265                session_id: "live-session".into(),
6266                storage: StorageLocator::File {
6267                    path: PathBuf::from("/tmp/live-session.jsonl"),
6268                },
6269            },
6270            cwd: Some(PathBuf::from("/project")),
6271            title: None,
6272            preview_candidates: Vec::new(),
6273            latest_message_candidates: Vec::new(),
6274            updated_at_ms: Some(1),
6275            message_count: None,
6276            model: None,
6277            parent_session_id: None,
6278            child_session_count: 0,
6279            nouns: Default::default(),
6280        };
6281        let peer = crate::claude_peer::ClaudePeerSession {
6282            pid: 42,
6283            session_id: "live-session".into(),
6284            cwd: Some(PathBuf::from("/project")),
6285            name: "peer".into(),
6286            socket_path: PathBuf::from("/tmp/peer.sock"),
6287            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6288            updated_at_ms: Some(1),
6289            version: Some("test".into()),
6290        };
6291
6292        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6293        assert!(value["live_endpoint"]
6294            .as_str()
6295            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6296    }
6297
6298    struct EndingRuntime {
6299        handle: RuntimeHandle,
6300        event: Option<HarnessEvent>,
6301        close_failures: usize,
6302    }
6303
6304    #[async_trait]
6305    impl RuntimeConnection for EndingRuntime {
6306        fn handle(&self) -> &RuntimeHandle {
6307            &self.handle
6308        }
6309
6310        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6311            unreachable!("ending runtime does not accept input")
6312        }
6313
6314        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6315            Ok(self.event.take())
6316        }
6317
6318        async fn interrupt(&mut self) -> crate::Result<()> {
6319            Ok(())
6320        }
6321
6322        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6323            Ok(())
6324        }
6325
6326        async fn close(&mut self) -> crate::Result<()> {
6327            if self.close_failures > 0 {
6328                self.close_failures -= 1;
6329                return Err(crate::Error::Other(
6330                    "cleanup temporarily unavailable".into(),
6331                ));
6332            }
6333            Ok(())
6334        }
6335    }
6336
6337    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6338        Box::new(EndingRuntime {
6339            handle: RuntimeHandle {
6340                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6341                runtime_id: "ending-session".into(),
6342                endpoint: RuntimeEndpoint::LocalProcess {
6343                    pid: None,
6344                    command: vec!["ending-runtime".into()],
6345                    protocol: "test".into(),
6346                },
6347            },
6348            event,
6349            close_failures: 0,
6350        })
6351    }
6352
6353    #[tokio::test]
6354    async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6355        let mut service = HarnessSessionService::new();
6356        let handle = ending_runtime(None).handle().clone();
6357        let runtime_id = handle.runtime_id.clone();
6358        let opened = service
6359            .insert_runtime(Box::new(EndingRuntime {
6360                handle,
6361                event: None,
6362                close_failures: 1,
6363            }))
6364            .unwrap();
6365        let connection = opened["connection"].as_str().unwrap().to_string();
6366        service.terminal_launches.insert(
6367            connection.clone(),
6368            StructuredLaunch {
6369                cwd: PathBuf::from("/fixture"),
6370                program: "fixture".into(),
6371                arguments: Vec::new(),
6372                env: BTreeMap::new(),
6373            },
6374        );
6375        let first = service
6376            .handle_async(request(
6377                1,
6378                "harness.v1.runtimes.close",
6379                json!({"connection": connection}),
6380            ))
6381            .await;
6382        // The harness's own teardown failed and the caller is told so...
6383        assert!(first.get("error").is_some(), "{first}");
6384        // ...but the connection is gone all the same. A connection whose close
6385        // cannot complete is exactly the one that must not stay registered:
6386        // holding it would answer every later call on this node with a turn
6387        // that is never going to end.
6388        assert!(!service.runtimes.contains_key(&connection));
6389        assert!(!service.terminal_launches.contains_key(&connection));
6390        assert!(!service.runtime_sequences.contains_key(&runtime_id));
6391        let again = service
6392            .handle_async(request(
6393                2,
6394                "harness.v1.runtimes.close",
6395                json!({"connection": connection}),
6396            ))
6397            .await;
6398        assert_eq!(again["error"]["code"], -32602, "{again}");
6399    }
6400
6401    fn request(id: u64, method: &str, params: Value) -> Value {
6402        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6403    }
6404
6405    // ---- ORCH-6: conversation nouns on `sessions.*` ----------------------
6406
6407    fn hermes_store() -> PathBuf {
6408        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6409    }
6410
6411    /// The discovery response for the Hermes fixture home, with the one
6412    /// machine-specific value (the absolute store path) replaced so the exact
6413    /// same JSON can be committed and replayed by the UI story.
6414    fn hermes_discovery(params: Value) -> Value {
6415        let mut response =
6416            HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6417        let store = hermes_store().display().to_string();
6418        for session in response["result"]["sessions"]
6419            .as_array_mut()
6420            .expect("sessions array")
6421        {
6422            if session["locator"]["storage"]["path"] == json!(store) {
6423                session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6424            }
6425            // `activity` reports a wall-clock observation instant, not a fact
6426            // about the session; it would make this response differ on every
6427            // call. The nouns under test are all session facts.
6428            session.as_object_mut().unwrap().remove("activity");
6429        }
6430        response["result"].take()
6431    }
6432
6433    fn hermes_query() -> Value {
6434        json!({
6435            "harnesses": ["hermes"],
6436            "homes": {"hermes": hermes_store()},
6437        })
6438    }
6439
6440    fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6441        result["sessions"]
6442            .as_array()
6443            .expect("sessions array")
6444            .iter()
6445            .find(|session| session["locator"]["session_id"] == json!(id))
6446            .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6447    }
6448
6449    #[test]
6450    fn orch6_discover_rows_carry_the_conversation_nouns() {
6451        let result = hermes_discovery(hermes_query());
6452
6453        // A Telegram DM: reached on a channel, no repo — the workspace IS the
6454        // channel (D2 precedence), and `main` is not a profile.
6455        let dm = row(&result, "tg-dm-1");
6456        assert_eq!(dm["trigger"], json!("channel"));
6457        assert_eq!(dm["surface"]["platform"], json!("telegram"));
6458        assert_eq!(dm["surface"]["kind"], json!("dm"));
6459        assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6460        assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6461        assert_eq!(
6462            dm["workspace"],
6463            json!({"kind": "channel", "value": "telegram:123456"})
6464        );
6465        assert!(dm.get("profile").is_none(), "{dm:#}");
6466
6467        // A cron fire: recurring, with the job recovered from the minted id.
6468        let fire = row(&result, "cron_job42_20260902_120000");
6469        assert_eq!(fire["trigger"], json!("cron"));
6470        assert_eq!(
6471            fire["recurrence"],
6472            json!({"job_id": "job42", "kind": "cron"})
6473        );
6474        assert_eq!(fire["workspace"]["kind"], json!("repo"));
6475
6476        // A profiled group session with a pending handoff: repo workspace
6477        // wins over the channel, and the chat stays on the surface key.
6478        let coder = row(&result, "tg-coder-1");
6479        assert_eq!(coder["trigger"], json!("channel"));
6480        assert_eq!(coder["profile"], json!("coder"));
6481        assert_eq!(coder["surface"]["thread_id"], json!("55"));
6482        assert_eq!(
6483            coder["surface"]["key"],
6484            json!("agent:coder:telegram:group:-100777:55")
6485        );
6486        assert_eq!(
6487            coder["workspace"],
6488            json!({"kind": "repo", "value": "/workspace/project"})
6489        );
6490        assert_eq!(
6491            coder["cross_surface"],
6492            json!({"state": "pending", "platform": "discord"})
6493        );
6494
6495        // A plain ACP session stays human-triggered with no surface at all.
6496        let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6497        assert_eq!(acp["trigger"], json!("human"));
6498        assert!(acp.get("surface").is_none(), "{acp:#}");
6499        assert_eq!(acp["workspace"], json!({"kind": "none"}));
6500    }
6501
6502    #[test]
6503    fn orch6_discover_filters_by_harness_and_profile() {
6504        let mut params = hermes_query();
6505        params["profile"] = json!("coder");
6506        let result = hermes_discovery(params);
6507        let ids: Vec<&str> = result["sessions"]
6508            .as_array()
6509            .expect("sessions array")
6510            .iter()
6511            .map(|session| session["locator"]["session_id"].as_str().unwrap())
6512            .collect();
6513        assert_eq!(ids, vec!["tg-coder-1"]);
6514
6515        // A profile no session is routed through returns nothing rather than
6516        // silently ignoring the filter.
6517        let mut missing = hermes_query();
6518        missing["profile"] = json!("nobody");
6519        assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6520
6521        // The harness filter is `harnesses`; an id no harness answers to is
6522        // an empty page, never every store on the box.
6523        let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6524        assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6525    }
6526
6527    #[test]
6528    fn orch6_load_reports_the_same_nouns_as_discovery() {
6529        let mut service = HarnessSessionService::new();
6530        let loaded = service.handle(request(
6531            1,
6532            "harness.v1.sessions.load",
6533            json!({"locator": {
6534                "harness": "hermes",
6535                "session_id": "tg-coder-1",
6536                "storage": {"kind": "file", "path": hermes_store()},
6537            }}),
6538        ));
6539        let session = &loaded["result"]["session"];
6540        let discovered = hermes_discovery(hermes_query());
6541        let row = row(&discovered, "tg-coder-1");
6542        for noun in [
6543            "trigger",
6544            "surface",
6545            "profile",
6546            "recurrence",
6547            "cross_surface",
6548            "workspace",
6549        ] {
6550            assert_eq!(
6551                session[noun],
6552                row.get(noun).cloned().unwrap_or(Value::Null),
6553                "`{noun}` disagrees between sessions.load and sessions.discover"
6554            );
6555        }
6556    }
6557
6558    /// ORCH-10: the fixture homes, as the RPC's `homes` override. Hermes's
6559    /// home is named by its `state.db`; OpenClaw's is the state directory.
6560    fn profile_fixture_homes() -> Value {
6561        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6562        json!({
6563            "hermes": fixtures.join("hermes_home/state.db"),
6564            "openclaw": fixtures.join("openclaw_home"),
6565        })
6566    }
6567
6568    fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6569        response["result"]["profiles"]
6570            .as_array()
6571            .unwrap_or_else(|| panic!("no profiles array in {response}"))
6572            .iter()
6573            .find(|row| row["harness"] == harness && row["name"] == name)
6574            .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6575    }
6576
6577    /// dev/01: every source answers in one row shape, over the committed
6578    /// fixture homes — the Hermes profile directory and its `state.db`
6579    /// partition, the OpenClaw agent directories and `openclaw.json`, and
6580    /// supercode's own presets.
6581    #[test]
6582    fn profiles_list_reads_every_source_uniformly() {
6583        let mut service = HarnessSessionService::new();
6584        let response = service.handle(request(
6585            1,
6586            "harness.v1.profiles.list",
6587            json!({"homes": profile_fixture_homes()}),
6588        ));
6589        assert_eq!(
6590            response["result"]["schema"],
6591            crate::profiles::PROFILES_SCHEMA
6592        );
6593
6594        let default = profile_row(&response, "hermes", "default");
6595        assert_eq!(default["kind"], "hermes_profile");
6596        assert_eq!(default["default"], true);
6597        assert_eq!(default["routes"], 0);
6598        assert_eq!(default["sessions"], 11);
6599        assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6600
6601        let coder = profile_row(&response, "hermes", "coder");
6602        assert_eq!(coder["kind"], "hermes_profile");
6603        assert_eq!(coder["default"], false);
6604        assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6605        assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6606        assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6607        assert!(coder["home"]
6608            .as_str()
6609            .unwrap()
6610            .ends_with("hermes_home/profiles/coder"));
6611
6612        let main = profile_row(&response, "openclaw", "main");
6613        assert_eq!(main["kind"], "openclaw_agent");
6614        // No entry declares `default: true` (real configs do not), so `main`
6615        // wins on OpenClaw's own convention rather than alphabetically.
6616        assert_eq!(main["default"], true);
6617        assert_eq!(main["routes"], 0);
6618        assert_eq!(main["sessions"], 4);
6619        assert_eq!(
6620            main["model"],
6621            Value::Null,
6622            "`agents.defaults.model` is an install default, not this agent's pin"
6623        );
6624
6625        let design = profile_row(&response, "openclaw", "design");
6626        assert_eq!(design["default"], false);
6627        assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6628        assert_eq!(design["sessions"], 0);
6629        assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6630
6631        let preset = profile_row(&response, "supercode", "supercode-default");
6632        assert_eq!(preset["kind"], "preset");
6633        assert_eq!(preset["default"], true);
6634        assert_eq!(preset["home"], Value::Null);
6635        assert_eq!(preset["routes"], Value::Null);
6636    }
6637
6638    /// Codex's own profiles are `[profiles.<name>]` tables, with the
6639    /// top-level `profile` key naming the default.
6640    #[test]
6641    fn profiles_list_reads_codex_profile_tables() {
6642        let codex_home = std::env::temp_dir().join(format!(
6643            "supercode-orch10-codex-{}-{}",
6644            std::process::id(),
6645            std::time::SystemTime::now()
6646                .duration_since(std::time::UNIX_EPOCH)
6647                .unwrap()
6648                .as_nanos()
6649        ));
6650        std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6651        std::fs::write(
6652            codex_home.join("config.toml"),
6653            "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6654        )
6655        .unwrap();
6656
6657        let mut service = HarnessSessionService::new();
6658        let response = service.handle(request(
6659            1,
6660            "harness.v1.profiles.list",
6661            json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6662        ));
6663        let rows = response["result"]["profiles"].as_array().unwrap();
6664        assert_eq!(rows.len(), 2, "{response}");
6665        let review = profile_row(&response, "codex", "review");
6666        assert_eq!(review["kind"], "codex_profile");
6667        assert_eq!(review["default"], true);
6668        assert_eq!(review["model"], "gpt-5.1-codex");
6669        assert_eq!(review["home"], Value::Null);
6670        assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6671
6672        let got = service.handle(request(
6673            2,
6674            "harness.v1.profiles.get",
6675            json!({
6676                "harness": "codex",
6677                "name": "fast",
6678                "homes": {"codex": codex_home.join("sessions")},
6679            }),
6680        ));
6681        assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6682        std::fs::remove_dir_all(&codex_home).ok();
6683    }
6684
6685    /// A verb a harness lacks fails with `UnsupportedAction`, never a silent
6686    /// empty list; an unknown name is an invalid argument, not an empty row.
6687    #[test]
6688    fn profiles_refuse_harnesses_without_the_concept() {
6689        let mut service = HarnessSessionService::new();
6690        let response = service.handle(request(
6691            1,
6692            "harness.v1.profiles.list",
6693            json!({"harness": "claude-code"}),
6694        ));
6695        assert_eq!(response["error"]["code"], -32020, "{response}");
6696
6697        let missing = service.handle(request(
6698            2,
6699            "harness.v1.profiles.get",
6700            json!({
6701                "harness": "hermes",
6702                "name": "no-such-profile",
6703                "homes": profile_fixture_homes(),
6704            }),
6705        ));
6706        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6707    }
6708
6709    /// The two methods are advertised, so a client discovers them from
6710    /// `harness.v1.capabilities` rather than from documentation.
6711    #[test]
6712    fn profiles_methods_are_advertised() {
6713        let mut service = HarnessSessionService::new();
6714        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6715        let methods = response["result"]["methods"].as_array().unwrap();
6716        for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6717            assert!(
6718                methods.iter().any(|entry| entry == method),
6719                "{method} is not advertised"
6720            );
6721        }
6722    }
6723
6724    // -----------------------------------------------------------------
6725    // ORCH-14 — channels
6726    // -----------------------------------------------------------------
6727
6728    fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6729        response["result"]["channels"]
6730            .as_array()
6731            .unwrap_or_else(|| panic!("no channels array in {response}"))
6732            .iter()
6733            .find(|row| row["harness"] == harness && row["name"] == name)
6734            .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6735    }
6736
6737    fn channels_list(harness: Option<&str>) -> Value {
6738        let mut params = json!({"homes": profile_fixture_homes()});
6739        if let Some(harness) = harness {
6740            params["harness"] = json!(harness);
6741        }
6742        HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6743    }
6744
6745    /// dev/01: both sources answer in one row shape over the committed
6746    /// fixture homes — Hermes's `platforms:` blocks with their `extra` maps,
6747    /// and OpenClaw's `channels.<name>` entries split per account.
6748    #[test]
6749    fn channels_list_reads_both_gateway_harnesses_uniformly() {
6750        let response = channels_list(None);
6751        assert_eq!(
6752            response["result"]["schema"],
6753            crate::channels::CHANNELS_SCHEMA
6754        );
6755
6756        // Hermes: a credentialed platform, a bridged `extra.key` platform,
6757        // and one the config explicitly disables.
6758        let telegram = channel_row(&response, "hermes", "telegram");
6759        assert_eq!(telegram["kind"], "telegram");
6760        assert_eq!(telegram["enabled"], true);
6761        assert_eq!(telegram["configured"], true);
6762        // The `sessions` count is the discovery rows whose surface platform
6763        // is telegram: the fixture's `agent:main:telegram:…` DM and the
6764        // `agent:coder:telegram:…` group.
6765        assert_eq!(telegram["sessions"], 2);
6766        let api = channel_row(&response, "hermes", "api_server");
6767        assert_eq!(api["configured"], true, "extra.key is a credential key");
6768        assert_eq!(api["sessions"], 0);
6769        let webhook = channel_row(&response, "hermes", "webhook");
6770        assert_eq!(webhook["enabled"], false);
6771        // Hermes lists no credential for `webhook`: declaring it is all it
6772        // needs, so a credential-less entry is still `configured`.
6773        assert_eq!(webhook["configured"], true);
6774
6775        // OpenClaw: one row per account, named `<channel>/<accountId>`.
6776        let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6777        assert_eq!(linked["kind"], "slack");
6778        assert_eq!(linked["account"], "T0FIXTURE");
6779        assert_eq!(linked["enabled"], true);
6780        assert_eq!(linked["configured"], true);
6781        let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6782        assert_eq!(unlinked["enabled"], false);
6783        assert_eq!(
6784            unlinked["configured"], false,
6785            "an account with no credential key is not configured"
6786        );
6787        // A single-account channel keeps its own name and names its account
6788        // inline.
6789        let telegram = channel_row(&response, "openclaw", "telegram");
6790        assert_eq!(telegram["account"], "hermes-fixture-bot");
6791        assert_eq!(telegram["configured"], true);
6792
6793        // `status` is never claimed from a config file.
6794        for row in response["result"]["channels"].as_array().unwrap() {
6795            assert_eq!(row["status"], "unknown", "{row}");
6796        }
6797    }
6798
6799    /// dev/01: no field of any emitted row carries a credential. The fixture
6800    /// homes hold four FAKE credential strings; a row that leaked one — as a
6801    /// value, an account label, or a name — fails here.
6802    #[test]
6803    fn channels_rows_never_carry_a_fixture_secret() {
6804        let secrets = [
6805            "FAKE-TOKEN-DO-NOT-EMIT",
6806            "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6807            "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6808            "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6809            "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6810        ];
6811        // The strings really are in the fixtures, so this test can fail.
6812        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6813        let raw = format!(
6814            "{}{}",
6815            std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6816            std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6817        );
6818        for secret in secrets {
6819            assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6820        }
6821
6822        let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6823        for secret in secrets {
6824            assert!(
6825                !emitted.contains(secret),
6826                "`{secret}` leaked into a channel row: {emitted}"
6827            );
6828        }
6829        // Belt and braces: no row FIELD is credential-shaped either, so a
6830        // future field cannot smuggle one past the literal scan.
6831        for row in channels_list(None)["result"]["channels"]
6832            .as_array()
6833            .unwrap()
6834        {
6835            for key in row.as_object().unwrap().keys() {
6836                let key = key.to_ascii_lowercase();
6837                assert!(
6838                    !["token", "key", "secret", "password", "credential"]
6839                        .iter()
6840                        .any(|marker| key.ends_with(marker)),
6841                    "`{key}` is a credential-shaped field on a channel row"
6842                );
6843            }
6844        }
6845    }
6846
6847    /// `status` answers one row by name, and refuses an unknown one.
6848    #[test]
6849    fn channels_status_reads_one_row_by_name() {
6850        let mut service = HarnessSessionService::new();
6851        let got = service.handle(request(
6852            1,
6853            "harness.v1.channels.status",
6854            json!({
6855                "harness": "openclaw",
6856                "name": "slack/T0FIXTURE",
6857                "homes": profile_fixture_homes(),
6858            }),
6859        ));
6860        assert_eq!(got["result"]["channel"]["kind"], "slack");
6861        assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6862        assert_eq!(got["result"]["channel"]["status"], "unknown");
6863
6864        let missing = service.handle(request(
6865            2,
6866            "harness.v1.channels.status",
6867            json!({
6868                "harness": "openclaw",
6869                "name": "no-such-channel",
6870                "homes": profile_fixture_homes(),
6871            }),
6872        ));
6873        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6874    }
6875
6876    /// A harness with no channel concept fails with `UnsupportedAction`,
6877    /// never a silent empty list — Claude Code included, because its channels
6878    /// are MCP-protocol declarations no config file names.
6879    #[test]
6880    fn channels_refuse_harnesses_without_the_concept() {
6881        let response = channels_list(Some("claude-code"));
6882        assert_eq!(response["error"]["code"], -32020, "{response}");
6883        let codex = channels_list(Some("codex"));
6884        assert_eq!(codex["error"]["code"], -32020, "{codex}");
6885    }
6886
6887    /// The harness filter restricts the rows rather than being ignored.
6888    #[test]
6889    fn channels_list_filters_by_harness() {
6890        let response = channels_list(Some("openclaw"));
6891        let rows = response["result"]["channels"].as_array().unwrap();
6892        assert!(!rows.is_empty(), "{response}");
6893        assert!(
6894            rows.iter().all(|row| row["harness"] == "openclaw"),
6895            "harness filter leaked: {response}"
6896        );
6897    }
6898
6899    /// Both methods are advertised, so a client discovers them from
6900    /// `harness.v1.capabilities` rather than from documentation.
6901    #[test]
6902    fn channels_methods_are_advertised() {
6903        let mut service = HarnessSessionService::new();
6904        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6905        let methods = response["result"]["methods"].as_array().unwrap();
6906        for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6907            assert!(
6908                methods.iter().any(|entry| entry == method),
6909                "{method} is not advertised"
6910            );
6911        }
6912    }
6913
6914    /// The UI story renders REAL rows: this writes the discovery response the
6915    /// two assertions above pin into the fixture the Storybook
6916    /// `Compositions/Universal nouns` stories import, and fails when the
6917    /// committed copy has drifted from what the service now answers.
6918    #[test]
6919    fn orch6_story_fixture_matches_the_live_discovery_response() {
6920        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6921            .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6922        let mut result = hermes_discovery(hermes_query());
6923        // `updated_at_ms` is derived from the fixture's own stored timestamps,
6924        // so the whole response is deterministic; drop only the cursor, which
6925        // is pagination state rather than a session fact.
6926        result.as_object_mut().unwrap().remove("next_cursor");
6927        let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6928        if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6929            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6930            std::fs::write(&path, &rendered).unwrap();
6931        }
6932        let committed = std::fs::read_to_string(&path).unwrap_or_default();
6933        assert_eq!(
6934            committed, rendered,
6935            "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6936             re-run with SUPERCODE_UPDATE_FIXTURES=1"
6937        );
6938    }
6939
6940    fn pi_locator() -> SessionLocator {
6941        SessionLocator {
6942            harness: HarnessId::from(HarnessId::PI),
6943            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6944            storage: StorageLocator::File {
6945                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6946                    .join("tests/fixtures/pi_session.jsonl"),
6947            },
6948        }
6949    }
6950
6951    fn opencode_locator() -> SessionLocator {
6952        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
6953        SessionLocator {
6954            harness: HarnessId::from(HarnessId::OPENCODE),
6955            session_id: session_id.into(),
6956            storage: StorageLocator::Sqlite {
6957                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6958                    .join("tests/fixtures/opencode_fixture/opencode.db"),
6959                selector: session_id.into(),
6960            },
6961        }
6962    }
6963
6964    fn grok_locator() -> SessionLocator {
6965        SessionLocator {
6966            harness: HarnessId::from(HarnessId::GROK),
6967            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
6968            storage: StorageLocator::File {
6969                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6970                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
6971            },
6972        }
6973    }
6974
6975    // ---- ORCH-11: `harness.v1.skills.list` -------------------------------
6976
6977    fn fixture_homes() -> Value {
6978        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6979        json!({
6980            "claude_code": fixtures.join("__absent__"),
6981            "codex": fixtures.join("__absent__"),
6982            "opencode": fixtures.join("__absent__"),
6983            "pi": fixtures.join("__absent__"),
6984            "agents": fixtures.join("__absent__"),
6985            "hermes": fixtures.join("hermes_home"),
6986            "openclaw": fixtures.join("openclaw_home"),
6987        })
6988    }
6989
6990    #[test]
6991    fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
6992        let root = std::env::temp_dir().join(format!(
6993            "supercode-preview-rpc-{}-{}",
6994            std::process::id(),
6995            std::time::SystemTime::now()
6996                .duration_since(std::time::UNIX_EPOCH)
6997                .unwrap()
6998                .as_nanos()
6999        ));
7000        std::fs::create_dir_all(&root).unwrap();
7001        for id in ["first", "second"] {
7002            std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7003                json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7004                json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7005            )).unwrap();
7006        }
7007        let mut service = HarnessSessionService::new();
7008        let query = json!({
7009            "harnesses": ["codex"], "homes": {"codex": root},
7010            "query": "nebula", "search_previews": true, "limit": 1
7011        });
7012        let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7013        assert!(first.get("error").is_none(), "{first}");
7014        assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7015        assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7016        let mut next_query = query.clone();
7017        next_query["cursor"] = first["result"]["next_cursor"].clone();
7018        let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7019        assert_eq!(next["result"]["receipt"]["returned"], 1);
7020        assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7021        assert_eq!(next["result"]["receipt"]["truncated"], false);
7022        assert_ne!(
7023            first["result"]["sessions"][0]["locator"],
7024            next["result"]["sessions"][0]["locator"]
7025        );
7026        let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7027        assert!(
7028            refused["error"]["message"]
7029                .as_str()
7030                .unwrap()
7031                .contains("use sessions.discover"),
7032            "{refused}"
7033        );
7034        std::fs::remove_dir_all(root).unwrap();
7035    }
7036
7037    #[test]
7038    fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7039        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7040        let root = std::env::temp_dir().join(format!(
7041            "supercode-index-rpc-{}-{}",
7042            std::process::id(),
7043            std::time::SystemTime::now()
7044                .duration_since(std::time::UNIX_EPOCH)
7045                .unwrap()
7046                .as_nanos()
7047        ));
7048        std::fs::create_dir_all(&root).unwrap();
7049        for id in ["first", "second"] {
7050            std::fs::write(root.join(format!("{id}.jsonl")), format!(
7051                "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7052            )).unwrap();
7053        }
7054        let mut service = HarnessSessionService::new();
7055        let opened = service.handle(request(
7056            1,
7057            "harness.v1.sessions.index.subscribe",
7058            json!({
7059                "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7060            }),
7061        ));
7062        assert!(opened.get("error").is_none(), "{opened:#}");
7063        let subscription = opened["result"]["subscription"]
7064            .as_str()
7065            .unwrap()
7066            .to_owned();
7067        assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7068        for params in [
7069            json!({"subscription": subscription, "limit": 0}),
7070            json!({"subscription": subscription, "limit": 2049}),
7071            json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7072            json!({"subscription": "unknown", "limit": 2}),
7073        ] {
7074            let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7075            assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7076        }
7077        for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7078            let response = service.handle(request(
7079                3,
7080                "harness.v1.sessions.index.resize",
7081                json!({
7082                    "subscription": subscription, "limit": limit
7083                }),
7084            ));
7085            assert!(response.get("error").is_none(), "{response:#}");
7086            assert_eq!(response["result"]["subscription"], subscription);
7087            assert_eq!(response["result"]["revision"], revision);
7088            assert_eq!(
7089                response["result"]["initial"].as_array().unwrap().len(),
7090                limit
7091            );
7092            assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7093            assert_eq!(service.index_subscriptions.len(), 1);
7094        }
7095        let removed = service.handle(request(
7096            4,
7097            "harness.v1.sessions.index.unsubscribe",
7098            json!({
7099                "subscription": subscription
7100            }),
7101        ));
7102        assert_eq!(removed["result"]["removed"], true);
7103        let stale = service.handle(request(
7104            5,
7105            "harness.v1.sessions.index.resize",
7106            json!({
7107                "subscription": subscription, "limit": 1
7108            }),
7109        ));
7110        assert_eq!(stale["error"]["code"], -32602);
7111        drop(service);
7112        std::fs::remove_dir_all(root).unwrap();
7113    }
7114
7115    fn skills_rows(params: Value) -> Vec<Value> {
7116        let response =
7117            HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7118        assert!(response.get("error").is_none(), "{response:#}");
7119        response["result"].as_array().cloned().unwrap_or_default()
7120    }
7121
7122    /// The uniform row over two harnesses at once, from the harnesses' own
7123    /// skill roots: name, harness, scope, location, description, version.
7124    #[test]
7125    fn skills_list_reads_the_hermes_and_openclaw_roots() {
7126        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7127        let rows = skills_rows(json!({
7128            "homes": fixture_homes(),
7129            "cwd": fixtures.join("hermes_home"),
7130        }));
7131        let arxiv = rows
7132            .iter()
7133            .find(|row| row["name"] == json!("arxiv-search"))
7134            .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7135        assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7136        assert_eq!(arxiv["scope"], json!("user"));
7137        assert_eq!(arxiv["version"], json!("1.4.0"));
7138        assert!(arxiv["location"]
7139            .as_str()
7140            .unwrap()
7141            .ends_with("hermes_home/skills/research/arxiv"));
7142
7143        // A directory with no SKILL.md still lists, by directory name.
7144        let bare = rows
7145            .iter()
7146            .find(|row| row["name"] == json!("bare-skill"))
7147            .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7148        assert_eq!(bare["enabled"], json!(null));
7149        assert!(bare.get("description").is_none());
7150
7151        let demo = rows
7152            .iter()
7153            .find(|row| row["name"] == json!("clawhub-demo"))
7154            .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7155        assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7156        assert_eq!(demo["scope"], json!("managed"));
7157        assert_eq!(demo["enabled"], json!(false));
7158    }
7159
7160    /// Both filters select against the same rows.
7161    #[test]
7162    fn skills_list_filters_by_harness_and_scope() {
7163        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7164        let hermes = skills_rows(json!({
7165            "homes": fixture_homes(),
7166            "cwd": fixtures.join("hermes_home"),
7167            "harness": HarnessId::HERMES,
7168        }));
7169        assert!(!hermes.is_empty());
7170        assert!(hermes
7171            .iter()
7172            .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7173
7174        let managed = skills_rows(json!({
7175            "homes": fixture_homes(),
7176            "cwd": fixtures.join("openclaw_home"),
7177            "harness": HarnessId::OPENCLAW,
7178            "scope": "managed",
7179        }));
7180        assert_eq!(managed.len(), 1, "{managed:#?}");
7181        assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7182
7183        let bundled = skills_rows(json!({
7184            "homes": fixture_homes(),
7185            "cwd": fixtures.join("openclaw_home"),
7186            "harness": HarnessId::OPENCLAW,
7187            "scope": "bundled",
7188        }));
7189        assert!(bundled.is_empty(), "{bundled:#?}");
7190    }
7191
7192    /// A harness supercode has no skills root for is refused by name, not
7193    /// answered with an empty list.
7194    #[test]
7195    fn skills_list_refuses_an_unknown_harness() {
7196        let response = HarnessSessionService::new().handle(request(
7197            1,
7198            "harness.v1.skills.list",
7199            json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7200        ));
7201        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7202        assert!(response["error"]["message"]
7203            .as_str()
7204            .unwrap()
7205            .contains("not-a-harness"));
7206    }
7207
7208    /// The method is advertised, and its SDK operation resolves it.
7209    #[test]
7210    fn skills_list_is_an_advertised_method_and_sdk_operation() {
7211        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7212        assert_eq!(
7213            SdkOperation::from_method("harness.v1.skills.list"),
7214            Some(SdkOperation::SkillsList)
7215        );
7216    }
7217
7218    // ---- ORCH-22: `harness.v1.skills.install|remove` ----------------------
7219
7220    /// Both controlled verbs are advertised and resolve to their operation.
7221    #[test]
7222    fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7223        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7224        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7225        assert_eq!(
7226            SdkOperation::from_method("harness.v1.skills.install"),
7227            Some(SdkOperation::SkillsInstall)
7228        );
7229        assert_eq!(
7230            SdkOperation::from_method("harness.v1.skills.remove"),
7231            Some(SdkOperation::SkillsRemove)
7232        );
7233    }
7234
7235    /// The directory door, end to end over the RPC: a local package lands in
7236    /// Claude Code's own user root and the outcome carries the operation and
7237    /// the row the ORCH-11 loader reads back.
7238    #[test]
7239    fn skills_install_and_remove_drive_the_directory_door() {
7240        let root = std::env::temp_dir().join(format!(
7241            "supercode-orch22-rpc-{}-{}",
7242            std::process::id(),
7243            std::time::SystemTime::now()
7244                .duration_since(std::time::UNIX_EPOCH)
7245                .unwrap()
7246                .as_nanos()
7247        ));
7248        let source = root.join("probe-src");
7249        std::fs::create_dir_all(&source).unwrap();
7250        std::fs::write(
7251            source.join("SKILL.md"),
7252            "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7253        )
7254        .unwrap();
7255        let homes = json!({
7256            "claude_code": root.join("claude_home"),
7257            "codex": root.join("__absent__"),
7258            "opencode": root.join("__absent__"),
7259            "pi": root.join("__absent__"),
7260            "hermes": root.join("__absent__"),
7261            "openclaw": root.join("__absent__"),
7262            "agents": root.join("__absent__"),
7263        });
7264
7265        let mut service = HarnessSessionService::new();
7266        let installed = service.handle(request(
7267            1,
7268            "harness.v1.skills.install",
7269            json!({
7270                "harness": HarnessId::CLAUDE_CODE,
7271                "source": source,
7272                "scope": "user",
7273                "cwd": root,
7274                "homes": homes,
7275            }),
7276        ));
7277        let result = &installed["result"];
7278        assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7279        assert_eq!(result["verb"], json!("install"));
7280        assert!(result["ran"]
7281            .as_str()
7282            .is_some_and(|ran| ran.starts_with("cp -R ")));
7283        assert_eq!(result["skill"]["scope"], json!("user"));
7284
7285        let removed = service.handle(request(
7286            2,
7287            "harness.v1.skills.remove",
7288            json!({
7289                "harness": HarnessId::CLAUDE_CODE,
7290                "name": "orch22-rpc",
7291                "scope": "user",
7292                "cwd": root,
7293                "homes": homes,
7294            }),
7295        ));
7296        assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7297        assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7298        std::fs::remove_dir_all(&root).ok();
7299    }
7300
7301    /// OpenClaw publishes no `skills remove` at the pin, so the uniform verb
7302    /// refuses with UnsupportedAction instead of deleting files itself.
7303    #[test]
7304    fn skills_remove_refuses_openclaw_at_the_pin() {
7305        let response = HarnessSessionService::new().handle(request(
7306            1,
7307            "harness.v1.skills.remove",
7308            json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7309        ));
7310        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7311        assert!(response["error"]["message"]
7312            .as_str()
7313            .unwrap()
7314            .contains("no `skills remove` verb"));
7315    }
7316
7317    /// A harness with no skills root at all is refused by name, with the
7318    /// same sentence `skills.list` gives it.
7319    #[test]
7320    fn skills_install_refuses_a_harness_without_a_skills_root() {
7321        let response = HarnessSessionService::new().handle(request(
7322            1,
7323            "harness.v1.skills.install",
7324            json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7325        ));
7326        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7327        assert!(response["error"]["message"]
7328            .as_str()
7329            .unwrap()
7330            .contains("not-a-harness"));
7331    }
7332
7333    // ---- ORCH-12: `harness.v1.memory.show|search` ------------------------
7334
7335    /// `HarnessHomes` for the committed fixture homes. Every root a test does
7336    /// not name is pinned at an absent path, so a read can never fall through
7337    /// to this machine's real harness homes. Note `hermes` is the `state.db`
7338    /// PATH (its parent is HERMES_HOME) and `claude_code` is the `projects`
7339    /// directory — the same contract discovery uses.
7340    fn memory_homes() -> Value {
7341        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7342        json!({
7343            "claude_code": fixtures.join("__absent__"),
7344            "codex": fixtures.join("__absent__"),
7345            "opencode": fixtures.join("__absent__"),
7346            "pi": fixtures.join("__absent__"),
7347            "grok": fixtures.join("__absent__"),
7348            "gemini": fixtures.join("__absent__"),
7349            "goose": fixtures.join("__absent__"),
7350            "supercode": fixtures.join("__absent__"),
7351            "hermes": fixtures.join("hermes_home/state.db"),
7352            "openclaw": fixtures.join("openclaw_home"),
7353        })
7354    }
7355
7356    fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7357        let response = HarnessSessionService::new().handle(request(1, method, params));
7358        assert!(response.get("error").is_none(), "{response:#}");
7359        assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7360        response["result"][key]
7361            .as_array()
7362            .cloned()
7363            .unwrap_or_default()
7364    }
7365
7366    fn memory_documents(params: Value) -> Vec<Value> {
7367        memory_call_ok("harness.v1.memory.show", params, "documents")
7368    }
7369
7370    fn memory_matches(params: Value) -> Vec<Value> {
7371        memory_call_ok("harness.v1.memory.search", params, "matches")
7372    }
7373
7374    fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7375        rows.iter()
7376            .find(|row| row["profile"] == profile && row["name"] == name)
7377            .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7378    }
7379
7380    /// Hermes: the built-in `MEMORY.md`/`USER.md` pair and the `memories/`
7381    /// topic files, for HERMES_HOME itself and for every profile home.
7382    #[test]
7383    fn memory_show_reads_the_hermes_profile_homes() {
7384        let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7385
7386        let notes = find_document(&rows, "default", "MEMORY.md");
7387        assert_eq!(notes["harness"], "hermes");
7388        assert_eq!(notes["scope"], "user");
7389        assert!(notes["size"].as_u64().unwrap() > 0);
7390        assert!(notes["updated_at"].is_string(), "{notes:#?}");
7391        // The default answer previews the head and never the whole body.
7392        assert!(notes.get("content").is_none(), "{notes:#?}");
7393        assert_eq!(notes["truncated"], true);
7394        assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7395
7396        let user = find_document(&rows, "default", "USER.md");
7397        assert_eq!(user["scope"], "user");
7398        assert!(user["preview"]
7399            .as_array()
7400            .unwrap()
7401            .iter()
7402            .any(|line| line.as_str().unwrap().contains("neovim")));
7403
7404        let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7405        assert!(topic["path"]
7406            .as_str()
7407            .unwrap()
7408            .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7409
7410        // Profile mode points HERMES_HOME at `<root>/profiles/<name>`.
7411        let coder = find_document(&rows, "coder", "MEMORY.md");
7412        assert_eq!(coder["scope"], "profile");
7413        assert!(coder["path"]
7414            .as_str()
7415            .unwrap()
7416            .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7417    }
7418
7419    /// `full` is the only way a body crosses the wire, and `profile` narrows
7420    /// the read to one home.
7421    #[test]
7422    fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7423        let rows = memory_documents(json!({
7424            "harness": "hermes",
7425            "profile": "coder",
7426            "full": true,
7427            "homes": memory_homes(),
7428        }));
7429        assert!(
7430            rows.iter().all(|row| row["profile"] == "coder"),
7431            "{rows:#?}"
7432        );
7433        let coder = find_document(&rows, "coder", "MEMORY.md");
7434        assert!(coder["content"]
7435            .as_str()
7436            .expect("full returns the body")
7437            .contains("anthropic/claude-opus-4-8"));
7438    }
7439
7440    /// OpenClaw: memory-core's files under each agent's workspace —
7441    /// `<state>/workspace` for the default agent, `<state>/workspace-<id>`
7442    /// for any other.
7443    #[test]
7444    fn memory_show_reads_the_openclaw_agent_workspaces() {
7445        let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7446
7447        let main = find_document(&rows, "main", "MEMORY.md");
7448        assert_eq!(main["scope"], "agent");
7449        assert!(main["path"]
7450            .as_str()
7451            .unwrap()
7452            .ends_with("openclaw_home/workspace/MEMORY.md"));
7453
7454        let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7455        assert!(topic["path"]
7456            .as_str()
7457            .unwrap()
7458            .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7459
7460        let design = find_document(&rows, "design", "MEMORY.md");
7461        assert!(design["path"]
7462            .as_str()
7463            .unwrap()
7464            .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7465    }
7466
7467    /// Claude Code: the auto-memory directory of the project the working tree
7468    /// belongs to, keyed by the enclosing git repository.
7469    #[test]
7470    fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7471        let scratch = std::env::temp_dir().join(format!(
7472            "supercode-orch12-cc-{}-{}",
7473            std::process::id(),
7474            std::time::SystemTime::now()
7475                .duration_since(std::time::UNIX_EPOCH)
7476                .unwrap()
7477                .as_nanos()
7478        ));
7479        let project = scratch.join("repo");
7480        std::fs::create_dir_all(project.join(".git")).unwrap();
7481        // Auto-memory is shared across a repo's worktrees, so a nested
7482        // working directory must resolve to the repo's own project dir.
7483        let worktree = project.join("crates/harness");
7484        std::fs::create_dir_all(&worktree).unwrap();
7485        let slug: String = project
7486            .to_string_lossy()
7487            .chars()
7488            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7489            .collect();
7490        let projects = scratch.join("claude/projects");
7491        let memory = projects.join(&slug).join("memory");
7492        std::fs::create_dir_all(&memory).unwrap();
7493        std::fs::write(
7494            memory.join("MEMORY.md"),
7495            "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7496        )
7497        .unwrap();
7498        std::fs::write(
7499            memory.join("build-box.md"),
7500            "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7501        )
7502        .unwrap();
7503
7504        let mut homes = memory_homes();
7505        homes["claude_code"] = json!(projects);
7506        let rows = memory_documents(json!({
7507            "harness": "claude-code",
7508            "cwd": worktree,
7509            "homes": homes,
7510        }));
7511        let index = find_document(&rows, &slug, "MEMORY.md");
7512        assert_eq!(index["harness"], "claude-code");
7513        assert_eq!(index["scope"], "project");
7514        let topic = find_document(&rows, &slug, "build-box.md");
7515        assert!(topic["preview"]
7516            .as_array()
7517            .unwrap()
7518            .iter()
7519            .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7520
7521        let hits = memory_matches(json!({
7522            "harness": "claude-code",
7523            "query": "pinned harnesses",
7524            "cwd": worktree,
7525            "homes": homes,
7526        }));
7527        assert_eq!(hits.len(), 1, "{hits:#?}");
7528        assert_eq!(hits[0]["name"], "MEMORY.md");
7529        assert_eq!(hits[0]["line"], 2);
7530
7531        let _ = std::fs::remove_dir_all(&scratch);
7532    }
7533
7534    /// A config-less OpenClaw install declares no default agent, but
7535    /// memory-core still resolves ONE agent to the default `workspace`
7536    /// directory — the same `main`-then-first convention the profile rows
7537    /// use. Measured against `openclaw memory status` on the pinned CLI
7538    /// (`docs/interop/research/orch12-memory-receipt-2026-09-03.json`).
7539    #[test]
7540    fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7541        let state = std::env::temp_dir().join(format!(
7542            "supercode-orch12-oc-{}-{}",
7543            std::process::id(),
7544            std::time::SystemTime::now()
7545                .duration_since(std::time::UNIX_EPOCH)
7546                .unwrap()
7547                .as_nanos()
7548        ));
7549        // No `openclaw.json`: only the agent home the gateway creates.
7550        std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7551        std::fs::create_dir_all(state.join("workspace")).unwrap();
7552        std::fs::write(
7553            state.join("workspace/MEMORY.md"),
7554            "the gateway websocket needs credentials\n",
7555        )
7556        .unwrap();
7557
7558        let mut homes = memory_homes();
7559        homes["openclaw"] = json!(state);
7560        let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7561        assert_eq!(rows.len(), 1, "{rows:#?}");
7562        let row = find_document(&rows, "main", "MEMORY.md");
7563        assert_eq!(row["scope"], "agent");
7564        assert!(row["path"]
7565            .as_str()
7566            .unwrap()
7567            .ends_with("workspace/MEMORY.md"));
7568
7569        let _ = std::fs::remove_dir_all(&state);
7570    }
7571
7572    /// Search is a plain scan over the same documents: a hit carries the
7573    /// path, line and excerpt; a miss is an empty list, not an error.
7574    #[test]
7575    fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7576        let hit = memory_matches(json!({
7577            "harness": "hermes",
7578            "query": "NEOVIM",
7579            "homes": memory_homes(),
7580        }));
7581        assert_eq!(hit.len(), 1, "{hit:#?}");
7582        assert_eq!(hit[0]["harness"], "hermes");
7583        assert_eq!(hit[0]["name"], "USER.md");
7584        assert_eq!(hit[0]["scope"], "user");
7585        assert_eq!(hit[0]["line"], 5);
7586        assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7587
7588        // A regular expression reaches the same lines.
7589        let regex = memory_matches(json!({
7590            "harness": "hermes",
7591            "query": "neo(vim|vi)",
7592            "regex": true,
7593            "homes": memory_homes(),
7594        }));
7595        assert_eq!(regex.len(), 1, "{regex:#?}");
7596
7597        let miss = memory_matches(json!({
7598            "harness": "hermes",
7599            "query": "no-memory-line-says-this",
7600            "homes": memory_homes(),
7601        }));
7602        assert!(miss.is_empty(), "{miss:#?}");
7603    }
7604
7605    /// The uniform-verb contract: a harness with no memory store at the pin
7606    /// is refused by name, and `session` only selects a Claude Code project.
7607    #[test]
7608    fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7609        for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7610            let response = HarnessSessionService::new().handle(request(
7611                1,
7612                method,
7613                json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7614            ));
7615            assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7616            assert!(response["error"]["message"]
7617                .as_str()
7618                .unwrap()
7619                .contains("codex"));
7620        }
7621
7622        let response = HarnessSessionService::new().handle(request(
7623            1,
7624            "harness.v1.memory.show",
7625            json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7626        ));
7627        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7628
7629        // `harness` is not optional: memory documents are the user's prose.
7630        let response = HarnessSessionService::new().handle(request(
7631            1,
7632            "harness.v1.memory.show",
7633            json!({"homes": memory_homes()}),
7634        ));
7635        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7636    }
7637
7638    /// Both methods are advertised, and their SDK operations resolve them.
7639    #[test]
7640    fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7641        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7642        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7643        assert_eq!(
7644            SdkOperation::from_method("harness.v1.memory.show"),
7645            Some(SdkOperation::MemoryShow)
7646        );
7647        assert_eq!(
7648            SdkOperation::from_method("harness.v1.memory.search"),
7649            Some(SdkOperation::MemorySearch)
7650        );
7651    }
7652
7653    // ---- ORCH-9: `harness.v1.approvals.list` -----------------------------
7654
7655    /// A runtime that raises one protocol request and then goes quiet, so a
7656    /// single poll delivers the request without closing the connection.
7657    struct RequestingRuntime {
7658        handle: RuntimeHandle,
7659        events: std::collections::VecDeque<HarnessEvent>,
7660        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7661    }
7662
7663    #[async_trait]
7664    impl RuntimeConnection for RequestingRuntime {
7665        fn handle(&self) -> &RuntimeHandle {
7666            &self.handle
7667        }
7668
7669        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7670            unreachable!("this runtime only raises requests")
7671        }
7672
7673        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7674            match self.events.pop_front() {
7675                Some(event) => Ok(Some(event)),
7676                // Quiet, not closed: `poll_sdk_events` times out and leaves
7677                // the connection open, the way a runtime blocked on a
7678                // permission request behaves.
7679                None => std::future::pending().await,
7680            }
7681        }
7682
7683        async fn interrupt(&mut self) -> crate::Result<()> {
7684            Ok(())
7685        }
7686
7687        async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7688            // Both halves are recorded: ORCH-20 has to prove not just that the
7689            // right request was answered but that the door received its own
7690            // reply envelope.
7691            self.answered
7692                .lock()
7693                .unwrap_or_else(std::sync::PoisonError::into_inner)
7694                .push(json!({"request_id": request_id, "response": response}));
7695            Ok(())
7696        }
7697
7698        async fn close(&mut self) -> crate::Result<()> {
7699            Ok(())
7700        }
7701    }
7702
7703    fn requesting_runtime(
7704        harness: &str,
7705        events: Vec<HarnessEvent>,
7706        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7707    ) -> Box<dyn RuntimeConnection> {
7708        requesting_runtime_named(harness, "hermes-live-session", events, answered)
7709    }
7710
7711    fn requesting_runtime_named(
7712        harness: &str,
7713        runtime_id: &str,
7714        events: Vec<HarnessEvent>,
7715        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7716    ) -> Box<dyn RuntimeConnection> {
7717        Box::new(RequestingRuntime {
7718            handle: RuntimeHandle {
7719                harness: HarnessId::from(harness),
7720                runtime_id: runtime_id.into(),
7721                endpoint: RuntimeEndpoint::LocalProcess {
7722                    pid: None,
7723                    command: vec!["hermes-acp".into()],
7724                    protocol: "acp".into(),
7725                },
7726            },
7727            events: events.into(),
7728            answered,
7729        })
7730    }
7731
7732    fn permission_event(id: u64, title: &str) -> HarnessEvent {
7733        HarnessEvent {
7734            sequence: None,
7735            kind: "session/request_permission".into(),
7736            payload: json!({
7737                "jsonrpc": "2.0",
7738                "id": id,
7739                "method": "session/request_permission",
7740                "params": {
7741                    "sessionId": "hermes-live-session",
7742                    "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7743                    "options": [
7744                        {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7745                        {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7746                        {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7747                    ],
7748                },
7749            }),
7750        }
7751    }
7752
7753    fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7754        let response = service.handle(request(1, "harness.v1.approvals.list", params));
7755        assert!(response.get("error").is_none(), "{response:#}");
7756        response["result"].clone()
7757    }
7758
7759    /// ORC-2 dev/01: the same uniform loop over the CLAUDE CODE door. The
7760    /// `can_use_tool` control request the CLI raises to its registered
7761    /// permission handler lists as one pending row, `approvals.resolve <id>
7762    /// allow_once` sends the `{behavior}` result the CLI accepts through
7763    /// `runtimes.respond`, and the row is gone. The frame is the one claude
7764    /// 2.1.258 wrote, transcribed from
7765    /// `docs/interop/research/orc2-claude-respond-receipt-2026-09-04.json`.
7766    #[tokio::test]
7767    async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7768        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7769        let mut service = HarnessSessionService::new();
7770        service.runtimes.insert(
7771            "runtime-cc".into(),
7772            requesting_runtime_named(
7773                HarnessId::CLAUDE_CODE,
7774                "claude-live-session",
7775                vec![HarnessEvent {
7776                    sequence: None,
7777                    kind: "control_request".into(),
7778                    payload: json!({
7779                        "type": "control_request",
7780                        "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7781                        "request": {
7782                            "subtype": "can_use_tool",
7783                            "tool_name": "Bash",
7784                            "display_name": "Bash",
7785                            "input": {"command": "touch probe-artifact.txt"},
7786                            "tool_use_id": "toolu_mock_1",
7787                        },
7788                    }),
7789                }],
7790                answered.clone(),
7791            ),
7792        );
7793
7794        let notifications = service.poll_runtimes().await;
7795        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7796
7797        let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7798        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7799        let row = &rows[0];
7800        assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7801        assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7802        assert_eq!(row["status"], "pending");
7803        assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7804        assert_eq!(row["runtime_id"], "claude-live-session");
7805        assert_eq!(
7806            row["options"]
7807                .as_array()
7808                .unwrap()
7809                .iter()
7810                .map(|option| option["id"].as_str().unwrap())
7811                .collect::<Vec<_>>(),
7812            vec!["allow", "deny"],
7813        );
7814
7815        let response = resolve(
7816            &mut service,
7817            json!({"id": row["id"], "decision": "allow_once"}),
7818        )
7819        .await;
7820        assert!(response.get("error").is_none(), "{response:#}");
7821        assert_eq!(response["result"]["option_id"], "allow");
7822        assert_eq!(
7823            answered
7824                .lock()
7825                .unwrap_or_else(std::sync::PoisonError::into_inner)
7826                .as_slice(),
7827            &[json!({
7828                "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7829                "response": {"behavior": "allow"},
7830            })],
7831        );
7832        assert_eq!(
7833            approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7834                .as_array()
7835                .map(Vec::len),
7836            Some(0),
7837        );
7838    }
7839
7840    /// dev/01: a live ACP permission request raised on a driven runtime is
7841    /// listable while the turn is blocked on it, and stops being listable
7842    /// the moment `runtimes.respond` answers it.
7843    #[tokio::test]
7844    async fn a_live_permission_request_lists_until_it_is_answered() {
7845        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7846        let mut service = HarnessSessionService::new();
7847        service.runtimes.insert(
7848            "runtime-1".into(),
7849            requesting_runtime(
7850                HarnessId::HERMES,
7851                vec![permission_event(7, "rm -rf build")],
7852                answered.clone(),
7853            ),
7854        );
7855
7856        let notifications = service.poll_runtimes().await;
7857        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7858
7859        let rows = approvals(&mut service, json!({}));
7860        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7861        let row = &rows[0];
7862        assert_eq!(row["id"], "runtime-1/7");
7863        assert_eq!(row["harness"], HarnessId::HERMES);
7864        assert_eq!(row["kind"], "live");
7865        assert_eq!(row["status"], "pending");
7866        assert_eq!(row["subject"], "rm -rf build");
7867        assert_eq!(row["session_id"], "hermes-live-session");
7868        assert_eq!(row["runtime_id"], "hermes-live-session");
7869        assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7870        assert!(
7871            row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7872            "{row:#}"
7873        );
7874        assert_eq!(
7875            row["options"]
7876                .as_array()
7877                .unwrap()
7878                .iter()
7879                .map(|option| option["id"].as_str().unwrap())
7880                .collect::<Vec<_>>(),
7881            vec!["allow_once", "allow_for_session", "deny"],
7882        );
7883
7884        // The filters select against the same rows.
7885        assert_eq!(
7886            approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7887                .as_array()
7888                .map(Vec::len),
7889            Some(1),
7890        );
7891        assert_eq!(
7892            approvals(&mut service, json!({"session": "some-other-session"}))
7893                .as_array()
7894                .map(Vec::len),
7895            Some(0),
7896        );
7897
7898        let response = service
7899            .handle_async(request(
7900                2,
7901                "harness.v1.runtimes.respond",
7902                json!({
7903                    "connection": "runtime-1",
7904                    "request_id": 7,
7905                    "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7906                }),
7907            ))
7908            .await;
7909        assert!(response.get("error").is_none(), "{response:#}");
7910        assert_eq!(
7911            answered
7912                .lock()
7913                .unwrap_or_else(std::sync::PoisonError::into_inner)
7914                .as_slice(),
7915            &[json!({
7916                "request_id": 7,
7917                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7918            })],
7919        );
7920
7921        let rows = approvals(&mut service, json!({}));
7922        assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7923    }
7924
7925    /// dev/01: supercode's own queued subagent approvals list through the
7926    /// same door, carrying the outcome the record holds.
7927    #[test]
7928    fn queued_subagent_approvals_list_through_the_same_door() {
7929        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7930            crate::subagents::QueuedApproval {
7931                child_agent_id: "child-7".into(),
7932                tool: "shell".into(),
7933                subject: Some("cargo publish --dry-run".into()),
7934                queued_at_ms: 1,
7935                outcome: None,
7936            },
7937            crate::subagents::QueuedApproval {
7938                child_agent_id: "child-8".into(),
7939                tool: "write_file".into(),
7940                subject: None,
7941                queued_at_ms: 2,
7942                outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7943            },
7944        ]));
7945        let mut service = HarnessSessionService::new();
7946        service.observe_subagent_approvals(queue);
7947
7948        let rows = approvals(&mut service, json!({}));
7949        assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
7950        assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
7951        assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
7952        assert_eq!(rows[0]["status"], "pending");
7953        assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
7954        assert_eq!(rows[1]["status"], "denied");
7955        assert!(rows[1]["options"].as_array().unwrap().is_empty());
7956
7957        // `--session` addresses a subagent row by its child agent id.
7958        let only = approvals(&mut service, json!({"session": "child-8"}));
7959        assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
7960        assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
7961    }
7962
7963    /// The uniform-verb contract: an id whose runtime door cannot carry a
7964    /// protocol request is refused BY NAME rather than answered with an empty
7965    /// list. Since ORC-2 gave Claude Code a permission-response primitive
7966    /// every registered harness can carry one, so the refusal is exercised on
7967    /// an unknown id — and the registered ids are asserted to be accepted.
7968    #[test]
7969    fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
7970        let response = HarnessSessionService::new().handle(request(
7971            1,
7972            "harness.v1.approvals.list",
7973            json!({"harness": "not-a-harness"}),
7974        ));
7975        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7976        assert!(response["error"]["message"]
7977            .as_str()
7978            .unwrap()
7979            .contains("not-a-harness"));
7980        for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
7981            let response = HarnessSessionService::new().handle(request(
7982                1,
7983                "harness.v1.approvals.list",
7984                json!({"harness": harness}),
7985            ));
7986            assert!(response.get("error").is_none(), "{harness}: {response:#}");
7987        }
7988    }
7989
7990    /// The method is advertised, its SDK operation resolves it, and the
7991    /// registry reports the concept as observed for every harness whose
7992    /// runtime door can carry a request.
7993    #[test]
7994    fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
7995        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
7996        assert_eq!(
7997            SdkOperation::from_method("harness.v1.approvals.list"),
7998            Some(SdkOperation::ApprovalsList)
7999        );
8000        let registry = harness_support_registry();
8001        for id in [
8002            HarnessId::HERMES,
8003            HarnessId::OPENCLAW,
8004            HarnessId::CODEX,
8005            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8006            // pending_request concept joins the other driven doors.
8007            HarnessId::CLAUDE_CODE,
8008        ] {
8009            let concept = registry
8010                .harnesses
8011                .iter()
8012                .find(|harness| harness.id.as_str() == id)
8013                .unwrap()
8014                .orchestration
8015                .concepts
8016                .iter()
8017                .find(|concept| concept.concept == "pending_request")
8018                .unwrap();
8019            assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8020            assert!(concept
8021                .methods
8022                .iter()
8023                .any(|method| method == "harness.v1.approvals.list"));
8024        }
8025    }
8026
8027    // ---- ORCH-20: `harness.v1.approvals.resolve` -------------------------
8028
8029    async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8030        service
8031            .handle_async(request(3, "harness.v1.approvals.resolve", params))
8032            .await
8033    }
8034
8035    /// dev/01: the whole loop on a driven runtime — list one pending row,
8036    /// answer it by ROW ID with one uniform decision, and see it gone. The
8037    /// door receives its own ACP envelope carrying the option it enumerated.
8038    #[tokio::test]
8039    async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8040        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8041        let mut service = HarnessSessionService::new();
8042        service.runtimes.insert(
8043            "runtime-1".into(),
8044            requesting_runtime(
8045                HarnessId::HERMES,
8046                vec![permission_event(7, "rm -rf build")],
8047                answered.clone(),
8048            ),
8049        );
8050        service.poll_runtimes().await;
8051
8052        let rows = approvals(&mut service, json!({}));
8053        assert_eq!(rows[0]["id"], "runtime-1/7");
8054
8055        let response = resolve(
8056            &mut service,
8057            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8058        )
8059        .await;
8060        assert!(response.get("error").is_none(), "{response:#}");
8061        assert_eq!(
8062            response["result"],
8063            json!({
8064                "id": "runtime-1/7",
8065                "decision": "allow_once",
8066                "option_id": "allow_once",
8067                "resolved": true,
8068            }),
8069        );
8070        // The harness's own door was called with its own envelope.
8071        assert_eq!(
8072            answered
8073                .lock()
8074                .unwrap_or_else(std::sync::PoisonError::into_inner)
8075                .as_slice(),
8076            &[json!({
8077                "request_id": 7,
8078                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8079            })],
8080        );
8081        // And the row is gone, the same way `runtimes.respond` drops it.
8082        assert_eq!(
8083            approvals(&mut service, json!({})).as_array().map(Vec::len),
8084            Some(0),
8085        );
8086        // Answering it twice is an honest miss, not a silent success.
8087        let response = resolve(
8088            &mut service,
8089            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8090        )
8091        .await;
8092        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8093    }
8094
8095    /// dev/01: deny travels the same path and picks the option the request
8096    /// itself classified as a refusal.
8097    #[tokio::test]
8098    async fn deny_selects_the_requests_own_reject_option() {
8099        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8100        let mut service = HarnessSessionService::new();
8101        service.runtimes.insert(
8102            "runtime-1".into(),
8103            requesting_runtime(
8104                HarnessId::HERMES,
8105                vec![permission_event(11, "git push --force")],
8106                answered.clone(),
8107            ),
8108        );
8109        service.poll_runtimes().await;
8110
8111        let response = resolve(
8112            &mut service,
8113            json!({"id": "runtime-1/11", "decision": "deny"}),
8114        )
8115        .await;
8116        assert!(response.get("error").is_none(), "{response:#}");
8117        // `deny` is the optionId whose ACP `kind` is `reject_once`.
8118        assert_eq!(response["result"]["option_id"], "deny");
8119        assert_eq!(
8120            answered
8121                .lock()
8122                .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8123            json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8124        );
8125        assert_eq!(
8126            approvals(&mut service, json!({})).as_array().map(Vec::len),
8127            Some(0),
8128        );
8129    }
8130
8131    /// dev/01: a decision this request does not offer is refused by name,
8132    /// listing the ones it does — never silently downgraded to a neighbour.
8133    #[tokio::test]
8134    async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8135        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8136        let mut service = HarnessSessionService::new();
8137        let mut event = permission_event(3, "rm -rf build");
8138        // A request offering only allow-once and deny, as hermes 0.21.0's
8139        // edit-approval layer raises one.
8140        event.payload["params"]["options"] = json!([
8141            {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8142            {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8143        ]);
8144        service.runtimes.insert(
8145            "runtime-1".into(),
8146            requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8147        );
8148        service.poll_runtimes().await;
8149
8150        let response = resolve(
8151            &mut service,
8152            json!({"id": "runtime-1/3", "decision": "allow_always"}),
8153        )
8154        .await;
8155        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8156        let message = response["error"]["message"].as_str().unwrap();
8157        assert!(message.contains("allow_always"), "{message}");
8158        assert!(message.contains("allow_once, deny"), "{message}");
8159        // Nothing was sent, and the request is still waiting for an answer.
8160        assert!(answered
8161            .lock()
8162            .unwrap_or_else(std::sync::PoisonError::into_inner)
8163            .is_empty());
8164        assert_eq!(
8165            approvals(&mut service, json!({})).as_array().map(Vec::len),
8166            Some(1),
8167        );
8168    }
8169
8170    /// dev/01: supercode's own queued subagent row is addressable but not
8171    /// answerable through this door — it is the parent's audit copy of a
8172    /// request its own handler answers. Refused by name, never a no-op.
8173    #[tokio::test]
8174    async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8175        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8176            crate::subagents::QueuedApproval {
8177                child_agent_id: "child-7".into(),
8178                tool: "shell".into(),
8179                subject: Some("cargo publish --dry-run".into()),
8180                queued_at_ms: 1,
8181                outcome: None,
8182            },
8183        ]));
8184        let mut service = HarnessSessionService::new();
8185        service.observe_subagent_approvals(queue.clone());
8186        let row = approvals(&mut service, json!({}))[0]["id"]
8187            .as_str()
8188            .unwrap()
8189            .to_string();
8190        assert_eq!(row, "supercode/subagent/child-7/1/0");
8191
8192        let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8193        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8194        let message = response["error"]["message"].as_str().unwrap();
8195        assert!(message.contains("queued subagent record"), "{message}");
8196        assert!(message.contains("request"), "{message}");
8197        // The audit record is untouched: nothing pretended to answer it.
8198        assert!(queue
8199            .lock()
8200            .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8201            .outcome
8202            .is_none());
8203    }
8204
8205    /// An id nobody is holding, and a call that names no decision at all,
8206    /// both fail with a message that says why.
8207    #[tokio::test]
8208    async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8209        let mut service = HarnessSessionService::new();
8210        let response = resolve(
8211            &mut service,
8212            json!({"id": "runtime-9/4", "decision": "deny"}),
8213        )
8214        .await;
8215        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8216        assert!(response["error"]["message"]
8217            .as_str()
8218            .unwrap()
8219            .contains("runtime-9/4"));
8220
8221        let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8222        let message = response["error"]["message"].as_str().unwrap();
8223        assert!(
8224            message.contains("allow_once | allow_always | deny"),
8225            "{message}"
8226        );
8227
8228        let response = resolve(
8229            &mut service,
8230            json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8231        )
8232        .await;
8233        assert!(response["error"]["message"]
8234            .as_str()
8235            .unwrap()
8236            .contains("not both"));
8237    }
8238
8239    /// The method is advertised, its SDK operation resolves it, and every
8240    /// harness whose runtime door can carry a request reports it on the
8241    /// CONTROLLED tier beside `runtimes.respond`.
8242    #[test]
8243    fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8244        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8245        assert_eq!(
8246            SdkOperation::from_method("harness.v1.approvals.resolve"),
8247            Some(SdkOperation::ApprovalsResolve)
8248        );
8249        assert_eq!(
8250            SdkOperation::ApprovalsResolve.action_name(),
8251            "approvals_resolve"
8252        );
8253        let registry = harness_support_registry();
8254        for id in [
8255            HarnessId::HERMES,
8256            HarnessId::OPENCLAW,
8257            HarnessId::CODEX,
8258            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8259            // pending_request concept joins the other driven doors.
8260            HarnessId::CLAUDE_CODE,
8261        ] {
8262            let concept = registry
8263                .harnesses
8264                .iter()
8265                .find(|harness| harness.id.as_str() == id)
8266                .unwrap()
8267                .orchestration
8268                .concepts
8269                .iter()
8270                .find(|concept| concept.concept == "pending_request")
8271                .unwrap();
8272            assert_eq!(
8273                concept.controlled,
8274                crate::ImplementationKind::BuiltIn,
8275                "{id}"
8276            );
8277            assert!(
8278                concept
8279                    .methods
8280                    .iter()
8281                    .any(|method| method == "harness.v1.approvals.resolve"),
8282                "{id}"
8283            );
8284        }
8285    }
8286
8287    #[test]
8288    fn capabilities_are_explicit_and_versioned() {
8289        let mut service = HarnessSessionService::new();
8290        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8291        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8292        assert_eq!(
8293            response["result"]["sdk"]["schema_version"],
8294            crate::SDK_SCHEMA_VERSION
8295        );
8296        assert_eq!(
8297            response["result"]["sdk"]["operations"]
8298                .as_array()
8299                .unwrap()
8300                .len(),
8301            SdkOperation::ALL.len()
8302        );
8303        assert_eq!(
8304            response["result"]["harnesses"].as_array().unwrap().len(),
8305            11
8306        );
8307        assert!(response["result"]["harnesses"]
8308            .as_array()
8309            .unwrap()
8310            .iter()
8311            .any(|harness| harness == HarnessId::GROK));
8312        assert!(response["result"]["harnesses"]
8313            .as_array()
8314            .unwrap()
8315            .iter()
8316            .any(|harness| harness == HarnessId::GOOSE));
8317    }
8318
8319    #[test]
8320    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8321        let noisy_stderr = crate::HarnessEvent {
8322            sequence: None,
8323            kind: "transport_stderr".into(),
8324            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8325        };
8326        assert_eq!(handshake_event_failure(&noisy_stderr), None);
8327
8328        let closed = crate::HarnessEvent {
8329            sequence: None,
8330            kind: "transport_closed".into(),
8331            payload: json!({}),
8332        };
8333        assert!(handshake_event_failure(&closed).is_some());
8334    }
8335
8336    #[tokio::test]
8337    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8338        let mut service = HarnessSessionService::new();
8339        service
8340            .runtimes
8341            .insert("raw-eof".into(), ending_runtime(None));
8342        service.runtimes.insert(
8343            "explicit-close".into(),
8344            ending_runtime(Some(HarnessEvent {
8345                sequence: None,
8346                kind: "transport_closed".into(),
8347                payload: json!({"message": "native transport exited"}),
8348            })),
8349        );
8350
8351        let notifications = service.poll_runtimes().await;
8352
8353        assert_eq!(notifications.len(), 2);
8354        assert!(notifications
8355            .iter()
8356            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8357        assert!(notifications.iter().all(|notification| {
8358            notification["params"]["session_id"] == "ending-session"
8359                && notification["params"]["connection"].is_string()
8360        }));
8361        let mut sequences = notifications
8362            .iter()
8363            .filter_map(|notification| notification["params"]["sequence"].as_u64())
8364            .collect::<Vec<_>>();
8365        sequences.sort_unstable();
8366        assert_eq!(sequences, vec![1, 2]);
8367        assert!(service.runtimes.is_empty());
8368    }
8369
8370    #[test]
8371    fn support_report_and_grok_default_binding_share_the_registry() {
8372        let mut service = HarnessSessionService::new();
8373        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8374        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8375        let params = RuntimeBackendParams {
8376            harness: HarnessId::from(HarnessId::GROK),
8377            protocol: None,
8378            launch: None,
8379            base_url: None,
8380            policy: RuntimePolicy::Default,
8381        };
8382        let backend = match runtime_backend(&params) {
8383            Ok(backend) => backend,
8384            Err(_) => panic!("Grok should bind through its registered ACP launch"),
8385        };
8386        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8387        assert!(backend.capabilities().start_session);
8388        let registered = harness_support_registry()
8389            .harnesses
8390            .into_iter()
8391            .find(|harness| harness.id.as_str() == HarnessId::GROK)
8392            .and_then(|harness| harness.runtime.default_launch)
8393            .unwrap();
8394        assert!(!registered
8395            .arguments
8396            .iter()
8397            .any(|argument| argument == "--always-approve"));
8398        assert!(runtime_launch(&params).is_none());
8399
8400        let yolo = RuntimeBackendParams {
8401            policy: RuntimePolicy::Yolo,
8402            ..params
8403        };
8404        assert!(runtime_launch(&yolo)
8405            .unwrap()
8406            .arguments
8407            .iter()
8408            .any(|argument| argument == "--always-approve"));
8409
8410        let mismatched_protocol = RuntimeBackendParams {
8411            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8412            protocol: Some("acp".into()),
8413            launch: None,
8414            base_url: None,
8415            policy: RuntimePolicy::Default,
8416        };
8417        assert!(runtime_backend(&mismatched_protocol).is_err());
8418    }
8419
8420    #[test]
8421    fn load_follow_and_unfollow_share_the_same_locator() {
8422        let mut service = HarnessSessionService::new();
8423        let locator = pi_locator();
8424        let loaded = service.handle(request(
8425            1,
8426            "harness.v1.sessions.load",
8427            json!({"locator": locator}),
8428        ));
8429        assert_eq!(
8430            loaded["result"]["session"]["session_id"],
8431            locator.session_id
8432        );
8433
8434        let followed = service.handle(request(
8435            2,
8436            "harness.v1.sessions.follow",
8437            json!({"locator": locator}),
8438        ));
8439        assert_eq!(followed["result"]["subscription"], "sub-1");
8440        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8441        assert!(service.poll().is_empty());
8442
8443        let unfollowed = service.handle(request(
8444            3,
8445            "harness.v1.sessions.unfollow",
8446            json!({"subscription": "sub-1"}),
8447        ));
8448        assert_eq!(unfollowed["result"]["removed"], true);
8449    }
8450
8451    #[test]
8452    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8453        let temp = std::env::temp_dir().join(format!(
8454            "supercode-bounded-view-{}-{}",
8455            std::process::id(),
8456            generated_session_id()
8457        ));
8458        let path = temp.join("parent.jsonl");
8459        let subagents = temp.join("parent/subagents");
8460        std::fs::create_dir_all(&subagents).unwrap();
8461        let long_last = "x".repeat(300);
8462        let parent_records = [
8463            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8464            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8465            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8466        ];
8467        std::fs::write(
8468            &path,
8469            format!(
8470                "{}\n",
8471                parent_records
8472                    .iter()
8473                    .map(Value::to_string)
8474                    .collect::<Vec<_>>()
8475                    .join("\n")
8476            ),
8477        )
8478        .unwrap();
8479        std::fs::write(
8480            subagents.join("agent-child.jsonl"),
8481            concat!(
8482                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8483                "\n",
8484            ),
8485        )
8486        .unwrap();
8487        let locator = SessionLocator {
8488            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8489            session_id: "parent".into(),
8490            storage: StorageLocator::File { path },
8491        };
8492        let mut service = HarnessSessionService::new();
8493
8494        let complete = service.handle(request(
8495            1,
8496            "harness.v1.sessions.load",
8497            json!({"locator": locator}),
8498        ));
8499        assert_eq!(
8500            complete["result"]["session"]["subagents"]
8501                .as_array()
8502                .unwrap()
8503                .len(),
8504            1
8505        );
8506
8507        let bounded = service.handle(request(
8508            2,
8509            "harness.v1.sessions.load",
8510            json!({
8511                "locator": locator,
8512                "view": {
8513                    "tail_messages": 1,
8514                    "max_message_chars": 256,
8515                    "include_subagents": false
8516                },
8517            }),
8518        ));
8519        let session = &bounded["result"]["session"];
8520        assert!(session["subagents"].as_array().unwrap().is_empty());
8521        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8522        assert_eq!(
8523            session["messages"][0]["content"],
8524            format!("{}\n…", "x".repeat(256))
8525        );
8526
8527        let followed = service.handle(request(
8528            3,
8529            "harness.v1.sessions.follow",
8530            json!({
8531                "locator": locator,
8532                "view": {
8533                    "tail_messages": 1,
8534                    "max_message_chars": 256,
8535                    "include_subagents": false
8536                },
8537            }),
8538        ));
8539        let initial = &followed["result"]["initial"]["session"];
8540        assert!(initial["subagents"].as_array().unwrap().is_empty());
8541        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8542
8543        let _ = std::fs::remove_dir_all(&temp);
8544    }
8545
8546    #[test]
8547    fn forty_megabyte_display_load_is_bounded_and_prompt() {
8548        let temp = std::env::temp_dir().join(format!(
8549            "supercode-large-display-view-{}-{}",
8550            std::process::id(),
8551            generated_session_id()
8552        ));
8553        std::fs::create_dir_all(&temp).unwrap();
8554        let path = temp.join("rollout.jsonl");
8555        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8556        writeln!(
8557            file,
8558            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8559        )
8560        .unwrap();
8561        let padding = "x".repeat(80 * 1024);
8562        for index in 0..512 {
8563            let marker = if index == 0 {
8564                "OLDEST-SHOULD-NOT-LOAD"
8565            } else if index == 511 {
8566                "LATEST-MUST-LOAD"
8567            } else {
8568                "bulk"
8569            };
8570            writeln!(
8571                file,
8572                "{}",
8573                json!({
8574                    "timestamp": "2026-01-01T00:00:01Z",
8575                    "type": "response_item",
8576                    "payload": {
8577                        "type": "message",
8578                        "role": "assistant",
8579                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8580                    },
8581                })
8582            )
8583            .unwrap();
8584        }
8585        file.flush().unwrap();
8586        drop(file);
8587        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8588
8589        let locator = SessionLocator {
8590            harness: HarnessId::from(HarnessId::CODEX),
8591            session_id: "large-display".into(),
8592            storage: StorageLocator::File { path },
8593        };
8594        let started = Instant::now();
8595        let response = HarnessSessionService::new().handle(request(
8596            1,
8597            "harness.v1.sessions.load",
8598            json!({
8599                "locator": locator,
8600                "view": {
8601                    "tail_messages": 500,
8602                    "max_message_chars": 1024,
8603                    "include_subagents": false,
8604                    "display_history": true,
8605                },
8606            }),
8607        ));
8608        let elapsed = started.elapsed();
8609        let wire = response.to_string();
8610        eprintln!(
8611            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8612            wire.len()
8613        );
8614        assert!(response.get("error").is_none(), "{response:#}");
8615        assert!(wire.contains("LATEST-MUST-LOAD"));
8616        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8617        assert!(
8618            wire.len() < 2 * 1024 * 1024,
8619            "bounded wire was {} bytes",
8620            wire.len()
8621        );
8622        assert!(
8623            elapsed.as_secs_f64() < 3.0,
8624            "bounded 40 MiB load took {elapsed:?}"
8625        );
8626
8627        let _ = std::fs::remove_dir_all(&temp);
8628    }
8629
8630    #[test]
8631    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8632        let temp = std::env::temp_dir().join(format!(
8633            "supercode-large-goose-view-{}-{}",
8634            std::process::id(),
8635            generated_session_id()
8636        ));
8637        std::fs::create_dir_all(&temp).unwrap();
8638        let path = temp.join("sessions.db");
8639        let connection = rusqlite::Connection::open(&path).unwrap();
8640        connection
8641            .execute_batch(
8642                "CREATE TABLE sessions (
8643                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8644                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8645                    session_type TEXT NOT NULL, extension_data TEXT,
8646                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8647                    archived_at TEXT
8648                 );
8649                 CREATE TABLE messages (
8650                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8651                    role TEXT NOT NULL, content_json TEXT NOT NULL,
8652                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
8653                 );",
8654            )
8655            .unwrap();
8656        connection
8657            .execute(
8658                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8659                rusqlite::params![
8660                    "goose-large",
8661                    "Large Goose session",
8662                    "/tmp",
8663                    "2026-01-01 00:00:00",
8664                    "2026-01-01 00:00:02",
8665                    "user",
8666                    "{}",
8667                    "auto",
8668                    "anthropic",
8669                    r#"{"model_name":"claude-sonnet"}"#,
8670                ],
8671            )
8672            .unwrap();
8673        let old_content = serde_json::to_string(&vec![json!({
8674            "type": "text",
8675            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8676        })])
8677        .unwrap();
8678        connection
8679            .execute(
8680                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8681                rusqlite::params!["goose-large", old_content],
8682            )
8683            .unwrap();
8684        connection
8685            .execute(
8686                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8687                rusqlite::params![
8688                    "goose-large",
8689                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8690                ],
8691            )
8692            .unwrap();
8693        drop(connection);
8694        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8695
8696        let locator = SessionLocator {
8697            harness: HarnessId::from(HarnessId::GOOSE),
8698            session_id: "goose-large".into(),
8699            storage: StorageLocator::Sqlite {
8700                path,
8701                selector: "goose-large".into(),
8702            },
8703        };
8704        let started = Instant::now();
8705        let response = HarnessSessionService::new().handle(request(
8706            1,
8707            "harness.v1.sessions.load",
8708            json!({
8709                "locator": locator,
8710                "view": {
8711                    "tail_messages": 1,
8712                    "max_message_chars": 1024,
8713                    "include_subagents": false,
8714                    "display_history": true,
8715                },
8716            }),
8717        ));
8718        let elapsed = started.elapsed();
8719        let wire = response.to_string();
8720        eprintln!(
8721            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8722            wire.len()
8723        );
8724        assert!(response.get("error").is_none(), "{response:#}");
8725        assert!(wire.contains("LATEST-MUST-LOAD"));
8726        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8727        assert!(
8728            wire.len() < 64 * 1024,
8729            "bounded wire was {} bytes",
8730            wire.len()
8731        );
8732        assert!(
8733            elapsed.as_secs_f64() < 1.0,
8734            "bounded Goose load took {elapsed:?}"
8735        );
8736
8737        let _ = std::fs::remove_dir_all(&temp);
8738    }
8739
8740    #[test]
8741    fn display_view_keeps_codex_assistant_history_across_compaction() {
8742        let temp = std::env::temp_dir().join(format!(
8743            "supercode-codex-display-view-{}-{}",
8744            std::process::id(),
8745            generated_session_id()
8746        ));
8747        std::fs::create_dir_all(&temp).unwrap();
8748        let path = temp.join("rollout.jsonl");
8749        std::fs::write(
8750            &path,
8751            concat!(
8752                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8753                "\n",
8754                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8755                "\n",
8756                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8757                "\n",
8758                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"}]}}"#,
8759                "\n",
8760                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8761                "\n",
8762                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8763                "\n",
8764            ),
8765        )
8766        .unwrap();
8767        let locator = SessionLocator {
8768            harness: HarnessId::from(HarnessId::CODEX),
8769            session_id: "codex-display".into(),
8770            storage: StorageLocator::File { path },
8771        };
8772        let mut service = HarnessSessionService::new();
8773
8774        let continuation = service.handle(request(
8775            1,
8776            "harness.v1.sessions.load",
8777            json!({"locator": locator}),
8778        ));
8779        let continuation_text = continuation["result"]["session"]["messages"].to_string();
8780        assert!(!continuation_text.contains("old answer"));
8781
8782        let display = service.handle(request(
8783            2,
8784            "harness.v1.sessions.load",
8785            json!({
8786                "locator": locator,
8787                "view": {
8788                    "tail_messages": 10,
8789                    "include_subagents": false,
8790                    "display_history": true,
8791                },
8792            }),
8793        ));
8794        let display_text = display["result"]["session"]["messages"].to_string();
8795        assert!(display_text.contains("old prompt"));
8796        assert!(display_text.contains("old answer"));
8797        assert!(display_text.contains("new prompt"));
8798        assert!(display_text.contains("new answer"));
8799
8800        let _ = std::fs::remove_dir_all(&temp);
8801    }
8802
8803    #[test]
8804    fn indexed_claude_windows_match_the_existing_wire_projection() {
8805        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8806            .join("tests/fixtures/claude_code_session.jsonl");
8807        let locator = SessionLocator {
8808            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8809            session_id: "fixture".into(),
8810            storage: StorageLocator::File { path },
8811        };
8812        let full = load_session(&locator).unwrap();
8813        for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8814            for offset in [0, 1, full.messages.len(), usize::MAX] {
8815                for limit in [0, 1, 3, usize::MAX] {
8816                    let options = SessionLoadOptions {
8817                        include_subagents: Some(false),
8818                        inline_media,
8819                        message_offset: Some(offset),
8820                        message_limit: Some(limit),
8821                        ..Default::default()
8822                    };
8823                    let expected = projected_session_result(&full, &options);
8824                    assert_eq!(
8825                        indexed_claude_window(&locator, &options).unwrap().unwrap(),
8826                        expected
8827                    );
8828                }
8829            }
8830            for tail in [0, 1, 3, usize::MAX] {
8831                let options = SessionLoadOptions {
8832                    include_subagents: Some(false),
8833                    inline_media,
8834                    message_tail: Some(tail),
8835                    ..Default::default()
8836                };
8837                assert_eq!(
8838                    indexed_claude_window(&locator, &options).unwrap().unwrap(),
8839                    projected_session_result(&full, &options)
8840                );
8841            }
8842        }
8843    }
8844
8845    #[test]
8846    fn load_supports_bounded_windows_and_media_metadata() {
8847        let mut service = HarnessSessionService::new();
8848        let locator = pi_locator();
8849        let bounded = service.handle(request(
8850            1,
8851            "harness.v1.sessions.load",
8852            json!({
8853                "locator": locator,
8854                "options": {
8855                    "include_subagents": false,
8856                    "message_limit": 2,
8857                    "message_offset": 1
8858                }
8859            }),
8860        ));
8861        assert_eq!(bounded["result"]["window"]["offset"], 1);
8862        assert_eq!(bounded["result"]["window"]["returned"], 2);
8863        assert!(bounded["result"]["summary"]["first_message"].is_object());
8864        assert!(bounded["result"]["summary"]["last_message"].is_object());
8865        assert_eq!(
8866            bounded["result"]["session"]["messages"]
8867                .as_array()
8868                .unwrap()
8869                .len(),
8870            2
8871        );
8872        assert!(bounded["result"]["session"]["subagents"]
8873            .as_array()
8874            .unwrap()
8875            .is_empty());
8876
8877        let tail = service.handle(request(
8878            2,
8879            "harness.v1.sessions.load",
8880            json!({"locator": locator, "options": {"message_tail": 1}}),
8881        ));
8882        assert_eq!(tail["result"]["window"]["returned"], 1);
8883        assert_eq!(tail["result"]["window"]["has_more"], true);
8884        assert_eq!(tail["result"]["window"]["has_older"], true);
8885        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8886        assert!(tail["result"]["summary"]["first_message"].is_object());
8887
8888        let metadata_only = service.handle(request(
8889            3,
8890            "harness.v1.sessions.load",
8891            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8892        ));
8893        assert!(metadata_only["result"]["session"]
8894            .to_string()
8895            .contains("media_reference"));
8896        assert!(!metadata_only["result"]["session"]
8897            .to_string()
8898            .contains("data:image/"));
8899    }
8900
8901    #[test]
8902    fn import_translate_branch_and_handoff_use_typed_artifacts() {
8903        let mut service = HarnessSessionService::new();
8904        let locator = pi_locator();
8905        let translated = service.handle(request(
8906            1,
8907            "harness.v1.sessions.translate",
8908            json!({"locator": locator, "target_harness": "grok"}),
8909        ));
8910        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8911        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8912        assert!(translated["result"]["artifact"]["content"]
8913            .as_str()
8914            .is_some_and(|content| !content.is_empty()));
8915
8916        for target in ["opencode", "open-code"] {
8917            let opencode = service.handle(request(
8918                6,
8919                "harness.v1.sessions.translate",
8920                json!({"locator": locator, "target_harness": target}),
8921            ));
8922            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8923        }
8924        let goose = service.handle(request(
8925            7,
8926            "harness.v1.sessions.translate",
8927            json!({"locator": locator, "target_harness": "goose"}),
8928        ));
8929        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8930        assert!(serde_json::from_str::<Value>(
8931            goose["result"]["artifact"]["content"].as_str().unwrap()
8932        )
8933        .unwrap()["conversation"]
8934            .is_array());
8935
8936        let imported = service.handle(request(
8937            2,
8938            "harness.v1.sessions.import",
8939            json!({
8940                "source_harness": "grok",
8941                "content": translated["result"]["artifact"]["content"],
8942            }),
8943        ));
8944        assert_eq!(imported["result"]["session"]["source"], "grok");
8945
8946        let branched = service.handle(request(
8947            3,
8948            "harness.v1.sessions.branch",
8949            json!({"locator": locator, "target_harness": "codex"}),
8950        ));
8951        assert_eq!(branched["result"]["parent"]["harness"], "pi");
8952        assert!(branched["result"]["bootstrap_prompt"]
8953            .as_str()
8954            .unwrap()
8955            .contains("frozen parent transcript"));
8956        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
8957
8958        let handoff = service.handle(request(
8959            4,
8960            "harness.v1.sessions.handoff",
8961            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
8962        ));
8963        assert_eq!(handoff["result"]["launch"]["program"], "pi");
8964        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
8965        assert_eq!(handoff["result"]["requires_materialization"], true);
8966
8967        let goose_handoff = service.handle(request(
8968            8,
8969            "harness.v1.sessions.handoff",
8970            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
8971        ));
8972        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
8973        assert_eq!(
8974            goose_handoff["result"]["materialize"]["arguments"],
8975            json!(["session", "import", "{artifact_path}"])
8976        );
8977
8978        let resumed = service.handle(request(
8979            5,
8980            "harness.v1.sessions.resume_instructions",
8981            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
8982        ));
8983        assert_eq!(resumed["result"]["launch"]["program"], "pi");
8984        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
8985    }
8986
8987    #[test]
8988    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
8989        let temp = std::env::temp_dir().join(format!(
8990            "supercode-service-reduce-{}-{}",
8991            std::process::id(),
8992            generated_session_id()
8993        ));
8994        let source_path = temp.join("source.jsonl");
8995        let store_root = temp.join("store");
8996        std::fs::create_dir_all(&temp).unwrap();
8997
8998        let mut records = vec![json!({
8999            "timestamp": "2026-01-01T00:00:00Z",
9000            "type": "session_meta",
9001            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9002        })];
9003        for turn in 0..16 {
9004            records.push(json!({
9005                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9006                "type": "response_item",
9007                "payload": {
9008                    "type": "message",
9009                    "role": "user",
9010                    "content": [{
9011                        "type": "input_text",
9012                        "text": format!("request {turn}: {}", "context ".repeat(80)),
9013                    }],
9014                },
9015            }));
9016            records.push(json!({
9017                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9018                "type": "response_item",
9019                "payload": {
9020                    "type": "message",
9021                    "role": "assistant",
9022                    "content": [{
9023                        "type": "output_text",
9024                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9025                    }],
9026                },
9027            }));
9028        }
9029        let source = format!(
9030            "{}\n",
9031            records
9032                .iter()
9033                .map(Value::to_string)
9034                .collect::<Vec<_>>()
9035                .join("\n")
9036        );
9037        std::fs::write(&source_path, &source).unwrap();
9038        let locator = SessionLocator {
9039            harness: HarnessId::from(HarnessId::CODEX),
9040            session_id: "codex-reduce".into(),
9041            storage: StorageLocator::File {
9042                path: source_path.clone(),
9043            },
9044        };
9045        let original = load_session(&locator).unwrap();
9046        let mut service =
9047            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9048
9049        let response = service.handle(request(
9050            1,
9051            "harness.v1.sessions.reduce",
9052            json!({
9053                "locator": locator,
9054                "target_harness": "claude-code",
9055                "keep_last": 4,
9056            }),
9057        ));
9058        assert!(response.get("error").is_none(), "{response:#}");
9059        let receipt = &response["result"]["receipt"];
9060        assert_eq!(receipt["source_harness"], "codex");
9061        assert_eq!(receipt["target_harness"], "claude-code");
9062        assert_eq!(receipt["verified"], true);
9063        assert_eq!(receipt["reversible"], true);
9064        assert!(receipt["reductions"].as_u64().unwrap() > 0);
9065        assert!(
9066            receipt["source_tokens"].as_u64().unwrap()
9067                > receipt["reduced_tokens"].as_u64().unwrap()
9068        );
9069        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9070        assert!(response["result"]["bootstrap_prompt"]
9071            .as_str()
9072            .unwrap()
9073            .contains("Do not guess hidden content"));
9074
9075        let rescue_id = receipt["id"].as_str().unwrap();
9076        let store = crate::SessionStore::open(&store_root).unwrap();
9077        let sidecar =
9078            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9079        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9080        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9081        let policy = reduce::ReductionPolicy {
9082            clear_turns_older_than: Some(4),
9083            ..Default::default()
9084        };
9085        let (restamped_view, reapplied_log) =
9086            reduce::project_messages(&sidecar.messages, &policy, &log);
9087        assert_eq!(
9088            messages_jsonl(&persisted_view).unwrap(),
9089            messages_jsonl(&restamped_view).unwrap()
9090        );
9091        assert_eq!(reapplied_log, log);
9092        reduce::verify_log(&log, &sidecar).unwrap();
9093        assert_eq!(
9094            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9095            original.messages
9096        );
9097        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9098
9099        std::fs::remove_dir_all(temp).ok();
9100    }
9101
9102    #[test]
9103    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9104        let temp = std::env::temp_dir().join(format!(
9105            "supercode-severed-view-{}-{}",
9106            std::process::id(),
9107            generated_session_id()
9108        ));
9109        std::fs::create_dir_all(&temp).unwrap();
9110        let path = temp.join("severed.jsonl");
9111        // A live record whose parent was pruned — what a compacted or
9112        // resumed-across-files Claude Code session looks like on disk.
9113        std::fs::write(
9114            &path,
9115            concat!(
9116                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9117                "\n",
9118                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9119                "\n",
9120            ),
9121        )
9122        .unwrap();
9123        let locator = SessionLocator {
9124            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9125            session_id: "severed".into(),
9126            storage: StorageLocator::File { path },
9127        };
9128        let mut service = HarnessSessionService::new();
9129
9130        let viewed = service.handle(request(
9131            1,
9132            "harness.v1.sessions.load",
9133            json!({"locator": locator}),
9134        ));
9135        let session = &viewed["result"]["session"];
9136        assert_eq!(session["fidelity"], "semantic");
9137        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9138        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9139            entry
9140                .as_str()
9141                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9142        }));
9143
9144        // Asking a READ surface for a lossless reconstruction gets the strict
9145        // refusal back, unchanged.
9146        let strict = service.handle(request(
9147            2,
9148            "harness.v1.sessions.load",
9149            json!({"locator": locator, "fidelity": "byte_lossless"}),
9150        ));
9151        assert!(strict["error"]["message"]
9152            .as_str()
9153            .unwrap()
9154            .contains("cannot reconstruct lossless Claude continuation"));
9155
9156        // Transfer/continuation surfaces have no view mode at all.
9157        let translated = service.handle(request(
9158            3,
9159            "harness.v1.sessions.translate",
9160            json!({"locator": locator, "target_harness": "codex"}),
9161        ));
9162        assert!(translated["error"]["message"]
9163            .as_str()
9164            .unwrap()
9165            .contains("cannot reconstruct lossless Claude continuation"));
9166        let resumed = service.handle(request(
9167            4,
9168            "harness.v1.sessions.resume_instructions",
9169            json!({"locator": locator}),
9170        ));
9171        assert!(resumed["error"]["message"]
9172            .as_str()
9173            .unwrap()
9174            .contains("cannot reconstruct lossless Claude continuation"));
9175
9176        let _ = std::fs::remove_dir_all(&temp);
9177    }
9178
9179    #[test]
9180    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9181        let codex = resume_launch(
9182            HarnessId::CODEX,
9183            "codex-session",
9184            Path::new("/tmp/project"),
9185            ResumePolicy::Yolo,
9186        )
9187        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9188        assert_eq!(codex.program, "codex");
9189        assert_eq!(
9190            codex.arguments,
9191            [
9192                "-c",
9193                "check_for_update_on_startup=false",
9194                "-c",
9195                "projects.\"/tmp/project\".trust_level=\"trusted\"",
9196                "--dangerously-bypass-approvals-and-sandbox",
9197                "--dangerously-bypass-hook-trust",
9198                "resume",
9199                "codex-session",
9200            ]
9201        );
9202
9203        let gemini = resume_launch(
9204            HarnessId::GEMINI,
9205            "gemini-session",
9206            Path::new("/tmp/project"),
9207            ResumePolicy::Yolo,
9208        )
9209        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9210        assert_eq!(gemini.program, "gemini");
9211        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9212
9213        let goose = resume_launch(
9214            HarnessId::GOOSE,
9215            "goose-session",
9216            Path::new("/tmp/project"),
9217            ResumePolicy::Yolo,
9218        )
9219        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9220        assert_eq!(goose.program, "goose");
9221        assert_eq!(
9222            goose.arguments,
9223            ["session", "--resume", "--session-id", "goose-session"]
9224        );
9225
9226        let supercode = resume_launch(
9227            HarnessId::SUPERCODE,
9228            "supercode-session",
9229            Path::new("/tmp/project"),
9230            ResumePolicy::Yolo,
9231        )
9232        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9233        assert_eq!(supercode.program, "supercode");
9234        assert_eq!(
9235            supercode.arguments,
9236            ["--dangerous", "resume", "supercode-session"]
9237        );
9238    }
9239
9240    #[test]
9241    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9242        let temp = std::env::temp_dir().join(format!(
9243            "supercode-harness-artifact-{}-{}",
9244            std::process::id(),
9245            generated_session_id()
9246        ));
9247        let main_path = temp.join("parent.jsonl");
9248        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9249        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9250        let fixture = std::fs::read_to_string(
9251            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9252                .join("tests/fixtures/claude_code_session.jsonl"),
9253        )
9254        .unwrap();
9255        let parent = fixture.trim_end_matches('\n');
9256        let child = fixture.trim_end_matches('\n');
9257        std::fs::write(&main_path, parent).unwrap();
9258        std::fs::write(&subagent_path, child).unwrap();
9259        let locator = SessionLocator {
9260            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9261            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9262            storage: StorageLocator::File {
9263                path: main_path.clone(),
9264            },
9265        };
9266        let mut service = HarnessSessionService::new();
9267        let claude = service.handle(request(
9268            1,
9269            "harness.v1.sessions.translate",
9270            json!({"locator": locator, "target_harness": "claude-code"}),
9271        ));
9272        let artifact = &claude["result"]["artifact"];
9273        assert_eq!(artifact["fidelity"], "byte_lossless");
9274        assert_eq!(artifact["content"], parent);
9275        let files = artifact["files"].as_array().unwrap();
9276        assert!(files.iter().any(|file| {
9277            file["role"] == "subagent"
9278                && file["path"]
9279                    .as_str()
9280                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9281                && file["content"] == child
9282        }));
9283        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9284
9285        let grok = service.handle(request(
9286            2,
9287            "harness.v1.sessions.translate",
9288            json!({"locator": grok_locator(), "target_harness": "grok"}),
9289        ));
9290        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9291        for name in ["summary.json", "updates.jsonl"] {
9292            let expected = std::fs::read_to_string(
9293                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9294                    .join("tests/fixtures/grok_session")
9295                    .join(name),
9296            )
9297            .unwrap();
9298            assert!(files.iter().any(|file| {
9299                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9300            }));
9301        }
9302        std::fs::remove_dir_all(temp).ok();
9303    }
9304
9305    #[test]
9306    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9307        let mut service = HarnessSessionService::new();
9308        let source = pi_locator();
9309        for (target, format) in [
9310            ("claude-code", SessionFormat::ClaudeCode),
9311            ("codex", SessionFormat::Codex),
9312            ("opencode", SessionFormat::OpenCode),
9313            ("pi", SessionFormat::Pi),
9314        ] {
9315            let result = service.handle(request(
9316                1,
9317                "harness.v1.sessions.handoff",
9318                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9319            ));
9320            let artifact = &result["result"]["artifact"];
9321            let target_id = artifact["session_id"].as_str().unwrap();
9322            assert_ne!(target_id, source.session_id, "{target}");
9323            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9324            assert_eq!(
9325                parsed.meta.session_id.as_deref(),
9326                Some(target_id),
9327                "{target}"
9328            );
9329            if target != "pi" {
9330                assert!(result["result"]["launch"]["arguments"]
9331                    .as_array()
9332                    .unwrap()
9333                    .iter()
9334                    .any(|argument| argument == target_id));
9335            }
9336            if target == "opencode" {
9337                assert!(target_id.starts_with("ses_"));
9338                fn assert_session_ids(value: &Value, target_id: &str) {
9339                    match value {
9340                        Value::Object(fields) => {
9341                            if let Some(session_id) = fields.get("sessionID") {
9342                                assert_eq!(session_id, target_id);
9343                            }
9344                            for child in fields.values() {
9345                                assert_session_ids(child, target_id);
9346                            }
9347                        }
9348                        Value::Array(values) => {
9349                            for child in values {
9350                                assert_session_ids(child, target_id);
9351                            }
9352                        }
9353                        _ => {}
9354                    }
9355                }
9356                let document: Value =
9357                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9358                assert_session_ids(&document, target_id);
9359            }
9360        }
9361
9362        let first = service.handle(request(
9363            2,
9364            "harness.v1.sessions.handoff",
9365            json!({"locator": source, "target_harness": "codex"}),
9366        ));
9367        let second = service.handle(request(
9368            3,
9369            "harness.v1.sessions.handoff",
9370            json!({"locator": source, "target_harness": "codex"}),
9371        ));
9372        assert_ne!(
9373            first["result"]["artifact"]["session_id"],
9374            second["result"]["artifact"]["session_id"]
9375        );
9376    }
9377
9378    #[test]
9379    fn grok_handoff_materializes_through_the_core_door() {
9380        let mut service = HarnessSessionService::new();
9381        let source = opencode_locator();
9382        let response = service.handle(request(
9383            1,
9384            "harness.v1.sessions.handoff",
9385            json!({
9386                "locator": source,
9387                "target_harness": "grok",
9388                "cwd": "/tmp/grok-handoff-project",
9389            }),
9390        ));
9391        let result = &response["result"];
9392
9393        // Grok has no import command: the artifact is Grok's own transcript under a fresh
9394        // identity, and `harness.v1.sessions.materialize` writes its store entry.
9395        assert_eq!(result["artifact"]["target_harness"], "grok");
9396        let artifact = Session::load_str(
9397            result["artifact"]["content"].as_str().unwrap(),
9398            SessionFormat::Grok,
9399        )
9400        .unwrap();
9401        assert!(!artifact.messages.is_empty());
9402        let target_session_id = result["artifact"]["session_id"].as_str().unwrap();
9403        assert_eq!(target_session_id.len(), 36);
9404        assert_ne!(target_session_id, opencode_locator().session_id);
9405        assert!(result["materialize"].is_null());
9406        assert_eq!(
9407            result["launch"]["arguments"],
9408            json!(["--resume", "{materialized_session_id}"])
9409        );
9410        assert!(result["note"]
9411            .as_str()
9412            .unwrap()
9413            .contains("harness.v1.sessions.materialize"));
9414    }
9415
9416    #[tokio::test]
9417    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9418        let mut service = HarnessSessionService::new();
9419        let inventory = service
9420            .handle_async(request(
9421                1,
9422                "harness.v1.harnesses.list",
9423                json!({"harnesses": ["missing"]}),
9424            ))
9425            .await;
9426        assert_eq!(inventory["error"]["code"], -32602);
9427
9428        let attached = service
9429            .handle_async(request(
9430                2,
9431                "harness.v1.runtimes.attach_existing",
9432                json!({"harness": "codex", "runtime_id": "thread-1"}),
9433            ))
9434            .await;
9435        assert_eq!(attached["error"]["code"], -32000);
9436        assert!(attached["error"]["message"]
9437            .as_str()
9438            .unwrap()
9439            .contains("runtimes.resume"));
9440    }
9441
9442    #[test]
9443    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9444        let mut service = HarnessSessionService::new();
9445        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9446        assert_eq!(invalid["error"]["code"], -32602);
9447        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9448        assert_eq!(unknown["error"]["code"], -32601);
9449    }
9450
9451    #[cfg(unix)]
9452    #[tokio::test]
9453    // The test mutates process-wide harness environment and deliberately
9454    // holds the global test lock until every async runtime operation ends.
9455    #[allow(clippy::await_holding_lock)]
9456    async fn async_service_drives_a_generic_acp_runtime() {
9457        let _environment_guard = crate::live_runtime::test_environment_lock();
9458        let script = r#"
9459            i=0
9460            while IFS= read -r line; do
9461              i=$((i + 1))
9462              case "$i" in
9463                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9464                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9465                3)
9466                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9467                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9468                  ;;
9469                4)
9470                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9471                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9472                  ;;
9473              esac
9474            done
9475        "#;
9476        let mut service = HarnessSessionService::new();
9477        let started = service
9478            .handle_async(request(
9479                1,
9480                "harness.v1.runtimes.start",
9481                json!({
9482                    "harness": "codex",
9483                    "protocol": "acp",
9484                    "cwd": std::env::current_dir().unwrap(),
9485                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9486                }),
9487            ))
9488            .await;
9489        assert_eq!(started["result"]["connection"], "runtime-1");
9490        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9491
9492        let terminal = service
9493            .handle_async(request(
9494                9,
9495                "harness.v1.runtimes.terminal_instructions",
9496                json!({"connection":"runtime-1"}),
9497            ))
9498            .await;
9499        let arguments = terminal["result"]["launch"]["arguments"]
9500            .as_array()
9501            .expect("hosted runtime should return terminal arguments");
9502        let endpoint_index = arguments
9503            .iter()
9504            .position(|value| value == "--endpoint")
9505            .expect("terminal command should use an opaque endpoint");
9506        let endpoint = LiveRuntimeEndpoint::parse(
9507            arguments[endpoint_index + 1]
9508                .as_str()
9509                .expect("endpoint argument should be text"),
9510        )
9511        .unwrap();
9512        assert!(!terminal.to_string().contains("Bearer"));
9513        let workspace = std::env::current_dir().unwrap();
9514        let receipt = resolve_live_runtime(
9515            &endpoint,
9516            &LiveRuntimeSource {
9517                harness: "codex".into(),
9518                session_id: "svc_acp".into(),
9519                workspace,
9520            },
9521        )
9522        .unwrap();
9523        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9524            .await
9525            .unwrap();
9526        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9527            .await
9528            .unwrap();
9529
9530        let sent = service
9531            .handle_async(request(
9532                2,
9533                "harness.v1.runtimes.send_input",
9534                json!({"connection": "runtime-1", "text": "hi"}),
9535            ))
9536            .await;
9537        assert_eq!(sent["result"]["turn_id"], "3");
9538
9539        let mut events = Vec::new();
9540        for _ in 0..20 {
9541            events.extend(service.poll_runtimes().await);
9542            if events.len() >= 2 {
9543                break;
9544            }
9545            tokio::time::sleep(Duration::from_millis(2)).await;
9546        }
9547        assert!(events
9548            .iter()
9549            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9550        assert!(events.iter().any(|event| {
9551            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9552        }));
9553
9554        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9555            loop {
9556                let event = attachment.next_event().await.unwrap();
9557                if event.kind == "text_delta" && event.payload["text"] == "ok" {
9558                    break;
9559                }
9560            }
9561        })
9562        .await;
9563        assert!(
9564            saw_editor_reply.is_ok(),
9565            "terminal should observe the editor-driven turn"
9566        );
9567
9568        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9569            .await
9570            .unwrap();
9571        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9572            loop {
9573                let event = attachment.next_event().await.unwrap();
9574                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9575                    break;
9576                }
9577            }
9578        })
9579        .await;
9580        assert!(
9581            saw_terminal_reply.is_ok(),
9582            "terminal should drive the same runtime"
9583        );
9584
9585        let closed = service
9586            .handle_async(request(
9587                3,
9588                "harness.v1.runtimes.close",
9589                json!({"connection": "runtime-1"}),
9590            ))
9591            .await;
9592        assert_eq!(closed["result"]["closed"], true);
9593    }
9594
9595    /// UNI-7 dev/02: a RUNNING mock gateway is detected through the real
9596    /// openclaw probe (config-declared endpoint, TCP connect), and an ACTIVE
9597    /// hermes WAL is detected through the real WAL-freshness probe; the
9598    /// negative sides (no listener, stale WAL, no config) stay undetected.
9599    #[test]
9600    fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9601        let home = connect_scratch_home("uni7-running");
9602
9603        // No config at all: hermes has no default endpoint, so no detection.
9604        // (openclaw's no-config behavior now probes its DOCUMENTED default
9605        // endpoint ws://127.0.0.1:18789 — see the connect launch's
9606        // `default_address` — which is real box state a hermetic test must
9607        // not assert either way; the closed-port negative below covers the
9608        // no-listener side deterministically.)
9609        assert!(probe_hermes_running(&home, 300_000).is_none());
9610
9611        // Mock gateway: a real TCP listener on an ephemeral port, declared in
9612        // the harness's own config file.
9613        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9614        let port = listener.local_addr().unwrap().port();
9615        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9616        std::fs::write(
9617            home.join(".openclaw/openclaw.json"),
9618            format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9619        )
9620        .unwrap();
9621        let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9622        assert!(matches!(
9623            running.method,
9624            RunningInstanceMethod::GatewayConnect
9625        ));
9626        assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9627        drop(listener);
9628        // Parallel tests also bind ephemeral loopback ports, so a just-freed
9629        // port can be re-bound by a NEIGHBORING test between drop and probe.
9630        // Detection on a closed port must fail — retry on a fresh port when
9631        // the freed one was recycled by someone else.
9632        let mut closed_detected = probe_openclaw_running(&home).is_some();
9633        for _ in 0..3 {
9634            if !closed_detected {
9635                break;
9636            }
9637            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9638            let port = listener.local_addr().unwrap().port();
9639            drop(listener);
9640            std::fs::write(
9641                home.join(".openclaw/openclaw.json"),
9642                format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9643            )
9644            .unwrap();
9645            closed_detected = probe_openclaw_running(&home).is_some();
9646        }
9647        assert!(
9648            !closed_detected,
9649            "a closed gateway must not read as running"
9650        );
9651
9652        // gateway.url form takes precedence over port.
9653        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9654        let port = listener.local_addr().unwrap().port();
9655        std::fs::write(
9656            home.join(".openclaw/openclaw.json"),
9657            format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9658        )
9659        .unwrap();
9660        assert!(probe_openclaw_running(&home).is_some());
9661        drop(listener);
9662
9663        // Hermes: an ACTIVE WAL (fresh stamp) is detected; a stale one is not.
9664        std::fs::create_dir_all(home.join(".hermes")).unwrap();
9665        let wal = home.join(".hermes/state.db-wal");
9666        std::fs::write(&wal, b"wal").unwrap();
9667        let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9668        assert!(matches!(
9669            running.method,
9670            RunningInstanceMethod::StoreWalActivity
9671        ));
9672        assert!(running.evidence.contains("state.db-wal"));
9673        let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9674        std::fs::File::options()
9675            .append(true)
9676            .open(&wal)
9677            .unwrap()
9678            .set_modified(stale)
9679            .unwrap();
9680        assert!(
9681            probe_hermes_running(&home, 300_000).is_none(),
9682            "a stale WAL (crash leftover) must not read as running"
9683        );
9684    }
9685
9686    fn connect_scratch_home(tag: &str) -> PathBuf {
9687        let dir = std::env::temp_dir().join(format!(
9688            "supercode-connect-service-{tag}-{}-{}",
9689            std::process::id(),
9690            std::time::SystemTime::now()
9691                .duration_since(std::time::UNIX_EPOCH)
9692                .unwrap()
9693                .as_nanos()
9694        ));
9695        std::fs::create_dir_all(&dir).unwrap();
9696        dir
9697    }
9698
9699    /// Minimal HTTP responder that speaks just enough OpenCode server to
9700    /// accept a health check, create a session, and hold an SSE stream open,
9701    /// while recording each request line with its Authorization header.
9702    async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9703        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9704        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9705        let address = listener.local_addr().unwrap();
9706        let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9707        tokio::spawn(async move {
9708            loop {
9709                let Ok((mut stream, _)) = listener.accept().await else {
9710                    break;
9711                };
9712                let request_sender = request_sender.clone();
9713                tokio::spawn(async move {
9714                    let (reader, mut writer) = stream.split();
9715                    let mut reader = BufReader::new(reader);
9716                    let mut request_line = String::new();
9717                    if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9718                        return;
9719                    }
9720                    let request_line = request_line.trim_end().to_string();
9721                    let mut authorization = String::new();
9722                    let mut content_length = 0usize;
9723                    loop {
9724                        let mut line = String::new();
9725                        if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9726                            return;
9727                        }
9728                        let line = line.trim_end();
9729                        if line.is_empty() {
9730                            break;
9731                        }
9732                        let lower = line.to_ascii_lowercase();
9733                        if let Some(value) = lower.strip_prefix("authorization:") {
9734                            authorization = value.trim().to_string();
9735                        }
9736                        if let Some(value) = lower.strip_prefix("content-length:") {
9737                            content_length = value.trim().parse().unwrap_or(0);
9738                        }
9739                    }
9740                    if content_length > 0 {
9741                        let mut body = vec![0u8; content_length];
9742                        let _ = reader.read_exact(&mut body).await;
9743                    }
9744                    let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9745                    if request_line.starts_with("GET /event") {
9746                        let _ = writer
9747                            .write_all(
9748                                b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9749                            )
9750                            .await;
9751                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9752                        return;
9753                    }
9754                    let body = if request_line.starts_with("POST /session") {
9755                        r#"{"id":"mock-session"}"#
9756                    } else {
9757                        r#"{"status":"ok"}"#
9758                    };
9759                    let response = format!(
9760                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9761                        body.len(),
9762                        body
9763                    );
9764                    let _ = writer.write_all(response.as_bytes()).await;
9765                });
9766            }
9767        });
9768        (format!("http://{address}"), request_receiver)
9769    }
9770
9771    fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9772        crate::HarnessSupportDescriptor {
9773            orchestration: Default::default(),
9774            id: HarnessId::from(HarnessId::OPENCODE),
9775            display_name: "OpenCode".into(),
9776            native: crate::NativeSupport {
9777                discover: crate::ImplementationKind::Absent,
9778                load: crate::ImplementationKind::Absent,
9779                follow: crate::ImplementationKind::Absent,
9780                import: crate::ImplementationKind::Absent,
9781                export: crate::ImplementationKind::Absent,
9782            },
9783            runtime: crate::RuntimeSupport {
9784                implementation: crate::ImplementationKind::BuiltIn,
9785                protocol: protocol.into(),
9786                default_launch: None,
9787                connect_launch: Some(crate::RuntimeConnectLaunch {
9788                    config_path: "~/opencode-tui.json".into(),
9789                    address_pointer: "/server/url".into(),
9790                    port_pointer: None,
9791                    default_address: None,
9792                    auth_pointer: Some("/server/token".into()),
9793                    protocol: protocol.into(),
9794                }),
9795                capabilities: crate::RuntimeCapabilities {
9796                    start_session: true,
9797                    resume_session: true,
9798                    attach_existing_process: true,
9799                    send_input: true,
9800                    stream_events: true,
9801                    interrupt: true,
9802                    steer: false,
9803                    respond_to_requests: true,
9804                },
9805            },
9806        }
9807    }
9808
9809    #[tokio::test]
9810    async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9811        let (base_url, mut requests) = mock_opencode_endpoint().await;
9812        let home = connect_scratch_home("open");
9813        std::fs::write(
9814            home.join("opencode-tui.json"),
9815            format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9816        )
9817        .unwrap();
9818
9819        let descriptor = connect_descriptor("opencode-http-sse");
9820        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9821        assert!(backend.capabilities().attach_existing_process);
9822
9823        let connection = backend
9824            .start(crate::RuntimeStartRequest {
9825                cwd: home.clone(),
9826                launch: None,
9827                mcp_servers: Vec::new(),
9828            })
9829            .await
9830            .unwrap();
9831        let handle = connection.handle();
9832        assert_eq!(handle.runtime_id, "mock-session");
9833        match &handle.endpoint {
9834            crate::RuntimeEndpoint::Http {
9835                base_url: endpoint, ..
9836            } => assert_eq!(endpoint, &base_url),
9837            other => panic!("connect mode must join the running endpoint, got {other:?}"),
9838        }
9839
9840        let mut seen = Vec::new();
9841        while let Ok(line) = requests.try_recv() {
9842            seen.push(line);
9843        }
9844        assert!(seen
9845            .iter()
9846            .any(|line| line.starts_with("GET /global/health")
9847                && line.contains("bearer connect-secret")));
9848        assert!(seen.iter().any(
9849            |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9850        ));
9851    }
9852
9853    /// UNI-5 dev/02, contract corrected by the 2026-08-31 blind walk: the
9854    /// full connect-mode attach path against a MOCK gateway bridge — no live
9855    /// gateway, no model spend. A scripted fake `openclaw` binary (a)
9856    /// asserts the REAL bridge contract — the resolved --url on argv and the
9857    /// credential via --token-file (the real bridge ignores the env var; the
9858    /// endpoint comes from openclaw-native `gateway.remote.url`, never the
9859    /// schema-invalid `gateway.url`) — then (b) speaks scripted ACP:
9860    /// initialize advertising sessionCapabilities.{list,resume},
9861    /// session/resume rebinding the requested session (join), and a
9862    /// prompted turn.
9863    #[tokio::test]
9864    async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9865        let home = connect_scratch_home("openclaw");
9866        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9867        std::fs::write(
9868            home.join(".openclaw/openclaw.json"),
9869            r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9870        )
9871        .unwrap();
9872        let script = home.join("openclaw");
9873        std::fs::write(
9874            &script,
9875            r#"#!/bin/sh
9876# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9877[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9878[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9879[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9880[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9881while IFS= read -r line; do
9882  case "$line" in
9883    *'"initialize"'*)
9884      printf '%s
9885' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9886    *'"session/resume"'*)
9887      printf '%s
9888' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9889    *'"session/new"'*)
9890      printf '%s
9891' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9892    *'"session/prompt"'*)
9893      printf '%s
9894' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9895      printf '%s
9896' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9897  esac
9898done
9899"#,
9900        )
9901        .unwrap();
9902        use std::os::unix::fs::PermissionsExt;
9903        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9904
9905        let mut descriptor = crate::harness_support_registry()
9906            .harnesses
9907            .into_iter()
9908            .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9909            .expect("openclaw must be registered");
9910        descriptor
9911            .runtime
9912            .connect_launch
9913            .as_mut()
9914            .unwrap()
9915            .config_path = "~/.openclaw/openclaw.json".into();
9916        descriptor.runtime.default_launch.as_mut().unwrap().program =
9917            script.to_string_lossy().into_owned();
9918        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9919        assert!(backend.capabilities().resume_session);
9920
9921        let joined = backend
9922            .attach(crate::RuntimeAttachRequest {
9923                runtime_id: "agent:main:main".into(),
9924                cwd: Some(home.clone()),
9925                launch: None,
9926                mcp_servers: Vec::new(),
9927            })
9928            .await;
9929        let mut connection = joined.expect("mock bridge attach must succeed");
9930        assert_eq!(connection.handle().runtime_id, "agent:main:main");
9931        let turn = connection
9932            .send_input(crate::RuntimeInput {
9933                text: "hello".into(),
9934                image_urls: Vec::new(),
9935            })
9936            .await;
9937        assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
9938        connection.close().await.unwrap();
9939    }
9940
9941    #[tokio::test]
9942    async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
9943        let home = connect_scratch_home("fail");
9944        std::fs::write(
9945            home.join("opencode-tui.json"),
9946            r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
9947        )
9948        .unwrap();
9949
9950        let gateway_only = connect_descriptor("acp-v1-jsonrpc");
9951        let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
9952            panic!("an ACP connect endpoint has no gateway client yet");
9953        };
9954        let message = format!("{error:?}");
9955        assert!(message.contains("acp-v1-jsonrpc"));
9956        assert!(!message.contains("connect-secret"));
9957
9958        let unreadable = connect_descriptor("opencode-http-sse");
9959        let missing_home = connect_scratch_home("missing");
9960        let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
9961            panic!("an unreadable connect config must fail closed");
9962        };
9963        let message = format!("{error:?}");
9964        assert!(message.contains("opencode-tui.json"));
9965        assert!(!message.contains("connect-secret"));
9966    }
9967
9968    // ---------------------------------------------------------------------
9969    // ORCH-7 — `harness.v1.jobs.list` / `jobs.get` over the committed fixtures
9970    // ---------------------------------------------------------------------
9971
9972    fn jobs_fixture_root() -> PathBuf {
9973        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
9974    }
9975
9976    /// Point only the three job-bearing homes at the fixtures. Nothing else is
9977    /// read, so the host machine's own harness homes cannot leak into a row.
9978    fn jobs_fixture_homes() -> Value {
9979        let root = jobs_fixture_root();
9980        json!({
9981            "claude_code": root.join("claude_jobs_home/projects"),
9982            "hermes": root.join("hermes_home/state.db"),
9983            "openclaw": root.join("openclaw_home"),
9984        })
9985    }
9986
9987    fn jobs_list(params: Value) -> Value {
9988        let mut service = HarnessSessionService::new();
9989        service.handle(request(1, "harness.v1.jobs.list", params))
9990    }
9991
9992    fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
9993        result["jobs"]
9994            .as_array()
9995            .expect("jobs is an array")
9996            .iter()
9997            .find(|job| job["id"] == id)
9998            .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
9999    }
10000
10001    #[test]
10002    fn gateway_health_derives_from_running_probe_and_install_state() {
10003        let running = RunningInstance {
10004            method: RunningInstanceMethod::GatewayConnect,
10005            evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10006            checked_at_ms: 1,
10007        };
10008        let up = gateway_health(
10009            HarnessId::OPENCLAW,
10010            true,
10011            Some(&running),
10012            Some("2026.7.1-2"),
10013        );
10014        assert_eq!(up.state, GatewayState::Up);
10015        assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10016        assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10017        // Hermes consults its own `gateway status` when the WAL heuristic says
10018        // nothing; a fake binary decides the verdict (the env var is global, so
10019        // the up/down cases run inside this one test, never in parallel).
10020        let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10021        std::fs::create_dir_all(&dir).unwrap();
10022        let fake = dir.join("hermes");
10023        let write_fake = |body: &str| {
10024            std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10025            #[cfg(unix)]
10026            {
10027                use std::os::unix::fs::PermissionsExt;
10028                std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10029            }
10030        };
10031        write_fake("echo '✗ Gateway service is not installed'");
10032        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10033            *slot.borrow_mut() = Some((
10034                HarnessId::HERMES.to_string(),
10035                fake.to_string_lossy().into_owned(),
10036            ))
10037        });
10038        let down = gateway_health(HarnessId::HERMES, true, None, None);
10039        assert_eq!(down.state, GatewayState::Down, "{down:?}");
10040        assert!(down.endpoint.is_none());
10041        assert!(down.evidence.contains("not installed"));
10042        write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10043        let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10044        assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10045        assert!(idle_but_up.evidence.contains("PID 4242"));
10046        write_fake("echo 'something unparseable'");
10047        let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10048        assert_eq!(no_verdict.state, GatewayState::Down);
10049        assert!(no_verdict.evidence.contains("no verdict"));
10050        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10051        let absent = gateway_health(HarnessId::HERMES, false, None, None);
10052        assert_eq!(absent.state, GatewayState::Unknown);
10053        let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10054        assert_eq!(core.state, GatewayState::Unknown);
10055        assert!(core.evidence.contains("per session"));
10056    }
10057
10058    #[test]
10059    fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10060        let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10061        let rows = response["result"]["triggers"]
10062            .as_array()
10063            .expect("triggers")
10064            .clone();
10065        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10066        assert!(
10067            hermes.iter().any(|r| r["name"] == "deploys"
10068                && r["route"] == "/webhooks/deploys"
10069                && r["kind"] == "webhook"),
10070            "{rows:#?}"
10071        );
10072        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10073        assert!(openclaw
10074            .iter()
10075            .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10076        assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10077            && r["kind"] == "hook_mapping"
10078            && r["target"]["action"] == "agent"));
10079        let rendered = response.to_string();
10080        for secret in [
10081            "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10082            "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10083        ] {
10084            assert!(!rendered.contains(secret), "{rendered}");
10085        }
10086        let refused =
10087            triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10088        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10089    }
10090
10091    fn triggers_list(params: Value) -> Value {
10092        let mut service = HarnessSessionService::new();
10093        service.handle(request(1, "harness.v1.triggers.list", params))
10094    }
10095
10096    #[test]
10097    fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10098        let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10099        let rows = response["result"]["routes"]
10100            .as_array()
10101            .expect("routes")
10102            .clone();
10103        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10104        assert_eq!(hermes.len(), 2, "{rows:#?}");
10105        assert_eq!(hermes[0]["target"], "coder");
10106        assert_eq!(hermes[0]["match"]["platform"], "slack");
10107        assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10108        assert_eq!(hermes[0]["specificity"], 4);
10109        assert_eq!(hermes[1]["default"], true);
10110        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10111        assert!(
10112            openclaw.iter().any(|r| r["target"] == "design"
10113                && r["match"]["platform"] == "slack"
10114                && r["specificity"] == 1),
10115            "{openclaw:#?}"
10116        );
10117        assert!(openclaw.iter().any(|r| r["default"] == true));
10118        // A core harness has no routing concept and is refused, never an empty list.
10119        let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10120        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10121    }
10122
10123    fn routes_list(params: Value) -> Value {
10124        let mut service = HarnessSessionService::new();
10125        service.handle(request(1, "harness.v1.routes.list", params))
10126    }
10127
10128    #[test]
10129    fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10130        let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10131        let result = &response["result"];
10132        let ids: Vec<&str> = result["jobs"]
10133            .as_array()
10134            .unwrap()
10135            .iter()
10136            .map(|job| job["id"].as_str().unwrap())
10137            .collect();
10138        assert_eq!(
10139            ids,
10140            vec![
10141                "release-watch",
10142                "toolu_wake_recheck",
10143                "digest-15m",
10144                "nightly-audit",
10145                "coder-standup",
10146                "ops-once-boot",
10147                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10148                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10149                "cron_standup",
10150                "cron_reindex",
10151            ],
10152            "{result}"
10153        );
10154
10155        // OpenClaw, pinned shape: rows come from `state/openclaw.sqlite`
10156        // (`cron_jobs.job_json` + runtime columns), captured from a real
10157        // 2026.7.1-2 gateway.
10158        let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10159        assert_eq!(health["harness"], "openclaw");
10160        assert_eq!(health["schedule"]["kind"], "interval");
10161        assert_eq!(health["schedule"]["minutes"], 10.0);
10162        assert_eq!(health["session_target"], "isolated");
10163        assert_eq!(health["payload"]["kind"], "prompt");
10164        assert_eq!(health["payload"]["text"], "nightly health check");
10165        // ORCH-13: the mode word (`announce`) and the channel it announces on
10166        // (`last`) are separate facts, and the store keeps both — in
10167        // `job_json.delivery` and in the `delivery_*` columns beside it.
10168        assert_eq!(health["deliver"]["mode"], "announce");
10169        assert_eq!(health["deliver"]["target"], "last");
10170        assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10171        let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10172        assert_eq!(digest["schedule"]["kind"], "cron");
10173        assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10174        assert_eq!(digest["session_target"], "main");
10175        assert_eq!(digest["payload"]["kind"], "system_event");
10176
10177        // Claude Code: session-scoped, one recurring cron and one one-shot wakeup.
10178        let cron = job_row(result, "release-watch");
10179        assert_eq!(cron["harness"], "claude-code");
10180        assert_eq!(cron["scope"], "session");
10181        assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10182        assert_eq!(cron["schedule"]["kind"], "cron");
10183        assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10184        assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10185        assert_eq!(cron["payload"]["kind"], "prompt");
10186        assert_eq!(cron["recurring"], true);
10187        assert_eq!(cron["deliver"]["target"], "session");
10188        let wakeup = job_row(result, "toolu_wake_recheck");
10189        assert_eq!(wakeup["payload"]["kind"], "wakeup");
10190        assert_eq!(wakeup["schedule"]["kind"], "once");
10191        assert_eq!(wakeup["recurring"], false);
10192        assert_eq!(wakeup["state"], "pending");
10193
10194        // Hermes: install-scoped, interval + origin delivery, and a paused cron.
10195        let interval = job_row(result, "digest-15m");
10196        assert_eq!(interval["harness"], "hermes");
10197        assert_eq!(interval["scope"], "install");
10198        assert_eq!(interval["profile"], Value::Null);
10199        assert_eq!(interval["schedule"]["kind"], "interval");
10200        assert_eq!(interval["schedule"]["minutes"], 15.0);
10201        assert_eq!(interval["schedule"]["display"], "every 15 min");
10202        assert_eq!(interval["deliver"]["target"], "origin");
10203        assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10204        assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10205        assert_eq!(interval["last_status"], "ok");
10206        let nightly = job_row(result, "nightly-audit");
10207        assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10208        assert_eq!(nightly["deliver"]["target"], "local");
10209        assert_eq!(nightly["enabled"], false);
10210        assert_eq!(nightly["state"], "paused");
10211        // The per-profile store carries the profile name from its own path.
10212        let profiled = job_row(result, "ops-once-boot");
10213        assert_eq!(profiled["profile"], "ops");
10214        assert_eq!(profiled["schedule"]["kind"], "once");
10215        assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10216        assert_eq!(profiled["payload"]["kind"], "script");
10217        // An explicit `<platform>:<chat>` target carries the chat itself.
10218        assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10219        assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10220        assert_eq!(profiled["recurring"], false);
10221
10222        // ORCH-13: a job delivering to its creating conversation carries that
10223        // conversation's whole surface — platform word, chat AND thread.
10224        let standup_to_group = job_row(result, "coder-standup");
10225        assert_eq!(standup_to_group["deliver"]["target"], "origin");
10226        assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10227        assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10228        // Hermes has no mode word and routes by adapter profile, not account.
10229        assert!(standup_to_group["deliver"]["mode"].is_null());
10230        assert!(standup_to_group["deliver"]["account"].is_null());
10231
10232        // OpenClaw: the session target and the delivery mode are the row's own
10233        // columns, not a footnote.
10234        let standup = job_row(result, "cron_standup");
10235        assert_eq!(standup["harness"], "openclaw");
10236        assert_eq!(standup["session_target"], "isolated");
10237        assert_eq!(standup["deliver"]["mode"], "announce");
10238        assert_eq!(standup["deliver"]["target"], "slack");
10239        assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10240        assert_eq!(standup["payload"]["kind"], "prompt");
10241        assert_eq!(standup["profile"], "main");
10242        let reindex = job_row(result, "cron_reindex");
10243        assert_eq!(reindex["session_target"], "main");
10244        assert_eq!(reindex["payload"]["kind"], "system_event");
10245        assert_eq!(reindex["schedule"]["kind"], "interval");
10246        assert_eq!(reindex["schedule"]["display"], "every 240 min");
10247        assert_eq!(reindex["enabled"], false);
10248
10249        // Every store consulted is named, so an empty answer is never silent.
10250        let states: Vec<(&str, &str)> = result["sources"]
10251            .as_array()
10252            .unwrap()
10253            .iter()
10254            .map(|source| {
10255                (
10256                    source["harness"].as_str().unwrap(),
10257                    source["state"].as_str().unwrap(),
10258                )
10259            })
10260            .collect();
10261        // The `coder` profile home has no cron store at all: it is named as
10262        // `absent_store`, not skipped, so "this profile schedules nothing" and
10263        // "this profile was never looked at" stay distinguishable.
10264        assert_eq!(
10265            states,
10266            vec![
10267                ("claude-code", "scanned"),
10268                ("hermes", "read"),
10269                ("hermes", "absent_store"),
10270                ("hermes", "read"),
10271                ("openclaw", "read"),
10272                ("openclaw", "read"),
10273            ],
10274            "{result}"
10275        );
10276    }
10277
10278    #[test]
10279    fn jobs_list_filters_by_harness_session_and_profile() {
10280        let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10281        let ids: Vec<&str> = by_harness["result"]["jobs"]
10282            .as_array()
10283            .unwrap()
10284            .iter()
10285            .map(|job| job["id"].as_str().unwrap())
10286            .collect();
10287        assert_eq!(
10288            ids,
10289            vec![
10290                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10291                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10292                "cron_standup",
10293                "cron_reindex",
10294            ]
10295        );
10296
10297        let by_session = jobs_list(json!({
10298            "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10299            "homes": jobs_fixture_homes(),
10300        }));
10301        let jobs = by_session["result"]["jobs"].as_array().unwrap();
10302        assert_eq!(jobs.len(), 2, "{by_session}");
10303        assert!(jobs
10304            .iter()
10305            .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10306
10307        let by_profile = jobs_list(json!({
10308            "harness": "hermes",
10309            "profile": "ops",
10310            "homes": jobs_fixture_homes(),
10311        }));
10312        let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10313        assert_eq!(jobs.len(), 1, "{by_profile}");
10314        assert_eq!(jobs[0]["id"], "ops-once-boot");
10315    }
10316
10317    #[test]
10318    fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10319        let mut service = HarnessSessionService::new();
10320        let hermes = service.handle(request(
10321            1,
10322            "harness.v1.jobs.get",
10323            json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10324        ));
10325        assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10326        // Native fields the uniform row does not carry survive on `source`.
10327        assert_eq!(hermes["result"]["source"]["provider"], "nous");
10328        assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10329
10330        let claude = service.handle(request(
10331            2,
10332            "harness.v1.jobs.get",
10333            json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10334        ));
10335        assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10336        assert_eq!(
10337            claude["result"]["source"]["tool_use_id"],
10338            "toolu_cron_release_watch"
10339        );
10340
10341        let missing = service.handle(request(
10342            3,
10343            "harness.v1.jobs.get",
10344            json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10345        ));
10346        assert!(missing["error"]["message"]
10347            .as_str()
10348            .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10349    }
10350
10351    #[test]
10352    fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10353        let mut service = HarnessSessionService::new();
10354        for (id, method, params) in [
10355            (
10356                1,
10357                "harness.v1.jobs.list",
10358                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10359            ),
10360            (
10361                2,
10362                "harness.v1.jobs.get",
10363                json!({"harness": "codex", "id": "anything"}),
10364            ),
10365        ] {
10366            let response = service.handle(request(id, method, params));
10367            assert_eq!(response["error"]["code"], -32020, "{response}");
10368            assert!(response["error"]["message"]
10369                .as_str()
10370                .is_some_and(|message| message.contains("has no scheduled jobs")));
10371            assert!(response.get("result").is_none());
10372        }
10373    }
10374
10375    #[test]
10376    fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10377        let scratch = std::env::temp_dir().join(format!(
10378            "supercode-jobs-migrated-{}-{}",
10379            std::process::id(),
10380            generated_session_id()
10381        ));
10382        std::fs::create_dir_all(&scratch).unwrap();
10383        let response = jobs_list(json!({
10384            "harness": "openclaw",
10385            "homes": {"openclaw": scratch.clone()},
10386        }));
10387        let result = &response["result"];
10388        assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10389        assert_eq!(result["sources"][0]["state"], "absent_store");
10390        assert_eq!(result["sources"][0]["harness"], "openclaw");
10391        std::fs::remove_dir_all(&scratch).ok();
10392    }
10393
10394    // ---------------------------------------------------------------------
10395    // ORCH-8 — `harness.v1.runs.list` / `runs.get` over the committed fire
10396    // stores: Hermes's `cron/executions.db` (root home + profile home) and
10397    // OpenClaw's `cron_run_logs`. Every fixture row is written by
10398    // `tests/fixtures/gen_runs_fixtures.py` against the harnesses' own DDL.
10399    // ---------------------------------------------------------------------
10400
10401    /// The health job in the committed OpenClaw fixture, which fired twice.
10402    const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10403    /// The digest job, whose single fire predates run ids.
10404    const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10405
10406    fn runs_list(params: Value) -> Value {
10407        let mut service = HarnessSessionService::new();
10408        service.handle(request(1, "harness.v1.runs.list", params))
10409    }
10410
10411    fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10412        result["runs"]
10413            .as_array()
10414            .expect("runs is an array")
10415            .iter()
10416            .find(|run| run["id"] == id)
10417            .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10418    }
10419
10420    #[test]
10421    fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10422        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10423        let result = &response["result"];
10424        let ids: Vec<&str> = result["runs"]
10425            .as_array()
10426            .expect("runs is an array")
10427            .iter()
10428            .map(|run| run["id"].as_str().unwrap())
10429            .collect();
10430        let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10431        assert_eq!(
10432            ids,
10433            vec![
10434                // Hermes, newest claim first, root ledger then profile ledger.
10435                "b2c3d4e5f60718293a4b5c6d7e8f9012",
10436                "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10437                "c3d4e5f60718293a4b5c6d7e8f901234",
10438                "f60718293a4b5c6d7e8f901234567890",
10439                "e5f60718293a4b5c6d7e8f9012345678",
10440                "d4e5f60718293a4b5c6d7e8f90123456",
10441                // OpenClaw, newest `ts` first.
10442                "run_health_0002",
10443                digest_fire.as_str(),
10444                "run_health_0001",
10445            ],
10446            "{result}"
10447        );
10448
10449        // The harness's OWN outcome word survives; nothing is renamed onto a
10450        // shared vocabulary.
10451        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10452        assert_eq!(failed["harness"], "hermes");
10453        assert_eq!(failed["job_id"], "job42");
10454        assert_eq!(failed["status"], "failed");
10455        assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10456        assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10457
10458        // Hermes's `unknown` — an attempt whose owner died before writing a
10459        // terminal state — is a fourth status, not folded into `failed`.
10460        let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10461        assert_eq!(abandoned["status"], "unknown");
10462        assert_eq!(abandoned["job_id"], "ops-once-boot");
10463
10464        // An unterminated fire has no finish, and no session is invented.
10465        let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10466        assert_eq!(running["status"], "running");
10467        assert!(running["finished_at"].is_null(), "{running}");
10468        assert!(running["session_id"].is_null(), "{running}");
10469
10470        // OpenClaw records the session on the row itself, and epoch-ms
10471        // timestamps are rendered as RFC 3339.
10472        let ok = run_row(result, "run_health_0001");
10473        assert_eq!(ok["harness"], "openclaw");
10474        assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10475        assert_eq!(ok["status"], "ok");
10476        assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10477        assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10478        assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10479        // OpenClaw's run log is written once, at finish: there is no claim.
10480        assert!(ok["claimed_at"].is_null(), "{ok}");
10481
10482        // A run-log row with no `run_id` falls back to the store's own
10483        // `(job_id, seq)` key rather than being dropped.
10484        assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10485
10486        // ORCH-13: a fire whose delivery nothing recorded says so, rather than
10487        // borrowing a neighbouring fire's outcome. Both of these ran on jobs
10488        // that deliver `local` (or have no job record at all), so no
10489        // obligation is addressed to a surface they could match.
10490        for id in [
10491            "b2c3d4e5f60718293a4b5c6d7e8f9012",
10492            "d4e5f60718293a4b5c6d7e8f90123456",
10493        ] {
10494            assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10495        }
10496
10497        // Every store consulted is named, including the profile home that has
10498        // no ledger — an empty history and an absent store are different.
10499        let sources = result["sources"].as_array().unwrap();
10500        let states: Vec<(&str, &str)> = sources
10501            .iter()
10502            .map(|source| {
10503                (
10504                    source["harness"].as_str().unwrap(),
10505                    source["state"].as_str().unwrap(),
10506                )
10507            })
10508            .collect();
10509        assert_eq!(
10510            states,
10511            vec![
10512                ("hermes", "read"),
10513                ("hermes", "absent_store"),
10514                ("hermes", "read"),
10515                ("openclaw", "read"),
10516            ],
10517            "{result}"
10518        );
10519        assert_eq!(sources[2]["profile"], "ops");
10520        assert!(sources[3]["path"]
10521            .as_str()
10522            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10523    }
10524
10525    #[test]
10526    fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10527        let response = runs_list(json!({
10528            "harness": "hermes",
10529            "job": "job42",
10530            "homes": jobs_fixture_homes(),
10531        }));
10532        let result = &response["result"];
10533        assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10534
10535        // Hermes writes NO link from an execution to its session. The fire
10536        // that ran the agent is joined to `cron_job42_<stamp>` because that
10537        // id's instant falls inside its [claimed_at, finished_at] window.
10538        let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10539        assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10540
10541        // The later fire failed before opening one. Its window holds no
10542        // session, so the row says so instead of re-using the earlier fire's
10543        // — the join is per-FIRE, not per-job.
10544        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10545        assert!(failed["session_id"].is_null(), "{failed}");
10546    }
10547
10548    /// ORCH-13: where a fire's output went, read from each harness's own
10549    /// delivery record — Hermes's `delivery_obligations` ledger inside
10550    /// `state.db`, OpenClaw's `delivery_*` run-log columns.
10551    #[test]
10552    fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10553        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10554        let result = &response["result"];
10555
10556        // Hermes: the ledger is the GATEWAY's, keyed by conversation and
10557        // surface, so the fire's own [claimed_at, finished_at] window picks
10558        // the obligation. The fire succeeded and so did the send.
10559        let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10560        assert_eq!(delivered["status"], "completed");
10561        assert_eq!(delivered["delivery"]["state"], "delivered");
10562        assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10563        assert_eq!(delivered["delivery"]["attempts"], 1);
10564        assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10565        assert_eq!(
10566            delivered["delivery"]["delivered_at"],
10567            "2026-09-02T09:00:30.400Z"
10568        );
10569
10570        // The next fire of the same job ALSO succeeded — and its output never
10571        // arrived. That is the fact `status` alone cannot carry.
10572        let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10573        assert_eq!(undelivered["status"], "completed");
10574        assert_eq!(undelivered["delivery"]["state"], "failed");
10575        assert_eq!(undelivered["delivery"]["attempts"], 3);
10576        assert_eq!(
10577            undelivered["delivery"]["last_error"],
10578            "telegram send failed: Bad Request: chat not found"
10579        );
10580        // Only a delivered obligation carries an instant of delivery; the
10581        // ledger's `updated_at` on a failed row dates the failure.
10582        assert!(
10583            undelivered["delivery"]["delivered_at"].is_null(),
10584            "{undelivered}"
10585        );
10586
10587        // OpenClaw writes the outcome onto the run-log row and declares the
10588        // address on the job, so the row's target is joined from `cron_jobs`.
10589        let announced = run_row(result, "run_health_0001");
10590        assert_eq!(announced["delivery"]["state"], "delivered");
10591        assert_eq!(announced["delivery"]["target"], "last");
10592        // Its run log counts no attempts and stamps no delivered-at.
10593        assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10594        assert!(
10595            announced["delivery"]["delivered_at"].is_null(),
10596            "{announced}"
10597        );
10598        let refused = run_row(result, "run_health_0002");
10599        assert_eq!(refused["delivery"]["state"], "not-delivered");
10600        assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10601
10602        // A run-log row with no delivery columns at all recorded no delivery:
10603        // the job's declared target is not evidence that anything was sent.
10604        let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10605        assert!(skipped["delivery"].is_null(), "{skipped}");
10606    }
10607
10608    /// A Hermes fire whose session carries a `session_key` is matched on that
10609    /// key FIRST — the most specific question the ledger can answer. Proven by
10610    /// moving the obligations off the job's surface on a COPY of the fixture,
10611    /// so only the session-key question can still find them.
10612    #[test]
10613    fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10614        let scratch = std::env::temp_dir().join(format!(
10615            "supercode-runs-delivery-{}-{}",
10616            std::process::id(),
10617            generated_session_id()
10618        ));
10619        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10620        let fixture = jobs_fixture_root().join("hermes_home");
10621        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10622        for name in ["cron/executions.db", "cron/jobs.json"] {
10623            std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10624        }
10625        {
10626            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10627            // The obligations now sit on a surface no job in this store
10628            // delivers to, so the surface question cannot match them.
10629            connection
10630                .execute(
10631                    "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10632                    [],
10633                )
10634                .unwrap();
10635            // A cron fire that ran inside a keyed conversation: the session
10636            // the window recovers carries `tg-coder-1`'s key.
10637            connection
10638                .execute(
10639                    "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10640                     ('cron_coder-standup_20260902_090010', 'cron', \
10641                      'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10642                    [],
10643                )
10644                .unwrap();
10645        }
10646        let response = runs_list(json!({
10647            "harness": "hermes",
10648            "job": "coder-standup",
10649            "homes": {"hermes": scratch.join("state.db")},
10650        }));
10651        let result = &response["result"];
10652        let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10653        assert_eq!(
10654            matched["session_id"], "cron_coder-standup_20260902_090010",
10655            "{result}"
10656        );
10657        assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10658        assert_eq!(
10659            matched["delivery"]["target"], "slack:C0FALLBACK:55",
10660            "{result}"
10661        );
10662        std::fs::remove_dir_all(&scratch).ok();
10663    }
10664
10665    #[test]
10666    fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10667        // A fire whose session was compressed mid-run is only readable at the
10668        // continuation, so that is what the row must report. Built on a COPY
10669        // of the committed fixture: no test writes to a fixture or to a real
10670        // harness home.
10671        let scratch = std::env::temp_dir().join(format!(
10672            "supercode-runs-compressed-{}-{}",
10673            std::process::id(),
10674            generated_session_id()
10675        ));
10676        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10677        let fixture = jobs_fixture_root().join("hermes_home");
10678        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10679        std::fs::copy(
10680            fixture.join("cron/executions.db"),
10681            scratch.join("cron/executions.db"),
10682        )
10683        .unwrap();
10684        {
10685            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10686            connection
10687                .execute(
10688                    "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10689                    ["cron_job42_20260902_120000"],
10690                )
10691                .unwrap();
10692            connection
10693                .execute(
10694                    "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10695                     VALUES ('job42-after-compaction', 'cron', \
10696                             'cron_job42_20260902_120000', 1788350000.0)",
10697                    [],
10698                )
10699                .unwrap();
10700        }
10701        let response = runs_list(json!({
10702            "harness": "hermes",
10703            "job": "job42",
10704            "homes": {"hermes": scratch.join("state.db")},
10705        }));
10706        let result = &response["result"];
10707        assert_eq!(
10708            run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10709            "job42-after-compaction",
10710            "{result}"
10711        );
10712        std::fs::remove_dir_all(&scratch).ok();
10713    }
10714
10715    #[test]
10716    fn runs_list_filters_by_job_and_caps_by_limit() {
10717        let by_job = runs_list(json!({
10718            "harness": "openclaw",
10719            "job": OPENCLAW_HEALTH_JOB,
10720            "homes": jobs_fixture_homes(),
10721        }));
10722        let ids: Vec<&str> = by_job["result"]["runs"]
10723            .as_array()
10724            .unwrap()
10725            .iter()
10726            .map(|run| run["id"].as_str().unwrap())
10727            .collect();
10728        assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10729
10730        let capped = runs_list(json!({
10731            "harness": "openclaw",
10732            "limit": 1,
10733            "homes": jobs_fixture_homes(),
10734        }));
10735        let runs = capped["result"]["runs"].as_array().unwrap();
10736        assert_eq!(runs.len(), 1, "{capped}");
10737        // Newest first, so the cap keeps the recent fire.
10738        assert_eq!(runs[0]["id"], "run_health_0002");
10739    }
10740
10741    #[test]
10742    fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10743        let mut service = HarnessSessionService::new();
10744        let hermes = service.handle(request(
10745            1,
10746            "harness.v1.runs.get",
10747            json!({
10748                "harness": "hermes",
10749                "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10750                "homes": jobs_fixture_homes(),
10751            }),
10752        ));
10753        assert_eq!(hermes["result"]["run"]["status"], "completed");
10754        assert_eq!(
10755            hermes["result"]["run"]["session_id"],
10756            "cron_job42_20260902_120000"
10757        );
10758        // Ledger columns the uniform row does not carry survive on `source`.
10759        assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10760        assert_eq!(hermes["result"]["source"]["pid"], 4242);
10761        assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10762
10763        let openclaw = service.handle(request(
10764            2,
10765            "harness.v1.runs.get",
10766            json!({
10767                "harness": "openclaw",
10768                "id": "run_health_0002",
10769                "homes": jobs_fixture_homes(),
10770            }),
10771        ));
10772        assert_eq!(openclaw["result"]["run"]["status"], "error");
10773        // ORCH-13: the run's delivery is projected AND the store's own columns
10774        // stay verbatim on `source`, so nothing about the fire is lost.
10775        assert_eq!(
10776            openclaw["result"]["source"]["delivery_status"],
10777            "not-delivered"
10778        );
10779        assert_eq!(
10780            openclaw["result"]["source"]["delivery_error"],
10781            "channel_not_found"
10782        );
10783        assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10784        assert_eq!(
10785            openclaw["result"]["run"]["delivery"]["state"],
10786            "not-delivered"
10787        );
10788        assert_eq!(
10789            openclaw["result"]["run"]["delivery"]["last_error"],
10790            "channel_not_found"
10791        );
10792
10793        let missing = service.handle(request(
10794            3,
10795            "harness.v1.runs.get",
10796            json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10797        ));
10798        assert!(missing["error"]["message"]
10799            .as_str()
10800            .is_some_and(|message| message.contains("no run `no-such-run`")));
10801    }
10802
10803    #[test]
10804    fn runs_refuse_a_harness_that_keeps_no_run_store() {
10805        let mut service = HarnessSessionService::new();
10806        for (id, method, params) in [
10807            // Claude Code HAS scheduled jobs but no fire store: its fires are
10808            // ordinary turns. It must refuse, not answer with an empty list.
10809            (
10810                1,
10811                "harness.v1.runs.list",
10812                json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10813            ),
10814            (
10815                2,
10816                "harness.v1.runs.get",
10817                json!({"harness": "claude-code", "id": "anything"}),
10818            ),
10819            (
10820                3,
10821                "harness.v1.runs.list",
10822                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10823            ),
10824        ] {
10825            let response = service.handle(request(id, method, params));
10826            assert_eq!(response["error"]["code"], -32020, "{response}");
10827            assert!(response["error"]["message"]
10828                .as_str()
10829                .is_some_and(|message| message.contains("keeps no run store")));
10830            assert!(response.get("result").is_none());
10831        }
10832    }
10833
10834    #[test]
10835    fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10836        let scratch = std::env::temp_dir().join(format!(
10837            "supercode-runs-empty-{}-{}",
10838            std::process::id(),
10839            generated_session_id()
10840        ));
10841        std::fs::create_dir_all(&scratch).unwrap();
10842        let response = runs_list(json!({
10843            "harness": "openclaw",
10844            "homes": {"openclaw": scratch.clone()},
10845        }));
10846        let result = &response["result"];
10847        assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10848        assert_eq!(result["sources"][0]["state"], "absent_store");
10849        assert!(result["sources"][0]["path"]
10850            .as_str()
10851            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10852        std::fs::remove_dir_all(&scratch).ok();
10853    }
10854}