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        // supercode reads its own config home ($SUPERCODE_HOME, else
5639        // $XDG_CONFIG_HOME/supercode, else ~/.config/supercode), not a fixed
5640        // place under HOME: a login kept under XDG_CONFIG_HOME probed as
5641        // "no API key found" while `supercode run` answered.
5642        if harness == HarnessId::SUPERCODE {
5643            let config_home = crate::agent::global_instructions_dir();
5644            for file in ["config.toml", "credentials.toml"] {
5645                copy_probe_path(
5646                    &config_home.join(file),
5647                    &root.join(".config/supercode").join(file),
5648                )?;
5649            }
5650        }
5651        configure_isolated_probe_auth(harness, &root)?;
5652
5653        let root_text = root.to_string_lossy().into_owned();
5654        for (key, value) in [
5655            ("HOME", root_text.clone()),
5656            (
5657                "XDG_CACHE_HOME",
5658                root.join(".cache").to_string_lossy().into_owned(),
5659            ),
5660            (
5661                "XDG_CONFIG_HOME",
5662                root.join(".config").to_string_lossy().into_owned(),
5663            ),
5664            (
5665                "XDG_DATA_HOME",
5666                root.join(".local/share").to_string_lossy().into_owned(),
5667            ),
5668        ] {
5669            launch.env.insert(key.into(), value);
5670        }
5671        let scoped = match harness {
5672            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5673            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5674            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5675            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5676            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5677            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5678            _ => None,
5679        };
5680        if let Some((key, value)) = scoped {
5681            launch
5682                .env
5683                .insert(key.into(), value.to_string_lossy().into_owned());
5684        }
5685        Ok(Self { launch, root })
5686    }
5687
5688    fn cleanup(&self) -> std::io::Result<()> {
5689        match std::fs::remove_dir_all(&self.root) {
5690            Ok(()) => Ok(()),
5691            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5692            Err(error) => Err(error),
5693        }
5694    }
5695}
5696
5697impl Drop for IsolatedProbeHome {
5698    fn drop(&mut self) {
5699        let _ = self.cleanup();
5700    }
5701}
5702
5703fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5704    match harness {
5705        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5706        // The gateway endpoint + token live in openclaw's own config; without
5707        // it the isolated probe dials the default endpoint unauthenticated
5708        // (PARITY-24 finding 2026-08-31).
5709        HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5710        HarnessId::CODEX => &[".codex/auth.json"],
5711        HarnessId::GEMINI => &[
5712            ".gemini/google_accounts.json",
5713            ".gemini/oauth_creds.json",
5714            ".gemini/settings.json",
5715        ],
5716        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5717        HarnessId::OPENCODE => &[
5718            ".config/opencode/auth.json",
5719            ".local/share/opencode/auth.json",
5720        ],
5721        HarnessId::PI => &[".pi/agent/auth.json"],
5722        // Hermes keeps its provider selection in config.yaml, its OAuth
5723        // credential pool in auth.json, and API keys in .env; without them
5724        // the isolated probe sees "No LLM provider configured" for a
5725        // hermes that answers fine from the user's real home.
5726        HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5727        _ => &[],
5728    }
5729}
5730
5731fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5732    copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5733}
5734
5735fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5736    if !source.is_file() {
5737        return Ok(());
5738    }
5739    if let Some(parent) = destination.parent() {
5740        std::fs::create_dir_all(parent)?;
5741        set_private_dir_permissions(parent)?;
5742    }
5743    std::fs::copy(source, destination)?;
5744    set_private_file_permissions(destination)
5745}
5746
5747fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5748    if harness != HarnessId::GEMINI {
5749        return Ok(());
5750    }
5751    let oauth = probe_home.join(".gemini/oauth_creds.json");
5752    if !oauth.is_file() {
5753        return Ok(());
5754    }
5755    let settings_path = probe_home.join(".gemini/settings.json");
5756    let mut settings = std::fs::read_to_string(&settings_path)
5757        .ok()
5758        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5759        .unwrap_or_else(|| json!({}));
5760    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5761    std::fs::write(
5762        &settings_path,
5763        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5764    )?;
5765    set_private_file_permissions(&settings_path)
5766}
5767
5768#[cfg(unix)]
5769fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5770    use std::os::unix::fs::PermissionsExt;
5771    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5772}
5773
5774#[cfg(not(unix))]
5775fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5776    Ok(())
5777}
5778
5779#[cfg(unix)]
5780fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5781    use std::os::unix::fs::PermissionsExt;
5782    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5783}
5784
5785#[cfg(not(unix))]
5786fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5787    Ok(())
5788}
5789
5790fn find_executable(program: &str) -> Option<PathBuf> {
5791    let candidate = PathBuf::from(program);
5792    if candidate.components().count() > 1 {
5793        return candidate.is_file().then_some(candidate);
5794    }
5795    let path = std::env::var_os("PATH")?;
5796    for directory in std::env::split_paths(&path) {
5797        let candidate = directory.join(program);
5798        if candidate.is_file() {
5799            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5800        }
5801        #[cfg(windows)]
5802        {
5803            for extension in ["exe", "cmd", "bat"] {
5804                let candidate = directory.join(format!("{program}.{extension}"));
5805                if candidate.is_file() {
5806                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5807                }
5808            }
5809        }
5810    }
5811    None
5812}
5813
5814async fn executable_version(executable: &Path) -> Option<String> {
5815    let mut command = tokio::process::Command::new(executable);
5816    command
5817        .arg("--version")
5818        .stdin(std::process::Stdio::null())
5819        .stdout(std::process::Stdio::piped())
5820        .stderr(std::process::Stdio::piped())
5821        .kill_on_drop(true);
5822    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5823        .await
5824        .ok()?
5825        .ok()?;
5826    let stdout = String::from_utf8_lossy(&output.stdout);
5827    let stderr = String::from_utf8_lossy(&output.stderr);
5828    stdout
5829        .lines()
5830        .chain(stderr.lines())
5831        .map(str::trim)
5832        .find(|line| !line.is_empty())
5833        .map(|line| truncate_text(line, 200))
5834}
5835
5836pub(crate) fn auth_evidence(harness: &str) -> bool {
5837    let env_names: &[&str] = match harness {
5838        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5839        HarnessId::CODEX => &["OPENAI_API_KEY"],
5840        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5841        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5842        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5843        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5844        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5845        _ => &[],
5846    };
5847    if env_names
5848        .iter()
5849        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5850    {
5851        return true;
5852    }
5853    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5854        return false;
5855    };
5856    let files: Vec<PathBuf> = match harness {
5857        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5858        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5859        HarnessId::OPENCODE => vec![
5860            home.join(".local/share/opencode/auth.json"),
5861            home.join(".config/opencode/auth.json"),
5862        ],
5863        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5864        HarnessId::GROK => vec![home.join(".grok/auth.json")],
5865        HarnessId::GEMINI => vec![
5866            home.join(".gemini/oauth_creds.json"),
5867            home.join(".gemini/google_accounts.json"),
5868        ],
5869        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5870        HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5871        _ => Vec::new(),
5872    };
5873    if files.into_iter().any(|path| {
5874        std::fs::metadata(path)
5875            .map(|metadata| metadata.is_file() && metadata.len() > 2)
5876            .unwrap_or(false)
5877    }) {
5878        return true;
5879    }
5880    // macOS keeps Claude Code's OAuth login in the Keychain, so
5881    // `.claude/.credentials.json` never exists there and the file probe above
5882    // reports a signed-in install as unauthenticated forever. A completed
5883    // login also writes an `oauthAccount` record into `~/.claude.json` on
5884    // every platform — file-based, prompt-free evidence (querying the
5885    // Keychain itself from an unsigned daemon can raise a UI prompt).
5886    if harness == HarnessId::CLAUDE_CODE {
5887        return std::fs::read_to_string(home.join(".claude.json"))
5888            .map(|text| text.contains("\"oauthAccount\""))
5889            .unwrap_or(false);
5890    }
5891    false
5892}
5893
5894fn looks_like_auth_error(message: &str) -> bool {
5895    let message = message.to_ascii_lowercase();
5896    [
5897        "auth",
5898        "login",
5899        "sign in",
5900        "sign-in",
5901        "credential",
5902        "unauthorized",
5903        "forbidden",
5904        "token",
5905    ]
5906    .iter()
5907    .any(|needle| message.contains(needle))
5908}
5909
5910fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5911    crate::RuntimeCapabilities {
5912        start_session: false,
5913        resume_session: false,
5914        attach_existing_process: false,
5915        send_input: false,
5916        stream_events: false,
5917        interrupt: false,
5918        steer: false,
5919        respond_to_requests: false,
5920    }
5921}
5922
5923fn truncate_text(text: &str, max_chars: usize) -> String {
5924    let mut chars = text.chars();
5925    let truncated = chars.by_ref().take(max_chars).collect::<String>();
5926    if chars.next().is_some() {
5927        format!("{truncated}…")
5928    } else {
5929        truncated
5930    }
5931}
5932
5933/// The process group a runtime's own handle names, when it names one.
5934///
5935/// Every adapter that spawns a local process spawns it as its own group
5936/// leader (`Command::process_group(0)`), so the endpoint's pid IS the group
5937/// id. A runtime reached over HTTP, or one supercode joined rather than
5938/// spawned, names no group here and is left alone.
5939fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5940    match &handle.endpoint {
5941        crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5942        crate::RuntimeEndpoint::Http { .. } => None,
5943    }
5944}
5945
5946/// SIGKILL a wedged runtime's whole process group, reporting whether there
5947/// was one to signal. This is the same group teardown a graceful `close`
5948/// performs; it runs here only when the graceful path blew its deadline,
5949/// because the task parked on the unanswered call still owns the process
5950/// handle and so no `Drop` of ours can reach it.
5951fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5952    match process_group {
5953        #[cfg(unix)]
5954        Some(pid) => {
5955            crate::lsp::kill_process_group(pid);
5956            true
5957        }
5958        #[cfg(not(unix))]
5959        Some(_) => false,
5960        None => false,
5961    }
5962}
5963
5964fn error_message(error: ServiceError) -> String {
5965    match error {
5966        ServiceError::InvalidParams(message)
5967        | ServiceError::Operation(message)
5968        | ServiceError::UnsupportedAction(message) => message,
5969        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5970        ServiceError::Sdk(error) => error.to_string(),
5971    }
5972}
5973
5974#[derive(Debug)]
5975enum ServiceError {
5976    InvalidParams(String),
5977    MethodNotFound,
5978    UnsupportedAction(String),
5979    Operation(String),
5980    Sdk(SdkError),
5981}
5982
5983fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5984    match error {
5985        ServiceError::InvalidParams(message) => {
5986            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5987        }
5988        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5989            SdkError::unsupported(operation)
5990        }
5991        ServiceError::Operation(message) => {
5992            let code = if message.contains("already in progress") {
5993                SdkErrorCode::Busy
5994            } else if message.contains("not supported by this runtime") {
5995                SdkErrorCode::UnsupportedAction
5996            } else if message.contains("unknown runtime connection") {
5997                SdkErrorCode::NotFound
5998            } else {
5999                SdkErrorCode::Execution
6000            };
6001            SdkError::new(code, operation, message)
6002        }
6003        ServiceError::Sdk(error) => error,
6004    }
6005}
6006
6007fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
6008    let error_code = error.code();
6009    let code = match error_code {
6010        SdkErrorCode::Unauthenticated => -32030,
6011        SdkErrorCode::Unauthorized => -32031,
6012        SdkErrorCode::ControllerRequired => -32032,
6013        SdkErrorCode::LeaseExpired => -32033,
6014        SdkErrorCode::InvalidArgument => -32602,
6015        SdkErrorCode::NotFound => -32004,
6016        SdkErrorCode::Busy => -32000,
6017        SdkErrorCode::UnsupportedAction => -32020,
6018        SdkErrorCode::Execution => -32002,
6019        SdkErrorCode::Transport => -32003,
6020    };
6021    json!({
6022        "jsonrpc": "2.0",
6023        "id": id,
6024        "error": {
6025            "code": code,
6026            "name": error_code,
6027            "operation": error.operation(),
6028            "message": error.to_string(),
6029        },
6030    })
6031}
6032
6033fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
6034    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
6035}
6036
6037fn operation(error: impl Into<crate::Error>) -> ServiceError {
6038    let error = error.into();
6039    match error {
6040        crate::Error::Sdk(error) => ServiceError::Sdk(error),
6041        error => ServiceError::Operation(error.to_string()),
6042    }
6043}
6044
6045/// ORCH-12 `harness.v1.memory.show|search` params. `homes` is the same
6046/// storage-root override every read-only method accepts, so a caller can
6047/// point the read at a fixture home without touching the real ones.
6048#[derive(Debug, Clone, Deserialize, Default)]
6049#[serde(default)]
6050struct MemoryRequest {
6051    /// Harness whose store is read. Required.
6052    harness: Option<String>,
6053    /// The needle, required by `search`.
6054    query: Option<String>,
6055    /// Hermes profile, OpenClaw agent, or Claude Code project.
6056    profile: Option<String>,
6057    /// Claude Code session id selecting a project store (`show` only).
6058    session: Option<String>,
6059    /// Include each document's whole text (`show` only).
6060    full: bool,
6061    /// Treat `query` as a regular expression (`search` only).
6062    regex: bool,
6063    /// Working tree whose project store is read.
6064    cwd: Option<std::path::PathBuf>,
6065    /// Storage roots to read.
6066    homes: crate::HarnessHomes,
6067}
6068
6069/// Read the memory noun. A harness with no memory store fails with
6070/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6071fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6072    let request = decode::<MemoryRequest>(params)?;
6073    let harness = request
6074        .harness
6075        .clone()
6076        .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6077    let to_service = |error: crate::memory::MemoryError| match error {
6078        crate::memory::MemoryError::UnsupportedHarness { .. }
6079        | crate::memory::MemoryError::SessionNotScoped { .. } => {
6080            ServiceError::UnsupportedAction(error.to_string())
6081        }
6082        other => ServiceError::InvalidParams(other.to_string()),
6083    };
6084    match method {
6085        "harness.v1.memory.show" => {
6086            let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6087                harness,
6088                profile: request.profile,
6089                session: request.session,
6090                full: request.full,
6091                cwd: request.cwd,
6092                homes: request.homes,
6093            })
6094            .map_err(to_service)?;
6095            Ok(json!({
6096                "schema": crate::memory::MEMORY_SCHEMA,
6097                "documents": documents,
6098            }))
6099        }
6100        "harness.v1.memory.search" => {
6101            let query = request
6102                .query
6103                .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6104            let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6105                harness,
6106                query,
6107                profile: request.profile,
6108                regex: request.regex,
6109                cwd: request.cwd,
6110                homes: request.homes,
6111            })
6112            .map_err(to_service)?;
6113            Ok(json!({
6114                "schema": crate::memory::MEMORY_SCHEMA,
6115                "matches": matches,
6116            }))
6117        }
6118        _ => Err(ServiceError::MethodNotFound),
6119    }
6120}
6121
6122/// ORCH-10 `harness.v1.profiles.list|get` params. `homes` is the same
6123/// storage-root override every read-only method accepts, so a caller can
6124/// point the read at a fixture home without touching the real ones.
6125#[derive(Debug, Clone, Deserialize)]
6126#[serde(default)]
6127struct ProfilesQuery {
6128    /// Restrict the listing to one harness. `get` requires it.
6129    harness: Option<String>,
6130    /// Profile name, required by `get`.
6131    name: Option<String>,
6132    /// Storage roots to read.
6133    homes: crate::HarnessHomes,
6134}
6135
6136impl Default for ProfilesQuery {
6137    fn default() -> Self {
6138        Self {
6139            harness: None,
6140            name: None,
6141            homes: crate::HarnessHomes::default(),
6142        }
6143    }
6144}
6145
6146/// Read the profile noun. A harness with no profile concept fails with
6147/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6148fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6149    let query = decode::<ProfilesQuery>(params)?;
6150    let to_service = |error: crate::profiles::ProfileError| match error {
6151        crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6152            ServiceError::UnsupportedAction(error.to_string())
6153        }
6154        crate::profiles::ProfileError::NotFound { .. } => {
6155            ServiceError::InvalidParams(error.to_string())
6156        }
6157    };
6158    match method {
6159        "harness.v1.profiles.list" => {
6160            let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6161                .map_err(to_service)?;
6162            Ok(json!({
6163                "schema": crate::profiles::PROFILES_SCHEMA,
6164                "profiles": profiles,
6165            }))
6166        }
6167        "harness.v1.profiles.get" => {
6168            let harness = query
6169                .harness
6170                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6171            let name = query
6172                .name
6173                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6174            let profile =
6175                crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6176            Ok(json!({
6177                "schema": crate::profiles::PROFILES_SCHEMA,
6178                "profile": profile,
6179            }))
6180        }
6181        _ => Err(ServiceError::MethodNotFound),
6182    }
6183}
6184
6185/// ORCH-14 `harness.v1.channels.list|status` params, the same storage-root
6186/// override every read-only method accepts so a caller can point the read at
6187/// a fixture home without touching the real ones.
6188#[derive(Debug, Clone, Deserialize)]
6189#[serde(default)]
6190struct ChannelsQuery {
6191    /// Restrict the listing to one harness. `status` requires it.
6192    harness: Option<String>,
6193    /// Channel name, required by `status`.
6194    name: Option<String>,
6195    /// Storage roots to read.
6196    homes: crate::HarnessHomes,
6197}
6198
6199impl Default for ChannelsQuery {
6200    fn default() -> Self {
6201        Self {
6202            harness: None,
6203            name: None,
6204            homes: crate::HarnessHomes::default(),
6205        }
6206    }
6207}
6208
6209/// Read the channel noun. A harness with no channel concept fails with
6210/// `UnsupportedAction` (RPC `-32020`), never an empty list. No row carries a
6211/// token, key or secret — see `crate::channels` "Secrecy".
6212#[derive(Debug, Clone, Deserialize)]
6213#[serde(default)]
6214struct RoutesQuery {
6215    harness: Option<String>,
6216    /// Restrict to routes targeting one profile / agent.
6217    profile: Option<String>,
6218    homes: crate::HarnessHomes,
6219}
6220
6221impl Default for RoutesQuery {
6222    fn default() -> Self {
6223        Self {
6224            harness: None,
6225            profile: None,
6226            homes: crate::HarnessHomes::default(),
6227        }
6228    }
6229}
6230
6231#[derive(Debug, Clone, Deserialize)]
6232#[serde(default)]
6233struct TriggersQuery {
6234    harness: Option<String>,
6235    homes: crate::HarnessHomes,
6236}
6237
6238impl Default for TriggersQuery {
6239    fn default() -> Self {
6240        Self {
6241            harness: None,
6242            homes: crate::HarnessHomes::default(),
6243        }
6244    }
6245}
6246
6247fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6248    let query = decode::<TriggersQuery>(params)?;
6249    let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6250        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6251    Ok(json!({
6252        "schema": crate::triggers::TRIGGERS_SCHEMA,
6253        "triggers": triggers,
6254    }))
6255}
6256
6257fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6258    let query = decode::<RoutesQuery>(params)?;
6259    let routes = crate::routes::list_routes(
6260        &query.homes,
6261        query.harness.as_deref(),
6262        query.profile.as_deref(),
6263    )
6264    .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6265    Ok(json!({
6266        "schema": crate::routes::ROUTES_SCHEMA,
6267        "routes": routes,
6268    }))
6269}
6270
6271fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6272    let query = decode::<ChannelsQuery>(params)?;
6273    let to_service = |error: crate::channels::ChannelError| match error {
6274        crate::channels::ChannelError::UnsupportedHarness { .. } => {
6275            ServiceError::UnsupportedAction(error.to_string())
6276        }
6277        crate::channels::ChannelError::NotFound { .. } => {
6278            ServiceError::InvalidParams(error.to_string())
6279        }
6280    };
6281    match method {
6282        "harness.v1.channels.list" => {
6283            let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6284                .map_err(to_service)?;
6285            Ok(json!({
6286                "schema": crate::channels::CHANNELS_SCHEMA,
6287                "channels": channels,
6288            }))
6289        }
6290        "harness.v1.channels.status" => {
6291            let harness = query
6292                .harness
6293                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6294            let name = query
6295                .name
6296                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6297            let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6298                .map_err(to_service)?;
6299            Ok(json!({
6300                "schema": crate::channels::CHANNELS_SCHEMA,
6301                "channel": channel,
6302            }))
6303        }
6304        _ => Err(ServiceError::MethodNotFound),
6305    }
6306}
6307
6308fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6309    json!({
6310        "jsonrpc": "2.0",
6311        "id": id,
6312        "error": {"code": code, "message": message},
6313    })
6314}
6315
6316#[cfg(test)]
6317mod tests {
6318    use super::*;
6319    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6320    use async_trait::async_trait;
6321    use std::io::Write;
6322    use std::path::PathBuf;
6323    use std::time::Instant;
6324
6325    #[test]
6326    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6327        let descriptor = SessionDescriptor {
6328            locator: SessionLocator {
6329                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6330                session_id: "live-session".into(),
6331                storage: StorageLocator::File {
6332                    path: PathBuf::from("/tmp/live-session.jsonl"),
6333                },
6334            },
6335            cwd: Some(PathBuf::from("/project")),
6336            title: None,
6337            preview_candidates: Vec::new(),
6338            latest_message_candidates: Vec::new(),
6339            updated_at_ms: Some(1),
6340            message_count: None,
6341            model: None,
6342            parent_session_id: None,
6343            child_session_count: 0,
6344            nouns: Default::default(),
6345        };
6346        let peer = crate::claude_peer::ClaudePeerSession {
6347            pid: 42,
6348            session_id: "live-session".into(),
6349            cwd: Some(PathBuf::from("/project")),
6350            name: "peer".into(),
6351            socket_path: PathBuf::from("/tmp/peer.sock"),
6352            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6353            updated_at_ms: Some(1),
6354            version: Some("test".into()),
6355        };
6356
6357        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6358        assert!(value["live_endpoint"]
6359            .as_str()
6360            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6361    }
6362
6363    struct EndingRuntime {
6364        handle: RuntimeHandle,
6365        event: Option<HarnessEvent>,
6366        close_failures: usize,
6367    }
6368
6369    #[async_trait]
6370    impl RuntimeConnection for EndingRuntime {
6371        fn handle(&self) -> &RuntimeHandle {
6372            &self.handle
6373        }
6374
6375        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6376            unreachable!("ending runtime does not accept input")
6377        }
6378
6379        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6380            Ok(self.event.take())
6381        }
6382
6383        async fn interrupt(&mut self) -> crate::Result<()> {
6384            Ok(())
6385        }
6386
6387        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6388            Ok(())
6389        }
6390
6391        async fn close(&mut self) -> crate::Result<()> {
6392            if self.close_failures > 0 {
6393                self.close_failures -= 1;
6394                return Err(crate::Error::Other(
6395                    "cleanup temporarily unavailable".into(),
6396                ));
6397            }
6398            Ok(())
6399        }
6400    }
6401
6402    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6403        Box::new(EndingRuntime {
6404            handle: RuntimeHandle {
6405                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6406                runtime_id: "ending-session".into(),
6407                endpoint: RuntimeEndpoint::LocalProcess {
6408                    pid: None,
6409                    command: vec!["ending-runtime".into()],
6410                    protocol: "test".into(),
6411                },
6412            },
6413            event,
6414            close_failures: 0,
6415        })
6416    }
6417
6418    #[tokio::test]
6419    async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6420        let mut service = HarnessSessionService::new();
6421        let handle = ending_runtime(None).handle().clone();
6422        let runtime_id = handle.runtime_id.clone();
6423        let opened = service
6424            .insert_runtime(Box::new(EndingRuntime {
6425                handle,
6426                event: None,
6427                close_failures: 1,
6428            }))
6429            .unwrap();
6430        let connection = opened["connection"].as_str().unwrap().to_string();
6431        service.terminal_launches.insert(
6432            connection.clone(),
6433            StructuredLaunch {
6434                cwd: PathBuf::from("/fixture"),
6435                program: "fixture".into(),
6436                arguments: Vec::new(),
6437                env: BTreeMap::new(),
6438            },
6439        );
6440        let first = service
6441            .handle_async(request(
6442                1,
6443                "harness.v1.runtimes.close",
6444                json!({"connection": connection}),
6445            ))
6446            .await;
6447        // The harness's own teardown failed and the caller is told so...
6448        assert!(first.get("error").is_some(), "{first}");
6449        // ...but the connection is gone all the same. A connection whose close
6450        // cannot complete is exactly the one that must not stay registered:
6451        // holding it would answer every later call on this node with a turn
6452        // that is never going to end.
6453        assert!(!service.runtimes.contains_key(&connection));
6454        assert!(!service.terminal_launches.contains_key(&connection));
6455        assert!(!service.runtime_sequences.contains_key(&runtime_id));
6456        let again = service
6457            .handle_async(request(
6458                2,
6459                "harness.v1.runtimes.close",
6460                json!({"connection": connection}),
6461            ))
6462            .await;
6463        assert_eq!(again["error"]["code"], -32602, "{again}");
6464    }
6465
6466    fn request(id: u64, method: &str, params: Value) -> Value {
6467        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6468    }
6469
6470    // ---- ORCH-6: conversation nouns on `sessions.*` ----------------------
6471
6472    fn hermes_store() -> PathBuf {
6473        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6474    }
6475
6476    /// The discovery response for the Hermes fixture home, with the one
6477    /// machine-specific value (the absolute store path) replaced so the exact
6478    /// same JSON can be committed and replayed by the UI story.
6479    fn hermes_discovery(params: Value) -> Value {
6480        let mut response =
6481            HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6482        let store = hermes_store().display().to_string();
6483        for session in response["result"]["sessions"]
6484            .as_array_mut()
6485            .expect("sessions array")
6486        {
6487            if session["locator"]["storage"]["path"] == json!(store) {
6488                session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6489            }
6490            // `activity` reports a wall-clock observation instant, not a fact
6491            // about the session; it would make this response differ on every
6492            // call. The nouns under test are all session facts.
6493            session.as_object_mut().unwrap().remove("activity");
6494        }
6495        response["result"].take()
6496    }
6497
6498    fn hermes_query() -> Value {
6499        json!({
6500            "harnesses": ["hermes"],
6501            "homes": {"hermes": hermes_store()},
6502        })
6503    }
6504
6505    fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6506        result["sessions"]
6507            .as_array()
6508            .expect("sessions array")
6509            .iter()
6510            .find(|session| session["locator"]["session_id"] == json!(id))
6511            .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6512    }
6513
6514    #[test]
6515    fn orch6_discover_rows_carry_the_conversation_nouns() {
6516        let result = hermes_discovery(hermes_query());
6517
6518        // A Telegram DM: reached on a channel, no repo — the workspace IS the
6519        // channel (D2 precedence), and `main` is not a profile.
6520        let dm = row(&result, "tg-dm-1");
6521        assert_eq!(dm["trigger"], json!("channel"));
6522        assert_eq!(dm["surface"]["platform"], json!("telegram"));
6523        assert_eq!(dm["surface"]["kind"], json!("dm"));
6524        assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6525        assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6526        assert_eq!(
6527            dm["workspace"],
6528            json!({"kind": "channel", "value": "telegram:123456"})
6529        );
6530        assert!(dm.get("profile").is_none(), "{dm:#}");
6531
6532        // A cron fire: recurring, with the job recovered from the minted id.
6533        let fire = row(&result, "cron_job42_20260902_120000");
6534        assert_eq!(fire["trigger"], json!("cron"));
6535        assert_eq!(
6536            fire["recurrence"],
6537            json!({"job_id": "job42", "kind": "cron"})
6538        );
6539        assert_eq!(fire["workspace"]["kind"], json!("repo"));
6540
6541        // A profiled group session with a pending handoff: repo workspace
6542        // wins over the channel, and the chat stays on the surface key.
6543        let coder = row(&result, "tg-coder-1");
6544        assert_eq!(coder["trigger"], json!("channel"));
6545        assert_eq!(coder["profile"], json!("coder"));
6546        assert_eq!(coder["surface"]["thread_id"], json!("55"));
6547        assert_eq!(
6548            coder["surface"]["key"],
6549            json!("agent:coder:telegram:group:-100777:55")
6550        );
6551        assert_eq!(
6552            coder["workspace"],
6553            json!({"kind": "repo", "value": "/workspace/project"})
6554        );
6555        assert_eq!(
6556            coder["cross_surface"],
6557            json!({"state": "pending", "platform": "discord"})
6558        );
6559
6560        // A plain ACP session stays human-triggered with no surface at all.
6561        let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6562        assert_eq!(acp["trigger"], json!("human"));
6563        assert!(acp.get("surface").is_none(), "{acp:#}");
6564        assert_eq!(acp["workspace"], json!({"kind": "none"}));
6565    }
6566
6567    #[test]
6568    fn orch6_discover_filters_by_harness_and_profile() {
6569        let mut params = hermes_query();
6570        params["profile"] = json!("coder");
6571        let result = hermes_discovery(params);
6572        let ids: Vec<&str> = result["sessions"]
6573            .as_array()
6574            .expect("sessions array")
6575            .iter()
6576            .map(|session| session["locator"]["session_id"].as_str().unwrap())
6577            .collect();
6578        assert_eq!(ids, vec!["tg-coder-1"]);
6579
6580        // A profile no session is routed through returns nothing rather than
6581        // silently ignoring the filter.
6582        let mut missing = hermes_query();
6583        missing["profile"] = json!("nobody");
6584        assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6585
6586        // The harness filter is `harnesses`; an id no harness answers to is
6587        // an empty page, never every store on the box.
6588        let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6589        assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6590    }
6591
6592    #[test]
6593    fn orch6_load_reports_the_same_nouns_as_discovery() {
6594        let mut service = HarnessSessionService::new();
6595        let loaded = service.handle(request(
6596            1,
6597            "harness.v1.sessions.load",
6598            json!({"locator": {
6599                "harness": "hermes",
6600                "session_id": "tg-coder-1",
6601                "storage": {"kind": "file", "path": hermes_store()},
6602            }}),
6603        ));
6604        let session = &loaded["result"]["session"];
6605        let discovered = hermes_discovery(hermes_query());
6606        let row = row(&discovered, "tg-coder-1");
6607        for noun in [
6608            "trigger",
6609            "surface",
6610            "profile",
6611            "recurrence",
6612            "cross_surface",
6613            "workspace",
6614        ] {
6615            assert_eq!(
6616                session[noun],
6617                row.get(noun).cloned().unwrap_or(Value::Null),
6618                "`{noun}` disagrees between sessions.load and sessions.discover"
6619            );
6620        }
6621    }
6622
6623    /// ORCH-10: the fixture homes, as the RPC's `homes` override. Hermes's
6624    /// home is named by its `state.db`; OpenClaw's is the state directory.
6625    fn profile_fixture_homes() -> Value {
6626        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6627        json!({
6628            "hermes": fixtures.join("hermes_home/state.db"),
6629            "openclaw": fixtures.join("openclaw_home"),
6630        })
6631    }
6632
6633    fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6634        response["result"]["profiles"]
6635            .as_array()
6636            .unwrap_or_else(|| panic!("no profiles array in {response}"))
6637            .iter()
6638            .find(|row| row["harness"] == harness && row["name"] == name)
6639            .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6640    }
6641
6642    /// dev/01: every source answers in one row shape, over the committed
6643    /// fixture homes — the Hermes profile directory and its `state.db`
6644    /// partition, the OpenClaw agent directories and `openclaw.json`, and
6645    /// supercode's own presets.
6646    #[test]
6647    fn profiles_list_reads_every_source_uniformly() {
6648        let mut service = HarnessSessionService::new();
6649        let response = service.handle(request(
6650            1,
6651            "harness.v1.profiles.list",
6652            json!({"homes": profile_fixture_homes()}),
6653        ));
6654        assert_eq!(
6655            response["result"]["schema"],
6656            crate::profiles::PROFILES_SCHEMA
6657        );
6658
6659        let default = profile_row(&response, "hermes", "default");
6660        assert_eq!(default["kind"], "hermes_profile");
6661        assert_eq!(default["default"], true);
6662        assert_eq!(default["routes"], 0);
6663        assert_eq!(default["sessions"], 11);
6664        assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6665
6666        let coder = profile_row(&response, "hermes", "coder");
6667        assert_eq!(coder["kind"], "hermes_profile");
6668        assert_eq!(coder["default"], false);
6669        assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6670        assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6671        assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6672        assert!(coder["home"]
6673            .as_str()
6674            .unwrap()
6675            .ends_with("hermes_home/profiles/coder"));
6676
6677        let main = profile_row(&response, "openclaw", "main");
6678        assert_eq!(main["kind"], "openclaw_agent");
6679        // No entry declares `default: true` (real configs do not), so `main`
6680        // wins on OpenClaw's own convention rather than alphabetically.
6681        assert_eq!(main["default"], true);
6682        assert_eq!(main["routes"], 0);
6683        assert_eq!(main["sessions"], 4);
6684        assert_eq!(
6685            main["model"],
6686            Value::Null,
6687            "`agents.defaults.model` is an install default, not this agent's pin"
6688        );
6689
6690        let design = profile_row(&response, "openclaw", "design");
6691        assert_eq!(design["default"], false);
6692        assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6693        assert_eq!(design["sessions"], 0);
6694        assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6695
6696        let preset = profile_row(&response, "supercode", "supercode-default");
6697        assert_eq!(preset["kind"], "preset");
6698        assert_eq!(preset["default"], true);
6699        assert_eq!(preset["home"], Value::Null);
6700        assert_eq!(preset["routes"], Value::Null);
6701    }
6702
6703    /// Codex's own profiles are `[profiles.<name>]` tables, with the
6704    /// top-level `profile` key naming the default.
6705    #[test]
6706    fn profiles_list_reads_codex_profile_tables() {
6707        let codex_home = std::env::temp_dir().join(format!(
6708            "supercode-orch10-codex-{}-{}",
6709            std::process::id(),
6710            std::time::SystemTime::now()
6711                .duration_since(std::time::UNIX_EPOCH)
6712                .unwrap()
6713                .as_nanos()
6714        ));
6715        std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6716        std::fs::write(
6717            codex_home.join("config.toml"),
6718            "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6719        )
6720        .unwrap();
6721
6722        let mut service = HarnessSessionService::new();
6723        let response = service.handle(request(
6724            1,
6725            "harness.v1.profiles.list",
6726            json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6727        ));
6728        let rows = response["result"]["profiles"].as_array().unwrap();
6729        assert_eq!(rows.len(), 2, "{response}");
6730        let review = profile_row(&response, "codex", "review");
6731        assert_eq!(review["kind"], "codex_profile");
6732        assert_eq!(review["default"], true);
6733        assert_eq!(review["model"], "gpt-5.1-codex");
6734        assert_eq!(review["home"], Value::Null);
6735        assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6736
6737        let got = service.handle(request(
6738            2,
6739            "harness.v1.profiles.get",
6740            json!({
6741                "harness": "codex",
6742                "name": "fast",
6743                "homes": {"codex": codex_home.join("sessions")},
6744            }),
6745        ));
6746        assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6747        std::fs::remove_dir_all(&codex_home).ok();
6748    }
6749
6750    /// A verb a harness lacks fails with `UnsupportedAction`, never a silent
6751    /// empty list; an unknown name is an invalid argument, not an empty row.
6752    #[test]
6753    fn profiles_refuse_harnesses_without_the_concept() {
6754        let mut service = HarnessSessionService::new();
6755        let response = service.handle(request(
6756            1,
6757            "harness.v1.profiles.list",
6758            json!({"harness": "claude-code"}),
6759        ));
6760        assert_eq!(response["error"]["code"], -32020, "{response}");
6761
6762        let missing = service.handle(request(
6763            2,
6764            "harness.v1.profiles.get",
6765            json!({
6766                "harness": "hermes",
6767                "name": "no-such-profile",
6768                "homes": profile_fixture_homes(),
6769            }),
6770        ));
6771        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6772    }
6773
6774    /// The two methods are advertised, so a client discovers them from
6775    /// `harness.v1.capabilities` rather than from documentation.
6776    #[test]
6777    fn profiles_methods_are_advertised() {
6778        let mut service = HarnessSessionService::new();
6779        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6780        let methods = response["result"]["methods"].as_array().unwrap();
6781        for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6782            assert!(
6783                methods.iter().any(|entry| entry == method),
6784                "{method} is not advertised"
6785            );
6786        }
6787    }
6788
6789    // -----------------------------------------------------------------
6790    // ORCH-14 — channels
6791    // -----------------------------------------------------------------
6792
6793    fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6794        response["result"]["channels"]
6795            .as_array()
6796            .unwrap_or_else(|| panic!("no channels array in {response}"))
6797            .iter()
6798            .find(|row| row["harness"] == harness && row["name"] == name)
6799            .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6800    }
6801
6802    fn channels_list(harness: Option<&str>) -> Value {
6803        let mut params = json!({"homes": profile_fixture_homes()});
6804        if let Some(harness) = harness {
6805            params["harness"] = json!(harness);
6806        }
6807        HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6808    }
6809
6810    /// dev/01: both sources answer in one row shape over the committed
6811    /// fixture homes — Hermes's `platforms:` blocks with their `extra` maps,
6812    /// and OpenClaw's `channels.<name>` entries split per account.
6813    #[test]
6814    fn channels_list_reads_both_gateway_harnesses_uniformly() {
6815        let response = channels_list(None);
6816        assert_eq!(
6817            response["result"]["schema"],
6818            crate::channels::CHANNELS_SCHEMA
6819        );
6820
6821        // Hermes: a credentialed platform, a bridged `extra.key` platform,
6822        // and one the config explicitly disables.
6823        let telegram = channel_row(&response, "hermes", "telegram");
6824        assert_eq!(telegram["kind"], "telegram");
6825        assert_eq!(telegram["enabled"], true);
6826        assert_eq!(telegram["configured"], true);
6827        // The `sessions` count is the discovery rows whose surface platform
6828        // is telegram: the fixture's `agent:main:telegram:…` DM and the
6829        // `agent:coder:telegram:…` group.
6830        assert_eq!(telegram["sessions"], 2);
6831        let api = channel_row(&response, "hermes", "api_server");
6832        assert_eq!(api["configured"], true, "extra.key is a credential key");
6833        assert_eq!(api["sessions"], 0);
6834        let webhook = channel_row(&response, "hermes", "webhook");
6835        assert_eq!(webhook["enabled"], false);
6836        // Hermes lists no credential for `webhook`: declaring it is all it
6837        // needs, so a credential-less entry is still `configured`.
6838        assert_eq!(webhook["configured"], true);
6839
6840        // OpenClaw: one row per account, named `<channel>/<accountId>`.
6841        let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6842        assert_eq!(linked["kind"], "slack");
6843        assert_eq!(linked["account"], "T0FIXTURE");
6844        assert_eq!(linked["enabled"], true);
6845        assert_eq!(linked["configured"], true);
6846        let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6847        assert_eq!(unlinked["enabled"], false);
6848        assert_eq!(
6849            unlinked["configured"], false,
6850            "an account with no credential key is not configured"
6851        );
6852        // A single-account channel keeps its own name and names its account
6853        // inline.
6854        let telegram = channel_row(&response, "openclaw", "telegram");
6855        assert_eq!(telegram["account"], "hermes-fixture-bot");
6856        assert_eq!(telegram["configured"], true);
6857
6858        // `status` is never claimed from a config file.
6859        for row in response["result"]["channels"].as_array().unwrap() {
6860            assert_eq!(row["status"], "unknown", "{row}");
6861        }
6862    }
6863
6864    /// dev/01: no field of any emitted row carries a credential. The fixture
6865    /// homes hold four FAKE credential strings; a row that leaked one — as a
6866    /// value, an account label, or a name — fails here.
6867    #[test]
6868    fn channels_rows_never_carry_a_fixture_secret() {
6869        let secrets = [
6870            "FAKE-TOKEN-DO-NOT-EMIT",
6871            "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6872            "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6873            "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6874            "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6875        ];
6876        // The strings really are in the fixtures, so this test can fail.
6877        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6878        let raw = format!(
6879            "{}{}",
6880            std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6881            std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6882        );
6883        for secret in secrets {
6884            assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6885        }
6886
6887        let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6888        for secret in secrets {
6889            assert!(
6890                !emitted.contains(secret),
6891                "`{secret}` leaked into a channel row: {emitted}"
6892            );
6893        }
6894        // Belt and braces: no row FIELD is credential-shaped either, so a
6895        // future field cannot smuggle one past the literal scan.
6896        for row in channels_list(None)["result"]["channels"]
6897            .as_array()
6898            .unwrap()
6899        {
6900            for key in row.as_object().unwrap().keys() {
6901                let key = key.to_ascii_lowercase();
6902                assert!(
6903                    !["token", "key", "secret", "password", "credential"]
6904                        .iter()
6905                        .any(|marker| key.ends_with(marker)),
6906                    "`{key}` is a credential-shaped field on a channel row"
6907                );
6908            }
6909        }
6910    }
6911
6912    /// `status` answers one row by name, and refuses an unknown one.
6913    #[test]
6914    fn channels_status_reads_one_row_by_name() {
6915        let mut service = HarnessSessionService::new();
6916        let got = service.handle(request(
6917            1,
6918            "harness.v1.channels.status",
6919            json!({
6920                "harness": "openclaw",
6921                "name": "slack/T0FIXTURE",
6922                "homes": profile_fixture_homes(),
6923            }),
6924        ));
6925        assert_eq!(got["result"]["channel"]["kind"], "slack");
6926        assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6927        assert_eq!(got["result"]["channel"]["status"], "unknown");
6928
6929        let missing = service.handle(request(
6930            2,
6931            "harness.v1.channels.status",
6932            json!({
6933                "harness": "openclaw",
6934                "name": "no-such-channel",
6935                "homes": profile_fixture_homes(),
6936            }),
6937        ));
6938        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6939    }
6940
6941    /// A harness with no channel concept fails with `UnsupportedAction`,
6942    /// never a silent empty list — Claude Code included, because its channels
6943    /// are MCP-protocol declarations no config file names.
6944    #[test]
6945    fn channels_refuse_harnesses_without_the_concept() {
6946        let response = channels_list(Some("claude-code"));
6947        assert_eq!(response["error"]["code"], -32020, "{response}");
6948        let codex = channels_list(Some("codex"));
6949        assert_eq!(codex["error"]["code"], -32020, "{codex}");
6950    }
6951
6952    /// The harness filter restricts the rows rather than being ignored.
6953    #[test]
6954    fn channels_list_filters_by_harness() {
6955        let response = channels_list(Some("openclaw"));
6956        let rows = response["result"]["channels"].as_array().unwrap();
6957        assert!(!rows.is_empty(), "{response}");
6958        assert!(
6959            rows.iter().all(|row| row["harness"] == "openclaw"),
6960            "harness filter leaked: {response}"
6961        );
6962    }
6963
6964    /// Both methods are advertised, so a client discovers them from
6965    /// `harness.v1.capabilities` rather than from documentation.
6966    #[test]
6967    fn channels_methods_are_advertised() {
6968        let mut service = HarnessSessionService::new();
6969        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6970        let methods = response["result"]["methods"].as_array().unwrap();
6971        for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6972            assert!(
6973                methods.iter().any(|entry| entry == method),
6974                "{method} is not advertised"
6975            );
6976        }
6977    }
6978
6979    /// The UI story renders REAL rows: this writes the discovery response the
6980    /// two assertions above pin into the fixture the Storybook
6981    /// `Compositions/Universal nouns` stories import, and fails when the
6982    /// committed copy has drifted from what the service now answers.
6983    #[test]
6984    fn orch6_story_fixture_matches_the_live_discovery_response() {
6985        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6986            .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6987        let mut result = hermes_discovery(hermes_query());
6988        // `updated_at_ms` is derived from the fixture's own stored timestamps,
6989        // so the whole response is deterministic; drop only the cursor, which
6990        // is pagination state rather than a session fact.
6991        result.as_object_mut().unwrap().remove("next_cursor");
6992        let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6993        if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6994            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6995            std::fs::write(&path, &rendered).unwrap();
6996        }
6997        let committed = std::fs::read_to_string(&path).unwrap_or_default();
6998        assert_eq!(
6999            committed, rendered,
7000            "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
7001             re-run with SUPERCODE_UPDATE_FIXTURES=1"
7002        );
7003    }
7004
7005    fn pi_locator() -> SessionLocator {
7006        SessionLocator {
7007            harness: HarnessId::from(HarnessId::PI),
7008            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
7009            storage: StorageLocator::File {
7010                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7011                    .join("tests/fixtures/pi_session.jsonl"),
7012            },
7013        }
7014    }
7015
7016    fn opencode_locator() -> SessionLocator {
7017        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
7018        SessionLocator {
7019            harness: HarnessId::from(HarnessId::OPENCODE),
7020            session_id: session_id.into(),
7021            storage: StorageLocator::Sqlite {
7022                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7023                    .join("tests/fixtures/opencode_fixture/opencode.db"),
7024                selector: session_id.into(),
7025            },
7026        }
7027    }
7028
7029    fn grok_locator() -> SessionLocator {
7030        SessionLocator {
7031            harness: HarnessId::from(HarnessId::GROK),
7032            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
7033            storage: StorageLocator::File {
7034                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7035                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
7036            },
7037        }
7038    }
7039
7040    // ---- ORCH-11: `harness.v1.skills.list` -------------------------------
7041
7042    fn fixture_homes() -> Value {
7043        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7044        json!({
7045            "claude_code": fixtures.join("__absent__"),
7046            "codex": fixtures.join("__absent__"),
7047            "opencode": fixtures.join("__absent__"),
7048            "pi": fixtures.join("__absent__"),
7049            "agents": fixtures.join("__absent__"),
7050            "hermes": fixtures.join("hermes_home"),
7051            "openclaw": fixtures.join("openclaw_home"),
7052        })
7053    }
7054
7055    #[test]
7056    fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
7057        let root = std::env::temp_dir().join(format!(
7058            "supercode-preview-rpc-{}-{}",
7059            std::process::id(),
7060            std::time::SystemTime::now()
7061                .duration_since(std::time::UNIX_EPOCH)
7062                .unwrap()
7063                .as_nanos()
7064        ));
7065        std::fs::create_dir_all(&root).unwrap();
7066        for id in ["first", "second"] {
7067            std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7068                json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7069                json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7070            )).unwrap();
7071        }
7072        let mut service = HarnessSessionService::new();
7073        let query = json!({
7074            "harnesses": ["codex"], "homes": {"codex": root},
7075            "query": "nebula", "search_previews": true, "limit": 1
7076        });
7077        let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7078        assert!(first.get("error").is_none(), "{first}");
7079        assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7080        assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7081        let mut next_query = query.clone();
7082        next_query["cursor"] = first["result"]["next_cursor"].clone();
7083        let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7084        assert_eq!(next["result"]["receipt"]["returned"], 1);
7085        assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7086        assert_eq!(next["result"]["receipt"]["truncated"], false);
7087        assert_ne!(
7088            first["result"]["sessions"][0]["locator"],
7089            next["result"]["sessions"][0]["locator"]
7090        );
7091        let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7092        assert!(
7093            refused["error"]["message"]
7094                .as_str()
7095                .unwrap()
7096                .contains("use sessions.discover"),
7097            "{refused}"
7098        );
7099        std::fs::remove_dir_all(root).unwrap();
7100    }
7101
7102    #[test]
7103    fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7104        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7105        let root = std::env::temp_dir().join(format!(
7106            "supercode-index-rpc-{}-{}",
7107            std::process::id(),
7108            std::time::SystemTime::now()
7109                .duration_since(std::time::UNIX_EPOCH)
7110                .unwrap()
7111                .as_nanos()
7112        ));
7113        std::fs::create_dir_all(&root).unwrap();
7114        for id in ["first", "second"] {
7115            std::fs::write(root.join(format!("{id}.jsonl")), format!(
7116                "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7117            )).unwrap();
7118        }
7119        let mut service = HarnessSessionService::new();
7120        let opened = service.handle(request(
7121            1,
7122            "harness.v1.sessions.index.subscribe",
7123            json!({
7124                "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7125            }),
7126        ));
7127        assert!(opened.get("error").is_none(), "{opened:#}");
7128        let subscription = opened["result"]["subscription"]
7129            .as_str()
7130            .unwrap()
7131            .to_owned();
7132        assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7133        for params in [
7134            json!({"subscription": subscription, "limit": 0}),
7135            json!({"subscription": subscription, "limit": 2049}),
7136            json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7137            json!({"subscription": "unknown", "limit": 2}),
7138        ] {
7139            let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7140            assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7141        }
7142        for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7143            let response = service.handle(request(
7144                3,
7145                "harness.v1.sessions.index.resize",
7146                json!({
7147                    "subscription": subscription, "limit": limit
7148                }),
7149            ));
7150            assert!(response.get("error").is_none(), "{response:#}");
7151            assert_eq!(response["result"]["subscription"], subscription);
7152            assert_eq!(response["result"]["revision"], revision);
7153            assert_eq!(
7154                response["result"]["initial"].as_array().unwrap().len(),
7155                limit
7156            );
7157            assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7158            assert_eq!(service.index_subscriptions.len(), 1);
7159        }
7160        let removed = service.handle(request(
7161            4,
7162            "harness.v1.sessions.index.unsubscribe",
7163            json!({
7164                "subscription": subscription
7165            }),
7166        ));
7167        assert_eq!(removed["result"]["removed"], true);
7168        let stale = service.handle(request(
7169            5,
7170            "harness.v1.sessions.index.resize",
7171            json!({
7172                "subscription": subscription, "limit": 1
7173            }),
7174        ));
7175        assert_eq!(stale["error"]["code"], -32602);
7176        drop(service);
7177        std::fs::remove_dir_all(root).unwrap();
7178    }
7179
7180    fn skills_rows(params: Value) -> Vec<Value> {
7181        let response =
7182            HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7183        assert!(response.get("error").is_none(), "{response:#}");
7184        response["result"].as_array().cloned().unwrap_or_default()
7185    }
7186
7187    /// The uniform row over two harnesses at once, from the harnesses' own
7188    /// skill roots: name, harness, scope, location, description, version.
7189    #[test]
7190    fn skills_list_reads_the_hermes_and_openclaw_roots() {
7191        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7192        let rows = skills_rows(json!({
7193            "homes": fixture_homes(),
7194            "cwd": fixtures.join("hermes_home"),
7195        }));
7196        let arxiv = rows
7197            .iter()
7198            .find(|row| row["name"] == json!("arxiv-search"))
7199            .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7200        assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7201        assert_eq!(arxiv["scope"], json!("user"));
7202        assert_eq!(arxiv["version"], json!("1.4.0"));
7203        assert!(arxiv["location"]
7204            .as_str()
7205            .unwrap()
7206            .ends_with("hermes_home/skills/research/arxiv"));
7207
7208        // A directory with no SKILL.md still lists, by directory name.
7209        let bare = rows
7210            .iter()
7211            .find(|row| row["name"] == json!("bare-skill"))
7212            .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7213        assert_eq!(bare["enabled"], json!(null));
7214        assert!(bare.get("description").is_none());
7215
7216        let demo = rows
7217            .iter()
7218            .find(|row| row["name"] == json!("clawhub-demo"))
7219            .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7220        assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7221        assert_eq!(demo["scope"], json!("managed"));
7222        assert_eq!(demo["enabled"], json!(false));
7223    }
7224
7225    /// Both filters select against the same rows.
7226    #[test]
7227    fn skills_list_filters_by_harness_and_scope() {
7228        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7229        let hermes = skills_rows(json!({
7230            "homes": fixture_homes(),
7231            "cwd": fixtures.join("hermes_home"),
7232            "harness": HarnessId::HERMES,
7233        }));
7234        assert!(!hermes.is_empty());
7235        assert!(hermes
7236            .iter()
7237            .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7238
7239        let managed = skills_rows(json!({
7240            "homes": fixture_homes(),
7241            "cwd": fixtures.join("openclaw_home"),
7242            "harness": HarnessId::OPENCLAW,
7243            "scope": "managed",
7244        }));
7245        assert_eq!(managed.len(), 1, "{managed:#?}");
7246        assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7247
7248        let bundled = skills_rows(json!({
7249            "homes": fixture_homes(),
7250            "cwd": fixtures.join("openclaw_home"),
7251            "harness": HarnessId::OPENCLAW,
7252            "scope": "bundled",
7253        }));
7254        assert!(bundled.is_empty(), "{bundled:#?}");
7255    }
7256
7257    /// A harness supercode has no skills root for is refused by name, not
7258    /// answered with an empty list.
7259    #[test]
7260    fn skills_list_refuses_an_unknown_harness() {
7261        let response = HarnessSessionService::new().handle(request(
7262            1,
7263            "harness.v1.skills.list",
7264            json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7265        ));
7266        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7267        assert!(response["error"]["message"]
7268            .as_str()
7269            .unwrap()
7270            .contains("not-a-harness"));
7271    }
7272
7273    /// The method is advertised, and its SDK operation resolves it.
7274    #[test]
7275    fn skills_list_is_an_advertised_method_and_sdk_operation() {
7276        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7277        assert_eq!(
7278            SdkOperation::from_method("harness.v1.skills.list"),
7279            Some(SdkOperation::SkillsList)
7280        );
7281    }
7282
7283    // ---- ORCH-22: `harness.v1.skills.install|remove` ----------------------
7284
7285    /// Both controlled verbs are advertised and resolve to their operation.
7286    #[test]
7287    fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7288        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7289        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7290        assert_eq!(
7291            SdkOperation::from_method("harness.v1.skills.install"),
7292            Some(SdkOperation::SkillsInstall)
7293        );
7294        assert_eq!(
7295            SdkOperation::from_method("harness.v1.skills.remove"),
7296            Some(SdkOperation::SkillsRemove)
7297        );
7298    }
7299
7300    /// The directory door, end to end over the RPC: a local package lands in
7301    /// Claude Code's own user root and the outcome carries the operation and
7302    /// the row the ORCH-11 loader reads back.
7303    #[test]
7304    fn skills_install_and_remove_drive_the_directory_door() {
7305        let root = std::env::temp_dir().join(format!(
7306            "supercode-orch22-rpc-{}-{}",
7307            std::process::id(),
7308            std::time::SystemTime::now()
7309                .duration_since(std::time::UNIX_EPOCH)
7310                .unwrap()
7311                .as_nanos()
7312        ));
7313        let source = root.join("probe-src");
7314        std::fs::create_dir_all(&source).unwrap();
7315        std::fs::write(
7316            source.join("SKILL.md"),
7317            "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7318        )
7319        .unwrap();
7320        let homes = json!({
7321            "claude_code": root.join("claude_home"),
7322            "codex": root.join("__absent__"),
7323            "opencode": root.join("__absent__"),
7324            "pi": root.join("__absent__"),
7325            "hermes": root.join("__absent__"),
7326            "openclaw": root.join("__absent__"),
7327            "agents": root.join("__absent__"),
7328        });
7329
7330        let mut service = HarnessSessionService::new();
7331        let installed = service.handle(request(
7332            1,
7333            "harness.v1.skills.install",
7334            json!({
7335                "harness": HarnessId::CLAUDE_CODE,
7336                "source": source,
7337                "scope": "user",
7338                "cwd": root,
7339                "homes": homes,
7340            }),
7341        ));
7342        let result = &installed["result"];
7343        assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7344        assert_eq!(result["verb"], json!("install"));
7345        assert!(result["ran"]
7346            .as_str()
7347            .is_some_and(|ran| ran.starts_with("cp -R ")));
7348        assert_eq!(result["skill"]["scope"], json!("user"));
7349
7350        let removed = service.handle(request(
7351            2,
7352            "harness.v1.skills.remove",
7353            json!({
7354                "harness": HarnessId::CLAUDE_CODE,
7355                "name": "orch22-rpc",
7356                "scope": "user",
7357                "cwd": root,
7358                "homes": homes,
7359            }),
7360        ));
7361        assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7362        assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7363        std::fs::remove_dir_all(&root).ok();
7364    }
7365
7366    /// OpenClaw publishes no `skills remove` at the pin, so the uniform verb
7367    /// refuses with UnsupportedAction instead of deleting files itself.
7368    #[test]
7369    fn skills_remove_refuses_openclaw_at_the_pin() {
7370        let response = HarnessSessionService::new().handle(request(
7371            1,
7372            "harness.v1.skills.remove",
7373            json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7374        ));
7375        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7376        assert!(response["error"]["message"]
7377            .as_str()
7378            .unwrap()
7379            .contains("no `skills remove` verb"));
7380    }
7381
7382    /// A harness with no skills root at all is refused by name, with the
7383    /// same sentence `skills.list` gives it.
7384    #[test]
7385    fn skills_install_refuses_a_harness_without_a_skills_root() {
7386        let response = HarnessSessionService::new().handle(request(
7387            1,
7388            "harness.v1.skills.install",
7389            json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7390        ));
7391        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7392        assert!(response["error"]["message"]
7393            .as_str()
7394            .unwrap()
7395            .contains("not-a-harness"));
7396    }
7397
7398    // ---- ORCH-12: `harness.v1.memory.show|search` ------------------------
7399
7400    /// `HarnessHomes` for the committed fixture homes. Every root a test does
7401    /// not name is pinned at an absent path, so a read can never fall through
7402    /// to this machine's real harness homes. Note `hermes` is the `state.db`
7403    /// PATH (its parent is HERMES_HOME) and `claude_code` is the `projects`
7404    /// directory — the same contract discovery uses.
7405    fn memory_homes() -> Value {
7406        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7407        json!({
7408            "claude_code": fixtures.join("__absent__"),
7409            "codex": fixtures.join("__absent__"),
7410            "opencode": fixtures.join("__absent__"),
7411            "pi": fixtures.join("__absent__"),
7412            "grok": fixtures.join("__absent__"),
7413            "gemini": fixtures.join("__absent__"),
7414            "goose": fixtures.join("__absent__"),
7415            "supercode": fixtures.join("__absent__"),
7416            "hermes": fixtures.join("hermes_home/state.db"),
7417            "openclaw": fixtures.join("openclaw_home"),
7418        })
7419    }
7420
7421    fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7422        let response = HarnessSessionService::new().handle(request(1, method, params));
7423        assert!(response.get("error").is_none(), "{response:#}");
7424        assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7425        response["result"][key]
7426            .as_array()
7427            .cloned()
7428            .unwrap_or_default()
7429    }
7430
7431    fn memory_documents(params: Value) -> Vec<Value> {
7432        memory_call_ok("harness.v1.memory.show", params, "documents")
7433    }
7434
7435    fn memory_matches(params: Value) -> Vec<Value> {
7436        memory_call_ok("harness.v1.memory.search", params, "matches")
7437    }
7438
7439    fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7440        rows.iter()
7441            .find(|row| row["profile"] == profile && row["name"] == name)
7442            .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7443    }
7444
7445    /// Hermes: the built-in `MEMORY.md`/`USER.md` pair and the `memories/`
7446    /// topic files, for HERMES_HOME itself and for every profile home.
7447    #[test]
7448    fn memory_show_reads_the_hermes_profile_homes() {
7449        let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7450
7451        let notes = find_document(&rows, "default", "MEMORY.md");
7452        assert_eq!(notes["harness"], "hermes");
7453        assert_eq!(notes["scope"], "user");
7454        assert!(notes["size"].as_u64().unwrap() > 0);
7455        assert!(notes["updated_at"].is_string(), "{notes:#?}");
7456        // The default answer previews the head and never the whole body.
7457        assert!(notes.get("content").is_none(), "{notes:#?}");
7458        assert_eq!(notes["truncated"], true);
7459        assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7460
7461        let user = find_document(&rows, "default", "USER.md");
7462        assert_eq!(user["scope"], "user");
7463        assert!(user["preview"]
7464            .as_array()
7465            .unwrap()
7466            .iter()
7467            .any(|line| line.as_str().unwrap().contains("neovim")));
7468
7469        let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7470        assert!(topic["path"]
7471            .as_str()
7472            .unwrap()
7473            .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7474
7475        // Profile mode points HERMES_HOME at `<root>/profiles/<name>`.
7476        let coder = find_document(&rows, "coder", "MEMORY.md");
7477        assert_eq!(coder["scope"], "profile");
7478        assert!(coder["path"]
7479            .as_str()
7480            .unwrap()
7481            .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7482    }
7483
7484    /// `full` is the only way a body crosses the wire, and `profile` narrows
7485    /// the read to one home.
7486    #[test]
7487    fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7488        let rows = memory_documents(json!({
7489            "harness": "hermes",
7490            "profile": "coder",
7491            "full": true,
7492            "homes": memory_homes(),
7493        }));
7494        assert!(
7495            rows.iter().all(|row| row["profile"] == "coder"),
7496            "{rows:#?}"
7497        );
7498        let coder = find_document(&rows, "coder", "MEMORY.md");
7499        assert!(coder["content"]
7500            .as_str()
7501            .expect("full returns the body")
7502            .contains("anthropic/claude-opus-4-8"));
7503    }
7504
7505    /// OpenClaw: memory-core's files under each agent's workspace —
7506    /// `<state>/workspace` for the default agent, `<state>/workspace-<id>`
7507    /// for any other.
7508    #[test]
7509    fn memory_show_reads_the_openclaw_agent_workspaces() {
7510        let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7511
7512        let main = find_document(&rows, "main", "MEMORY.md");
7513        assert_eq!(main["scope"], "agent");
7514        assert!(main["path"]
7515            .as_str()
7516            .unwrap()
7517            .ends_with("openclaw_home/workspace/MEMORY.md"));
7518
7519        let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7520        assert!(topic["path"]
7521            .as_str()
7522            .unwrap()
7523            .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7524
7525        let design = find_document(&rows, "design", "MEMORY.md");
7526        assert!(design["path"]
7527            .as_str()
7528            .unwrap()
7529            .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7530    }
7531
7532    /// Claude Code: the auto-memory directory of the project the working tree
7533    /// belongs to, keyed by the enclosing git repository.
7534    #[test]
7535    fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7536        let scratch = std::env::temp_dir().join(format!(
7537            "supercode-orch12-cc-{}-{}",
7538            std::process::id(),
7539            std::time::SystemTime::now()
7540                .duration_since(std::time::UNIX_EPOCH)
7541                .unwrap()
7542                .as_nanos()
7543        ));
7544        let project = scratch.join("repo");
7545        std::fs::create_dir_all(project.join(".git")).unwrap();
7546        // Auto-memory is shared across a repo's worktrees, so a nested
7547        // working directory must resolve to the repo's own project dir.
7548        let worktree = project.join("crates/harness");
7549        std::fs::create_dir_all(&worktree).unwrap();
7550        let slug: String = project
7551            .to_string_lossy()
7552            .chars()
7553            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7554            .collect();
7555        let projects = scratch.join("claude/projects");
7556        let memory = projects.join(&slug).join("memory");
7557        std::fs::create_dir_all(&memory).unwrap();
7558        std::fs::write(
7559            memory.join("MEMORY.md"),
7560            "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7561        )
7562        .unwrap();
7563        std::fs::write(
7564            memory.join("build-box.md"),
7565            "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7566        )
7567        .unwrap();
7568
7569        let mut homes = memory_homes();
7570        homes["claude_code"] = json!(projects);
7571        let rows = memory_documents(json!({
7572            "harness": "claude-code",
7573            "cwd": worktree,
7574            "homes": homes,
7575        }));
7576        let index = find_document(&rows, &slug, "MEMORY.md");
7577        assert_eq!(index["harness"], "claude-code");
7578        assert_eq!(index["scope"], "project");
7579        let topic = find_document(&rows, &slug, "build-box.md");
7580        assert!(topic["preview"]
7581            .as_array()
7582            .unwrap()
7583            .iter()
7584            .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7585
7586        let hits = memory_matches(json!({
7587            "harness": "claude-code",
7588            "query": "pinned harnesses",
7589            "cwd": worktree,
7590            "homes": homes,
7591        }));
7592        assert_eq!(hits.len(), 1, "{hits:#?}");
7593        assert_eq!(hits[0]["name"], "MEMORY.md");
7594        assert_eq!(hits[0]["line"], 2);
7595
7596        let _ = std::fs::remove_dir_all(&scratch);
7597    }
7598
7599    /// A config-less OpenClaw install declares no default agent, but
7600    /// memory-core still resolves ONE agent to the default `workspace`
7601    /// directory — the same `main`-then-first convention the profile rows
7602    /// use. Measured against `openclaw memory status` on the pinned CLI
7603    /// (`docs/interop/research/orch12-memory-receipt-2026-09-03.json`).
7604    #[test]
7605    fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7606        let state = std::env::temp_dir().join(format!(
7607            "supercode-orch12-oc-{}-{}",
7608            std::process::id(),
7609            std::time::SystemTime::now()
7610                .duration_since(std::time::UNIX_EPOCH)
7611                .unwrap()
7612                .as_nanos()
7613        ));
7614        // No `openclaw.json`: only the agent home the gateway creates.
7615        std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7616        std::fs::create_dir_all(state.join("workspace")).unwrap();
7617        std::fs::write(
7618            state.join("workspace/MEMORY.md"),
7619            "the gateway websocket needs credentials\n",
7620        )
7621        .unwrap();
7622
7623        let mut homes = memory_homes();
7624        homes["openclaw"] = json!(state);
7625        let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7626        assert_eq!(rows.len(), 1, "{rows:#?}");
7627        let row = find_document(&rows, "main", "MEMORY.md");
7628        assert_eq!(row["scope"], "agent");
7629        assert!(row["path"]
7630            .as_str()
7631            .unwrap()
7632            .ends_with("workspace/MEMORY.md"));
7633
7634        let _ = std::fs::remove_dir_all(&state);
7635    }
7636
7637    /// Search is a plain scan over the same documents: a hit carries the
7638    /// path, line and excerpt; a miss is an empty list, not an error.
7639    #[test]
7640    fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7641        let hit = memory_matches(json!({
7642            "harness": "hermes",
7643            "query": "NEOVIM",
7644            "homes": memory_homes(),
7645        }));
7646        assert_eq!(hit.len(), 1, "{hit:#?}");
7647        assert_eq!(hit[0]["harness"], "hermes");
7648        assert_eq!(hit[0]["name"], "USER.md");
7649        assert_eq!(hit[0]["scope"], "user");
7650        assert_eq!(hit[0]["line"], 5);
7651        assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7652
7653        // A regular expression reaches the same lines.
7654        let regex = memory_matches(json!({
7655            "harness": "hermes",
7656            "query": "neo(vim|vi)",
7657            "regex": true,
7658            "homes": memory_homes(),
7659        }));
7660        assert_eq!(regex.len(), 1, "{regex:#?}");
7661
7662        let miss = memory_matches(json!({
7663            "harness": "hermes",
7664            "query": "no-memory-line-says-this",
7665            "homes": memory_homes(),
7666        }));
7667        assert!(miss.is_empty(), "{miss:#?}");
7668    }
7669
7670    /// The uniform-verb contract: a harness with no memory store at the pin
7671    /// is refused by name, and `session` only selects a Claude Code project.
7672    #[test]
7673    fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7674        for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7675            let response = HarnessSessionService::new().handle(request(
7676                1,
7677                method,
7678                json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7679            ));
7680            assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7681            assert!(response["error"]["message"]
7682                .as_str()
7683                .unwrap()
7684                .contains("codex"));
7685        }
7686
7687        let response = HarnessSessionService::new().handle(request(
7688            1,
7689            "harness.v1.memory.show",
7690            json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7691        ));
7692        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7693
7694        // `harness` is not optional: memory documents are the user's prose.
7695        let response = HarnessSessionService::new().handle(request(
7696            1,
7697            "harness.v1.memory.show",
7698            json!({"homes": memory_homes()}),
7699        ));
7700        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7701    }
7702
7703    /// Both methods are advertised, and their SDK operations resolve them.
7704    #[test]
7705    fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7706        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7707        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7708        assert_eq!(
7709            SdkOperation::from_method("harness.v1.memory.show"),
7710            Some(SdkOperation::MemoryShow)
7711        );
7712        assert_eq!(
7713            SdkOperation::from_method("harness.v1.memory.search"),
7714            Some(SdkOperation::MemorySearch)
7715        );
7716    }
7717
7718    // ---- ORCH-9: `harness.v1.approvals.list` -----------------------------
7719
7720    /// A runtime that raises one protocol request and then goes quiet, so a
7721    /// single poll delivers the request without closing the connection.
7722    struct RequestingRuntime {
7723        handle: RuntimeHandle,
7724        events: std::collections::VecDeque<HarnessEvent>,
7725        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7726    }
7727
7728    #[async_trait]
7729    impl RuntimeConnection for RequestingRuntime {
7730        fn handle(&self) -> &RuntimeHandle {
7731            &self.handle
7732        }
7733
7734        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7735            unreachable!("this runtime only raises requests")
7736        }
7737
7738        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7739            match self.events.pop_front() {
7740                Some(event) => Ok(Some(event)),
7741                // Quiet, not closed: `poll_sdk_events` times out and leaves
7742                // the connection open, the way a runtime blocked on a
7743                // permission request behaves.
7744                None => std::future::pending().await,
7745            }
7746        }
7747
7748        async fn interrupt(&mut self) -> crate::Result<()> {
7749            Ok(())
7750        }
7751
7752        async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7753            // Both halves are recorded: ORCH-20 has to prove not just that the
7754            // right request was answered but that the door received its own
7755            // reply envelope.
7756            self.answered
7757                .lock()
7758                .unwrap_or_else(std::sync::PoisonError::into_inner)
7759                .push(json!({"request_id": request_id, "response": response}));
7760            Ok(())
7761        }
7762
7763        async fn close(&mut self) -> crate::Result<()> {
7764            Ok(())
7765        }
7766    }
7767
7768    fn requesting_runtime(
7769        harness: &str,
7770        events: Vec<HarnessEvent>,
7771        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7772    ) -> Box<dyn RuntimeConnection> {
7773        requesting_runtime_named(harness, "hermes-live-session", events, answered)
7774    }
7775
7776    fn requesting_runtime_named(
7777        harness: &str,
7778        runtime_id: &str,
7779        events: Vec<HarnessEvent>,
7780        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7781    ) -> Box<dyn RuntimeConnection> {
7782        Box::new(RequestingRuntime {
7783            handle: RuntimeHandle {
7784                harness: HarnessId::from(harness),
7785                runtime_id: runtime_id.into(),
7786                endpoint: RuntimeEndpoint::LocalProcess {
7787                    pid: None,
7788                    command: vec!["hermes-acp".into()],
7789                    protocol: "acp".into(),
7790                },
7791            },
7792            events: events.into(),
7793            answered,
7794        })
7795    }
7796
7797    fn permission_event(id: u64, title: &str) -> HarnessEvent {
7798        HarnessEvent {
7799            sequence: None,
7800            kind: "session/request_permission".into(),
7801            payload: json!({
7802                "jsonrpc": "2.0",
7803                "id": id,
7804                "method": "session/request_permission",
7805                "params": {
7806                    "sessionId": "hermes-live-session",
7807                    "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7808                    "options": [
7809                        {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7810                        {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7811                        {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7812                    ],
7813                },
7814            }),
7815        }
7816    }
7817
7818    fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7819        let response = service.handle(request(1, "harness.v1.approvals.list", params));
7820        assert!(response.get("error").is_none(), "{response:#}");
7821        response["result"].clone()
7822    }
7823
7824    /// ORC-2 dev/01: the same uniform loop over the CLAUDE CODE door. The
7825    /// `can_use_tool` control request the CLI raises to its registered
7826    /// permission handler lists as one pending row, `approvals.resolve <id>
7827    /// allow_once` sends the `{behavior}` result the CLI accepts through
7828    /// `runtimes.respond`, and the row is gone. The frame is the one claude
7829    /// 2.1.258 wrote, transcribed from
7830    /// `docs/interop/research/orc2-claude-respond-receipt-2026-09-04.json`.
7831    #[tokio::test]
7832    async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7833        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7834        let mut service = HarnessSessionService::new();
7835        service.runtimes.insert(
7836            "runtime-cc".into(),
7837            requesting_runtime_named(
7838                HarnessId::CLAUDE_CODE,
7839                "claude-live-session",
7840                vec![HarnessEvent {
7841                    sequence: None,
7842                    kind: "control_request".into(),
7843                    payload: json!({
7844                        "type": "control_request",
7845                        "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7846                        "request": {
7847                            "subtype": "can_use_tool",
7848                            "tool_name": "Bash",
7849                            "display_name": "Bash",
7850                            "input": {"command": "touch probe-artifact.txt"},
7851                            "tool_use_id": "toolu_mock_1",
7852                        },
7853                    }),
7854                }],
7855                answered.clone(),
7856            ),
7857        );
7858
7859        let notifications = service.poll_runtimes().await;
7860        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7861
7862        let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7863        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7864        let row = &rows[0];
7865        assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7866        assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7867        assert_eq!(row["status"], "pending");
7868        assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7869        assert_eq!(row["runtime_id"], "claude-live-session");
7870        assert_eq!(
7871            row["options"]
7872                .as_array()
7873                .unwrap()
7874                .iter()
7875                .map(|option| option["id"].as_str().unwrap())
7876                .collect::<Vec<_>>(),
7877            vec!["allow", "deny"],
7878        );
7879
7880        let response = resolve(
7881            &mut service,
7882            json!({"id": row["id"], "decision": "allow_once"}),
7883        )
7884        .await;
7885        assert!(response.get("error").is_none(), "{response:#}");
7886        assert_eq!(response["result"]["option_id"], "allow");
7887        assert_eq!(
7888            answered
7889                .lock()
7890                .unwrap_or_else(std::sync::PoisonError::into_inner)
7891                .as_slice(),
7892            &[json!({
7893                "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7894                "response": {"behavior": "allow"},
7895            })],
7896        );
7897        assert_eq!(
7898            approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7899                .as_array()
7900                .map(Vec::len),
7901            Some(0),
7902        );
7903    }
7904
7905    /// dev/01: a live ACP permission request raised on a driven runtime is
7906    /// listable while the turn is blocked on it, and stops being listable
7907    /// the moment `runtimes.respond` answers it.
7908    #[tokio::test]
7909    async fn a_live_permission_request_lists_until_it_is_answered() {
7910        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7911        let mut service = HarnessSessionService::new();
7912        service.runtimes.insert(
7913            "runtime-1".into(),
7914            requesting_runtime(
7915                HarnessId::HERMES,
7916                vec![permission_event(7, "rm -rf build")],
7917                answered.clone(),
7918            ),
7919        );
7920
7921        let notifications = service.poll_runtimes().await;
7922        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7923
7924        let rows = approvals(&mut service, json!({}));
7925        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7926        let row = &rows[0];
7927        assert_eq!(row["id"], "runtime-1/7");
7928        assert_eq!(row["harness"], HarnessId::HERMES);
7929        assert_eq!(row["kind"], "live");
7930        assert_eq!(row["status"], "pending");
7931        assert_eq!(row["subject"], "rm -rf build");
7932        assert_eq!(row["session_id"], "hermes-live-session");
7933        assert_eq!(row["runtime_id"], "hermes-live-session");
7934        assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7935        assert!(
7936            row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7937            "{row:#}"
7938        );
7939        assert_eq!(
7940            row["options"]
7941                .as_array()
7942                .unwrap()
7943                .iter()
7944                .map(|option| option["id"].as_str().unwrap())
7945                .collect::<Vec<_>>(),
7946            vec!["allow_once", "allow_for_session", "deny"],
7947        );
7948
7949        // The filters select against the same rows.
7950        assert_eq!(
7951            approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7952                .as_array()
7953                .map(Vec::len),
7954            Some(1),
7955        );
7956        assert_eq!(
7957            approvals(&mut service, json!({"session": "some-other-session"}))
7958                .as_array()
7959                .map(Vec::len),
7960            Some(0),
7961        );
7962
7963        let response = service
7964            .handle_async(request(
7965                2,
7966                "harness.v1.runtimes.respond",
7967                json!({
7968                    "connection": "runtime-1",
7969                    "request_id": 7,
7970                    "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7971                }),
7972            ))
7973            .await;
7974        assert!(response.get("error").is_none(), "{response:#}");
7975        assert_eq!(
7976            answered
7977                .lock()
7978                .unwrap_or_else(std::sync::PoisonError::into_inner)
7979                .as_slice(),
7980            &[json!({
7981                "request_id": 7,
7982                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7983            })],
7984        );
7985
7986        let rows = approvals(&mut service, json!({}));
7987        assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7988    }
7989
7990    /// dev/01: supercode's own queued subagent approvals list through the
7991    /// same door, carrying the outcome the record holds.
7992    #[test]
7993    fn queued_subagent_approvals_list_through_the_same_door() {
7994        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7995            crate::subagents::QueuedApproval {
7996                child_agent_id: "child-7".into(),
7997                tool: "shell".into(),
7998                subject: Some("cargo publish --dry-run".into()),
7999                queued_at_ms: 1,
8000                outcome: None,
8001            },
8002            crate::subagents::QueuedApproval {
8003                child_agent_id: "child-8".into(),
8004                tool: "write_file".into(),
8005                subject: None,
8006                queued_at_ms: 2,
8007                outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
8008            },
8009        ]));
8010        let mut service = HarnessSessionService::new();
8011        service.observe_subagent_approvals(queue);
8012
8013        let rows = approvals(&mut service, json!({}));
8014        assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
8015        assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
8016        assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
8017        assert_eq!(rows[0]["status"], "pending");
8018        assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
8019        assert_eq!(rows[1]["status"], "denied");
8020        assert!(rows[1]["options"].as_array().unwrap().is_empty());
8021
8022        // `--session` addresses a subagent row by its child agent id.
8023        let only = approvals(&mut service, json!({"session": "child-8"}));
8024        assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
8025        assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
8026    }
8027
8028    /// The uniform-verb contract: an id whose runtime door cannot carry a
8029    /// protocol request is refused BY NAME rather than answered with an empty
8030    /// list. Since ORC-2 gave Claude Code a permission-response primitive
8031    /// every registered harness can carry one, so the refusal is exercised on
8032    /// an unknown id — and the registered ids are asserted to be accepted.
8033    #[test]
8034    fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
8035        let response = HarnessSessionService::new().handle(request(
8036            1,
8037            "harness.v1.approvals.list",
8038            json!({"harness": "not-a-harness"}),
8039        ));
8040        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
8041        assert!(response["error"]["message"]
8042            .as_str()
8043            .unwrap()
8044            .contains("not-a-harness"));
8045        for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
8046            let response = HarnessSessionService::new().handle(request(
8047                1,
8048                "harness.v1.approvals.list",
8049                json!({"harness": harness}),
8050            ));
8051            assert!(response.get("error").is_none(), "{harness}: {response:#}");
8052        }
8053    }
8054
8055    /// The method is advertised, its SDK operation resolves it, and the
8056    /// registry reports the concept as observed for every harness whose
8057    /// runtime door can carry a request.
8058    #[test]
8059    fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
8060        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8061        assert_eq!(
8062            SdkOperation::from_method("harness.v1.approvals.list"),
8063            Some(SdkOperation::ApprovalsList)
8064        );
8065        let registry = harness_support_registry();
8066        for id in [
8067            HarnessId::HERMES,
8068            HarnessId::OPENCLAW,
8069            HarnessId::CODEX,
8070            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8071            // pending_request concept joins the other driven doors.
8072            HarnessId::CLAUDE_CODE,
8073        ] {
8074            let concept = registry
8075                .harnesses
8076                .iter()
8077                .find(|harness| harness.id.as_str() == id)
8078                .unwrap()
8079                .orchestration
8080                .concepts
8081                .iter()
8082                .find(|concept| concept.concept == "pending_request")
8083                .unwrap();
8084            assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8085            assert!(concept
8086                .methods
8087                .iter()
8088                .any(|method| method == "harness.v1.approvals.list"));
8089        }
8090    }
8091
8092    // ---- ORCH-20: `harness.v1.approvals.resolve` -------------------------
8093
8094    async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8095        service
8096            .handle_async(request(3, "harness.v1.approvals.resolve", params))
8097            .await
8098    }
8099
8100    /// dev/01: the whole loop on a driven runtime — list one pending row,
8101    /// answer it by ROW ID with one uniform decision, and see it gone. The
8102    /// door receives its own ACP envelope carrying the option it enumerated.
8103    #[tokio::test]
8104    async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8105        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8106        let mut service = HarnessSessionService::new();
8107        service.runtimes.insert(
8108            "runtime-1".into(),
8109            requesting_runtime(
8110                HarnessId::HERMES,
8111                vec![permission_event(7, "rm -rf build")],
8112                answered.clone(),
8113            ),
8114        );
8115        service.poll_runtimes().await;
8116
8117        let rows = approvals(&mut service, json!({}));
8118        assert_eq!(rows[0]["id"], "runtime-1/7");
8119
8120        let response = resolve(
8121            &mut service,
8122            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8123        )
8124        .await;
8125        assert!(response.get("error").is_none(), "{response:#}");
8126        assert_eq!(
8127            response["result"],
8128            json!({
8129                "id": "runtime-1/7",
8130                "decision": "allow_once",
8131                "option_id": "allow_once",
8132                "resolved": true,
8133            }),
8134        );
8135        // The harness's own door was called with its own envelope.
8136        assert_eq!(
8137            answered
8138                .lock()
8139                .unwrap_or_else(std::sync::PoisonError::into_inner)
8140                .as_slice(),
8141            &[json!({
8142                "request_id": 7,
8143                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8144            })],
8145        );
8146        // And the row is gone, the same way `runtimes.respond` drops it.
8147        assert_eq!(
8148            approvals(&mut service, json!({})).as_array().map(Vec::len),
8149            Some(0),
8150        );
8151        // Answering it twice is an honest miss, not a silent success.
8152        let response = resolve(
8153            &mut service,
8154            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8155        )
8156        .await;
8157        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8158    }
8159
8160    /// dev/01: deny travels the same path and picks the option the request
8161    /// itself classified as a refusal.
8162    #[tokio::test]
8163    async fn deny_selects_the_requests_own_reject_option() {
8164        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8165        let mut service = HarnessSessionService::new();
8166        service.runtimes.insert(
8167            "runtime-1".into(),
8168            requesting_runtime(
8169                HarnessId::HERMES,
8170                vec![permission_event(11, "git push --force")],
8171                answered.clone(),
8172            ),
8173        );
8174        service.poll_runtimes().await;
8175
8176        let response = resolve(
8177            &mut service,
8178            json!({"id": "runtime-1/11", "decision": "deny"}),
8179        )
8180        .await;
8181        assert!(response.get("error").is_none(), "{response:#}");
8182        // `deny` is the optionId whose ACP `kind` is `reject_once`.
8183        assert_eq!(response["result"]["option_id"], "deny");
8184        assert_eq!(
8185            answered
8186                .lock()
8187                .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8188            json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8189        );
8190        assert_eq!(
8191            approvals(&mut service, json!({})).as_array().map(Vec::len),
8192            Some(0),
8193        );
8194    }
8195
8196    /// dev/01: a decision this request does not offer is refused by name,
8197    /// listing the ones it does — never silently downgraded to a neighbour.
8198    #[tokio::test]
8199    async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8200        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8201        let mut service = HarnessSessionService::new();
8202        let mut event = permission_event(3, "rm -rf build");
8203        // A request offering only allow-once and deny, as hermes 0.21.0's
8204        // edit-approval layer raises one.
8205        event.payload["params"]["options"] = json!([
8206            {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8207            {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8208        ]);
8209        service.runtimes.insert(
8210            "runtime-1".into(),
8211            requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8212        );
8213        service.poll_runtimes().await;
8214
8215        let response = resolve(
8216            &mut service,
8217            json!({"id": "runtime-1/3", "decision": "allow_always"}),
8218        )
8219        .await;
8220        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8221        let message = response["error"]["message"].as_str().unwrap();
8222        assert!(message.contains("allow_always"), "{message}");
8223        assert!(message.contains("allow_once, deny"), "{message}");
8224        // Nothing was sent, and the request is still waiting for an answer.
8225        assert!(answered
8226            .lock()
8227            .unwrap_or_else(std::sync::PoisonError::into_inner)
8228            .is_empty());
8229        assert_eq!(
8230            approvals(&mut service, json!({})).as_array().map(Vec::len),
8231            Some(1),
8232        );
8233    }
8234
8235    /// dev/01: supercode's own queued subagent row is addressable but not
8236    /// answerable through this door — it is the parent's audit copy of a
8237    /// request its own handler answers. Refused by name, never a no-op.
8238    #[tokio::test]
8239    async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8240        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8241            crate::subagents::QueuedApproval {
8242                child_agent_id: "child-7".into(),
8243                tool: "shell".into(),
8244                subject: Some("cargo publish --dry-run".into()),
8245                queued_at_ms: 1,
8246                outcome: None,
8247            },
8248        ]));
8249        let mut service = HarnessSessionService::new();
8250        service.observe_subagent_approvals(queue.clone());
8251        let row = approvals(&mut service, json!({}))[0]["id"]
8252            .as_str()
8253            .unwrap()
8254            .to_string();
8255        assert_eq!(row, "supercode/subagent/child-7/1/0");
8256
8257        let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8258        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8259        let message = response["error"]["message"].as_str().unwrap();
8260        assert!(message.contains("queued subagent record"), "{message}");
8261        assert!(message.contains("request"), "{message}");
8262        // The audit record is untouched: nothing pretended to answer it.
8263        assert!(queue
8264            .lock()
8265            .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8266            .outcome
8267            .is_none());
8268    }
8269
8270    /// An id nobody is holding, and a call that names no decision at all,
8271    /// both fail with a message that says why.
8272    #[tokio::test]
8273    async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8274        let mut service = HarnessSessionService::new();
8275        let response = resolve(
8276            &mut service,
8277            json!({"id": "runtime-9/4", "decision": "deny"}),
8278        )
8279        .await;
8280        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8281        assert!(response["error"]["message"]
8282            .as_str()
8283            .unwrap()
8284            .contains("runtime-9/4"));
8285
8286        let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8287        let message = response["error"]["message"].as_str().unwrap();
8288        assert!(
8289            message.contains("allow_once | allow_always | deny"),
8290            "{message}"
8291        );
8292
8293        let response = resolve(
8294            &mut service,
8295            json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8296        )
8297        .await;
8298        assert!(response["error"]["message"]
8299            .as_str()
8300            .unwrap()
8301            .contains("not both"));
8302    }
8303
8304    /// The method is advertised, its SDK operation resolves it, and every
8305    /// harness whose runtime door can carry a request reports it on the
8306    /// CONTROLLED tier beside `runtimes.respond`.
8307    #[test]
8308    fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8309        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8310        assert_eq!(
8311            SdkOperation::from_method("harness.v1.approvals.resolve"),
8312            Some(SdkOperation::ApprovalsResolve)
8313        );
8314        assert_eq!(
8315            SdkOperation::ApprovalsResolve.action_name(),
8316            "approvals_resolve"
8317        );
8318        let registry = harness_support_registry();
8319        for id in [
8320            HarnessId::HERMES,
8321            HarnessId::OPENCLAW,
8322            HarnessId::CODEX,
8323            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8324            // pending_request concept joins the other driven doors.
8325            HarnessId::CLAUDE_CODE,
8326        ] {
8327            let concept = registry
8328                .harnesses
8329                .iter()
8330                .find(|harness| harness.id.as_str() == id)
8331                .unwrap()
8332                .orchestration
8333                .concepts
8334                .iter()
8335                .find(|concept| concept.concept == "pending_request")
8336                .unwrap();
8337            assert_eq!(
8338                concept.controlled,
8339                crate::ImplementationKind::BuiltIn,
8340                "{id}"
8341            );
8342            assert!(
8343                concept
8344                    .methods
8345                    .iter()
8346                    .any(|method| method == "harness.v1.approvals.resolve"),
8347                "{id}"
8348            );
8349        }
8350    }
8351
8352    #[test]
8353    fn capabilities_are_explicit_and_versioned() {
8354        let mut service = HarnessSessionService::new();
8355        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8356        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8357        assert_eq!(
8358            response["result"]["sdk"]["schema_version"],
8359            crate::SDK_SCHEMA_VERSION
8360        );
8361        assert_eq!(
8362            response["result"]["sdk"]["operations"]
8363                .as_array()
8364                .unwrap()
8365                .len(),
8366            SdkOperation::ALL.len()
8367        );
8368        assert_eq!(
8369            response["result"]["harnesses"].as_array().unwrap().len(),
8370            11
8371        );
8372        assert!(response["result"]["harnesses"]
8373            .as_array()
8374            .unwrap()
8375            .iter()
8376            .any(|harness| harness == HarnessId::GROK));
8377        assert!(response["result"]["harnesses"]
8378            .as_array()
8379            .unwrap()
8380            .iter()
8381            .any(|harness| harness == HarnessId::GOOSE));
8382    }
8383
8384    #[test]
8385    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8386        let noisy_stderr = crate::HarnessEvent {
8387            sequence: None,
8388            kind: "transport_stderr".into(),
8389            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8390        };
8391        assert_eq!(handshake_event_failure(&noisy_stderr), None);
8392
8393        let closed = crate::HarnessEvent {
8394            sequence: None,
8395            kind: "transport_closed".into(),
8396            payload: json!({}),
8397        };
8398        assert!(handshake_event_failure(&closed).is_some());
8399    }
8400
8401    #[tokio::test]
8402    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8403        let mut service = HarnessSessionService::new();
8404        service
8405            .runtimes
8406            .insert("raw-eof".into(), ending_runtime(None));
8407        service.runtimes.insert(
8408            "explicit-close".into(),
8409            ending_runtime(Some(HarnessEvent {
8410                sequence: None,
8411                kind: "transport_closed".into(),
8412                payload: json!({"message": "native transport exited"}),
8413            })),
8414        );
8415
8416        let notifications = service.poll_runtimes().await;
8417
8418        assert_eq!(notifications.len(), 2);
8419        assert!(notifications
8420            .iter()
8421            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8422        assert!(notifications.iter().all(|notification| {
8423            notification["params"]["session_id"] == "ending-session"
8424                && notification["params"]["connection"].is_string()
8425        }));
8426        let mut sequences = notifications
8427            .iter()
8428            .filter_map(|notification| notification["params"]["sequence"].as_u64())
8429            .collect::<Vec<_>>();
8430        sequences.sort_unstable();
8431        assert_eq!(sequences, vec![1, 2]);
8432        assert!(service.runtimes.is_empty());
8433    }
8434
8435    #[test]
8436    fn support_report_and_grok_default_binding_share_the_registry() {
8437        let mut service = HarnessSessionService::new();
8438        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8439        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8440        let params = RuntimeBackendParams {
8441            harness: HarnessId::from(HarnessId::GROK),
8442            protocol: None,
8443            launch: None,
8444            base_url: None,
8445            policy: RuntimePolicy::Default,
8446        };
8447        let backend = match runtime_backend(&params) {
8448            Ok(backend) => backend,
8449            Err(_) => panic!("Grok should bind through its registered ACP launch"),
8450        };
8451        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8452        assert!(backend.capabilities().start_session);
8453        let registered = harness_support_registry()
8454            .harnesses
8455            .into_iter()
8456            .find(|harness| harness.id.as_str() == HarnessId::GROK)
8457            .and_then(|harness| harness.runtime.default_launch)
8458            .unwrap();
8459        assert!(!registered
8460            .arguments
8461            .iter()
8462            .any(|argument| argument == "--always-approve"));
8463        assert!(runtime_launch(&params).is_none());
8464
8465        let yolo = RuntimeBackendParams {
8466            policy: RuntimePolicy::Yolo,
8467            ..params
8468        };
8469        assert!(runtime_launch(&yolo)
8470            .unwrap()
8471            .arguments
8472            .iter()
8473            .any(|argument| argument == "--always-approve"));
8474
8475        let mismatched_protocol = RuntimeBackendParams {
8476            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8477            protocol: Some("acp".into()),
8478            launch: None,
8479            base_url: None,
8480            policy: RuntimePolicy::Default,
8481        };
8482        assert!(runtime_backend(&mismatched_protocol).is_err());
8483    }
8484
8485    #[test]
8486    fn load_follow_and_unfollow_share_the_same_locator() {
8487        let mut service = HarnessSessionService::new();
8488        let locator = pi_locator();
8489        let loaded = service.handle(request(
8490            1,
8491            "harness.v1.sessions.load",
8492            json!({"locator": locator}),
8493        ));
8494        assert_eq!(
8495            loaded["result"]["session"]["session_id"],
8496            locator.session_id
8497        );
8498
8499        let followed = service.handle(request(
8500            2,
8501            "harness.v1.sessions.follow",
8502            json!({"locator": locator}),
8503        ));
8504        assert_eq!(followed["result"]["subscription"], "sub-1");
8505        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8506        assert!(service.poll().is_empty());
8507
8508        let unfollowed = service.handle(request(
8509            3,
8510            "harness.v1.sessions.unfollow",
8511            json!({"subscription": "sub-1"}),
8512        ));
8513        assert_eq!(unfollowed["result"]["removed"], true);
8514    }
8515
8516    #[test]
8517    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8518        let temp = std::env::temp_dir().join(format!(
8519            "supercode-bounded-view-{}-{}",
8520            std::process::id(),
8521            generated_session_id()
8522        ));
8523        let path = temp.join("parent.jsonl");
8524        let subagents = temp.join("parent/subagents");
8525        std::fs::create_dir_all(&subagents).unwrap();
8526        let long_last = "x".repeat(300);
8527        let parent_records = [
8528            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8529            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8530            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8531        ];
8532        std::fs::write(
8533            &path,
8534            format!(
8535                "{}\n",
8536                parent_records
8537                    .iter()
8538                    .map(Value::to_string)
8539                    .collect::<Vec<_>>()
8540                    .join("\n")
8541            ),
8542        )
8543        .unwrap();
8544        std::fs::write(
8545            subagents.join("agent-child.jsonl"),
8546            concat!(
8547                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8548                "\n",
8549            ),
8550        )
8551        .unwrap();
8552        let locator = SessionLocator {
8553            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8554            session_id: "parent".into(),
8555            storage: StorageLocator::File { path },
8556        };
8557        let mut service = HarnessSessionService::new();
8558
8559        let complete = service.handle(request(
8560            1,
8561            "harness.v1.sessions.load",
8562            json!({"locator": locator}),
8563        ));
8564        assert_eq!(
8565            complete["result"]["session"]["subagents"]
8566                .as_array()
8567                .unwrap()
8568                .len(),
8569            1
8570        );
8571
8572        let bounded = service.handle(request(
8573            2,
8574            "harness.v1.sessions.load",
8575            json!({
8576                "locator": locator,
8577                "view": {
8578                    "tail_messages": 1,
8579                    "max_message_chars": 256,
8580                    "include_subagents": false
8581                },
8582            }),
8583        ));
8584        let session = &bounded["result"]["session"];
8585        assert!(session["subagents"].as_array().unwrap().is_empty());
8586        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8587        assert_eq!(
8588            session["messages"][0]["content"],
8589            format!("{}\n…", "x".repeat(256))
8590        );
8591
8592        let followed = service.handle(request(
8593            3,
8594            "harness.v1.sessions.follow",
8595            json!({
8596                "locator": locator,
8597                "view": {
8598                    "tail_messages": 1,
8599                    "max_message_chars": 256,
8600                    "include_subagents": false
8601                },
8602            }),
8603        ));
8604        let initial = &followed["result"]["initial"]["session"];
8605        assert!(initial["subagents"].as_array().unwrap().is_empty());
8606        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8607
8608        let _ = std::fs::remove_dir_all(&temp);
8609    }
8610
8611    #[test]
8612    fn forty_megabyte_display_load_is_bounded_and_prompt() {
8613        let temp = std::env::temp_dir().join(format!(
8614            "supercode-large-display-view-{}-{}",
8615            std::process::id(),
8616            generated_session_id()
8617        ));
8618        std::fs::create_dir_all(&temp).unwrap();
8619        let path = temp.join("rollout.jsonl");
8620        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8621        writeln!(
8622            file,
8623            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8624        )
8625        .unwrap();
8626        let padding = "x".repeat(80 * 1024);
8627        for index in 0..512 {
8628            let marker = if index == 0 {
8629                "OLDEST-SHOULD-NOT-LOAD"
8630            } else if index == 511 {
8631                "LATEST-MUST-LOAD"
8632            } else {
8633                "bulk"
8634            };
8635            writeln!(
8636                file,
8637                "{}",
8638                json!({
8639                    "timestamp": "2026-01-01T00:00:01Z",
8640                    "type": "response_item",
8641                    "payload": {
8642                        "type": "message",
8643                        "role": "assistant",
8644                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8645                    },
8646                })
8647            )
8648            .unwrap();
8649        }
8650        file.flush().unwrap();
8651        drop(file);
8652        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8653
8654        let locator = SessionLocator {
8655            harness: HarnessId::from(HarnessId::CODEX),
8656            session_id: "large-display".into(),
8657            storage: StorageLocator::File { path },
8658        };
8659        let started = Instant::now();
8660        let response = HarnessSessionService::new().handle(request(
8661            1,
8662            "harness.v1.sessions.load",
8663            json!({
8664                "locator": locator,
8665                "view": {
8666                    "tail_messages": 500,
8667                    "max_message_chars": 1024,
8668                    "include_subagents": false,
8669                    "display_history": true,
8670                },
8671            }),
8672        ));
8673        let elapsed = started.elapsed();
8674        let wire = response.to_string();
8675        eprintln!(
8676            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8677            wire.len()
8678        );
8679        assert!(response.get("error").is_none(), "{response:#}");
8680        assert!(wire.contains("LATEST-MUST-LOAD"));
8681        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8682        assert!(
8683            wire.len() < 2 * 1024 * 1024,
8684            "bounded wire was {} bytes",
8685            wire.len()
8686        );
8687        assert!(
8688            elapsed.as_secs_f64() < 3.0,
8689            "bounded 40 MiB load took {elapsed:?}"
8690        );
8691
8692        let _ = std::fs::remove_dir_all(&temp);
8693    }
8694
8695    #[test]
8696    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8697        let temp = std::env::temp_dir().join(format!(
8698            "supercode-large-goose-view-{}-{}",
8699            std::process::id(),
8700            generated_session_id()
8701        ));
8702        std::fs::create_dir_all(&temp).unwrap();
8703        let path = temp.join("sessions.db");
8704        let connection = rusqlite::Connection::open(&path).unwrap();
8705        connection
8706            .execute_batch(
8707                "CREATE TABLE sessions (
8708                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8709                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8710                    session_type TEXT NOT NULL, extension_data TEXT,
8711                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8712                    archived_at TEXT
8713                 );
8714                 CREATE TABLE messages (
8715                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8716                    role TEXT NOT NULL, content_json TEXT NOT NULL,
8717                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
8718                 );",
8719            )
8720            .unwrap();
8721        connection
8722            .execute(
8723                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8724                rusqlite::params![
8725                    "goose-large",
8726                    "Large Goose session",
8727                    "/tmp",
8728                    "2026-01-01 00:00:00",
8729                    "2026-01-01 00:00:02",
8730                    "user",
8731                    "{}",
8732                    "auto",
8733                    "anthropic",
8734                    r#"{"model_name":"claude-sonnet"}"#,
8735                ],
8736            )
8737            .unwrap();
8738        let old_content = serde_json::to_string(&vec![json!({
8739            "type": "text",
8740            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8741        })])
8742        .unwrap();
8743        connection
8744            .execute(
8745                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8746                rusqlite::params!["goose-large", old_content],
8747            )
8748            .unwrap();
8749        connection
8750            .execute(
8751                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8752                rusqlite::params![
8753                    "goose-large",
8754                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8755                ],
8756            )
8757            .unwrap();
8758        drop(connection);
8759        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8760
8761        let locator = SessionLocator {
8762            harness: HarnessId::from(HarnessId::GOOSE),
8763            session_id: "goose-large".into(),
8764            storage: StorageLocator::Sqlite {
8765                path,
8766                selector: "goose-large".into(),
8767            },
8768        };
8769        let started = Instant::now();
8770        let response = HarnessSessionService::new().handle(request(
8771            1,
8772            "harness.v1.sessions.load",
8773            json!({
8774                "locator": locator,
8775                "view": {
8776                    "tail_messages": 1,
8777                    "max_message_chars": 1024,
8778                    "include_subagents": false,
8779                    "display_history": true,
8780                },
8781            }),
8782        ));
8783        let elapsed = started.elapsed();
8784        let wire = response.to_string();
8785        eprintln!(
8786            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8787            wire.len()
8788        );
8789        assert!(response.get("error").is_none(), "{response:#}");
8790        assert!(wire.contains("LATEST-MUST-LOAD"));
8791        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8792        assert!(
8793            wire.len() < 64 * 1024,
8794            "bounded wire was {} bytes",
8795            wire.len()
8796        );
8797        assert!(
8798            elapsed.as_secs_f64() < 1.0,
8799            "bounded Goose load took {elapsed:?}"
8800        );
8801
8802        let _ = std::fs::remove_dir_all(&temp);
8803    }
8804
8805    #[test]
8806    fn display_view_keeps_codex_assistant_history_across_compaction() {
8807        let temp = std::env::temp_dir().join(format!(
8808            "supercode-codex-display-view-{}-{}",
8809            std::process::id(),
8810            generated_session_id()
8811        ));
8812        std::fs::create_dir_all(&temp).unwrap();
8813        let path = temp.join("rollout.jsonl");
8814        std::fs::write(
8815            &path,
8816            concat!(
8817                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8818                "\n",
8819                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8820                "\n",
8821                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8822                "\n",
8823                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"}]}}"#,
8824                "\n",
8825                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8826                "\n",
8827                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8828                "\n",
8829            ),
8830        )
8831        .unwrap();
8832        let locator = SessionLocator {
8833            harness: HarnessId::from(HarnessId::CODEX),
8834            session_id: "codex-display".into(),
8835            storage: StorageLocator::File { path },
8836        };
8837        let mut service = HarnessSessionService::new();
8838
8839        let continuation = service.handle(request(
8840            1,
8841            "harness.v1.sessions.load",
8842            json!({"locator": locator}),
8843        ));
8844        let continuation_text = continuation["result"]["session"]["messages"].to_string();
8845        assert!(!continuation_text.contains("old answer"));
8846
8847        let display = service.handle(request(
8848            2,
8849            "harness.v1.sessions.load",
8850            json!({
8851                "locator": locator,
8852                "view": {
8853                    "tail_messages": 10,
8854                    "include_subagents": false,
8855                    "display_history": true,
8856                },
8857            }),
8858        ));
8859        let display_text = display["result"]["session"]["messages"].to_string();
8860        assert!(display_text.contains("old prompt"));
8861        assert!(display_text.contains("old answer"));
8862        assert!(display_text.contains("new prompt"));
8863        assert!(display_text.contains("new answer"));
8864
8865        let _ = std::fs::remove_dir_all(&temp);
8866    }
8867
8868    #[test]
8869    fn indexed_claude_windows_match_the_existing_wire_projection() {
8870        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8871            .join("tests/fixtures/claude_code_session.jsonl");
8872        let locator = SessionLocator {
8873            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8874            session_id: "fixture".into(),
8875            storage: StorageLocator::File { path },
8876        };
8877        let full = load_session(&locator).unwrap();
8878        for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8879            for offset in [0, 1, full.messages.len(), usize::MAX] {
8880                for limit in [0, 1, 3, usize::MAX] {
8881                    let options = SessionLoadOptions {
8882                        include_subagents: Some(false),
8883                        inline_media,
8884                        message_offset: Some(offset),
8885                        message_limit: Some(limit),
8886                        ..Default::default()
8887                    };
8888                    let expected = projected_session_result(&full, &options);
8889                    assert_eq!(
8890                        indexed_claude_window(&locator, &options).unwrap().unwrap(),
8891                        expected
8892                    );
8893                }
8894            }
8895            for tail in [0, 1, 3, usize::MAX] {
8896                let options = SessionLoadOptions {
8897                    include_subagents: Some(false),
8898                    inline_media,
8899                    message_tail: Some(tail),
8900                    ..Default::default()
8901                };
8902                assert_eq!(
8903                    indexed_claude_window(&locator, &options).unwrap().unwrap(),
8904                    projected_session_result(&full, &options)
8905                );
8906            }
8907        }
8908    }
8909
8910    #[test]
8911    fn load_supports_bounded_windows_and_media_metadata() {
8912        let mut service = HarnessSessionService::new();
8913        let locator = pi_locator();
8914        let bounded = service.handle(request(
8915            1,
8916            "harness.v1.sessions.load",
8917            json!({
8918                "locator": locator,
8919                "options": {
8920                    "include_subagents": false,
8921                    "message_limit": 2,
8922                    "message_offset": 1
8923                }
8924            }),
8925        ));
8926        assert_eq!(bounded["result"]["window"]["offset"], 1);
8927        assert_eq!(bounded["result"]["window"]["returned"], 2);
8928        assert!(bounded["result"]["summary"]["first_message"].is_object());
8929        assert!(bounded["result"]["summary"]["last_message"].is_object());
8930        assert_eq!(
8931            bounded["result"]["session"]["messages"]
8932                .as_array()
8933                .unwrap()
8934                .len(),
8935            2
8936        );
8937        assert!(bounded["result"]["session"]["subagents"]
8938            .as_array()
8939            .unwrap()
8940            .is_empty());
8941
8942        let tail = service.handle(request(
8943            2,
8944            "harness.v1.sessions.load",
8945            json!({"locator": locator, "options": {"message_tail": 1}}),
8946        ));
8947        assert_eq!(tail["result"]["window"]["returned"], 1);
8948        assert_eq!(tail["result"]["window"]["has_more"], true);
8949        assert_eq!(tail["result"]["window"]["has_older"], true);
8950        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8951        assert!(tail["result"]["summary"]["first_message"].is_object());
8952
8953        let metadata_only = service.handle(request(
8954            3,
8955            "harness.v1.sessions.load",
8956            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8957        ));
8958        assert!(metadata_only["result"]["session"]
8959            .to_string()
8960            .contains("media_reference"));
8961        assert!(!metadata_only["result"]["session"]
8962            .to_string()
8963            .contains("data:image/"));
8964    }
8965
8966    #[test]
8967    fn import_translate_branch_and_handoff_use_typed_artifacts() {
8968        let mut service = HarnessSessionService::new();
8969        let locator = pi_locator();
8970        let translated = service.handle(request(
8971            1,
8972            "harness.v1.sessions.translate",
8973            json!({"locator": locator, "target_harness": "grok"}),
8974        ));
8975        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8976        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8977        assert!(translated["result"]["artifact"]["content"]
8978            .as_str()
8979            .is_some_and(|content| !content.is_empty()));
8980
8981        for target in ["opencode", "open-code"] {
8982            let opencode = service.handle(request(
8983                6,
8984                "harness.v1.sessions.translate",
8985                json!({"locator": locator, "target_harness": target}),
8986            ));
8987            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8988        }
8989        let goose = service.handle(request(
8990            7,
8991            "harness.v1.sessions.translate",
8992            json!({"locator": locator, "target_harness": "goose"}),
8993        ));
8994        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8995        assert!(serde_json::from_str::<Value>(
8996            goose["result"]["artifact"]["content"].as_str().unwrap()
8997        )
8998        .unwrap()["conversation"]
8999            .is_array());
9000
9001        let imported = service.handle(request(
9002            2,
9003            "harness.v1.sessions.import",
9004            json!({
9005                "source_harness": "grok",
9006                "content": translated["result"]["artifact"]["content"],
9007            }),
9008        ));
9009        assert_eq!(imported["result"]["session"]["source"], "grok");
9010
9011        let branched = service.handle(request(
9012            3,
9013            "harness.v1.sessions.branch",
9014            json!({"locator": locator, "target_harness": "codex"}),
9015        ));
9016        assert_eq!(branched["result"]["parent"]["harness"], "pi");
9017        assert!(branched["result"]["bootstrap_prompt"]
9018            .as_str()
9019            .unwrap()
9020            .contains("frozen parent transcript"));
9021        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
9022
9023        let handoff = service.handle(request(
9024            4,
9025            "harness.v1.sessions.handoff",
9026            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
9027        ));
9028        assert_eq!(handoff["result"]["launch"]["program"], "pi");
9029        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
9030        assert_eq!(handoff["result"]["requires_materialization"], true);
9031
9032        let goose_handoff = service.handle(request(
9033            8,
9034            "harness.v1.sessions.handoff",
9035            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
9036        ));
9037        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
9038        assert_eq!(
9039            goose_handoff["result"]["materialize"]["arguments"],
9040            json!(["session", "import", "{artifact_path}"])
9041        );
9042
9043        let resumed = service.handle(request(
9044            5,
9045            "harness.v1.sessions.resume_instructions",
9046            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
9047        ));
9048        assert_eq!(resumed["result"]["launch"]["program"], "pi");
9049        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
9050    }
9051
9052    #[test]
9053    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
9054        let temp = std::env::temp_dir().join(format!(
9055            "supercode-service-reduce-{}-{}",
9056            std::process::id(),
9057            generated_session_id()
9058        ));
9059        let source_path = temp.join("source.jsonl");
9060        let store_root = temp.join("store");
9061        std::fs::create_dir_all(&temp).unwrap();
9062
9063        let mut records = vec![json!({
9064            "timestamp": "2026-01-01T00:00:00Z",
9065            "type": "session_meta",
9066            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9067        })];
9068        for turn in 0..16 {
9069            records.push(json!({
9070                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9071                "type": "response_item",
9072                "payload": {
9073                    "type": "message",
9074                    "role": "user",
9075                    "content": [{
9076                        "type": "input_text",
9077                        "text": format!("request {turn}: {}", "context ".repeat(80)),
9078                    }],
9079                },
9080            }));
9081            records.push(json!({
9082                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9083                "type": "response_item",
9084                "payload": {
9085                    "type": "message",
9086                    "role": "assistant",
9087                    "content": [{
9088                        "type": "output_text",
9089                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9090                    }],
9091                },
9092            }));
9093        }
9094        let source = format!(
9095            "{}\n",
9096            records
9097                .iter()
9098                .map(Value::to_string)
9099                .collect::<Vec<_>>()
9100                .join("\n")
9101        );
9102        std::fs::write(&source_path, &source).unwrap();
9103        let locator = SessionLocator {
9104            harness: HarnessId::from(HarnessId::CODEX),
9105            session_id: "codex-reduce".into(),
9106            storage: StorageLocator::File {
9107                path: source_path.clone(),
9108            },
9109        };
9110        let original = load_session(&locator).unwrap();
9111        let mut service =
9112            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9113
9114        let response = service.handle(request(
9115            1,
9116            "harness.v1.sessions.reduce",
9117            json!({
9118                "locator": locator,
9119                "target_harness": "claude-code",
9120                "keep_last": 4,
9121            }),
9122        ));
9123        assert!(response.get("error").is_none(), "{response:#}");
9124        let receipt = &response["result"]["receipt"];
9125        assert_eq!(receipt["source_harness"], "codex");
9126        assert_eq!(receipt["target_harness"], "claude-code");
9127        assert_eq!(receipt["verified"], true);
9128        assert_eq!(receipt["reversible"], true);
9129        assert!(receipt["reductions"].as_u64().unwrap() > 0);
9130        assert!(
9131            receipt["source_tokens"].as_u64().unwrap()
9132                > receipt["reduced_tokens"].as_u64().unwrap()
9133        );
9134        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9135        assert!(response["result"]["bootstrap_prompt"]
9136            .as_str()
9137            .unwrap()
9138            .contains("Do not guess hidden content"));
9139
9140        let rescue_id = receipt["id"].as_str().unwrap();
9141        let store = crate::SessionStore::open(&store_root).unwrap();
9142        let sidecar =
9143            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9144        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9145        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9146        let policy = reduce::ReductionPolicy {
9147            clear_turns_older_than: Some(4),
9148            ..Default::default()
9149        };
9150        let (restamped_view, reapplied_log) =
9151            reduce::project_messages(&sidecar.messages, &policy, &log);
9152        assert_eq!(
9153            messages_jsonl(&persisted_view).unwrap(),
9154            messages_jsonl(&restamped_view).unwrap()
9155        );
9156        assert_eq!(reapplied_log, log);
9157        reduce::verify_log(&log, &sidecar).unwrap();
9158        assert_eq!(
9159            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9160            original.messages
9161        );
9162        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9163
9164        std::fs::remove_dir_all(temp).ok();
9165    }
9166
9167    #[test]
9168    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9169        let temp = std::env::temp_dir().join(format!(
9170            "supercode-severed-view-{}-{}",
9171            std::process::id(),
9172            generated_session_id()
9173        ));
9174        std::fs::create_dir_all(&temp).unwrap();
9175        let path = temp.join("severed.jsonl");
9176        // A live record whose parent was pruned — what a compacted or
9177        // resumed-across-files Claude Code session looks like on disk.
9178        std::fs::write(
9179            &path,
9180            concat!(
9181                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9182                "\n",
9183                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9184                "\n",
9185            ),
9186        )
9187        .unwrap();
9188        let locator = SessionLocator {
9189            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9190            session_id: "severed".into(),
9191            storage: StorageLocator::File { path },
9192        };
9193        let mut service = HarnessSessionService::new();
9194
9195        let viewed = service.handle(request(
9196            1,
9197            "harness.v1.sessions.load",
9198            json!({"locator": locator}),
9199        ));
9200        let session = &viewed["result"]["session"];
9201        assert_eq!(session["fidelity"], "semantic");
9202        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9203        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9204            entry
9205                .as_str()
9206                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9207        }));
9208
9209        // Asking a READ surface for a lossless reconstruction gets the strict
9210        // refusal back, unchanged.
9211        let strict = service.handle(request(
9212            2,
9213            "harness.v1.sessions.load",
9214            json!({"locator": locator, "fidelity": "byte_lossless"}),
9215        ));
9216        assert!(strict["error"]["message"]
9217            .as_str()
9218            .unwrap()
9219            .contains("cannot reconstruct lossless Claude continuation"));
9220
9221        // Transfer/continuation surfaces have no view mode at all.
9222        let translated = service.handle(request(
9223            3,
9224            "harness.v1.sessions.translate",
9225            json!({"locator": locator, "target_harness": "codex"}),
9226        ));
9227        assert!(translated["error"]["message"]
9228            .as_str()
9229            .unwrap()
9230            .contains("cannot reconstruct lossless Claude continuation"));
9231        let resumed = service.handle(request(
9232            4,
9233            "harness.v1.sessions.resume_instructions",
9234            json!({"locator": locator}),
9235        ));
9236        assert!(resumed["error"]["message"]
9237            .as_str()
9238            .unwrap()
9239            .contains("cannot reconstruct lossless Claude continuation"));
9240
9241        let _ = std::fs::remove_dir_all(&temp);
9242    }
9243
9244    #[test]
9245    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9246        let codex = resume_launch(
9247            HarnessId::CODEX,
9248            "codex-session",
9249            Path::new("/tmp/project"),
9250            ResumePolicy::Yolo,
9251        )
9252        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9253        assert_eq!(codex.program, "codex");
9254        assert_eq!(
9255            codex.arguments,
9256            [
9257                "-c",
9258                "check_for_update_on_startup=false",
9259                "-c",
9260                "projects.\"/tmp/project\".trust_level=\"trusted\"",
9261                "--dangerously-bypass-approvals-and-sandbox",
9262                "--dangerously-bypass-hook-trust",
9263                "resume",
9264                "codex-session",
9265            ]
9266        );
9267
9268        let gemini = resume_launch(
9269            HarnessId::GEMINI,
9270            "gemini-session",
9271            Path::new("/tmp/project"),
9272            ResumePolicy::Yolo,
9273        )
9274        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9275        assert_eq!(gemini.program, "gemini");
9276        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9277
9278        let goose = resume_launch(
9279            HarnessId::GOOSE,
9280            "goose-session",
9281            Path::new("/tmp/project"),
9282            ResumePolicy::Yolo,
9283        )
9284        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9285        assert_eq!(goose.program, "goose");
9286        assert_eq!(
9287            goose.arguments,
9288            ["session", "--resume", "--session-id", "goose-session"]
9289        );
9290
9291        let supercode = resume_launch(
9292            HarnessId::SUPERCODE,
9293            "supercode-session",
9294            Path::new("/tmp/project"),
9295            ResumePolicy::Yolo,
9296        )
9297        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9298        assert_eq!(supercode.program, "supercode");
9299        assert_eq!(
9300            supercode.arguments,
9301            ["--dangerous", "resume", "supercode-session"]
9302        );
9303    }
9304
9305    #[test]
9306    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9307        let temp = std::env::temp_dir().join(format!(
9308            "supercode-harness-artifact-{}-{}",
9309            std::process::id(),
9310            generated_session_id()
9311        ));
9312        let main_path = temp.join("parent.jsonl");
9313        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9314        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9315        let fixture = std::fs::read_to_string(
9316            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9317                .join("tests/fixtures/claude_code_session.jsonl"),
9318        )
9319        .unwrap();
9320        let parent = fixture.trim_end_matches('\n');
9321        let child = fixture.trim_end_matches('\n');
9322        std::fs::write(&main_path, parent).unwrap();
9323        std::fs::write(&subagent_path, child).unwrap();
9324        let locator = SessionLocator {
9325            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9326            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9327            storage: StorageLocator::File {
9328                path: main_path.clone(),
9329            },
9330        };
9331        let mut service = HarnessSessionService::new();
9332        let claude = service.handle(request(
9333            1,
9334            "harness.v1.sessions.translate",
9335            json!({"locator": locator, "target_harness": "claude-code"}),
9336        ));
9337        let artifact = &claude["result"]["artifact"];
9338        assert_eq!(artifact["fidelity"], "byte_lossless");
9339        assert_eq!(artifact["content"], parent);
9340        let files = artifact["files"].as_array().unwrap();
9341        assert!(files.iter().any(|file| {
9342            file["role"] == "subagent"
9343                && file["path"]
9344                    .as_str()
9345                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9346                && file["content"] == child
9347        }));
9348        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9349
9350        let grok = service.handle(request(
9351            2,
9352            "harness.v1.sessions.translate",
9353            json!({"locator": grok_locator(), "target_harness": "grok"}),
9354        ));
9355        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9356        for name in ["summary.json", "updates.jsonl"] {
9357            let expected = std::fs::read_to_string(
9358                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9359                    .join("tests/fixtures/grok_session")
9360                    .join(name),
9361            )
9362            .unwrap();
9363            assert!(files.iter().any(|file| {
9364                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9365            }));
9366        }
9367        std::fs::remove_dir_all(temp).ok();
9368    }
9369
9370    #[test]
9371    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9372        let mut service = HarnessSessionService::new();
9373        let source = pi_locator();
9374        for (target, format) in [
9375            ("claude-code", SessionFormat::ClaudeCode),
9376            ("codex", SessionFormat::Codex),
9377            ("opencode", SessionFormat::OpenCode),
9378            ("pi", SessionFormat::Pi),
9379        ] {
9380            let result = service.handle(request(
9381                1,
9382                "harness.v1.sessions.handoff",
9383                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9384            ));
9385            let artifact = &result["result"]["artifact"];
9386            let target_id = artifact["session_id"].as_str().unwrap();
9387            assert_ne!(target_id, source.session_id, "{target}");
9388            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9389            assert_eq!(
9390                parsed.meta.session_id.as_deref(),
9391                Some(target_id),
9392                "{target}"
9393            );
9394            if target != "pi" {
9395                assert!(result["result"]["launch"]["arguments"]
9396                    .as_array()
9397                    .unwrap()
9398                    .iter()
9399                    .any(|argument| argument == target_id));
9400            }
9401            if target == "opencode" {
9402                assert!(target_id.starts_with("ses_"));
9403                fn assert_session_ids(value: &Value, target_id: &str) {
9404                    match value {
9405                        Value::Object(fields) => {
9406                            if let Some(session_id) = fields.get("sessionID") {
9407                                assert_eq!(session_id, target_id);
9408                            }
9409                            for child in fields.values() {
9410                                assert_session_ids(child, target_id);
9411                            }
9412                        }
9413                        Value::Array(values) => {
9414                            for child in values {
9415                                assert_session_ids(child, target_id);
9416                            }
9417                        }
9418                        _ => {}
9419                    }
9420                }
9421                let document: Value =
9422                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9423                assert_session_ids(&document, target_id);
9424            }
9425        }
9426
9427        let first = service.handle(request(
9428            2,
9429            "harness.v1.sessions.handoff",
9430            json!({"locator": source, "target_harness": "codex"}),
9431        ));
9432        let second = service.handle(request(
9433            3,
9434            "harness.v1.sessions.handoff",
9435            json!({"locator": source, "target_harness": "codex"}),
9436        ));
9437        assert_ne!(
9438            first["result"]["artifact"]["session_id"],
9439            second["result"]["artifact"]["session_id"]
9440        );
9441    }
9442
9443    #[test]
9444    fn grok_handoff_uses_the_official_importer_contract() {
9445        let mut service = HarnessSessionService::new();
9446        let source = opencode_locator();
9447        let response = service.handle(request(
9448            1,
9449            "harness.v1.sessions.handoff",
9450            json!({
9451                "locator": source,
9452                "target_harness": "grok",
9453                "cwd": "/tmp/grok-handoff-project",
9454            }),
9455        ));
9456        let result = &response["result"];
9457
9458        // The target is Grok, but the artifact truthfully names the Claude Code wire
9459        // format accepted by Grok's official importer. Raw Grok chat_history JSONL is
9460        // not a complete stock-resumable bundle.
9461        assert_eq!(result["artifact"]["target_harness"], "claude-code");
9462        assert!(result["artifact"]["suggested_filename"]
9463            .as_str()
9464            .unwrap()
9465            .ends_with(".grok-import.claude-code.jsonl"));
9466        let artifact = Session::load_str(
9467            result["artifact"]["content"].as_str().unwrap(),
9468            SessionFormat::ClaudeCode,
9469        )
9470        .unwrap();
9471        assert_eq!(
9472            artifact.meta.cwd.as_deref(),
9473            Some(Path::new("/tmp/grok-handoff-project"))
9474        );
9475        let target_session_id = artifact.meta.session_id.as_deref().unwrap();
9476        assert_eq!(target_session_id.len(), 36);
9477        assert_eq!(target_session_id.as_bytes()[14], b'4');
9478        assert_ne!(target_session_id, opencode_locator().session_id);
9479        assert_eq!(
9480            result["artifact"]["session_id"],
9481            artifact.meta.session_id.as_deref().unwrap()
9482        );
9483
9484        assert_eq!(
9485            result["materialize"]["arguments"],
9486            json!(["import", "--json", "{artifact_path}"])
9487        );
9488        assert_eq!(
9489            result["launch"]["arguments"],
9490            json!(["--resume", "{imported_session_id}", "--fork-session"])
9491        );
9492        assert!(result["note"]
9493            .as_str()
9494            .unwrap()
9495            .contains("outcome=imported"));
9496        assert!(!result["launch"]["arguments"]
9497            .as_array()
9498            .unwrap()
9499            .iter()
9500            .any(|argument| argument == &opencode_locator().session_id));
9501    }
9502
9503    #[tokio::test]
9504    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9505        let mut service = HarnessSessionService::new();
9506        let inventory = service
9507            .handle_async(request(
9508                1,
9509                "harness.v1.harnesses.list",
9510                json!({"harnesses": ["missing"]}),
9511            ))
9512            .await;
9513        assert_eq!(inventory["error"]["code"], -32602);
9514
9515        let attached = service
9516            .handle_async(request(
9517                2,
9518                "harness.v1.runtimes.attach_existing",
9519                json!({"harness": "codex", "runtime_id": "thread-1"}),
9520            ))
9521            .await;
9522        assert_eq!(attached["error"]["code"], -32000);
9523        assert!(attached["error"]["message"]
9524            .as_str()
9525            .unwrap()
9526            .contains("runtimes.resume"));
9527    }
9528
9529    #[test]
9530    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9531        let mut service = HarnessSessionService::new();
9532        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9533        assert_eq!(invalid["error"]["code"], -32602);
9534        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9535        assert_eq!(unknown["error"]["code"], -32601);
9536    }
9537
9538    #[cfg(unix)]
9539    #[tokio::test]
9540    // The test mutates process-wide harness environment and deliberately
9541    // holds the global test lock until every async runtime operation ends.
9542    #[allow(clippy::await_holding_lock)]
9543    async fn async_service_drives_a_generic_acp_runtime() {
9544        let _environment_guard = crate::live_runtime::test_environment_lock();
9545        let script = r#"
9546            i=0
9547            while IFS= read -r line; do
9548              i=$((i + 1))
9549              case "$i" in
9550                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9551                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9552                3)
9553                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9554                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9555                  ;;
9556                4)
9557                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9558                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9559                  ;;
9560              esac
9561            done
9562        "#;
9563        let mut service = HarnessSessionService::new();
9564        let started = service
9565            .handle_async(request(
9566                1,
9567                "harness.v1.runtimes.start",
9568                json!({
9569                    "harness": "codex",
9570                    "protocol": "acp",
9571                    "cwd": std::env::current_dir().unwrap(),
9572                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9573                }),
9574            ))
9575            .await;
9576        assert_eq!(started["result"]["connection"], "runtime-1");
9577        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9578
9579        let terminal = service
9580            .handle_async(request(
9581                9,
9582                "harness.v1.runtimes.terminal_instructions",
9583                json!({"connection":"runtime-1"}),
9584            ))
9585            .await;
9586        let arguments = terminal["result"]["launch"]["arguments"]
9587            .as_array()
9588            .expect("hosted runtime should return terminal arguments");
9589        let endpoint_index = arguments
9590            .iter()
9591            .position(|value| value == "--endpoint")
9592            .expect("terminal command should use an opaque endpoint");
9593        let endpoint = LiveRuntimeEndpoint::parse(
9594            arguments[endpoint_index + 1]
9595                .as_str()
9596                .expect("endpoint argument should be text"),
9597        )
9598        .unwrap();
9599        assert!(!terminal.to_string().contains("Bearer"));
9600        let workspace = std::env::current_dir().unwrap();
9601        let receipt = resolve_live_runtime(
9602            &endpoint,
9603            &LiveRuntimeSource {
9604                harness: "codex".into(),
9605                session_id: "svc_acp".into(),
9606                workspace,
9607            },
9608        )
9609        .unwrap();
9610        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9611            .await
9612            .unwrap();
9613        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9614            .await
9615            .unwrap();
9616
9617        let sent = service
9618            .handle_async(request(
9619                2,
9620                "harness.v1.runtimes.send_input",
9621                json!({"connection": "runtime-1", "text": "hi"}),
9622            ))
9623            .await;
9624        assert_eq!(sent["result"]["turn_id"], "3");
9625
9626        let mut events = Vec::new();
9627        for _ in 0..20 {
9628            events.extend(service.poll_runtimes().await);
9629            if events.len() >= 2 {
9630                break;
9631            }
9632            tokio::time::sleep(Duration::from_millis(2)).await;
9633        }
9634        assert!(events
9635            .iter()
9636            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9637        assert!(events.iter().any(|event| {
9638            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9639        }));
9640
9641        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9642            loop {
9643                let event = attachment.next_event().await.unwrap();
9644                if event.kind == "text_delta" && event.payload["text"] == "ok" {
9645                    break;
9646                }
9647            }
9648        })
9649        .await;
9650        assert!(
9651            saw_editor_reply.is_ok(),
9652            "terminal should observe the editor-driven turn"
9653        );
9654
9655        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9656            .await
9657            .unwrap();
9658        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9659            loop {
9660                let event = attachment.next_event().await.unwrap();
9661                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9662                    break;
9663                }
9664            }
9665        })
9666        .await;
9667        assert!(
9668            saw_terminal_reply.is_ok(),
9669            "terminal should drive the same runtime"
9670        );
9671
9672        let closed = service
9673            .handle_async(request(
9674                3,
9675                "harness.v1.runtimes.close",
9676                json!({"connection": "runtime-1"}),
9677            ))
9678            .await;
9679        assert_eq!(closed["result"]["closed"], true);
9680    }
9681
9682    /// UNI-7 dev/02: a RUNNING mock gateway is detected through the real
9683    /// openclaw probe (config-declared endpoint, TCP connect), and an ACTIVE
9684    /// hermes WAL is detected through the real WAL-freshness probe; the
9685    /// negative sides (no listener, stale WAL, no config) stay undetected.
9686    #[test]
9687    fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9688        let home = connect_scratch_home("uni7-running");
9689
9690        // No config at all: hermes has no default endpoint, so no detection.
9691        // (openclaw's no-config behavior now probes its DOCUMENTED default
9692        // endpoint ws://127.0.0.1:18789 — see the connect launch's
9693        // `default_address` — which is real box state a hermetic test must
9694        // not assert either way; the closed-port negative below covers the
9695        // no-listener side deterministically.)
9696        assert!(probe_hermes_running(&home, 300_000).is_none());
9697
9698        // Mock gateway: a real TCP listener on an ephemeral port, declared in
9699        // the harness's own config file.
9700        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9701        let port = listener.local_addr().unwrap().port();
9702        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9703        std::fs::write(
9704            home.join(".openclaw/openclaw.json"),
9705            format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9706        )
9707        .unwrap();
9708        let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9709        assert!(matches!(
9710            running.method,
9711            RunningInstanceMethod::GatewayConnect
9712        ));
9713        assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9714        drop(listener);
9715        // Parallel tests also bind ephemeral loopback ports, so a just-freed
9716        // port can be re-bound by a NEIGHBORING test between drop and probe.
9717        // Detection on a closed port must fail — retry on a fresh port when
9718        // the freed one was recycled by someone else.
9719        let mut closed_detected = probe_openclaw_running(&home).is_some();
9720        for _ in 0..3 {
9721            if !closed_detected {
9722                break;
9723            }
9724            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9725            let port = listener.local_addr().unwrap().port();
9726            drop(listener);
9727            std::fs::write(
9728                home.join(".openclaw/openclaw.json"),
9729                format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9730            )
9731            .unwrap();
9732            closed_detected = probe_openclaw_running(&home).is_some();
9733        }
9734        assert!(
9735            !closed_detected,
9736            "a closed gateway must not read as running"
9737        );
9738
9739        // gateway.url form takes precedence over port.
9740        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9741        let port = listener.local_addr().unwrap().port();
9742        std::fs::write(
9743            home.join(".openclaw/openclaw.json"),
9744            format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9745        )
9746        .unwrap();
9747        assert!(probe_openclaw_running(&home).is_some());
9748        drop(listener);
9749
9750        // Hermes: an ACTIVE WAL (fresh stamp) is detected; a stale one is not.
9751        std::fs::create_dir_all(home.join(".hermes")).unwrap();
9752        let wal = home.join(".hermes/state.db-wal");
9753        std::fs::write(&wal, b"wal").unwrap();
9754        let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9755        assert!(matches!(
9756            running.method,
9757            RunningInstanceMethod::StoreWalActivity
9758        ));
9759        assert!(running.evidence.contains("state.db-wal"));
9760        let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9761        std::fs::File::options()
9762            .append(true)
9763            .open(&wal)
9764            .unwrap()
9765            .set_modified(stale)
9766            .unwrap();
9767        assert!(
9768            probe_hermes_running(&home, 300_000).is_none(),
9769            "a stale WAL (crash leftover) must not read as running"
9770        );
9771    }
9772
9773    fn connect_scratch_home(tag: &str) -> PathBuf {
9774        let dir = std::env::temp_dir().join(format!(
9775            "supercode-connect-service-{tag}-{}-{}",
9776            std::process::id(),
9777            std::time::SystemTime::now()
9778                .duration_since(std::time::UNIX_EPOCH)
9779                .unwrap()
9780                .as_nanos()
9781        ));
9782        std::fs::create_dir_all(&dir).unwrap();
9783        dir
9784    }
9785
9786    /// Minimal HTTP responder that speaks just enough OpenCode server to
9787    /// accept a health check, create a session, and hold an SSE stream open,
9788    /// while recording each request line with its Authorization header.
9789    async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9790        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9791        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9792        let address = listener.local_addr().unwrap();
9793        let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9794        tokio::spawn(async move {
9795            loop {
9796                let Ok((mut stream, _)) = listener.accept().await else {
9797                    break;
9798                };
9799                let request_sender = request_sender.clone();
9800                tokio::spawn(async move {
9801                    let (reader, mut writer) = stream.split();
9802                    let mut reader = BufReader::new(reader);
9803                    let mut request_line = String::new();
9804                    if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9805                        return;
9806                    }
9807                    let request_line = request_line.trim_end().to_string();
9808                    let mut authorization = String::new();
9809                    let mut content_length = 0usize;
9810                    loop {
9811                        let mut line = String::new();
9812                        if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9813                            return;
9814                        }
9815                        let line = line.trim_end();
9816                        if line.is_empty() {
9817                            break;
9818                        }
9819                        let lower = line.to_ascii_lowercase();
9820                        if let Some(value) = lower.strip_prefix("authorization:") {
9821                            authorization = value.trim().to_string();
9822                        }
9823                        if let Some(value) = lower.strip_prefix("content-length:") {
9824                            content_length = value.trim().parse().unwrap_or(0);
9825                        }
9826                    }
9827                    if content_length > 0 {
9828                        let mut body = vec![0u8; content_length];
9829                        let _ = reader.read_exact(&mut body).await;
9830                    }
9831                    let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9832                    if request_line.starts_with("GET /event") {
9833                        let _ = writer
9834                            .write_all(
9835                                b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9836                            )
9837                            .await;
9838                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9839                        return;
9840                    }
9841                    let body = if request_line.starts_with("POST /session") {
9842                        r#"{"id":"mock-session"}"#
9843                    } else {
9844                        r#"{"status":"ok"}"#
9845                    };
9846                    let response = format!(
9847                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9848                        body.len(),
9849                        body
9850                    );
9851                    let _ = writer.write_all(response.as_bytes()).await;
9852                });
9853            }
9854        });
9855        (format!("http://{address}"), request_receiver)
9856    }
9857
9858    fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9859        crate::HarnessSupportDescriptor {
9860            orchestration: Default::default(),
9861            id: HarnessId::from(HarnessId::OPENCODE),
9862            display_name: "OpenCode".into(),
9863            native: crate::NativeSupport {
9864                discover: crate::ImplementationKind::Absent,
9865                load: crate::ImplementationKind::Absent,
9866                follow: crate::ImplementationKind::Absent,
9867                import: crate::ImplementationKind::Absent,
9868                export: crate::ImplementationKind::Absent,
9869            },
9870            runtime: crate::RuntimeSupport {
9871                implementation: crate::ImplementationKind::BuiltIn,
9872                protocol: protocol.into(),
9873                default_launch: None,
9874                connect_launch: Some(crate::RuntimeConnectLaunch {
9875                    config_path: "~/opencode-tui.json".into(),
9876                    address_pointer: "/server/url".into(),
9877                    port_pointer: None,
9878                    default_address: None,
9879                    auth_pointer: Some("/server/token".into()),
9880                    protocol: protocol.into(),
9881                }),
9882                capabilities: crate::RuntimeCapabilities {
9883                    start_session: true,
9884                    resume_session: true,
9885                    attach_existing_process: true,
9886                    send_input: true,
9887                    stream_events: true,
9888                    interrupt: true,
9889                    steer: false,
9890                    respond_to_requests: true,
9891                },
9892            },
9893        }
9894    }
9895
9896    #[tokio::test]
9897    async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9898        let (base_url, mut requests) = mock_opencode_endpoint().await;
9899        let home = connect_scratch_home("open");
9900        std::fs::write(
9901            home.join("opencode-tui.json"),
9902            format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9903        )
9904        .unwrap();
9905
9906        let descriptor = connect_descriptor("opencode-http-sse");
9907        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9908        assert!(backend.capabilities().attach_existing_process);
9909
9910        let connection = backend
9911            .start(crate::RuntimeStartRequest {
9912                cwd: home.clone(),
9913                launch: None,
9914                mcp_servers: Vec::new(),
9915            })
9916            .await
9917            .unwrap();
9918        let handle = connection.handle();
9919        assert_eq!(handle.runtime_id, "mock-session");
9920        match &handle.endpoint {
9921            crate::RuntimeEndpoint::Http {
9922                base_url: endpoint, ..
9923            } => assert_eq!(endpoint, &base_url),
9924            other => panic!("connect mode must join the running endpoint, got {other:?}"),
9925        }
9926
9927        let mut seen = Vec::new();
9928        while let Ok(line) = requests.try_recv() {
9929            seen.push(line);
9930        }
9931        assert!(seen
9932            .iter()
9933            .any(|line| line.starts_with("GET /global/health")
9934                && line.contains("bearer connect-secret")));
9935        assert!(seen.iter().any(
9936            |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9937        ));
9938    }
9939
9940    /// UNI-5 dev/02, contract corrected by the 2026-08-31 blind walk: the
9941    /// full connect-mode attach path against a MOCK gateway bridge — no live
9942    /// gateway, no model spend. A scripted fake `openclaw` binary (a)
9943    /// asserts the REAL bridge contract — the resolved --url on argv and the
9944    /// credential via --token-file (the real bridge ignores the env var; the
9945    /// endpoint comes from openclaw-native `gateway.remote.url`, never the
9946    /// schema-invalid `gateway.url`) — then (b) speaks scripted ACP:
9947    /// initialize advertising sessionCapabilities.{list,resume},
9948    /// session/resume rebinding the requested session (join), and a
9949    /// prompted turn.
9950    #[tokio::test]
9951    async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9952        let home = connect_scratch_home("openclaw");
9953        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9954        std::fs::write(
9955            home.join(".openclaw/openclaw.json"),
9956            r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9957        )
9958        .unwrap();
9959        let script = home.join("openclaw");
9960        std::fs::write(
9961            &script,
9962            r#"#!/bin/sh
9963# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9964[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9965[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9966[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9967[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9968while IFS= read -r line; do
9969  case "$line" in
9970    *'"initialize"'*)
9971      printf '%s
9972' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9973    *'"session/resume"'*)
9974      printf '%s
9975' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9976    *'"session/new"'*)
9977      printf '%s
9978' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9979    *'"session/prompt"'*)
9980      printf '%s
9981' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9982      printf '%s
9983' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9984  esac
9985done
9986"#,
9987        )
9988        .unwrap();
9989        use std::os::unix::fs::PermissionsExt;
9990        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9991
9992        let mut descriptor = crate::harness_support_registry()
9993            .harnesses
9994            .into_iter()
9995            .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9996            .expect("openclaw must be registered");
9997        descriptor
9998            .runtime
9999            .connect_launch
10000            .as_mut()
10001            .unwrap()
10002            .config_path = "~/.openclaw/openclaw.json".into();
10003        descriptor.runtime.default_launch.as_mut().unwrap().program =
10004            script.to_string_lossy().into_owned();
10005        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10006        assert!(backend.capabilities().resume_session);
10007
10008        let joined = backend
10009            .attach(crate::RuntimeAttachRequest {
10010                runtime_id: "agent:main:main".into(),
10011                cwd: Some(home.clone()),
10012                launch: None,
10013                mcp_servers: Vec::new(),
10014            })
10015            .await;
10016        let mut connection = joined.expect("mock bridge attach must succeed");
10017        assert_eq!(connection.handle().runtime_id, "agent:main:main");
10018        let turn = connection
10019            .send_input(crate::RuntimeInput {
10020                text: "hello".into(),
10021                image_urls: Vec::new(),
10022            })
10023            .await;
10024        assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
10025        connection.close().await.unwrap();
10026    }
10027
10028    #[tokio::test]
10029    async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
10030        let home = connect_scratch_home("fail");
10031        std::fs::write(
10032            home.join("opencode-tui.json"),
10033            r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
10034        )
10035        .unwrap();
10036
10037        let gateway_only = connect_descriptor("acp-v1-jsonrpc");
10038        let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
10039            panic!("an ACP connect endpoint has no gateway client yet");
10040        };
10041        let message = format!("{error:?}");
10042        assert!(message.contains("acp-v1-jsonrpc"));
10043        assert!(!message.contains("connect-secret"));
10044
10045        let unreadable = connect_descriptor("opencode-http-sse");
10046        let missing_home = connect_scratch_home("missing");
10047        let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
10048            panic!("an unreadable connect config must fail closed");
10049        };
10050        let message = format!("{error:?}");
10051        assert!(message.contains("opencode-tui.json"));
10052        assert!(!message.contains("connect-secret"));
10053    }
10054
10055    // ---------------------------------------------------------------------
10056    // ORCH-7 — `harness.v1.jobs.list` / `jobs.get` over the committed fixtures
10057    // ---------------------------------------------------------------------
10058
10059    fn jobs_fixture_root() -> PathBuf {
10060        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10061    }
10062
10063    /// Point only the three job-bearing homes at the fixtures. Nothing else is
10064    /// read, so the host machine's own harness homes cannot leak into a row.
10065    fn jobs_fixture_homes() -> Value {
10066        let root = jobs_fixture_root();
10067        json!({
10068            "claude_code": root.join("claude_jobs_home/projects"),
10069            "hermes": root.join("hermes_home/state.db"),
10070            "openclaw": root.join("openclaw_home"),
10071        })
10072    }
10073
10074    fn jobs_list(params: Value) -> Value {
10075        let mut service = HarnessSessionService::new();
10076        service.handle(request(1, "harness.v1.jobs.list", params))
10077    }
10078
10079    fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10080        result["jobs"]
10081            .as_array()
10082            .expect("jobs is an array")
10083            .iter()
10084            .find(|job| job["id"] == id)
10085            .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10086    }
10087
10088    #[test]
10089    fn gateway_health_derives_from_running_probe_and_install_state() {
10090        let running = RunningInstance {
10091            method: RunningInstanceMethod::GatewayConnect,
10092            evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10093            checked_at_ms: 1,
10094        };
10095        let up = gateway_health(
10096            HarnessId::OPENCLAW,
10097            true,
10098            Some(&running),
10099            Some("2026.7.1-2"),
10100        );
10101        assert_eq!(up.state, GatewayState::Up);
10102        assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10103        assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10104        // Hermes consults its own `gateway status` when the WAL heuristic says
10105        // nothing; a fake binary decides the verdict (the env var is global, so
10106        // the up/down cases run inside this one test, never in parallel).
10107        let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10108        std::fs::create_dir_all(&dir).unwrap();
10109        let fake = dir.join("hermes");
10110        let write_fake = |body: &str| {
10111            std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10112            #[cfg(unix)]
10113            {
10114                use std::os::unix::fs::PermissionsExt;
10115                std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10116            }
10117        };
10118        write_fake("echo '✗ Gateway service is not installed'");
10119        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10120            *slot.borrow_mut() = Some((
10121                HarnessId::HERMES.to_string(),
10122                fake.to_string_lossy().into_owned(),
10123            ))
10124        });
10125        let down = gateway_health(HarnessId::HERMES, true, None, None);
10126        assert_eq!(down.state, GatewayState::Down, "{down:?}");
10127        assert!(down.endpoint.is_none());
10128        assert!(down.evidence.contains("not installed"));
10129        write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10130        let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10131        assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10132        assert!(idle_but_up.evidence.contains("PID 4242"));
10133        write_fake("echo 'something unparseable'");
10134        let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10135        assert_eq!(no_verdict.state, GatewayState::Down);
10136        assert!(no_verdict.evidence.contains("no verdict"));
10137        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10138        let absent = gateway_health(HarnessId::HERMES, false, None, None);
10139        assert_eq!(absent.state, GatewayState::Unknown);
10140        let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10141        assert_eq!(core.state, GatewayState::Unknown);
10142        assert!(core.evidence.contains("per session"));
10143    }
10144
10145    #[test]
10146    fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10147        let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10148        let rows = response["result"]["triggers"]
10149            .as_array()
10150            .expect("triggers")
10151            .clone();
10152        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10153        assert!(
10154            hermes.iter().any(|r| r["name"] == "deploys"
10155                && r["route"] == "/webhooks/deploys"
10156                && r["kind"] == "webhook"),
10157            "{rows:#?}"
10158        );
10159        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10160        assert!(openclaw
10161            .iter()
10162            .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10163        assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10164            && r["kind"] == "hook_mapping"
10165            && r["target"]["action"] == "agent"));
10166        let rendered = response.to_string();
10167        for secret in [
10168            "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10169            "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10170        ] {
10171            assert!(!rendered.contains(secret), "{rendered}");
10172        }
10173        let refused =
10174            triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10175        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10176    }
10177
10178    fn triggers_list(params: Value) -> Value {
10179        let mut service = HarnessSessionService::new();
10180        service.handle(request(1, "harness.v1.triggers.list", params))
10181    }
10182
10183    #[test]
10184    fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10185        let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10186        let rows = response["result"]["routes"]
10187            .as_array()
10188            .expect("routes")
10189            .clone();
10190        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10191        assert_eq!(hermes.len(), 2, "{rows:#?}");
10192        assert_eq!(hermes[0]["target"], "coder");
10193        assert_eq!(hermes[0]["match"]["platform"], "slack");
10194        assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10195        assert_eq!(hermes[0]["specificity"], 4);
10196        assert_eq!(hermes[1]["default"], true);
10197        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10198        assert!(
10199            openclaw.iter().any(|r| r["target"] == "design"
10200                && r["match"]["platform"] == "slack"
10201                && r["specificity"] == 1),
10202            "{openclaw:#?}"
10203        );
10204        assert!(openclaw.iter().any(|r| r["default"] == true));
10205        // A core harness has no routing concept and is refused, never an empty list.
10206        let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10207        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10208    }
10209
10210    fn routes_list(params: Value) -> Value {
10211        let mut service = HarnessSessionService::new();
10212        service.handle(request(1, "harness.v1.routes.list", params))
10213    }
10214
10215    #[test]
10216    fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10217        let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10218        let result = &response["result"];
10219        let ids: Vec<&str> = result["jobs"]
10220            .as_array()
10221            .unwrap()
10222            .iter()
10223            .map(|job| job["id"].as_str().unwrap())
10224            .collect();
10225        assert_eq!(
10226            ids,
10227            vec![
10228                "release-watch",
10229                "toolu_wake_recheck",
10230                "digest-15m",
10231                "nightly-audit",
10232                "coder-standup",
10233                "ops-once-boot",
10234                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10235                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10236                "cron_standup",
10237                "cron_reindex",
10238            ],
10239            "{result}"
10240        );
10241
10242        // OpenClaw, pinned shape: rows come from `state/openclaw.sqlite`
10243        // (`cron_jobs.job_json` + runtime columns), captured from a real
10244        // 2026.7.1-2 gateway.
10245        let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10246        assert_eq!(health["harness"], "openclaw");
10247        assert_eq!(health["schedule"]["kind"], "interval");
10248        assert_eq!(health["schedule"]["minutes"], 10.0);
10249        assert_eq!(health["session_target"], "isolated");
10250        assert_eq!(health["payload"]["kind"], "prompt");
10251        assert_eq!(health["payload"]["text"], "nightly health check");
10252        // ORCH-13: the mode word (`announce`) and the channel it announces on
10253        // (`last`) are separate facts, and the store keeps both — in
10254        // `job_json.delivery` and in the `delivery_*` columns beside it.
10255        assert_eq!(health["deliver"]["mode"], "announce");
10256        assert_eq!(health["deliver"]["target"], "last");
10257        assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10258        let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10259        assert_eq!(digest["schedule"]["kind"], "cron");
10260        assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10261        assert_eq!(digest["session_target"], "main");
10262        assert_eq!(digest["payload"]["kind"], "system_event");
10263
10264        // Claude Code: session-scoped, one recurring cron and one one-shot wakeup.
10265        let cron = job_row(result, "release-watch");
10266        assert_eq!(cron["harness"], "claude-code");
10267        assert_eq!(cron["scope"], "session");
10268        assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10269        assert_eq!(cron["schedule"]["kind"], "cron");
10270        assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10271        assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10272        assert_eq!(cron["payload"]["kind"], "prompt");
10273        assert_eq!(cron["recurring"], true);
10274        assert_eq!(cron["deliver"]["target"], "session");
10275        let wakeup = job_row(result, "toolu_wake_recheck");
10276        assert_eq!(wakeup["payload"]["kind"], "wakeup");
10277        assert_eq!(wakeup["schedule"]["kind"], "once");
10278        assert_eq!(wakeup["recurring"], false);
10279        assert_eq!(wakeup["state"], "pending");
10280
10281        // Hermes: install-scoped, interval + origin delivery, and a paused cron.
10282        let interval = job_row(result, "digest-15m");
10283        assert_eq!(interval["harness"], "hermes");
10284        assert_eq!(interval["scope"], "install");
10285        assert_eq!(interval["profile"], Value::Null);
10286        assert_eq!(interval["schedule"]["kind"], "interval");
10287        assert_eq!(interval["schedule"]["minutes"], 15.0);
10288        assert_eq!(interval["schedule"]["display"], "every 15 min");
10289        assert_eq!(interval["deliver"]["target"], "origin");
10290        assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10291        assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10292        assert_eq!(interval["last_status"], "ok");
10293        let nightly = job_row(result, "nightly-audit");
10294        assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10295        assert_eq!(nightly["deliver"]["target"], "local");
10296        assert_eq!(nightly["enabled"], false);
10297        assert_eq!(nightly["state"], "paused");
10298        // The per-profile store carries the profile name from its own path.
10299        let profiled = job_row(result, "ops-once-boot");
10300        assert_eq!(profiled["profile"], "ops");
10301        assert_eq!(profiled["schedule"]["kind"], "once");
10302        assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10303        assert_eq!(profiled["payload"]["kind"], "script");
10304        // An explicit `<platform>:<chat>` target carries the chat itself.
10305        assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10306        assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10307        assert_eq!(profiled["recurring"], false);
10308
10309        // ORCH-13: a job delivering to its creating conversation carries that
10310        // conversation's whole surface — platform word, chat AND thread.
10311        let standup_to_group = job_row(result, "coder-standup");
10312        assert_eq!(standup_to_group["deliver"]["target"], "origin");
10313        assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10314        assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10315        // Hermes has no mode word and routes by adapter profile, not account.
10316        assert!(standup_to_group["deliver"]["mode"].is_null());
10317        assert!(standup_to_group["deliver"]["account"].is_null());
10318
10319        // OpenClaw: the session target and the delivery mode are the row's own
10320        // columns, not a footnote.
10321        let standup = job_row(result, "cron_standup");
10322        assert_eq!(standup["harness"], "openclaw");
10323        assert_eq!(standup["session_target"], "isolated");
10324        assert_eq!(standup["deliver"]["mode"], "announce");
10325        assert_eq!(standup["deliver"]["target"], "slack");
10326        assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10327        assert_eq!(standup["payload"]["kind"], "prompt");
10328        assert_eq!(standup["profile"], "main");
10329        let reindex = job_row(result, "cron_reindex");
10330        assert_eq!(reindex["session_target"], "main");
10331        assert_eq!(reindex["payload"]["kind"], "system_event");
10332        assert_eq!(reindex["schedule"]["kind"], "interval");
10333        assert_eq!(reindex["schedule"]["display"], "every 240 min");
10334        assert_eq!(reindex["enabled"], false);
10335
10336        // Every store consulted is named, so an empty answer is never silent.
10337        let states: Vec<(&str, &str)> = result["sources"]
10338            .as_array()
10339            .unwrap()
10340            .iter()
10341            .map(|source| {
10342                (
10343                    source["harness"].as_str().unwrap(),
10344                    source["state"].as_str().unwrap(),
10345                )
10346            })
10347            .collect();
10348        // The `coder` profile home has no cron store at all: it is named as
10349        // `absent_store`, not skipped, so "this profile schedules nothing" and
10350        // "this profile was never looked at" stay distinguishable.
10351        assert_eq!(
10352            states,
10353            vec![
10354                ("claude-code", "scanned"),
10355                ("hermes", "read"),
10356                ("hermes", "absent_store"),
10357                ("hermes", "read"),
10358                ("openclaw", "read"),
10359                ("openclaw", "read"),
10360            ],
10361            "{result}"
10362        );
10363    }
10364
10365    #[test]
10366    fn jobs_list_filters_by_harness_session_and_profile() {
10367        let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10368        let ids: Vec<&str> = by_harness["result"]["jobs"]
10369            .as_array()
10370            .unwrap()
10371            .iter()
10372            .map(|job| job["id"].as_str().unwrap())
10373            .collect();
10374        assert_eq!(
10375            ids,
10376            vec![
10377                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10378                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10379                "cron_standup",
10380                "cron_reindex",
10381            ]
10382        );
10383
10384        let by_session = jobs_list(json!({
10385            "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10386            "homes": jobs_fixture_homes(),
10387        }));
10388        let jobs = by_session["result"]["jobs"].as_array().unwrap();
10389        assert_eq!(jobs.len(), 2, "{by_session}");
10390        assert!(jobs
10391            .iter()
10392            .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10393
10394        let by_profile = jobs_list(json!({
10395            "harness": "hermes",
10396            "profile": "ops",
10397            "homes": jobs_fixture_homes(),
10398        }));
10399        let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10400        assert_eq!(jobs.len(), 1, "{by_profile}");
10401        assert_eq!(jobs[0]["id"], "ops-once-boot");
10402    }
10403
10404    #[test]
10405    fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10406        let mut service = HarnessSessionService::new();
10407        let hermes = service.handle(request(
10408            1,
10409            "harness.v1.jobs.get",
10410            json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10411        ));
10412        assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10413        // Native fields the uniform row does not carry survive on `source`.
10414        assert_eq!(hermes["result"]["source"]["provider"], "nous");
10415        assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10416
10417        let claude = service.handle(request(
10418            2,
10419            "harness.v1.jobs.get",
10420            json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10421        ));
10422        assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10423        assert_eq!(
10424            claude["result"]["source"]["tool_use_id"],
10425            "toolu_cron_release_watch"
10426        );
10427
10428        let missing = service.handle(request(
10429            3,
10430            "harness.v1.jobs.get",
10431            json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10432        ));
10433        assert!(missing["error"]["message"]
10434            .as_str()
10435            .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10436    }
10437
10438    #[test]
10439    fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10440        let mut service = HarnessSessionService::new();
10441        for (id, method, params) in [
10442            (
10443                1,
10444                "harness.v1.jobs.list",
10445                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10446            ),
10447            (
10448                2,
10449                "harness.v1.jobs.get",
10450                json!({"harness": "codex", "id": "anything"}),
10451            ),
10452        ] {
10453            let response = service.handle(request(id, method, params));
10454            assert_eq!(response["error"]["code"], -32020, "{response}");
10455            assert!(response["error"]["message"]
10456                .as_str()
10457                .is_some_and(|message| message.contains("has no scheduled jobs")));
10458            assert!(response.get("result").is_none());
10459        }
10460    }
10461
10462    #[test]
10463    fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10464        let scratch = std::env::temp_dir().join(format!(
10465            "supercode-jobs-migrated-{}-{}",
10466            std::process::id(),
10467            generated_session_id()
10468        ));
10469        std::fs::create_dir_all(&scratch).unwrap();
10470        let response = jobs_list(json!({
10471            "harness": "openclaw",
10472            "homes": {"openclaw": scratch.clone()},
10473        }));
10474        let result = &response["result"];
10475        assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10476        assert_eq!(result["sources"][0]["state"], "absent_store");
10477        assert_eq!(result["sources"][0]["harness"], "openclaw");
10478        std::fs::remove_dir_all(&scratch).ok();
10479    }
10480
10481    // ---------------------------------------------------------------------
10482    // ORCH-8 — `harness.v1.runs.list` / `runs.get` over the committed fire
10483    // stores: Hermes's `cron/executions.db` (root home + profile home) and
10484    // OpenClaw's `cron_run_logs`. Every fixture row is written by
10485    // `tests/fixtures/gen_runs_fixtures.py` against the harnesses' own DDL.
10486    // ---------------------------------------------------------------------
10487
10488    /// The health job in the committed OpenClaw fixture, which fired twice.
10489    const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10490    /// The digest job, whose single fire predates run ids.
10491    const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10492
10493    fn runs_list(params: Value) -> Value {
10494        let mut service = HarnessSessionService::new();
10495        service.handle(request(1, "harness.v1.runs.list", params))
10496    }
10497
10498    fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10499        result["runs"]
10500            .as_array()
10501            .expect("runs is an array")
10502            .iter()
10503            .find(|run| run["id"] == id)
10504            .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10505    }
10506
10507    #[test]
10508    fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10509        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10510        let result = &response["result"];
10511        let ids: Vec<&str> = result["runs"]
10512            .as_array()
10513            .expect("runs is an array")
10514            .iter()
10515            .map(|run| run["id"].as_str().unwrap())
10516            .collect();
10517        let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10518        assert_eq!(
10519            ids,
10520            vec![
10521                // Hermes, newest claim first, root ledger then profile ledger.
10522                "b2c3d4e5f60718293a4b5c6d7e8f9012",
10523                "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10524                "c3d4e5f60718293a4b5c6d7e8f901234",
10525                "f60718293a4b5c6d7e8f901234567890",
10526                "e5f60718293a4b5c6d7e8f9012345678",
10527                "d4e5f60718293a4b5c6d7e8f90123456",
10528                // OpenClaw, newest `ts` first.
10529                "run_health_0002",
10530                digest_fire.as_str(),
10531                "run_health_0001",
10532            ],
10533            "{result}"
10534        );
10535
10536        // The harness's OWN outcome word survives; nothing is renamed onto a
10537        // shared vocabulary.
10538        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10539        assert_eq!(failed["harness"], "hermes");
10540        assert_eq!(failed["job_id"], "job42");
10541        assert_eq!(failed["status"], "failed");
10542        assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10543        assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10544
10545        // Hermes's `unknown` — an attempt whose owner died before writing a
10546        // terminal state — is a fourth status, not folded into `failed`.
10547        let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10548        assert_eq!(abandoned["status"], "unknown");
10549        assert_eq!(abandoned["job_id"], "ops-once-boot");
10550
10551        // An unterminated fire has no finish, and no session is invented.
10552        let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10553        assert_eq!(running["status"], "running");
10554        assert!(running["finished_at"].is_null(), "{running}");
10555        assert!(running["session_id"].is_null(), "{running}");
10556
10557        // OpenClaw records the session on the row itself, and epoch-ms
10558        // timestamps are rendered as RFC 3339.
10559        let ok = run_row(result, "run_health_0001");
10560        assert_eq!(ok["harness"], "openclaw");
10561        assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10562        assert_eq!(ok["status"], "ok");
10563        assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10564        assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10565        assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10566        // OpenClaw's run log is written once, at finish: there is no claim.
10567        assert!(ok["claimed_at"].is_null(), "{ok}");
10568
10569        // A run-log row with no `run_id` falls back to the store's own
10570        // `(job_id, seq)` key rather than being dropped.
10571        assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10572
10573        // ORCH-13: a fire whose delivery nothing recorded says so, rather than
10574        // borrowing a neighbouring fire's outcome. Both of these ran on jobs
10575        // that deliver `local` (or have no job record at all), so no
10576        // obligation is addressed to a surface they could match.
10577        for id in [
10578            "b2c3d4e5f60718293a4b5c6d7e8f9012",
10579            "d4e5f60718293a4b5c6d7e8f90123456",
10580        ] {
10581            assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10582        }
10583
10584        // Every store consulted is named, including the profile home that has
10585        // no ledger — an empty history and an absent store are different.
10586        let sources = result["sources"].as_array().unwrap();
10587        let states: Vec<(&str, &str)> = sources
10588            .iter()
10589            .map(|source| {
10590                (
10591                    source["harness"].as_str().unwrap(),
10592                    source["state"].as_str().unwrap(),
10593                )
10594            })
10595            .collect();
10596        assert_eq!(
10597            states,
10598            vec![
10599                ("hermes", "read"),
10600                ("hermes", "absent_store"),
10601                ("hermes", "read"),
10602                ("openclaw", "read"),
10603            ],
10604            "{result}"
10605        );
10606        assert_eq!(sources[2]["profile"], "ops");
10607        assert!(sources[3]["path"]
10608            .as_str()
10609            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10610    }
10611
10612    #[test]
10613    fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10614        let response = runs_list(json!({
10615            "harness": "hermes",
10616            "job": "job42",
10617            "homes": jobs_fixture_homes(),
10618        }));
10619        let result = &response["result"];
10620        assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10621
10622        // Hermes writes NO link from an execution to its session. The fire
10623        // that ran the agent is joined to `cron_job42_<stamp>` because that
10624        // id's instant falls inside its [claimed_at, finished_at] window.
10625        let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10626        assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10627
10628        // The later fire failed before opening one. Its window holds no
10629        // session, so the row says so instead of re-using the earlier fire's
10630        // — the join is per-FIRE, not per-job.
10631        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10632        assert!(failed["session_id"].is_null(), "{failed}");
10633    }
10634
10635    /// ORCH-13: where a fire's output went, read from each harness's own
10636    /// delivery record — Hermes's `delivery_obligations` ledger inside
10637    /// `state.db`, OpenClaw's `delivery_*` run-log columns.
10638    #[test]
10639    fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10640        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10641        let result = &response["result"];
10642
10643        // Hermes: the ledger is the GATEWAY's, keyed by conversation and
10644        // surface, so the fire's own [claimed_at, finished_at] window picks
10645        // the obligation. The fire succeeded and so did the send.
10646        let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10647        assert_eq!(delivered["status"], "completed");
10648        assert_eq!(delivered["delivery"]["state"], "delivered");
10649        assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10650        assert_eq!(delivered["delivery"]["attempts"], 1);
10651        assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10652        assert_eq!(
10653            delivered["delivery"]["delivered_at"],
10654            "2026-09-02T09:00:30.400Z"
10655        );
10656
10657        // The next fire of the same job ALSO succeeded — and its output never
10658        // arrived. That is the fact `status` alone cannot carry.
10659        let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10660        assert_eq!(undelivered["status"], "completed");
10661        assert_eq!(undelivered["delivery"]["state"], "failed");
10662        assert_eq!(undelivered["delivery"]["attempts"], 3);
10663        assert_eq!(
10664            undelivered["delivery"]["last_error"],
10665            "telegram send failed: Bad Request: chat not found"
10666        );
10667        // Only a delivered obligation carries an instant of delivery; the
10668        // ledger's `updated_at` on a failed row dates the failure.
10669        assert!(
10670            undelivered["delivery"]["delivered_at"].is_null(),
10671            "{undelivered}"
10672        );
10673
10674        // OpenClaw writes the outcome onto the run-log row and declares the
10675        // address on the job, so the row's target is joined from `cron_jobs`.
10676        let announced = run_row(result, "run_health_0001");
10677        assert_eq!(announced["delivery"]["state"], "delivered");
10678        assert_eq!(announced["delivery"]["target"], "last");
10679        // Its run log counts no attempts and stamps no delivered-at.
10680        assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10681        assert!(
10682            announced["delivery"]["delivered_at"].is_null(),
10683            "{announced}"
10684        );
10685        let refused = run_row(result, "run_health_0002");
10686        assert_eq!(refused["delivery"]["state"], "not-delivered");
10687        assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10688
10689        // A run-log row with no delivery columns at all recorded no delivery:
10690        // the job's declared target is not evidence that anything was sent.
10691        let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10692        assert!(skipped["delivery"].is_null(), "{skipped}");
10693    }
10694
10695    /// A Hermes fire whose session carries a `session_key` is matched on that
10696    /// key FIRST — the most specific question the ledger can answer. Proven by
10697    /// moving the obligations off the job's surface on a COPY of the fixture,
10698    /// so only the session-key question can still find them.
10699    #[test]
10700    fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10701        let scratch = std::env::temp_dir().join(format!(
10702            "supercode-runs-delivery-{}-{}",
10703            std::process::id(),
10704            generated_session_id()
10705        ));
10706        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10707        let fixture = jobs_fixture_root().join("hermes_home");
10708        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10709        for name in ["cron/executions.db", "cron/jobs.json"] {
10710            std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10711        }
10712        {
10713            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10714            // The obligations now sit on a surface no job in this store
10715            // delivers to, so the surface question cannot match them.
10716            connection
10717                .execute(
10718                    "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10719                    [],
10720                )
10721                .unwrap();
10722            // A cron fire that ran inside a keyed conversation: the session
10723            // the window recovers carries `tg-coder-1`'s key.
10724            connection
10725                .execute(
10726                    "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10727                     ('cron_coder-standup_20260902_090010', 'cron', \
10728                      'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10729                    [],
10730                )
10731                .unwrap();
10732        }
10733        let response = runs_list(json!({
10734            "harness": "hermes",
10735            "job": "coder-standup",
10736            "homes": {"hermes": scratch.join("state.db")},
10737        }));
10738        let result = &response["result"];
10739        let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10740        assert_eq!(
10741            matched["session_id"], "cron_coder-standup_20260902_090010",
10742            "{result}"
10743        );
10744        assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10745        assert_eq!(
10746            matched["delivery"]["target"], "slack:C0FALLBACK:55",
10747            "{result}"
10748        );
10749        std::fs::remove_dir_all(&scratch).ok();
10750    }
10751
10752    #[test]
10753    fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10754        // A fire whose session was compressed mid-run is only readable at the
10755        // continuation, so that is what the row must report. Built on a COPY
10756        // of the committed fixture: no test writes to a fixture or to a real
10757        // harness home.
10758        let scratch = std::env::temp_dir().join(format!(
10759            "supercode-runs-compressed-{}-{}",
10760            std::process::id(),
10761            generated_session_id()
10762        ));
10763        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10764        let fixture = jobs_fixture_root().join("hermes_home");
10765        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10766        std::fs::copy(
10767            fixture.join("cron/executions.db"),
10768            scratch.join("cron/executions.db"),
10769        )
10770        .unwrap();
10771        {
10772            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10773            connection
10774                .execute(
10775                    "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10776                    ["cron_job42_20260902_120000"],
10777                )
10778                .unwrap();
10779            connection
10780                .execute(
10781                    "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10782                     VALUES ('job42-after-compaction', 'cron', \
10783                             'cron_job42_20260902_120000', 1788350000.0)",
10784                    [],
10785                )
10786                .unwrap();
10787        }
10788        let response = runs_list(json!({
10789            "harness": "hermes",
10790            "job": "job42",
10791            "homes": {"hermes": scratch.join("state.db")},
10792        }));
10793        let result = &response["result"];
10794        assert_eq!(
10795            run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10796            "job42-after-compaction",
10797            "{result}"
10798        );
10799        std::fs::remove_dir_all(&scratch).ok();
10800    }
10801
10802    #[test]
10803    fn runs_list_filters_by_job_and_caps_by_limit() {
10804        let by_job = runs_list(json!({
10805            "harness": "openclaw",
10806            "job": OPENCLAW_HEALTH_JOB,
10807            "homes": jobs_fixture_homes(),
10808        }));
10809        let ids: Vec<&str> = by_job["result"]["runs"]
10810            .as_array()
10811            .unwrap()
10812            .iter()
10813            .map(|run| run["id"].as_str().unwrap())
10814            .collect();
10815        assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10816
10817        let capped = runs_list(json!({
10818            "harness": "openclaw",
10819            "limit": 1,
10820            "homes": jobs_fixture_homes(),
10821        }));
10822        let runs = capped["result"]["runs"].as_array().unwrap();
10823        assert_eq!(runs.len(), 1, "{capped}");
10824        // Newest first, so the cap keeps the recent fire.
10825        assert_eq!(runs[0]["id"], "run_health_0002");
10826    }
10827
10828    #[test]
10829    fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10830        let mut service = HarnessSessionService::new();
10831        let hermes = service.handle(request(
10832            1,
10833            "harness.v1.runs.get",
10834            json!({
10835                "harness": "hermes",
10836                "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10837                "homes": jobs_fixture_homes(),
10838            }),
10839        ));
10840        assert_eq!(hermes["result"]["run"]["status"], "completed");
10841        assert_eq!(
10842            hermes["result"]["run"]["session_id"],
10843            "cron_job42_20260902_120000"
10844        );
10845        // Ledger columns the uniform row does not carry survive on `source`.
10846        assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10847        assert_eq!(hermes["result"]["source"]["pid"], 4242);
10848        assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10849
10850        let openclaw = service.handle(request(
10851            2,
10852            "harness.v1.runs.get",
10853            json!({
10854                "harness": "openclaw",
10855                "id": "run_health_0002",
10856                "homes": jobs_fixture_homes(),
10857            }),
10858        ));
10859        assert_eq!(openclaw["result"]["run"]["status"], "error");
10860        // ORCH-13: the run's delivery is projected AND the store's own columns
10861        // stay verbatim on `source`, so nothing about the fire is lost.
10862        assert_eq!(
10863            openclaw["result"]["source"]["delivery_status"],
10864            "not-delivered"
10865        );
10866        assert_eq!(
10867            openclaw["result"]["source"]["delivery_error"],
10868            "channel_not_found"
10869        );
10870        assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10871        assert_eq!(
10872            openclaw["result"]["run"]["delivery"]["state"],
10873            "not-delivered"
10874        );
10875        assert_eq!(
10876            openclaw["result"]["run"]["delivery"]["last_error"],
10877            "channel_not_found"
10878        );
10879
10880        let missing = service.handle(request(
10881            3,
10882            "harness.v1.runs.get",
10883            json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10884        ));
10885        assert!(missing["error"]["message"]
10886            .as_str()
10887            .is_some_and(|message| message.contains("no run `no-such-run`")));
10888    }
10889
10890    #[test]
10891    fn runs_refuse_a_harness_that_keeps_no_run_store() {
10892        let mut service = HarnessSessionService::new();
10893        for (id, method, params) in [
10894            // Claude Code HAS scheduled jobs but no fire store: its fires are
10895            // ordinary turns. It must refuse, not answer with an empty list.
10896            (
10897                1,
10898                "harness.v1.runs.list",
10899                json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10900            ),
10901            (
10902                2,
10903                "harness.v1.runs.get",
10904                json!({"harness": "claude-code", "id": "anything"}),
10905            ),
10906            (
10907                3,
10908                "harness.v1.runs.list",
10909                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10910            ),
10911        ] {
10912            let response = service.handle(request(id, method, params));
10913            assert_eq!(response["error"]["code"], -32020, "{response}");
10914            assert!(response["error"]["message"]
10915                .as_str()
10916                .is_some_and(|message| message.contains("keeps no run store")));
10917            assert!(response.get("result").is_none());
10918        }
10919    }
10920
10921    #[test]
10922    fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10923        let scratch = std::env::temp_dir().join(format!(
10924            "supercode-runs-empty-{}-{}",
10925            std::process::id(),
10926            generated_session_id()
10927        ));
10928        std::fs::create_dir_all(&scratch).unwrap();
10929        let response = runs_list(json!({
10930            "harness": "openclaw",
10931            "homes": {"openclaw": scratch.clone()},
10932        }));
10933        let result = &response["result"];
10934        assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10935        assert_eq!(result["sources"][0]["state"], "absent_store");
10936        assert!(result["sources"][0]["path"]
10937            .as_str()
10938            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10939        std::fs::remove_dir_all(&scratch).ok();
10940    }
10941}