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.apply",
82    "harness.v1.jobs.notepad",
83    "harness.v1.jobs.notepad_set",
84    "harness.v1.jobs.notepad_delete",
85    "harness.v1.model_route.apply",
86    "harness.v1.sessions.new",
87    "harness.v1.sessions.reset",
88    "harness.v1.sessions.archive",
89    "harness.v1.sessions.delete",
90    "harness.v1.runs.list",
91    "harness.v1.runs.get",
92    "harness.v1.approvals.list",
93    "harness.v1.approvals.resolve",
94    "harness.v1.runtimes.capabilities",
95    "harness.v1.runtimes.start",
96    "harness.v1.runtimes.resume",
97    "harness.v1.runtimes.attach_existing",
98    "harness.v1.runtimes.attach",
99    "harness.v1.runtimes.send_input",
100    "harness.v1.runtimes.interrupt",
101    "harness.v1.runtimes.steer",
102    "harness.v1.runtimes.respond",
103    "harness.v1.runtimes.terminal_instructions",
104    "harness.v1.runtimes.acquire_control",
105    "harness.v1.runtimes.heartbeat",
106    "harness.v1.runtimes.detach",
107    "harness.v1.runtimes.close",
108    "harness.v1.profiles.list",
109    "harness.v1.profiles.get",
110    "harness.v1.profiles.create",
111    "harness.v1.profiles.delete",
112    "harness.v1.channels.list",
113    "harness.v1.routes.list",
114    "harness.v1.triggers.list",
115    "harness.v1.channels.status",
116    "harness.v1.orchestration.load",
117    "harness.v1.orchestration.save",
118    "harness.v1.orchestration.compile",
119    "harness.v1.orchestration.decompile",
120    "harness.v1.orchestration.import",
121    "harness.v1.orchestration.export",
122    "harness.v1.workflow.load",
123];
124
125/// Protocol namespace implemented by this service.
126pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
127/// Notification method emitted for followed-session changes.
128pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
129/// Notification method emitted for normalized session-activity transitions.
130pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
131/// Notification method emitted for revisioned session-list changes.
132pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
133/// Notification method emitted for live runtime events.
134pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
135
136/// Stateful persisted-session service. Each instance owns its follow
137/// subscriptions; discovery and loading remain read-only.
138pub struct HarnessSessionService {
139    catalog: HarnessCatalog,
140    followers: BTreeMap<String, SessionFollower>,
141    followed_sources: BTreeMap<String, FollowedSource>,
142    activity_subscriptions: BTreeMap<String, ActivitySubscription>,
143    index_subscriptions: BTreeMap<String, crate::session_index::SessionIndexSubscription>,
144    index_notifier: Arc<Notify>,
145    #[cfg(feature = "adapter-api")]
146    activity_monitor: crate::session_activity::SessionActivityMonitor,
147    next_subscription: u64,
148    runtimes: BTreeMap<String, Box<dyn RuntimeConnection>>,
149    /// Connections lent to a detached call that is running right now. The
150    /// runtime itself is OUT of `runtimes` for that whole call, and these
151    /// names are how a second caller is told the connection is busy rather
152    /// than unknown.
153    runtimes_in_flight: BTreeSet<String>,
154    terminal_launches: BTreeMap<String, StructuredLaunch>,
155    runtime_sequences: BTreeMap<String, u64>,
156    next_runtime: u64,
157    reduction_store_root: Option<PathBuf>,
158    /// ORCH-9: live permission/approval requests outstanding on the open
159    /// runtime connections above, fed by the same event pump that publishes
160    /// `harness.v1.runtimes.event`.
161    approvals: crate::approvals::ApprovalRegistry,
162    /// ORCH-9: supercode's own queued subagent approvals, when the host that
163    /// owns this service publishes its parent queue here.
164    subagent_approvals: Option<Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>>,
165}
166
167impl Default for HarnessSessionService {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl HarnessSessionService {
174    /// Create an empty service instance.
175    pub fn new() -> Self {
176        Self {
177            catalog: HarnessCatalog::new(),
178            followers: BTreeMap::new(),
179            followed_sources: BTreeMap::new(),
180            activity_subscriptions: BTreeMap::new(),
181            index_subscriptions: BTreeMap::new(),
182            index_notifier: Arc::new(Notify::new()),
183            #[cfg(feature = "adapter-api")]
184            activity_monitor: Default::default(),
185            next_subscription: 1,
186            runtimes: BTreeMap::new(),
187            runtimes_in_flight: BTreeSet::new(),
188            terminal_launches: BTreeMap::new(),
189            runtime_sequences: BTreeMap::new(),
190            next_runtime: 1,
191            reduction_store_root: None,
192            approvals: crate::approvals::ApprovalRegistry::new(),
193            subagent_approvals: None,
194        }
195    }
196
197    /// Override the trusted, service-owned store used for durable reduction
198    /// bundles. Embedders and tests use this to keep all writes inside an
199    /// explicitly selected root; the CLI otherwise uses the normal
200    /// `$SUPERCODE_HOME/sessions` location.
201    pub fn with_reduction_store_root(mut self, root: impl Into<PathBuf>) -> Self {
202        self.reduction_store_root = Some(root.into());
203        self
204    }
205
206    /// ORCH-9: publish the parent's own subagent-approval queue into
207    /// `harness.v1.approvals.list`.
208    ///
209    /// This is the SAME `Arc` an [`crate::Agent`] pushes into
210    /// (`Agent::pending_child_approvals`), so a host that runs supercode's own
211    /// loop beside this service surfaces those requests through the uniform
212    /// door without copying them anywhere.
213    pub fn observe_subagent_approvals(
214        &mut self,
215        queue: Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>,
216    ) {
217        self.subagent_approvals = Some(queue);
218    }
219
220    /// ORCH-9: every approval request this service can see, newest last.
221    ///
222    /// Two sources, both live: the requests outstanding on the open runtime
223    /// connections, and supercode's own queued subagent approvals. There is
224    /// no file or database source at the pinned harness versions (see
225    /// [`crate::approvals`]), so a stored or proposal row is never produced.
226    pub fn approvals(&self, query: &crate::approvals::ApprovalsQuery) -> Vec<crate::ApprovalRow> {
227        let now = crate::approvals::now_ms();
228        let mut rows = self.approvals.rows(now);
229        if let Some(queue) = self.subagent_approvals.as_ref() {
230            let queued = queue
231                .lock()
232                .unwrap_or_else(std::sync::PoisonError::into_inner)
233                .clone();
234            rows.extend(crate::approvals::subagent_rows(&queued, now));
235        }
236        rows.retain(|row| query.matches(row));
237        rows.sort_by(|left, right| {
238            left.requested_at_ms
239                .cmp(&right.requested_at_ms)
240                .then_with(|| left.id.cmp(&right.id))
241        });
242        rows
243    }
244
245    /// ORCH-20 (controlled tier): answer one listed approval request by its
246    /// row id and one uniform decision.
247    ///
248    /// The decision is translated into the option token and reply envelope
249    /// the door that raised the request already accepts
250    /// ([`crate::approvals::plan_reply`]), and the answer is then sent by
251    /// calling `harness.v1.runtimes.respond` itself — the same code path, the
252    /// same adapter, the same bookkeeping that drops the row. This verb adds
253    /// a translation and nothing else.
254    async fn approvals_resolve(
255        &mut self,
256        params: Value,
257    ) -> std::result::Result<Value, ServiceError> {
258        let params = decode::<crate::approvals::ApprovalsResolveParams>(params)?;
259        if params.id.trim().is_empty() {
260            return Err(ServiceError::InvalidParams(
261                "approvals resolve requires the `id` of a listed approval row".into(),
262            ));
263        }
264        let choice = match (params.decision, params.option_id.as_deref()) {
265            (Some(_), Some(_)) => {
266                return Err(ServiceError::InvalidParams(
267                    "approvals resolve takes either `decision` or `option_id`, not both".into(),
268                ))
269            }
270            (Some(decision), None) => crate::approvals::ApprovalChoice::Decision(decision),
271            (None, Some(option)) => crate::approvals::ApprovalChoice::Option(option.to_string()),
272            (None, None) => {
273                return Err(ServiceError::InvalidParams(format!(
274                    "approvals resolve requires `decision` ({}) or an explicit `option_id`",
275                    crate::approvals::ApprovalDecision::ALL
276                        .map(|decision| decision.as_str())
277                        .join(" | "),
278                )))
279            }
280        };
281        let resolution = self
282            .approvals
283            .resolution(&params.id, &choice)
284            .map_err(|error| ServiceError::InvalidParams(error.to_string()))?;
285        // The harness's own door, unchanged: this is the identical call
286        // `harness.v1.runtimes.respond` performs for a caller who built the
287        // envelope by hand, including dropping the answered row.
288        self.runtime_call(
289            "harness.v1.runtimes.respond",
290            json!({
291                "connection": resolution.connection,
292                "request_id": resolution.request_id,
293                "response": resolution.response,
294            }),
295        )
296        .await?;
297        Ok(json!({
298            "id": params.id,
299            "decision": params.decision.map(|decision| decision.as_str()),
300            "option_id": resolution.option_id,
301            "resolved": true,
302        }))
303    }
304
305    /// Return the edge-triggered wakeup used by session-index filesystem
306    /// subscriptions. Transports can await this instead of polling indexes.
307    #[cfg(feature = "adapter-api")]
308    pub fn session_index_notifier(&self) -> Arc<Notify> {
309        Arc::clone(&self.index_notifier)
310    }
311
312    /// Handle one JSON-RPC 2.0 request and return one JSON-RPC response.
313    #[cfg(feature = "adapter-api")]
314    pub fn handle(&mut self, request: Value) -> Value {
315        let id = request.get("id").cloned().unwrap_or(Value::Null);
316        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
317            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
318        }
319        let Some(method) = request.get("method").and_then(Value::as_str) else {
320            return rpc_error(id, -32600, "request is missing `method`");
321        };
322        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
323        match self.call(method, params) {
324            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
325            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
326            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
327            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
328            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
329            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
330        }
331    }
332
333    /// Handle either a persisted-session request or an asynchronous live
334    /// runtime request.
335    #[cfg(feature = "adapter-api")]
336    pub async fn handle_async(&mut self, request: Value) -> Value {
337        let method = request
338            .get("method")
339            .and_then(Value::as_str)
340            .unwrap_or_default();
341        if matches!(
342            method,
343            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe"
344        ) {
345            let id = request.get("id").cloned().unwrap_or(Value::Null);
346            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
347                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
348            }
349            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
350            return match self.inventory_call(method, params).await {
351                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
352                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
353                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
354                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
355                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
356                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
357            };
358        }
359        if matches!(
360            method,
361            "harness.v1.harnesses.auth.methods"
362                | "harness.v1.harnesses.auth.begin"
363                | "harness.v1.harnesses.auth.verify"
364        ) {
365            let id = request.get("id").cloned().unwrap_or(Value::Null);
366            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
367                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
368            }
369            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
370            return match self.harness_authentication_call(method, params).await {
371                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
372                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
373                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
374                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
375                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
376                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
377            };
378        }
379        // ORCH-19 controlled tier. Answered here rather than through the SDK
380        // operation dispatch below so the harness's OWN refusal reaches the
381        // caller: `sdk_error` collapses every `UnsupportedAction` to one
382        // generic sentence, and the whole point of this tier is that a
383        // refusal names which door the harness does have.
384        if matches!(
385            method,
386            "harness.v1.sessions.new"
387                | "harness.v1.sessions.reset"
388                | "harness.v1.sessions.archive"
389                | "harness.v1.sessions.delete"
390        ) {
391            let id = request.get("id").cloned().unwrap_or(Value::Null);
392            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
393                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
394            }
395            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
396            let verb = match method {
397                "harness.v1.sessions.new" => crate::SessionVerb::New,
398                "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
399                "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
400                _ => crate::SessionVerb::Delete,
401            };
402            return match self.mutate_session(verb, params).await {
403                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
404                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
405                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
406                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
407                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
408                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
409            };
410        }
411        if method == "harness.v1.sessions.message" {
412            let id = request.get("id").cloned().unwrap_or(Value::Null);
413            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
414                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
415            }
416            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
417            return match self.message_call(params).await {
418                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
419                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
420                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
421                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
422                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
423                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
424            };
425        }
426        if matches!(
427            method,
428            "harness.v1.harnesses.settings" | "harness.v1.harnesses.configure"
429        ) {
430            let id = request.get("id").cloned().unwrap_or(Value::Null);
431            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
432                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
433            }
434            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
435            return match self.harness_settings_call(method, params) {
436                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
437                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
438                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
439                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
440                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
441                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
442            };
443        }
444        if method == "harness.v1.sessions.activity.subscribe" {
445            let id = request.get("id").cloned().unwrap_or(Value::Null);
446            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
447                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
448            }
449            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
450            return match self.subscribe_session_activity(params).await {
451                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
452                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
453                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
454                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
455                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
456                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
457            };
458        }
459        if let Some(operation) = SdkOperation::from_method(method) {
460            let id = request.get("id").cloned().unwrap_or(Value::Null);
461            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
462                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
463            }
464            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
465            return match self.execute(SdkRequest { operation, params }).await {
466                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
467                Err(error) => sdk_rpc_error(id, &error),
468            };
469        }
470        if !method.starts_with("harness.v1.runtimes.") {
471            return self.handle(request);
472        }
473        let id = request.get("id").cloned().unwrap_or(Value::Null);
474        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
475            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
476        }
477        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
478        match self.runtime_call(method, params).await {
479            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
480            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
481            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
482            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
483            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
484            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
485        }
486    }
487
488    /// Poll all active subscriptions once and return zero or more JSON-RPC
489    /// notifications. Recoverable follower errors are delivered as events.
490    #[cfg(feature = "adapter-api")]
491    pub fn poll(&mut self) -> Vec<Value> {
492        let mut notifications = Vec::new();
493        for (subscription, follower) in &mut self.followers {
494            match follower.poll() {
495                Ok(Some(event)) => notifications.push(json!({
496                    "jsonrpc": "2.0",
497                    "method": SESSION_EVENT_METHOD,
498                    "params": {
499                        "subscription": subscription,
500                        "event": event.to_json(),
501                    }
502                })),
503                Ok(None) => {}
504                Err(error) => notifications.push(json!({
505                    "jsonrpc": "2.0",
506                    "method": SESSION_EVENT_METHOD,
507                    "params": {
508                        "subscription": subscription,
509                        "event": {
510                            "type": "watch_error",
511                            "recoverable": true,
512                            "message": error.to_string(),
513                        },
514                    }
515                })),
516            }
517        }
518        notifications
519    }
520
521    /// Report each followed session's live-runtime lifecycle state on that
522    /// session's own subscription, emitting only when the state changes.
523    ///
524    /// A growing transcript is not evidence that an agent is working, so the
525    /// state comes from the live-runtime registry and nowhere else. A followed
526    /// session with no registered Supercode runtime — a harness running outside
527    /// Supercode — reports `persisted`, which says plainly that its activity is
528    /// unknown rather than guessing at it. These events carry no sequence
529    /// number and no transcript content; they never interleave with the
530    /// content follower's sequenced stream.
531    #[cfg(feature = "adapter-api")]
532    pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
533        let registry = crate::LocalRuntimeRegistry::new();
534        let authorization = crate::RuntimeAuthorization::observer();
535        let mut notifications = Vec::new();
536        for (subscription, source) in &mut self.followed_sources {
537            let state = match registry
538                .source_state(&source.harness, &source.session_id, &authorization)
539                .await
540            {
541                Ok(Some(state)) => state,
542                Ok(None) => crate::RuntimeRegistryState::Persisted,
543                // A failed registry read is not evidence of a state change.
544                Err(_) => continue,
545            };
546            if source.reported.as_deref() == Some(state.as_str()) {
547                continue;
548            }
549            source.reported = Some(state.as_str().to_string());
550            notifications.push(json!({
551                "jsonrpc": "2.0",
552                "method": SESSION_EVENT_METHOD,
553                "params": {
554                    "subscription": subscription,
555                    "event": {"type": "runtime_state", "state": state.as_str()},
556                },
557            }));
558        }
559        notifications
560    }
561
562    /// Poll normalized activity subscriptions, emitting only proven state
563    /// transitions. Every subscription is bulk-sampled so stock-harness
564    /// process and registry discovery happens once per UI, not once per row.
565    #[cfg(feature = "adapter-api")]
566    pub async fn poll_session_activities(&mut self) -> Vec<Value> {
567        let subscriptions = self
568            .activity_subscriptions
569            .iter()
570            .map(|(id, subscription)| {
571                (
572                    id.clone(),
573                    subscription.locators.clone(),
574                    subscription.homes.clone(),
575                )
576            })
577            .collect::<Vec<_>>();
578        let mut notifications = Vec::new();
579        for (subscription_id, locators, homes) in subscriptions {
580            let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
581                // A failed evidence read proves no transition. Retain the last
582                // good state instead of flashing every row to persisted.
583                continue;
584            };
585            let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
586                continue;
587            };
588            let mut changed = Vec::new();
589            for activity in activities {
590                let key = activity.key();
591                if subscription
592                    .reported
593                    .get(&key)
594                    .is_some_and(|previous| previous.same_state(&activity))
595                {
596                    continue;
597                }
598                subscription.reported.insert(key, activity.clone());
599                changed.push(activity);
600            }
601            if !changed.is_empty() {
602                notifications.push(json!({
603                    "jsonrpc": "2.0",
604                    "method": SESSION_ACTIVITY_EVENT_METHOD,
605                    "params": {
606                        "subscription": subscription_id,
607                        "activities": changed,
608                    },
609                }));
610            }
611        }
612        notifications
613    }
614
615    /// Drain native-store invalidations and emit revisioned descriptor deltas.
616    /// An idle subscription performs no catalog or transcript reads between
617    /// its minute-scale recovery reconciliations.
618    #[cfg(feature = "adapter-api")]
619    pub fn poll_session_indexes(&mut self) -> Vec<Value> {
620        let mut notifications = Vec::new();
621        for (subscription, index) in &mut self.index_subscriptions {
622            let homes = index.homes().clone();
623            match index.poll() {
624                Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
625                    Ok(changes) => notifications.push(json!({
626                        "jsonrpc": "2.0",
627                        "method": SESSION_INDEX_EVENT_METHOD,
628                        "params": {
629                            "subscription": subscription,
630                            "revision": delta.revision,
631                            "changes": changes,
632                        },
633                    })),
634                    Err(error) => notifications.push(json!({
635                        "jsonrpc": "2.0",
636                        "method": SESSION_INDEX_EVENT_METHOD,
637                        "params": {
638                            "subscription": subscription,
639                            "error": {"recoverable": true, "message": error_message(error)},
640                        },
641                    })),
642                },
643                Ok(None) => {}
644                Err(error) => notifications.push(json!({
645                    "jsonrpc": "2.0",
646                    "method": SESSION_INDEX_EVENT_METHOD,
647                    "params": {
648                        "subscription": subscription,
649                        "error": {"recoverable": true, "message": error},
650                    },
651                })),
652            }
653        }
654        notifications
655    }
656
657    #[cfg(feature = "adapter-api")]
658    async fn subscribe_session_activity(
659        &mut self,
660        params: Value,
661    ) -> std::result::Result<Value, ServiceError> {
662        let params = decode::<ActivitySubscribeParams>(params)?;
663        if params.locators.is_empty() {
664            return Err(ServiceError::InvalidParams(
665                "sessions.activity.subscribe requires at least one locator".into(),
666            ));
667        }
668        if params.locators.len() > 2_048 {
669            return Err(ServiceError::InvalidParams(
670                "sessions.activity.subscribe accepts at most 2048 locators".into(),
671            ));
672        }
673        let initial = self
674            .activity_monitor
675            .resolve(&params.locators, &params.homes)
676            .await
677            .map_err(ServiceError::Sdk)?;
678        let subscription = format!("activity-sub-{}", self.next_subscription);
679        self.next_subscription += 1;
680        let reported = initial
681            .iter()
682            .cloned()
683            .map(|activity| (activity.key(), activity))
684            .collect();
685        self.activity_subscriptions.insert(
686            subscription.clone(),
687            ActivitySubscription {
688                locators: params.locators,
689                homes: params.homes,
690                reported,
691            },
692        );
693        Ok(json!({"subscription": subscription, "initial": initial}))
694    }
695
696    /// Non-blockingly sample one event from every connected live runtime.
697    #[cfg(feature = "adapter-api")]
698    pub async fn poll_runtimes(&mut self) -> Vec<Value> {
699        self.poll_sdk_events()
700            .await
701            .into_iter()
702            .map(|(connection, runtime_event)| {
703                json!({
704                    "jsonrpc": "2.0",
705                    "method": RUNTIME_EVENT_METHOD,
706                    "params": {
707                        "connection": connection,
708                        "session_id": runtime_event.session_id,
709                        "sequence": runtime_event.event.sequence,
710                        "event": {
711                            "kind": runtime_event.event.kind,
712                            "payload": runtime_event.event.payload,
713                        },
714                    },
715                })
716            })
717            .collect()
718    }
719
720    async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
721        let mut events = Vec::new();
722        let mut closed = Vec::new();
723        let now_ms = crate::approvals::now_ms();
724        for (connection, runtime) in &mut self.runtimes {
725            let session_id = runtime.handle().runtime_id.clone();
726            let harness = runtime.handle().harness.clone();
727            // Drain what the runtime already has: a turn is several events
728            // (updates, then the protocol's completion), and delivering one
729            // per poll would cost a poll interval each. A zero timeout takes
730            // only what is ready — an idle runtime costs nothing.
731            for _ in 0..256 {
732                match tokio::time::timeout(Duration::ZERO, runtime.next_event()).await {
733                    Ok(Ok(Some(event))) => {
734                        let terminal = event.kind == "transport_closed";
735                        // ORCH-9: a permission/approval request arrives as an
736                        // ordinary event; it becomes listable here and stops
737                        // being listable when `runtimes.respond` answers it.
738                        self.approvals
739                            .observe(connection, &harness, &session_id, &event, now_ms);
740                        let next_sequence = self
741                            .runtime_sequences
742                            .entry(session_id.clone())
743                            .or_insert(0);
744                        let sequence = event.sequence.unwrap_or_else(|| {
745                            *next_sequence = next_sequence.saturating_add(1);
746                            *next_sequence
747                        });
748                        *next_sequence = (*next_sequence).max(sequence);
749                        events.push((
750                            connection.clone(),
751                            SdkRuntimeEvent {
752                                session_id: session_id.clone(),
753                                event: SdkEvent {
754                                    sequence,
755                                    kind: event.kind,
756                                    payload: event.payload,
757                                },
758                            },
759                        ));
760                        if terminal {
761                            closed.push(connection.clone());
762                            break;
763                        }
764                    }
765                    Ok(Ok(None)) => {
766                        let sequence = self
767                            .runtime_sequences
768                            .entry(session_id.clone())
769                            .or_insert(0);
770                        *sequence = sequence.saturating_add(1);
771                        events.push((
772                        connection.clone(),
773                        SdkRuntimeEvent {
774                            session_id,
775                            event: SdkEvent {
776                                sequence: *sequence,
777                                kind: "transport_closed".into(),
778                                payload: json!({"message": "Harness runtime transport closed."}),
779                            },
780                        },
781                    ));
782                        closed.push(connection.clone());
783                        break;
784                    }
785                    Err(_) => break,
786                    Ok(Err(error)) => {
787                        let sequence = self
788                            .runtime_sequences
789                            .entry(session_id.clone())
790                            .or_insert(0);
791                        *sequence = sequence.saturating_add(1);
792                        events.push((
793                        connection.clone(),
794                        SdkRuntimeEvent {
795                            session_id,
796                            event: SdkEvent {
797                                sequence: *sequence,
798                                kind: "transport_error".into(),
799                                payload: json!({"message": error.to_string(), "terminal": true}),
800                            },
801                        },
802                    ));
803                        closed.push(connection.clone());
804                        break;
805                    }
806                }
807            }
808        }
809        for connection in closed {
810            if let Some(runtime) = self.runtimes.remove(&connection) {
811                self.runtime_sequences.remove(&runtime.handle().runtime_id);
812            }
813            self.terminal_launches.remove(&connection);
814            // A connection that is gone cannot answer anything it was
815            // holding; those requests stop being listable with it.
816            self.approvals.forget(&connection);
817        }
818        events
819    }
820
821    fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
822        match method {
823            "harness.v1.capabilities" => Ok(json!({
824                "version": HARNESS_SERVICE_VERSION,
825                "sdk": self.capabilities(),
826                "methods": HARNESS_SERVICE_METHODS,
827                "notifications": [
828                    SESSION_EVENT_METHOD,
829                    SESSION_ACTIVITY_EVENT_METHOD,
830                    SESSION_INDEX_EVENT_METHOD,
831                    RUNTIME_EVENT_METHOD
832                ],
833                "harnesses": harness_support_registry()
834                    .harnesses
835                    .into_iter()
836                    .map(|harness| harness.id)
837                    .collect::<Vec<_>>(),
838            })),
839            "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
840                .map_err(|error| ServiceError::Operation(error.to_string())),
841            "harness.v1.profiles.list" | "harness.v1.profiles.get" => profiles_call(method, params),
842            // ORCH-21 controlled tier. Each verb translates to the HARNESS'S
843            // OWN profile verb and runs it (`crate::profiles_control`);
844            // supercode makes and removes nothing itself. The row returned is
845            // re-read through the ORCH-10 loader afterwards, and `ran`
846            // narrates the exact command.
847            "harness.v1.profiles.create" => {
848                mutate_profile(crate::profiles_control::ProfileVerb::Create, params)
849            }
850            "harness.v1.profiles.delete" => {
851                mutate_profile(crate::profiles_control::ProfileVerb::Delete, params)
852            }
853            "harness.v1.channels.list" | "harness.v1.channels.status" => {
854                channels_call(method, params)
855            }
856            // ORCH-15 observed tier: which profile / agent a surface tuple
857            // resolves to, read from each gateway harness's own config.
858            "harness.v1.routes.list" => routes_call(params),
859            // ORCH-16 observed tier: inbound webhook routes / hook mappings.
860            "harness.v1.triggers.list" => triggers_call(params),
861            // ONT-4: the orchestration doors. One home folder in, one typed orchestration
862            // value out (and back). Every one of the four is
863            // `crate::orchestration_doors`, which the `supercode orchestration` verbs call
864            // too — the RPC adds nothing but the envelope. A vault VALUE
865            // never crosses this wire: a load or a compile answers with the
866            // `.env` KEY NAMES, and a caller that needs a value reads the
867            // home's own `.env`.
868            // the workflow layer's read door: a harness's board as one typed value,
869            // the same code the `supercode workflow load` verb calls
870            "harness.v1.workflow.load" => {
871                let params = decode::<WorkflowLoadParams>(params)?;
872                let read =
873                    crate::workflow_doors::load(params.from, &params.home).map_err(operation)?;
874                serde_json::to_value(read)
875                    .map_err(|error| ServiceError::Operation(error.to_string()))
876            }
877            "harness.v1.orchestration.load" => {
878                let params = decode::<OrchestrationLoadParams>(params)?;
879                let read = crate::orchestration_doors::load(&params.root, params.flavor)
880                    .map_err(operation)?;
881                serde_json::to_value(read)
882                    .map_err(|error| ServiceError::Operation(error.to_string()))
883            }
884            "harness.v1.orchestration.save" => {
885                let params = decode::<OrchestrationSaveParams>(params)?;
886                let saved = crate::orchestration_doors::save(
887                    &params.root,
888                    params.orchestration,
889                    params.vault,
890                )
891                .map_err(operation)?;
892                serde_json::to_value(saved)
893                    .map_err(|error| ServiceError::Operation(error.to_string()))
894            }
895            "harness.v1.orchestration.compile" => {
896                let params = decode::<OrchestrationCompileParams>(params)?;
897                let read = crate::orchestration_doors::compile(params.from, &params.home)
898                    .map_err(operation)?;
899                serde_json::to_value(read)
900                    .map_err(|error| ServiceError::Operation(error.to_string()))
901            }
902            "harness.v1.orchestration.decompile" => {
903                let params = decode::<OrchestrationDecompileParams>(params)?;
904                let report = crate::orchestration_doors::decompile(
905                    params.to,
906                    params.orchestration,
907                    &params.source,
908                    params.source_flavor,
909                    &params.dest,
910                    params.vault,
911                )
912                .map_err(operation)?;
913                serde_json::to_value(report)
914                    .map_err(|error| ServiceError::Operation(error.to_string()))
915            }
916            // a migration keeps the credential in this process: a compile and
917            // a save (import), a load and a decompile (export), composed here
918            // because composed by a client the secret would have to cross
919            // the wire
920            "harness.v1.orchestration.import" => {
921                let params = decode::<OrchestrationImportParams>(params)?;
922                let imported =
923                    crate::orchestration_doors::import(params.from, &params.home, &params.into)
924                        .map_err(operation)?;
925                serde_json::to_value(imported)
926                    .map_err(|error| ServiceError::Operation(error.to_string()))
927            }
928            "harness.v1.orchestration.export" => {
929                let params = decode::<OrchestrationExportParams>(params)?;
930                let report =
931                    crate::orchestration_doors::export(params.to, &params.root, &params.dest)
932                        .map_err(operation)?;
933                serde_json::to_value(report)
934                    .map_err(|error| ServiceError::Operation(error.to_string()))
935            }
936            // ORCH-12 observed tier: read and search the persistent memory
937            // documents a harness keeps on disk. Read-only — every write
938            // (`hermes memory off`, `openclaw memory forget|reset`, Claude
939            // Code's `/memory`) stays the harness's own verb. A harness with
940            // no memory store is refused with UnsupportedAction.
941            "harness.v1.memory.show" | "harness.v1.memory.search" => memory_call(method, params),
942            // ORCH-11 observed tier: read-only enumeration of every harness's
943            // installed skill packages. An unknown harness id is refused with
944            // UnsupportedAction — every harness supports skills, so a filter
945            // that matches nothing is a caller error, never an empty listing.
946            "harness.v1.skills.list" => {
947                let query = decode::<crate::skills::SkillsQuery>(params)?;
948                if let Some(harness) = query.harness.as_deref() {
949                    if !crate::skills::SKILL_HARNESSES.contains(&harness) {
950                        return Err(ServiceError::UnsupportedAction(format!(
951                            "`{harness}` has no skills root supercode reads"
952                        )));
953                    }
954                }
955                serde_json::to_value(crate::skills::list_skills(&query))
956                    .map_err(|error| ServiceError::Operation(error.to_string()))
957            }
958            // ORCH-22 controlled tier: each verb goes through the door the
959            // HARNESS publishes — `hermes skills install|uninstall`,
960            // `openclaw skills install`, and for the core four the loader's
961            // own directory, which is the only skills door those harnesses
962            // have. supercode resolves no registry and unpacks no archive.
963            // The row returned is re-read through the ORCH-11 loader
964            // afterwards, and `ran` narrates exactly what was performed.
965            "harness.v1.skills.install" => {
966                mutate_skill(crate::skills_control::SkillVerb::Install, params)
967            }
968            "harness.v1.skills.remove" => {
969                mutate_skill(crate::skills_control::SkillVerb::Remove, params)
970            }
971            // ORCH-9 observed tier: the approval requests waiting for an
972            // answer. At the pinned harness versions the only uniform source
973            // is a LIVE request held by an open runtime connection, plus
974            // supercode's own queued subagent approvals — neither Hermes
975            // 0.21.0 nor OpenClaw 2026.7.1-2 has an approvals door to read
976            // (see `crate::approvals`). A harness whose runtime cannot carry
977            // a protocol request at all is refused by name.
978            "harness.v1.approvals.list" => {
979                let query = decode::<crate::approvals::ApprovalsQuery>(params)?;
980                if let Some(harness) = query.harness.as_deref() {
981                    if !crate::approvals::lists_approvals(harness) {
982                        return Err(ServiceError::UnsupportedAction(format!(
983                            "`{harness}` has no runtime door that carries an approval request"
984                        )));
985                    }
986                }
987                serde_json::to_value(self.approvals(&query))
988                    .map_err(|error| ServiceError::Operation(error.to_string()))
989            }
990            "harness.v1.sessions.discover" => {
991                let query = decode::<DiscoveryQuery>(params)?;
992                let page = discover_session_page(&query).map_err(operation)?;
993                // Claude Code is the one harness that publishes its RUNNING
994                // sessions. The registry is read once per discovery and joined
995                // by session id; every record in it has already survived a
996                // `kill(pid, 0)` liveness check inside `read_registry`.
997                let peers = if page
998                    .sessions
999                    .iter()
1000                    .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
1001                {
1002                    crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(
1003                        &query.homes,
1004                    ))
1005                } else {
1006                    Vec::new()
1007                };
1008                let activities = crate::session_activity::resolve_stock_session_activities(
1009                    &page
1010                        .sessions
1011                        .iter()
1012                        .map(|session| session.locator.clone())
1013                        .collect::<Vec<_>>(),
1014                    &query.homes,
1015                )
1016                .into_iter()
1017                .map(|activity| (activity.key(), activity))
1018                .collect::<BTreeMap<_, _>>();
1019                let sessions = page
1020                    .sessions
1021                    .into_iter()
1022                    .map(|session| {
1023                        let mut value = live_descriptor_value(&session, &peers)?;
1024                        let activity_key = (
1025                            session.locator.harness.as_str().to_string(),
1026                            session.locator.session_id.clone(),
1027                        );
1028                        if let Some(activity) = activities.get(&activity_key) {
1029                            value["activity"] = serde_json::to_value(activity)
1030                                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1031                            if let Some(status) = legacy_live_status(activity) {
1032                                value["live_status"] = json!(status);
1033                            }
1034                        }
1035                        Ok(value)
1036                    })
1037                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1038                let mut result = json!({"sessions": sessions, "next_cursor": page.next_cursor});
1039                // Preserve the metadata-only wire shape, but carry the catalog's
1040                // proof/counts when the caller explicitly requests preview search.
1041                if query.search_previews {
1042                    result["receipt"] = serde_json::to_value(page.receipt)
1043                        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1044                }
1045                Ok(result)
1046            }
1047            "harness.v1.sessions.load" => {
1048                let params = decode::<LoadSessionParams>(params)?;
1049                if let Some(options) = &params.options {
1050                    options.validate()?;
1051                    if let Some(result) = indexed_claude_window(&params.read.locator, options)? {
1052                        return Ok(result);
1053                    }
1054                    return load_session(&params.read.locator)
1055                        .map(|session| projected_session_result(&session, options))
1056                        .map_err(operation);
1057                }
1058                let mut session = if params.read.display_history() {
1059                    self.catalog
1060                        .load_display_view(
1061                            &params.read.locator,
1062                            params.read.read_fidelity(),
1063                            params.read.tail_messages().unwrap_or(500),
1064                        )
1065                        .map_err(crate::Error::from)
1066                } else if params.read.include_subagents() {
1067                    load_session_with_fidelity(&params.read.locator, params.read.read_fidelity())
1068                } else {
1069                    self.catalog
1070                        .load_parent_with_fidelity(
1071                            &params.read.locator,
1072                            params.read.read_fidelity(),
1073                        )
1074                        .map_err(crate::Error::from)
1075                }
1076                .map_err(operation)?;
1077                params.read.bound_session(&mut session);
1078                Ok(json!({"session": normalized_session_json(&session)}))
1079            }
1080            "harness.v1.sessions.follow" => {
1081                let params = decode::<LocatorParams>(params)?;
1082                let mut follower = self
1083                    .catalog
1084                    .follow_read_view(
1085                        &params.locator,
1086                        params.read_fidelity(),
1087                        params.include_subagents(),
1088                        params.tail_messages(),
1089                        params.max_message_chars(),
1090                        params.display_history(),
1091                    )
1092                    .map_err(operation)?;
1093                let initial = follower
1094                    .poll()
1095                    .map_err(operation)?
1096                    .map(|event| event.to_json());
1097                let subscription = format!("sub-{}", self.next_subscription);
1098                self.next_subscription += 1;
1099                self.followers.insert(subscription.clone(), follower);
1100                self.followed_sources.insert(
1101                    subscription.clone(),
1102                    FollowedSource {
1103                        harness: params.locator.harness.as_str().to_string(),
1104                        session_id: params.locator.session_id.clone(),
1105                        reported: None,
1106                    },
1107                );
1108                Ok(json!({"subscription": subscription, "initial": initial}))
1109            }
1110            "harness.v1.sessions.unfollow" => {
1111                let params = decode::<UnfollowParams>(params)?;
1112                self.followed_sources.remove(&params.subscription);
1113                Ok(json!({
1114                    "removed": self.followers.remove(&params.subscription).is_some()
1115                }))
1116            }
1117            "harness.v1.sessions.activity.unsubscribe" => {
1118                let params = decode::<UnfollowParams>(params)?;
1119                Ok(json!({
1120                    "removed": self.activity_subscriptions.remove(&params.subscription).is_some()
1121                }))
1122            }
1123            "harness.v1.sessions.index.subscribe" => {
1124                let query = decode::<DiscoveryQuery>(params)?;
1125                crate::session_index::validate_query(&query)
1126                    .map_err(ServiceError::InvalidParams)?;
1127                let homes = query.homes.clone();
1128                let (index, initial) = crate::session_index::SessionIndexSubscription::open(
1129                    query,
1130                    Arc::clone(&self.index_notifier),
1131                )
1132                .map_err(ServiceError::Operation)?;
1133                let peers = peers_for_descriptors(&initial, &homes);
1134                let initial = initial
1135                    .iter()
1136                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1137                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1138                let subscription = format!("index-sub-{}", self.next_subscription);
1139                self.next_subscription += 1;
1140                self.index_subscriptions.insert(subscription.clone(), index);
1141                Ok(json!({
1142                    "subscription": subscription,
1143                    "revision": 1,
1144                    "initial": initial,
1145                }))
1146            }
1147            "harness.v1.sessions.index.resize" => {
1148                let params = decode::<IndexResizeParams>(params)?;
1149                crate::session_index::validate_limit(params.limit)
1150                    .map_err(ServiceError::InvalidParams)?;
1151                let index = self
1152                    .index_subscriptions
1153                    .get_mut(&params.subscription)
1154                    .ok_or_else(|| {
1155                        ServiceError::InvalidParams("unknown session index subscription".into())
1156                    })?;
1157                let prepared = index
1158                    .prepare_resize(params.limit)
1159                    .map_err(ServiceError::Operation)?;
1160                let peers = peers_for_descriptors(&prepared.page.sessions, index.homes());
1161                let initial = prepared
1162                    .page
1163                    .sessions
1164                    .iter()
1165                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1166                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1167                let response = json!({
1168                    "subscription": params.subscription,
1169                    "revision": prepared.revision,
1170                    "initial": initial,
1171                    "receipt": prepared.page.receipt,
1172                });
1173                index.commit_resize(prepared);
1174                Ok(response)
1175            }
1176            "harness.v1.sessions.index.unsubscribe" => {
1177                let params = decode::<UnfollowParams>(params)?;
1178                Ok(json!({
1179                    "removed": self.index_subscriptions.remove(&params.subscription).is_some()
1180                }))
1181            }
1182            "harness.v1.sessions.import" => {
1183                let params = decode::<ImportSessionParams>(params)?;
1184                let session = Session::load_str(&params.content, params.source_harness.into())
1185                    .map_err(operation)?;
1186                Ok(json!({"session": normalized_session_json(&session)}))
1187            }
1188            "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
1189                let params = decode::<ExportSessionParams>(params)?;
1190                let session = load_session(&params.locator).map_err(operation)?;
1191                let artifact = session_artifact(&params.locator, &session, params.target_harness)?;
1192                if method == "harness.v1.sessions.export"
1193                    && params.target_harness == TransferFormat::Hermes
1194                {
1195                    // UNI-18: write through Hermes's own door, never into its store
1196                    let imported = crate::hermes_import::import_into_hermes(&session, None)
1197                        .map_err(operation)?;
1198                    return Ok(json!({"artifact": artifact, "imported": imported}));
1199                }
1200                Ok(json!({"artifact": artifact}))
1201            }
1202            "harness.v1.sessions.reduce" => {
1203                let params = decode::<ReduceSessionParams>(params)?;
1204                self.reduce_session(params)
1205            }
1206            "harness.v1.sessions.branch" => {
1207                let params = decode::<BranchSessionParams>(params)?;
1208                let session = load_session(&params.locator).map_err(operation)?;
1209                let storage = params.locator.storage.path().display().to_string();
1210                let bootstrap_prompt = format!(
1211                    "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.",
1212                    params.locator.harness.as_str(), params.locator.session_id, storage
1213                );
1214                let artifact = params
1215                    .target_harness
1216                    .map(|target| session_artifact(&params.locator, &session, target))
1217                    .transpose()?;
1218                Ok(json!({
1219                    "parent": params.locator,
1220                    "session": normalized_session_json(&session),
1221                    "bootstrap_prompt": bootstrap_prompt,
1222                    "artifact": artifact,
1223                }))
1224            }
1225            "harness.v1.sessions.handoff" => {
1226                let params = decode::<HandoffSessionParams>(params)?;
1227                let session = load_session(&params.locator).map_err(operation)?;
1228                let cwd = params
1229                    .cwd
1230                    .or_else(|| session.meta.cwd.clone())
1231                    .unwrap_or_else(|| PathBuf::from("."));
1232                let artifact =
1233                    handoff_artifact(&params.locator, &session, params.target_harness, &cwd)?;
1234                let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
1235                    ServiceError::Operation(
1236                        "handoff artifact omitted target session identity".into(),
1237                    )
1238                })?;
1239                let instructions =
1240                    handoff_instructions(params.target_harness, target_session_id, &cwd);
1241                Ok(json!({
1242                    "artifact": artifact,
1243                    "launch": instructions.launch,
1244                    "materialize": instructions.materialize,
1245                    "requires_materialization": instructions.requires_materialization,
1246                    "note": instructions.note,
1247                }))
1248            }
1249            "harness.v1.sessions.materialize" => {
1250                let params = decode::<MaterializeSessionParams>(params)?;
1251                let locator = crate::native_materialize::materialize_native_artifact(
1252                    params.artifact,
1253                    &params.cwd,
1254                )
1255                .map_err(ServiceError::Operation)?;
1256                Ok(json!({"locator": locator}))
1257            }
1258            // ORCH-7 observed tier. Read-only: the handlers open the harness's
1259            // own job store (Claude Code's session JSONL, Hermes's and
1260            // OpenClaw's `cron/jobs.json`) and never write, fire, or schedule.
1261            "harness.v1.jobs.list" => {
1262                let query = decode::<crate::jobs::JobsQuery>(params)?;
1263                if let Some(harness) = query.harness.as_deref() {
1264                    refuse_harness_without_jobs(harness, "jobs.list")?;
1265                }
1266                let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1267                serde_json::to_value(listing)
1268                    .map_err(|error| ServiceError::Operation(error.to_string()))
1269            }
1270            "harness.v1.jobs.get" => {
1271                let params = decode::<JobsGetParams>(params)?;
1272                refuse_harness_without_jobs(&params.harness, "jobs.get")?;
1273                match crate::jobs::get_job(&params.harness, &params.id, &params.homes)
1274                    .map_err(operation)?
1275                {
1276                    Some((job, source)) => Ok(json!({"job": job, "source": source})),
1277                    None => Err(ServiceError::Operation(format!(
1278                        "`{}` has no scheduled job `{}`",
1279                        params.harness, params.id
1280                    ))),
1281                }
1282            }
1283            // ORCH-18 controlled tier. Each verb translates to the HARNESS'S
1284            // OWN cron verb and runs it (`crate::jobs_control`); supercode
1285            // schedules nothing. The row returned is re-read from the
1286            // harness's store afterwards, and `ran` narrates the exact command
1287            // with any credential redacted.
1288            "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1289            "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1290            "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1291            "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1292            "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1293            "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1294            "harness.v1.jobs.notepad"
1295            | "harness.v1.jobs.notepad_set"
1296            | "harness.v1.jobs.notepad_delete" => {
1297                let request = decode::<crate::jobs_notepad::JobNotepadRequest>(params)?;
1298                refuse_harness_without_jobs(&request.harness, "jobs.notepad")?;
1299                let answer = match method {
1300                    "harness.v1.jobs.notepad_set" => crate::jobs_notepad::set(&request),
1301                    "harness.v1.jobs.notepad_delete" => crate::jobs_notepad::delete(&request),
1302                    _ => crate::jobs_notepad::read(&request),
1303                }
1304                .map_err(job_control_error)?;
1305                serde_json::to_value(answer)
1306                    .map_err(|error| ServiceError::Operation(error.to_string()))
1307            }
1308            "harness.v1.model_route.apply" => {
1309                let request = decode::<crate::model_route::ModelRouteApply>(params)?;
1310                let outcome = crate::model_route::apply(&request).map_err(job_control_error)?;
1311                serde_json::to_value(outcome)
1312                    .map_err(|error| ServiceError::Operation(error.to_string()))
1313            }
1314            "harness.v1.jobs.apply" => {
1315                let request = decode::<crate::jobs_apply::JobsApply>(params)?;
1316                refuse_harness_without_jobs(&request.harness, "jobs.apply")?;
1317                let outcome = crate::jobs_apply::apply(&request).map_err(job_control_error)?;
1318                serde_json::to_value(outcome)
1319                    .map_err(|error| ServiceError::Operation(error.to_string()))
1320            }
1321            // ORCH-8 observed tier. Read-only: the handlers open the harness's
1322            // own run store (Hermes's `cron/executions.db`, OpenClaw's
1323            // `cron_run_logs`) and never claim, retry, or prune a fire.
1324            "harness.v1.runs.list" => {
1325                let query = decode::<crate::runs::RunsQuery>(params)?;
1326                if let Some(harness) = query.harness.as_deref() {
1327                    refuse_harness_without_runs(harness, "runs.list")?;
1328                }
1329                let listing = crate::runs::list_runs(&query).map_err(operation)?;
1330                serde_json::to_value(listing)
1331                    .map_err(|error| ServiceError::Operation(error.to_string()))
1332            }
1333            "harness.v1.runs.get" => {
1334                let params = decode::<RunsGetParams>(params)?;
1335                refuse_harness_without_runs(&params.harness, "runs.get")?;
1336                match crate::runs::get_run(&params.harness, &params.id, &params.homes)
1337                    .map_err(operation)?
1338                {
1339                    Some((run, source)) => Ok(json!({"run": run, "source": source})),
1340                    None => Err(ServiceError::Operation(format!(
1341                        "`{}` has no run `{}`",
1342                        params.harness, params.id
1343                    ))),
1344                }
1345            }
1346            "harness.v1.sessions.resume_instructions" => {
1347                let params = decode::<ResumeInstructionsParams>(params)?;
1348                let session = load_session(&params.locator).map_err(operation)?;
1349                let cwd = params
1350                    .cwd
1351                    .or(session.meta.cwd)
1352                    .unwrap_or_else(|| PathBuf::from("."));
1353                let launch = resume_launch(
1354                    params.locator.harness.as_str(),
1355                    &params.locator.session_id,
1356                    &cwd,
1357                    params.policy,
1358                )?;
1359                Ok(json!({"launch": launch}))
1360            }
1361            _ => Err(ServiceError::MethodNotFound),
1362        }
1363    }
1364
1365    fn reduce_session(
1366        &self,
1367        params: ReduceSessionParams,
1368    ) -> std::result::Result<Value, ServiceError> {
1369        let session = load_session(&params.locator).map_err(operation)?;
1370        if session.messages.is_empty() {
1371            return Err(ServiceError::InvalidParams(
1372                "cannot reduce an empty session".into(),
1373            ));
1374        }
1375        let keep_last = params.keep_last.clamp(1, 128);
1376        let policy = reduce::ReductionPolicy {
1377            clear_turns_older_than: Some(keep_last),
1378            ..Default::default()
1379        };
1380        let (view, log) =
1381            reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1382        if log.reductions.is_empty() {
1383            return Err(ServiceError::UnsupportedAction(format!(
1384                "session `{}` is already too small for a meaningful reversible reduction",
1385                params.locator.session_id
1386            )));
1387        }
1388        let source_tokens = tokens::estimate_view_tokens(&session.messages);
1389        let reduced_tokens = tokens::estimate_view_tokens(&view);
1390        if reduced_tokens >= source_tokens {
1391            return Err(ServiceError::UnsupportedAction(format!(
1392                "session `{}` has no token-reducing reversible projection",
1393                params.locator.session_id
1394            )));
1395        }
1396
1397        let store_root = self
1398            .reduction_store_root
1399            .clone()
1400            .unwrap_or_else(default_reduction_store_root);
1401        let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1402        let rescue_id = format!("rescue-{}", generated_session_id());
1403        let imported = session
1404            .imported_message_count
1405            .unwrap_or(session.messages.len())
1406            .min(session.messages.len());
1407        let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1408        let view_jsonl = messages_jsonl(&view)?;
1409        let title = format!(
1410            "Reduced {} continuation from {}",
1411            params.target_harness.id(),
1412            params.locator.session_id
1413        );
1414
1415        // Durability order is intentional: the full source of truth lands
1416        // before either object that can refer to it. A crash may leave an
1417        // unused sidecar, but can never leave a reduced view whose originals
1418        // were not durably written first.
1419        store
1420            .save_sidecar(&rescue_id, &sidecar_jsonl)
1421            .map_err(operation)?;
1422        store
1423            .save_reduction_log(&rescue_id, &log)
1424            .map_err(operation)?;
1425        store
1426            .save(&rescue_id, &title, &view_jsonl)
1427            .map_err(operation)?;
1428
1429        let source_bytes = serde_json::to_vec(&session.messages)
1430            .map_err(|error| ServiceError::Operation(error.to_string()))?
1431            .len() as u64;
1432        let reduced_bytes = serde_json::to_vec(&view)
1433            .map_err(|error| ServiceError::Operation(error.to_string()))?
1434            .len() as u64;
1435        store
1436            .set_reduction_stats(
1437                &rescue_id,
1438                &title,
1439                source_bytes,
1440                reduced_bytes,
1441                log.reductions.len() as u32,
1442            )
1443            .map_err(operation)?;
1444
1445        // The receipt is issued only after a real disk reload. This proves
1446        // the exact files another process will consume, not the convenient
1447        // in-memory values that produced them.
1448        let reloaded_sidecar = store
1449            .load_sidecar(&rescue_id)
1450            .map_err(operation)?
1451            .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1452        let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1453        let reloaded_log = store
1454            .load_reduction_log(&rescue_id)
1455            .map_err(operation)?
1456            .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1457        let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1458        reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1459        // `sc.reduction` is deliberately in-memory-only metadata: it must
1460        // never leak onto a provider-facing transcript. Reapplying the
1461        // durable log to the durable sidecar restores those ids. Comparing
1462        // its wire form with the transcript reloaded above proves that the
1463        // persisted view is exactly the deterministic projection before we
1464        // use the restamped form for inversion.
1465        let (restamped_view, restamped_log) =
1466            reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1467        if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1468            return Err(ServiceError::Operation(
1469                "persisted reduction view does not match its durable log and sidecar".into(),
1470            ));
1471        }
1472        if restamped_log != reloaded_log {
1473            return Err(ServiceError::Operation(
1474                "reapplying the durable reduction log changed its identity".into(),
1475            ));
1476        }
1477        let inverted =
1478            reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1479        if inverted != session.messages {
1480            return Err(ServiceError::Operation(
1481                "reduction inversion did not restore the source messages byte-exactly".into(),
1482            ));
1483        }
1484
1485        let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1486        let sidecar_path = store.sidecar_path(&rescue_id);
1487        let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1488        let bootstrap_prompt = reduced_bootstrap_prompt(
1489            &params.locator,
1490            params.target_harness,
1491            &view_jsonl,
1492            &sidecar_path,
1493            &reduction_log_path,
1494        );
1495        let mut reduced_session = session.clone();
1496        reduced_session.meta.session_id = Some(rescue_id.clone());
1497        reduced_session.messages = view;
1498
1499        Ok(json!({
1500            "session": normalized_session_json(&reduced_session),
1501            "bootstrap_prompt": bootstrap_prompt,
1502            "receipt": {
1503                "id": rescue_id,
1504                "sidecar_id": rescue_id,
1505                "source_harness": params.locator.harness,
1506                "target_harness": params.target_harness.id(),
1507                "source_tokens": source_tokens,
1508                "reduced_tokens": reduced_tokens,
1509                "ratio": ratio,
1510                "source_bytes": source_bytes,
1511                "reduced_bytes": reduced_bytes,
1512                "reductions": reloaded_log.reductions.len(),
1513                "sidecar_path": sidecar_path,
1514                "reduction_log_path": reduction_log_path,
1515                "verified": true,
1516                "reversible": true,
1517            }
1518        }))
1519    }
1520
1521    /// Recognize the one request family whose waiting happens entirely
1522    /// outside this service's state, and hand a transport the half it can run
1523    /// off the task that owns the service.
1524    ///
1525    /// Opening a runtime is the only door here that waits on a foreign
1526    /// program: it spawns the harness's own binary and completes that
1527    /// program's protocol handshake, which takes as long as the program takes
1528    /// to answer. A transport that awaited the whole request inline would
1529    /// stop reading its own input for that whole time, so ONE slow launch
1530    /// would queue every later request on the same server — including reads
1531    /// like `sessions.discover` that touch no runtime at all. Splitting the
1532    /// request lets the transport spawn [`RuntimeOpen::open`] and keep
1533    /// reading, then pay only the short bookkeeping half
1534    /// ([`Self::register_open_runtime`]) when the runtime is up.
1535    ///
1536    /// `None` for every other method: those are answered by
1537    /// [`Self::handle_async`] as before.
1538    pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1539        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1540            return None;
1541        }
1542        let method = request.get("method").and_then(Value::as_str)?;
1543        if !RUNTIME_OPEN_METHODS.contains(&method) {
1544            return None;
1545        }
1546        Some(RuntimeOpen {
1547            id: request.get("id").cloned().unwrap_or(Value::Null),
1548            method: method.to_string(),
1549            params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1550        })
1551    }
1552
1553    /// Recognize a [`DETACHED_METHODS`] request and hand a transport the
1554    /// whole of it: the service-state half is read here and now, and what
1555    /// remains waits on a foreign program with nothing of this service's in
1556    /// hand.
1557    ///
1558    /// Same reason as [`Self::runtime_open`], different doors. Probing a
1559    /// harness starts it and completes its handshake; couriering a message
1560    /// runs a `claude` process to completion; a conversation verb runs the
1561    /// harness's own CLI or calls its HTTP API. A transport that awaited any
1562    /// of those inline would stop reading its own input for that whole time,
1563    /// so one probe of an unhealthy harness would queue every later request
1564    /// on the same server.
1565    ///
1566    /// Unlike an opening runtime there is no bookkeeping half: the answer
1567    /// [`DetachedCall::run`] produces is the caller's complete response, so a
1568    /// transport writes it without coming back here.
1569    ///
1570    /// `None` for every other method — including the LIVE `sessions.new` /
1571    /// `sessions.reset` door and `runtimes.close`, which wait on a runtime
1572    /// connection this service owns and so are split off by
1573    /// [`Self::detach_runtime`] instead.
1574    pub fn detach(&self, request: &Value) -> Option<DetachedCall> {
1575        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1576            return None;
1577        }
1578        let method = request.get("method").and_then(Value::as_str)?;
1579        if !DETACHED_METHODS.contains(&method) {
1580            return None;
1581        }
1582        let id = request.get("id").cloned().unwrap_or(Value::Null);
1583        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1584        let work = match method {
1585            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe" => self
1586                .inventory_work(method, params)
1587                .map(DetachedWork::Inventory),
1588            "harness.v1.sessions.message" => {
1589                decode::<MessageSessionParams>(params).map(DetachedWork::Message)
1590            }
1591            _ => {
1592                let verb = match method {
1593                    "harness.v1.sessions.new" => crate::SessionVerb::New,
1594                    "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
1595                    "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
1596                    _ => crate::SessionVerb::Delete,
1597                };
1598                match decode::<crate::SessionMutation>(params) {
1599                    Ok(mutation) => {
1600                        match crate::sessions_control::door(&mutation.harness, verb) {
1601                            // The live door needs the open runtime connection
1602                            // this service owns; it stays inline.
1603                            Ok(crate::SessionDoor::Live(_)) => return None,
1604                            Ok(_) => Ok(DetachedWork::SessionMutation { verb, mutation }),
1605                            Err(error) => Err(session_control_error(error)),
1606                        }
1607                    }
1608                    Err(error) => Err(error),
1609                }
1610            }
1611        };
1612        Some(DetachedCall {
1613            id,
1614            method: method.to_string(),
1615            work: work.map(Work::Free),
1616        })
1617    }
1618
1619    /// Recognize the two doors that wait on a runtime THIS SERVICE OWNS, and
1620    /// hand a transport the whole of each by lending the connection out.
1621    ///
1622    /// `runtimes.close` surrenders its runtime for good; the LIVE
1623    /// `sessions.new` / `sessions.reset` door borrows one for the length of
1624    /// the slash command and gives it back through
1625    /// [`Self::finish_detached`]. Both are bounded by
1626    /// [`RUNTIME_CONTROL_DEADLINE`], and a wedged runtime spends all of it —
1627    /// which is exactly as long as a transport that awaited them inline would
1628    /// stop reading its own input.
1629    ///
1630    /// `None` for every other method, and for the `sessions.new` /
1631    /// `sessions.reset` doors that are not live: [`Self::detach`] owns those.
1632    pub fn detach_runtime(&mut self, request: &Value) -> Option<DetachedCall> {
1633        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1634            return None;
1635        }
1636        let method = request.get("method").and_then(Value::as_str)?;
1637        let id = request.get("id").cloned().unwrap_or(Value::Null);
1638        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1639        let work = match method {
1640            "harness.v1.runtimes.close" => decode::<RuntimeConnectionParams>(params)
1641                .and_then(|params| self.surrender_runtime(&params.connection))
1642                .map(|(runtime, process_group)| {
1643                    Work::Runtime(RuntimeWork::Close {
1644                        runtime,
1645                        process_group,
1646                    })
1647                }),
1648            "harness.v1.sessions.new" | "harness.v1.sessions.reset" => {
1649                let verb = if method == "harness.v1.sessions.new" {
1650                    crate::SessionVerb::New
1651                } else {
1652                    crate::SessionVerb::Reset
1653                };
1654                let mutation = decode::<crate::SessionMutation>(params).ok()?;
1655                // Everything but the live door — including a refusal and a
1656                // request naming no connection — is `detach`'s or
1657                // `handle_async`'s to answer.
1658                let Ok(crate::SessionDoor::Live(command)) =
1659                    crate::sessions_control::door(&mutation.harness, verb)
1660                else {
1661                    return None;
1662                };
1663                let connection = mutation
1664                    .connection
1665                    .clone()
1666                    .filter(|value| !value.trim().is_empty())?;
1667                self.lend_runtime(&connection).map(|runtime| {
1668                    let session = live_session_name(runtime.as_ref(), &mutation);
1669                    Work::Runtime(RuntimeWork::LiveCommand {
1670                        connection,
1671                        runtime,
1672                        verb,
1673                        mutation,
1674                        command,
1675                        session,
1676                    })
1677                })
1678            }
1679            _ => return None,
1680        };
1681        Some(DetachedCall {
1682            id,
1683            method: method.to_string(),
1684            work,
1685        })
1686    }
1687
1688    /// Take back whatever a detached call borrowed and hand over the caller's
1689    /// response. Every answer from [`DetachedCall::run`] comes through here,
1690    /// so a lent-out connection is back in the service before the response
1691    /// that used it is written.
1692    pub fn finish_detached(&mut self, answer: DetachedAnswer) -> Value {
1693        let DetachedAnswer { response, returned } = answer;
1694        if let Some(ReturnedRuntime {
1695            connection,
1696            runtime,
1697        }) = returned
1698        {
1699            self.runtimes_in_flight.remove(&connection);
1700            self.runtimes.insert(connection, runtime);
1701        }
1702        response
1703    }
1704
1705    /// Answer a request split out by [`Self::runtime_open`] and already
1706    /// awaited by [`RuntimeOpen::open`]: register the runtime this service now
1707    /// owns and build its JSON-RPC response.
1708    pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1709        let OpenedRuntime { id, outcome } = opened;
1710        let result = match outcome {
1711            Ok(open) => self.register_open_runtime(open).await,
1712            Err(error) => Err(error),
1713        };
1714        service_response(id, result)
1715    }
1716
1717    /// Take ownership of an opened runtime.
1718    async fn register_open_runtime(
1719        &mut self,
1720        open: OpenRuntime,
1721    ) -> std::result::Result<Value, ServiceError> {
1722        match open {
1723            OpenRuntime::Hosted {
1724                runtime,
1725                capabilities,
1726                workspace,
1727            } => {
1728                self.insert_hosted_runtime(runtime, capabilities, workspace)
1729                    .await
1730            }
1731            OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1732        }
1733    }
1734
1735    async fn runtime_call(
1736        &mut self,
1737        method: &str,
1738        params: Value,
1739    ) -> std::result::Result<Value, ServiceError> {
1740        match method {
1741            "harness.v1.runtimes.capabilities" => {
1742                let params = decode::<RuntimeBackendParams>(params)?;
1743                let backend = runtime_backend(&params)?;
1744                Ok(json!({
1745                    "harness": backend.harness(),
1746                    "capabilities": backend.capabilities(),
1747                }))
1748            }
1749            method if RUNTIME_OPEN_METHODS.contains(&method) => {
1750                self.register_open_runtime(open_runtime(method, params).await?)
1751                    .await
1752            }
1753            "harness.v1.runtimes.send_input" => {
1754                let params = decode::<RuntimeInputParams>(params)?;
1755                let image_urls = validate_runtime_image_urls(params.image_urls)?;
1756                let runtime = self.runtime_mut(&params.connection)?;
1757                let turn_id = within_control_deadline(
1758                    method,
1759                    runtime.send_input(RuntimeInput {
1760                        text: params.text,
1761                        image_urls,
1762                    }),
1763                )
1764                .await?
1765                .map_err(operation)?;
1766                Ok(json!({"turn_id": turn_id}))
1767            }
1768            "harness.v1.runtimes.interrupt" => {
1769                let params = decode::<RuntimeConnectionParams>(params)?;
1770                within_control_deadline(method, self.runtime_mut(&params.connection)?.interrupt())
1771                    .await?
1772                    .map_err(operation)?;
1773                Ok(json!({}))
1774            }
1775            "harness.v1.runtimes.steer" => {
1776                let params = decode::<RuntimeInputParams>(params)?;
1777                if !params.image_urls.is_empty() {
1778                    return Err(ServiceError::InvalidParams(
1779                        "runtime steering accepts text only".into(),
1780                    ));
1781                }
1782                let text = params.text.trim();
1783                if text.is_empty() || text.chars().count() > 50_000 {
1784                    return Err(ServiceError::InvalidParams(
1785                        "runtime steering requires 1 to 50,000 text characters".into(),
1786                    ));
1787                }
1788                within_control_deadline(
1789                    method,
1790                    self.runtime_mut(&params.connection)?
1791                        .steer(text.to_string()),
1792                )
1793                .await?
1794                .map_err(operation)?;
1795                Ok(json!({}))
1796            }
1797            "harness.v1.runtimes.respond" => {
1798                let params = decode::<RuntimeRespondParams>(params)?;
1799                let request_id = params.request_id.clone();
1800                within_control_deadline(
1801                    method,
1802                    self.runtime_mut(&params.connection)?
1803                        .respond(params.request_id, params.response),
1804                )
1805                .await?
1806                .map_err(operation)?;
1807                // ORCH-9: an answered request is no longer waiting for one.
1808                self.approvals.answered(&params.connection, &request_id);
1809                Ok(json!({}))
1810            }
1811            "harness.v1.runtimes.acquire_control" => {
1812                let params = decode::<RuntimeConnectionParams>(params)?;
1813                let snapshot = within_control_deadline(
1814                    method,
1815                    self.runtime_mut(&params.connection)?.acquire_control(),
1816                )
1817                .await?
1818                .map_err(operation)?;
1819                serde_json::to_value(snapshot)
1820                    .map_err(|error| ServiceError::Operation(error.to_string()))
1821            }
1822            "harness.v1.runtimes.heartbeat" => {
1823                let params = decode::<RuntimeConnectionParams>(params)?;
1824                let snapshot = within_control_deadline(
1825                    method,
1826                    self.runtime_mut(&params.connection)?.heartbeat(),
1827                )
1828                .await?
1829                .map_err(operation)?;
1830                serde_json::to_value(snapshot)
1831                    .map_err(|error| ServiceError::Operation(error.to_string()))
1832            }
1833            "harness.v1.runtimes.detach" => {
1834                let params = decode::<RuntimeConnectionParams>(params)?;
1835                let snapshot =
1836                    within_control_deadline(method, self.runtime_mut(&params.connection)?.detach())
1837                        .await?
1838                        .map_err(operation)?;
1839                serde_json::to_value(snapshot)
1840                    .map_err(|error| ServiceError::Operation(error.to_string()))
1841            }
1842            "harness.v1.runtimes.terminal_instructions" => {
1843                let params = decode::<RuntimeConnectionParams>(params)?;
1844                let launch = self
1845                    .terminal_launches
1846                    .get(&params.connection)
1847                    .ok_or_else(|| {
1848                        ServiceError::Operation(
1849                            "this runtime is not hosted for terminal attachment".into(),
1850                        )
1851                    })?;
1852                Ok(json!({"launch":launch}))
1853            }
1854            "harness.v1.runtimes.close" => {
1855                let params = decode::<RuntimeConnectionParams>(params)?;
1856                let (runtime, process_group) = self.surrender_runtime(&params.connection)?;
1857                close_runtime(runtime, process_group).await
1858            }
1859            _ => Err(ServiceError::MethodNotFound),
1860        }
1861    }
1862
1863    /// Deliver one message into a session that is running right now.
1864    #[cfg(feature = "adapter-api")]
1865    async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1866        let params = decode::<MessageSessionParams>(params)?;
1867        Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
1868    }
1869
1870    #[cfg(feature = "adapter-api")]
1871    fn harness_settings_call(
1872        &self,
1873        method: &str,
1874        params: Value,
1875    ) -> std::result::Result<Value, ServiceError> {
1876        let homes = crate::HarnessHomes::default();
1877        match method {
1878            "harness.v1.harnesses.settings" => {
1879                let params = decode::<HarnessSettingsParams>(params)?;
1880                let report = crate::inspect_harness_interop_settings(&homes, &params.harness)
1881                    .map_err(|error| ServiceError::Operation(error.to_string()))?;
1882                serde_json::to_value(report)
1883                    .map_err(|error| ServiceError::Operation(error.to_string()))
1884            }
1885            "harness.v1.harnesses.configure" => {
1886                let params = decode::<ConfigureHarnessParams>(params)?;
1887                let report = crate::configure_harness_interop_settings(
1888                    &homes,
1889                    &params.harness,
1890                    &params.changes,
1891                    params.expected_revision.as_deref(),
1892                )
1893                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1894                serde_json::to_value(report)
1895                    .map_err(|error| ServiceError::Operation(error.to_string()))
1896            }
1897            _ => Err(ServiceError::MethodNotFound),
1898        }
1899    }
1900
1901    fn insert_runtime(
1902        &mut self,
1903        runtime: Box<dyn RuntimeConnection>,
1904    ) -> std::result::Result<Value, ServiceError> {
1905        let connection = format!("runtime-{}", self.next_runtime);
1906        self.next_runtime += 1;
1907        let handle = runtime.handle().clone();
1908        self.runtime_sequences
1909            .entry(handle.runtime_id.clone())
1910            .or_insert(0);
1911        self.runtimes.insert(connection.clone(), runtime);
1912        Ok(json!({"connection": connection, "handle": handle}))
1913    }
1914
1915    #[cfg(feature = "adapter-api")]
1916    async fn insert_hosted_runtime(
1917        &mut self,
1918        runtime: Box<dyn RuntimeConnection>,
1919        capabilities: crate::RuntimeCapabilities,
1920        workspace: PathBuf,
1921    ) -> std::result::Result<Value, ServiceError> {
1922        let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1923        let token: std::sync::Arc<str> = crate::server::generate_token().into();
1924        let server = crate::server::run_frontend_http(
1925            host.clone(),
1926            host.frontend_sender(),
1927            "127.0.0.1:0",
1928            token.clone(),
1929            connection.handle().runtime_id.clone(),
1930        )
1931        .await
1932        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1933        let source = LiveRuntimeSource {
1934            harness: connection.handle().harness.as_str().to_string(),
1935            session_id: connection.handle().runtime_id.clone(),
1936            workspace: workspace.clone(),
1937        };
1938        let registration = register_live_runtime(
1939            connection.handle().runtime_id.clone(),
1940            source.clone(),
1941            format!("http://{}", server.address()),
1942            token.to_string(),
1943        )
1944        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1945        let endpoint = registration.endpoint().to_string();
1946        let launch = StructuredLaunch {
1947            cwd: workspace,
1948            // Pin attachment to the executable hosting this runtime. A bare
1949            // `supercode` could resolve to an older global install whose CLI
1950            // does not understand the receipt it is being asked to open.
1951            program: std::env::current_exe()
1952                .ok()
1953                .map(|path| path.to_string_lossy().into_owned())
1954                .unwrap_or_else(|| "supercode".into()),
1955            arguments: vec![
1956                "harness".into(),
1957                "attach".into(),
1958                "--endpoint".into(),
1959                endpoint,
1960                "--harness".into(),
1961                source.harness,
1962                "--session".into(),
1963                source.session_id,
1964            ],
1965            env: BTreeMap::new(),
1966        };
1967        let lease = HostedRuntimeLease {
1968            connection,
1969            _host: host,
1970            _registration: registration,
1971            _server: server,
1972        };
1973        let opened = self.insert_runtime(Box::new(lease))?;
1974        let connection_id = opened["connection"]
1975            .as_str()
1976            .expect("insert_runtime returns a connection id")
1977            .to_string();
1978        self.terminal_launches.insert(connection_id, launch);
1979        Ok(opened)
1980    }
1981
1982    #[cfg(not(feature = "adapter-api"))]
1983    async fn insert_hosted_runtime(
1984        &mut self,
1985        runtime: Box<dyn RuntimeConnection>,
1986        _capabilities: crate::RuntimeCapabilities,
1987        _workspace: PathBuf,
1988    ) -> std::result::Result<Value, ServiceError> {
1989        self.insert_runtime(runtime)
1990    }
1991
1992    fn runtime_mut(
1993        &mut self,
1994        connection: &str,
1995    ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1996        if self.runtimes_in_flight.contains(connection) {
1997            return Err(self.lent_out(connection));
1998        }
1999        self.runtimes.get_mut(connection).ok_or_else(|| {
2000            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2001        })
2002    }
2003
2004    /// What a caller is told about a connection that is out on a detached
2005    /// call. It is not gone and it is not free: it is mid-call, which is the
2006    /// same answer the runtime itself gives a second turn.
2007    fn lent_out(&self, connection: &str) -> ServiceError {
2008        ServiceError::Operation(format!(
2009            "runtime connection `{connection}`: a harness turn is already in progress"
2010        ))
2011    }
2012
2013    /// Take a runtime OUT of the service for the duration of one detached
2014    /// call, leaving its name marked as lent out.
2015    fn lend_runtime(
2016        &mut self,
2017        connection: &str,
2018    ) -> std::result::Result<Box<dyn RuntimeConnection>, ServiceError> {
2019        if self.runtimes_in_flight.contains(connection) {
2020            return Err(self.lent_out(connection));
2021        }
2022        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2023            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2024        })?;
2025        self.runtimes_in_flight.insert(connection.to_string());
2026        Ok(runtime)
2027    }
2028
2029    /// Surrender a runtime for good: the connection and everything the
2030    /// service hung off it are gone before its teardown is even attempted.
2031    ///
2032    /// `close` is what a caller reaches for when a runtime has stopped
2033    /// answering, and a runtime that has stopped answering is exactly the one
2034    /// whose graceful close cannot complete: a hosted runtime's own loop
2035    /// parks on the call the runtime never answered, so it never dequeues the
2036    /// shutdown either. Keeping the entry until teardown succeeded made a
2037    /// wedged runtime permanent — every later call on that connection, and
2038    /// every new turn, answered "a harness turn is already in progress" with
2039    /// no way to take the connection back.
2040    fn surrender_runtime(
2041        &mut self,
2042        connection: &str,
2043    ) -> std::result::Result<(Box<dyn RuntimeConnection>, Option<u32>), ServiceError> {
2044        if self.runtimes_in_flight.contains(connection) {
2045            return Err(self.lent_out(connection));
2046        }
2047        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2048            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2049        })?;
2050        let process_group = runtime_process_group(runtime.handle());
2051        let runtime_id = runtime.handle().runtime_id.clone();
2052        self.terminal_launches.remove(connection);
2053        self.runtime_sequences.remove(&runtime_id);
2054        self.approvals.forget(connection);
2055        Ok((runtime, process_group))
2056    }
2057
2058    /// SIGKILL the process group of every runtime this service owns, without
2059    /// waiting on any of them.
2060    ///
2061    /// A host leaving for good calls this BEFORE dropping the service. The
2062    /// handle this service holds is not the runtime's connection: a hosted
2063    /// runtime's real transport lives in the task driving it, so neither
2064    /// exiting the process nor dropping these handles reaches the harness
2065    /// process — while dropping them does remove each runtime's live-runtime
2066    /// receipt. Signalling first is what keeps a removed receipt from
2067    /// advertising a harness that is still running.
2068    pub fn kill_all_runtime_groups(&self) -> usize {
2069        self.runtimes
2070            .values()
2071            .filter(|runtime| kill_runtime_process_group(runtime_process_group(runtime.handle())))
2072            .count()
2073    }
2074
2075    /// ORCH-19: run one conversation-lifecycle verb through the harness's own
2076    /// door.
2077    ///
2078    /// Two doors, one shape. A CLI / HTTP / own-store door is self-contained
2079    /// in [`crate::sessions_control`]. A LIVE door (Hermes's and OpenClaw's
2080    /// `/new` and `/reset`, which are slash commands their gateway interprets
2081    /// INSIDE a session) is performed here, because only the service owns the
2082    /// open runtime connection — the command is typed through the very same
2083    /// `send_input` path a human's message takes, so supercode invents no
2084    /// private channel.
2085    async fn mutate_session(
2086        &mut self,
2087        verb: crate::SessionVerb,
2088        params: Value,
2089    ) -> std::result::Result<Value, ServiceError> {
2090        let mutation = decode::<crate::SessionMutation>(params)?;
2091        let door = crate::sessions_control::door(&mutation.harness, verb)
2092            .map_err(session_control_error)?;
2093        let outcome = match door {
2094            // The live door types the slash command through an open hosted
2095            // runtime, which only exists with the `adapter-api` feature; the
2096            // CLI / HTTP / own-store doors below need nothing extra.
2097            #[cfg(not(feature = "adapter-api"))]
2098            crate::SessionDoor::Live(command) => {
2099                return Err(ServiceError::Operation(format!(
2100                    "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
2101                     session, which needs this build's `adapter-api` feature",
2102                    mutation.harness,
2103                    verb.as_str()
2104                )));
2105            }
2106            #[cfg(feature = "adapter-api")]
2107            crate::SessionDoor::Live(command) => {
2108                let connection = mutation
2109                    .connection
2110                    .clone()
2111                    .filter(|value| !value.trim().is_empty())
2112                    .ok_or_else(|| {
2113                        ServiceError::InvalidParams(format!(
2114                            "`{}` performs `sessions.{}` by typing `{command}` into a live \
2115                             driven session: pass the `connection` of an open runtime \
2116                             (`harness.v1.runtimes.start`)",
2117                            mutation.harness,
2118                            verb.as_str()
2119                        ))
2120                    })?;
2121                let runtime = self.runtime_mut(&connection)?;
2122                let session = live_session_name(runtime.as_ref(), &mutation);
2123                // Typing into a live session is a control call on an open
2124                // runtime, and a wedged runtime never accepts one, so it is
2125                // bounded exactly like the other control verbs. A transport
2126                // with a loop of its own lends the connection out instead of
2127                // waiting here: see [`Self::detach_runtime`].
2128                return type_live_command(runtime.as_mut(), verb, &mutation, command, session)
2129                    .await;
2130            }
2131            _ => run_session_mutation(verb, &mutation).await?,
2132        };
2133        serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
2134    }
2135
2136    /// Answer an inventory request whole, for callers that have nowhere to
2137    /// put the waiting half. A transport with a loop of its own splits it
2138    /// instead: see [`Self::detach`].
2139    async fn inventory_call(
2140        &self,
2141        method: &str,
2142        params: Value,
2143    ) -> std::result::Result<Value, ServiceError> {
2144        run_inventory(self.inventory_work(method, params)?).await
2145    }
2146
2147    /// The half of an inventory request that reads this service's state:
2148    /// resolve the selection and count the persisted sessions each row
2149    /// reports. What remains — finding executables, asking them their
2150    /// version, and (at `probe: handshake`) starting each harness and
2151    /// completing its protocol handshake — touches no service state at all.
2152    fn inventory_work(
2153        &self,
2154        method: &str,
2155        params: Value,
2156    ) -> std::result::Result<InventoryWork, ServiceError> {
2157        let mut params = decode::<HarnessInventoryParams>(params)?;
2158        if method == "harness.v1.harnesses.probe" {
2159            let harness = params.harness.take().ok_or_else(|| {
2160                ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
2161            })?;
2162            params.harnesses = vec![harness];
2163        }
2164        let selected = params
2165            .harnesses
2166            .iter()
2167            .map(HarnessId::as_str)
2168            .collect::<std::collections::BTreeSet<_>>();
2169        let supported = harness_support_registry()
2170            .harnesses
2171            .into_iter()
2172            .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
2173            .collect::<Vec<_>>();
2174        if !params.harnesses.is_empty() && supported.len() != selected.len() {
2175            let known = supported
2176                .iter()
2177                .map(|harness| harness.id.as_str())
2178                .collect::<std::collections::BTreeSet<_>>();
2179            let missing = params
2180                .harnesses
2181                .iter()
2182                .filter(|id| !known.contains(id.as_str()))
2183                .map(HarnessId::as_str)
2184                .collect::<Vec<_>>();
2185            return Err(ServiceError::InvalidParams(format!(
2186                "unknown harness(es): {}",
2187                missing.join(", ")
2188            )));
2189        }
2190        let global_counts = params
2191            .include_sessions
2192            .then(|| self.session_counts(None, &params.harnesses));
2193        let workspace_counts = params
2194            .include_sessions
2195            .then(|| {
2196                params
2197                    .workspace
2198                    .as_deref()
2199                    .map(|workspace| self.session_counts(Some(workspace), &params.harnesses))
2200            })
2201            .flatten();
2202        Ok(InventoryWork {
2203            params,
2204            supported,
2205            global_counts,
2206            workspace_counts,
2207        })
2208    }
2209
2210    #[cfg(feature = "adapter-api")]
2211    async fn harness_authentication_call(
2212        &self,
2213        method: &str,
2214        params: Value,
2215    ) -> std::result::Result<Value, ServiceError> {
2216        match method {
2217            "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
2218                let params = decode::<HarnessAuthenticationParams>(params)?;
2219                serde_json::to_value(crate::inspect_harness_authentication(&params.harness).await)
2220                    .map_err(|error| ServiceError::Operation(error.to_string()))
2221            }
2222            "harness.v1.harnesses.auth.begin" => {
2223                let params = decode::<BeginHarnessAuthenticationParams>(params)?;
2224                let cwd = params
2225                    .cwd
2226                    .or_else(|| std::env::current_dir().ok())
2227                    .unwrap_or_else(|| PathBuf::from("."));
2228                let plan = crate::harness_authentication_plan(
2229                    &params.harness,
2230                    params.environment,
2231                    params.method,
2232                    &cwd,
2233                )
2234                .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
2235                serde_json::to_value(plan)
2236                    .map_err(|error| ServiceError::Operation(error.to_string()))
2237            }
2238            _ => Err(ServiceError::MethodNotFound),
2239        }
2240    }
2241
2242    fn session_counts(
2243        &self,
2244        workspace: Option<&Path>,
2245        harnesses: &[HarnessId],
2246    ) -> BTreeMap<String, usize> {
2247        let mut counts = BTreeMap::new();
2248        for session in self
2249            .catalog
2250            .discover(&DiscoveryQuery {
2251                workspace: workspace.map(Path::to_path_buf),
2252                harnesses: harnesses.to_vec(),
2253                ..DiscoveryQuery::default()
2254            })
2255            .unwrap_or_default()
2256        {
2257            *counts
2258                .entry(session.locator.harness.as_str().to_string())
2259                .or_insert(0) += 1;
2260        }
2261        counts
2262    }
2263}
2264
2265#[async_trait::async_trait]
2266impl SdkService for HarnessSessionService {
2267    fn capabilities(&self) -> SdkCapabilities {
2268        SdkCapabilities::default()
2269    }
2270
2271    async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2272        if request.operation == SdkOperation::Events {
2273            let events = self
2274                .poll_sdk_events()
2275                .await
2276                .into_iter()
2277                .map(|(_, event)| event)
2278                .collect::<Vec<_>>();
2279            return serde_json::to_value(events).map_err(|error| {
2280                SdkError::new(
2281                    SdkErrorCode::Execution,
2282                    request.operation,
2283                    error.to_string(),
2284                )
2285            });
2286        }
2287        if self.runtimes.is_empty()
2288            && matches!(
2289                request.operation,
2290                SdkOperation::Input
2291                    | SdkOperation::Interrupt
2292                    | SdkOperation::Steer
2293                    | SdkOperation::Respond
2294                    | SdkOperation::Close
2295            )
2296        {
2297            return Err(SdkError::unsupported(request.operation));
2298        }
2299        let method = request
2300            .operation
2301            .method()
2302            .ok_or_else(|| SdkError::unsupported(request.operation))?;
2303        let result = match request.operation {
2304            SdkOperation::Discover
2305            | SdkOperation::Load
2306            | SdkOperation::Export
2307            | SdkOperation::ProfilesList
2308            | SdkOperation::ProfilesGet
2309            | SdkOperation::ProfilesCreate
2310            | SdkOperation::ProfilesDelete
2311            | SdkOperation::SkillsList
2312            | SdkOperation::SkillsInstall
2313            | SdkOperation::SkillsRemove
2314            | SdkOperation::ChannelsList
2315            | SdkOperation::RoutesList
2316            | SdkOperation::TriggersList
2317            | SdkOperation::ChannelsStatus
2318            | SdkOperation::MemoryShow
2319            | SdkOperation::MemorySearch
2320            | SdkOperation::JobsList
2321            | SdkOperation::JobsGet
2322            | SdkOperation::JobsCreate
2323            | SdkOperation::JobsUpdate
2324            | SdkOperation::JobsPause
2325            | SdkOperation::JobsResume
2326            | SdkOperation::JobsRun
2327            | SdkOperation::JobsDelete
2328            | SdkOperation::JobsApply
2329            | SdkOperation::JobsNotepad
2330            | SdkOperation::JobsNotepadSet
2331            | SdkOperation::JobsNotepadDelete
2332            | SdkOperation::ModelRouteApply
2333            | SdkOperation::RunsList
2334            | SdkOperation::RunsGet
2335            | SdkOperation::ApprovalsList
2336            | SdkOperation::OrchestrationLoad
2337            | SdkOperation::OrchestrationSave
2338            | SdkOperation::OrchestrationCompile
2339            | SdkOperation::OrchestrationDecompile
2340            | SdkOperation::OrchestrationImport
2341            | SdkOperation::OrchestrationExport
2342            | SdkOperation::WorkflowLoad => self.call(method, request.params),
2343            // ORCH-20: answering needs the live connection, so it takes the
2344            // async door and ends in `harness.v1.runtimes.respond`.
2345            SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2346            SdkOperation::Start
2347            | SdkOperation::Resume
2348            | SdkOperation::Input
2349            | SdkOperation::Interrupt
2350            | SdkOperation::Steer
2351            | SdkOperation::Respond
2352            | SdkOperation::Close => self.runtime_call(method, request.params).await,
2353            // ORCH-19 controlled tier. Every verb goes through the HARNESS'S
2354            // OWN door — its CLI, its HTTP API, or its slash command typed
2355            // into a live driven session — and returns the row re-read from
2356            // the harness's store afterwards.
2357            SdkOperation::SessionsNew => {
2358                self.mutate_session(crate::SessionVerb::New, request.params)
2359                    .await
2360            }
2361            SdkOperation::SessionsReset => {
2362                self.mutate_session(crate::SessionVerb::Reset, request.params)
2363                    .await
2364            }
2365            SdkOperation::SessionsArchive => {
2366                self.mutate_session(crate::SessionVerb::Archive, request.params)
2367                    .await
2368            }
2369            SdkOperation::SessionsDelete => {
2370                self.mutate_session(crate::SessionVerb::Delete, request.params)
2371                    .await
2372            }
2373            SdkOperation::Events => unreachable!("handled before method dispatch"),
2374        };
2375        result.map_err(|error| sdk_error(request.operation, error))
2376    }
2377
2378    async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2379        Ok(self
2380            .poll_sdk_events()
2381            .await
2382            .into_iter()
2383            .map(|(_, event)| event)
2384            .collect())
2385    }
2386}
2387
2388#[cfg(feature = "adapter-api")]
2389struct HostedRuntimeLease {
2390    connection: HostedHarnessConnection,
2391    _host: std::sync::Arc<HostedHarnessRuntime>,
2392    _registration: LiveRuntimeRegistration,
2393    _server: crate::server::FrontendHttpServer,
2394}
2395
2396#[async_trait::async_trait]
2397#[cfg(feature = "adapter-api")]
2398impl RuntimeConnection for HostedRuntimeLease {
2399    fn handle(&self) -> &crate::RuntimeHandle {
2400        self.connection.handle()
2401    }
2402
2403    async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2404        self.connection.send_input(input).await
2405    }
2406
2407    async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2408        self.connection.next_event().await
2409    }
2410
2411    async fn interrupt(&mut self) -> crate::Result<()> {
2412        self.connection.interrupt().await
2413    }
2414
2415    // the lease must forward every verb its capabilities advertise; without
2416    // this, steer fell to the trait default and refused a turn it claimed
2417    async fn steer(&mut self, text: String) -> crate::Result<()> {
2418        self.connection.steer(text).await
2419    }
2420
2421    async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2422        self.connection.respond(request_id, response).await
2423    }
2424
2425    async fn close(&mut self) -> crate::Result<()> {
2426        self.connection.close().await
2427    }
2428}
2429
2430/// One inventory request's waiting half, already separated from the service
2431/// state it reads. See [`HarnessSessionService::inventory_work`].
2432struct InventoryWork {
2433    params: HarnessInventoryParams,
2434    supported: Vec<crate::HarnessSupportDescriptor>,
2435    global_counts: Option<BTreeMap<String, usize>>,
2436    workspace_counts: Option<BTreeMap<String, usize>>,
2437}
2438
2439/// Perform one conversation-lifecycle verb through a door that is
2440/// self-contained in [`crate::sessions_control`]: the harness's own CLI, its
2441/// HTTP API, the orchestrator daemon's socket, or supercode's own store.
2442/// Touches no service state, so this runs on any task. The LIVE door is not
2443/// here — it types its slash command through a runtime connection the service
2444/// owns, and is performed by [`HarnessSessionService::mutate_session`].
2445async fn run_session_mutation(
2446    verb: crate::SessionVerb,
2447    mutation: &crate::SessionMutation,
2448) -> std::result::Result<crate::SessionMutationOutcome, ServiceError> {
2449    // Only the HTTP door actually awaits anything. The CLI, store and daemon
2450    // doors run the harness's own program, or its store, with calls that
2451    // block the calling THREAD from start to finish — a future that never
2452    // yields, which no timeout around it can interrupt and which would hold a
2453    // runtime worker for as long as the harness takes. They go to a blocking
2454    // task, where blocking is what the thread is for.
2455    let door =
2456        crate::sessions_control::door(&mutation.harness, verb).map_err(session_control_error)?;
2457    if let crate::SessionDoor::Http = door {
2458        return crate::sessions_control::mutate(verb, mutation)
2459            .await
2460            .map_err(session_control_error);
2461    }
2462    let mutation = mutation.clone();
2463    tokio::task::spawn_blocking(move || crate::sessions_control::mutate_blocking(verb, &mutation))
2464        .await
2465        .map_err(|error| {
2466            ServiceError::Operation(format!("the conversation verb could not be run: {error}"))
2467        })?
2468        .map_err(session_control_error)
2469}
2470
2471/// Probe every selected harness and assemble the report. Touches no service
2472/// state, so this runs on any task.
2473async fn run_inventory(work: InventoryWork) -> std::result::Result<Value, ServiceError> {
2474    let InventoryWork {
2475        params,
2476        supported,
2477        global_counts,
2478        workspace_counts,
2479    } = work;
2480    let probes = supported.into_iter().map(|descriptor| {
2481        let global = global_counts
2482            .as_ref()
2483            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2484        let workspace = workspace_counts
2485            .as_ref()
2486            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2487        probe_harness(descriptor, &params, global, workspace)
2488    });
2489    let harnesses = futures::future::join_all(probes).await;
2490    serde_json::to_value(HarnessInventoryReport {
2491        probe: params.probe,
2492        workspace: params.workspace,
2493        harnesses,
2494    })
2495    .map_err(|error| ServiceError::Operation(error.to_string()))
2496}
2497
2498async fn probe_harness(
2499    descriptor: crate::HarnessSupportDescriptor,
2500    params: &HarnessInventoryParams,
2501    global: Option<usize>,
2502    workspace: Option<usize>,
2503) -> LocalHarness {
2504    let launch = descriptor.runtime.default_launch.as_ref();
2505    // ORC-7: the orchestrator publishes no runtime launch — it is not an
2506    // adapter supercode connects a turn to. What "installed" means for it
2507    // is that its Node daemon entry is present, so the row answers from
2508    // that instead of from a PATH lookup it could never satisfy.
2509    let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
2510        .then(crate::orchestrator::daemon_entry)
2511        .and_then(Result::ok);
2512    let executable = match &orchestrator_entry {
2513        Some(entry) => Some(entry.clone()),
2514        None => launch.and_then(|launch| find_executable(&launch.program)),
2515    };
2516    let installed = executable.is_some();
2517    let version = if params.skip_versions || orchestrator_entry.is_some() {
2518        // The orchestrator's "executable" is a Node module, not a CLI
2519        // with a `--version` flag; running it to ask would start a daemon.
2520        None
2521    } else {
2522        match executable.as_deref() {
2523            Some(path) => executable_version(path).await,
2524            None => None,
2525        }
2526    };
2527    let configured = auth_evidence(descriptor.id.as_str());
2528    let mut auth = if configured {
2529        HarnessAuthState::Configured
2530    } else if matches!(
2531        descriptor.id.as_str(),
2532        HarnessId::CLAUDE_CODE | HarnessId::CODEX
2533    ) {
2534        // These two adapters have explicit native status/login contracts
2535        // and complete local evidence coverage (including Claude's macOS
2536        // Keychain-backed oauthAccount marker). Treating absent evidence
2537        // as unknown advertises a start that will only fail interactively.
2538        HarnessAuthState::Required
2539    } else {
2540        HarnessAuthState::Unknown
2541    };
2542    let mut runtime = if installed {
2543        HarnessRuntimeState::Degraded
2544    } else {
2545        HarnessRuntimeState::Unavailable
2546    };
2547    let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2548    let mut reason = (!installed).then(|| {
2549        if is_orchestrator {
2550            format!(
2551                "{} is supported but its daemon entry `{}` was not found",
2552                descriptor.display_name,
2553                crate::orchestrator::DAEMON_ENTRY
2554            )
2555        } else {
2556            format!(
2557                "{} is supported but `{}` was not found on PATH",
2558                descriptor.display_name,
2559                launch
2560                    .map(|launch| launch.program.as_str())
2561                    .unwrap_or("executable")
2562            )
2563        }
2564    });
2565    let mut repair = (!installed).then(|| {
2566        if is_orchestrator {
2567            format!(
2568                "Install the `supercode-orchestrator` package so `{}` resolves.",
2569                crate::orchestrator::DAEMON_ENTRY
2570            )
2571        } else {
2572            format!(
2573                "Install {} and ensure `{}` is on PATH.",
2574                descriptor.display_name,
2575                launch
2576                    .map(|launch| launch.program.as_str())
2577                    .unwrap_or("its executable")
2578            )
2579        }
2580    });
2581
2582    if installed && params.probe == HarnessProbeLevel::Handshake {
2583        let backend_params = RuntimeBackendParams {
2584            harness: descriptor.id.clone(),
2585            protocol: None,
2586            launch: None,
2587            base_url: None,
2588            policy: RuntimePolicy::Default,
2589        };
2590        match runtime_backend(&backend_params) {
2591            Ok(backend) => {
2592                let cwd = params
2593                    .workspace
2594                    .clone()
2595                    .or_else(|| std::env::current_dir().ok())
2596                    .unwrap_or_else(|| PathBuf::from("."));
2597                let isolated = descriptor
2598                    .runtime
2599                    .default_launch
2600                    .clone()
2601                    .and_then(|launch| IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok());
2602                let Some(isolated) = isolated else {
2603                    reason = Some(
2604                        "No-prompt runtime handshake could not create its isolated harness home."
2605                            .into(),
2606                    );
2607                    repair = Some(
2608                        "Check temporary-directory permissions, then run the handshake probe again."
2609                            .into(),
2610                    );
2611                    let running = probe_running_instance(descriptor.id.as_str());
2612                    return LocalHarness {
2613                        gateway: gateway_health(
2614                            descriptor.id.as_str(),
2615                            installed,
2616                            running.as_ref(),
2617                            version.as_deref(),
2618                        ),
2619                        id: descriptor.id,
2620                        display_name: descriptor.display_name,
2621                        supported: true,
2622                        installed,
2623                        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2624                        version,
2625                        auth,
2626                        runtime,
2627                        protocol: descriptor.runtime.protocol,
2628                        capabilities: descriptor.runtime.capabilities.clone(),
2629                        effective_capabilities: descriptor.runtime.capabilities,
2630                        sessions: HarnessSessionCounts { global, workspace },
2631                        running,
2632                        reason,
2633                        repair,
2634                    };
2635                };
2636                match tokio::time::timeout(
2637                    Duration::from_secs(30),
2638                    backend.start(RuntimeStartRequest {
2639                        cwd,
2640                        launch: Some(isolated.launch.clone()),
2641                        mcp_servers: Vec::new(),
2642                    }),
2643                )
2644                .await
2645                {
2646                    Ok(Ok(mut connection)) => {
2647                        match stabilize_handshake(connection.as_mut()).await {
2648                            Ok(()) => {
2649                                auth = HarnessAuthState::Ready;
2650                                runtime = HarnessRuntimeState::Ready;
2651                                reason = Some(
2652                                    "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2653                                        .into(),
2654                                );
2655                                repair = None;
2656                            }
2657                            Err(message) => {
2658                                auth = if looks_like_auth_error(&message) {
2659                                    HarnessAuthState::Required
2660                                } else if configured {
2661                                    HarnessAuthState::Configured
2662                                } else {
2663                                    HarnessAuthState::Unknown
2664                                };
2665                                reason = Some(format!(
2666                                    "No-prompt runtime handshake became unhealthy during startup: {message}"
2667                                ));
2668                                repair = Some(if auth == HarnessAuthState::Required {
2669                                    format!(
2670                                        "Run `{}` interactively once and complete sign-in, then probe again.",
2671                                        launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2672                                    )
2673                                } else {
2674                                    "Run the harness directly to inspect its startup failure, then probe again."
2675                                        .into()
2676                                });
2677                            }
2678                        }
2679                        let _ =
2680                            tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2681                    }
2682                    Ok(Err(error)) => {
2683                        let message = truncate_text(&error.to_string(), 500);
2684                        auth = if looks_like_auth_error(&message) {
2685                            HarnessAuthState::Required
2686                        } else if configured {
2687                            HarnessAuthState::Configured
2688                        } else {
2689                            HarnessAuthState::Unknown
2690                        };
2691                        reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2692                        repair = Some(if auth == HarnessAuthState::Required {
2693                            format!(
2694                                "Run `{}` interactively once and complete sign-in, then probe again.",
2695                                launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2696                            )
2697                        } else {
2698                            "Check the harness installation and run the handshake probe again."
2699                                .into()
2700                        });
2701                    }
2702                    Err(_) => {
2703                        reason =
2704                            Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2705                        repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2706                    }
2707                }
2708                // Keep the isolated home alive through process teardown.
2709                // Otherwise the compiler may release the last meaningful
2710                // use after cloning `launch`, and a still-starting CLI can
2711                // recreate its state directory after Drop removed it.
2712                // Some Node-based launchers finish a short asynchronous
2713                // installation-id write just after their parent process
2714                // is reaped. Remove once immediately, allow that bounded
2715                // writer to settle, then perform the authoritative pass.
2716                let _ = isolated.cleanup();
2717                tokio::time::sleep(Duration::from_millis(250)).await;
2718                if let Err(error) = isolated.cleanup() {
2719                    auth = if configured {
2720                        HarnessAuthState::Configured
2721                    } else {
2722                        HarnessAuthState::Unknown
2723                    };
2724                    runtime = HarnessRuntimeState::Degraded;
2725                    reason = Some(format!(
2726                        "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2727                    ));
2728                    repair = Some(
2729                        "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2730                            .into(),
2731                    );
2732                }
2733            }
2734            Err(error) => {
2735                reason = Some(error_message(error));
2736            }
2737        }
2738    } else if installed && configured {
2739        reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2740    } else if installed && auth == HarnessAuthState::Required {
2741        reason = Some("Executable found, but no native authentication evidence is present.".into());
2742        repair = Some(format!(
2743            "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2744            descriptor.id.as_str()
2745        ));
2746    } else if installed {
2747        reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2748        repair = Some(format!(
2749            "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2750            launch
2751                .map(|launch| launch.program.as_str())
2752                .unwrap_or("the harness")
2753        ));
2754    }
2755
2756    let effective_capabilities = if installed {
2757        descriptor.runtime.capabilities.clone()
2758    } else {
2759        unavailable_capabilities()
2760    };
2761    let running = probe_running_instance(descriptor.id.as_str());
2762    LocalHarness {
2763        gateway: gateway_health(
2764            descriptor.id.as_str(),
2765            installed,
2766            running.as_ref(),
2767            version.as_deref(),
2768        ),
2769        id: descriptor.id,
2770        display_name: descriptor.display_name,
2771        supported: true,
2772        installed,
2773        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2774        version,
2775        auth,
2776        runtime,
2777        protocol: descriptor.runtime.protocol,
2778        capabilities: descriptor.runtime.capabilities,
2779        effective_capabilities,
2780        sessions: HarnessSessionCounts { global, workspace },
2781        running,
2782        reason,
2783        repair,
2784    }
2785}
2786
2787async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2788    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2789    loop {
2790        let now = tokio::time::Instant::now();
2791        if now >= deadline {
2792            return Ok(());
2793        }
2794        match tokio::time::timeout(deadline - now, connection.next_event()).await {
2795            Err(_) => return Ok(()),
2796            Ok(Ok(Some(event))) => {
2797                if let Some(message) = handshake_event_failure(&event) {
2798                    return Err(truncate_text(&message, 500));
2799                }
2800            }
2801            Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2802            Ok(Err(error)) => return Err(error.to_string()),
2803        }
2804    }
2805}
2806
2807fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2808    let detail = event
2809        .payload
2810        .get("message")
2811        .or_else(|| event.payload.get("line"))
2812        .and_then(Value::as_str)
2813        .unwrap_or(event.kind.as_str());
2814    match event.kind.as_str() {
2815        "transport_closed" => Some("runtime transport closed during startup".into()),
2816        "transport_error" => Some(format!("runtime transport error: {detail}")),
2817        "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2818        // Stderr is retained as a runtime event, but is not transport health.
2819        // Grok, for example, can log an AuthorizationRequired error from an
2820        // optional background worker while its ACP session continues to send
2821        // updates and complete prompts normally.
2822        _ => None,
2823    }
2824}
2825
2826fn indexed_claude_window(
2827    locator: &SessionLocator,
2828    options: &SessionLoadOptions,
2829) -> std::result::Result<Option<Value>, ServiceError> {
2830    use supercode_interchange::session::ClaudeReadIndex;
2831    // Exact parent-only window: recursive/full-artifact requests retain the
2832    // existing owner. This is not a bounded display-history substitution.
2833    if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2834        || options.include_subagents != Some(false)
2835    {
2836        return Ok(None);
2837    }
2838    let crate::StorageLocator::File { path } = &locator.storage else {
2839        return Ok(None);
2840    };
2841    if !ClaudeReadIndex::supports(path)
2842        .map_err(|error| ServiceError::Operation(error.to_string()))?
2843    {
2844        return Ok(None);
2845    }
2846    let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2847        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2848    let total = index.len();
2849    let (offset, end) = projected_message_window(total, options);
2850    let session = index
2851        .read_messages(offset..end)
2852        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2853    let summary = index
2854        .read_summary()
2855        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2856    let selected_options = SessionLoadOptions {
2857        message_offset: None,
2858        message_limit: None,
2859        message_tail: None,
2860        ..options.clone()
2861    };
2862    let mut selected = projected_session_json(&session, &selected_options);
2863    selected["raw_record_count"] = json!(index.raw_record_count());
2864    Ok(Some(json!({
2865        "session": selected,
2866        "summary": projected_session_summary(&summary, options),
2867        "window": {
2868            "has_more": offset > 0 || end < total, "has_newer": end < total,
2869            "has_older": offset > 0, "newer_items": index.item_count(end..total),
2870            "offset": offset, "older_items": index.item_count(0..offset),
2871            "returned": end - offset, "total_messages": total,
2872        }
2873    })))
2874}
2875
2876fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2877    let total_messages = session.messages.len();
2878    let (offset, end) = projected_message_window(total_messages, options);
2879    json!({
2880        "session": projected_session_json(session, options),
2881        "summary": projected_session_summary(session, options),
2882        "window": {
2883            "has_more": offset > 0 || end < total_messages,
2884            "has_newer": end < total_messages,
2885            "has_older": offset > 0,
2886            "newer_items": normalized_item_count(&session.messages[end..]),
2887            "offset": offset,
2888            "older_items": normalized_item_count(&session.messages[..offset]),
2889            "returned": end.saturating_sub(offset),
2890            "total_messages": total_messages,
2891        }
2892    })
2893}
2894
2895fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2896    messages
2897        .iter()
2898        .map(|message| {
2899            let conversation = usize::from(
2900                matches!(message.role, Role::Assistant | Role::User)
2901                    && message_has_content(message),
2902            );
2903            let tool_result =
2904                usize::from(message.role == Role::Tool && message_has_content(message));
2905            conversation + tool_result + message.tool_calls().len()
2906        })
2907        .sum()
2908}
2909
2910fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2911    let mut conversational = session.messages.iter().filter(|message| {
2912        matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2913    });
2914    let first_message = conversational.clone().next();
2915    let last_message = conversational.next_back();
2916    let mut assistant = session
2917        .messages
2918        .iter()
2919        .filter(|message| message.role == Role::Assistant && message_has_content(message));
2920    let first_assistant_message = assistant.clone().next();
2921    let last_assistant_message = assistant.next_back();
2922    let end_of_turn = session
2923        .messages
2924        .iter()
2925        .rev()
2926        .find(|message| message.role != Role::System)
2927        .is_some_and(|message| {
2928            message.role == Role::Assistant
2929                && message_has_content(message)
2930                && message.tool_calls().is_empty()
2931        });
2932    let project = |message: Option<&crate::ChatMessage>| {
2933        message.map(|message| project_inline_media(message_json(message), options))
2934    };
2935    json!({
2936        "end_of_turn": end_of_turn,
2937        "first_assistant_message": project(first_assistant_message),
2938        "first_message": project(first_message),
2939        "last_assistant_message": project(last_assistant_message),
2940        "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2941        "last_message": project(last_message),
2942    })
2943}
2944
2945fn message_has_content(message: &crate::ChatMessage) -> bool {
2946    message
2947        .content
2948        .as_deref()
2949        .is_some_and(|content| !content.trim().is_empty())
2950        || message
2951            .content_parts
2952            .as_ref()
2953            .is_some_and(|parts| !parts.is_empty())
2954}
2955
2956fn message_text(message: &crate::ChatMessage) -> String {
2957    if let Some(content) = &message.content {
2958        return content.clone();
2959    }
2960    message
2961        .content_parts
2962        .as_ref()
2963        .into_iter()
2964        .flatten()
2965        .filter_map(|part| part.get("text").and_then(Value::as_str))
2966        .collect::<Vec<_>>()
2967        .join("\n")
2968}
2969
2970fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2971    let (offset, end) = projected_message_window(session.messages.len(), options);
2972    let messages = session.messages[offset..end]
2973        .iter()
2974        .map(|message| project_inline_media(message_json(message), options))
2975        .collect::<Vec<_>>();
2976    let subagents = if options.include_subagents.unwrap_or(true) {
2977        // The reported window describes the top-level transcript. Applying it
2978        // recursively would silently truncate subagents without returning a
2979        // window for each child. Keep their histories complete while carrying
2980        // the caller's media policy through the tree.
2981        let subagent_options = SessionLoadOptions {
2982            message_limit: None,
2983            message_offset: None,
2984            message_tail: None,
2985            ..options.clone()
2986        };
2987        session
2988            .subagents
2989            .iter()
2990            .map(|subagent| projected_session_json(subagent, &subagent_options))
2991            .collect::<Vec<_>>()
2992    } else {
2993        Vec::new()
2994    };
2995    json!({
2996        "source": match session.meta.source {
2997            SessionSource::ClaudeCode => "claude_code",
2998            SessionSource::Codex => "codex",
2999            SessionSource::Gemini => "gemini",
3000            SessionSource::Goose => "goose",
3001            SessionSource::Grok => "grok",
3002            SessionSource::Native => "native",
3003            SessionSource::OpenClaw => "openclaw",
3004            SessionSource::Hermes => "hermes",
3005            SessionSource::OpenCode => "opencode",
3006            SessionSource::Pi => "pi",
3007        },
3008        "session_id": session.meta.session_id,
3009        "ended_at": session.meta.ended_at,
3010        "end_reason": session.meta.end_reason,
3011        "model": session.meta.model,
3012        "cwd": session.meta.cwd,
3013        "system_prompt": session.meta.system_prompt,
3014        "agent_id": session.meta.agent_id,
3015        "parent_tool_use_id": session.meta.parent_tool_use_id,
3016        "lineage": session.meta.lineage,
3017        "messages": messages,
3018        "subagents": subagents,
3019        "raw_record_count": session.raw.len(),
3020        "parse_error_lines": session.parse_error_lines,
3021    })
3022}
3023
3024fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
3025    if let Some(tail) = options.message_tail {
3026        return (total.saturating_sub(tail), total);
3027    }
3028    let offset = options.message_offset.unwrap_or(0).min(total);
3029    let end = options
3030        .message_limit
3031        .map(|limit| offset.saturating_add(limit).min(total))
3032        .unwrap_or(total);
3033    (offset, end)
3034}
3035
3036fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
3037    let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
3038        return message;
3039    };
3040    for part in parts {
3041        let Some(url) = part
3042            .get("image_url")
3043            .and_then(|image| image.get("url"))
3044            .and_then(Value::as_str)
3045        else {
3046            continue;
3047        };
3048        let Some(rest) = url.strip_prefix("data:") else {
3049            continue;
3050        };
3051        let Some((media_type, encoded)) = rest.split_once(";base64,") else {
3052            continue;
3053        };
3054        let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
3055        let decoded_bytes = encoded.len().saturating_mul(3) / 4;
3056        let decoded_bytes = decoded_bytes.saturating_sub(padding);
3057        let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
3058            || options
3059                .max_inline_media_bytes
3060                .is_some_and(|limit| decoded_bytes > limit);
3061        if should_elide {
3062            *part = json!({
3063                "type": "media_reference",
3064                "media_type": media_type,
3065                "encoding": "base64",
3066                "encoded_bytes": encoded.len(),
3067                "decoded_bytes": decoded_bytes,
3068                "omitted": true,
3069            });
3070        }
3071    }
3072    message
3073}
3074
3075#[derive(Deserialize)]
3076struct LocatorParams {
3077    locator: SessionLocator,
3078    /// Optional fidelity for the READ surfaces (`sessions.load`,
3079    /// `sessions.follow`).
3080    ///
3081    /// Omitted means [`Fidelity::Semantic`]: these two methods only ever
3082    /// produce a read-only view, and a compacted or resumed-across-files
3083    /// transcript — the everyday shape of a long Claude Code session — has no
3084    /// losslessly reconstructable record graph, so refusing to render it made
3085    /// the mirror unusable rather than accurate. A caller that intends to
3086    /// CONTINUE from what it reads asks for a lossless level explicitly and
3087    /// gets the strict refusal back. Every other method (export, translate,
3088    /// branch, handoff, resume_instructions) is lossless-only and has no
3089    /// such knob.
3090    #[serde(default)]
3091    fidelity: Option<Fidelity>,
3092    /// Optional bounded frontend projection. Absent preserves the historical
3093    /// complete-session read contract.
3094    #[serde(default)]
3095    view: Option<SessionReadView>,
3096}
3097
3098#[derive(Deserialize)]
3099struct SessionReadView {
3100    /// Number of trailing normalized messages to return. Zero is treated as
3101    /// one so a caller cannot accidentally request an unbounded empty mode.
3102    #[serde(default)]
3103    tail_messages: Option<usize>,
3104    /// Whether Claude Code child transcripts belong in this view. The
3105    /// frontend default is false; the legacy no-view path remains true.
3106    #[serde(default)]
3107    include_subagents: bool,
3108    /// Preserve human-visible native history across model-context compaction.
3109    #[serde(default)]
3110    display_history: bool,
3111    /// Bound each individual text field so a single tool result cannot turn a
3112    /// small message window into a hundred-megabyte RPC response.
3113    #[serde(default)]
3114    max_message_chars: Option<usize>,
3115}
3116
3117impl LocatorParams {
3118    fn read_fidelity(&self) -> Fidelity {
3119        self.fidelity.unwrap_or(Fidelity::Semantic)
3120    }
3121
3122    fn include_subagents(&self) -> bool {
3123        self.view
3124            .as_ref()
3125            .map(|view| view.include_subagents)
3126            .unwrap_or(true)
3127    }
3128
3129    fn tail_messages(&self) -> Option<usize> {
3130        self.view
3131            .as_ref()
3132            .and_then(|view| view.tail_messages)
3133            .map(|limit| limit.clamp(1, 5_000))
3134    }
3135
3136    fn display_history(&self) -> bool {
3137        self.view.as_ref().is_some_and(|view| view.display_history)
3138    }
3139
3140    fn max_message_chars(&self) -> Option<usize> {
3141        self.view
3142            .as_ref()
3143            .and_then(|view| view.max_message_chars)
3144            .map(|limit| limit.clamp(256, 64_000))
3145    }
3146
3147    fn bound_session(&self, session: &mut Session) {
3148        bound_session_view(session, self.tail_messages(), self.max_message_chars());
3149    }
3150}
3151
3152#[derive(Debug, Clone, Copy, Default, Deserialize)]
3153#[serde(rename_all = "snake_case")]
3154enum InlineMediaMode {
3155    #[default]
3156    Full,
3157    Metadata,
3158}
3159
3160#[derive(Debug, Clone, Default, Deserialize)]
3161#[serde(default)]
3162struct SessionLoadOptions {
3163    include_subagents: Option<bool>,
3164    inline_media: InlineMediaMode,
3165    max_inline_media_bytes: Option<usize>,
3166    message_limit: Option<usize>,
3167    message_offset: Option<usize>,
3168    message_tail: Option<usize>,
3169}
3170
3171impl SessionLoadOptions {
3172    fn validate(&self) -> std::result::Result<(), ServiceError> {
3173        if self.message_tail.is_some()
3174            && (self.message_limit.is_some() || self.message_offset.is_some())
3175        {
3176            return Err(ServiceError::InvalidParams(
3177                "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
3178                    .into(),
3179            ));
3180        }
3181        Ok(())
3182    }
3183}
3184
3185#[derive(Deserialize)]
3186struct LoadSessionParams {
3187    #[serde(flatten)]
3188    read: LocatorParams,
3189    #[serde(default)]
3190    options: Option<SessionLoadOptions>,
3191}
3192
3193#[derive(Deserialize)]
3194struct UnfollowParams {
3195    subscription: String,
3196}
3197
3198#[derive(Debug, Deserialize)]
3199#[serde(deny_unknown_fields)]
3200struct IndexResizeParams {
3201    subscription: String,
3202    limit: usize,
3203}
3204
3205#[derive(Deserialize)]
3206struct ActivitySubscribeParams {
3207    locators: Vec<SessionLocator>,
3208    #[serde(default)]
3209    homes: crate::HarnessHomes,
3210}
3211
3212#[derive(Deserialize)]
3213struct MessageSessionParams {
3214    locator: SessionLocator,
3215    text: String,
3216    /// Same storage roots discovery accepts, so a caller (and a test) can
3217    /// point the live-session registry somewhere other than `$HOME`.
3218    #[serde(default)]
3219    homes: crate::HarnessHomes,
3220}
3221
3222#[derive(Deserialize)]
3223#[serde(deny_unknown_fields)]
3224struct HarnessSettingsParams {
3225    harness: String,
3226}
3227
3228#[derive(Deserialize)]
3229#[serde(deny_unknown_fields)]
3230struct ConfigureHarnessParams {
3231    harness: String,
3232    #[serde(default)]
3233    changes: Vec<crate::HarnessSettingChange>,
3234    #[serde(default)]
3235    expected_revision: Option<String>,
3236}
3237
3238fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3239    match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3240        Ok(report) => (
3241            serde_json::to_value(report).unwrap_or(Value::Null),
3242            Value::Null,
3243        ),
3244        Err(error) => (
3245            Value::Null,
3246            Value::String(format!(
3247                "Supercode could not inspect Claude Code inbound controls: {error}"
3248            )),
3249        ),
3250    }
3251}
3252
3253/// Deliver `text` into a session that is running right now, or say why not.
3254///
3255/// A refusal is a RESULT, not a JSON-RPC error: "that session is persisted
3256/// only" is an answer about the session, which a mirror renders next to the
3257/// transcript, and this service's error envelope carries no structured data
3258/// field a machine-readable reason could survive in.
3259///
3260/// `delivered_to_bus` is the honest ceiling of what the courier proves. The
3261/// message reached the receiving session's inbox; whether that session ever
3262/// reads it is governed by ITS OWN inbound controls (`crossSessionInbound`,
3263/// approval dialogs), which Supercode neither sees nor overrides.
3264async fn message_live_session(
3265    params: &MessageSessionParams,
3266    runner: &dyn crate::claude_peer::CourierRunner,
3267) -> Value {
3268    if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
3269        return json!({
3270            "delivered_to_bus": false,
3271            "refusal": {
3272                "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
3273                "message": format!(
3274                    "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
3275                    params.locator.harness.as_str()
3276                ),
3277            },
3278        });
3279    }
3280    let (inbound_controls, inbound_controls_error) =
3281        claude_inbound_controls_or_error(&params.homes);
3282    match crate::claude_peer::message_claude_peer(
3283        &params.homes,
3284        &params.locator.session_id,
3285        &params.text,
3286        runner,
3287    )
3288    .await
3289    {
3290        Ok(delivery) => json!({
3291            "delivered_to_bus": true,
3292            "target": {
3293                "session_id": delivery.target.session_id,
3294                "name": delivery.target.name,
3295                "pid": delivery.target.pid,
3296                "cwd": delivery.target.cwd,
3297                "status": delivery.target.status.map(|status| status.as_str()),
3298            },
3299            "courier": {
3300                "model": crate::claude_peer::COURIER_MODEL,
3301                "report": delivery.courier_report,
3302            },
3303            "inbound_controls": inbound_controls,
3304            "inbound_controls_error": inbound_controls_error,
3305        }),
3306        Err(refusal) => json!({
3307            "delivered_to_bus": false,
3308            "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
3309            "inbound_controls": inbound_controls,
3310            "inbound_controls_error": inbound_controls_error,
3311        }),
3312    }
3313}
3314
3315/// Source identity of one follow subscription, plus the last lifecycle state
3316/// already reported on it. The follower itself stays purely persistence-facing.
3317// Only the adapter-api poll reads these; the subscription bookkeeping itself is
3318// shared by both builds.
3319#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3320struct FollowedSource {
3321    harness: String,
3322    session_id: String,
3323    reported: Option<String>,
3324}
3325
3326#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3327struct ActivitySubscription {
3328    locators: Vec<SessionLocator>,
3329    homes: crate::HarnessHomes,
3330    reported: BTreeMap<(String, String), crate::SessionActivity>,
3331}
3332
3333fn peers_for_descriptors(
3334    descriptors: &[SessionDescriptor],
3335    homes: &HarnessHomes,
3336) -> Vec<crate::claude_peer::ClaudePeerSession> {
3337    if descriptors
3338        .iter()
3339        .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
3340    {
3341        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3342    } else {
3343        Vec::new()
3344    }
3345}
3346
3347/// Add the live address that makes an indexed row behaviorally equivalent to a discovered row.
3348///
3349/// The durable index owns only persistence metadata. Live endpoints remain projections: every
3350/// message/attach operation revalidates its authority, so publishing one here never trusts a stale
3351/// browser-held handle. Reading the Claude registry once per batch keeps this O(peers + rows).
3352fn live_descriptor_value(
3353    session: &SessionDescriptor,
3354    peers: &[crate::claude_peer::ClaudePeerSession],
3355) -> std::result::Result<Value, ServiceError> {
3356    let mut value = serde_json::to_value(session)
3357        .map_err(|error| ServiceError::Operation(error.to_string()))?;
3358    if let Some(workspace) = &session.cwd {
3359        let source = LiveRuntimeSource {
3360            harness: session.locator.harness.as_str().to_string(),
3361            session_id: session.locator.session_id.clone(),
3362            workspace: workspace.clone(),
3363        };
3364        if let Some(endpoint) = discover_live_runtime(&source)
3365            .map_err(|error| ServiceError::Operation(error.to_string()))?
3366        {
3367            value["live_endpoint"] = json!(endpoint.as_str());
3368        }
3369    }
3370    if value.get("live_endpoint").is_none() {
3371        if let Some(peer) = peers.iter().find(|peer| {
3372            session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3373                && peer.session_id == session.locator.session_id
3374        }) {
3375            value["live_endpoint"] = json!(peer.endpoint().as_str());
3376        }
3377    }
3378    Ok(value)
3379}
3380
3381fn live_index_changes(
3382    changes: Vec<crate::session_index::SessionIndexChange>,
3383    homes: &HarnessHomes,
3384) -> std::result::Result<Vec<Value>, ServiceError> {
3385    use crate::session_index::SessionIndexChange;
3386    let has_claude = changes.iter().any(|change| match change {
3387        SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3388            descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3389        }
3390        SessionIndexChange::Removed { .. } => false,
3391    });
3392    let peers = if has_claude {
3393        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3394    } else {
3395        Vec::new()
3396    };
3397    changes
3398        .into_iter()
3399        .map(|change| match change {
3400            SessionIndexChange::Added { descriptor } => Ok(json!({
3401                "kind": "added",
3402                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3403            })),
3404            SessionIndexChange::Updated { descriptor } => Ok(json!({
3405                "kind": "updated",
3406                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3407            })),
3408            SessionIndexChange::Removed { key } => Ok(json!({
3409                "kind": "removed",
3410                "key": key,
3411            })),
3412        })
3413        .collect()
3414}
3415
3416fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3417    use crate::{SessionPresence, SessionTurnState};
3418    match (activity.presence, activity.turn) {
3419        (SessionPresence::Persisted, _) => None,
3420        (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3421        (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3422        // The normalized activity object can honestly report a live owner even
3423        // when the stock harness never published a turn status. Preserve the
3424        // older field's stricter contract instead of guessing `running`.
3425        (SessionPresence::Running, SessionTurnState::Unknown)
3426            if activity.evidence.native_state.is_none() =>
3427        {
3428            None
3429        }
3430        (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3431    }
3432}
3433
3434#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3435#[serde(rename_all = "kebab-case")]
3436enum TransferFormat {
3437    ClaudeCode,
3438    Codex,
3439    #[serde(rename = "opencode", alias = "open-code")]
3440    OpenCode,
3441    Pi,
3442    Grok,
3443    Gemini,
3444    Goose,
3445    /// UNI-18: a Hermes target. Its artifact is the Codex rollout that
3446    /// `hermes sessions import --from codex` reads; `sessions.export` performs
3447    /// that import into the Hermes home.
3448    Hermes,
3449}
3450
3451impl TransferFormat {
3452    fn id(self) -> &'static str {
3453        match self {
3454            Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3455            Self::Codex => HarnessId::CODEX,
3456            Self::OpenCode => HarnessId::OPENCODE,
3457            Self::Pi => HarnessId::PI,
3458            Self::Grok => HarnessId::GROK,
3459            Self::Gemini => HarnessId::GEMINI,
3460            Self::Goose => HarnessId::GOOSE,
3461            Self::Hermes => HarnessId::HERMES,
3462        }
3463    }
3464}
3465
3466impl From<TransferFormat> for SessionFormat {
3467    fn from(value: TransferFormat) -> Self {
3468        match value {
3469            TransferFormat::ClaudeCode => Self::ClaudeCode,
3470            TransferFormat::Codex => Self::Codex,
3471            TransferFormat::OpenCode => Self::OpenCode,
3472            TransferFormat::Pi => Self::Pi,
3473            TransferFormat::Grok => Self::Grok,
3474            TransferFormat::Gemini => Self::Gemini,
3475            TransferFormat::Goose => Self::Goose,
3476            // a Hermes artifact is the Codex rollout Hermes imports
3477            TransferFormat::Hermes => Self::Codex,
3478        }
3479    }
3480}
3481
3482#[derive(Deserialize)]
3483struct ImportSessionParams {
3484    source_harness: TransferFormat,
3485    content: String,
3486}
3487
3488#[derive(Deserialize)]
3489struct ExportSessionParams {
3490    locator: SessionLocator,
3491    target_harness: TransferFormat,
3492}
3493
3494#[derive(Deserialize)]
3495struct ReduceSessionParams {
3496    locator: SessionLocator,
3497    target_harness: TransferFormat,
3498    #[serde(default = "default_keep_last")]
3499    keep_last: usize,
3500}
3501
3502fn default_keep_last() -> usize {
3503    6
3504}
3505
3506#[derive(Deserialize)]
3507struct BranchSessionParams {
3508    locator: SessionLocator,
3509    #[serde(default)]
3510    target_harness: Option<TransferFormat>,
3511}
3512
3513#[derive(Deserialize)]
3514struct HandoffSessionParams {
3515    locator: SessionLocator,
3516    target_harness: TransferFormat,
3517    #[serde(default)]
3518    cwd: Option<PathBuf>,
3519}
3520
3521#[derive(Deserialize)]
3522struct MaterializeSessionParams {
3523    artifact: crate::native_materialize::MaterializeArtifact,
3524    cwd: PathBuf,
3525}
3526
3527#[derive(Debug, Clone, Copy, Default, Deserialize)]
3528#[serde(rename_all = "snake_case")]
3529enum ResumePolicy {
3530    #[default]
3531    Default,
3532    Yolo,
3533}
3534
3535#[derive(Deserialize)]
3536struct ResumeInstructionsParams {
3537    locator: SessionLocator,
3538    #[serde(default)]
3539    cwd: Option<PathBuf>,
3540    #[serde(default)]
3541    policy: ResumePolicy,
3542}
3543
3544/// `harness.v1.workflow.load` parameters: which harness's board, and its home.
3545#[derive(Deserialize)]
3546struct WorkflowLoadParams {
3547    from: crate::workflow_doors::WorkflowHarness,
3548    home: PathBuf,
3549}
3550
3551/// ONT-4 `harness.v1.orchestration.load` parameters. `flavor` says which layout the
3552/// folder is read as; our own is the default.
3553#[derive(Deserialize)]
3554struct OrchestrationLoadParams {
3555    root: PathBuf,
3556    #[serde(default)]
3557    flavor: crate::orchestration_doors::HomeFlavor,
3558}
3559
3560/// ONT-4 `harness.v1.orchestration.save` parameters. `vault` is merged into the
3561/// home's own secrets; a caller that sends none keeps what is on disk.
3562#[derive(Deserialize)]
3563struct OrchestrationSaveParams {
3564    root: PathBuf,
3565    orchestration: crate::orchestration::Orchestration,
3566    #[serde(default)]
3567    vault: BTreeMap<String, String>,
3568}
3569
3570/// ONT-4 `harness.v1.orchestration.compile` parameters.
3571#[derive(Deserialize)]
3572struct OrchestrationCompileParams {
3573    from: crate::orchestration_doors::OrchestrationHarness,
3574    home: PathBuf,
3575}
3576
3577/// ONT-4 `harness.v1.orchestration.decompile` parameters. `source` is the home the
3578/// orchestration was compiled from: it is re-compiled to recover the io bookkeeping
3579/// that byte reuse and the live-store refusal (UNI-18) are decided from.
3580#[derive(Deserialize)]
3581struct OrchestrationDecompileParams {
3582    to: crate::orchestration_doors::OrchestrationHarness,
3583    orchestration: crate::orchestration::Orchestration,
3584    source: PathBuf,
3585    #[serde(default)]
3586    source_flavor: crate::orchestration_doors::SourceFlavor,
3587    dest: PathBuf,
3588    #[serde(default)]
3589    vault: BTreeMap<String, String>,
3590}
3591
3592/// `harness.v1.orchestration.import` parameters: another harness's home, and the
3593/// folder of ours it becomes.
3594#[derive(Deserialize)]
3595struct OrchestrationImportParams {
3596    from: crate::orchestration_doors::OrchestrationHarness,
3597    home: PathBuf,
3598    into: PathBuf,
3599}
3600
3601/// `harness.v1.orchestration.export` parameters: a folder of ours, and the home of
3602/// another harness it becomes.
3603#[derive(Deserialize)]
3604struct OrchestrationExportParams {
3605    to: crate::orchestration_doors::OrchestrationHarness,
3606    root: PathBuf,
3607    dest: PathBuf,
3608}
3609
3610/// `harness.v1.jobs.get` parameters.
3611#[derive(Deserialize)]
3612struct JobsGetParams {
3613    harness: String,
3614    id: String,
3615    #[serde(default)]
3616    homes: crate::HarnessHomes,
3617}
3618
3619/// ORCH-18: run one mutating job verb through the harness's own CLI.
3620///
3621/// The refusal ladder is deliberate: a harness with no scheduled-job concept
3622/// at all answers with the SAME sentence `jobs.list` gives it, and a harness
3623/// that has jobs but publishes no client-callable verb (Claude Code, whose
3624/// jobs are created by the model inside a session) answers with its own
3625/// reason. Neither is ever a silent no-op.
3626fn mutate_job(
3627    verb: crate::jobs_control::JobVerb,
3628    params: Value,
3629) -> std::result::Result<Value, ServiceError> {
3630    let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3631    refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3632    let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3633    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3634}
3635
3636/// ORCH-22: run one mutating skills verb through the harness's own door.
3637///
3638/// The refusal ladder mirrors `jobs.*`: a harness with no skills root at all
3639/// answers with the same sentence `skills.list` gives it, and a harness whose
3640/// door does not publish this verb (OpenClaw has no `skills remove` at the
3641/// pin) answers with its own reason. Neither is ever a silent no-op.
3642fn mutate_skill(
3643    verb: crate::skills_control::SkillVerb,
3644    params: Value,
3645) -> std::result::Result<Value, ServiceError> {
3646    let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3647    if !crate::skills_control::supports_skill_control(&mutation.harness) {
3648        return Err(ServiceError::UnsupportedAction(format!(
3649            "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3650            mutation.harness,
3651            verb.as_str(),
3652            crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3653        )));
3654    }
3655    let outcome =
3656        crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3657    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3658}
3659
3660/// The skills twin of [`job_control_error`], with the same mapping rule.
3661fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3662    match error {
3663        crate::skills_control::SkillControlError::Unsupported(message) => {
3664            ServiceError::UnsupportedAction(message)
3665        }
3666        crate::skills_control::SkillControlError::Invalid(message) => {
3667            ServiceError::InvalidParams(message)
3668        }
3669        crate::skills_control::SkillControlError::Failed(message) => {
3670            ServiceError::Operation(message)
3671        }
3672    }
3673}
3674
3675/// ORCH-21: run one mutating profile verb through the harness's own CLI.
3676///
3677/// The refusal ladder mirrors `mutate_job`'s: a harness with no profile
3678/// concept at all answers with the SAME sentence `profiles.list` gives it, and
3679/// a harness that HAS profiles but publishes no client-callable verb (Codex's
3680/// file-authored `[profiles.<name>]` tables, supercode's compiled-in presets)
3681/// answers with its own reason. Neither is ever a silent no-op.
3682fn mutate_profile(
3683    verb: crate::profiles_control::ProfileVerb,
3684    params: Value,
3685) -> std::result::Result<Value, ServiceError> {
3686    let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3687    let outcome =
3688        crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3689    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3690}
3691
3692/// The same mapping `job_control_error` applies, for the profile noun.
3693fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3694    match error {
3695        crate::profiles_control::ProfileControlError::Unsupported(message) => {
3696            ServiceError::UnsupportedAction(message)
3697        }
3698        crate::profiles_control::ProfileControlError::Invalid(message) => {
3699            ServiceError::InvalidParams(message)
3700        }
3701        crate::profiles_control::ProfileControlError::Failed(message) => {
3702            ServiceError::Operation(message)
3703        }
3704    }
3705}
3706
3707/// Map a controlled-tier failure onto the service's error vocabulary. A verb
3708/// the harness lacks is `UnsupportedAction`; a harness verb that RAN and
3709/// failed carries its own stderr through as the operation error.
3710fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3711    match error {
3712        crate::jobs_control::JobControlError::Unsupported(message) => {
3713            ServiceError::UnsupportedAction(message)
3714        }
3715        crate::jobs_control::JobControlError::Invalid(message) => {
3716            ServiceError::InvalidParams(message)
3717        }
3718        crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3719    }
3720}
3721
3722/// Map an ORCH-19 controlled-tier failure onto the service's error
3723/// vocabulary. A verb the harness has no door for is `UnsupportedAction`; a
3724/// door that RAN and failed carries the harness's own stderr / HTTP body
3725/// through as the operation error.
3726fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3727    match error {
3728        crate::SessionControlError::Unsupported(message) => {
3729            ServiceError::UnsupportedAction(message)
3730        }
3731        crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3732        crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3733    }
3734}
3735
3736/// A harness without a scheduled-job concept refuses the verb rather than
3737/// answering with an empty list — an absent capability and an empty inventory
3738/// are different answers (the same rule `runtimes.capabilities` applies to
3739/// `steer`).
3740fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3741    if crate::jobs::supports_jobs(harness) {
3742        return Ok(());
3743    }
3744    Err(ServiceError::UnsupportedAction(format!(
3745        "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3746        crate::jobs::JOB_HARNESSES.join(", ")
3747    )))
3748}
3749
3750/// `harness.v1.runs.get` parameters.
3751#[derive(Deserialize)]
3752struct RunsGetParams {
3753    harness: String,
3754    id: String,
3755    #[serde(default)]
3756    homes: crate::HarnessHomes,
3757}
3758
3759/// A harness with no run store refuses the verb rather than answering with an
3760/// empty history — the same rule `jobs.list` applies. Claude Code lands here
3761/// on purpose: its cron fires are ordinary turns inside the session that
3762/// created the job, so there is no fire record to list.
3763fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3764    if crate::runs::supports_runs(harness) {
3765        return Ok(());
3766    }
3767    Err(ServiceError::UnsupportedAction(format!(
3768        "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3769        crate::runs::RUN_HARNESSES.join(", ")
3770    )))
3771}
3772
3773#[derive(Serialize)]
3774struct SessionArtifact {
3775    source_harness: HarnessId,
3776    target_harness: &'static str,
3777    session_id: Option<String>,
3778    content: String,
3779    suggested_filename: String,
3780    files: Vec<SessionArtifactFile>,
3781    fidelity: Fidelity,
3782    residue: Vec<String>,
3783}
3784
3785#[derive(Serialize)]
3786struct SessionArtifactFile {
3787    path: String,
3788    content: String,
3789    role: ArtifactFileRole,
3790}
3791
3792#[derive(Serialize)]
3793#[serde(rename_all = "snake_case")]
3794enum ArtifactFileRole {
3795    Primary,
3796    Subagent,
3797    Bundle,
3798    SourceRecovery,
3799}
3800
3801#[derive(Serialize)]
3802struct StructuredLaunch {
3803    cwd: PathBuf,
3804    program: String,
3805    arguments: Vec<String>,
3806    env: BTreeMap<String, String>,
3807}
3808
3809struct HandoffInstructions {
3810    launch: StructuredLaunch,
3811    materialize: Option<StructuredLaunch>,
3812    requires_materialization: bool,
3813    note: String,
3814}
3815
3816#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3817#[serde(rename_all = "snake_case")]
3818enum HarnessProbeLevel {
3819    #[default]
3820    Passive,
3821    Handshake,
3822}
3823
3824#[derive(Default, Deserialize)]
3825#[serde(default)]
3826struct HarnessInventoryParams {
3827    harness: Option<HarnessId>,
3828    harnesses: Vec<HarnessId>,
3829    workspace: Option<PathBuf>,
3830    probe: HarnessProbeLevel,
3831    include_sessions: bool,
3832    /// Omit subprocess-based `--version` calls when a latency-sensitive UI only needs readiness.
3833    skip_versions: bool,
3834}
3835
3836#[derive(Deserialize)]
3837struct HarnessAuthenticationParams {
3838    harness: HarnessId,
3839}
3840
3841#[derive(Deserialize)]
3842struct BeginHarnessAuthenticationParams {
3843    harness: HarnessId,
3844    #[serde(default = "local_browser_authentication_environment")]
3845    environment: crate::HarnessAuthenticationEnvironment,
3846    #[serde(default)]
3847    method: Option<crate::HarnessAuthenticationMethodId>,
3848    #[serde(default)]
3849    cwd: Option<PathBuf>,
3850}
3851
3852fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3853    crate::HarnessAuthenticationEnvironment::LocalBrowser
3854}
3855
3856#[derive(Serialize)]
3857struct HarnessInventoryReport {
3858    probe: HarnessProbeLevel,
3859    workspace: Option<PathBuf>,
3860    harnesses: Vec<LocalHarness>,
3861}
3862
3863#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3864#[serde(rename_all = "snake_case")]
3865enum HarnessAuthState {
3866    Ready,
3867    Configured,
3868    Required,
3869    Unknown,
3870}
3871
3872#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3873#[serde(rename_all = "snake_case")]
3874enum HarnessRuntimeState {
3875    Ready,
3876    Degraded,
3877    Unavailable,
3878}
3879
3880#[derive(Serialize)]
3881struct HarnessSessionCounts {
3882    global: Option<usize>,
3883    workspace: Option<usize>,
3884}
3885
3886/// Receipt-backed evidence that a harness has a RUNNING instance right now,
3887/// distinct from being merely installed (UNI-7). Detection is passive and
3888/// default-on: a gateway liveness connect for daemon harnesses, a fresh
3889/// SQLite WAL stamp for store-writer harnesses (precedent: the opencode
3890/// follower's -wal/-shm freshness). Control stays behind per-connection
3891/// grants — this reports observations only.
3892/// ORCH-17: the gateway-health noun on an inventory row. Derived from the
3893/// UNI-7 running-instance probe (Hermes: `state.db-wal` freshness; OpenClaw:
3894/// a TCP connect to the gateway endpoint resolved from its OWN config) plus
3895/// the executable version — never by starting anything.
3896#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3897#[serde(rename_all = "snake_case")]
3898pub enum GatewayState {
3899    Up,
3900    Down,
3901    Unknown,
3902}
3903
3904/// ORCH-17: `gateway` on a `harness.v1.harnesses.list` row.
3905#[derive(Debug, Clone, Serialize)]
3906pub struct GatewayHealth {
3907    pub state: GatewayState,
3908    /// The endpoint supercode would connect to (OpenClaw: the gateway
3909    /// WebSocket resolved from `openclaw.json`; core harnesses: their
3910    /// declared connect address when one exists). `None` when the harness
3911    /// has no single endpoint (Hermes multiplexes platforms).
3912    #[serde(skip_serializing_if = "Option::is_none")]
3913    pub endpoint: Option<String>,
3914    #[serde(skip_serializing_if = "Option::is_none")]
3915    pub version: Option<String>,
3916    /// What the verdict rests on, or why it is `unknown`.
3917    pub evidence: String,
3918    pub checked_at_ms: u64,
3919}
3920
3921/// OpenClaw's gateway WebSocket endpoint, resolved from its own config the
3922/// way the registry's connect descriptor prescribes (`gateway.url`, else
3923/// `gateway.port`, else the documented default).
3924fn openclaw_gateway_endpoint(home: &Path) -> String {
3925    let config_path = home.join(".openclaw/openclaw.json");
3926    let gateway = std::fs::read_to_string(&config_path)
3927        .ok()
3928        .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3929        .and_then(|config| config.get("gateway").cloned());
3930    if let Some(url) = gateway
3931        .as_ref()
3932        .and_then(|gateway| gateway.get("url"))
3933        .and_then(serde_json::Value::as_str)
3934    {
3935        return url.to_string();
3936    }
3937    let port = gateway
3938        .as_ref()
3939        .and_then(|gateway| gateway.get("port"))
3940        .and_then(serde_json::Value::as_u64)
3941        .unwrap_or(18789);
3942    format!("ws://127.0.0.1:{port}")
3943}
3944
3945/// Ask Hermes itself (`hermes gateway status`, read-only, ~1 s) whether its
3946/// gateway is up. The command is per-host launchd/systemd text without a JSON
3947/// form at 0.19–0.21; the verdict is read from the lines it prints:
3948/// "supervised by launchd (PID …)" / "is running" → up, "not running" /
3949/// "not installed" → down, anything else → no verdict. `SUPERCODE_HERMES_BIN`
3950/// overrides the executable so a fake can stand in under test.
3951fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3952    let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3953    let output = std::process::Command::new(&program)
3954        .args(["gateway", "status"])
3955        .stdin(std::process::Stdio::null())
3956        .output()
3957        .ok()?;
3958    let text = format!(
3959        "{}{}",
3960        String::from_utf8_lossy(&output.stdout),
3961        String::from_utf8_lossy(&output.stderr)
3962    );
3963    let verdict = text.lines().find_map(|line| {
3964        let l = line.trim();
3965        if l.contains("supervised by launchd (PID")
3966            || l.contains("supervised by systemd (PID")
3967            || l.contains("Gateway is running")
3968            || l.contains("process is running")
3969        {
3970            Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3971        } else if l.contains("not running") || l.contains("not installed") {
3972            Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3973        } else {
3974            None
3975        }
3976    });
3977    verdict
3978}
3979
3980fn gateway_health(
3981    id: &str,
3982    installed: bool,
3983    running: Option<&RunningInstance>,
3984    version: Option<&str>,
3985) -> GatewayHealth {
3986    let checked_at_ms = now_epoch_ms();
3987    let home = std::env::var_os("HOME").map(PathBuf::from);
3988    match id {
3989        HarnessId::HERMES | HarnessId::OPENCLAW => {
3990            let endpoint = (id == HarnessId::OPENCLAW)
3991                .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3992                .flatten();
3993            let (state, evidence) = match running {
3994                Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3995                None if !installed => (
3996                    GatewayState::Unknown,
3997                    format!("`{id}` is not installed; no gateway to probe"),
3998                ),
3999                None if id == HarnessId::HERMES => match hermes_gateway_status() {
4000                    // The harness's own door outranks the WAL heuristic: an idle
4001                    // gateway writes nothing for minutes yet is up.
4002                    Some((state, evidence)) => (state, evidence),
4003                    None => (
4004                        GatewayState::Down,
4005                        "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
4006                    ),
4007                },
4008                None => (
4009                    GatewayState::Down,
4010                    format!(
4011                        "no TCP listener at {}",
4012                        endpoint.as_deref().unwrap_or("the gateway endpoint")
4013                    ),
4014                ),
4015            };
4016            GatewayHealth {
4017                state,
4018                endpoint,
4019                version: version.map(str::to_string),
4020                evidence,
4021                checked_at_ms,
4022            }
4023        }
4024        // ORC-7: the orchestrator's gateway IS its daemon, and the daemon's
4025        // own lease file is the record of it. A lease naming a live pid is
4026        // up; a lease whose process is gone is down and says so as a STALE
4027        // lease, never as "no lease"; no lease at all is down. Nothing is
4028        // started, and no port is guessed — the daemon multiplexes adapters
4029        // the way Hermes does, so it has no single endpoint either.
4030        HarnessId::ORCHESTRATOR => {
4031            let root = crate::HarnessHomes::default().orchestrator;
4032            let (state, evidence) = match crate::orchestrator::read_lease(&root) {
4033                Some(lease) if crate::orchestrator::pid_is_live(lease.pid) => (
4034                    GatewayState::Up,
4035                    format!(
4036                        "`{}` names pid {} (started {}), which is live",
4037                        crate::orchestrator::lock_path(&root).display(),
4038                        lease.pid,
4039                        lease.started_at
4040                    ),
4041                ),
4042                Some(lease) => (
4043                    GatewayState::Down,
4044                    format!(
4045                        "stale lease `{}`: pid {} is gone",
4046                        crate::orchestrator::lock_path(&root).display(),
4047                        lease.pid
4048                    ),
4049                ),
4050                None => (
4051                    GatewayState::Down,
4052                    format!(
4053                        "no lease at `{}`; `supercode orchestrator start` writes one",
4054                        crate::orchestrator::lock_path(&root).display()
4055                    ),
4056                ),
4057            };
4058            GatewayHealth {
4059                state,
4060                endpoint: None,
4061                version: version.map(str::to_string),
4062                evidence,
4063                checked_at_ms,
4064            }
4065        }
4066        _ => GatewayHealth {
4067            state: GatewayState::Unknown,
4068            endpoint: None,
4069            version: version.map(str::to_string),
4070            evidence: format!("`{id}` runs per session, not as a gateway"),
4071            checked_at_ms,
4072        },
4073    }
4074}
4075
4076#[derive(Debug, Clone, Serialize)]
4077struct RunningInstance {
4078    /// How the instance was detected.
4079    method: RunningInstanceMethod,
4080    /// The evidence the verdict rests on (endpoint reached / WAL path+age).
4081    evidence: String,
4082    /// Epoch-ms instant the probe executed.
4083    checked_at_ms: u64,
4084}
4085
4086#[derive(Debug, Clone, Copy, Serialize)]
4087#[serde(rename_all = "snake_case")]
4088enum RunningInstanceMethod {
4089    /// A TCP connect to the harness's own configured gateway endpoint
4090    /// succeeded.
4091    GatewayConnect,
4092    /// The harness's session store has an active SQLite WAL (a live writer
4093    /// holds the store open and stamped it recently).
4094    StoreWalActivity,
4095}
4096
4097fn now_epoch_ms() -> u64 {
4098    std::time::SystemTime::now()
4099        .duration_since(std::time::UNIX_EPOCH)
4100        .map(|elapsed| elapsed.as_millis() as u64)
4101        .unwrap_or(0)
4102}
4103
4104/// OpenClaw: the gateway endpoint comes from the harness's OWN config
4105/// (`<home>/.openclaw/openclaw.json` — `gateway.url` or `gateway.port`,
4106/// default port 18789); a successful TCP connect is the running signal.
4107fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4108    let config_path = home.join(".openclaw/openclaw.json");
4109    let text = std::fs::read_to_string(&config_path).ok();
4110    let gateway = text
4111        .as_deref()
4112        .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4113        .and_then(|config| config.get("gateway").cloned());
4114    let address = gateway
4115        .as_ref()
4116        .and_then(|gateway| gateway.get("url"))
4117        .and_then(serde_json::Value::as_str)
4118        .and_then(|url| {
4119            url.split("://").nth(1).map(|rest| {
4120                rest.trim_end_matches('/')
4121                    .split('/')
4122                    .next()
4123                    .unwrap_or(rest)
4124                    .to_string()
4125            })
4126        })
4127        .unwrap_or_else(|| {
4128            let port = gateway
4129                .as_ref()
4130                .and_then(|gateway| gateway.get("port"))
4131                .and_then(serde_json::Value::as_u64)
4132                .unwrap_or(18789);
4133            format!("127.0.0.1:{port}")
4134        });
4135    let reachable = std::net::TcpStream::connect_timeout(
4136        &address.parse().ok()?,
4137        std::time::Duration::from_millis(400),
4138    )
4139    .is_ok();
4140    reachable.then(|| RunningInstance {
4141        method: RunningInstanceMethod::GatewayConnect,
4142        evidence: format!(
4143            "gateway endpoint {address} accepted a TCP connect (from {})",
4144            config_path.display()
4145        ),
4146        checked_at_ms: now_epoch_ms(),
4147    })
4148}
4149
4150/// Hermes: `<home>/.hermes/state.db-wal` freshly modified means a live writer
4151/// holds the store open (SQLite WAL exists only while a connection is open;
4152/// a recent stamp distinguishes an active instance from a stale crash
4153/// leftover).
4154fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4155    let wal = home.join(".hermes/state.db-wal");
4156    let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4157    let age_ms = std::time::SystemTime::now()
4158        .duration_since(modified)
4159        .map(|age| age.as_millis() as u64)
4160        .unwrap_or(u64::MAX);
4161    (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4162        method: RunningInstanceMethod::StoreWalActivity,
4163        evidence: format!(
4164            "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4165            wal.display()
4166        ),
4167        checked_at_ms: now_epoch_ms(),
4168    })
4169}
4170
4171/// Default-on running-instance detection for the harnesses that have one.
4172fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4173    let home = std::env::var_os("HOME").map(PathBuf::from)?;
4174    match id {
4175        HarnessId::OPENCLAW => probe_openclaw_running(&home),
4176        HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4177        _ => None,
4178    }
4179}
4180
4181#[derive(Serialize)]
4182struct LocalHarness {
4183    id: HarnessId,
4184    display_name: String,
4185    supported: bool,
4186    installed: bool,
4187    executable: Option<String>,
4188    version: Option<String>,
4189    auth: HarnessAuthState,
4190    runtime: HarnessRuntimeState,
4191    protocol: String,
4192    capabilities: crate::RuntimeCapabilities,
4193    effective_capabilities: crate::RuntimeCapabilities,
4194    sessions: HarnessSessionCounts,
4195    /// Receipt-backed running-instance detection (None = not detected or the
4196    /// harness has no running-instance concept). Distinct from `installed`.
4197    #[serde(skip_serializing_if = "Option::is_none")]
4198    running: Option<RunningInstance>,
4199    /// ORCH-17: gateway health derived from `running` + the harness's own config.
4200    gateway: GatewayHealth,
4201    reason: Option<String>,
4202    repair: Option<String>,
4203}
4204
4205#[derive(Clone, Deserialize)]
4206struct RuntimeBackendParams {
4207    harness: HarnessId,
4208    #[serde(default)]
4209    protocol: Option<String>,
4210    #[serde(default)]
4211    launch: Option<RuntimeLaunch>,
4212    #[serde(default)]
4213    base_url: Option<String>,
4214    #[serde(default)]
4215    policy: RuntimePolicy,
4216}
4217
4218#[derive(Debug, Clone, Copy, Default, Deserialize)]
4219#[serde(rename_all = "snake_case")]
4220enum RuntimePolicy {
4221    #[default]
4222    Default,
4223    Yolo,
4224}
4225
4226#[derive(Deserialize)]
4227struct RuntimeStartParams {
4228    #[serde(flatten)]
4229    backend: RuntimeBackendParams,
4230    cwd: PathBuf,
4231    /// MCP servers to mount into the new session through the harness's own
4232    /// start door (ORC-6). Backends without such a door ignore them.
4233    #[serde(default)]
4234    mcp_servers: Vec<crate::McpServerLaunch>,
4235}
4236
4237#[derive(Deserialize)]
4238struct RuntimeAttachParams {
4239    #[serde(flatten)]
4240    backend: RuntimeBackendParams,
4241    runtime_id: String,
4242    #[serde(default)]
4243    cwd: Option<PathBuf>,
4244    /// MCP servers to mount into the resumed session (the start door's own
4245    /// field, carried again because a session's tools die with its process).
4246    #[serde(default)]
4247    mcp_servers: Vec<crate::McpServerLaunch>,
4248}
4249
4250#[derive(Deserialize)]
4251struct RuntimeConnectionParams {
4252    connection: String,
4253}
4254
4255#[derive(Deserialize)]
4256struct RuntimeInputParams {
4257    connection: String,
4258    text: String,
4259    #[serde(default)]
4260    image_urls: Vec<String>,
4261}
4262
4263const MAX_RUNTIME_IMAGES: usize = 4;
4264const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4265const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4266
4267fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4268    if image_urls.len() > MAX_RUNTIME_IMAGES {
4269        return Err(ServiceError::InvalidParams(format!(
4270            "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4271        )));
4272    }
4273    let mut total = 0usize;
4274    for url in &image_urls {
4275        if !(url.starts_with("data:image/")
4276            || url.starts_with("https://")
4277            || url.starts_with("http://"))
4278        {
4279            return Err(ServiceError::InvalidParams(
4280                "runtime images must be image data URLs or HTTP(S) URLs".into(),
4281            ));
4282        }
4283        if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4284            return Err(ServiceError::InvalidParams(format!(
4285                "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4286            )));
4287        }
4288        total = total.saturating_add(url.len());
4289    }
4290    if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4291        return Err(ServiceError::InvalidParams(format!(
4292            "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4293        )));
4294    }
4295    Ok(image_urls)
4296}
4297
4298#[derive(Deserialize)]
4299struct RuntimeRespondParams {
4300    connection: String,
4301    request_id: Value,
4302    response: Value,
4303}
4304
4305fn default_reduction_store_root() -> PathBuf {
4306    if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4307        return PathBuf::from(root).join("sessions");
4308    }
4309    if let Some(home) = std::env::var_os("HOME") {
4310        return PathBuf::from(home).join(".supercode").join("sessions");
4311    }
4312    PathBuf::from(".supercode").join("sessions")
4313}
4314
4315fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4316    let mut output = String::new();
4317    for message in messages {
4318        output.push_str(
4319            &serde_json::to_string(message)
4320                .map_err(|error| ServiceError::Operation(error.to_string()))?,
4321        );
4322        output.push('\n');
4323    }
4324    Ok(output)
4325}
4326
4327fn parse_messages_jsonl(
4328    content: &str,
4329) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4330    content
4331        .lines()
4332        .enumerate()
4333        .filter(|(_, line)| !line.trim().is_empty())
4334        .map(|(index, line)| {
4335            serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4336                ServiceError::Operation(format!(
4337                    "reduced transcript line {} is invalid: {error}",
4338                    index + 1
4339                ))
4340            })
4341        })
4342        .collect()
4343}
4344
4345fn reduced_bootstrap_prompt(
4346    source: &SessionLocator,
4347    target: TransferFormat,
4348    view_jsonl: &str,
4349    sidecar_path: &Path,
4350    reduction_log_path: &Path,
4351) -> String {
4352    format!(
4353        "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4354         \n\
4355         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\
4356         \n\
4357         <supercode-reduced-session source-session=\"{source_id}\">\n\
4358         {view_jsonl}\
4359         </supercode-reduced-session>\n\
4360         \n\
4361         Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4362        source_harness = source.harness.as_str(),
4363        target_harness = target.id(),
4364        sidecar = sidecar_path.display(),
4365        log = reduction_log_path.display(),
4366        source_id = source.session_id,
4367    )
4368}
4369
4370fn session_artifact(
4371    locator: &SessionLocator,
4372    session: &Session,
4373    target: TransferFormat,
4374) -> std::result::Result<SessionArtifact, ServiceError> {
4375    session_artifact_with_id(locator, session, target, None)
4376}
4377
4378fn session_artifact_with_id(
4379    locator: &SessionLocator,
4380    session: &Session,
4381    target: TransferFormat,
4382    target_session_id: Option<&str>,
4383) -> std::result::Result<SessionArtifact, ServiceError> {
4384    let format: SessionFormat = target.into();
4385    let diagonal = format.source() == session.meta.source;
4386    let has_appended_turns = session
4387        .imported_message_count
4388        .is_some_and(|imported| imported < session.messages.len());
4389    let content = if let Some(id) = target_session_id {
4390        if diagonal && format != SessionFormat::OpenCode {
4391            session
4392                .to_jsonl_spliced(format, Some(id))
4393                .map_err(operation)?
4394        } else {
4395            let mut rewritten = session.clone();
4396            rewritten.meta.session_id = Some(id.to_string());
4397            rewritten.to_jsonl(format).map_err(operation)?
4398        }
4399    } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4400        session.raw_verbatim()
4401    } else if diagonal {
4402        session.to_jsonl_spliced(format, None).map_err(operation)?
4403    } else {
4404        session.to_jsonl(format).map_err(operation)?
4405    };
4406    let stem = sanitize_filename(
4407        target_session_id
4408            .or(session.meta.session_id.as_deref())
4409            .unwrap_or(&locator.session_id),
4410    );
4411    let suggested_filename = if diagonal && target == TransferFormat::Grok {
4412        "chat_history.jsonl".to_string()
4413    } else if target == TransferFormat::Goose {
4414        format!("{stem}.goose.json")
4415    } else {
4416        format!("{stem}.{}.jsonl", target.id())
4417    };
4418    let mut files = vec![SessionArtifactFile {
4419        path: suggested_filename.clone(),
4420        content: content.clone(),
4421        role: ArtifactFileRole::Primary,
4422    }];
4423    if target == TransferFormat::ClaudeCode {
4424        let bundle_stem = Path::new(&suggested_filename)
4425            .file_stem()
4426            .and_then(|stem| stem.to_str())
4427            .unwrap_or(&stem);
4428        let mut child_paths = BTreeSet::new();
4429        for (index, subagent) in session.subagents.iter().enumerate() {
4430            let agent_id = subagent
4431                .meta
4432                .agent_id
4433                .as_deref()
4434                .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4435                .map(sanitize_filename)
4436                .filter(|id| !id.is_empty())
4437                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4438            let child_has_appended_turns = subagent
4439                .imported_message_count
4440                .is_some_and(|imported| imported < subagent.messages.len());
4441            let child_content = if target_session_id.is_none()
4442                && subagent.meta.source == SessionSource::ClaudeCode
4443                && subagent.raw_is_verbatim
4444                && !child_has_appended_turns
4445            {
4446                subagent.raw_verbatim()
4447            } else if subagent.meta.source == SessionSource::ClaudeCode {
4448                subagent
4449                    .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4450                    .map_err(operation)?
4451            } else {
4452                let mut child = subagent.clone();
4453                if let Some(id) = target_session_id {
4454                    child.meta.session_id = Some(id.to_string());
4455                }
4456                child
4457                    .to_jsonl(SessionFormat::ClaudeCode)
4458                    .map_err(operation)?
4459            };
4460            let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4461            if !child_paths.insert(path.clone()) {
4462                return Err(ServiceError::Operation(format!(
4463                    "Claude subagent ids collide at artifact path `{path}`"
4464                )));
4465            }
4466            files.push(SessionArtifactFile {
4467                path,
4468                content: child_content,
4469                role: ArtifactFileRole::Subagent,
4470            });
4471        }
4472    }
4473    if diagonal && target == TransferFormat::Grok {
4474        append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4475    }
4476    if !diagonal || !session.raw_is_verbatim {
4477        files.push(SessionArtifactFile {
4478            path: "recovery/source.supercode.jsonl".into(),
4479            content: session.to_native_jsonl(),
4480            role: ArtifactFileRole::SourceRecovery,
4481        });
4482        for (index, subagent) in session.subagents.iter().enumerate() {
4483            let id = subagent
4484                .meta
4485                .agent_id
4486                .as_deref()
4487                .map(sanitize_filename)
4488                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4489            files.push(SessionArtifactFile {
4490                path: format!("recovery/subagents/{id}.supercode.jsonl"),
4491                content: subagent.to_native_jsonl(),
4492                role: ArtifactFileRole::SourceRecovery,
4493            });
4494        }
4495    }
4496    if !diagonal && session.meta.source == SessionSource::Grok {
4497        append_grok_bundle_files(
4498            locator,
4499            "recovery/grok/",
4500            ArtifactFileRole::SourceRecovery,
4501            &mut files,
4502        )?;
4503    }
4504    let (fidelity, residue) = if diagonal
4505        && target_session_id.is_none()
4506        && session.raw_is_verbatim
4507        && !has_appended_turns
4508    {
4509        (Fidelity::ByteLossless, Vec::new())
4510    } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4511        (
4512            Fidelity::ValueLossless,
4513            vec![if target_session_id.is_some() {
4514                "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4515            } else {
4516                "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4517            }],
4518        )
4519    } else {
4520        (
4521            Fidelity::Semantic,
4522            vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4523        )
4524    };
4525    Ok(SessionArtifact {
4526        source_harness: locator.harness.clone(),
4527        target_harness: target.id(),
4528        session_id: target_session_id
4529            .map(str::to_string)
4530            .or_else(|| session.meta.session_id.clone()),
4531        content,
4532        suggested_filename,
4533        files,
4534        fidelity,
4535        residue,
4536    })
4537}
4538
4539fn append_grok_bundle_files(
4540    locator: &SessionLocator,
4541    prefix: &str,
4542    role: ArtifactFileRole,
4543    files: &mut Vec<SessionArtifactFile>,
4544) -> std::result::Result<(), ServiceError> {
4545    let primary = locator.storage.path();
4546    if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4547        return Err(ServiceError::Operation(format!(
4548            "Grok bundle locator must name chat_history.jsonl, got {}",
4549            primary.display()
4550        )));
4551    }
4552    let parent = primary.parent().ok_or_else(|| {
4553        ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4554    })?;
4555    for name in ["summary.json", "updates.jsonl"] {
4556        let path = parent.join(name);
4557        let metadata = match std::fs::symlink_metadata(&path) {
4558            Ok(metadata) => metadata,
4559            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4560            Err(error) => return Err(ServiceError::Operation(error.to_string())),
4561        };
4562        if metadata.file_type().is_symlink() || !metadata.is_file() {
4563            return Err(ServiceError::Operation(format!(
4564                "refusing non-regular Grok bundle member {}",
4565                path.display()
4566            )));
4567        }
4568        let content = std::fs::read_to_string(&path).map_err(|error| {
4569            ServiceError::Operation(format!(
4570                "Grok bundle member {} is not representable as UTF-8: {error}",
4571                path.display()
4572            ))
4573        })?;
4574        files.push(SessionArtifactFile {
4575            path: format!("{prefix}{name}"),
4576            content,
4577            role: match role {
4578                ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4579                _ => ArtifactFileRole::SourceRecovery,
4580            },
4581        });
4582    }
4583    Ok(())
4584}
4585
4586fn handoff_artifact(
4587    locator: &SessionLocator,
4588    session: &Session,
4589    target: TransferFormat,
4590    cwd: &Path,
4591) -> std::result::Result<SessionArtifact, ServiceError> {
4592    if target != TransferFormat::Grok {
4593        let target_session_id = target_session_id(target);
4594        return session_artifact_with_id(locator, session, target, Some(&target_session_id));
4595    }
4596
4597    // Stock Grok's importer accepts Claude/Codex transcripts and materializes its own
4598    // multi-file session bundle. A synthesized Grok chat_history.jsonl alone is not a
4599    // resumable handoff because updates.jsonl is the authoritative restore log.
4600    let mut importable = session.clone();
4601    // The Claude importer validates sessionId as a UUID. Source harness identities
4602    // are not portable (OpenCode, for example, uses `ses_...`), and a handoff must
4603    // not overwrite an existing target session when the source already uses UUIDs.
4604    // Mint a distinct target identity and still bind the importer-returned ID at
4605    // launch time because the importer remains the authority on materialization.
4606    importable.meta.session_id = Some(target_session_id(TransferFormat::ClaudeCode));
4607    importable.meta.cwd = Some(if cwd.is_absolute() {
4608        cwd.to_path_buf()
4609    } else {
4610        std::env::current_dir()
4611            .map_err(|error| ServiceError::Operation(error.to_string()))?
4612            .join(cwd)
4613    });
4614    let content = importable
4615        .to_jsonl(SessionFormat::ClaudeCode)
4616        .map_err(operation)?;
4617    let stem = sanitize_filename(
4618        importable
4619            .meta
4620            .session_id
4621            .as_deref()
4622            .unwrap_or(&locator.session_id),
4623    );
4624    let suggested_filename = format!("{stem}.grok-import.claude-code.jsonl");
4625    Ok(SessionArtifact {
4626        source_harness: locator.harness.clone(),
4627        // This names the artifact's actual wire format. The requested handoff target
4628        // remains Grok; its official importer is the materialization boundary.
4629        target_harness: TransferFormat::ClaudeCode.id(),
4630        session_id: importable.meta.session_id.clone(),
4631        content: content.clone(),
4632        suggested_filename: suggested_filename.clone(),
4633        files: vec![SessionArtifactFile {
4634            path: suggested_filename,
4635            content,
4636            role: ArtifactFileRole::Primary,
4637        }],
4638        fidelity: Fidelity::Semantic,
4639        residue: vec!["Grok's stock importer accepts a Claude Code transcript, not a complete Grok updates/session bundle".into()],
4640    })
4641}
4642
4643fn target_session_id(target: TransferFormat) -> String {
4644    let uuid = generated_session_id();
4645    match target {
4646        TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4647        TransferFormat::ClaudeCode
4648        | TransferFormat::Codex
4649        | TransferFormat::Pi
4650        | TransferFormat::Grok
4651        | TransferFormat::Gemini
4652        | TransferFormat::Goose
4653        | TransferFormat::Hermes => uuid,
4654    }
4655}
4656
4657fn sanitize_filename(value: &str) -> String {
4658    let value = value
4659        .chars()
4660        .map(|character| {
4661            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4662                character
4663            } else {
4664                '-'
4665            }
4666        })
4667        .collect::<String>();
4668    let value = value.trim_matches('-');
4669    if value.is_empty() {
4670        "session".into()
4671    } else {
4672        value.chars().take(100).collect()
4673    }
4674}
4675
4676fn handoff_instructions(
4677    target: TransferFormat,
4678    session_id: &str,
4679    cwd: &Path,
4680) -> HandoffInstructions {
4681    let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4682        cwd: cwd.to_path_buf(),
4683        program: program.into(),
4684        arguments,
4685        env: BTreeMap::new(),
4686    };
4687    match target {
4688        TransferFormat::ClaudeCode => HandoffInstructions {
4689            launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4690            materialize: None,
4691            requires_materialization: true,
4692            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(),
4693        },
4694        TransferFormat::Hermes => HandoffInstructions {
4695            launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4696            materialize: None,
4697            requires_materialization: true,
4698            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(),
4699        },
4700        TransferFormat::Codex => HandoffInstructions {
4701            launch: launch("codex", vec!["resume".into(), session_id.into()]),
4702            materialize: None,
4703            requires_materialization: true,
4704            note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4705        },
4706        TransferFormat::OpenCode => HandoffInstructions {
4707            launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4708            materialize: Some(launch(
4709                "opencode",
4710                vec!["import".into(), "{artifact_path}".into()],
4711            )),
4712            requires_materialization: true,
4713            note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4714        },
4715        TransferFormat::Pi => HandoffInstructions {
4716            launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4717            materialize: None,
4718            requires_materialization: true,
4719            note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4720        },
4721        TransferFormat::Grok => HandoffInstructions {
4722            launch: launch(
4723                "grok",
4724                vec![
4725                    "--resume".into(),
4726                    "{imported_session_id}".into(),
4727                    "--fork-session".into(),
4728                ],
4729            ),
4730            materialize: Some(launch(
4731                "grok",
4732                vec!["import".into(), "--json".into(), "{artifact_path}".into()],
4733            )),
4734            requires_materialization: true,
4735            note: "The artifact is Claude Code JSONL for Grok's official importer. Write it to a file, run the materialize command, read sessionId from its NDJSON outcome=imported record, replace {imported_session_id} in the launch arguments, then launch a writable fork of the imported session.".into(),
4736        },
4737        TransferFormat::Gemini => HandoffInstructions {
4738            launch: launch(
4739                "gemini",
4740                vec!["--session-file".into(), "{artifact_path}".into()],
4741            ),
4742            materialize: None,
4743            requires_materialization: true,
4744            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(),
4745        },
4746        TransferFormat::Goose => HandoffInstructions {
4747            launch: launch(
4748                "goose",
4749                vec![
4750                    "session".into(),
4751                    "--resume".into(),
4752                    "--session-id".into(),
4753                    "{imported_session_id}".into(),
4754                ],
4755            ),
4756            materialize: Some(launch(
4757                "goose",
4758                vec!["session".into(), "import".into(), "{artifact_path}".into()],
4759            )),
4760            requires_materialization: true,
4761            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(),
4762        },
4763    }
4764}
4765
4766fn resume_launch(
4767    harness: &str,
4768    session_id: &str,
4769    cwd: &Path,
4770    policy: ResumePolicy,
4771) -> std::result::Result<StructuredLaunch, ServiceError> {
4772    let mut arguments = Vec::new();
4773    let program = match harness {
4774        HarnessId::GROK => {
4775            if matches!(policy, ResumePolicy::Yolo) {
4776                if crate::support::self_sandbox_supported() {
4777                    arguments.extend(["--sandbox".into(), "workspace".into()]);
4778                }
4779                arguments.push("--always-approve".into());
4780            }
4781            arguments.extend(["--resume".into(), session_id.into()]);
4782            "grok"
4783        }
4784        HarnessId::CODEX => {
4785            let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4786                .expect("a filesystem path always serializes as JSON text");
4787            arguments.extend([
4788                "-c".into(),
4789                "check_for_update_on_startup=false".into(),
4790                "-c".into(),
4791                format!("projects.{cwd_key}.trust_level=\"trusted\""),
4792            ]);
4793            if matches!(policy, ResumePolicy::Yolo) {
4794                arguments.extend([
4795                    "--dangerously-bypass-approvals-and-sandbox".into(),
4796                    "--dangerously-bypass-hook-trust".into(),
4797                ]);
4798            }
4799            arguments.extend(["resume".into(), session_id.into()]);
4800            "codex"
4801        }
4802        HarnessId::CLAUDE_CODE => {
4803            if matches!(policy, ResumePolicy::Yolo) {
4804                arguments.push("--dangerously-skip-permissions".into());
4805            }
4806            arguments.extend(["--resume".into(), session_id.into()]);
4807            "claude"
4808        }
4809        HarnessId::GEMINI => {
4810            if matches!(policy, ResumePolicy::Yolo) {
4811                arguments.push("--yolo".into());
4812            }
4813            arguments.extend(["--resume".into(), session_id.into()]);
4814            "gemini"
4815        }
4816        HarnessId::GOOSE => {
4817            arguments.extend([
4818                "session".into(),
4819                "--resume".into(),
4820                "--session-id".into(),
4821                session_id.into(),
4822            ]);
4823            "goose"
4824        }
4825        HarnessId::PI => {
4826            if matches!(policy, ResumePolicy::Yolo) {
4827                arguments.push("--approve".into());
4828            }
4829            arguments.extend(["--session".into(), session_id.into()]);
4830            "pi"
4831        }
4832        HarnessId::OPENCODE => {
4833            arguments.extend(["--session".into(), session_id.into()]);
4834            "opencode"
4835        }
4836        HarnessId::SUPERCODE => {
4837            if matches!(policy, ResumePolicy::Yolo) {
4838                arguments.push("--dangerous".into());
4839            }
4840            arguments.extend(["resume".into(), session_id.into()]);
4841            "supercode"
4842        }
4843        other => {
4844            return Err(ServiceError::InvalidParams(format!(
4845                "no structured resume launch is registered for harness `{other}`"
4846            )))
4847        }
4848    };
4849    Ok(StructuredLaunch {
4850        cwd: cwd.to_path_buf(),
4851        program: program.into(),
4852        arguments,
4853        env: BTreeMap::new(),
4854    })
4855}
4856
4857/// Stage the resolved gateway credential in a private (0600) file so the
4858/// bridge can read it via `--token-file` — the delivery the real `openclaw
4859/// acp` accepts. One stable file per endpoint (keyed by an address digest,
4860/// no secret material in the name), overwritten on every connect so files
4861/// never accumulate and a rotated token never goes stale on disk.
4862fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4863    let digest = blake3::hash(address.as_bytes()).to_hex();
4864    let path = std::env::temp_dir().join(format!(
4865        "supercode-openclaw-gateway-token-{}",
4866        &digest.as_str()[..16]
4867    ));
4868    #[cfg(unix)]
4869    {
4870        use std::io::Write;
4871        use std::os::unix::fs::OpenOptionsExt;
4872        let mut file = std::fs::OpenOptions::new()
4873            .write(true)
4874            .create(true)
4875            .truncate(true)
4876            .mode(0o600)
4877            .open(&path)?;
4878        file.write_all(secret.as_bytes())?;
4879    }
4880    #[cfg(not(unix))]
4881    std::fs::write(&path, secret)?;
4882    Ok(path)
4883}
4884
4885/// Open a connect-mode descriptor: resolve the endpoint address and
4886/// credential from the harness's own config file and build the backend that
4887/// joins the already-running endpoint. Fails closed with a specific
4888/// diagnostic when the config cannot be resolved or the declared protocol has
4889/// no connect-capable client yet.
4890fn open_connect_descriptor(
4891    descriptor: &crate::HarnessSupportDescriptor,
4892    home: &Path,
4893) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4894    let Some(connect) = &descriptor.runtime.connect_launch else {
4895        return Err(ServiceError::InvalidParams(format!(
4896            "harness `{}` has no registered connect-mode launch",
4897            descriptor.id.as_str()
4898        )));
4899    };
4900    let resolved = connect
4901        .resolve(home)
4902        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4903    match (descriptor.id.as_str(), connect.protocol.as_str()) {
4904        (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4905            let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4906            if let Some(token) = resolved.auth {
4907                backend = backend.with_bearer(token);
4908            }
4909            Ok(Box::new(backend))
4910        }
4911        (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4912            // OpenClaw's own `openclaw acp` binary is the gateway client: a
4913            // stdio ACP bridge that joins the RUNNING gateway at the resolved
4914            // endpoint. Blind-walk finding 2026-08-31: the real bridge does
4915            // NOT honor OPENCLAW_GATEWAY_TOKEN from the environment — the
4916            // credential must arrive via `--token-file` (never bare `--token`
4917            // on argv, where process listings could read it). The env var is
4918            // still set for older bridges that did read it. Requires openclaw
4919            // >= 2026.7: the 2026.2 bridge drops its gateway socket
4920            // mid-prompt and advertises no session resume (executed finding,
4921            // docs/interop/research/openclaw-acp-dialect-2026-08-30.json).
4922            let mut env = BTreeMap::new();
4923            let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4924            if let Some(token) = resolved.auth {
4925                let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4926                    .map_err(|error| {
4927                        ServiceError::UnsupportedAction(format!(
4928                            "could not stage the gateway credential for the bridge: {error}"
4929                        ))
4930                    })?;
4931                arguments.push("--token-file".into());
4932                arguments.push(token_path.to_string_lossy().into_owned());
4933                env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4934            }
4935            // The bridge program comes from the descriptor's own default
4936            // launch (the compiled registry pins `openclaw`), so tests can
4937            // substitute an absolute mock-bridge path without touching
4938            // process-global state.
4939            let program = descriptor
4940                .runtime
4941                .default_launch
4942                .as_ref()
4943                .map(|launch| launch.program.clone())
4944                .unwrap_or_else(|| "openclaw".into());
4945            let launch = RuntimeLaunch {
4946                program,
4947                arguments,
4948                env,
4949            };
4950            Ok(Box::new(
4951                crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4952                    .with_resume_support(descriptor.runtime.capabilities.resume_session),
4953            ))
4954        }
4955        _ => Err(ServiceError::UnsupportedAction(format!(
4956            "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4957            descriptor.id.as_str(),
4958            connect.protocol
4959        ))),
4960    }
4961}
4962
4963/// The registry's connect-mode launch for this harness, honored only when the
4964/// caller supplied neither an explicit launch nor a base URL.
4965fn registry_connect_descriptor(
4966    params: &RuntimeBackendParams,
4967) -> Option<crate::HarnessSupportDescriptor> {
4968    if params.launch.is_some() || params.base_url.is_some() {
4969        return None;
4970    }
4971    harness_support_registry()
4972        .harnesses
4973        .into_iter()
4974        .find(|descriptor| descriptor.id == params.harness)
4975        .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4976}
4977
4978fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4979    std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4980        ServiceError::UnsupportedAction(
4981            "connect-mode launches need HOME to locate the harness config".into(),
4982        )
4983    })
4984}
4985
4986/// The doors that open a runtime: each spawns or joins a program and waits on
4987/// that program's protocol handshake before it can answer.
4988pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4989    "harness.v1.runtimes.start",
4990    "harness.v1.runtimes.resume",
4991    "harness.v1.runtimes.attach",
4992    "harness.v1.runtimes.attach_existing",
4993];
4994
4995/// How long a runtime gets to finish opening before its caller is answered an
4996/// error instead. A program that never speaks the protocol at all — the wrong
4997/// binary, a shim that prints usage and waits — never answers the handshake,
4998/// so the wait is unbounded without this.
4999pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
5000
5001/// How long a control call on an ALREADY-open runtime — send input, interrupt,
5002/// steer, respond, close — gets before its caller is answered an error
5003/// instead. A live runtime answers these in milliseconds; a wedged one never
5004/// answers at all, and `close` is exactly what a caller reaches for when it
5005/// suspects that.
5006pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
5007
5008/// The doors whose work happens entirely OUTSIDE this service's state once
5009/// its state has been read: probing harnesses, couriering a message into a
5010/// live session, and performing a conversation verb through a harness's own
5011/// CLI / HTTP / store door. Every one of them waits on a child process or a
5012/// network peer. See [`HarnessSessionService::detach`].
5013pub const DETACHED_METHODS: &[&str] = &[
5014    "harness.v1.harnesses.list",
5015    "harness.v1.harnesses.probe",
5016    "harness.v1.sessions.message",
5017    "harness.v1.sessions.new",
5018    "harness.v1.sessions.reset",
5019    "harness.v1.sessions.archive",
5020    "harness.v1.sessions.delete",
5021];
5022
5023/// How long a request moved off a transport's loop gets before its caller is
5024/// answered an error instead. Each of these already bounds its own inner
5025/// waits (a probe's handshake, the courier's run); this is the backstop for
5026/// the ones that do not — a harness CLI that never exits — so no caller waits
5027/// forever on a detached task no one is watching.
5028pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
5029
5030/// How long `sessions.discover` gets before its caller is answered an error
5031/// instead. Discovery reads each harness's own store, and a store on a cold
5032/// or unavailable mount answers at the filesystem's pace rather than its own.
5033///
5034/// Deliberately shorter than the clients' own request deadline (30s): the
5035/// server's answer names the store that did not answer, and it is only read
5036/// if it lands before the client stops listening.
5037pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
5038
5039/// Bound one control call on an open runtime by [`RUNTIME_CONTROL_DEADLINE`],
5040/// naming the method and the bound when it blows.
5041async fn within_control_deadline<F: std::future::Future>(
5042    method: &str,
5043    call: F,
5044) -> std::result::Result<F::Output, ServiceError> {
5045    tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
5046        .await
5047        .map_err(|_| {
5048            ServiceError::Operation(format!(
5049                "`{method}` gave up after {}s: the runtime did not answer",
5050                RUNTIME_CONTROL_DEADLINE.as_secs()
5051            ))
5052        })
5053}
5054
5055/// One [`RUNTIME_OPEN_METHODS`] request, parsed but not yet started. See
5056/// [`HarnessSessionService::runtime_open`] for why it exists apart from
5057/// [`HarnessSessionService::handle_async`].
5058pub struct RuntimeOpen {
5059    id: Value,
5060    method: String,
5061    params: Value,
5062}
5063
5064impl RuntimeOpen {
5065    /// Do the waiting: spawn or join the program and complete its handshake,
5066    /// bounded by [`RUNTIME_OPEN_DEADLINE`]. Touches no service state, so this
5067    /// runs on any task.
5068    pub async fn open(self) -> OpenedRuntime {
5069        let Self { id, method, params } = self;
5070        let outcome = open_runtime(&method, params).await;
5071        OpenedRuntime { id, outcome }
5072    }
5073}
5074
5075/// The result of [`RuntimeOpen::open`], ready for
5076/// [`HarnessSessionService::finish_runtime_open`].
5077pub struct OpenedRuntime {
5078    id: Value,
5079    outcome: std::result::Result<OpenRuntime, ServiceError>,
5080}
5081
5082/// One detached request: the half that reads this service's state already
5083/// done, and the half that waits not yet started. See
5084/// [`HarnessSessionService::detach`] and
5085/// [`HarnessSessionService::detach_runtime`].
5086pub struct DetachedCall {
5087    id: Value,
5088    method: String,
5089    work: std::result::Result<Work, ServiceError>,
5090}
5091
5092impl DetachedCall {
5093    /// Do the waiting and answer. Runs on any task: whatever this call needed
5094    /// from the service was taken before it left.
5095    pub async fn run(self) -> DetachedAnswer {
5096        let Self { id, method, work } = self;
5097        match work {
5098            // A call holding a runtime is already bounded by
5099            // RUNTIME_CONTROL_DEADLINE, and its future OWNS that connection:
5100            // a second timeout around it would drop the connection mid-call
5101            // and take down a runtime its caller still has.
5102            Ok(Work::Runtime(work)) => {
5103                let (result, returned) = work.run().await;
5104                DetachedAnswer {
5105                    response: service_response(id, result),
5106                    returned,
5107                }
5108            }
5109            Ok(Work::Free(work)) => {
5110                let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5111                    Ok(result) => result,
5112                    Err(_) => Err(ServiceError::Operation(format!(
5113                        "`{method}` gave up after {}s: the harness it waits on did not answer",
5114                        DETACHED_CALL_DEADLINE.as_secs()
5115                    ))),
5116                };
5117                DetachedAnswer {
5118                    response: service_response(id, result),
5119                    returned: None,
5120                }
5121            }
5122            Err(error) => DetachedAnswer {
5123                response: service_response(id, Err(error)),
5124                returned: None,
5125            },
5126        }
5127    }
5128}
5129
5130/// One detached call's complete answer, plus whatever it must hand back to
5131/// the service before that answer is written. See
5132/// [`HarnessSessionService::finish_detached`].
5133pub struct DetachedAnswer {
5134    response: Value,
5135    returned: Option<ReturnedRuntime>,
5136}
5137
5138impl DetachedAnswer {
5139    /// The caller's JSON-RPC response, for a transport that owns no service
5140    /// to give a borrowed connection back to.
5141    pub fn into_response(self) -> Value {
5142        self.response
5143    }
5144}
5145
5146/// A connection lent to a detached call, on its way back to the service that
5147/// owns it.
5148pub struct ReturnedRuntime {
5149    connection: String,
5150    runtime: Box<dyn RuntimeConnection>,
5151}
5152
5153/// The waiting half of one detached request: with nothing of the service's
5154/// in hand, or holding a connection the service lent out for the call.
5155enum Work {
5156    Free(DetachedWork),
5157    Runtime(RuntimeWork),
5158}
5159
5160/// The waiting half of one detached request that holds nothing of the
5161/// service's.
5162enum DetachedWork {
5163    /// Probe the selected harnesses: find their executables, ask each its
5164    /// version, and at `probe: handshake` start each one and complete its
5165    /// protocol handshake.
5166    Inventory(InventoryWork),
5167    /// Run the courier that delivers one message into a live session.
5168    Message(MessageSessionParams),
5169    /// Perform one conversation verb through the harness's own CLI, HTTP API,
5170    /// daemon socket, or supercode's own store.
5171    SessionMutation {
5172        verb: crate::SessionVerb,
5173        mutation: crate::SessionMutation,
5174    },
5175}
5176
5177impl DetachedWork {
5178    async fn run(self) -> std::result::Result<Value, ServiceError> {
5179        match self {
5180            Self::Inventory(work) => run_inventory(work).await,
5181            Self::Message(params) => {
5182                Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
5183            }
5184            Self::SessionMutation { verb, mutation } => {
5185                let outcome = run_session_mutation(verb, &mutation).await?;
5186                serde_json::to_value(outcome)
5187                    .map_err(|error| ServiceError::Operation(error.to_string()))
5188            }
5189        }
5190    }
5191}
5192
5193/// One detached call that holds a runtime connection for its whole run.
5194enum RuntimeWork {
5195    /// Tear down a runtime the service has already surrendered.
5196    Close {
5197        runtime: Box<dyn RuntimeConnection>,
5198        process_group: Option<u32>,
5199    },
5200    /// Type one live slash command through a borrowed connection, then give
5201    /// the connection back.
5202    LiveCommand {
5203        connection: String,
5204        runtime: Box<dyn RuntimeConnection>,
5205        verb: crate::SessionVerb,
5206        mutation: crate::SessionMutation,
5207        command: &'static str,
5208        session: String,
5209    },
5210}
5211
5212/// What one [`RuntimeWork`] answers with: the caller's result, and the
5213/// connection to give back when the call only borrowed one.
5214type RuntimeWorkAnswer = (
5215    std::result::Result<Value, ServiceError>,
5216    Option<ReturnedRuntime>,
5217);
5218
5219impl RuntimeWork {
5220    async fn run(self) -> RuntimeWorkAnswer {
5221        match self {
5222            Self::Close {
5223                runtime,
5224                process_group,
5225            } => (close_runtime(runtime, process_group).await, None),
5226            Self::LiveCommand {
5227                connection,
5228                mut runtime,
5229                verb,
5230                mutation,
5231                command,
5232                session,
5233            } => {
5234                let result =
5235                    type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5236                (
5237                    result,
5238                    Some(ReturnedRuntime {
5239                        connection,
5240                        runtime,
5241                    }),
5242                )
5243            }
5244        }
5245    }
5246}
5247
5248/// Tear down a runtime already out of the service, within
5249/// [`RUNTIME_CONTROL_DEADLINE`].
5250async fn close_runtime(
5251    mut runtime: Box<dyn RuntimeConnection>,
5252    process_group: Option<u32>,
5253) -> std::result::Result<Value, ServiceError> {
5254    match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5255        Ok(result) => {
5256            result.map_err(operation)?;
5257            Ok(json!({"closed": true}))
5258        }
5259        Err(deadline) => {
5260            // Dropping the handle is not enough: the process that stopped
5261            // answering is held by a task parked on it, so nothing here runs
5262            // its Drop. Signal the group the graceful path would have
5263            // signalled, then say so.
5264            let killed = kill_runtime_process_group(process_group);
5265            drop(runtime);
5266            Ok(json!({
5267                "closed": true,
5268                "killed": killed,
5269                "detail": error_message(deadline),
5270            }))
5271        }
5272    }
5273}
5274
5275/// The conversation a live `sessions.new` / `sessions.reset` acts on: the one
5276/// the request named, or the runtime's own session.
5277fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5278    mutation
5279        .session
5280        .clone()
5281        .filter(|value| !value.trim().is_empty())
5282        .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5283}
5284
5285/// Type one harness slash command into a live session through the very same
5286/// `send_input` path a human's message takes, within
5287/// [`RUNTIME_CONTROL_DEADLINE`].
5288async fn type_live_command(
5289    runtime: &mut dyn RuntimeConnection,
5290    verb: crate::SessionVerb,
5291    mutation: &crate::SessionMutation,
5292    command: &str,
5293    session: String,
5294) -> std::result::Result<Value, ServiceError> {
5295    within_control_deadline(
5296        &format!("sessions.{}", verb.as_str()),
5297        runtime.send_input(RuntimeInput {
5298            text: command.to_string(),
5299            image_urls: Vec::new(),
5300        }),
5301    )
5302    .await?
5303    .map_err(operation)?;
5304    let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5305        .map_err(session_control_error)?;
5306    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5307}
5308
5309/// A runtime that is up and whose handshake completed, with what the service
5310/// needs to take ownership of it.
5311enum OpenRuntime {
5312    /// supercode spawned this process, so it also hosts it: a frontend server,
5313    /// a live-runtime registration and a terminal launch of its own.
5314    Hosted {
5315        runtime: Box<dyn RuntimeConnection>,
5316        capabilities: crate::RuntimeCapabilities,
5317        workspace: PathBuf,
5318    },
5319    /// `attach_existing` joined a process supercode does not own. It is
5320    /// registered as a bare connection and hosts nothing.
5321    Joined { runtime: Box<dyn RuntimeConnection> },
5322}
5323
5324/// Open the runtime one [`RUNTIME_OPEN_METHODS`] request asks for, within
5325/// [`RUNTIME_OPEN_DEADLINE`]. The error a blown deadline answers names the
5326/// method and the bound, so a caller reads why it was cut loose instead of
5327/// waiting on a handshake that is never coming.
5328async fn open_runtime(
5329    method: &str,
5330    params: Value,
5331) -> std::result::Result<OpenRuntime, ServiceError> {
5332    match tokio::time::timeout(
5333        RUNTIME_OPEN_DEADLINE,
5334        open_runtime_unbounded(method, params),
5335    )
5336    .await
5337    {
5338        Ok(result) => result,
5339        Err(_) => Err(ServiceError::Operation(format!(
5340            "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5341            RUNTIME_OPEN_DEADLINE.as_secs()
5342        ))),
5343    }
5344}
5345
5346async fn open_runtime_unbounded(
5347    method: &str,
5348    params: Value,
5349) -> std::result::Result<OpenRuntime, ServiceError> {
5350    match method {
5351        "harness.v1.runtimes.start" => {
5352            let params = decode::<RuntimeStartParams>(params)?;
5353            let backend = runtime_backend(&params.backend)?;
5354            let capabilities = backend.capabilities();
5355            let workspace = params.cwd.clone();
5356            let runtime = backend
5357                .start(RuntimeStartRequest {
5358                    cwd: params.cwd,
5359                    launch: runtime_launch(&params.backend),
5360                    mcp_servers: params.mcp_servers,
5361                })
5362                .await
5363                .map_err(operation)?;
5364            Ok(OpenRuntime::Hosted {
5365                runtime,
5366                capabilities,
5367                workspace,
5368            })
5369        }
5370        "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5371            let params = decode::<RuntimeAttachParams>(params)?;
5372            let backend = runtime_backend(&params.backend)?;
5373            let capabilities = backend.capabilities();
5374            let workspace = params
5375                .cwd
5376                .clone()
5377                .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5378            let runtime = backend
5379                .attach(RuntimeAttachRequest {
5380                    runtime_id: params.runtime_id,
5381                    cwd: params.cwd,
5382                    launch: runtime_launch(&params.backend),
5383                    mcp_servers: params.mcp_servers,
5384                })
5385                .await
5386                .map_err(operation)?;
5387            Ok(OpenRuntime::Hosted {
5388                runtime,
5389                capabilities,
5390                workspace,
5391            })
5392        }
5393        "harness.v1.runtimes.attach_existing" => {
5394            let params = decode::<RuntimeAttachParams>(params)?;
5395            let backend: Box<dyn RuntimeBackend> = match params
5396                .backend
5397                .base_url
5398                .as_deref()
5399                .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5400            {
5401                Some(endpoint) => {
5402                    #[cfg(not(feature = "adapter-api"))]
5403                    {
5404                        let _ = endpoint;
5405                        return Err(ServiceError::UnsupportedAction(
5406                            "live HTTP attachment adapter is not compiled".into(),
5407                        ));
5408                    }
5409                    #[cfg(feature = "adapter-api")]
5410                    {
5411                        let workspace = params.cwd.clone().ok_or_else(|| {
5412                            ServiceError::InvalidParams(
5413                                "Supercode live attach requires the project cwd".into(),
5414                            )
5415                        })?;
5416                        let source = LiveRuntimeSource {
5417                            harness: params.backend.harness.as_str().to_string(),
5418                            session_id: params.runtime_id.clone(),
5419                            workspace,
5420                        };
5421                        let receipt = resolve_live_runtime(&endpoint, &source)
5422                            .map_err(|error| ServiceError::Operation(error.to_string()))?;
5423                        Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5424                    }
5425                }
5426                None => runtime_backend(&params.backend)?,
5427            };
5428            let capabilities = backend.capabilities();
5429            if !capabilities.attach_existing_process {
5430                return Err(ServiceError::Operation(format!(
5431                    "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5432                    backend.harness().as_str()
5433                )));
5434            }
5435            let runtime = backend
5436                .attach_existing(RuntimeAttachRequest {
5437                    runtime_id: params.runtime_id,
5438                    cwd: params.cwd,
5439                    launch: runtime_launch(&params.backend),
5440                    mcp_servers: params.mcp_servers,
5441                })
5442                .await
5443                .map_err(operation)?;
5444            Ok(OpenRuntime::Joined { runtime })
5445        }
5446        _ => Err(ServiceError::MethodNotFound),
5447    }
5448}
5449
5450/// Wrap one service outcome in its JSON-RPC 2.0 envelope.
5451fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5452    match result {
5453        Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5454        Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5455        Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5456        Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5457        Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5458        Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5459    }
5460}
5461
5462fn runtime_backend(
5463    params: &RuntimeBackendParams,
5464) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5465    if let Some(descriptor) = registry_connect_descriptor(params) {
5466        return open_connect_descriptor(&descriptor, &service_home()?);
5467    }
5468    if params.protocol.as_deref() == Some("acp") {
5469        let launch = params
5470            .launch
5471            .clone()
5472            .or_else(|| {
5473                harness_support_registry()
5474                    .harnesses
5475                    .into_iter()
5476                    .find(|harness| harness.id == params.harness)
5477                    .filter(|harness| {
5478                        harness.runtime.implementation == ImplementationKind::GenericProtocol
5479                            && harness.runtime.protocol.starts_with("acp")
5480                    })
5481                    .and_then(|harness| harness.runtime.default_launch)
5482            })
5483            .ok_or_else(|| {
5484                ServiceError::InvalidParams(
5485                    "an ACP runtime requires `launch` unless the harness has a registered default"
5486                        .into(),
5487                )
5488            })?;
5489        let resume_session = harness_support_registry()
5490            .harnesses
5491            .into_iter()
5492            .find(|harness| harness.id == params.harness)
5493            .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5494        return Ok(Box::new(
5495            AcpRuntimeBackend::new(params.harness.clone(), launch)
5496                .with_resume_support(resume_session),
5497        ));
5498    }
5499    let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5500        HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5501        HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5502        HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5503        HarnessId::OPENCODE => match &params.base_url {
5504            Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5505            None => Box::new(OpenCodeRuntimeBackend::new()),
5506        },
5507        harness => {
5508            let descriptor = harness_support_registry()
5509                .harnesses
5510                .into_iter()
5511                .find(|descriptor| descriptor.id.as_str() == harness)
5512                .filter(|descriptor| {
5513                    descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5514                        && descriptor.runtime.protocol.starts_with("acp")
5515                });
5516            let Some(descriptor) = descriptor else {
5517                return Err(ServiceError::InvalidParams(format!(
5518                    "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5519                )));
5520            };
5521            let resume = descriptor.runtime.capabilities.resume_session;
5522            Box::new(
5523                AcpRuntimeBackend::new(
5524                    descriptor.id,
5525                    descriptor
5526                        .runtime
5527                        .default_launch
5528                        .expect("generic ACP registry entry includes its launch"),
5529                )
5530                .with_resume_support(resume),
5531            )
5532        }
5533    };
5534    Ok(backend)
5535}
5536
5537fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5538    if let Some(launch) = &params.launch {
5539        return Some(launch.clone());
5540    }
5541    if !matches!(params.policy, RuntimePolicy::Yolo) {
5542        return None;
5543    }
5544    let launch = match params.harness.as_str() {
5545        HarnessId::GROK => RuntimeLaunch {
5546            program: "grok".into(),
5547            arguments: {
5548                let mut arguments: Vec<String> = Vec::new();
5549                if crate::support::self_sandbox_supported() {
5550                    arguments.extend(["--sandbox".into(), "workspace".into()]);
5551                }
5552                arguments.extend([
5553                    "--always-approve".into(),
5554                    "agent".into(),
5555                    "--no-leader".into(),
5556                    "stdio".into(),
5557                ]);
5558                arguments
5559            },
5560            env: BTreeMap::from([("GROK_AGENT_DASHBOARD".into(), "0".into())]),
5561        },
5562        HarnessId::CODEX => RuntimeLaunch {
5563            program: "codex".into(),
5564            arguments: vec![
5565                "--dangerously-bypass-approvals-and-sandbox".into(),
5566                "--dangerously-bypass-hook-trust".into(),
5567                "app-server".into(),
5568            ],
5569            env: BTreeMap::new(),
5570        },
5571        HarnessId::CLAUDE_CODE => RuntimeLaunch {
5572            program: "claude".into(),
5573            arguments: vec![
5574                "--dangerously-skip-permissions".into(),
5575                "--print".into(),
5576                "--input-format".into(),
5577                "stream-json".into(),
5578                "--output-format".into(),
5579                "stream-json".into(),
5580                "--verbose".into(),
5581            ],
5582            env: BTreeMap::new(),
5583        },
5584        HarnessId::PI => RuntimeLaunch {
5585            program: "pi".into(),
5586            arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5587            env: BTreeMap::new(),
5588        },
5589        HarnessId::OPENCODE => RuntimeLaunch {
5590            program: "opencode".into(),
5591            arguments: vec!["serve".into()],
5592            env: BTreeMap::new(),
5593        },
5594        HarnessId::GEMINI => RuntimeLaunch {
5595            program: "gemini".into(),
5596            arguments: vec!["--acp".into(), "--yolo".into()],
5597            env: BTreeMap::new(),
5598        },
5599        HarnessId::GOOSE => RuntimeLaunch {
5600            program: "goose".into(),
5601            arguments: vec!["acp".into()],
5602            env: BTreeMap::new(),
5603        },
5604        HarnessId::SUPERCODE => RuntimeLaunch {
5605            program: "supercode".into(),
5606            arguments: vec!["acp".into(), "--dangerous".into()],
5607            env: BTreeMap::new(),
5608        },
5609        _ => return None,
5610    };
5611    Some(launch)
5612}
5613
5614/// Disposable harness state for a no-prompt readiness probe. Merely opening
5615/// several stock CLIs writes a session header or migrates configuration, so a
5616/// handshake must never point at the user's real home. Authentication files
5617/// are copied into the private temporary home; all writes disappear with the
5618/// guard after the connection closes.
5619struct IsolatedProbeHome {
5620    launch: RuntimeLaunch,
5621    root: PathBuf,
5622}
5623
5624impl IsolatedProbeHome {
5625    fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5626        let root = std::env::temp_dir().join(format!(
5627            "supercode-harness-probe-{harness}-{}",
5628            generated_session_id()
5629        ));
5630        std::fs::create_dir_all(&root)?;
5631        set_private_dir_permissions(&root)?;
5632
5633        if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
5634            for relative in probe_auth_files(harness) {
5635                copy_probe_file(&source_home, &root, relative)?;
5636            }
5637        }
5638        configure_isolated_probe_auth(harness, &root)?;
5639
5640        let root_text = root.to_string_lossy().into_owned();
5641        for (key, value) in [
5642            ("HOME", root_text.clone()),
5643            (
5644                "XDG_CACHE_HOME",
5645                root.join(".cache").to_string_lossy().into_owned(),
5646            ),
5647            (
5648                "XDG_CONFIG_HOME",
5649                root.join(".config").to_string_lossy().into_owned(),
5650            ),
5651            (
5652                "XDG_DATA_HOME",
5653                root.join(".local/share").to_string_lossy().into_owned(),
5654            ),
5655        ] {
5656            launch.env.insert(key.into(), value);
5657        }
5658        let scoped = match harness {
5659            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5660            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5661            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5662            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5663            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5664            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5665            _ => None,
5666        };
5667        if let Some((key, value)) = scoped {
5668            launch
5669                .env
5670                .insert(key.into(), value.to_string_lossy().into_owned());
5671        }
5672        Ok(Self { launch, root })
5673    }
5674
5675    fn cleanup(&self) -> std::io::Result<()> {
5676        match std::fs::remove_dir_all(&self.root) {
5677            Ok(()) => Ok(()),
5678            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5679            Err(error) => Err(error),
5680        }
5681    }
5682}
5683
5684impl Drop for IsolatedProbeHome {
5685    fn drop(&mut self) {
5686        let _ = self.cleanup();
5687    }
5688}
5689
5690fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5691    match harness {
5692        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5693        // The gateway endpoint + token live in openclaw's own config; without
5694        // it the isolated probe dials the default endpoint unauthenticated
5695        // (PARITY-24 finding 2026-08-31).
5696        HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5697        HarnessId::CODEX => &[".codex/auth.json"],
5698        HarnessId::GEMINI => &[
5699            ".gemini/google_accounts.json",
5700            ".gemini/oauth_creds.json",
5701            ".gemini/settings.json",
5702        ],
5703        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5704        HarnessId::OPENCODE => &[
5705            ".config/opencode/auth.json",
5706            ".local/share/opencode/auth.json",
5707        ],
5708        HarnessId::PI => &[".pi/agent/auth.json"],
5709        // Hermes keeps its provider selection in config.yaml, its OAuth
5710        // credential pool in auth.json, and API keys in .env; without them
5711        // the isolated probe sees "No LLM provider configured" for a
5712        // hermes that answers fine from the user's real home.
5713        HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5714        HarnessId::SUPERCODE => &[
5715            ".config/supercode/config.toml",
5716            ".config/supercode/credentials.toml",
5717        ],
5718        _ => &[],
5719    }
5720}
5721
5722fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5723    let source = source_home.join(relative);
5724    if !source.is_file() {
5725        return Ok(());
5726    }
5727    let destination = probe_home.join(relative);
5728    if let Some(parent) = destination.parent() {
5729        std::fs::create_dir_all(parent)?;
5730        set_private_dir_permissions(parent)?;
5731    }
5732    std::fs::copy(source, &destination)?;
5733    set_private_file_permissions(&destination)
5734}
5735
5736fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5737    if harness != HarnessId::GEMINI {
5738        return Ok(());
5739    }
5740    let oauth = probe_home.join(".gemini/oauth_creds.json");
5741    if !oauth.is_file() {
5742        return Ok(());
5743    }
5744    let settings_path = probe_home.join(".gemini/settings.json");
5745    let mut settings = std::fs::read_to_string(&settings_path)
5746        .ok()
5747        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5748        .unwrap_or_else(|| json!({}));
5749    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5750    std::fs::write(
5751        &settings_path,
5752        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5753    )?;
5754    set_private_file_permissions(&settings_path)
5755}
5756
5757#[cfg(unix)]
5758fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5759    use std::os::unix::fs::PermissionsExt;
5760    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5761}
5762
5763#[cfg(not(unix))]
5764fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5765    Ok(())
5766}
5767
5768#[cfg(unix)]
5769fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5770    use std::os::unix::fs::PermissionsExt;
5771    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5772}
5773
5774#[cfg(not(unix))]
5775fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5776    Ok(())
5777}
5778
5779fn find_executable(program: &str) -> Option<PathBuf> {
5780    let candidate = PathBuf::from(program);
5781    if candidate.components().count() > 1 {
5782        return candidate.is_file().then_some(candidate);
5783    }
5784    let path = std::env::var_os("PATH")?;
5785    for directory in std::env::split_paths(&path) {
5786        let candidate = directory.join(program);
5787        if candidate.is_file() {
5788            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5789        }
5790        #[cfg(windows)]
5791        {
5792            for extension in ["exe", "cmd", "bat"] {
5793                let candidate = directory.join(format!("{program}.{extension}"));
5794                if candidate.is_file() {
5795                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5796                }
5797            }
5798        }
5799    }
5800    None
5801}
5802
5803async fn executable_version(executable: &Path) -> Option<String> {
5804    let mut command = tokio::process::Command::new(executable);
5805    command
5806        .arg("--version")
5807        .stdin(std::process::Stdio::null())
5808        .stdout(std::process::Stdio::piped())
5809        .stderr(std::process::Stdio::piped())
5810        .kill_on_drop(true);
5811    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5812        .await
5813        .ok()?
5814        .ok()?;
5815    let stdout = String::from_utf8_lossy(&output.stdout);
5816    let stderr = String::from_utf8_lossy(&output.stderr);
5817    stdout
5818        .lines()
5819        .chain(stderr.lines())
5820        .map(str::trim)
5821        .find(|line| !line.is_empty())
5822        .map(|line| truncate_text(line, 200))
5823}
5824
5825pub(crate) fn auth_evidence(harness: &str) -> bool {
5826    let env_names: &[&str] = match harness {
5827        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5828        HarnessId::CODEX => &["OPENAI_API_KEY"],
5829        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5830        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5831        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5832        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5833        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5834        _ => &[],
5835    };
5836    if env_names
5837        .iter()
5838        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5839    {
5840        return true;
5841    }
5842    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5843        return false;
5844    };
5845    let files: Vec<PathBuf> = match harness {
5846        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5847        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5848        HarnessId::OPENCODE => vec![
5849            home.join(".local/share/opencode/auth.json"),
5850            home.join(".config/opencode/auth.json"),
5851        ],
5852        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5853        HarnessId::GROK => vec![home.join(".grok/auth.json")],
5854        HarnessId::GEMINI => vec![
5855            home.join(".gemini/oauth_creds.json"),
5856            home.join(".gemini/google_accounts.json"),
5857        ],
5858        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5859        HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5860        _ => Vec::new(),
5861    };
5862    if files.into_iter().any(|path| {
5863        std::fs::metadata(path)
5864            .map(|metadata| metadata.is_file() && metadata.len() > 2)
5865            .unwrap_or(false)
5866    }) {
5867        return true;
5868    }
5869    // macOS keeps Claude Code's OAuth login in the Keychain, so
5870    // `.claude/.credentials.json` never exists there and the file probe above
5871    // reports a signed-in install as unauthenticated forever. A completed
5872    // login also writes an `oauthAccount` record into `~/.claude.json` on
5873    // every platform — file-based, prompt-free evidence (querying the
5874    // Keychain itself from an unsigned daemon can raise a UI prompt).
5875    if harness == HarnessId::CLAUDE_CODE {
5876        return std::fs::read_to_string(home.join(".claude.json"))
5877            .map(|text| text.contains("\"oauthAccount\""))
5878            .unwrap_or(false);
5879    }
5880    false
5881}
5882
5883fn looks_like_auth_error(message: &str) -> bool {
5884    let message = message.to_ascii_lowercase();
5885    [
5886        "auth",
5887        "login",
5888        "sign in",
5889        "sign-in",
5890        "credential",
5891        "unauthorized",
5892        "forbidden",
5893        "token",
5894    ]
5895    .iter()
5896    .any(|needle| message.contains(needle))
5897}
5898
5899fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5900    crate::RuntimeCapabilities {
5901        start_session: false,
5902        resume_session: false,
5903        attach_existing_process: false,
5904        send_input: false,
5905        stream_events: false,
5906        interrupt: false,
5907        steer: false,
5908        respond_to_requests: false,
5909    }
5910}
5911
5912fn truncate_text(text: &str, max_chars: usize) -> String {
5913    let mut chars = text.chars();
5914    let truncated = chars.by_ref().take(max_chars).collect::<String>();
5915    if chars.next().is_some() {
5916        format!("{truncated}…")
5917    } else {
5918        truncated
5919    }
5920}
5921
5922/// The process group a runtime's own handle names, when it names one.
5923///
5924/// Every adapter that spawns a local process spawns it as its own group
5925/// leader (`Command::process_group(0)`), so the endpoint's pid IS the group
5926/// id. A runtime reached over HTTP, or one supercode joined rather than
5927/// spawned, names no group here and is left alone.
5928fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5929    match &handle.endpoint {
5930        crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5931        crate::RuntimeEndpoint::Http { .. } => None,
5932    }
5933}
5934
5935/// SIGKILL a wedged runtime's whole process group, reporting whether there
5936/// was one to signal. This is the same group teardown a graceful `close`
5937/// performs; it runs here only when the graceful path blew its deadline,
5938/// because the task parked on the unanswered call still owns the process
5939/// handle and so no `Drop` of ours can reach it.
5940fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5941    match process_group {
5942        #[cfg(unix)]
5943        Some(pid) => {
5944            crate::lsp::kill_process_group(pid);
5945            true
5946        }
5947        #[cfg(not(unix))]
5948        Some(_) => false,
5949        None => false,
5950    }
5951}
5952
5953fn error_message(error: ServiceError) -> String {
5954    match error {
5955        ServiceError::InvalidParams(message)
5956        | ServiceError::Operation(message)
5957        | ServiceError::UnsupportedAction(message) => message,
5958        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5959        ServiceError::Sdk(error) => error.to_string(),
5960    }
5961}
5962
5963#[derive(Debug)]
5964enum ServiceError {
5965    InvalidParams(String),
5966    MethodNotFound,
5967    UnsupportedAction(String),
5968    Operation(String),
5969    Sdk(SdkError),
5970}
5971
5972fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5973    match error {
5974        ServiceError::InvalidParams(message) => {
5975            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5976        }
5977        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5978            SdkError::unsupported(operation)
5979        }
5980        ServiceError::Operation(message) => {
5981            let code = if message.contains("already in progress") {
5982                SdkErrorCode::Busy
5983            } else if message.contains("not supported by this runtime") {
5984                SdkErrorCode::UnsupportedAction
5985            } else if message.contains("unknown runtime connection") {
5986                SdkErrorCode::NotFound
5987            } else {
5988                SdkErrorCode::Execution
5989            };
5990            SdkError::new(code, operation, message)
5991        }
5992        ServiceError::Sdk(error) => error,
5993    }
5994}
5995
5996fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5997    let error_code = error.code();
5998    let code = match error_code {
5999        SdkErrorCode::Unauthenticated => -32030,
6000        SdkErrorCode::Unauthorized => -32031,
6001        SdkErrorCode::ControllerRequired => -32032,
6002        SdkErrorCode::LeaseExpired => -32033,
6003        SdkErrorCode::InvalidArgument => -32602,
6004        SdkErrorCode::NotFound => -32004,
6005        SdkErrorCode::Busy => -32000,
6006        SdkErrorCode::UnsupportedAction => -32020,
6007        SdkErrorCode::Execution => -32002,
6008        SdkErrorCode::Transport => -32003,
6009    };
6010    json!({
6011        "jsonrpc": "2.0",
6012        "id": id,
6013        "error": {
6014            "code": code,
6015            "name": error_code,
6016            "operation": error.operation(),
6017            "message": error.to_string(),
6018        },
6019    })
6020}
6021
6022fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
6023    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
6024}
6025
6026fn operation(error: impl Into<crate::Error>) -> ServiceError {
6027    let error = error.into();
6028    match error {
6029        crate::Error::Sdk(error) => ServiceError::Sdk(error),
6030        error => ServiceError::Operation(error.to_string()),
6031    }
6032}
6033
6034/// ORCH-12 `harness.v1.memory.show|search` params. `homes` is the same
6035/// storage-root override every read-only method accepts, so a caller can
6036/// point the read at a fixture home without touching the real ones.
6037#[derive(Debug, Clone, Deserialize, Default)]
6038#[serde(default)]
6039struct MemoryRequest {
6040    /// Harness whose store is read. Required.
6041    harness: Option<String>,
6042    /// The needle, required by `search`.
6043    query: Option<String>,
6044    /// Hermes profile, OpenClaw agent, or Claude Code project.
6045    profile: Option<String>,
6046    /// Claude Code session id selecting a project store (`show` only).
6047    session: Option<String>,
6048    /// Include each document's whole text (`show` only).
6049    full: bool,
6050    /// Treat `query` as a regular expression (`search` only).
6051    regex: bool,
6052    /// Working tree whose project store is read.
6053    cwd: Option<std::path::PathBuf>,
6054    /// Storage roots to read.
6055    homes: crate::HarnessHomes,
6056}
6057
6058/// Read the memory noun. A harness with no memory store fails with
6059/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6060fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6061    let request = decode::<MemoryRequest>(params)?;
6062    let harness = request
6063        .harness
6064        .clone()
6065        .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6066    let to_service = |error: crate::memory::MemoryError| match error {
6067        crate::memory::MemoryError::UnsupportedHarness { .. }
6068        | crate::memory::MemoryError::SessionNotScoped { .. } => {
6069            ServiceError::UnsupportedAction(error.to_string())
6070        }
6071        other => ServiceError::InvalidParams(other.to_string()),
6072    };
6073    match method {
6074        "harness.v1.memory.show" => {
6075            let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6076                harness,
6077                profile: request.profile,
6078                session: request.session,
6079                full: request.full,
6080                cwd: request.cwd,
6081                homes: request.homes,
6082            })
6083            .map_err(to_service)?;
6084            Ok(json!({
6085                "schema": crate::memory::MEMORY_SCHEMA,
6086                "documents": documents,
6087            }))
6088        }
6089        "harness.v1.memory.search" => {
6090            let query = request
6091                .query
6092                .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6093            let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6094                harness,
6095                query,
6096                profile: request.profile,
6097                regex: request.regex,
6098                cwd: request.cwd,
6099                homes: request.homes,
6100            })
6101            .map_err(to_service)?;
6102            Ok(json!({
6103                "schema": crate::memory::MEMORY_SCHEMA,
6104                "matches": matches,
6105            }))
6106        }
6107        _ => Err(ServiceError::MethodNotFound),
6108    }
6109}
6110
6111/// ORCH-10 `harness.v1.profiles.list|get` params. `homes` is the same
6112/// storage-root override every read-only method accepts, so a caller can
6113/// point the read at a fixture home without touching the real ones.
6114#[derive(Debug, Clone, Deserialize)]
6115#[serde(default)]
6116struct ProfilesQuery {
6117    /// Restrict the listing to one harness. `get` requires it.
6118    harness: Option<String>,
6119    /// Profile name, required by `get`.
6120    name: Option<String>,
6121    /// Storage roots to read.
6122    homes: crate::HarnessHomes,
6123}
6124
6125impl Default for ProfilesQuery {
6126    fn default() -> Self {
6127        Self {
6128            harness: None,
6129            name: None,
6130            homes: crate::HarnessHomes::default(),
6131        }
6132    }
6133}
6134
6135/// Read the profile noun. A harness with no profile concept fails with
6136/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6137fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6138    let query = decode::<ProfilesQuery>(params)?;
6139    let to_service = |error: crate::profiles::ProfileError| match error {
6140        crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6141            ServiceError::UnsupportedAction(error.to_string())
6142        }
6143        crate::profiles::ProfileError::NotFound { .. } => {
6144            ServiceError::InvalidParams(error.to_string())
6145        }
6146    };
6147    match method {
6148        "harness.v1.profiles.list" => {
6149            let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6150                .map_err(to_service)?;
6151            Ok(json!({
6152                "schema": crate::profiles::PROFILES_SCHEMA,
6153                "profiles": profiles,
6154            }))
6155        }
6156        "harness.v1.profiles.get" => {
6157            let harness = query
6158                .harness
6159                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6160            let name = query
6161                .name
6162                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6163            let profile =
6164                crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6165            Ok(json!({
6166                "schema": crate::profiles::PROFILES_SCHEMA,
6167                "profile": profile,
6168            }))
6169        }
6170        _ => Err(ServiceError::MethodNotFound),
6171    }
6172}
6173
6174/// ORCH-14 `harness.v1.channels.list|status` params, the same storage-root
6175/// override every read-only method accepts so a caller can point the read at
6176/// a fixture home without touching the real ones.
6177#[derive(Debug, Clone, Deserialize)]
6178#[serde(default)]
6179struct ChannelsQuery {
6180    /// Restrict the listing to one harness. `status` requires it.
6181    harness: Option<String>,
6182    /// Channel name, required by `status`.
6183    name: Option<String>,
6184    /// Storage roots to read.
6185    homes: crate::HarnessHomes,
6186}
6187
6188impl Default for ChannelsQuery {
6189    fn default() -> Self {
6190        Self {
6191            harness: None,
6192            name: None,
6193            homes: crate::HarnessHomes::default(),
6194        }
6195    }
6196}
6197
6198/// Read the channel noun. A harness with no channel concept fails with
6199/// `UnsupportedAction` (RPC `-32020`), never an empty list. No row carries a
6200/// token, key or secret — see `crate::channels` "Secrecy".
6201#[derive(Debug, Clone, Deserialize)]
6202#[serde(default)]
6203struct RoutesQuery {
6204    harness: Option<String>,
6205    /// Restrict to routes targeting one profile / agent.
6206    profile: Option<String>,
6207    homes: crate::HarnessHomes,
6208}
6209
6210impl Default for RoutesQuery {
6211    fn default() -> Self {
6212        Self {
6213            harness: None,
6214            profile: None,
6215            homes: crate::HarnessHomes::default(),
6216        }
6217    }
6218}
6219
6220#[derive(Debug, Clone, Deserialize)]
6221#[serde(default)]
6222struct TriggersQuery {
6223    harness: Option<String>,
6224    homes: crate::HarnessHomes,
6225}
6226
6227impl Default for TriggersQuery {
6228    fn default() -> Self {
6229        Self {
6230            harness: None,
6231            homes: crate::HarnessHomes::default(),
6232        }
6233    }
6234}
6235
6236fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6237    let query = decode::<TriggersQuery>(params)?;
6238    let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6239        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6240    Ok(json!({
6241        "schema": crate::triggers::TRIGGERS_SCHEMA,
6242        "triggers": triggers,
6243    }))
6244}
6245
6246fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6247    let query = decode::<RoutesQuery>(params)?;
6248    let routes = crate::routes::list_routes(
6249        &query.homes,
6250        query.harness.as_deref(),
6251        query.profile.as_deref(),
6252    )
6253    .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6254    Ok(json!({
6255        "schema": crate::routes::ROUTES_SCHEMA,
6256        "routes": routes,
6257    }))
6258}
6259
6260fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6261    let query = decode::<ChannelsQuery>(params)?;
6262    let to_service = |error: crate::channels::ChannelError| match error {
6263        crate::channels::ChannelError::UnsupportedHarness { .. } => {
6264            ServiceError::UnsupportedAction(error.to_string())
6265        }
6266        crate::channels::ChannelError::NotFound { .. } => {
6267            ServiceError::InvalidParams(error.to_string())
6268        }
6269    };
6270    match method {
6271        "harness.v1.channels.list" => {
6272            let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6273                .map_err(to_service)?;
6274            Ok(json!({
6275                "schema": crate::channels::CHANNELS_SCHEMA,
6276                "channels": channels,
6277            }))
6278        }
6279        "harness.v1.channels.status" => {
6280            let harness = query
6281                .harness
6282                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6283            let name = query
6284                .name
6285                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6286            let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6287                .map_err(to_service)?;
6288            Ok(json!({
6289                "schema": crate::channels::CHANNELS_SCHEMA,
6290                "channel": channel,
6291            }))
6292        }
6293        _ => Err(ServiceError::MethodNotFound),
6294    }
6295}
6296
6297fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6298    json!({
6299        "jsonrpc": "2.0",
6300        "id": id,
6301        "error": {"code": code, "message": message},
6302    })
6303}
6304
6305#[cfg(test)]
6306mod tests {
6307    use super::*;
6308    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6309    use async_trait::async_trait;
6310    use std::io::Write;
6311    use std::path::PathBuf;
6312    use std::time::Instant;
6313
6314    #[test]
6315    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6316        let descriptor = SessionDescriptor {
6317            locator: SessionLocator {
6318                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6319                session_id: "live-session".into(),
6320                storage: StorageLocator::File {
6321                    path: PathBuf::from("/tmp/live-session.jsonl"),
6322                },
6323            },
6324            cwd: Some(PathBuf::from("/project")),
6325            title: None,
6326            preview_candidates: Vec::new(),
6327            latest_message_candidates: Vec::new(),
6328            updated_at_ms: Some(1),
6329            message_count: None,
6330            model: None,
6331            parent_session_id: None,
6332            child_session_count: 0,
6333            nouns: Default::default(),
6334        };
6335        let peer = crate::claude_peer::ClaudePeerSession {
6336            pid: 42,
6337            session_id: "live-session".into(),
6338            cwd: Some(PathBuf::from("/project")),
6339            name: "peer".into(),
6340            socket_path: PathBuf::from("/tmp/peer.sock"),
6341            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6342            updated_at_ms: Some(1),
6343            version: Some("test".into()),
6344        };
6345
6346        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6347        assert!(value["live_endpoint"]
6348            .as_str()
6349            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6350    }
6351
6352    struct EndingRuntime {
6353        handle: RuntimeHandle,
6354        event: Option<HarnessEvent>,
6355        close_failures: usize,
6356    }
6357
6358    #[async_trait]
6359    impl RuntimeConnection for EndingRuntime {
6360        fn handle(&self) -> &RuntimeHandle {
6361            &self.handle
6362        }
6363
6364        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6365            unreachable!("ending runtime does not accept input")
6366        }
6367
6368        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6369            Ok(self.event.take())
6370        }
6371
6372        async fn interrupt(&mut self) -> crate::Result<()> {
6373            Ok(())
6374        }
6375
6376        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6377            Ok(())
6378        }
6379
6380        async fn close(&mut self) -> crate::Result<()> {
6381            if self.close_failures > 0 {
6382                self.close_failures -= 1;
6383                return Err(crate::Error::Other(
6384                    "cleanup temporarily unavailable".into(),
6385                ));
6386            }
6387            Ok(())
6388        }
6389    }
6390
6391    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6392        Box::new(EndingRuntime {
6393            handle: RuntimeHandle {
6394                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6395                runtime_id: "ending-session".into(),
6396                endpoint: RuntimeEndpoint::LocalProcess {
6397                    pid: None,
6398                    command: vec!["ending-runtime".into()],
6399                    protocol: "test".into(),
6400                },
6401            },
6402            event,
6403            close_failures: 0,
6404        })
6405    }
6406
6407    #[tokio::test]
6408    async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6409        let mut service = HarnessSessionService::new();
6410        let handle = ending_runtime(None).handle().clone();
6411        let runtime_id = handle.runtime_id.clone();
6412        let opened = service
6413            .insert_runtime(Box::new(EndingRuntime {
6414                handle,
6415                event: None,
6416                close_failures: 1,
6417            }))
6418            .unwrap();
6419        let connection = opened["connection"].as_str().unwrap().to_string();
6420        service.terminal_launches.insert(
6421            connection.clone(),
6422            StructuredLaunch {
6423                cwd: PathBuf::from("/fixture"),
6424                program: "fixture".into(),
6425                arguments: Vec::new(),
6426                env: BTreeMap::new(),
6427            },
6428        );
6429        let first = service
6430            .handle_async(request(
6431                1,
6432                "harness.v1.runtimes.close",
6433                json!({"connection": connection}),
6434            ))
6435            .await;
6436        // The harness's own teardown failed and the caller is told so...
6437        assert!(first.get("error").is_some(), "{first}");
6438        // ...but the connection is gone all the same. A connection whose close
6439        // cannot complete is exactly the one that must not stay registered:
6440        // holding it would answer every later call on this node with a turn
6441        // that is never going to end.
6442        assert!(!service.runtimes.contains_key(&connection));
6443        assert!(!service.terminal_launches.contains_key(&connection));
6444        assert!(!service.runtime_sequences.contains_key(&runtime_id));
6445        let again = service
6446            .handle_async(request(
6447                2,
6448                "harness.v1.runtimes.close",
6449                json!({"connection": connection}),
6450            ))
6451            .await;
6452        assert_eq!(again["error"]["code"], -32602, "{again}");
6453    }
6454
6455    fn request(id: u64, method: &str, params: Value) -> Value {
6456        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6457    }
6458
6459    // ---- ORCH-6: conversation nouns on `sessions.*` ----------------------
6460
6461    fn hermes_store() -> PathBuf {
6462        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6463    }
6464
6465    /// The discovery response for the Hermes fixture home, with the one
6466    /// machine-specific value (the absolute store path) replaced so the exact
6467    /// same JSON can be committed and replayed by the UI story.
6468    fn hermes_discovery(params: Value) -> Value {
6469        let mut response =
6470            HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6471        let store = hermes_store().display().to_string();
6472        for session in response["result"]["sessions"]
6473            .as_array_mut()
6474            .expect("sessions array")
6475        {
6476            if session["locator"]["storage"]["path"] == json!(store) {
6477                session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6478            }
6479            // `activity` reports a wall-clock observation instant, not a fact
6480            // about the session; it would make this response differ on every
6481            // call. The nouns under test are all session facts.
6482            session.as_object_mut().unwrap().remove("activity");
6483        }
6484        response["result"].take()
6485    }
6486
6487    fn hermes_query() -> Value {
6488        json!({
6489            "harnesses": ["hermes"],
6490            "homes": {"hermes": hermes_store()},
6491        })
6492    }
6493
6494    fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6495        result["sessions"]
6496            .as_array()
6497            .expect("sessions array")
6498            .iter()
6499            .find(|session| session["locator"]["session_id"] == json!(id))
6500            .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6501    }
6502
6503    #[test]
6504    fn orch6_discover_rows_carry_the_conversation_nouns() {
6505        let result = hermes_discovery(hermes_query());
6506
6507        // A Telegram DM: reached on a channel, no repo — the workspace IS the
6508        // channel (D2 precedence), and `main` is not a profile.
6509        let dm = row(&result, "tg-dm-1");
6510        assert_eq!(dm["trigger"], json!("channel"));
6511        assert_eq!(dm["surface"]["platform"], json!("telegram"));
6512        assert_eq!(dm["surface"]["kind"], json!("dm"));
6513        assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6514        assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6515        assert_eq!(
6516            dm["workspace"],
6517            json!({"kind": "channel", "value": "telegram:123456"})
6518        );
6519        assert!(dm.get("profile").is_none(), "{dm:#}");
6520
6521        // A cron fire: recurring, with the job recovered from the minted id.
6522        let fire = row(&result, "cron_job42_20260902_120000");
6523        assert_eq!(fire["trigger"], json!("cron"));
6524        assert_eq!(
6525            fire["recurrence"],
6526            json!({"job_id": "job42", "kind": "cron"})
6527        );
6528        assert_eq!(fire["workspace"]["kind"], json!("repo"));
6529
6530        // A profiled group session with a pending handoff: repo workspace
6531        // wins over the channel, and the chat stays on the surface key.
6532        let coder = row(&result, "tg-coder-1");
6533        assert_eq!(coder["trigger"], json!("channel"));
6534        assert_eq!(coder["profile"], json!("coder"));
6535        assert_eq!(coder["surface"]["thread_id"], json!("55"));
6536        assert_eq!(
6537            coder["surface"]["key"],
6538            json!("agent:coder:telegram:group:-100777:55")
6539        );
6540        assert_eq!(
6541            coder["workspace"],
6542            json!({"kind": "repo", "value": "/workspace/project"})
6543        );
6544        assert_eq!(
6545            coder["cross_surface"],
6546            json!({"state": "pending", "platform": "discord"})
6547        );
6548
6549        // A plain ACP session stays human-triggered with no surface at all.
6550        let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6551        assert_eq!(acp["trigger"], json!("human"));
6552        assert!(acp.get("surface").is_none(), "{acp:#}");
6553        assert_eq!(acp["workspace"], json!({"kind": "none"}));
6554    }
6555
6556    #[test]
6557    fn orch6_discover_filters_by_harness_and_profile() {
6558        let mut params = hermes_query();
6559        params["profile"] = json!("coder");
6560        let result = hermes_discovery(params);
6561        let ids: Vec<&str> = result["sessions"]
6562            .as_array()
6563            .expect("sessions array")
6564            .iter()
6565            .map(|session| session["locator"]["session_id"].as_str().unwrap())
6566            .collect();
6567        assert_eq!(ids, vec!["tg-coder-1"]);
6568
6569        // A profile no session is routed through returns nothing rather than
6570        // silently ignoring the filter.
6571        let mut missing = hermes_query();
6572        missing["profile"] = json!("nobody");
6573        assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6574
6575        // The harness filter is `harnesses`; an id no harness answers to is
6576        // an empty page, never every store on the box.
6577        let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6578        assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6579    }
6580
6581    #[test]
6582    fn orch6_load_reports_the_same_nouns_as_discovery() {
6583        let mut service = HarnessSessionService::new();
6584        let loaded = service.handle(request(
6585            1,
6586            "harness.v1.sessions.load",
6587            json!({"locator": {
6588                "harness": "hermes",
6589                "session_id": "tg-coder-1",
6590                "storage": {"kind": "file", "path": hermes_store()},
6591            }}),
6592        ));
6593        let session = &loaded["result"]["session"];
6594        let discovered = hermes_discovery(hermes_query());
6595        let row = row(&discovered, "tg-coder-1");
6596        for noun in [
6597            "trigger",
6598            "surface",
6599            "profile",
6600            "recurrence",
6601            "cross_surface",
6602            "workspace",
6603        ] {
6604            assert_eq!(
6605                session[noun],
6606                row.get(noun).cloned().unwrap_or(Value::Null),
6607                "`{noun}` disagrees between sessions.load and sessions.discover"
6608            );
6609        }
6610    }
6611
6612    /// ORCH-10: the fixture homes, as the RPC's `homes` override. Hermes's
6613    /// home is named by its `state.db`; OpenClaw's is the state directory.
6614    fn profile_fixture_homes() -> Value {
6615        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6616        json!({
6617            "hermes": fixtures.join("hermes_home/state.db"),
6618            "openclaw": fixtures.join("openclaw_home"),
6619        })
6620    }
6621
6622    fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6623        response["result"]["profiles"]
6624            .as_array()
6625            .unwrap_or_else(|| panic!("no profiles array in {response}"))
6626            .iter()
6627            .find(|row| row["harness"] == harness && row["name"] == name)
6628            .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6629    }
6630
6631    /// dev/01: every source answers in one row shape, over the committed
6632    /// fixture homes — the Hermes profile directory and its `state.db`
6633    /// partition, the OpenClaw agent directories and `openclaw.json`, and
6634    /// supercode's own presets.
6635    #[test]
6636    fn profiles_list_reads_every_source_uniformly() {
6637        let mut service = HarnessSessionService::new();
6638        let response = service.handle(request(
6639            1,
6640            "harness.v1.profiles.list",
6641            json!({"homes": profile_fixture_homes()}),
6642        ));
6643        assert_eq!(
6644            response["result"]["schema"],
6645            crate::profiles::PROFILES_SCHEMA
6646        );
6647
6648        let default = profile_row(&response, "hermes", "default");
6649        assert_eq!(default["kind"], "hermes_profile");
6650        assert_eq!(default["default"], true);
6651        assert_eq!(default["routes"], 0);
6652        assert_eq!(default["sessions"], 11);
6653        assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6654
6655        let coder = profile_row(&response, "hermes", "coder");
6656        assert_eq!(coder["kind"], "hermes_profile");
6657        assert_eq!(coder["default"], false);
6658        assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6659        assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6660        assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6661        assert!(coder["home"]
6662            .as_str()
6663            .unwrap()
6664            .ends_with("hermes_home/profiles/coder"));
6665
6666        let main = profile_row(&response, "openclaw", "main");
6667        assert_eq!(main["kind"], "openclaw_agent");
6668        // No entry declares `default: true` (real configs do not), so `main`
6669        // wins on OpenClaw's own convention rather than alphabetically.
6670        assert_eq!(main["default"], true);
6671        assert_eq!(main["routes"], 0);
6672        assert_eq!(main["sessions"], 4);
6673        assert_eq!(
6674            main["model"],
6675            Value::Null,
6676            "`agents.defaults.model` is an install default, not this agent's pin"
6677        );
6678
6679        let design = profile_row(&response, "openclaw", "design");
6680        assert_eq!(design["default"], false);
6681        assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6682        assert_eq!(design["sessions"], 0);
6683        assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6684
6685        let preset = profile_row(&response, "supercode", "supercode-default");
6686        assert_eq!(preset["kind"], "preset");
6687        assert_eq!(preset["default"], true);
6688        assert_eq!(preset["home"], Value::Null);
6689        assert_eq!(preset["routes"], Value::Null);
6690    }
6691
6692    /// Codex's own profiles are `[profiles.<name>]` tables, with the
6693    /// top-level `profile` key naming the default.
6694    #[test]
6695    fn profiles_list_reads_codex_profile_tables() {
6696        let codex_home = std::env::temp_dir().join(format!(
6697            "supercode-orch10-codex-{}-{}",
6698            std::process::id(),
6699            std::time::SystemTime::now()
6700                .duration_since(std::time::UNIX_EPOCH)
6701                .unwrap()
6702                .as_nanos()
6703        ));
6704        std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6705        std::fs::write(
6706            codex_home.join("config.toml"),
6707            "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6708        )
6709        .unwrap();
6710
6711        let mut service = HarnessSessionService::new();
6712        let response = service.handle(request(
6713            1,
6714            "harness.v1.profiles.list",
6715            json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6716        ));
6717        let rows = response["result"]["profiles"].as_array().unwrap();
6718        assert_eq!(rows.len(), 2, "{response}");
6719        let review = profile_row(&response, "codex", "review");
6720        assert_eq!(review["kind"], "codex_profile");
6721        assert_eq!(review["default"], true);
6722        assert_eq!(review["model"], "gpt-5.1-codex");
6723        assert_eq!(review["home"], Value::Null);
6724        assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6725
6726        let got = service.handle(request(
6727            2,
6728            "harness.v1.profiles.get",
6729            json!({
6730                "harness": "codex",
6731                "name": "fast",
6732                "homes": {"codex": codex_home.join("sessions")},
6733            }),
6734        ));
6735        assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6736        std::fs::remove_dir_all(&codex_home).ok();
6737    }
6738
6739    /// A verb a harness lacks fails with `UnsupportedAction`, never a silent
6740    /// empty list; an unknown name is an invalid argument, not an empty row.
6741    #[test]
6742    fn profiles_refuse_harnesses_without_the_concept() {
6743        let mut service = HarnessSessionService::new();
6744        let response = service.handle(request(
6745            1,
6746            "harness.v1.profiles.list",
6747            json!({"harness": "claude-code"}),
6748        ));
6749        assert_eq!(response["error"]["code"], -32020, "{response}");
6750
6751        let missing = service.handle(request(
6752            2,
6753            "harness.v1.profiles.get",
6754            json!({
6755                "harness": "hermes",
6756                "name": "no-such-profile",
6757                "homes": profile_fixture_homes(),
6758            }),
6759        ));
6760        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6761    }
6762
6763    /// The two methods are advertised, so a client discovers them from
6764    /// `harness.v1.capabilities` rather than from documentation.
6765    #[test]
6766    fn profiles_methods_are_advertised() {
6767        let mut service = HarnessSessionService::new();
6768        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6769        let methods = response["result"]["methods"].as_array().unwrap();
6770        for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6771            assert!(
6772                methods.iter().any(|entry| entry == method),
6773                "{method} is not advertised"
6774            );
6775        }
6776    }
6777
6778    // -----------------------------------------------------------------
6779    // ORCH-14 — channels
6780    // -----------------------------------------------------------------
6781
6782    fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6783        response["result"]["channels"]
6784            .as_array()
6785            .unwrap_or_else(|| panic!("no channels array in {response}"))
6786            .iter()
6787            .find(|row| row["harness"] == harness && row["name"] == name)
6788            .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6789    }
6790
6791    fn channels_list(harness: Option<&str>) -> Value {
6792        let mut params = json!({"homes": profile_fixture_homes()});
6793        if let Some(harness) = harness {
6794            params["harness"] = json!(harness);
6795        }
6796        HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6797    }
6798
6799    /// dev/01: both sources answer in one row shape over the committed
6800    /// fixture homes — Hermes's `platforms:` blocks with their `extra` maps,
6801    /// and OpenClaw's `channels.<name>` entries split per account.
6802    #[test]
6803    fn channels_list_reads_both_gateway_harnesses_uniformly() {
6804        let response = channels_list(None);
6805        assert_eq!(
6806            response["result"]["schema"],
6807            crate::channels::CHANNELS_SCHEMA
6808        );
6809
6810        // Hermes: a credentialed platform, a bridged `extra.key` platform,
6811        // and one the config explicitly disables.
6812        let telegram = channel_row(&response, "hermes", "telegram");
6813        assert_eq!(telegram["kind"], "telegram");
6814        assert_eq!(telegram["enabled"], true);
6815        assert_eq!(telegram["configured"], true);
6816        // The `sessions` count is the discovery rows whose surface platform
6817        // is telegram: the fixture's `agent:main:telegram:…` DM and the
6818        // `agent:coder:telegram:…` group.
6819        assert_eq!(telegram["sessions"], 2);
6820        let api = channel_row(&response, "hermes", "api_server");
6821        assert_eq!(api["configured"], true, "extra.key is a credential key");
6822        assert_eq!(api["sessions"], 0);
6823        let webhook = channel_row(&response, "hermes", "webhook");
6824        assert_eq!(webhook["enabled"], false);
6825        // Hermes lists no credential for `webhook`: declaring it is all it
6826        // needs, so a credential-less entry is still `configured`.
6827        assert_eq!(webhook["configured"], true);
6828
6829        // OpenClaw: one row per account, named `<channel>/<accountId>`.
6830        let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6831        assert_eq!(linked["kind"], "slack");
6832        assert_eq!(linked["account"], "T0FIXTURE");
6833        assert_eq!(linked["enabled"], true);
6834        assert_eq!(linked["configured"], true);
6835        let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6836        assert_eq!(unlinked["enabled"], false);
6837        assert_eq!(
6838            unlinked["configured"], false,
6839            "an account with no credential key is not configured"
6840        );
6841        // A single-account channel keeps its own name and names its account
6842        // inline.
6843        let telegram = channel_row(&response, "openclaw", "telegram");
6844        assert_eq!(telegram["account"], "hermes-fixture-bot");
6845        assert_eq!(telegram["configured"], true);
6846
6847        // `status` is never claimed from a config file.
6848        for row in response["result"]["channels"].as_array().unwrap() {
6849            assert_eq!(row["status"], "unknown", "{row}");
6850        }
6851    }
6852
6853    /// dev/01: no field of any emitted row carries a credential. The fixture
6854    /// homes hold four FAKE credential strings; a row that leaked one — as a
6855    /// value, an account label, or a name — fails here.
6856    #[test]
6857    fn channels_rows_never_carry_a_fixture_secret() {
6858        let secrets = [
6859            "FAKE-TOKEN-DO-NOT-EMIT",
6860            "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6861            "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6862            "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6863            "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6864        ];
6865        // The strings really are in the fixtures, so this test can fail.
6866        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6867        let raw = format!(
6868            "{}{}",
6869            std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6870            std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6871        );
6872        for secret in secrets {
6873            assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6874        }
6875
6876        let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6877        for secret in secrets {
6878            assert!(
6879                !emitted.contains(secret),
6880                "`{secret}` leaked into a channel row: {emitted}"
6881            );
6882        }
6883        // Belt and braces: no row FIELD is credential-shaped either, so a
6884        // future field cannot smuggle one past the literal scan.
6885        for row in channels_list(None)["result"]["channels"]
6886            .as_array()
6887            .unwrap()
6888        {
6889            for key in row.as_object().unwrap().keys() {
6890                let key = key.to_ascii_lowercase();
6891                assert!(
6892                    !["token", "key", "secret", "password", "credential"]
6893                        .iter()
6894                        .any(|marker| key.ends_with(marker)),
6895                    "`{key}` is a credential-shaped field on a channel row"
6896                );
6897            }
6898        }
6899    }
6900
6901    /// `status` answers one row by name, and refuses an unknown one.
6902    #[test]
6903    fn channels_status_reads_one_row_by_name() {
6904        let mut service = HarnessSessionService::new();
6905        let got = service.handle(request(
6906            1,
6907            "harness.v1.channels.status",
6908            json!({
6909                "harness": "openclaw",
6910                "name": "slack/T0FIXTURE",
6911                "homes": profile_fixture_homes(),
6912            }),
6913        ));
6914        assert_eq!(got["result"]["channel"]["kind"], "slack");
6915        assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6916        assert_eq!(got["result"]["channel"]["status"], "unknown");
6917
6918        let missing = service.handle(request(
6919            2,
6920            "harness.v1.channels.status",
6921            json!({
6922                "harness": "openclaw",
6923                "name": "no-such-channel",
6924                "homes": profile_fixture_homes(),
6925            }),
6926        ));
6927        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6928    }
6929
6930    /// A harness with no channel concept fails with `UnsupportedAction`,
6931    /// never a silent empty list — Claude Code included, because its channels
6932    /// are MCP-protocol declarations no config file names.
6933    #[test]
6934    fn channels_refuse_harnesses_without_the_concept() {
6935        let response = channels_list(Some("claude-code"));
6936        assert_eq!(response["error"]["code"], -32020, "{response}");
6937        let codex = channels_list(Some("codex"));
6938        assert_eq!(codex["error"]["code"], -32020, "{codex}");
6939    }
6940
6941    /// The harness filter restricts the rows rather than being ignored.
6942    #[test]
6943    fn channels_list_filters_by_harness() {
6944        let response = channels_list(Some("openclaw"));
6945        let rows = response["result"]["channels"].as_array().unwrap();
6946        assert!(!rows.is_empty(), "{response}");
6947        assert!(
6948            rows.iter().all(|row| row["harness"] == "openclaw"),
6949            "harness filter leaked: {response}"
6950        );
6951    }
6952
6953    /// Both methods are advertised, so a client discovers them from
6954    /// `harness.v1.capabilities` rather than from documentation.
6955    #[test]
6956    fn channels_methods_are_advertised() {
6957        let mut service = HarnessSessionService::new();
6958        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6959        let methods = response["result"]["methods"].as_array().unwrap();
6960        for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6961            assert!(
6962                methods.iter().any(|entry| entry == method),
6963                "{method} is not advertised"
6964            );
6965        }
6966    }
6967
6968    /// The UI story renders REAL rows: this writes the discovery response the
6969    /// two assertions above pin into the fixture the Storybook
6970    /// `Compositions/Universal nouns` stories import, and fails when the
6971    /// committed copy has drifted from what the service now answers.
6972    #[test]
6973    fn orch6_story_fixture_matches_the_live_discovery_response() {
6974        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6975            .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6976        let mut result = hermes_discovery(hermes_query());
6977        // `updated_at_ms` is derived from the fixture's own stored timestamps,
6978        // so the whole response is deterministic; drop only the cursor, which
6979        // is pagination state rather than a session fact.
6980        result.as_object_mut().unwrap().remove("next_cursor");
6981        let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6982        if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6983            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6984            std::fs::write(&path, &rendered).unwrap();
6985        }
6986        let committed = std::fs::read_to_string(&path).unwrap_or_default();
6987        assert_eq!(
6988            committed, rendered,
6989            "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6990             re-run with SUPERCODE_UPDATE_FIXTURES=1"
6991        );
6992    }
6993
6994    fn pi_locator() -> SessionLocator {
6995        SessionLocator {
6996            harness: HarnessId::from(HarnessId::PI),
6997            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6998            storage: StorageLocator::File {
6999                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7000                    .join("tests/fixtures/pi_session.jsonl"),
7001            },
7002        }
7003    }
7004
7005    fn opencode_locator() -> SessionLocator {
7006        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
7007        SessionLocator {
7008            harness: HarnessId::from(HarnessId::OPENCODE),
7009            session_id: session_id.into(),
7010            storage: StorageLocator::Sqlite {
7011                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7012                    .join("tests/fixtures/opencode_fixture/opencode.db"),
7013                selector: session_id.into(),
7014            },
7015        }
7016    }
7017
7018    fn grok_locator() -> SessionLocator {
7019        SessionLocator {
7020            harness: HarnessId::from(HarnessId::GROK),
7021            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
7022            storage: StorageLocator::File {
7023                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7024                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
7025            },
7026        }
7027    }
7028
7029    // ---- ORCH-11: `harness.v1.skills.list` -------------------------------
7030
7031    fn fixture_homes() -> Value {
7032        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7033        json!({
7034            "claude_code": fixtures.join("__absent__"),
7035            "codex": fixtures.join("__absent__"),
7036            "opencode": fixtures.join("__absent__"),
7037            "pi": fixtures.join("__absent__"),
7038            "agents": fixtures.join("__absent__"),
7039            "hermes": fixtures.join("hermes_home"),
7040            "openclaw": fixtures.join("openclaw_home"),
7041        })
7042    }
7043
7044    #[test]
7045    fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
7046        let root = std::env::temp_dir().join(format!(
7047            "supercode-preview-rpc-{}-{}",
7048            std::process::id(),
7049            std::time::SystemTime::now()
7050                .duration_since(std::time::UNIX_EPOCH)
7051                .unwrap()
7052                .as_nanos()
7053        ));
7054        std::fs::create_dir_all(&root).unwrap();
7055        for id in ["first", "second"] {
7056            std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7057                json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7058                json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7059            )).unwrap();
7060        }
7061        let mut service = HarnessSessionService::new();
7062        let query = json!({
7063            "harnesses": ["codex"], "homes": {"codex": root},
7064            "query": "nebula", "search_previews": true, "limit": 1
7065        });
7066        let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7067        assert!(first.get("error").is_none(), "{first}");
7068        assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7069        assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7070        let mut next_query = query.clone();
7071        next_query["cursor"] = first["result"]["next_cursor"].clone();
7072        let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7073        assert_eq!(next["result"]["receipt"]["returned"], 1);
7074        assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7075        assert_eq!(next["result"]["receipt"]["truncated"], false);
7076        assert_ne!(
7077            first["result"]["sessions"][0]["locator"],
7078            next["result"]["sessions"][0]["locator"]
7079        );
7080        let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7081        assert!(
7082            refused["error"]["message"]
7083                .as_str()
7084                .unwrap()
7085                .contains("use sessions.discover"),
7086            "{refused}"
7087        );
7088        std::fs::remove_dir_all(root).unwrap();
7089    }
7090
7091    #[test]
7092    fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7093        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7094        let root = std::env::temp_dir().join(format!(
7095            "supercode-index-rpc-{}-{}",
7096            std::process::id(),
7097            std::time::SystemTime::now()
7098                .duration_since(std::time::UNIX_EPOCH)
7099                .unwrap()
7100                .as_nanos()
7101        ));
7102        std::fs::create_dir_all(&root).unwrap();
7103        for id in ["first", "second"] {
7104            std::fs::write(root.join(format!("{id}.jsonl")), format!(
7105                "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7106            )).unwrap();
7107        }
7108        let mut service = HarnessSessionService::new();
7109        let opened = service.handle(request(
7110            1,
7111            "harness.v1.sessions.index.subscribe",
7112            json!({
7113                "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7114            }),
7115        ));
7116        assert!(opened.get("error").is_none(), "{opened:#}");
7117        let subscription = opened["result"]["subscription"]
7118            .as_str()
7119            .unwrap()
7120            .to_owned();
7121        assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7122        for params in [
7123            json!({"subscription": subscription, "limit": 0}),
7124            json!({"subscription": subscription, "limit": 2049}),
7125            json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7126            json!({"subscription": "unknown", "limit": 2}),
7127        ] {
7128            let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7129            assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7130        }
7131        for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7132            let response = service.handle(request(
7133                3,
7134                "harness.v1.sessions.index.resize",
7135                json!({
7136                    "subscription": subscription, "limit": limit
7137                }),
7138            ));
7139            assert!(response.get("error").is_none(), "{response:#}");
7140            assert_eq!(response["result"]["subscription"], subscription);
7141            assert_eq!(response["result"]["revision"], revision);
7142            assert_eq!(
7143                response["result"]["initial"].as_array().unwrap().len(),
7144                limit
7145            );
7146            assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7147            assert_eq!(service.index_subscriptions.len(), 1);
7148        }
7149        let removed = service.handle(request(
7150            4,
7151            "harness.v1.sessions.index.unsubscribe",
7152            json!({
7153                "subscription": subscription
7154            }),
7155        ));
7156        assert_eq!(removed["result"]["removed"], true);
7157        let stale = service.handle(request(
7158            5,
7159            "harness.v1.sessions.index.resize",
7160            json!({
7161                "subscription": subscription, "limit": 1
7162            }),
7163        ));
7164        assert_eq!(stale["error"]["code"], -32602);
7165        drop(service);
7166        std::fs::remove_dir_all(root).unwrap();
7167    }
7168
7169    fn skills_rows(params: Value) -> Vec<Value> {
7170        let response =
7171            HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7172        assert!(response.get("error").is_none(), "{response:#}");
7173        response["result"].as_array().cloned().unwrap_or_default()
7174    }
7175
7176    /// The uniform row over two harnesses at once, from the harnesses' own
7177    /// skill roots: name, harness, scope, location, description, version.
7178    #[test]
7179    fn skills_list_reads_the_hermes_and_openclaw_roots() {
7180        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7181        let rows = skills_rows(json!({
7182            "homes": fixture_homes(),
7183            "cwd": fixtures.join("hermes_home"),
7184        }));
7185        let arxiv = rows
7186            .iter()
7187            .find(|row| row["name"] == json!("arxiv-search"))
7188            .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7189        assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7190        assert_eq!(arxiv["scope"], json!("user"));
7191        assert_eq!(arxiv["version"], json!("1.4.0"));
7192        assert!(arxiv["location"]
7193            .as_str()
7194            .unwrap()
7195            .ends_with("hermes_home/skills/research/arxiv"));
7196
7197        // A directory with no SKILL.md still lists, by directory name.
7198        let bare = rows
7199            .iter()
7200            .find(|row| row["name"] == json!("bare-skill"))
7201            .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7202        assert_eq!(bare["enabled"], json!(null));
7203        assert!(bare.get("description").is_none());
7204
7205        let demo = rows
7206            .iter()
7207            .find(|row| row["name"] == json!("clawhub-demo"))
7208            .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7209        assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7210        assert_eq!(demo["scope"], json!("managed"));
7211        assert_eq!(demo["enabled"], json!(false));
7212    }
7213
7214    /// Both filters select against the same rows.
7215    #[test]
7216    fn skills_list_filters_by_harness_and_scope() {
7217        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7218        let hermes = skills_rows(json!({
7219            "homes": fixture_homes(),
7220            "cwd": fixtures.join("hermes_home"),
7221            "harness": HarnessId::HERMES,
7222        }));
7223        assert!(!hermes.is_empty());
7224        assert!(hermes
7225            .iter()
7226            .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7227
7228        let managed = skills_rows(json!({
7229            "homes": fixture_homes(),
7230            "cwd": fixtures.join("openclaw_home"),
7231            "harness": HarnessId::OPENCLAW,
7232            "scope": "managed",
7233        }));
7234        assert_eq!(managed.len(), 1, "{managed:#?}");
7235        assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7236
7237        let bundled = skills_rows(json!({
7238            "homes": fixture_homes(),
7239            "cwd": fixtures.join("openclaw_home"),
7240            "harness": HarnessId::OPENCLAW,
7241            "scope": "bundled",
7242        }));
7243        assert!(bundled.is_empty(), "{bundled:#?}");
7244    }
7245
7246    /// A harness supercode has no skills root for is refused by name, not
7247    /// answered with an empty list.
7248    #[test]
7249    fn skills_list_refuses_an_unknown_harness() {
7250        let response = HarnessSessionService::new().handle(request(
7251            1,
7252            "harness.v1.skills.list",
7253            json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7254        ));
7255        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7256        assert!(response["error"]["message"]
7257            .as_str()
7258            .unwrap()
7259            .contains("not-a-harness"));
7260    }
7261
7262    /// The method is advertised, and its SDK operation resolves it.
7263    #[test]
7264    fn skills_list_is_an_advertised_method_and_sdk_operation() {
7265        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7266        assert_eq!(
7267            SdkOperation::from_method("harness.v1.skills.list"),
7268            Some(SdkOperation::SkillsList)
7269        );
7270    }
7271
7272    // ---- ORCH-22: `harness.v1.skills.install|remove` ----------------------
7273
7274    /// Both controlled verbs are advertised and resolve to their operation.
7275    #[test]
7276    fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7277        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7278        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7279        assert_eq!(
7280            SdkOperation::from_method("harness.v1.skills.install"),
7281            Some(SdkOperation::SkillsInstall)
7282        );
7283        assert_eq!(
7284            SdkOperation::from_method("harness.v1.skills.remove"),
7285            Some(SdkOperation::SkillsRemove)
7286        );
7287    }
7288
7289    /// The directory door, end to end over the RPC: a local package lands in
7290    /// Claude Code's own user root and the outcome carries the operation and
7291    /// the row the ORCH-11 loader reads back.
7292    #[test]
7293    fn skills_install_and_remove_drive_the_directory_door() {
7294        let root = std::env::temp_dir().join(format!(
7295            "supercode-orch22-rpc-{}-{}",
7296            std::process::id(),
7297            std::time::SystemTime::now()
7298                .duration_since(std::time::UNIX_EPOCH)
7299                .unwrap()
7300                .as_nanos()
7301        ));
7302        let source = root.join("probe-src");
7303        std::fs::create_dir_all(&source).unwrap();
7304        std::fs::write(
7305            source.join("SKILL.md"),
7306            "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7307        )
7308        .unwrap();
7309        let homes = json!({
7310            "claude_code": root.join("claude_home"),
7311            "codex": root.join("__absent__"),
7312            "opencode": root.join("__absent__"),
7313            "pi": root.join("__absent__"),
7314            "hermes": root.join("__absent__"),
7315            "openclaw": root.join("__absent__"),
7316            "agents": root.join("__absent__"),
7317        });
7318
7319        let mut service = HarnessSessionService::new();
7320        let installed = service.handle(request(
7321            1,
7322            "harness.v1.skills.install",
7323            json!({
7324                "harness": HarnessId::CLAUDE_CODE,
7325                "source": source,
7326                "scope": "user",
7327                "cwd": root,
7328                "homes": homes,
7329            }),
7330        ));
7331        let result = &installed["result"];
7332        assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7333        assert_eq!(result["verb"], json!("install"));
7334        assert!(result["ran"]
7335            .as_str()
7336            .is_some_and(|ran| ran.starts_with("cp -R ")));
7337        assert_eq!(result["skill"]["scope"], json!("user"));
7338
7339        let removed = service.handle(request(
7340            2,
7341            "harness.v1.skills.remove",
7342            json!({
7343                "harness": HarnessId::CLAUDE_CODE,
7344                "name": "orch22-rpc",
7345                "scope": "user",
7346                "cwd": root,
7347                "homes": homes,
7348            }),
7349        ));
7350        assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7351        assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7352        std::fs::remove_dir_all(&root).ok();
7353    }
7354
7355    /// OpenClaw publishes no `skills remove` at the pin, so the uniform verb
7356    /// refuses with UnsupportedAction instead of deleting files itself.
7357    #[test]
7358    fn skills_remove_refuses_openclaw_at_the_pin() {
7359        let response = HarnessSessionService::new().handle(request(
7360            1,
7361            "harness.v1.skills.remove",
7362            json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7363        ));
7364        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7365        assert!(response["error"]["message"]
7366            .as_str()
7367            .unwrap()
7368            .contains("no `skills remove` verb"));
7369    }
7370
7371    /// A harness with no skills root at all is refused by name, with the
7372    /// same sentence `skills.list` gives it.
7373    #[test]
7374    fn skills_install_refuses_a_harness_without_a_skills_root() {
7375        let response = HarnessSessionService::new().handle(request(
7376            1,
7377            "harness.v1.skills.install",
7378            json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7379        ));
7380        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7381        assert!(response["error"]["message"]
7382            .as_str()
7383            .unwrap()
7384            .contains("not-a-harness"));
7385    }
7386
7387    // ---- ORCH-12: `harness.v1.memory.show|search` ------------------------
7388
7389    /// `HarnessHomes` for the committed fixture homes. Every root a test does
7390    /// not name is pinned at an absent path, so a read can never fall through
7391    /// to this machine's real harness homes. Note `hermes` is the `state.db`
7392    /// PATH (its parent is HERMES_HOME) and `claude_code` is the `projects`
7393    /// directory — the same contract discovery uses.
7394    fn memory_homes() -> Value {
7395        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7396        json!({
7397            "claude_code": fixtures.join("__absent__"),
7398            "codex": fixtures.join("__absent__"),
7399            "opencode": fixtures.join("__absent__"),
7400            "pi": fixtures.join("__absent__"),
7401            "grok": fixtures.join("__absent__"),
7402            "gemini": fixtures.join("__absent__"),
7403            "goose": fixtures.join("__absent__"),
7404            "supercode": fixtures.join("__absent__"),
7405            "hermes": fixtures.join("hermes_home/state.db"),
7406            "openclaw": fixtures.join("openclaw_home"),
7407        })
7408    }
7409
7410    fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7411        let response = HarnessSessionService::new().handle(request(1, method, params));
7412        assert!(response.get("error").is_none(), "{response:#}");
7413        assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7414        response["result"][key]
7415            .as_array()
7416            .cloned()
7417            .unwrap_or_default()
7418    }
7419
7420    fn memory_documents(params: Value) -> Vec<Value> {
7421        memory_call_ok("harness.v1.memory.show", params, "documents")
7422    }
7423
7424    fn memory_matches(params: Value) -> Vec<Value> {
7425        memory_call_ok("harness.v1.memory.search", params, "matches")
7426    }
7427
7428    fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7429        rows.iter()
7430            .find(|row| row["profile"] == profile && row["name"] == name)
7431            .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7432    }
7433
7434    /// Hermes: the built-in `MEMORY.md`/`USER.md` pair and the `memories/`
7435    /// topic files, for HERMES_HOME itself and for every profile home.
7436    #[test]
7437    fn memory_show_reads_the_hermes_profile_homes() {
7438        let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7439
7440        let notes = find_document(&rows, "default", "MEMORY.md");
7441        assert_eq!(notes["harness"], "hermes");
7442        assert_eq!(notes["scope"], "user");
7443        assert!(notes["size"].as_u64().unwrap() > 0);
7444        assert!(notes["updated_at"].is_string(), "{notes:#?}");
7445        // The default answer previews the head and never the whole body.
7446        assert!(notes.get("content").is_none(), "{notes:#?}");
7447        assert_eq!(notes["truncated"], true);
7448        assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7449
7450        let user = find_document(&rows, "default", "USER.md");
7451        assert_eq!(user["scope"], "user");
7452        assert!(user["preview"]
7453            .as_array()
7454            .unwrap()
7455            .iter()
7456            .any(|line| line.as_str().unwrap().contains("neovim")));
7457
7458        let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7459        assert!(topic["path"]
7460            .as_str()
7461            .unwrap()
7462            .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7463
7464        // Profile mode points HERMES_HOME at `<root>/profiles/<name>`.
7465        let coder = find_document(&rows, "coder", "MEMORY.md");
7466        assert_eq!(coder["scope"], "profile");
7467        assert!(coder["path"]
7468            .as_str()
7469            .unwrap()
7470            .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7471    }
7472
7473    /// `full` is the only way a body crosses the wire, and `profile` narrows
7474    /// the read to one home.
7475    #[test]
7476    fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7477        let rows = memory_documents(json!({
7478            "harness": "hermes",
7479            "profile": "coder",
7480            "full": true,
7481            "homes": memory_homes(),
7482        }));
7483        assert!(
7484            rows.iter().all(|row| row["profile"] == "coder"),
7485            "{rows:#?}"
7486        );
7487        let coder = find_document(&rows, "coder", "MEMORY.md");
7488        assert!(coder["content"]
7489            .as_str()
7490            .expect("full returns the body")
7491            .contains("anthropic/claude-opus-4-8"));
7492    }
7493
7494    /// OpenClaw: memory-core's files under each agent's workspace —
7495    /// `<state>/workspace` for the default agent, `<state>/workspace-<id>`
7496    /// for any other.
7497    #[test]
7498    fn memory_show_reads_the_openclaw_agent_workspaces() {
7499        let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7500
7501        let main = find_document(&rows, "main", "MEMORY.md");
7502        assert_eq!(main["scope"], "agent");
7503        assert!(main["path"]
7504            .as_str()
7505            .unwrap()
7506            .ends_with("openclaw_home/workspace/MEMORY.md"));
7507
7508        let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7509        assert!(topic["path"]
7510            .as_str()
7511            .unwrap()
7512            .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7513
7514        let design = find_document(&rows, "design", "MEMORY.md");
7515        assert!(design["path"]
7516            .as_str()
7517            .unwrap()
7518            .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7519    }
7520
7521    /// Claude Code: the auto-memory directory of the project the working tree
7522    /// belongs to, keyed by the enclosing git repository.
7523    #[test]
7524    fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7525        let scratch = std::env::temp_dir().join(format!(
7526            "supercode-orch12-cc-{}-{}",
7527            std::process::id(),
7528            std::time::SystemTime::now()
7529                .duration_since(std::time::UNIX_EPOCH)
7530                .unwrap()
7531                .as_nanos()
7532        ));
7533        let project = scratch.join("repo");
7534        std::fs::create_dir_all(project.join(".git")).unwrap();
7535        // Auto-memory is shared across a repo's worktrees, so a nested
7536        // working directory must resolve to the repo's own project dir.
7537        let worktree = project.join("crates/harness");
7538        std::fs::create_dir_all(&worktree).unwrap();
7539        let slug: String = project
7540            .to_string_lossy()
7541            .chars()
7542            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7543            .collect();
7544        let projects = scratch.join("claude/projects");
7545        let memory = projects.join(&slug).join("memory");
7546        std::fs::create_dir_all(&memory).unwrap();
7547        std::fs::write(
7548            memory.join("MEMORY.md"),
7549            "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7550        )
7551        .unwrap();
7552        std::fs::write(
7553            memory.join("build-box.md"),
7554            "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7555        )
7556        .unwrap();
7557
7558        let mut homes = memory_homes();
7559        homes["claude_code"] = json!(projects);
7560        let rows = memory_documents(json!({
7561            "harness": "claude-code",
7562            "cwd": worktree,
7563            "homes": homes,
7564        }));
7565        let index = find_document(&rows, &slug, "MEMORY.md");
7566        assert_eq!(index["harness"], "claude-code");
7567        assert_eq!(index["scope"], "project");
7568        let topic = find_document(&rows, &slug, "build-box.md");
7569        assert!(topic["preview"]
7570            .as_array()
7571            .unwrap()
7572            .iter()
7573            .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7574
7575        let hits = memory_matches(json!({
7576            "harness": "claude-code",
7577            "query": "pinned harnesses",
7578            "cwd": worktree,
7579            "homes": homes,
7580        }));
7581        assert_eq!(hits.len(), 1, "{hits:#?}");
7582        assert_eq!(hits[0]["name"], "MEMORY.md");
7583        assert_eq!(hits[0]["line"], 2);
7584
7585        let _ = std::fs::remove_dir_all(&scratch);
7586    }
7587
7588    /// A config-less OpenClaw install declares no default agent, but
7589    /// memory-core still resolves ONE agent to the default `workspace`
7590    /// directory — the same `main`-then-first convention the profile rows
7591    /// use. Measured against `openclaw memory status` on the pinned CLI
7592    /// (`docs/interop/research/orch12-memory-receipt-2026-09-03.json`).
7593    #[test]
7594    fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7595        let state = std::env::temp_dir().join(format!(
7596            "supercode-orch12-oc-{}-{}",
7597            std::process::id(),
7598            std::time::SystemTime::now()
7599                .duration_since(std::time::UNIX_EPOCH)
7600                .unwrap()
7601                .as_nanos()
7602        ));
7603        // No `openclaw.json`: only the agent home the gateway creates.
7604        std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7605        std::fs::create_dir_all(state.join("workspace")).unwrap();
7606        std::fs::write(
7607            state.join("workspace/MEMORY.md"),
7608            "the gateway websocket needs credentials\n",
7609        )
7610        .unwrap();
7611
7612        let mut homes = memory_homes();
7613        homes["openclaw"] = json!(state);
7614        let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7615        assert_eq!(rows.len(), 1, "{rows:#?}");
7616        let row = find_document(&rows, "main", "MEMORY.md");
7617        assert_eq!(row["scope"], "agent");
7618        assert!(row["path"]
7619            .as_str()
7620            .unwrap()
7621            .ends_with("workspace/MEMORY.md"));
7622
7623        let _ = std::fs::remove_dir_all(&state);
7624    }
7625
7626    /// Search is a plain scan over the same documents: a hit carries the
7627    /// path, line and excerpt; a miss is an empty list, not an error.
7628    #[test]
7629    fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7630        let hit = memory_matches(json!({
7631            "harness": "hermes",
7632            "query": "NEOVIM",
7633            "homes": memory_homes(),
7634        }));
7635        assert_eq!(hit.len(), 1, "{hit:#?}");
7636        assert_eq!(hit[0]["harness"], "hermes");
7637        assert_eq!(hit[0]["name"], "USER.md");
7638        assert_eq!(hit[0]["scope"], "user");
7639        assert_eq!(hit[0]["line"], 5);
7640        assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7641
7642        // A regular expression reaches the same lines.
7643        let regex = memory_matches(json!({
7644            "harness": "hermes",
7645            "query": "neo(vim|vi)",
7646            "regex": true,
7647            "homes": memory_homes(),
7648        }));
7649        assert_eq!(regex.len(), 1, "{regex:#?}");
7650
7651        let miss = memory_matches(json!({
7652            "harness": "hermes",
7653            "query": "no-memory-line-says-this",
7654            "homes": memory_homes(),
7655        }));
7656        assert!(miss.is_empty(), "{miss:#?}");
7657    }
7658
7659    /// The uniform-verb contract: a harness with no memory store at the pin
7660    /// is refused by name, and `session` only selects a Claude Code project.
7661    #[test]
7662    fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7663        for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7664            let response = HarnessSessionService::new().handle(request(
7665                1,
7666                method,
7667                json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7668            ));
7669            assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7670            assert!(response["error"]["message"]
7671                .as_str()
7672                .unwrap()
7673                .contains("codex"));
7674        }
7675
7676        let response = HarnessSessionService::new().handle(request(
7677            1,
7678            "harness.v1.memory.show",
7679            json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7680        ));
7681        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7682
7683        // `harness` is not optional: memory documents are the user's prose.
7684        let response = HarnessSessionService::new().handle(request(
7685            1,
7686            "harness.v1.memory.show",
7687            json!({"homes": memory_homes()}),
7688        ));
7689        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7690    }
7691
7692    /// Both methods are advertised, and their SDK operations resolve them.
7693    #[test]
7694    fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7695        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7696        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7697        assert_eq!(
7698            SdkOperation::from_method("harness.v1.memory.show"),
7699            Some(SdkOperation::MemoryShow)
7700        );
7701        assert_eq!(
7702            SdkOperation::from_method("harness.v1.memory.search"),
7703            Some(SdkOperation::MemorySearch)
7704        );
7705    }
7706
7707    // ---- ORCH-9: `harness.v1.approvals.list` -----------------------------
7708
7709    /// A runtime that raises one protocol request and then goes quiet, so a
7710    /// single poll delivers the request without closing the connection.
7711    struct RequestingRuntime {
7712        handle: RuntimeHandle,
7713        events: std::collections::VecDeque<HarnessEvent>,
7714        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7715    }
7716
7717    #[async_trait]
7718    impl RuntimeConnection for RequestingRuntime {
7719        fn handle(&self) -> &RuntimeHandle {
7720            &self.handle
7721        }
7722
7723        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7724            unreachable!("this runtime only raises requests")
7725        }
7726
7727        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7728            match self.events.pop_front() {
7729                Some(event) => Ok(Some(event)),
7730                // Quiet, not closed: `poll_sdk_events` times out and leaves
7731                // the connection open, the way a runtime blocked on a
7732                // permission request behaves.
7733                None => std::future::pending().await,
7734            }
7735        }
7736
7737        async fn interrupt(&mut self) -> crate::Result<()> {
7738            Ok(())
7739        }
7740
7741        async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7742            // Both halves are recorded: ORCH-20 has to prove not just that the
7743            // right request was answered but that the door received its own
7744            // reply envelope.
7745            self.answered
7746                .lock()
7747                .unwrap_or_else(std::sync::PoisonError::into_inner)
7748                .push(json!({"request_id": request_id, "response": response}));
7749            Ok(())
7750        }
7751
7752        async fn close(&mut self) -> crate::Result<()> {
7753            Ok(())
7754        }
7755    }
7756
7757    fn requesting_runtime(
7758        harness: &str,
7759        events: Vec<HarnessEvent>,
7760        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7761    ) -> Box<dyn RuntimeConnection> {
7762        requesting_runtime_named(harness, "hermes-live-session", events, answered)
7763    }
7764
7765    fn requesting_runtime_named(
7766        harness: &str,
7767        runtime_id: &str,
7768        events: Vec<HarnessEvent>,
7769        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7770    ) -> Box<dyn RuntimeConnection> {
7771        Box::new(RequestingRuntime {
7772            handle: RuntimeHandle {
7773                harness: HarnessId::from(harness),
7774                runtime_id: runtime_id.into(),
7775                endpoint: RuntimeEndpoint::LocalProcess {
7776                    pid: None,
7777                    command: vec!["hermes-acp".into()],
7778                    protocol: "acp".into(),
7779                },
7780            },
7781            events: events.into(),
7782            answered,
7783        })
7784    }
7785
7786    fn permission_event(id: u64, title: &str) -> HarnessEvent {
7787        HarnessEvent {
7788            sequence: None,
7789            kind: "session/request_permission".into(),
7790            payload: json!({
7791                "jsonrpc": "2.0",
7792                "id": id,
7793                "method": "session/request_permission",
7794                "params": {
7795                    "sessionId": "hermes-live-session",
7796                    "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7797                    "options": [
7798                        {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7799                        {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7800                        {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7801                    ],
7802                },
7803            }),
7804        }
7805    }
7806
7807    fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7808        let response = service.handle(request(1, "harness.v1.approvals.list", params));
7809        assert!(response.get("error").is_none(), "{response:#}");
7810        response["result"].clone()
7811    }
7812
7813    /// ORC-2 dev/01: the same uniform loop over the CLAUDE CODE door. The
7814    /// `can_use_tool` control request the CLI raises to its registered
7815    /// permission handler lists as one pending row, `approvals.resolve <id>
7816    /// allow_once` sends the `{behavior}` result the CLI accepts through
7817    /// `runtimes.respond`, and the row is gone. The frame is the one claude
7818    /// 2.1.258 wrote, transcribed from
7819    /// `docs/interop/research/orc2-claude-respond-receipt-2026-09-04.json`.
7820    #[tokio::test]
7821    async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7822        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7823        let mut service = HarnessSessionService::new();
7824        service.runtimes.insert(
7825            "runtime-cc".into(),
7826            requesting_runtime_named(
7827                HarnessId::CLAUDE_CODE,
7828                "claude-live-session",
7829                vec![HarnessEvent {
7830                    sequence: None,
7831                    kind: "control_request".into(),
7832                    payload: json!({
7833                        "type": "control_request",
7834                        "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7835                        "request": {
7836                            "subtype": "can_use_tool",
7837                            "tool_name": "Bash",
7838                            "display_name": "Bash",
7839                            "input": {"command": "touch probe-artifact.txt"},
7840                            "tool_use_id": "toolu_mock_1",
7841                        },
7842                    }),
7843                }],
7844                answered.clone(),
7845            ),
7846        );
7847
7848        let notifications = service.poll_runtimes().await;
7849        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7850
7851        let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7852        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7853        let row = &rows[0];
7854        assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7855        assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7856        assert_eq!(row["status"], "pending");
7857        assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7858        assert_eq!(row["runtime_id"], "claude-live-session");
7859        assert_eq!(
7860            row["options"]
7861                .as_array()
7862                .unwrap()
7863                .iter()
7864                .map(|option| option["id"].as_str().unwrap())
7865                .collect::<Vec<_>>(),
7866            vec!["allow", "deny"],
7867        );
7868
7869        let response = resolve(
7870            &mut service,
7871            json!({"id": row["id"], "decision": "allow_once"}),
7872        )
7873        .await;
7874        assert!(response.get("error").is_none(), "{response:#}");
7875        assert_eq!(response["result"]["option_id"], "allow");
7876        assert_eq!(
7877            answered
7878                .lock()
7879                .unwrap_or_else(std::sync::PoisonError::into_inner)
7880                .as_slice(),
7881            &[json!({
7882                "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7883                "response": {"behavior": "allow"},
7884            })],
7885        );
7886        assert_eq!(
7887            approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7888                .as_array()
7889                .map(Vec::len),
7890            Some(0),
7891        );
7892    }
7893
7894    /// dev/01: a live ACP permission request raised on a driven runtime is
7895    /// listable while the turn is blocked on it, and stops being listable
7896    /// the moment `runtimes.respond` answers it.
7897    #[tokio::test]
7898    async fn a_live_permission_request_lists_until_it_is_answered() {
7899        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7900        let mut service = HarnessSessionService::new();
7901        service.runtimes.insert(
7902            "runtime-1".into(),
7903            requesting_runtime(
7904                HarnessId::HERMES,
7905                vec![permission_event(7, "rm -rf build")],
7906                answered.clone(),
7907            ),
7908        );
7909
7910        let notifications = service.poll_runtimes().await;
7911        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7912
7913        let rows = approvals(&mut service, json!({}));
7914        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7915        let row = &rows[0];
7916        assert_eq!(row["id"], "runtime-1/7");
7917        assert_eq!(row["harness"], HarnessId::HERMES);
7918        assert_eq!(row["kind"], "live");
7919        assert_eq!(row["status"], "pending");
7920        assert_eq!(row["subject"], "rm -rf build");
7921        assert_eq!(row["session_id"], "hermes-live-session");
7922        assert_eq!(row["runtime_id"], "hermes-live-session");
7923        assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7924        assert!(
7925            row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7926            "{row:#}"
7927        );
7928        assert_eq!(
7929            row["options"]
7930                .as_array()
7931                .unwrap()
7932                .iter()
7933                .map(|option| option["id"].as_str().unwrap())
7934                .collect::<Vec<_>>(),
7935            vec!["allow_once", "allow_for_session", "deny"],
7936        );
7937
7938        // The filters select against the same rows.
7939        assert_eq!(
7940            approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7941                .as_array()
7942                .map(Vec::len),
7943            Some(1),
7944        );
7945        assert_eq!(
7946            approvals(&mut service, json!({"session": "some-other-session"}))
7947                .as_array()
7948                .map(Vec::len),
7949            Some(0),
7950        );
7951
7952        let response = service
7953            .handle_async(request(
7954                2,
7955                "harness.v1.runtimes.respond",
7956                json!({
7957                    "connection": "runtime-1",
7958                    "request_id": 7,
7959                    "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7960                }),
7961            ))
7962            .await;
7963        assert!(response.get("error").is_none(), "{response:#}");
7964        assert_eq!(
7965            answered
7966                .lock()
7967                .unwrap_or_else(std::sync::PoisonError::into_inner)
7968                .as_slice(),
7969            &[json!({
7970                "request_id": 7,
7971                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7972            })],
7973        );
7974
7975        let rows = approvals(&mut service, json!({}));
7976        assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7977    }
7978
7979    /// dev/01: supercode's own queued subagent approvals list through the
7980    /// same door, carrying the outcome the record holds.
7981    #[test]
7982    fn queued_subagent_approvals_list_through_the_same_door() {
7983        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7984            crate::subagents::QueuedApproval {
7985                child_agent_id: "child-7".into(),
7986                tool: "shell".into(),
7987                subject: Some("cargo publish --dry-run".into()),
7988                queued_at_ms: 1,
7989                outcome: None,
7990            },
7991            crate::subagents::QueuedApproval {
7992                child_agent_id: "child-8".into(),
7993                tool: "write_file".into(),
7994                subject: None,
7995                queued_at_ms: 2,
7996                outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7997            },
7998        ]));
7999        let mut service = HarnessSessionService::new();
8000        service.observe_subagent_approvals(queue);
8001
8002        let rows = approvals(&mut service, json!({}));
8003        assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
8004        assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
8005        assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
8006        assert_eq!(rows[0]["status"], "pending");
8007        assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
8008        assert_eq!(rows[1]["status"], "denied");
8009        assert!(rows[1]["options"].as_array().unwrap().is_empty());
8010
8011        // `--session` addresses a subagent row by its child agent id.
8012        let only = approvals(&mut service, json!({"session": "child-8"}));
8013        assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
8014        assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
8015    }
8016
8017    /// The uniform-verb contract: an id whose runtime door cannot carry a
8018    /// protocol request is refused BY NAME rather than answered with an empty
8019    /// list. Since ORC-2 gave Claude Code a permission-response primitive
8020    /// every registered harness can carry one, so the refusal is exercised on
8021    /// an unknown id — and the registered ids are asserted to be accepted.
8022    #[test]
8023    fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
8024        let response = HarnessSessionService::new().handle(request(
8025            1,
8026            "harness.v1.approvals.list",
8027            json!({"harness": "not-a-harness"}),
8028        ));
8029        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
8030        assert!(response["error"]["message"]
8031            .as_str()
8032            .unwrap()
8033            .contains("not-a-harness"));
8034        for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
8035            let response = HarnessSessionService::new().handle(request(
8036                1,
8037                "harness.v1.approvals.list",
8038                json!({"harness": harness}),
8039            ));
8040            assert!(response.get("error").is_none(), "{harness}: {response:#}");
8041        }
8042    }
8043
8044    /// The method is advertised, its SDK operation resolves it, and the
8045    /// registry reports the concept as observed for every harness whose
8046    /// runtime door can carry a request.
8047    #[test]
8048    fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
8049        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8050        assert_eq!(
8051            SdkOperation::from_method("harness.v1.approvals.list"),
8052            Some(SdkOperation::ApprovalsList)
8053        );
8054        let registry = harness_support_registry();
8055        for id in [
8056            HarnessId::HERMES,
8057            HarnessId::OPENCLAW,
8058            HarnessId::CODEX,
8059            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8060            // pending_request concept joins the other driven doors.
8061            HarnessId::CLAUDE_CODE,
8062        ] {
8063            let concept = registry
8064                .harnesses
8065                .iter()
8066                .find(|harness| harness.id.as_str() == id)
8067                .unwrap()
8068                .orchestration
8069                .concepts
8070                .iter()
8071                .find(|concept| concept.concept == "pending_request")
8072                .unwrap();
8073            assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8074            assert!(concept
8075                .methods
8076                .iter()
8077                .any(|method| method == "harness.v1.approvals.list"));
8078        }
8079    }
8080
8081    // ---- ORCH-20: `harness.v1.approvals.resolve` -------------------------
8082
8083    async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8084        service
8085            .handle_async(request(3, "harness.v1.approvals.resolve", params))
8086            .await
8087    }
8088
8089    /// dev/01: the whole loop on a driven runtime — list one pending row,
8090    /// answer it by ROW ID with one uniform decision, and see it gone. The
8091    /// door receives its own ACP envelope carrying the option it enumerated.
8092    #[tokio::test]
8093    async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8094        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8095        let mut service = HarnessSessionService::new();
8096        service.runtimes.insert(
8097            "runtime-1".into(),
8098            requesting_runtime(
8099                HarnessId::HERMES,
8100                vec![permission_event(7, "rm -rf build")],
8101                answered.clone(),
8102            ),
8103        );
8104        service.poll_runtimes().await;
8105
8106        let rows = approvals(&mut service, json!({}));
8107        assert_eq!(rows[0]["id"], "runtime-1/7");
8108
8109        let response = resolve(
8110            &mut service,
8111            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8112        )
8113        .await;
8114        assert!(response.get("error").is_none(), "{response:#}");
8115        assert_eq!(
8116            response["result"],
8117            json!({
8118                "id": "runtime-1/7",
8119                "decision": "allow_once",
8120                "option_id": "allow_once",
8121                "resolved": true,
8122            }),
8123        );
8124        // The harness's own door was called with its own envelope.
8125        assert_eq!(
8126            answered
8127                .lock()
8128                .unwrap_or_else(std::sync::PoisonError::into_inner)
8129                .as_slice(),
8130            &[json!({
8131                "request_id": 7,
8132                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8133            })],
8134        );
8135        // And the row is gone, the same way `runtimes.respond` drops it.
8136        assert_eq!(
8137            approvals(&mut service, json!({})).as_array().map(Vec::len),
8138            Some(0),
8139        );
8140        // Answering it twice is an honest miss, not a silent success.
8141        let response = resolve(
8142            &mut service,
8143            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8144        )
8145        .await;
8146        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8147    }
8148
8149    /// dev/01: deny travels the same path and picks the option the request
8150    /// itself classified as a refusal.
8151    #[tokio::test]
8152    async fn deny_selects_the_requests_own_reject_option() {
8153        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8154        let mut service = HarnessSessionService::new();
8155        service.runtimes.insert(
8156            "runtime-1".into(),
8157            requesting_runtime(
8158                HarnessId::HERMES,
8159                vec![permission_event(11, "git push --force")],
8160                answered.clone(),
8161            ),
8162        );
8163        service.poll_runtimes().await;
8164
8165        let response = resolve(
8166            &mut service,
8167            json!({"id": "runtime-1/11", "decision": "deny"}),
8168        )
8169        .await;
8170        assert!(response.get("error").is_none(), "{response:#}");
8171        // `deny` is the optionId whose ACP `kind` is `reject_once`.
8172        assert_eq!(response["result"]["option_id"], "deny");
8173        assert_eq!(
8174            answered
8175                .lock()
8176                .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8177            json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8178        );
8179        assert_eq!(
8180            approvals(&mut service, json!({})).as_array().map(Vec::len),
8181            Some(0),
8182        );
8183    }
8184
8185    /// dev/01: a decision this request does not offer is refused by name,
8186    /// listing the ones it does — never silently downgraded to a neighbour.
8187    #[tokio::test]
8188    async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8189        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8190        let mut service = HarnessSessionService::new();
8191        let mut event = permission_event(3, "rm -rf build");
8192        // A request offering only allow-once and deny, as hermes 0.21.0's
8193        // edit-approval layer raises one.
8194        event.payload["params"]["options"] = json!([
8195            {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8196            {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8197        ]);
8198        service.runtimes.insert(
8199            "runtime-1".into(),
8200            requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8201        );
8202        service.poll_runtimes().await;
8203
8204        let response = resolve(
8205            &mut service,
8206            json!({"id": "runtime-1/3", "decision": "allow_always"}),
8207        )
8208        .await;
8209        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8210        let message = response["error"]["message"].as_str().unwrap();
8211        assert!(message.contains("allow_always"), "{message}");
8212        assert!(message.contains("allow_once, deny"), "{message}");
8213        // Nothing was sent, and the request is still waiting for an answer.
8214        assert!(answered
8215            .lock()
8216            .unwrap_or_else(std::sync::PoisonError::into_inner)
8217            .is_empty());
8218        assert_eq!(
8219            approvals(&mut service, json!({})).as_array().map(Vec::len),
8220            Some(1),
8221        );
8222    }
8223
8224    /// dev/01: supercode's own queued subagent row is addressable but not
8225    /// answerable through this door — it is the parent's audit copy of a
8226    /// request its own handler answers. Refused by name, never a no-op.
8227    #[tokio::test]
8228    async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8229        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8230            crate::subagents::QueuedApproval {
8231                child_agent_id: "child-7".into(),
8232                tool: "shell".into(),
8233                subject: Some("cargo publish --dry-run".into()),
8234                queued_at_ms: 1,
8235                outcome: None,
8236            },
8237        ]));
8238        let mut service = HarnessSessionService::new();
8239        service.observe_subagent_approvals(queue.clone());
8240        let row = approvals(&mut service, json!({}))[0]["id"]
8241            .as_str()
8242            .unwrap()
8243            .to_string();
8244        assert_eq!(row, "supercode/subagent/child-7/1/0");
8245
8246        let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8247        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8248        let message = response["error"]["message"].as_str().unwrap();
8249        assert!(message.contains("queued subagent record"), "{message}");
8250        assert!(message.contains("request"), "{message}");
8251        // The audit record is untouched: nothing pretended to answer it.
8252        assert!(queue
8253            .lock()
8254            .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8255            .outcome
8256            .is_none());
8257    }
8258
8259    /// An id nobody is holding, and a call that names no decision at all,
8260    /// both fail with a message that says why.
8261    #[tokio::test]
8262    async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8263        let mut service = HarnessSessionService::new();
8264        let response = resolve(
8265            &mut service,
8266            json!({"id": "runtime-9/4", "decision": "deny"}),
8267        )
8268        .await;
8269        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8270        assert!(response["error"]["message"]
8271            .as_str()
8272            .unwrap()
8273            .contains("runtime-9/4"));
8274
8275        let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8276        let message = response["error"]["message"].as_str().unwrap();
8277        assert!(
8278            message.contains("allow_once | allow_always | deny"),
8279            "{message}"
8280        );
8281
8282        let response = resolve(
8283            &mut service,
8284            json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8285        )
8286        .await;
8287        assert!(response["error"]["message"]
8288            .as_str()
8289            .unwrap()
8290            .contains("not both"));
8291    }
8292
8293    /// The method is advertised, its SDK operation resolves it, and every
8294    /// harness whose runtime door can carry a request reports it on the
8295    /// CONTROLLED tier beside `runtimes.respond`.
8296    #[test]
8297    fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8298        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8299        assert_eq!(
8300            SdkOperation::from_method("harness.v1.approvals.resolve"),
8301            Some(SdkOperation::ApprovalsResolve)
8302        );
8303        assert_eq!(
8304            SdkOperation::ApprovalsResolve.action_name(),
8305            "approvals_resolve"
8306        );
8307        let registry = harness_support_registry();
8308        for id in [
8309            HarnessId::HERMES,
8310            HarnessId::OPENCLAW,
8311            HarnessId::CODEX,
8312            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8313            // pending_request concept joins the other driven doors.
8314            HarnessId::CLAUDE_CODE,
8315        ] {
8316            let concept = registry
8317                .harnesses
8318                .iter()
8319                .find(|harness| harness.id.as_str() == id)
8320                .unwrap()
8321                .orchestration
8322                .concepts
8323                .iter()
8324                .find(|concept| concept.concept == "pending_request")
8325                .unwrap();
8326            assert_eq!(
8327                concept.controlled,
8328                crate::ImplementationKind::BuiltIn,
8329                "{id}"
8330            );
8331            assert!(
8332                concept
8333                    .methods
8334                    .iter()
8335                    .any(|method| method == "harness.v1.approvals.resolve"),
8336                "{id}"
8337            );
8338        }
8339    }
8340
8341    #[test]
8342    fn capabilities_are_explicit_and_versioned() {
8343        let mut service = HarnessSessionService::new();
8344        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8345        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8346        assert_eq!(
8347            response["result"]["sdk"]["schema_version"],
8348            crate::SDK_SCHEMA_VERSION
8349        );
8350        assert_eq!(
8351            response["result"]["sdk"]["operations"]
8352                .as_array()
8353                .unwrap()
8354                .len(),
8355            SdkOperation::ALL.len()
8356        );
8357        assert_eq!(
8358            response["result"]["harnesses"].as_array().unwrap().len(),
8359            11
8360        );
8361        assert!(response["result"]["harnesses"]
8362            .as_array()
8363            .unwrap()
8364            .iter()
8365            .any(|harness| harness == HarnessId::GROK));
8366        assert!(response["result"]["harnesses"]
8367            .as_array()
8368            .unwrap()
8369            .iter()
8370            .any(|harness| harness == HarnessId::GOOSE));
8371    }
8372
8373    #[test]
8374    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8375        let noisy_stderr = crate::HarnessEvent {
8376            sequence: None,
8377            kind: "transport_stderr".into(),
8378            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8379        };
8380        assert_eq!(handshake_event_failure(&noisy_stderr), None);
8381
8382        let closed = crate::HarnessEvent {
8383            sequence: None,
8384            kind: "transport_closed".into(),
8385            payload: json!({}),
8386        };
8387        assert!(handshake_event_failure(&closed).is_some());
8388    }
8389
8390    #[tokio::test]
8391    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8392        let mut service = HarnessSessionService::new();
8393        service
8394            .runtimes
8395            .insert("raw-eof".into(), ending_runtime(None));
8396        service.runtimes.insert(
8397            "explicit-close".into(),
8398            ending_runtime(Some(HarnessEvent {
8399                sequence: None,
8400                kind: "transport_closed".into(),
8401                payload: json!({"message": "native transport exited"}),
8402            })),
8403        );
8404
8405        let notifications = service.poll_runtimes().await;
8406
8407        assert_eq!(notifications.len(), 2);
8408        assert!(notifications
8409            .iter()
8410            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8411        assert!(notifications.iter().all(|notification| {
8412            notification["params"]["session_id"] == "ending-session"
8413                && notification["params"]["connection"].is_string()
8414        }));
8415        let mut sequences = notifications
8416            .iter()
8417            .filter_map(|notification| notification["params"]["sequence"].as_u64())
8418            .collect::<Vec<_>>();
8419        sequences.sort_unstable();
8420        assert_eq!(sequences, vec![1, 2]);
8421        assert!(service.runtimes.is_empty());
8422    }
8423
8424    #[test]
8425    fn support_report_and_grok_default_binding_share_the_registry() {
8426        let mut service = HarnessSessionService::new();
8427        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8428        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8429        let params = RuntimeBackendParams {
8430            harness: HarnessId::from(HarnessId::GROK),
8431            protocol: None,
8432            launch: None,
8433            base_url: None,
8434            policy: RuntimePolicy::Default,
8435        };
8436        let backend = match runtime_backend(&params) {
8437            Ok(backend) => backend,
8438            Err(_) => panic!("Grok should bind through its registered ACP launch"),
8439        };
8440        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8441        assert!(backend.capabilities().start_session);
8442        let registered = harness_support_registry()
8443            .harnesses
8444            .into_iter()
8445            .find(|harness| harness.id.as_str() == HarnessId::GROK)
8446            .and_then(|harness| harness.runtime.default_launch)
8447            .unwrap();
8448        assert!(!registered
8449            .arguments
8450            .iter()
8451            .any(|argument| argument == "--always-approve"));
8452        assert!(runtime_launch(&params).is_none());
8453
8454        let yolo = RuntimeBackendParams {
8455            policy: RuntimePolicy::Yolo,
8456            ..params
8457        };
8458        assert!(runtime_launch(&yolo)
8459            .unwrap()
8460            .arguments
8461            .iter()
8462            .any(|argument| argument == "--always-approve"));
8463
8464        let mismatched_protocol = RuntimeBackendParams {
8465            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8466            protocol: Some("acp".into()),
8467            launch: None,
8468            base_url: None,
8469            policy: RuntimePolicy::Default,
8470        };
8471        assert!(runtime_backend(&mismatched_protocol).is_err());
8472    }
8473
8474    #[test]
8475    fn load_follow_and_unfollow_share_the_same_locator() {
8476        let mut service = HarnessSessionService::new();
8477        let locator = pi_locator();
8478        let loaded = service.handle(request(
8479            1,
8480            "harness.v1.sessions.load",
8481            json!({"locator": locator}),
8482        ));
8483        assert_eq!(
8484            loaded["result"]["session"]["session_id"],
8485            locator.session_id
8486        );
8487
8488        let followed = service.handle(request(
8489            2,
8490            "harness.v1.sessions.follow",
8491            json!({"locator": locator}),
8492        ));
8493        assert_eq!(followed["result"]["subscription"], "sub-1");
8494        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8495        assert!(service.poll().is_empty());
8496
8497        let unfollowed = service.handle(request(
8498            3,
8499            "harness.v1.sessions.unfollow",
8500            json!({"subscription": "sub-1"}),
8501        ));
8502        assert_eq!(unfollowed["result"]["removed"], true);
8503    }
8504
8505    #[test]
8506    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8507        let temp = std::env::temp_dir().join(format!(
8508            "supercode-bounded-view-{}-{}",
8509            std::process::id(),
8510            generated_session_id()
8511        ));
8512        let path = temp.join("parent.jsonl");
8513        let subagents = temp.join("parent/subagents");
8514        std::fs::create_dir_all(&subagents).unwrap();
8515        let long_last = "x".repeat(300);
8516        let parent_records = [
8517            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8518            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8519            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8520        ];
8521        std::fs::write(
8522            &path,
8523            format!(
8524                "{}\n",
8525                parent_records
8526                    .iter()
8527                    .map(Value::to_string)
8528                    .collect::<Vec<_>>()
8529                    .join("\n")
8530            ),
8531        )
8532        .unwrap();
8533        std::fs::write(
8534            subagents.join("agent-child.jsonl"),
8535            concat!(
8536                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8537                "\n",
8538            ),
8539        )
8540        .unwrap();
8541        let locator = SessionLocator {
8542            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8543            session_id: "parent".into(),
8544            storage: StorageLocator::File { path },
8545        };
8546        let mut service = HarnessSessionService::new();
8547
8548        let complete = service.handle(request(
8549            1,
8550            "harness.v1.sessions.load",
8551            json!({"locator": locator}),
8552        ));
8553        assert_eq!(
8554            complete["result"]["session"]["subagents"]
8555                .as_array()
8556                .unwrap()
8557                .len(),
8558            1
8559        );
8560
8561        let bounded = service.handle(request(
8562            2,
8563            "harness.v1.sessions.load",
8564            json!({
8565                "locator": locator,
8566                "view": {
8567                    "tail_messages": 1,
8568                    "max_message_chars": 256,
8569                    "include_subagents": false
8570                },
8571            }),
8572        ));
8573        let session = &bounded["result"]["session"];
8574        assert!(session["subagents"].as_array().unwrap().is_empty());
8575        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8576        assert_eq!(
8577            session["messages"][0]["content"],
8578            format!("{}\n…", "x".repeat(256))
8579        );
8580
8581        let followed = service.handle(request(
8582            3,
8583            "harness.v1.sessions.follow",
8584            json!({
8585                "locator": locator,
8586                "view": {
8587                    "tail_messages": 1,
8588                    "max_message_chars": 256,
8589                    "include_subagents": false
8590                },
8591            }),
8592        ));
8593        let initial = &followed["result"]["initial"]["session"];
8594        assert!(initial["subagents"].as_array().unwrap().is_empty());
8595        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8596
8597        let _ = std::fs::remove_dir_all(&temp);
8598    }
8599
8600    #[test]
8601    fn forty_megabyte_display_load_is_bounded_and_prompt() {
8602        let temp = std::env::temp_dir().join(format!(
8603            "supercode-large-display-view-{}-{}",
8604            std::process::id(),
8605            generated_session_id()
8606        ));
8607        std::fs::create_dir_all(&temp).unwrap();
8608        let path = temp.join("rollout.jsonl");
8609        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8610        writeln!(
8611            file,
8612            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8613        )
8614        .unwrap();
8615        let padding = "x".repeat(80 * 1024);
8616        for index in 0..512 {
8617            let marker = if index == 0 {
8618                "OLDEST-SHOULD-NOT-LOAD"
8619            } else if index == 511 {
8620                "LATEST-MUST-LOAD"
8621            } else {
8622                "bulk"
8623            };
8624            writeln!(
8625                file,
8626                "{}",
8627                json!({
8628                    "timestamp": "2026-01-01T00:00:01Z",
8629                    "type": "response_item",
8630                    "payload": {
8631                        "type": "message",
8632                        "role": "assistant",
8633                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8634                    },
8635                })
8636            )
8637            .unwrap();
8638        }
8639        file.flush().unwrap();
8640        drop(file);
8641        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8642
8643        let locator = SessionLocator {
8644            harness: HarnessId::from(HarnessId::CODEX),
8645            session_id: "large-display".into(),
8646            storage: StorageLocator::File { path },
8647        };
8648        let started = Instant::now();
8649        let response = HarnessSessionService::new().handle(request(
8650            1,
8651            "harness.v1.sessions.load",
8652            json!({
8653                "locator": locator,
8654                "view": {
8655                    "tail_messages": 500,
8656                    "max_message_chars": 1024,
8657                    "include_subagents": false,
8658                    "display_history": true,
8659                },
8660            }),
8661        ));
8662        let elapsed = started.elapsed();
8663        let wire = response.to_string();
8664        eprintln!(
8665            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8666            wire.len()
8667        );
8668        assert!(response.get("error").is_none(), "{response:#}");
8669        assert!(wire.contains("LATEST-MUST-LOAD"));
8670        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8671        assert!(
8672            wire.len() < 2 * 1024 * 1024,
8673            "bounded wire was {} bytes",
8674            wire.len()
8675        );
8676        assert!(
8677            elapsed.as_secs_f64() < 3.0,
8678            "bounded 40 MiB load took {elapsed:?}"
8679        );
8680
8681        let _ = std::fs::remove_dir_all(&temp);
8682    }
8683
8684    #[test]
8685    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8686        let temp = std::env::temp_dir().join(format!(
8687            "supercode-large-goose-view-{}-{}",
8688            std::process::id(),
8689            generated_session_id()
8690        ));
8691        std::fs::create_dir_all(&temp).unwrap();
8692        let path = temp.join("sessions.db");
8693        let connection = rusqlite::Connection::open(&path).unwrap();
8694        connection
8695            .execute_batch(
8696                "CREATE TABLE sessions (
8697                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8698                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8699                    session_type TEXT NOT NULL, extension_data TEXT,
8700                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8701                    archived_at TEXT
8702                 );
8703                 CREATE TABLE messages (
8704                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8705                    role TEXT NOT NULL, content_json TEXT NOT NULL,
8706                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
8707                 );",
8708            )
8709            .unwrap();
8710        connection
8711            .execute(
8712                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8713                rusqlite::params![
8714                    "goose-large",
8715                    "Large Goose session",
8716                    "/tmp",
8717                    "2026-01-01 00:00:00",
8718                    "2026-01-01 00:00:02",
8719                    "user",
8720                    "{}",
8721                    "auto",
8722                    "anthropic",
8723                    r#"{"model_name":"claude-sonnet"}"#,
8724                ],
8725            )
8726            .unwrap();
8727        let old_content = serde_json::to_string(&vec![json!({
8728            "type": "text",
8729            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8730        })])
8731        .unwrap();
8732        connection
8733            .execute(
8734                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8735                rusqlite::params!["goose-large", old_content],
8736            )
8737            .unwrap();
8738        connection
8739            .execute(
8740                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8741                rusqlite::params![
8742                    "goose-large",
8743                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8744                ],
8745            )
8746            .unwrap();
8747        drop(connection);
8748        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8749
8750        let locator = SessionLocator {
8751            harness: HarnessId::from(HarnessId::GOOSE),
8752            session_id: "goose-large".into(),
8753            storage: StorageLocator::Sqlite {
8754                path,
8755                selector: "goose-large".into(),
8756            },
8757        };
8758        let started = Instant::now();
8759        let response = HarnessSessionService::new().handle(request(
8760            1,
8761            "harness.v1.sessions.load",
8762            json!({
8763                "locator": locator,
8764                "view": {
8765                    "tail_messages": 1,
8766                    "max_message_chars": 1024,
8767                    "include_subagents": false,
8768                    "display_history": true,
8769                },
8770            }),
8771        ));
8772        let elapsed = started.elapsed();
8773        let wire = response.to_string();
8774        eprintln!(
8775            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8776            wire.len()
8777        );
8778        assert!(response.get("error").is_none(), "{response:#}");
8779        assert!(wire.contains("LATEST-MUST-LOAD"));
8780        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8781        assert!(
8782            wire.len() < 64 * 1024,
8783            "bounded wire was {} bytes",
8784            wire.len()
8785        );
8786        assert!(
8787            elapsed.as_secs_f64() < 1.0,
8788            "bounded Goose load took {elapsed:?}"
8789        );
8790
8791        let _ = std::fs::remove_dir_all(&temp);
8792    }
8793
8794    #[test]
8795    fn display_view_keeps_codex_assistant_history_across_compaction() {
8796        let temp = std::env::temp_dir().join(format!(
8797            "supercode-codex-display-view-{}-{}",
8798            std::process::id(),
8799            generated_session_id()
8800        ));
8801        std::fs::create_dir_all(&temp).unwrap();
8802        let path = temp.join("rollout.jsonl");
8803        std::fs::write(
8804            &path,
8805            concat!(
8806                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8807                "\n",
8808                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8809                "\n",
8810                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8811                "\n",
8812                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"}]}}"#,
8813                "\n",
8814                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8815                "\n",
8816                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8817                "\n",
8818            ),
8819        )
8820        .unwrap();
8821        let locator = SessionLocator {
8822            harness: HarnessId::from(HarnessId::CODEX),
8823            session_id: "codex-display".into(),
8824            storage: StorageLocator::File { path },
8825        };
8826        let mut service = HarnessSessionService::new();
8827
8828        let continuation = service.handle(request(
8829            1,
8830            "harness.v1.sessions.load",
8831            json!({"locator": locator}),
8832        ));
8833        let continuation_text = continuation["result"]["session"]["messages"].to_string();
8834        assert!(!continuation_text.contains("old answer"));
8835
8836        let display = service.handle(request(
8837            2,
8838            "harness.v1.sessions.load",
8839            json!({
8840                "locator": locator,
8841                "view": {
8842                    "tail_messages": 10,
8843                    "include_subagents": false,
8844                    "display_history": true,
8845                },
8846            }),
8847        ));
8848        let display_text = display["result"]["session"]["messages"].to_string();
8849        assert!(display_text.contains("old prompt"));
8850        assert!(display_text.contains("old answer"));
8851        assert!(display_text.contains("new prompt"));
8852        assert!(display_text.contains("new answer"));
8853
8854        let _ = std::fs::remove_dir_all(&temp);
8855    }
8856
8857    #[test]
8858    fn indexed_claude_windows_match_the_existing_wire_projection() {
8859        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8860            .join("tests/fixtures/claude_code_session.jsonl");
8861        let locator = SessionLocator {
8862            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8863            session_id: "fixture".into(),
8864            storage: StorageLocator::File { path },
8865        };
8866        let full = load_session(&locator).unwrap();
8867        for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8868            for offset in [0, 1, full.messages.len(), usize::MAX] {
8869                for limit in [0, 1, 3, usize::MAX] {
8870                    let options = SessionLoadOptions {
8871                        include_subagents: Some(false),
8872                        inline_media,
8873                        message_offset: Some(offset),
8874                        message_limit: Some(limit),
8875                        ..Default::default()
8876                    };
8877                    let expected = projected_session_result(&full, &options);
8878                    assert_eq!(
8879                        indexed_claude_window(&locator, &options).unwrap().unwrap(),
8880                        expected
8881                    );
8882                }
8883            }
8884            for tail in [0, 1, 3, usize::MAX] {
8885                let options = SessionLoadOptions {
8886                    include_subagents: Some(false),
8887                    inline_media,
8888                    message_tail: Some(tail),
8889                    ..Default::default()
8890                };
8891                assert_eq!(
8892                    indexed_claude_window(&locator, &options).unwrap().unwrap(),
8893                    projected_session_result(&full, &options)
8894                );
8895            }
8896        }
8897    }
8898
8899    #[test]
8900    fn load_supports_bounded_windows_and_media_metadata() {
8901        let mut service = HarnessSessionService::new();
8902        let locator = pi_locator();
8903        let bounded = service.handle(request(
8904            1,
8905            "harness.v1.sessions.load",
8906            json!({
8907                "locator": locator,
8908                "options": {
8909                    "include_subagents": false,
8910                    "message_limit": 2,
8911                    "message_offset": 1
8912                }
8913            }),
8914        ));
8915        assert_eq!(bounded["result"]["window"]["offset"], 1);
8916        assert_eq!(bounded["result"]["window"]["returned"], 2);
8917        assert!(bounded["result"]["summary"]["first_message"].is_object());
8918        assert!(bounded["result"]["summary"]["last_message"].is_object());
8919        assert_eq!(
8920            bounded["result"]["session"]["messages"]
8921                .as_array()
8922                .unwrap()
8923                .len(),
8924            2
8925        );
8926        assert!(bounded["result"]["session"]["subagents"]
8927            .as_array()
8928            .unwrap()
8929            .is_empty());
8930
8931        let tail = service.handle(request(
8932            2,
8933            "harness.v1.sessions.load",
8934            json!({"locator": locator, "options": {"message_tail": 1}}),
8935        ));
8936        assert_eq!(tail["result"]["window"]["returned"], 1);
8937        assert_eq!(tail["result"]["window"]["has_more"], true);
8938        assert_eq!(tail["result"]["window"]["has_older"], true);
8939        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8940        assert!(tail["result"]["summary"]["first_message"].is_object());
8941
8942        let metadata_only = service.handle(request(
8943            3,
8944            "harness.v1.sessions.load",
8945            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8946        ));
8947        assert!(metadata_only["result"]["session"]
8948            .to_string()
8949            .contains("media_reference"));
8950        assert!(!metadata_only["result"]["session"]
8951            .to_string()
8952            .contains("data:image/"));
8953    }
8954
8955    #[test]
8956    fn import_translate_branch_and_handoff_use_typed_artifacts() {
8957        let mut service = HarnessSessionService::new();
8958        let locator = pi_locator();
8959        let translated = service.handle(request(
8960            1,
8961            "harness.v1.sessions.translate",
8962            json!({"locator": locator, "target_harness": "grok"}),
8963        ));
8964        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8965        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8966        assert!(translated["result"]["artifact"]["content"]
8967            .as_str()
8968            .is_some_and(|content| !content.is_empty()));
8969
8970        for target in ["opencode", "open-code"] {
8971            let opencode = service.handle(request(
8972                6,
8973                "harness.v1.sessions.translate",
8974                json!({"locator": locator, "target_harness": target}),
8975            ));
8976            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8977        }
8978        let goose = service.handle(request(
8979            7,
8980            "harness.v1.sessions.translate",
8981            json!({"locator": locator, "target_harness": "goose"}),
8982        ));
8983        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8984        assert!(serde_json::from_str::<Value>(
8985            goose["result"]["artifact"]["content"].as_str().unwrap()
8986        )
8987        .unwrap()["conversation"]
8988            .is_array());
8989
8990        let imported = service.handle(request(
8991            2,
8992            "harness.v1.sessions.import",
8993            json!({
8994                "source_harness": "grok",
8995                "content": translated["result"]["artifact"]["content"],
8996            }),
8997        ));
8998        assert_eq!(imported["result"]["session"]["source"], "grok");
8999
9000        let branched = service.handle(request(
9001            3,
9002            "harness.v1.sessions.branch",
9003            json!({"locator": locator, "target_harness": "codex"}),
9004        ));
9005        assert_eq!(branched["result"]["parent"]["harness"], "pi");
9006        assert!(branched["result"]["bootstrap_prompt"]
9007            .as_str()
9008            .unwrap()
9009            .contains("frozen parent transcript"));
9010        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
9011
9012        let handoff = service.handle(request(
9013            4,
9014            "harness.v1.sessions.handoff",
9015            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
9016        ));
9017        assert_eq!(handoff["result"]["launch"]["program"], "pi");
9018        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
9019        assert_eq!(handoff["result"]["requires_materialization"], true);
9020
9021        let goose_handoff = service.handle(request(
9022            8,
9023            "harness.v1.sessions.handoff",
9024            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
9025        ));
9026        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
9027        assert_eq!(
9028            goose_handoff["result"]["materialize"]["arguments"],
9029            json!(["session", "import", "{artifact_path}"])
9030        );
9031
9032        let resumed = service.handle(request(
9033            5,
9034            "harness.v1.sessions.resume_instructions",
9035            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
9036        ));
9037        assert_eq!(resumed["result"]["launch"]["program"], "pi");
9038        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
9039    }
9040
9041    #[test]
9042    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
9043        let temp = std::env::temp_dir().join(format!(
9044            "supercode-service-reduce-{}-{}",
9045            std::process::id(),
9046            generated_session_id()
9047        ));
9048        let source_path = temp.join("source.jsonl");
9049        let store_root = temp.join("store");
9050        std::fs::create_dir_all(&temp).unwrap();
9051
9052        let mut records = vec![json!({
9053            "timestamp": "2026-01-01T00:00:00Z",
9054            "type": "session_meta",
9055            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9056        })];
9057        for turn in 0..16 {
9058            records.push(json!({
9059                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9060                "type": "response_item",
9061                "payload": {
9062                    "type": "message",
9063                    "role": "user",
9064                    "content": [{
9065                        "type": "input_text",
9066                        "text": format!("request {turn}: {}", "context ".repeat(80)),
9067                    }],
9068                },
9069            }));
9070            records.push(json!({
9071                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9072                "type": "response_item",
9073                "payload": {
9074                    "type": "message",
9075                    "role": "assistant",
9076                    "content": [{
9077                        "type": "output_text",
9078                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9079                    }],
9080                },
9081            }));
9082        }
9083        let source = format!(
9084            "{}\n",
9085            records
9086                .iter()
9087                .map(Value::to_string)
9088                .collect::<Vec<_>>()
9089                .join("\n")
9090        );
9091        std::fs::write(&source_path, &source).unwrap();
9092        let locator = SessionLocator {
9093            harness: HarnessId::from(HarnessId::CODEX),
9094            session_id: "codex-reduce".into(),
9095            storage: StorageLocator::File {
9096                path: source_path.clone(),
9097            },
9098        };
9099        let original = load_session(&locator).unwrap();
9100        let mut service =
9101            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9102
9103        let response = service.handle(request(
9104            1,
9105            "harness.v1.sessions.reduce",
9106            json!({
9107                "locator": locator,
9108                "target_harness": "claude-code",
9109                "keep_last": 4,
9110            }),
9111        ));
9112        assert!(response.get("error").is_none(), "{response:#}");
9113        let receipt = &response["result"]["receipt"];
9114        assert_eq!(receipt["source_harness"], "codex");
9115        assert_eq!(receipt["target_harness"], "claude-code");
9116        assert_eq!(receipt["verified"], true);
9117        assert_eq!(receipt["reversible"], true);
9118        assert!(receipt["reductions"].as_u64().unwrap() > 0);
9119        assert!(
9120            receipt["source_tokens"].as_u64().unwrap()
9121                > receipt["reduced_tokens"].as_u64().unwrap()
9122        );
9123        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9124        assert!(response["result"]["bootstrap_prompt"]
9125            .as_str()
9126            .unwrap()
9127            .contains("Do not guess hidden content"));
9128
9129        let rescue_id = receipt["id"].as_str().unwrap();
9130        let store = crate::SessionStore::open(&store_root).unwrap();
9131        let sidecar =
9132            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9133        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9134        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9135        let policy = reduce::ReductionPolicy {
9136            clear_turns_older_than: Some(4),
9137            ..Default::default()
9138        };
9139        let (restamped_view, reapplied_log) =
9140            reduce::project_messages(&sidecar.messages, &policy, &log);
9141        assert_eq!(
9142            messages_jsonl(&persisted_view).unwrap(),
9143            messages_jsonl(&restamped_view).unwrap()
9144        );
9145        assert_eq!(reapplied_log, log);
9146        reduce::verify_log(&log, &sidecar).unwrap();
9147        assert_eq!(
9148            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9149            original.messages
9150        );
9151        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9152
9153        std::fs::remove_dir_all(temp).ok();
9154    }
9155
9156    #[test]
9157    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9158        let temp = std::env::temp_dir().join(format!(
9159            "supercode-severed-view-{}-{}",
9160            std::process::id(),
9161            generated_session_id()
9162        ));
9163        std::fs::create_dir_all(&temp).unwrap();
9164        let path = temp.join("severed.jsonl");
9165        // A live record whose parent was pruned — what a compacted or
9166        // resumed-across-files Claude Code session looks like on disk.
9167        std::fs::write(
9168            &path,
9169            concat!(
9170                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9171                "\n",
9172                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9173                "\n",
9174            ),
9175        )
9176        .unwrap();
9177        let locator = SessionLocator {
9178            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9179            session_id: "severed".into(),
9180            storage: StorageLocator::File { path },
9181        };
9182        let mut service = HarnessSessionService::new();
9183
9184        let viewed = service.handle(request(
9185            1,
9186            "harness.v1.sessions.load",
9187            json!({"locator": locator}),
9188        ));
9189        let session = &viewed["result"]["session"];
9190        assert_eq!(session["fidelity"], "semantic");
9191        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9192        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9193            entry
9194                .as_str()
9195                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9196        }));
9197
9198        // Asking a READ surface for a lossless reconstruction gets the strict
9199        // refusal back, unchanged.
9200        let strict = service.handle(request(
9201            2,
9202            "harness.v1.sessions.load",
9203            json!({"locator": locator, "fidelity": "byte_lossless"}),
9204        ));
9205        assert!(strict["error"]["message"]
9206            .as_str()
9207            .unwrap()
9208            .contains("cannot reconstruct lossless Claude continuation"));
9209
9210        // Transfer/continuation surfaces have no view mode at all.
9211        let translated = service.handle(request(
9212            3,
9213            "harness.v1.sessions.translate",
9214            json!({"locator": locator, "target_harness": "codex"}),
9215        ));
9216        assert!(translated["error"]["message"]
9217            .as_str()
9218            .unwrap()
9219            .contains("cannot reconstruct lossless Claude continuation"));
9220        let resumed = service.handle(request(
9221            4,
9222            "harness.v1.sessions.resume_instructions",
9223            json!({"locator": locator}),
9224        ));
9225        assert!(resumed["error"]["message"]
9226            .as_str()
9227            .unwrap()
9228            .contains("cannot reconstruct lossless Claude continuation"));
9229
9230        let _ = std::fs::remove_dir_all(&temp);
9231    }
9232
9233    #[test]
9234    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9235        let codex = resume_launch(
9236            HarnessId::CODEX,
9237            "codex-session",
9238            Path::new("/tmp/project"),
9239            ResumePolicy::Yolo,
9240        )
9241        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9242        assert_eq!(codex.program, "codex");
9243        assert_eq!(
9244            codex.arguments,
9245            [
9246                "-c",
9247                "check_for_update_on_startup=false",
9248                "-c",
9249                "projects.\"/tmp/project\".trust_level=\"trusted\"",
9250                "--dangerously-bypass-approvals-and-sandbox",
9251                "--dangerously-bypass-hook-trust",
9252                "resume",
9253                "codex-session",
9254            ]
9255        );
9256
9257        let gemini = resume_launch(
9258            HarnessId::GEMINI,
9259            "gemini-session",
9260            Path::new("/tmp/project"),
9261            ResumePolicy::Yolo,
9262        )
9263        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9264        assert_eq!(gemini.program, "gemini");
9265        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9266
9267        let goose = resume_launch(
9268            HarnessId::GOOSE,
9269            "goose-session",
9270            Path::new("/tmp/project"),
9271            ResumePolicy::Yolo,
9272        )
9273        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9274        assert_eq!(goose.program, "goose");
9275        assert_eq!(
9276            goose.arguments,
9277            ["session", "--resume", "--session-id", "goose-session"]
9278        );
9279
9280        let supercode = resume_launch(
9281            HarnessId::SUPERCODE,
9282            "supercode-session",
9283            Path::new("/tmp/project"),
9284            ResumePolicy::Yolo,
9285        )
9286        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9287        assert_eq!(supercode.program, "supercode");
9288        assert_eq!(
9289            supercode.arguments,
9290            ["--dangerous", "resume", "supercode-session"]
9291        );
9292    }
9293
9294    #[test]
9295    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9296        let temp = std::env::temp_dir().join(format!(
9297            "supercode-harness-artifact-{}-{}",
9298            std::process::id(),
9299            generated_session_id()
9300        ));
9301        let main_path = temp.join("parent.jsonl");
9302        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9303        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9304        let fixture = std::fs::read_to_string(
9305            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9306                .join("tests/fixtures/claude_code_session.jsonl"),
9307        )
9308        .unwrap();
9309        let parent = fixture.trim_end_matches('\n');
9310        let child = fixture.trim_end_matches('\n');
9311        std::fs::write(&main_path, parent).unwrap();
9312        std::fs::write(&subagent_path, child).unwrap();
9313        let locator = SessionLocator {
9314            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9315            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9316            storage: StorageLocator::File {
9317                path: main_path.clone(),
9318            },
9319        };
9320        let mut service = HarnessSessionService::new();
9321        let claude = service.handle(request(
9322            1,
9323            "harness.v1.sessions.translate",
9324            json!({"locator": locator, "target_harness": "claude-code"}),
9325        ));
9326        let artifact = &claude["result"]["artifact"];
9327        assert_eq!(artifact["fidelity"], "byte_lossless");
9328        assert_eq!(artifact["content"], parent);
9329        let files = artifact["files"].as_array().unwrap();
9330        assert!(files.iter().any(|file| {
9331            file["role"] == "subagent"
9332                && file["path"]
9333                    .as_str()
9334                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9335                && file["content"] == child
9336        }));
9337        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9338
9339        let grok = service.handle(request(
9340            2,
9341            "harness.v1.sessions.translate",
9342            json!({"locator": grok_locator(), "target_harness": "grok"}),
9343        ));
9344        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9345        for name in ["summary.json", "updates.jsonl"] {
9346            let expected = std::fs::read_to_string(
9347                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9348                    .join("tests/fixtures/grok_session")
9349                    .join(name),
9350            )
9351            .unwrap();
9352            assert!(files.iter().any(|file| {
9353                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9354            }));
9355        }
9356        std::fs::remove_dir_all(temp).ok();
9357    }
9358
9359    #[test]
9360    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9361        let mut service = HarnessSessionService::new();
9362        let source = pi_locator();
9363        for (target, format) in [
9364            ("claude-code", SessionFormat::ClaudeCode),
9365            ("codex", SessionFormat::Codex),
9366            ("opencode", SessionFormat::OpenCode),
9367            ("pi", SessionFormat::Pi),
9368        ] {
9369            let result = service.handle(request(
9370                1,
9371                "harness.v1.sessions.handoff",
9372                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9373            ));
9374            let artifact = &result["result"]["artifact"];
9375            let target_id = artifact["session_id"].as_str().unwrap();
9376            assert_ne!(target_id, source.session_id, "{target}");
9377            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9378            assert_eq!(
9379                parsed.meta.session_id.as_deref(),
9380                Some(target_id),
9381                "{target}"
9382            );
9383            if target != "pi" {
9384                assert!(result["result"]["launch"]["arguments"]
9385                    .as_array()
9386                    .unwrap()
9387                    .iter()
9388                    .any(|argument| argument == target_id));
9389            }
9390            if target == "opencode" {
9391                assert!(target_id.starts_with("ses_"));
9392                fn assert_session_ids(value: &Value, target_id: &str) {
9393                    match value {
9394                        Value::Object(fields) => {
9395                            if let Some(session_id) = fields.get("sessionID") {
9396                                assert_eq!(session_id, target_id);
9397                            }
9398                            for child in fields.values() {
9399                                assert_session_ids(child, target_id);
9400                            }
9401                        }
9402                        Value::Array(values) => {
9403                            for child in values {
9404                                assert_session_ids(child, target_id);
9405                            }
9406                        }
9407                        _ => {}
9408                    }
9409                }
9410                let document: Value =
9411                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9412                assert_session_ids(&document, target_id);
9413            }
9414        }
9415
9416        let first = service.handle(request(
9417            2,
9418            "harness.v1.sessions.handoff",
9419            json!({"locator": source, "target_harness": "codex"}),
9420        ));
9421        let second = service.handle(request(
9422            3,
9423            "harness.v1.sessions.handoff",
9424            json!({"locator": source, "target_harness": "codex"}),
9425        ));
9426        assert_ne!(
9427            first["result"]["artifact"]["session_id"],
9428            second["result"]["artifact"]["session_id"]
9429        );
9430    }
9431
9432    #[test]
9433    fn grok_handoff_uses_the_official_importer_contract() {
9434        let mut service = HarnessSessionService::new();
9435        let source = opencode_locator();
9436        let response = service.handle(request(
9437            1,
9438            "harness.v1.sessions.handoff",
9439            json!({
9440                "locator": source,
9441                "target_harness": "grok",
9442                "cwd": "/tmp/grok-handoff-project",
9443            }),
9444        ));
9445        let result = &response["result"];
9446
9447        // The target is Grok, but the artifact truthfully names the Claude Code wire
9448        // format accepted by Grok's official importer. Raw Grok chat_history JSONL is
9449        // not a complete stock-resumable bundle.
9450        assert_eq!(result["artifact"]["target_harness"], "claude-code");
9451        assert!(result["artifact"]["suggested_filename"]
9452            .as_str()
9453            .unwrap()
9454            .ends_with(".grok-import.claude-code.jsonl"));
9455        let artifact = Session::load_str(
9456            result["artifact"]["content"].as_str().unwrap(),
9457            SessionFormat::ClaudeCode,
9458        )
9459        .unwrap();
9460        assert_eq!(
9461            artifact.meta.cwd.as_deref(),
9462            Some(Path::new("/tmp/grok-handoff-project"))
9463        );
9464        let target_session_id = artifact.meta.session_id.as_deref().unwrap();
9465        assert_eq!(target_session_id.len(), 36);
9466        assert_eq!(target_session_id.as_bytes()[14], b'4');
9467        assert_ne!(target_session_id, opencode_locator().session_id);
9468        assert_eq!(
9469            result["artifact"]["session_id"],
9470            artifact.meta.session_id.as_deref().unwrap()
9471        );
9472
9473        assert_eq!(
9474            result["materialize"]["arguments"],
9475            json!(["import", "--json", "{artifact_path}"])
9476        );
9477        assert_eq!(
9478            result["launch"]["arguments"],
9479            json!(["--resume", "{imported_session_id}", "--fork-session"])
9480        );
9481        assert!(result["note"]
9482            .as_str()
9483            .unwrap()
9484            .contains("outcome=imported"));
9485        assert!(!result["launch"]["arguments"]
9486            .as_array()
9487            .unwrap()
9488            .iter()
9489            .any(|argument| argument == &opencode_locator().session_id));
9490    }
9491
9492    #[tokio::test]
9493    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9494        let mut service = HarnessSessionService::new();
9495        let inventory = service
9496            .handle_async(request(
9497                1,
9498                "harness.v1.harnesses.list",
9499                json!({"harnesses": ["missing"]}),
9500            ))
9501            .await;
9502        assert_eq!(inventory["error"]["code"], -32602);
9503
9504        let attached = service
9505            .handle_async(request(
9506                2,
9507                "harness.v1.runtimes.attach_existing",
9508                json!({"harness": "codex", "runtime_id": "thread-1"}),
9509            ))
9510            .await;
9511        assert_eq!(attached["error"]["code"], -32000);
9512        assert!(attached["error"]["message"]
9513            .as_str()
9514            .unwrap()
9515            .contains("runtimes.resume"));
9516    }
9517
9518    #[test]
9519    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9520        let mut service = HarnessSessionService::new();
9521        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9522        assert_eq!(invalid["error"]["code"], -32602);
9523        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9524        assert_eq!(unknown["error"]["code"], -32601);
9525    }
9526
9527    #[cfg(unix)]
9528    #[tokio::test]
9529    // The test mutates process-wide harness environment and deliberately
9530    // holds the global test lock until every async runtime operation ends.
9531    #[allow(clippy::await_holding_lock)]
9532    async fn async_service_drives_a_generic_acp_runtime() {
9533        let _environment_guard = crate::live_runtime::test_environment_lock();
9534        let script = r#"
9535            i=0
9536            while IFS= read -r line; do
9537              i=$((i + 1))
9538              case "$i" in
9539                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9540                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9541                3)
9542                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9543                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9544                  ;;
9545                4)
9546                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9547                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9548                  ;;
9549              esac
9550            done
9551        "#;
9552        let mut service = HarnessSessionService::new();
9553        let started = service
9554            .handle_async(request(
9555                1,
9556                "harness.v1.runtimes.start",
9557                json!({
9558                    "harness": "codex",
9559                    "protocol": "acp",
9560                    "cwd": std::env::current_dir().unwrap(),
9561                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9562                }),
9563            ))
9564            .await;
9565        assert_eq!(started["result"]["connection"], "runtime-1");
9566        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9567
9568        let terminal = service
9569            .handle_async(request(
9570                9,
9571                "harness.v1.runtimes.terminal_instructions",
9572                json!({"connection":"runtime-1"}),
9573            ))
9574            .await;
9575        let arguments = terminal["result"]["launch"]["arguments"]
9576            .as_array()
9577            .expect("hosted runtime should return terminal arguments");
9578        let endpoint_index = arguments
9579            .iter()
9580            .position(|value| value == "--endpoint")
9581            .expect("terminal command should use an opaque endpoint");
9582        let endpoint = LiveRuntimeEndpoint::parse(
9583            arguments[endpoint_index + 1]
9584                .as_str()
9585                .expect("endpoint argument should be text"),
9586        )
9587        .unwrap();
9588        assert!(!terminal.to_string().contains("Bearer"));
9589        let workspace = std::env::current_dir().unwrap();
9590        let receipt = resolve_live_runtime(
9591            &endpoint,
9592            &LiveRuntimeSource {
9593                harness: "codex".into(),
9594                session_id: "svc_acp".into(),
9595                workspace,
9596            },
9597        )
9598        .unwrap();
9599        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9600            .await
9601            .unwrap();
9602        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9603            .await
9604            .unwrap();
9605
9606        let sent = service
9607            .handle_async(request(
9608                2,
9609                "harness.v1.runtimes.send_input",
9610                json!({"connection": "runtime-1", "text": "hi"}),
9611            ))
9612            .await;
9613        assert_eq!(sent["result"]["turn_id"], "3");
9614
9615        let mut events = Vec::new();
9616        for _ in 0..20 {
9617            events.extend(service.poll_runtimes().await);
9618            if events.len() >= 2 {
9619                break;
9620            }
9621            tokio::time::sleep(Duration::from_millis(2)).await;
9622        }
9623        assert!(events
9624            .iter()
9625            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9626        assert!(events.iter().any(|event| {
9627            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9628        }));
9629
9630        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9631            loop {
9632                let event = attachment.next_event().await.unwrap();
9633                if event.kind == "text_delta" && event.payload["text"] == "ok" {
9634                    break;
9635                }
9636            }
9637        })
9638        .await;
9639        assert!(
9640            saw_editor_reply.is_ok(),
9641            "terminal should observe the editor-driven turn"
9642        );
9643
9644        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9645            .await
9646            .unwrap();
9647        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9648            loop {
9649                let event = attachment.next_event().await.unwrap();
9650                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9651                    break;
9652                }
9653            }
9654        })
9655        .await;
9656        assert!(
9657            saw_terminal_reply.is_ok(),
9658            "terminal should drive the same runtime"
9659        );
9660
9661        let closed = service
9662            .handle_async(request(
9663                3,
9664                "harness.v1.runtimes.close",
9665                json!({"connection": "runtime-1"}),
9666            ))
9667            .await;
9668        assert_eq!(closed["result"]["closed"], true);
9669    }
9670
9671    /// UNI-7 dev/02: a RUNNING mock gateway is detected through the real
9672    /// openclaw probe (config-declared endpoint, TCP connect), and an ACTIVE
9673    /// hermes WAL is detected through the real WAL-freshness probe; the
9674    /// negative sides (no listener, stale WAL, no config) stay undetected.
9675    #[test]
9676    fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9677        let home = connect_scratch_home("uni7-running");
9678
9679        // No config at all: hermes has no default endpoint, so no detection.
9680        // (openclaw's no-config behavior now probes its DOCUMENTED default
9681        // endpoint ws://127.0.0.1:18789 — see the connect launch's
9682        // `default_address` — which is real box state a hermetic test must
9683        // not assert either way; the closed-port negative below covers the
9684        // no-listener side deterministically.)
9685        assert!(probe_hermes_running(&home, 300_000).is_none());
9686
9687        // Mock gateway: a real TCP listener on an ephemeral port, declared in
9688        // the harness's own config file.
9689        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9690        let port = listener.local_addr().unwrap().port();
9691        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9692        std::fs::write(
9693            home.join(".openclaw/openclaw.json"),
9694            format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9695        )
9696        .unwrap();
9697        let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9698        assert!(matches!(
9699            running.method,
9700            RunningInstanceMethod::GatewayConnect
9701        ));
9702        assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9703        drop(listener);
9704        // Parallel tests also bind ephemeral loopback ports, so a just-freed
9705        // port can be re-bound by a NEIGHBORING test between drop and probe.
9706        // Detection on a closed port must fail — retry on a fresh port when
9707        // the freed one was recycled by someone else.
9708        let mut closed_detected = probe_openclaw_running(&home).is_some();
9709        for _ in 0..3 {
9710            if !closed_detected {
9711                break;
9712            }
9713            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9714            let port = listener.local_addr().unwrap().port();
9715            drop(listener);
9716            std::fs::write(
9717                home.join(".openclaw/openclaw.json"),
9718                format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9719            )
9720            .unwrap();
9721            closed_detected = probe_openclaw_running(&home).is_some();
9722        }
9723        assert!(
9724            !closed_detected,
9725            "a closed gateway must not read as running"
9726        );
9727
9728        // gateway.url form takes precedence over port.
9729        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9730        let port = listener.local_addr().unwrap().port();
9731        std::fs::write(
9732            home.join(".openclaw/openclaw.json"),
9733            format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9734        )
9735        .unwrap();
9736        assert!(probe_openclaw_running(&home).is_some());
9737        drop(listener);
9738
9739        // Hermes: an ACTIVE WAL (fresh stamp) is detected; a stale one is not.
9740        std::fs::create_dir_all(home.join(".hermes")).unwrap();
9741        let wal = home.join(".hermes/state.db-wal");
9742        std::fs::write(&wal, b"wal").unwrap();
9743        let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9744        assert!(matches!(
9745            running.method,
9746            RunningInstanceMethod::StoreWalActivity
9747        ));
9748        assert!(running.evidence.contains("state.db-wal"));
9749        let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9750        std::fs::File::options()
9751            .append(true)
9752            .open(&wal)
9753            .unwrap()
9754            .set_modified(stale)
9755            .unwrap();
9756        assert!(
9757            probe_hermes_running(&home, 300_000).is_none(),
9758            "a stale WAL (crash leftover) must not read as running"
9759        );
9760    }
9761
9762    fn connect_scratch_home(tag: &str) -> PathBuf {
9763        let dir = std::env::temp_dir().join(format!(
9764            "supercode-connect-service-{tag}-{}-{}",
9765            std::process::id(),
9766            std::time::SystemTime::now()
9767                .duration_since(std::time::UNIX_EPOCH)
9768                .unwrap()
9769                .as_nanos()
9770        ));
9771        std::fs::create_dir_all(&dir).unwrap();
9772        dir
9773    }
9774
9775    /// Minimal HTTP responder that speaks just enough OpenCode server to
9776    /// accept a health check, create a session, and hold an SSE stream open,
9777    /// while recording each request line with its Authorization header.
9778    async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9779        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9780        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9781        let address = listener.local_addr().unwrap();
9782        let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9783        tokio::spawn(async move {
9784            loop {
9785                let Ok((mut stream, _)) = listener.accept().await else {
9786                    break;
9787                };
9788                let request_sender = request_sender.clone();
9789                tokio::spawn(async move {
9790                    let (reader, mut writer) = stream.split();
9791                    let mut reader = BufReader::new(reader);
9792                    let mut request_line = String::new();
9793                    if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9794                        return;
9795                    }
9796                    let request_line = request_line.trim_end().to_string();
9797                    let mut authorization = String::new();
9798                    let mut content_length = 0usize;
9799                    loop {
9800                        let mut line = String::new();
9801                        if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9802                            return;
9803                        }
9804                        let line = line.trim_end();
9805                        if line.is_empty() {
9806                            break;
9807                        }
9808                        let lower = line.to_ascii_lowercase();
9809                        if let Some(value) = lower.strip_prefix("authorization:") {
9810                            authorization = value.trim().to_string();
9811                        }
9812                        if let Some(value) = lower.strip_prefix("content-length:") {
9813                            content_length = value.trim().parse().unwrap_or(0);
9814                        }
9815                    }
9816                    if content_length > 0 {
9817                        let mut body = vec![0u8; content_length];
9818                        let _ = reader.read_exact(&mut body).await;
9819                    }
9820                    let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9821                    if request_line.starts_with("GET /event") {
9822                        let _ = writer
9823                            .write_all(
9824                                b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9825                            )
9826                            .await;
9827                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9828                        return;
9829                    }
9830                    let body = if request_line.starts_with("POST /session") {
9831                        r#"{"id":"mock-session"}"#
9832                    } else {
9833                        r#"{"status":"ok"}"#
9834                    };
9835                    let response = format!(
9836                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9837                        body.len(),
9838                        body
9839                    );
9840                    let _ = writer.write_all(response.as_bytes()).await;
9841                });
9842            }
9843        });
9844        (format!("http://{address}"), request_receiver)
9845    }
9846
9847    fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9848        crate::HarnessSupportDescriptor {
9849            orchestration: Default::default(),
9850            id: HarnessId::from(HarnessId::OPENCODE),
9851            display_name: "OpenCode".into(),
9852            native: crate::NativeSupport {
9853                discover: crate::ImplementationKind::Absent,
9854                load: crate::ImplementationKind::Absent,
9855                follow: crate::ImplementationKind::Absent,
9856                import: crate::ImplementationKind::Absent,
9857                export: crate::ImplementationKind::Absent,
9858            },
9859            runtime: crate::RuntimeSupport {
9860                implementation: crate::ImplementationKind::BuiltIn,
9861                protocol: protocol.into(),
9862                default_launch: None,
9863                connect_launch: Some(crate::RuntimeConnectLaunch {
9864                    config_path: "~/opencode-tui.json".into(),
9865                    address_pointer: "/server/url".into(),
9866                    port_pointer: None,
9867                    default_address: None,
9868                    auth_pointer: Some("/server/token".into()),
9869                    protocol: protocol.into(),
9870                }),
9871                capabilities: crate::RuntimeCapabilities {
9872                    start_session: true,
9873                    resume_session: true,
9874                    attach_existing_process: true,
9875                    send_input: true,
9876                    stream_events: true,
9877                    interrupt: true,
9878                    steer: false,
9879                    respond_to_requests: true,
9880                },
9881            },
9882        }
9883    }
9884
9885    #[tokio::test]
9886    async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9887        let (base_url, mut requests) = mock_opencode_endpoint().await;
9888        let home = connect_scratch_home("open");
9889        std::fs::write(
9890            home.join("opencode-tui.json"),
9891            format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9892        )
9893        .unwrap();
9894
9895        let descriptor = connect_descriptor("opencode-http-sse");
9896        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9897        assert!(backend.capabilities().attach_existing_process);
9898
9899        let connection = backend
9900            .start(crate::RuntimeStartRequest {
9901                cwd: home.clone(),
9902                launch: None,
9903                mcp_servers: Vec::new(),
9904            })
9905            .await
9906            .unwrap();
9907        let handle = connection.handle();
9908        assert_eq!(handle.runtime_id, "mock-session");
9909        match &handle.endpoint {
9910            crate::RuntimeEndpoint::Http {
9911                base_url: endpoint, ..
9912            } => assert_eq!(endpoint, &base_url),
9913            other => panic!("connect mode must join the running endpoint, got {other:?}"),
9914        }
9915
9916        let mut seen = Vec::new();
9917        while let Ok(line) = requests.try_recv() {
9918            seen.push(line);
9919        }
9920        assert!(seen
9921            .iter()
9922            .any(|line| line.starts_with("GET /global/health")
9923                && line.contains("bearer connect-secret")));
9924        assert!(seen.iter().any(
9925            |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9926        ));
9927    }
9928
9929    /// UNI-5 dev/02, contract corrected by the 2026-08-31 blind walk: the
9930    /// full connect-mode attach path against a MOCK gateway bridge — no live
9931    /// gateway, no model spend. A scripted fake `openclaw` binary (a)
9932    /// asserts the REAL bridge contract — the resolved --url on argv and the
9933    /// credential via --token-file (the real bridge ignores the env var; the
9934    /// endpoint comes from openclaw-native `gateway.remote.url`, never the
9935    /// schema-invalid `gateway.url`) — then (b) speaks scripted ACP:
9936    /// initialize advertising sessionCapabilities.{list,resume},
9937    /// session/resume rebinding the requested session (join), and a
9938    /// prompted turn.
9939    #[tokio::test]
9940    async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9941        let home = connect_scratch_home("openclaw");
9942        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9943        std::fs::write(
9944            home.join(".openclaw/openclaw.json"),
9945            r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9946        )
9947        .unwrap();
9948        let script = home.join("openclaw");
9949        std::fs::write(
9950            &script,
9951            r#"#!/bin/sh
9952# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9953[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9954[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9955[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9956[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9957while IFS= read -r line; do
9958  case "$line" in
9959    *'"initialize"'*)
9960      printf '%s
9961' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9962    *'"session/resume"'*)
9963      printf '%s
9964' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9965    *'"session/new"'*)
9966      printf '%s
9967' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9968    *'"session/prompt"'*)
9969      printf '%s
9970' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9971      printf '%s
9972' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9973  esac
9974done
9975"#,
9976        )
9977        .unwrap();
9978        use std::os::unix::fs::PermissionsExt;
9979        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9980
9981        let mut descriptor = crate::harness_support_registry()
9982            .harnesses
9983            .into_iter()
9984            .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9985            .expect("openclaw must be registered");
9986        descriptor
9987            .runtime
9988            .connect_launch
9989            .as_mut()
9990            .unwrap()
9991            .config_path = "~/.openclaw/openclaw.json".into();
9992        descriptor.runtime.default_launch.as_mut().unwrap().program =
9993            script.to_string_lossy().into_owned();
9994        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9995        assert!(backend.capabilities().resume_session);
9996
9997        let joined = backend
9998            .attach(crate::RuntimeAttachRequest {
9999                runtime_id: "agent:main:main".into(),
10000                cwd: Some(home.clone()),
10001                launch: None,
10002                mcp_servers: Vec::new(),
10003            })
10004            .await;
10005        let mut connection = joined.expect("mock bridge attach must succeed");
10006        assert_eq!(connection.handle().runtime_id, "agent:main:main");
10007        let turn = connection
10008            .send_input(crate::RuntimeInput {
10009                text: "hello".into(),
10010                image_urls: Vec::new(),
10011            })
10012            .await;
10013        assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
10014        connection.close().await.unwrap();
10015    }
10016
10017    #[tokio::test]
10018    async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
10019        let home = connect_scratch_home("fail");
10020        std::fs::write(
10021            home.join("opencode-tui.json"),
10022            r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
10023        )
10024        .unwrap();
10025
10026        let gateway_only = connect_descriptor("acp-v1-jsonrpc");
10027        let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
10028            panic!("an ACP connect endpoint has no gateway client yet");
10029        };
10030        let message = format!("{error:?}");
10031        assert!(message.contains("acp-v1-jsonrpc"));
10032        assert!(!message.contains("connect-secret"));
10033
10034        let unreadable = connect_descriptor("opencode-http-sse");
10035        let missing_home = connect_scratch_home("missing");
10036        let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
10037            panic!("an unreadable connect config must fail closed");
10038        };
10039        let message = format!("{error:?}");
10040        assert!(message.contains("opencode-tui.json"));
10041        assert!(!message.contains("connect-secret"));
10042    }
10043
10044    // ---------------------------------------------------------------------
10045    // ORCH-7 — `harness.v1.jobs.list` / `jobs.get` over the committed fixtures
10046    // ---------------------------------------------------------------------
10047
10048    fn jobs_fixture_root() -> PathBuf {
10049        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10050    }
10051
10052    /// Point only the three job-bearing homes at the fixtures. Nothing else is
10053    /// read, so the host machine's own harness homes cannot leak into a row.
10054    fn jobs_fixture_homes() -> Value {
10055        let root = jobs_fixture_root();
10056        json!({
10057            "claude_code": root.join("claude_jobs_home/projects"),
10058            "hermes": root.join("hermes_home/state.db"),
10059            "openclaw": root.join("openclaw_home"),
10060        })
10061    }
10062
10063    fn jobs_list(params: Value) -> Value {
10064        let mut service = HarnessSessionService::new();
10065        service.handle(request(1, "harness.v1.jobs.list", params))
10066    }
10067
10068    fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10069        result["jobs"]
10070            .as_array()
10071            .expect("jobs is an array")
10072            .iter()
10073            .find(|job| job["id"] == id)
10074            .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10075    }
10076
10077    #[test]
10078    fn gateway_health_derives_from_running_probe_and_install_state() {
10079        let running = RunningInstance {
10080            method: RunningInstanceMethod::GatewayConnect,
10081            evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10082            checked_at_ms: 1,
10083        };
10084        let up = gateway_health(
10085            HarnessId::OPENCLAW,
10086            true,
10087            Some(&running),
10088            Some("2026.7.1-2"),
10089        );
10090        assert_eq!(up.state, GatewayState::Up);
10091        assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10092        assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10093        // Hermes consults its own `gateway status` when the WAL heuristic says
10094        // nothing; a fake binary decides the verdict (the env var is global, so
10095        // the up/down cases run inside this one test, never in parallel).
10096        let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10097        std::fs::create_dir_all(&dir).unwrap();
10098        let fake = dir.join("hermes");
10099        let write_fake = |body: &str| {
10100            std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10101            #[cfg(unix)]
10102            {
10103                use std::os::unix::fs::PermissionsExt;
10104                std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10105            }
10106        };
10107        write_fake("echo '✗ Gateway service is not installed'");
10108        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10109            *slot.borrow_mut() = Some((
10110                HarnessId::HERMES.to_string(),
10111                fake.to_string_lossy().into_owned(),
10112            ))
10113        });
10114        let down = gateway_health(HarnessId::HERMES, true, None, None);
10115        assert_eq!(down.state, GatewayState::Down, "{down:?}");
10116        assert!(down.endpoint.is_none());
10117        assert!(down.evidence.contains("not installed"));
10118        write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10119        let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10120        assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10121        assert!(idle_but_up.evidence.contains("PID 4242"));
10122        write_fake("echo 'something unparseable'");
10123        let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10124        assert_eq!(no_verdict.state, GatewayState::Down);
10125        assert!(no_verdict.evidence.contains("no verdict"));
10126        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10127        let absent = gateway_health(HarnessId::HERMES, false, None, None);
10128        assert_eq!(absent.state, GatewayState::Unknown);
10129        let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10130        assert_eq!(core.state, GatewayState::Unknown);
10131        assert!(core.evidence.contains("per session"));
10132    }
10133
10134    #[test]
10135    fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10136        let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10137        let rows = response["result"]["triggers"]
10138            .as_array()
10139            .expect("triggers")
10140            .clone();
10141        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10142        assert!(
10143            hermes.iter().any(|r| r["name"] == "deploys"
10144                && r["route"] == "/webhooks/deploys"
10145                && r["kind"] == "webhook"),
10146            "{rows:#?}"
10147        );
10148        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10149        assert!(openclaw
10150            .iter()
10151            .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10152        assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10153            && r["kind"] == "hook_mapping"
10154            && r["target"]["action"] == "agent"));
10155        let rendered = response.to_string();
10156        for secret in [
10157            "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10158            "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10159        ] {
10160            assert!(!rendered.contains(secret), "{rendered}");
10161        }
10162        let refused =
10163            triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10164        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10165    }
10166
10167    fn triggers_list(params: Value) -> Value {
10168        let mut service = HarnessSessionService::new();
10169        service.handle(request(1, "harness.v1.triggers.list", params))
10170    }
10171
10172    #[test]
10173    fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10174        let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10175        let rows = response["result"]["routes"]
10176            .as_array()
10177            .expect("routes")
10178            .clone();
10179        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10180        assert_eq!(hermes.len(), 2, "{rows:#?}");
10181        assert_eq!(hermes[0]["target"], "coder");
10182        assert_eq!(hermes[0]["match"]["platform"], "slack");
10183        assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10184        assert_eq!(hermes[0]["specificity"], 4);
10185        assert_eq!(hermes[1]["default"], true);
10186        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10187        assert!(
10188            openclaw.iter().any(|r| r["target"] == "design"
10189                && r["match"]["platform"] == "slack"
10190                && r["specificity"] == 1),
10191            "{openclaw:#?}"
10192        );
10193        assert!(openclaw.iter().any(|r| r["default"] == true));
10194        // A core harness has no routing concept and is refused, never an empty list.
10195        let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10196        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10197    }
10198
10199    fn routes_list(params: Value) -> Value {
10200        let mut service = HarnessSessionService::new();
10201        service.handle(request(1, "harness.v1.routes.list", params))
10202    }
10203
10204    #[test]
10205    fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10206        let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10207        let result = &response["result"];
10208        let ids: Vec<&str> = result["jobs"]
10209            .as_array()
10210            .unwrap()
10211            .iter()
10212            .map(|job| job["id"].as_str().unwrap())
10213            .collect();
10214        assert_eq!(
10215            ids,
10216            vec![
10217                "release-watch",
10218                "toolu_wake_recheck",
10219                "digest-15m",
10220                "nightly-audit",
10221                "coder-standup",
10222                "ops-once-boot",
10223                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10224                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10225                "cron_standup",
10226                "cron_reindex",
10227            ],
10228            "{result}"
10229        );
10230
10231        // OpenClaw, pinned shape: rows come from `state/openclaw.sqlite`
10232        // (`cron_jobs.job_json` + runtime columns), captured from a real
10233        // 2026.7.1-2 gateway.
10234        let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10235        assert_eq!(health["harness"], "openclaw");
10236        assert_eq!(health["schedule"]["kind"], "interval");
10237        assert_eq!(health["schedule"]["minutes"], 10.0);
10238        assert_eq!(health["session_target"], "isolated");
10239        assert_eq!(health["payload"]["kind"], "prompt");
10240        assert_eq!(health["payload"]["text"], "nightly health check");
10241        // ORCH-13: the mode word (`announce`) and the channel it announces on
10242        // (`last`) are separate facts, and the store keeps both — in
10243        // `job_json.delivery` and in the `delivery_*` columns beside it.
10244        assert_eq!(health["deliver"]["mode"], "announce");
10245        assert_eq!(health["deliver"]["target"], "last");
10246        assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10247        let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10248        assert_eq!(digest["schedule"]["kind"], "cron");
10249        assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10250        assert_eq!(digest["session_target"], "main");
10251        assert_eq!(digest["payload"]["kind"], "system_event");
10252
10253        // Claude Code: session-scoped, one recurring cron and one one-shot wakeup.
10254        let cron = job_row(result, "release-watch");
10255        assert_eq!(cron["harness"], "claude-code");
10256        assert_eq!(cron["scope"], "session");
10257        assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10258        assert_eq!(cron["schedule"]["kind"], "cron");
10259        assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10260        assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10261        assert_eq!(cron["payload"]["kind"], "prompt");
10262        assert_eq!(cron["recurring"], true);
10263        assert_eq!(cron["deliver"]["target"], "session");
10264        let wakeup = job_row(result, "toolu_wake_recheck");
10265        assert_eq!(wakeup["payload"]["kind"], "wakeup");
10266        assert_eq!(wakeup["schedule"]["kind"], "once");
10267        assert_eq!(wakeup["recurring"], false);
10268        assert_eq!(wakeup["state"], "pending");
10269
10270        // Hermes: install-scoped, interval + origin delivery, and a paused cron.
10271        let interval = job_row(result, "digest-15m");
10272        assert_eq!(interval["harness"], "hermes");
10273        assert_eq!(interval["scope"], "install");
10274        assert_eq!(interval["profile"], Value::Null);
10275        assert_eq!(interval["schedule"]["kind"], "interval");
10276        assert_eq!(interval["schedule"]["minutes"], 15.0);
10277        assert_eq!(interval["schedule"]["display"], "every 15 min");
10278        assert_eq!(interval["deliver"]["target"], "origin");
10279        assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10280        assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10281        assert_eq!(interval["last_status"], "ok");
10282        let nightly = job_row(result, "nightly-audit");
10283        assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10284        assert_eq!(nightly["deliver"]["target"], "local");
10285        assert_eq!(nightly["enabled"], false);
10286        assert_eq!(nightly["state"], "paused");
10287        // The per-profile store carries the profile name from its own path.
10288        let profiled = job_row(result, "ops-once-boot");
10289        assert_eq!(profiled["profile"], "ops");
10290        assert_eq!(profiled["schedule"]["kind"], "once");
10291        assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10292        assert_eq!(profiled["payload"]["kind"], "script");
10293        // An explicit `<platform>:<chat>` target carries the chat itself.
10294        assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10295        assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10296        assert_eq!(profiled["recurring"], false);
10297
10298        // ORCH-13: a job delivering to its creating conversation carries that
10299        // conversation's whole surface — platform word, chat AND thread.
10300        let standup_to_group = job_row(result, "coder-standup");
10301        assert_eq!(standup_to_group["deliver"]["target"], "origin");
10302        assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10303        assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10304        // Hermes has no mode word and routes by adapter profile, not account.
10305        assert!(standup_to_group["deliver"]["mode"].is_null());
10306        assert!(standup_to_group["deliver"]["account"].is_null());
10307
10308        // OpenClaw: the session target and the delivery mode are the row's own
10309        // columns, not a footnote.
10310        let standup = job_row(result, "cron_standup");
10311        assert_eq!(standup["harness"], "openclaw");
10312        assert_eq!(standup["session_target"], "isolated");
10313        assert_eq!(standup["deliver"]["mode"], "announce");
10314        assert_eq!(standup["deliver"]["target"], "slack");
10315        assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10316        assert_eq!(standup["payload"]["kind"], "prompt");
10317        assert_eq!(standup["profile"], "main");
10318        let reindex = job_row(result, "cron_reindex");
10319        assert_eq!(reindex["session_target"], "main");
10320        assert_eq!(reindex["payload"]["kind"], "system_event");
10321        assert_eq!(reindex["schedule"]["kind"], "interval");
10322        assert_eq!(reindex["schedule"]["display"], "every 240 min");
10323        assert_eq!(reindex["enabled"], false);
10324
10325        // Every store consulted is named, so an empty answer is never silent.
10326        let states: Vec<(&str, &str)> = result["sources"]
10327            .as_array()
10328            .unwrap()
10329            .iter()
10330            .map(|source| {
10331                (
10332                    source["harness"].as_str().unwrap(),
10333                    source["state"].as_str().unwrap(),
10334                )
10335            })
10336            .collect();
10337        // The `coder` profile home has no cron store at all: it is named as
10338        // `absent_store`, not skipped, so "this profile schedules nothing" and
10339        // "this profile was never looked at" stay distinguishable.
10340        assert_eq!(
10341            states,
10342            vec![
10343                ("claude-code", "scanned"),
10344                ("hermes", "read"),
10345                ("hermes", "absent_store"),
10346                ("hermes", "read"),
10347                ("openclaw", "read"),
10348                ("openclaw", "read"),
10349            ],
10350            "{result}"
10351        );
10352    }
10353
10354    #[test]
10355    fn jobs_list_filters_by_harness_session_and_profile() {
10356        let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10357        let ids: Vec<&str> = by_harness["result"]["jobs"]
10358            .as_array()
10359            .unwrap()
10360            .iter()
10361            .map(|job| job["id"].as_str().unwrap())
10362            .collect();
10363        assert_eq!(
10364            ids,
10365            vec![
10366                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10367                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10368                "cron_standup",
10369                "cron_reindex",
10370            ]
10371        );
10372
10373        let by_session = jobs_list(json!({
10374            "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10375            "homes": jobs_fixture_homes(),
10376        }));
10377        let jobs = by_session["result"]["jobs"].as_array().unwrap();
10378        assert_eq!(jobs.len(), 2, "{by_session}");
10379        assert!(jobs
10380            .iter()
10381            .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10382
10383        let by_profile = jobs_list(json!({
10384            "harness": "hermes",
10385            "profile": "ops",
10386            "homes": jobs_fixture_homes(),
10387        }));
10388        let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10389        assert_eq!(jobs.len(), 1, "{by_profile}");
10390        assert_eq!(jobs[0]["id"], "ops-once-boot");
10391    }
10392
10393    #[test]
10394    fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10395        let mut service = HarnessSessionService::new();
10396        let hermes = service.handle(request(
10397            1,
10398            "harness.v1.jobs.get",
10399            json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10400        ));
10401        assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10402        // Native fields the uniform row does not carry survive on `source`.
10403        assert_eq!(hermes["result"]["source"]["provider"], "nous");
10404        assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10405
10406        let claude = service.handle(request(
10407            2,
10408            "harness.v1.jobs.get",
10409            json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10410        ));
10411        assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10412        assert_eq!(
10413            claude["result"]["source"]["tool_use_id"],
10414            "toolu_cron_release_watch"
10415        );
10416
10417        let missing = service.handle(request(
10418            3,
10419            "harness.v1.jobs.get",
10420            json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10421        ));
10422        assert!(missing["error"]["message"]
10423            .as_str()
10424            .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10425    }
10426
10427    #[test]
10428    fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10429        let mut service = HarnessSessionService::new();
10430        for (id, method, params) in [
10431            (
10432                1,
10433                "harness.v1.jobs.list",
10434                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10435            ),
10436            (
10437                2,
10438                "harness.v1.jobs.get",
10439                json!({"harness": "codex", "id": "anything"}),
10440            ),
10441        ] {
10442            let response = service.handle(request(id, method, params));
10443            assert_eq!(response["error"]["code"], -32020, "{response}");
10444            assert!(response["error"]["message"]
10445                .as_str()
10446                .is_some_and(|message| message.contains("has no scheduled jobs")));
10447            assert!(response.get("result").is_none());
10448        }
10449    }
10450
10451    #[test]
10452    fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10453        let scratch = std::env::temp_dir().join(format!(
10454            "supercode-jobs-migrated-{}-{}",
10455            std::process::id(),
10456            generated_session_id()
10457        ));
10458        std::fs::create_dir_all(&scratch).unwrap();
10459        let response = jobs_list(json!({
10460            "harness": "openclaw",
10461            "homes": {"openclaw": scratch.clone()},
10462        }));
10463        let result = &response["result"];
10464        assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10465        assert_eq!(result["sources"][0]["state"], "absent_store");
10466        assert_eq!(result["sources"][0]["harness"], "openclaw");
10467        std::fs::remove_dir_all(&scratch).ok();
10468    }
10469
10470    // ---------------------------------------------------------------------
10471    // ORCH-8 — `harness.v1.runs.list` / `runs.get` over the committed fire
10472    // stores: Hermes's `cron/executions.db` (root home + profile home) and
10473    // OpenClaw's `cron_run_logs`. Every fixture row is written by
10474    // `tests/fixtures/gen_runs_fixtures.py` against the harnesses' own DDL.
10475    // ---------------------------------------------------------------------
10476
10477    /// The health job in the committed OpenClaw fixture, which fired twice.
10478    const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10479    /// The digest job, whose single fire predates run ids.
10480    const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10481
10482    fn runs_list(params: Value) -> Value {
10483        let mut service = HarnessSessionService::new();
10484        service.handle(request(1, "harness.v1.runs.list", params))
10485    }
10486
10487    fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10488        result["runs"]
10489            .as_array()
10490            .expect("runs is an array")
10491            .iter()
10492            .find(|run| run["id"] == id)
10493            .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10494    }
10495
10496    #[test]
10497    fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10498        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10499        let result = &response["result"];
10500        let ids: Vec<&str> = result["runs"]
10501            .as_array()
10502            .expect("runs is an array")
10503            .iter()
10504            .map(|run| run["id"].as_str().unwrap())
10505            .collect();
10506        let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10507        assert_eq!(
10508            ids,
10509            vec![
10510                // Hermes, newest claim first, root ledger then profile ledger.
10511                "b2c3d4e5f60718293a4b5c6d7e8f9012",
10512                "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10513                "c3d4e5f60718293a4b5c6d7e8f901234",
10514                "f60718293a4b5c6d7e8f901234567890",
10515                "e5f60718293a4b5c6d7e8f9012345678",
10516                "d4e5f60718293a4b5c6d7e8f90123456",
10517                // OpenClaw, newest `ts` first.
10518                "run_health_0002",
10519                digest_fire.as_str(),
10520                "run_health_0001",
10521            ],
10522            "{result}"
10523        );
10524
10525        // The harness's OWN outcome word survives; nothing is renamed onto a
10526        // shared vocabulary.
10527        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10528        assert_eq!(failed["harness"], "hermes");
10529        assert_eq!(failed["job_id"], "job42");
10530        assert_eq!(failed["status"], "failed");
10531        assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10532        assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10533
10534        // Hermes's `unknown` — an attempt whose owner died before writing a
10535        // terminal state — is a fourth status, not folded into `failed`.
10536        let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10537        assert_eq!(abandoned["status"], "unknown");
10538        assert_eq!(abandoned["job_id"], "ops-once-boot");
10539
10540        // An unterminated fire has no finish, and no session is invented.
10541        let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10542        assert_eq!(running["status"], "running");
10543        assert!(running["finished_at"].is_null(), "{running}");
10544        assert!(running["session_id"].is_null(), "{running}");
10545
10546        // OpenClaw records the session on the row itself, and epoch-ms
10547        // timestamps are rendered as RFC 3339.
10548        let ok = run_row(result, "run_health_0001");
10549        assert_eq!(ok["harness"], "openclaw");
10550        assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10551        assert_eq!(ok["status"], "ok");
10552        assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10553        assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10554        assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10555        // OpenClaw's run log is written once, at finish: there is no claim.
10556        assert!(ok["claimed_at"].is_null(), "{ok}");
10557
10558        // A run-log row with no `run_id` falls back to the store's own
10559        // `(job_id, seq)` key rather than being dropped.
10560        assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10561
10562        // ORCH-13: a fire whose delivery nothing recorded says so, rather than
10563        // borrowing a neighbouring fire's outcome. Both of these ran on jobs
10564        // that deliver `local` (or have no job record at all), so no
10565        // obligation is addressed to a surface they could match.
10566        for id in [
10567            "b2c3d4e5f60718293a4b5c6d7e8f9012",
10568            "d4e5f60718293a4b5c6d7e8f90123456",
10569        ] {
10570            assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10571        }
10572
10573        // Every store consulted is named, including the profile home that has
10574        // no ledger — an empty history and an absent store are different.
10575        let sources = result["sources"].as_array().unwrap();
10576        let states: Vec<(&str, &str)> = sources
10577            .iter()
10578            .map(|source| {
10579                (
10580                    source["harness"].as_str().unwrap(),
10581                    source["state"].as_str().unwrap(),
10582                )
10583            })
10584            .collect();
10585        assert_eq!(
10586            states,
10587            vec![
10588                ("hermes", "read"),
10589                ("hermes", "absent_store"),
10590                ("hermes", "read"),
10591                ("openclaw", "read"),
10592            ],
10593            "{result}"
10594        );
10595        assert_eq!(sources[2]["profile"], "ops");
10596        assert!(sources[3]["path"]
10597            .as_str()
10598            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10599    }
10600
10601    #[test]
10602    fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10603        let response = runs_list(json!({
10604            "harness": "hermes",
10605            "job": "job42",
10606            "homes": jobs_fixture_homes(),
10607        }));
10608        let result = &response["result"];
10609        assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10610
10611        // Hermes writes NO link from an execution to its session. The fire
10612        // that ran the agent is joined to `cron_job42_<stamp>` because that
10613        // id's instant falls inside its [claimed_at, finished_at] window.
10614        let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10615        assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10616
10617        // The later fire failed before opening one. Its window holds no
10618        // session, so the row says so instead of re-using the earlier fire's
10619        // — the join is per-FIRE, not per-job.
10620        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10621        assert!(failed["session_id"].is_null(), "{failed}");
10622    }
10623
10624    /// ORCH-13: where a fire's output went, read from each harness's own
10625    /// delivery record — Hermes's `delivery_obligations` ledger inside
10626    /// `state.db`, OpenClaw's `delivery_*` run-log columns.
10627    #[test]
10628    fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10629        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10630        let result = &response["result"];
10631
10632        // Hermes: the ledger is the GATEWAY's, keyed by conversation and
10633        // surface, so the fire's own [claimed_at, finished_at] window picks
10634        // the obligation. The fire succeeded and so did the send.
10635        let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10636        assert_eq!(delivered["status"], "completed");
10637        assert_eq!(delivered["delivery"]["state"], "delivered");
10638        assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10639        assert_eq!(delivered["delivery"]["attempts"], 1);
10640        assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10641        assert_eq!(
10642            delivered["delivery"]["delivered_at"],
10643            "2026-09-02T09:00:30.400Z"
10644        );
10645
10646        // The next fire of the same job ALSO succeeded — and its output never
10647        // arrived. That is the fact `status` alone cannot carry.
10648        let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10649        assert_eq!(undelivered["status"], "completed");
10650        assert_eq!(undelivered["delivery"]["state"], "failed");
10651        assert_eq!(undelivered["delivery"]["attempts"], 3);
10652        assert_eq!(
10653            undelivered["delivery"]["last_error"],
10654            "telegram send failed: Bad Request: chat not found"
10655        );
10656        // Only a delivered obligation carries an instant of delivery; the
10657        // ledger's `updated_at` on a failed row dates the failure.
10658        assert!(
10659            undelivered["delivery"]["delivered_at"].is_null(),
10660            "{undelivered}"
10661        );
10662
10663        // OpenClaw writes the outcome onto the run-log row and declares the
10664        // address on the job, so the row's target is joined from `cron_jobs`.
10665        let announced = run_row(result, "run_health_0001");
10666        assert_eq!(announced["delivery"]["state"], "delivered");
10667        assert_eq!(announced["delivery"]["target"], "last");
10668        // Its run log counts no attempts and stamps no delivered-at.
10669        assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10670        assert!(
10671            announced["delivery"]["delivered_at"].is_null(),
10672            "{announced}"
10673        );
10674        let refused = run_row(result, "run_health_0002");
10675        assert_eq!(refused["delivery"]["state"], "not-delivered");
10676        assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10677
10678        // A run-log row with no delivery columns at all recorded no delivery:
10679        // the job's declared target is not evidence that anything was sent.
10680        let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10681        assert!(skipped["delivery"].is_null(), "{skipped}");
10682    }
10683
10684    /// A Hermes fire whose session carries a `session_key` is matched on that
10685    /// key FIRST — the most specific question the ledger can answer. Proven by
10686    /// moving the obligations off the job's surface on a COPY of the fixture,
10687    /// so only the session-key question can still find them.
10688    #[test]
10689    fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10690        let scratch = std::env::temp_dir().join(format!(
10691            "supercode-runs-delivery-{}-{}",
10692            std::process::id(),
10693            generated_session_id()
10694        ));
10695        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10696        let fixture = jobs_fixture_root().join("hermes_home");
10697        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10698        for name in ["cron/executions.db", "cron/jobs.json"] {
10699            std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10700        }
10701        {
10702            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10703            // The obligations now sit on a surface no job in this store
10704            // delivers to, so the surface question cannot match them.
10705            connection
10706                .execute(
10707                    "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10708                    [],
10709                )
10710                .unwrap();
10711            // A cron fire that ran inside a keyed conversation: the session
10712            // the window recovers carries `tg-coder-1`'s key.
10713            connection
10714                .execute(
10715                    "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10716                     ('cron_coder-standup_20260902_090010', 'cron', \
10717                      'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10718                    [],
10719                )
10720                .unwrap();
10721        }
10722        let response = runs_list(json!({
10723            "harness": "hermes",
10724            "job": "coder-standup",
10725            "homes": {"hermes": scratch.join("state.db")},
10726        }));
10727        let result = &response["result"];
10728        let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10729        assert_eq!(
10730            matched["session_id"], "cron_coder-standup_20260902_090010",
10731            "{result}"
10732        );
10733        assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10734        assert_eq!(
10735            matched["delivery"]["target"], "slack:C0FALLBACK:55",
10736            "{result}"
10737        );
10738        std::fs::remove_dir_all(&scratch).ok();
10739    }
10740
10741    #[test]
10742    fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10743        // A fire whose session was compressed mid-run is only readable at the
10744        // continuation, so that is what the row must report. Built on a COPY
10745        // of the committed fixture: no test writes to a fixture or to a real
10746        // harness home.
10747        let scratch = std::env::temp_dir().join(format!(
10748            "supercode-runs-compressed-{}-{}",
10749            std::process::id(),
10750            generated_session_id()
10751        ));
10752        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10753        let fixture = jobs_fixture_root().join("hermes_home");
10754        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10755        std::fs::copy(
10756            fixture.join("cron/executions.db"),
10757            scratch.join("cron/executions.db"),
10758        )
10759        .unwrap();
10760        {
10761            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10762            connection
10763                .execute(
10764                    "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10765                    ["cron_job42_20260902_120000"],
10766                )
10767                .unwrap();
10768            connection
10769                .execute(
10770                    "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10771                     VALUES ('job42-after-compaction', 'cron', \
10772                             'cron_job42_20260902_120000', 1788350000.0)",
10773                    [],
10774                )
10775                .unwrap();
10776        }
10777        let response = runs_list(json!({
10778            "harness": "hermes",
10779            "job": "job42",
10780            "homes": {"hermes": scratch.join("state.db")},
10781        }));
10782        let result = &response["result"];
10783        assert_eq!(
10784            run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10785            "job42-after-compaction",
10786            "{result}"
10787        );
10788        std::fs::remove_dir_all(&scratch).ok();
10789    }
10790
10791    #[test]
10792    fn runs_list_filters_by_job_and_caps_by_limit() {
10793        let by_job = runs_list(json!({
10794            "harness": "openclaw",
10795            "job": OPENCLAW_HEALTH_JOB,
10796            "homes": jobs_fixture_homes(),
10797        }));
10798        let ids: Vec<&str> = by_job["result"]["runs"]
10799            .as_array()
10800            .unwrap()
10801            .iter()
10802            .map(|run| run["id"].as_str().unwrap())
10803            .collect();
10804        assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10805
10806        let capped = runs_list(json!({
10807            "harness": "openclaw",
10808            "limit": 1,
10809            "homes": jobs_fixture_homes(),
10810        }));
10811        let runs = capped["result"]["runs"].as_array().unwrap();
10812        assert_eq!(runs.len(), 1, "{capped}");
10813        // Newest first, so the cap keeps the recent fire.
10814        assert_eq!(runs[0]["id"], "run_health_0002");
10815    }
10816
10817    #[test]
10818    fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10819        let mut service = HarnessSessionService::new();
10820        let hermes = service.handle(request(
10821            1,
10822            "harness.v1.runs.get",
10823            json!({
10824                "harness": "hermes",
10825                "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10826                "homes": jobs_fixture_homes(),
10827            }),
10828        ));
10829        assert_eq!(hermes["result"]["run"]["status"], "completed");
10830        assert_eq!(
10831            hermes["result"]["run"]["session_id"],
10832            "cron_job42_20260902_120000"
10833        );
10834        // Ledger columns the uniform row does not carry survive on `source`.
10835        assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10836        assert_eq!(hermes["result"]["source"]["pid"], 4242);
10837        assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10838
10839        let openclaw = service.handle(request(
10840            2,
10841            "harness.v1.runs.get",
10842            json!({
10843                "harness": "openclaw",
10844                "id": "run_health_0002",
10845                "homes": jobs_fixture_homes(),
10846            }),
10847        ));
10848        assert_eq!(openclaw["result"]["run"]["status"], "error");
10849        // ORCH-13: the run's delivery is projected AND the store's own columns
10850        // stay verbatim on `source`, so nothing about the fire is lost.
10851        assert_eq!(
10852            openclaw["result"]["source"]["delivery_status"],
10853            "not-delivered"
10854        );
10855        assert_eq!(
10856            openclaw["result"]["source"]["delivery_error"],
10857            "channel_not_found"
10858        );
10859        assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10860        assert_eq!(
10861            openclaw["result"]["run"]["delivery"]["state"],
10862            "not-delivered"
10863        );
10864        assert_eq!(
10865            openclaw["result"]["run"]["delivery"]["last_error"],
10866            "channel_not_found"
10867        );
10868
10869        let missing = service.handle(request(
10870            3,
10871            "harness.v1.runs.get",
10872            json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10873        ));
10874        assert!(missing["error"]["message"]
10875            .as_str()
10876            .is_some_and(|message| message.contains("no run `no-such-run`")));
10877    }
10878
10879    #[test]
10880    fn runs_refuse_a_harness_that_keeps_no_run_store() {
10881        let mut service = HarnessSessionService::new();
10882        for (id, method, params) in [
10883            // Claude Code HAS scheduled jobs but no fire store: its fires are
10884            // ordinary turns. It must refuse, not answer with an empty list.
10885            (
10886                1,
10887                "harness.v1.runs.list",
10888                json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10889            ),
10890            (
10891                2,
10892                "harness.v1.runs.get",
10893                json!({"harness": "claude-code", "id": "anything"}),
10894            ),
10895            (
10896                3,
10897                "harness.v1.runs.list",
10898                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10899            ),
10900        ] {
10901            let response = service.handle(request(id, method, params));
10902            assert_eq!(response["error"]["code"], -32020, "{response}");
10903            assert!(response["error"]["message"]
10904                .as_str()
10905                .is_some_and(|message| message.contains("keeps no run store")));
10906            assert!(response.get("result").is_none());
10907        }
10908    }
10909
10910    #[test]
10911    fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10912        let scratch = std::env::temp_dir().join(format!(
10913            "supercode-runs-empty-{}-{}",
10914            std::process::id(),
10915            generated_session_id()
10916        ));
10917        std::fs::create_dir_all(&scratch).unwrap();
10918        let response = runs_list(json!({
10919            "harness": "openclaw",
10920            "homes": {"openclaw": scratch.clone()},
10921        }));
10922        let result = &response["result"];
10923        assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10924        assert_eq!(result["sources"][0]["state"], "absent_store");
10925        assert!(result["sources"][0]["path"]
10926            .as_str()
10927            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10928        std::fs::remove_dir_all(&scratch).ok();
10929    }
10930}