1use 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::reduce;
17use crate::runtime::generated_session_id;
18#[cfg(feature = "adapter-api")]
19use crate::runtime::{HostedHarnessConnection, HostedHarnessRuntime};
20use crate::sdk::{
21 discover_session_page, load_session, load_session_with_fidelity, SdkCapabilities, SdkError,
22 SdkErrorCode, SdkEvent, SdkOperation, SdkRequest, SdkRuntimeEvent, SdkService,
23};
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};
35#[cfg(feature = "adapter-api")]
36use crate::{register_live_runtime, resolve_live_runtime, LiveRuntimeRegistration};
37use supercode_interchange::watch::{bound_session_view, message_json, normalized_session_json};
38
39pub 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.activity_under",
57 "harness.v1.sessions.index.subscribe",
58 "harness.v1.sessions.index.resize",
59 "harness.v1.sessions.index.unsubscribe",
60 "harness.v1.sessions.message",
61 "harness.v1.sessions.inbox",
62 "harness.v1.sessions.import",
63 "harness.v1.sessions.export",
64 "harness.v1.sessions.translate",
65 "harness.v1.sessions.reduce",
66 "harness.v1.sessions.branch",
67 "harness.v1.sessions.handoff",
68 "harness.v1.sessions.materialize",
69 "harness.v1.sessions.resume_instructions",
70 "harness.v1.skills.list",
71 "harness.v1.skills.install",
72 "harness.v1.skills.remove",
73 "harness.v1.memory.show",
74 "harness.v1.memory.search",
75 "harness.v1.jobs.list",
76 "harness.v1.jobs.get",
77 "harness.v1.jobs.create",
78 "harness.v1.jobs.update",
79 "harness.v1.jobs.pause",
80 "harness.v1.jobs.resume",
81 "harness.v1.jobs.run",
82 "harness.v1.jobs.delete",
83 "harness.v1.jobs.notepad",
84 "harness.v1.jobs.notepad_set",
85 "harness.v1.jobs.notepad_delete",
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
125pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
127pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
129pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
131pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
133pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
135
136pub 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 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 approvals: crate::approvals::ApprovalRegistry,
162 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 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 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 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 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 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(¶ms.id, &choice)
284 .map_err(|error| ServiceError::InvalidParams(error.to_string()))?;
285 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 #[cfg(feature = "adapter-api")]
308 pub fn session_index_notifier(&self) -> Arc<Notify> {
309 Arc::clone(&self.index_notifier)
310 }
311
312 #[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 #[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 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_under" {
445 let id = request.get("id").cloned().unwrap_or(Value::Null);
446 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
447 return match activity_under_call(params).await {
448 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
449 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
450 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
451 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
452 Err(_) => rpc_error(id, -32000, "sessions.activity_under failed"),
453 };
454 }
455 if method == "harness.v1.sessions.activity.subscribe" {
456 let id = request.get("id").cloned().unwrap_or(Value::Null);
457 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
458 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
459 }
460 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
461 return match self.subscribe_session_activity(params).await {
462 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
463 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
464 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
465 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
466 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
467 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
468 };
469 }
470 if let Some(operation) = SdkOperation::from_method(method) {
471 let id = request.get("id").cloned().unwrap_or(Value::Null);
472 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
473 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
474 }
475 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
476 return match self.execute(SdkRequest { operation, params }).await {
477 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
478 Err(error) => sdk_rpc_error(id, &error),
479 };
480 }
481 if !method.starts_with("harness.v1.runtimes.") {
482 return self.handle(request);
483 }
484 let id = request.get("id").cloned().unwrap_or(Value::Null);
485 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
486 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
487 }
488 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
489 match self.runtime_call(method, params).await {
490 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
491 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
492 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
493 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
494 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
495 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
496 }
497 }
498
499 #[cfg(feature = "adapter-api")]
502 pub fn poll(&mut self) -> Vec<Value> {
503 let mut notifications = Vec::new();
504 for (subscription, follower) in &mut self.followers {
505 match follower.poll() {
506 Ok(Some(event)) => notifications.push(json!({
507 "jsonrpc": "2.0",
508 "method": SESSION_EVENT_METHOD,
509 "params": {
510 "subscription": subscription,
511 "event": event.to_json(),
512 }
513 })),
514 Ok(None) => {}
515 Err(error) => notifications.push(json!({
516 "jsonrpc": "2.0",
517 "method": SESSION_EVENT_METHOD,
518 "params": {
519 "subscription": subscription,
520 "event": {
521 "type": "watch_error",
522 "recoverable": true,
523 "message": error.to_string(),
524 },
525 }
526 })),
527 }
528 }
529 notifications
530 }
531
532 #[cfg(feature = "adapter-api")]
543 pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
544 let registry = crate::LocalRuntimeRegistry::new();
545 let authorization = crate::RuntimeAuthorization::observer();
546 let mut notifications = Vec::new();
547 for (subscription, source) in &mut self.followed_sources {
548 let state = match registry
549 .source_state(&source.harness, &source.session_id, &authorization)
550 .await
551 {
552 Ok(Some(state)) => state,
553 Ok(None) => crate::RuntimeRegistryState::Persisted,
554 Err(_) => continue,
556 };
557 if source.reported.as_deref() == Some(state.as_str()) {
558 continue;
559 }
560 source.reported = Some(state.as_str().to_string());
561 notifications.push(json!({
562 "jsonrpc": "2.0",
563 "method": SESSION_EVENT_METHOD,
564 "params": {
565 "subscription": subscription,
566 "event": {"type": "runtime_state", "state": state.as_str()},
567 },
568 }));
569 }
570 notifications
571 }
572
573 #[cfg(feature = "adapter-api")]
577 pub async fn poll_session_activities(&mut self) -> Vec<Value> {
578 let subscriptions = self
579 .activity_subscriptions
580 .iter()
581 .map(|(id, subscription)| {
582 (
583 id.clone(),
584 subscription.locators.clone(),
585 subscription.homes.clone(),
586 )
587 })
588 .collect::<Vec<_>>();
589 let mut notifications = Vec::new();
590 for (subscription_id, locators, homes) in subscriptions {
591 let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
592 continue;
595 };
596 let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
597 continue;
598 };
599 let mut changed = Vec::new();
600 for activity in activities {
601 let key = activity.key();
602 if subscription
603 .reported
604 .get(&key)
605 .is_some_and(|previous| previous.same_state(&activity))
606 {
607 continue;
608 }
609 subscription.reported.insert(key, activity.clone());
610 changed.push(activity);
611 }
612 if !changed.is_empty() {
613 notifications.push(json!({
614 "jsonrpc": "2.0",
615 "method": SESSION_ACTIVITY_EVENT_METHOD,
616 "params": {
617 "subscription": subscription_id,
618 "activities": changed,
619 },
620 }));
621 }
622 }
623 notifications
624 }
625
626 #[cfg(feature = "adapter-api")]
630 pub fn poll_session_indexes(&mut self) -> Vec<Value> {
631 let mut notifications = Vec::new();
632 for (subscription, index) in &mut self.index_subscriptions {
633 let homes = index.homes().clone();
634 match index.poll() {
635 Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
636 Ok(changes) => notifications.push(json!({
637 "jsonrpc": "2.0",
638 "method": SESSION_INDEX_EVENT_METHOD,
639 "params": {
640 "subscription": subscription,
641 "revision": delta.revision,
642 "changes": changes,
643 },
644 })),
645 Err(error) => notifications.push(json!({
646 "jsonrpc": "2.0",
647 "method": SESSION_INDEX_EVENT_METHOD,
648 "params": {
649 "subscription": subscription,
650 "error": {"recoverable": true, "message": error_message(error)},
651 },
652 })),
653 },
654 Ok(None) => {}
655 Err(error) => notifications.push(json!({
656 "jsonrpc": "2.0",
657 "method": SESSION_INDEX_EVENT_METHOD,
658 "params": {
659 "subscription": subscription,
660 "error": {"recoverable": true, "message": error},
661 },
662 })),
663 }
664 }
665 notifications
666 }
667
668 #[cfg(feature = "adapter-api")]
669 async fn subscribe_session_activity(
670 &mut self,
671 params: Value,
672 ) -> std::result::Result<Value, ServiceError> {
673 let params = decode::<ActivitySubscribeParams>(params)?;
674 if params.locators.is_empty() {
675 return Err(ServiceError::InvalidParams(
676 "sessions.activity.subscribe requires at least one locator".into(),
677 ));
678 }
679 if params.locators.len() > 2_048 {
680 return Err(ServiceError::InvalidParams(
681 "sessions.activity.subscribe accepts at most 2048 locators".into(),
682 ));
683 }
684 let initial = self
685 .activity_monitor
686 .resolve(¶ms.locators, ¶ms.homes)
687 .await
688 .map_err(ServiceError::Sdk)?;
689 let subscription = format!("activity-sub-{}", self.next_subscription);
690 self.next_subscription += 1;
691 let reported = initial
692 .iter()
693 .cloned()
694 .map(|activity| (activity.key(), activity))
695 .collect();
696 self.activity_subscriptions.insert(
697 subscription.clone(),
698 ActivitySubscription {
699 locators: params.locators,
700 homes: params.homes,
701 reported,
702 },
703 );
704 Ok(json!({"subscription": subscription, "initial": initial}))
705 }
706
707 #[cfg(feature = "adapter-api")]
709 pub async fn poll_runtimes(&mut self) -> Vec<Value> {
710 self.poll_sdk_events()
711 .await
712 .into_iter()
713 .map(|(connection, runtime_event)| {
714 json!({
715 "jsonrpc": "2.0",
716 "method": RUNTIME_EVENT_METHOD,
717 "params": {
718 "connection": connection,
719 "session_id": runtime_event.session_id,
720 "sequence": runtime_event.event.sequence,
721 "event": {
722 "kind": runtime_event.event.kind,
723 "payload": runtime_event.event.payload,
724 },
725 },
726 })
727 })
728 .collect()
729 }
730
731 async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
732 let mut events = Vec::new();
733 let mut closed = Vec::new();
734 let now_ms = crate::approvals::now_ms();
735 for (connection, runtime) in &mut self.runtimes {
736 let session_id = runtime.handle().runtime_id.clone();
737 let harness = runtime.handle().harness.clone();
738 for _ in 0..256 {
743 match tokio::time::timeout(Duration::ZERO, runtime.next_event()).await {
744 Ok(Ok(Some(event))) => {
745 let terminal = event.kind == "transport_closed";
746 self.approvals
750 .observe(connection, &harness, &session_id, &event, now_ms);
751 let next_sequence = self
752 .runtime_sequences
753 .entry(session_id.clone())
754 .or_insert(0);
755 let sequence = event.sequence.unwrap_or_else(|| {
756 *next_sequence = next_sequence.saturating_add(1);
757 *next_sequence
758 });
759 *next_sequence = (*next_sequence).max(sequence);
760 events.push((
761 connection.clone(),
762 SdkRuntimeEvent {
763 session_id: session_id.clone(),
764 event: SdkEvent {
765 sequence,
766 kind: event.kind,
767 payload: event.payload,
768 },
769 },
770 ));
771 if terminal {
772 closed.push(connection.clone());
773 break;
774 }
775 }
776 Ok(Ok(None)) => {
777 let sequence = self
778 .runtime_sequences
779 .entry(session_id.clone())
780 .or_insert(0);
781 *sequence = sequence.saturating_add(1);
782 events.push((
783 connection.clone(),
784 SdkRuntimeEvent {
785 session_id,
786 event: SdkEvent {
787 sequence: *sequence,
788 kind: "transport_closed".into(),
789 payload: json!({"message": "Harness runtime transport closed."}),
790 },
791 },
792 ));
793 closed.push(connection.clone());
794 break;
795 }
796 Err(_) => break,
797 Ok(Err(error)) => {
798 let sequence = self
799 .runtime_sequences
800 .entry(session_id.clone())
801 .or_insert(0);
802 *sequence = sequence.saturating_add(1);
803 events.push((
804 connection.clone(),
805 SdkRuntimeEvent {
806 session_id,
807 event: SdkEvent {
808 sequence: *sequence,
809 kind: "transport_error".into(),
810 payload: json!({"message": error.to_string(), "terminal": true}),
811 },
812 },
813 ));
814 closed.push(connection.clone());
815 break;
816 }
817 }
818 }
819 }
820 for connection in closed {
821 if let Some(runtime) = self.runtimes.remove(&connection) {
822 self.runtime_sequences.remove(&runtime.handle().runtime_id);
823 }
824 self.terminal_launches.remove(&connection);
825 self.approvals.forget(&connection);
828 }
829 events
830 }
831
832 fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
833 match method {
834 "harness.v1.capabilities" => Ok(json!({
835 "version": HARNESS_SERVICE_VERSION,
836 "sdk": self.capabilities(),
837 "methods": HARNESS_SERVICE_METHODS,
838 "notifications": [
839 SESSION_EVENT_METHOD,
840 SESSION_ACTIVITY_EVENT_METHOD,
841 SESSION_INDEX_EVENT_METHOD,
842 RUNTIME_EVENT_METHOD
843 ],
844 "harnesses": harness_support_registry()
845 .harnesses
846 .into_iter()
847 .map(|harness| harness.id)
848 .collect::<Vec<_>>(),
849 })),
850 "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
851 .map_err(|error| ServiceError::Operation(error.to_string())),
852 "harness.v1.profiles.list" | "harness.v1.profiles.get" => profiles_call(method, params),
853 "harness.v1.profiles.create" => {
859 mutate_profile(crate::profiles_control::ProfileVerb::Create, params)
860 }
861 "harness.v1.profiles.delete" => {
862 mutate_profile(crate::profiles_control::ProfileVerb::Delete, params)
863 }
864 "harness.v1.channels.list" | "harness.v1.channels.status" => {
865 channels_call(method, params)
866 }
867 "harness.v1.routes.list" => routes_call(params),
870 "harness.v1.triggers.list" => triggers_call(params),
872 "harness.v1.workflow.load" => {
882 let params = decode::<WorkflowLoadParams>(params)?;
883 let read =
884 crate::workflow_doors::load(params.from, ¶ms.home).map_err(operation)?;
885 serde_json::to_value(read)
886 .map_err(|error| ServiceError::Operation(error.to_string()))
887 }
888 "harness.v1.orchestration.load" => {
889 let params = decode::<OrchestrationLoadParams>(params)?;
890 let read = crate::orchestration_doors::load(¶ms.root, params.flavor)
891 .map_err(operation)?;
892 serde_json::to_value(read)
893 .map_err(|error| ServiceError::Operation(error.to_string()))
894 }
895 "harness.v1.orchestration.save" => {
896 let params = decode::<OrchestrationSaveParams>(params)?;
897 let saved = crate::orchestration_doors::save(
898 ¶ms.root,
899 params.orchestration,
900 params.vault,
901 )
902 .map_err(operation)?;
903 serde_json::to_value(saved)
904 .map_err(|error| ServiceError::Operation(error.to_string()))
905 }
906 "harness.v1.orchestration.compile" => {
907 let params = decode::<OrchestrationCompileParams>(params)?;
908 let read = crate::orchestration_doors::compile(params.from, ¶ms.home)
909 .map_err(operation)?;
910 serde_json::to_value(read)
911 .map_err(|error| ServiceError::Operation(error.to_string()))
912 }
913 "harness.v1.orchestration.decompile" => {
914 let params = decode::<OrchestrationDecompileParams>(params)?;
915 let report = crate::orchestration_doors::decompile(
916 params.to,
917 params.orchestration,
918 ¶ms.source,
919 params.source_flavor,
920 ¶ms.dest,
921 params.vault,
922 )
923 .map_err(operation)?;
924 serde_json::to_value(report)
925 .map_err(|error| ServiceError::Operation(error.to_string()))
926 }
927 "harness.v1.orchestration.import" => {
932 let params = decode::<OrchestrationImportParams>(params)?;
933 let imported =
934 crate::orchestration_doors::import(params.from, ¶ms.home, ¶ms.into)
935 .map_err(operation)?;
936 serde_json::to_value(imported)
937 .map_err(|error| ServiceError::Operation(error.to_string()))
938 }
939 "harness.v1.orchestration.export" => {
940 let params = decode::<OrchestrationExportParams>(params)?;
941 let report =
942 crate::orchestration_doors::export(params.to, ¶ms.root, ¶ms.dest)
943 .map_err(operation)?;
944 serde_json::to_value(report)
945 .map_err(|error| ServiceError::Operation(error.to_string()))
946 }
947 "harness.v1.memory.show" | "harness.v1.memory.search" => memory_call(method, params),
953 "harness.v1.skills.list" => {
958 let query = decode::<crate::skills::SkillsQuery>(params)?;
959 if let Some(harness) = query.harness.as_deref() {
960 if !crate::skills::SKILL_HARNESSES.contains(&harness) {
961 return Err(ServiceError::UnsupportedAction(format!(
962 "`{harness}` has no skills root Volter Harness reads"
963 )));
964 }
965 }
966 serde_json::to_value(crate::skills::list_skills(&query))
967 .map_err(|error| ServiceError::Operation(error.to_string()))
968 }
969 "harness.v1.skills.install" => {
977 mutate_skill(crate::skills_control::SkillVerb::Install, params)
978 }
979 "harness.v1.skills.remove" => {
980 mutate_skill(crate::skills_control::SkillVerb::Remove, params)
981 }
982 "harness.v1.approvals.list" => {
990 let query = decode::<crate::approvals::ApprovalsQuery>(params)?;
991 if let Some(harness) = query.harness.as_deref() {
992 if !crate::approvals::lists_approvals(harness) {
993 return Err(ServiceError::UnsupportedAction(format!(
994 "`{harness}` has no runtime door that carries an approval request"
995 )));
996 }
997 }
998 serde_json::to_value(self.approvals(&query))
999 .map_err(|error| ServiceError::Operation(error.to_string()))
1000 }
1001 "harness.v1.sessions.discover" => {
1002 let query = decode::<DiscoveryQuery>(params)?;
1003 let page = discover_session_page(&query).map_err(operation)?;
1004 let doors = crate::mail_route::LiveSessions::read(&query.homes);
1009 let activities = crate::session_activity::resolve_stock_session_activities(
1010 &page
1011 .sessions
1012 .iter()
1013 .map(|session| session.locator.clone())
1014 .collect::<Vec<_>>(),
1015 &query.homes,
1016 )
1017 .into_iter()
1018 .map(|activity| (activity.key(), activity))
1019 .collect::<BTreeMap<_, _>>();
1020 let sessions = page
1021 .sessions
1022 .into_iter()
1023 .map(|session| {
1024 let mut value = live_descriptor_value(&session, &doors)?;
1025 let activity_key = (
1026 session.locator.harness.as_str().to_string(),
1027 session.locator.session_id.clone(),
1028 );
1029 if let Some(activity) = activities.get(&activity_key) {
1030 value["activity"] = serde_json::to_value(activity)
1031 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1032 if let Some(status) = legacy_live_status(activity) {
1033 value["live_status"] = json!(status);
1034 }
1035 }
1036 Ok(value)
1037 })
1038 .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1039 let mut result = json!({"sessions": sessions, "next_cursor": page.next_cursor});
1040 if query.search_previews {
1043 result["receipt"] = serde_json::to_value(page.receipt)
1044 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1045 }
1046 Ok(result)
1047 }
1048 "harness.v1.sessions.inbox" => inbox_call(decode::<InboxParams>(params)?),
1049 "harness.v1.sessions.load" => {
1050 let params = decode::<LoadSessionParams>(params)?;
1051 if let Some(options) = ¶ms.options {
1052 options.validate()?;
1053 if let Some(result) = indexed_claude_window(¶ms.read.locator, options)? {
1054 return Ok(result);
1055 }
1056 return load_session(¶ms.read.locator)
1057 .map(|session| projected_session_result(&session, options))
1058 .map_err(operation);
1059 }
1060 let mut session = if params.read.display_history() {
1061 self.catalog
1062 .load_display_view(
1063 ¶ms.read.locator,
1064 params.read.read_fidelity(),
1065 params.read.tail_messages().unwrap_or(500),
1066 )
1067 .map_err(crate::Error::from)
1068 } else if params.read.include_subagents() {
1069 load_session_with_fidelity(¶ms.read.locator, params.read.read_fidelity())
1070 } else {
1071 self.catalog
1072 .load_parent_with_fidelity(
1073 ¶ms.read.locator,
1074 params.read.read_fidelity(),
1075 )
1076 .map_err(crate::Error::from)
1077 }
1078 .map_err(operation)?;
1079 params.read.bound_session(&mut session);
1080 Ok(json!({"session": normalized_session_json(&session)}))
1081 }
1082 "harness.v1.sessions.follow" => {
1083 let params = decode::<LocatorParams>(params)?;
1084 let mut follower = self
1085 .catalog
1086 .follow_read_view(
1087 ¶ms.locator,
1088 params.read_fidelity(),
1089 params.include_subagents(),
1090 params.tail_messages(),
1091 params.max_message_chars(),
1092 params.display_history(),
1093 )
1094 .map_err(operation)?;
1095 let initial = follower
1096 .poll()
1097 .map_err(operation)?
1098 .map(|event| event.to_json());
1099 let subscription = format!("sub-{}", self.next_subscription);
1100 self.next_subscription += 1;
1101 self.followers.insert(subscription.clone(), follower);
1102 self.followed_sources.insert(
1103 subscription.clone(),
1104 FollowedSource {
1105 harness: params.locator.harness.as_str().to_string(),
1106 session_id: params.locator.session_id.clone(),
1107 reported: None,
1108 },
1109 );
1110 Ok(json!({"subscription": subscription, "initial": initial}))
1111 }
1112 "harness.v1.sessions.unfollow" => {
1113 let params = decode::<UnfollowParams>(params)?;
1114 self.followed_sources.remove(¶ms.subscription);
1115 Ok(json!({
1116 "removed": self.followers.remove(¶ms.subscription).is_some()
1117 }))
1118 }
1119 "harness.v1.sessions.activity.unsubscribe" => {
1120 let params = decode::<UnfollowParams>(params)?;
1121 Ok(json!({
1122 "removed": self.activity_subscriptions.remove(¶ms.subscription).is_some()
1123 }))
1124 }
1125 "harness.v1.sessions.index.subscribe" => {
1126 let query = decode::<DiscoveryQuery>(params)?;
1127 crate::session_index::validate_query(&query)
1128 .map_err(ServiceError::InvalidParams)?;
1129 let homes = query.homes.clone();
1130 let (index, initial) = crate::session_index::SessionIndexSubscription::open(
1131 query,
1132 Arc::clone(&self.index_notifier),
1133 )
1134 .map_err(ServiceError::Operation)?;
1135 let doors = crate::mail_route::LiveSessions::read(&homes);
1136 let initial = initial
1137 .iter()
1138 .map(|descriptor| live_descriptor_value(descriptor, &doors))
1139 .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1140 let subscription = format!("index-sub-{}", self.next_subscription);
1141 self.next_subscription += 1;
1142 self.index_subscriptions.insert(subscription.clone(), index);
1143 Ok(json!({
1144 "subscription": subscription,
1145 "revision": 1,
1146 "initial": initial,
1147 }))
1148 }
1149 "harness.v1.sessions.index.resize" => {
1150 let params = decode::<IndexResizeParams>(params)?;
1151 crate::session_index::validate_limit(params.limit)
1152 .map_err(ServiceError::InvalidParams)?;
1153 let index = self
1154 .index_subscriptions
1155 .get_mut(¶ms.subscription)
1156 .ok_or_else(|| {
1157 ServiceError::InvalidParams("unknown session index subscription".into())
1158 })?;
1159 let prepared = index
1160 .prepare_resize(params.limit)
1161 .map_err(ServiceError::Operation)?;
1162 let doors = crate::mail_route::LiveSessions::read(index.homes());
1163 let initial = prepared
1164 .page
1165 .sessions
1166 .iter()
1167 .map(|descriptor| live_descriptor_value(descriptor, &doors))
1168 .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1169 let response = json!({
1170 "subscription": params.subscription,
1171 "revision": prepared.revision,
1172 "initial": initial,
1173 "receipt": prepared.page.receipt,
1174 });
1175 index.commit_resize(prepared);
1176 Ok(response)
1177 }
1178 "harness.v1.sessions.index.unsubscribe" => {
1179 let params = decode::<UnfollowParams>(params)?;
1180 Ok(json!({
1181 "removed": self.index_subscriptions.remove(¶ms.subscription).is_some()
1182 }))
1183 }
1184 "harness.v1.sessions.import" => {
1185 let params = decode::<ImportSessionParams>(params)?;
1186 let session = Session::load_str(¶ms.content, params.source_harness.into())
1187 .map_err(operation)?;
1188 Ok(json!({"session": normalized_session_json(&session)}))
1189 }
1190 "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
1191 let params = decode::<ExportSessionParams>(params)?;
1192 let session = load_session(¶ms.locator).map_err(operation)?;
1193 let artifact = session_artifact(¶ms.locator, &session, params.target_harness)?;
1194 if method == "harness.v1.sessions.export"
1195 && params.target_harness == TransferFormat::Hermes
1196 {
1197 let imported = crate::hermes_import::import_into_hermes(&session, None)
1199 .map_err(operation)?;
1200 return Ok(json!({"artifact": artifact, "imported": imported}));
1201 }
1202 Ok(json!({"artifact": artifact}))
1203 }
1204 "harness.v1.sessions.reduce" => {
1205 let params = decode::<ReduceSessionParams>(params)?;
1206 self.reduce_session(params)
1207 }
1208 "harness.v1.sessions.branch" => {
1209 let params = decode::<BranchSessionParams>(params)?;
1210 let session = load_session(¶ms.locator).map_err(operation)?;
1211 let storage = params.locator.storage.path().display().to_string();
1212 let bootstrap_prompt = format!(
1213 "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.",
1214 params.locator.harness.as_str(), params.locator.session_id, storage
1215 );
1216 let artifact = params
1217 .target_harness
1218 .map(|target| session_artifact(¶ms.locator, &session, target))
1219 .transpose()?;
1220 Ok(json!({
1221 "parent": params.locator,
1222 "session": normalized_session_json(&session),
1223 "bootstrap_prompt": bootstrap_prompt,
1224 "artifact": artifact,
1225 }))
1226 }
1227 "harness.v1.sessions.handoff" => {
1228 let params = decode::<HandoffSessionParams>(params)?;
1229 let session = load_session(¶ms.locator).map_err(operation)?;
1230 let cwd = params
1231 .cwd
1232 .or_else(|| session.meta.cwd.clone())
1233 .unwrap_or_else(|| PathBuf::from("."));
1234 let artifact = handoff_artifact(¶ms.locator, &session, params.target_harness)?;
1235 let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
1236 ServiceError::Operation(
1237 "handoff artifact omitted target session identity".into(),
1238 )
1239 })?;
1240 let instructions =
1241 handoff_instructions(params.target_harness, target_session_id, &cwd);
1242 Ok(json!({
1243 "artifact": artifact,
1244 "launch": instructions.launch,
1245 "materialize": instructions.materialize,
1246 "requires_materialization": instructions.requires_materialization,
1247 "note": instructions.note,
1248 }))
1249 }
1250 "harness.v1.sessions.materialize" => {
1251 let params = decode::<MaterializeSessionParams>(params)?;
1252 for file in ¶ms.artifact.files {
1256 if file.role == "source_recovery"
1257 && file.path == "recovery/source.supercode.jsonl"
1258 {
1259 if let Ok(source) = Session::from_native_str(&file.content) {
1260 crate::residue_store::store_segments(&source);
1261 }
1262 }
1263 }
1264 let locator = crate::native_materialize::materialize_native_artifact(
1265 params.artifact,
1266 ¶ms.cwd,
1267 ¶ms.homes,
1268 )
1269 .map_err(ServiceError::Operation)?;
1270 Ok(json!({"locator": locator}))
1271 }
1272 "harness.v1.jobs.list" => {
1276 let query = decode::<crate::jobs::JobsQuery>(params)?;
1277 if let Some(harness) = query.harness.as_deref() {
1278 refuse_harness_without_jobs(harness, "jobs.list")?;
1279 }
1280 let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1281 serde_json::to_value(listing)
1282 .map_err(|error| ServiceError::Operation(error.to_string()))
1283 }
1284 "harness.v1.jobs.get" => {
1285 let params = decode::<JobsGetParams>(params)?;
1286 refuse_harness_without_jobs(¶ms.harness, "jobs.get")?;
1287 match crate::jobs::get_job(¶ms.harness, ¶ms.id, ¶ms.homes)
1288 .map_err(operation)?
1289 {
1290 Some((job, source)) => Ok(json!({"job": job, "source": source})),
1291 None => Err(ServiceError::Operation(format!(
1292 "`{}` has no scheduled job `{}`",
1293 params.harness, params.id
1294 ))),
1295 }
1296 }
1297 "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1303 "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1304 "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1305 "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1306 "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1307 "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1308 "harness.v1.jobs.notepad"
1309 | "harness.v1.jobs.notepad_set"
1310 | "harness.v1.jobs.notepad_delete" => {
1311 let request = decode::<crate::jobs_notepad::JobNotepadRequest>(params)?;
1312 refuse_harness_without_jobs(&request.harness, "jobs.notepad")?;
1313 let answer = match method {
1314 "harness.v1.jobs.notepad_set" => crate::jobs_notepad::set(&request),
1315 "harness.v1.jobs.notepad_delete" => crate::jobs_notepad::delete(&request),
1316 _ => crate::jobs_notepad::read(&request),
1317 }
1318 .map_err(job_control_error)?;
1319 serde_json::to_value(answer)
1320 .map_err(|error| ServiceError::Operation(error.to_string()))
1321 }
1322 "harness.v1.runs.list" => {
1326 let query = decode::<crate::runs::RunsQuery>(params)?;
1327 if let Some(harness) = query.harness.as_deref() {
1328 refuse_harness_without_runs(harness, "runs.list")?;
1329 }
1330 let listing = crate::runs::list_runs(&query).map_err(operation)?;
1331 serde_json::to_value(listing)
1332 .map_err(|error| ServiceError::Operation(error.to_string()))
1333 }
1334 "harness.v1.runs.get" => {
1335 let params = decode::<RunsGetParams>(params)?;
1336 refuse_harness_without_runs(¶ms.harness, "runs.get")?;
1337 match crate::runs::get_run(¶ms.harness, ¶ms.id, ¶ms.homes)
1338 .map_err(operation)?
1339 {
1340 Some((run, source)) => Ok(json!({"run": run, "source": source})),
1341 None => Err(ServiceError::Operation(format!(
1342 "`{}` has no run `{}`",
1343 params.harness, params.id
1344 ))),
1345 }
1346 }
1347 "harness.v1.sessions.resume_instructions" => {
1348 let params = decode::<ResumeInstructionsParams>(params)?;
1349 let session = load_session(¶ms.locator).map_err(operation)?;
1350 let cwd = params
1351 .cwd
1352 .or(session.meta.cwd)
1353 .unwrap_or_else(|| PathBuf::from("."));
1354 let launch = resume_launch(
1355 params.locator.harness.as_str(),
1356 ¶ms.locator.session_id,
1357 &cwd,
1358 params.policy,
1359 )?;
1360 Ok(json!({"launch": launch}))
1361 }
1362 _ => Err(ServiceError::MethodNotFound),
1363 }
1364 }
1365
1366 fn reduce_session(
1367 &self,
1368 params: ReduceSessionParams,
1369 ) -> std::result::Result<Value, ServiceError> {
1370 let session = load_session(¶ms.locator).map_err(operation)?;
1371 if session.messages.is_empty() {
1372 return Err(ServiceError::InvalidParams(
1373 "cannot reduce an empty session".into(),
1374 ));
1375 }
1376 let keep_last = params.keep_last.clamp(1, 128);
1377 let policy = reduce::ReductionPolicy {
1378 clear_turns_older_than: Some(keep_last),
1379 ..Default::default()
1380 };
1381 let (view, log) =
1382 reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1383 if log.reductions.is_empty() {
1384 return Err(ServiceError::UnsupportedAction(format!(
1385 "session `{}` is already too small for a meaningful reversible reduction",
1386 params.locator.session_id
1387 )));
1388 }
1389 let source_tokens = supercode_runtime::estimate_view_tokens(&session.messages);
1390 let reduced_tokens = supercode_runtime::estimate_view_tokens(&view);
1391 if reduced_tokens >= source_tokens {
1392 return Err(ServiceError::UnsupportedAction(format!(
1393 "session `{}` has no token-reducing reversible projection",
1394 params.locator.session_id
1395 )));
1396 }
1397
1398 let store_root = self
1399 .reduction_store_root
1400 .clone()
1401 .unwrap_or_else(default_reduction_store_root);
1402 let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1403 let rescue_id = format!("rescue-{}", generated_session_id());
1404 let imported = session
1405 .imported_message_count
1406 .unwrap_or(session.messages.len())
1407 .min(session.messages.len());
1408 let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1409 let view_jsonl = messages_jsonl(&view)?;
1410 let title = format!(
1411 "Reduced {} continuation from {}",
1412 params.target_harness.id(),
1413 params.locator.session_id
1414 );
1415
1416 store
1421 .save_sidecar(&rescue_id, &sidecar_jsonl)
1422 .map_err(operation)?;
1423 store
1424 .save_reduction_log(&rescue_id, &log)
1425 .map_err(operation)?;
1426 store
1427 .save(&rescue_id, &title, &view_jsonl)
1428 .map_err(operation)?;
1429
1430 let source_bytes = serde_json::to_vec(&session.messages)
1431 .map_err(|error| ServiceError::Operation(error.to_string()))?
1432 .len() as u64;
1433 let reduced_bytes = serde_json::to_vec(&view)
1434 .map_err(|error| ServiceError::Operation(error.to_string()))?
1435 .len() as u64;
1436 store
1437 .set_reduction_stats(
1438 &rescue_id,
1439 &title,
1440 source_bytes,
1441 reduced_bytes,
1442 log.reductions.len() as u32,
1443 )
1444 .map_err(operation)?;
1445
1446 let reloaded_sidecar = store
1450 .load_sidecar(&rescue_id)
1451 .map_err(operation)?
1452 .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1453 let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1454 let reloaded_log = store
1455 .load_reduction_log(&rescue_id)
1456 .map_err(operation)?
1457 .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1458 let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1459 reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1460 let (restamped_view, restamped_log) =
1467 reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1468 if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1469 return Err(ServiceError::Operation(
1470 "persisted reduction view does not match its durable log and sidecar".into(),
1471 ));
1472 }
1473 if restamped_log != reloaded_log {
1474 return Err(ServiceError::Operation(
1475 "reapplying the durable reduction log changed its identity".into(),
1476 ));
1477 }
1478 let inverted =
1479 reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1480 if inverted != session.messages {
1481 return Err(ServiceError::Operation(
1482 "reduction inversion did not restore the source messages byte-exactly".into(),
1483 ));
1484 }
1485
1486 let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1487 let sidecar_path = store.sidecar_path(&rescue_id);
1488 let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1489 let bootstrap_prompt = reduced_bootstrap_prompt(
1490 ¶ms.locator,
1491 params.target_harness,
1492 &view_jsonl,
1493 &sidecar_path,
1494 &reduction_log_path,
1495 );
1496 let mut reduced_session = session.clone();
1497 reduced_session.meta.session_id = Some(rescue_id.clone());
1498 reduced_session.messages = view;
1499
1500 Ok(json!({
1501 "session": normalized_session_json(&reduced_session),
1502 "bootstrap_prompt": bootstrap_prompt,
1503 "receipt": {
1504 "id": rescue_id,
1505 "sidecar_id": rescue_id,
1506 "source_harness": params.locator.harness,
1507 "target_harness": params.target_harness.id(),
1508 "source_tokens": source_tokens,
1509 "reduced_tokens": reduced_tokens,
1510 "ratio": ratio,
1511 "source_bytes": source_bytes,
1512 "reduced_bytes": reduced_bytes,
1513 "reductions": reloaded_log.reductions.len(),
1514 "sidecar_path": sidecar_path,
1515 "reduction_log_path": reduction_log_path,
1516 "verified": true,
1517 "reversible": true,
1518 }
1519 }))
1520 }
1521
1522 pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1540 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1541 return None;
1542 }
1543 let method = request.get("method").and_then(Value::as_str)?;
1544 if !RUNTIME_OPEN_METHODS.contains(&method) {
1545 return None;
1546 }
1547 Some(RuntimeOpen {
1548 id: request.get("id").cloned().unwrap_or(Value::Null),
1549 method: method.to_string(),
1550 params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1551 })
1552 }
1553
1554 pub fn detach(&self, request: &Value) -> Option<DetachedCall> {
1576 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1577 return None;
1578 }
1579 let method = request.get("method").and_then(Value::as_str)?;
1580 if !DETACHED_METHODS.contains(&method) {
1581 return None;
1582 }
1583 let id = request.get("id").cloned().unwrap_or(Value::Null);
1584 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1585 let work = match method {
1586 "harness.v1.harnesses.list" | "harness.v1.harnesses.probe" => self
1587 .inventory_work(method, params)
1588 .map(DetachedWork::Inventory),
1589 "harness.v1.sessions.message" => {
1590 decode::<MessageSessionParams>(params).map(DetachedWork::Message)
1591 }
1592 _ => {
1593 let verb = match method {
1594 "harness.v1.sessions.new" => crate::SessionVerb::New,
1595 "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
1596 "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
1597 _ => crate::SessionVerb::Delete,
1598 };
1599 match decode::<crate::SessionMutation>(params) {
1600 Ok(mutation) => {
1601 match crate::sessions_control::door(&mutation.harness, verb) {
1602 Ok(crate::SessionDoor::Live(_)) => return None,
1605 Ok(_) => Ok(DetachedWork::SessionMutation { verb, mutation }),
1606 Err(error) => Err(session_control_error(error)),
1607 }
1608 }
1609 Err(error) => Err(error),
1610 }
1611 }
1612 };
1613 Some(DetachedCall {
1614 id,
1615 method: method.to_string(),
1616 work: work.map(Work::Free),
1617 })
1618 }
1619
1620 pub fn detach_runtime(&mut self, request: &Value) -> Option<DetachedCall> {
1634 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1635 return None;
1636 }
1637 let method = request.get("method").and_then(Value::as_str)?;
1638 let id = request.get("id").cloned().unwrap_or(Value::Null);
1639 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1640 let work = match method {
1641 "harness.v1.runtimes.close" => decode::<RuntimeConnectionParams>(params)
1642 .and_then(|params| self.surrender_runtime(¶ms.connection))
1643 .map(|(runtime, process_group)| {
1644 Work::Runtime(RuntimeWork::Close {
1645 runtime,
1646 process_group,
1647 })
1648 }),
1649 "harness.v1.sessions.new" | "harness.v1.sessions.reset" => {
1650 let verb = if method == "harness.v1.sessions.new" {
1651 crate::SessionVerb::New
1652 } else {
1653 crate::SessionVerb::Reset
1654 };
1655 let mutation = decode::<crate::SessionMutation>(params).ok()?;
1656 let Ok(crate::SessionDoor::Live(command)) =
1660 crate::sessions_control::door(&mutation.harness, verb)
1661 else {
1662 return None;
1663 };
1664 let connection = mutation
1665 .connection
1666 .clone()
1667 .filter(|value| !value.trim().is_empty())?;
1668 self.lend_runtime(&connection).map(|runtime| {
1669 let session = live_session_name(runtime.as_ref(), &mutation);
1670 Work::Runtime(RuntimeWork::LiveCommand {
1671 connection,
1672 runtime,
1673 verb,
1674 mutation,
1675 command,
1676 session,
1677 })
1678 })
1679 }
1680 _ => return None,
1681 };
1682 Some(DetachedCall {
1683 id,
1684 method: method.to_string(),
1685 work,
1686 })
1687 }
1688
1689 pub fn finish_detached(&mut self, answer: DetachedAnswer) -> Value {
1694 let DetachedAnswer { response, returned } = answer;
1695 if let Some(ReturnedRuntime {
1696 connection,
1697 runtime,
1698 }) = returned
1699 {
1700 self.runtimes_in_flight.remove(&connection);
1701 self.runtimes.insert(connection, runtime);
1702 }
1703 response
1704 }
1705
1706 pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1710 let OpenedRuntime { id, outcome } = opened;
1711 let result = match outcome {
1712 Ok(open) => self.register_open_runtime(open).await,
1713 Err(error) => Err(error),
1714 };
1715 service_response(id, result)
1716 }
1717
1718 async fn register_open_runtime(
1720 &mut self,
1721 open: OpenRuntime,
1722 ) -> std::result::Result<Value, ServiceError> {
1723 match open {
1724 OpenRuntime::Hosted {
1725 runtime,
1726 capabilities,
1727 workspace,
1728 } => {
1729 self.insert_hosted_runtime(runtime, capabilities, workspace)
1730 .await
1731 }
1732 OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1733 }
1734 }
1735
1736 async fn runtime_call(
1737 &mut self,
1738 method: &str,
1739 params: Value,
1740 ) -> std::result::Result<Value, ServiceError> {
1741 match method {
1742 "harness.v1.runtimes.capabilities" => {
1743 let params = decode::<RuntimeBackendParams>(params)?;
1744 let backend = runtime_backend(¶ms)?;
1745 Ok(json!({
1746 "harness": backend.harness(),
1747 "capabilities": backend.capabilities(),
1748 }))
1749 }
1750 method if RUNTIME_OPEN_METHODS.contains(&method) => {
1751 self.register_open_runtime(open_runtime(method, params).await?)
1752 .await
1753 }
1754 "harness.v1.runtimes.send_input" => {
1755 let params = decode::<RuntimeInputParams>(params)?;
1756 let image_urls = validate_runtime_image_urls(params.image_urls)?;
1757 let runtime = self.runtime_mut(¶ms.connection)?;
1758 let turn_id = within_control_deadline(
1759 method,
1760 runtime.send_input(RuntimeInput {
1761 text: params.text,
1762 image_urls,
1763 }),
1764 )
1765 .await?
1766 .map_err(operation)?;
1767 Ok(json!({"turn_id": turn_id}))
1768 }
1769 "harness.v1.runtimes.interrupt" => {
1770 let params = decode::<RuntimeConnectionParams>(params)?;
1771 within_control_deadline(method, self.runtime_mut(¶ms.connection)?.interrupt())
1772 .await?
1773 .map_err(operation)?;
1774 Ok(json!({}))
1775 }
1776 "harness.v1.runtimes.steer" => {
1777 let params = decode::<RuntimeInputParams>(params)?;
1778 if !params.image_urls.is_empty() {
1779 return Err(ServiceError::InvalidParams(
1780 "runtime steering accepts text only".into(),
1781 ));
1782 }
1783 let text = params.text.trim();
1784 if text.is_empty() || text.chars().count() > 50_000 {
1785 return Err(ServiceError::InvalidParams(
1786 "runtime steering requires 1 to 50,000 text characters".into(),
1787 ));
1788 }
1789 within_control_deadline(
1790 method,
1791 self.runtime_mut(¶ms.connection)?
1792 .steer(text.to_string()),
1793 )
1794 .await?
1795 .map_err(operation)?;
1796 Ok(json!({}))
1797 }
1798 "harness.v1.runtimes.respond" => {
1799 let params = decode::<RuntimeRespondParams>(params)?;
1800 let request_id = params.request_id.clone();
1801 within_control_deadline(
1802 method,
1803 self.runtime_mut(¶ms.connection)?
1804 .respond(params.request_id, params.response),
1805 )
1806 .await?
1807 .map_err(operation)?;
1808 self.approvals.answered(¶ms.connection, &request_id);
1810 Ok(json!({}))
1811 }
1812 "harness.v1.runtimes.acquire_control" => {
1813 let params = decode::<RuntimeConnectionParams>(params)?;
1814 let snapshot = within_control_deadline(
1815 method,
1816 self.runtime_mut(¶ms.connection)?.acquire_control(),
1817 )
1818 .await?
1819 .map_err(operation)?;
1820 serde_json::to_value(snapshot)
1821 .map_err(|error| ServiceError::Operation(error.to_string()))
1822 }
1823 "harness.v1.runtimes.heartbeat" => {
1824 let params = decode::<RuntimeConnectionParams>(params)?;
1825 let snapshot = within_control_deadline(
1826 method,
1827 self.runtime_mut(¶ms.connection)?.heartbeat(),
1828 )
1829 .await?
1830 .map_err(operation)?;
1831 serde_json::to_value(snapshot)
1832 .map_err(|error| ServiceError::Operation(error.to_string()))
1833 }
1834 "harness.v1.runtimes.detach" => {
1835 let params = decode::<RuntimeConnectionParams>(params)?;
1836 let snapshot =
1837 within_control_deadline(method, self.runtime_mut(¶ms.connection)?.detach())
1838 .await?
1839 .map_err(operation)?;
1840 serde_json::to_value(snapshot)
1841 .map_err(|error| ServiceError::Operation(error.to_string()))
1842 }
1843 "harness.v1.runtimes.terminal_instructions" => {
1844 let params = decode::<RuntimeConnectionParams>(params)?;
1845 let launch = self
1846 .terminal_launches
1847 .get(¶ms.connection)
1848 .ok_or_else(|| {
1849 ServiceError::Operation(
1850 "this runtime is not hosted for terminal attachment".into(),
1851 )
1852 })?;
1853 Ok(json!({"launch":launch}))
1854 }
1855 "harness.v1.runtimes.close" => {
1856 let params = decode::<RuntimeConnectionParams>(params)?;
1857 let (runtime, process_group) = self.surrender_runtime(¶ms.connection)?;
1858 close_runtime(runtime, process_group).await
1859 }
1860 _ => Err(ServiceError::MethodNotFound),
1861 }
1862 }
1863
1864 #[cfg(feature = "adapter-api")]
1866 async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1867 let params = decode::<MessageSessionParams>(params)?;
1868 Ok(message_live_session(¶ms).await)
1869 }
1870
1871 #[cfg(feature = "adapter-api")]
1872 fn harness_settings_call(
1873 &self,
1874 method: &str,
1875 params: Value,
1876 ) -> std::result::Result<Value, ServiceError> {
1877 let homes = crate::HarnessHomes::default();
1878 match method {
1879 "harness.v1.harnesses.settings" => {
1880 let params = decode::<HarnessSettingsParams>(params)?;
1881 let report = crate::inspect_harness_interop_settings(&homes, ¶ms.harness)
1882 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1883 serde_json::to_value(report)
1884 .map_err(|error| ServiceError::Operation(error.to_string()))
1885 }
1886 "harness.v1.harnesses.configure" => {
1887 let params = decode::<ConfigureHarnessParams>(params)?;
1888 let report = crate::configure_harness_interop_settings(
1889 &homes,
1890 ¶ms.harness,
1891 ¶ms.changes,
1892 params.expected_revision.as_deref(),
1893 )
1894 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1895 serde_json::to_value(report)
1896 .map_err(|error| ServiceError::Operation(error.to_string()))
1897 }
1898 _ => Err(ServiceError::MethodNotFound),
1899 }
1900 }
1901
1902 fn insert_runtime(
1903 &mut self,
1904 runtime: Box<dyn RuntimeConnection>,
1905 ) -> std::result::Result<Value, ServiceError> {
1906 let connection = format!("runtime-{}", self.next_runtime);
1907 self.next_runtime += 1;
1908 let handle = runtime.handle().clone();
1909 self.runtime_sequences
1910 .entry(handle.runtime_id.clone())
1911 .or_insert(0);
1912 self.runtimes.insert(connection.clone(), runtime);
1913 Ok(json!({"connection": connection, "handle": handle}))
1914 }
1915
1916 #[cfg(feature = "adapter-api")]
1917 async fn insert_hosted_runtime(
1918 &mut self,
1919 runtime: Box<dyn RuntimeConnection>,
1920 capabilities: crate::RuntimeCapabilities,
1921 workspace: PathBuf,
1922 ) -> std::result::Result<Value, ServiceError> {
1923 let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1924 let token: std::sync::Arc<str> = crate::server::generate_token().into();
1925 let server = crate::server::run_frontend_http(
1926 host.clone(),
1927 host.frontend_sender(),
1928 "127.0.0.1:0",
1929 token.clone(),
1930 connection.handle().runtime_id.clone(),
1931 )
1932 .await
1933 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1934 let source = LiveRuntimeSource {
1935 harness: connection.handle().harness.as_str().to_string(),
1936 session_id: connection.handle().runtime_id.clone(),
1937 workspace: workspace.clone(),
1938 };
1939 let registration = register_live_runtime(
1940 connection.handle().runtime_id.clone(),
1941 source.clone(),
1942 format!("http://{}", server.address()),
1943 token.to_string(),
1944 )
1945 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1946 let endpoint = registration.endpoint().to_string();
1947 let launch = StructuredLaunch {
1948 cwd: workspace,
1949 program: std::env::current_exe()
1953 .ok()
1954 .map(|path| path.to_string_lossy().into_owned())
1955 .unwrap_or_else(|| "supercode".into()),
1956 arguments: vec![
1957 "open".into(),
1958 endpoint,
1959 "--harness".into(),
1960 source.harness,
1961 "--session".into(),
1962 source.session_id,
1963 ],
1964 env: BTreeMap::new(),
1965 };
1966 let lease = HostedRuntimeLease {
1967 connection,
1968 _host: host,
1969 _registration: registration,
1970 _server: server,
1971 };
1972 let opened = self.insert_runtime(Box::new(lease))?;
1973 let connection_id = opened["connection"]
1974 .as_str()
1975 .expect("insert_runtime returns a connection id")
1976 .to_string();
1977 self.terminal_launches.insert(connection_id, launch);
1978 Ok(opened)
1979 }
1980
1981 #[cfg(not(feature = "adapter-api"))]
1982 async fn insert_hosted_runtime(
1983 &mut self,
1984 runtime: Box<dyn RuntimeConnection>,
1985 _capabilities: crate::RuntimeCapabilities,
1986 _workspace: PathBuf,
1987 ) -> std::result::Result<Value, ServiceError> {
1988 self.insert_runtime(runtime)
1989 }
1990
1991 fn runtime_mut(
1992 &mut self,
1993 connection: &str,
1994 ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1995 if self.runtimes_in_flight.contains(connection) {
1996 return Err(self.lent_out(connection));
1997 }
1998 self.runtimes.get_mut(connection).ok_or_else(|| {
1999 ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2000 })
2001 }
2002
2003 fn lent_out(&self, connection: &str) -> ServiceError {
2007 ServiceError::Operation(format!(
2008 "runtime connection `{connection}`: a harness turn is already in progress"
2009 ))
2010 }
2011
2012 fn lend_runtime(
2015 &mut self,
2016 connection: &str,
2017 ) -> std::result::Result<Box<dyn RuntimeConnection>, ServiceError> {
2018 if self.runtimes_in_flight.contains(connection) {
2019 return Err(self.lent_out(connection));
2020 }
2021 let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2022 ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2023 })?;
2024 self.runtimes_in_flight.insert(connection.to_string());
2025 Ok(runtime)
2026 }
2027
2028 fn surrender_runtime(
2040 &mut self,
2041 connection: &str,
2042 ) -> std::result::Result<(Box<dyn RuntimeConnection>, Option<u32>), ServiceError> {
2043 if self.runtimes_in_flight.contains(connection) {
2044 return Err(self.lent_out(connection));
2045 }
2046 let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2047 ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2048 })?;
2049 let process_group = runtime_process_group(runtime.handle());
2050 let runtime_id = runtime.handle().runtime_id.clone();
2051 self.terminal_launches.remove(connection);
2052 self.runtime_sequences.remove(&runtime_id);
2053 self.approvals.forget(connection);
2054 Ok((runtime, process_group))
2055 }
2056
2057 pub fn kill_all_runtime_groups(&self) -> usize {
2068 self.runtimes
2069 .values()
2070 .filter(|runtime| kill_runtime_process_group(runtime_process_group(runtime.handle())))
2071 .count()
2072 }
2073
2074 async fn mutate_session(
2085 &mut self,
2086 verb: crate::SessionVerb,
2087 params: Value,
2088 ) -> std::result::Result<Value, ServiceError> {
2089 let mutation = decode::<crate::SessionMutation>(params)?;
2090 let door = crate::sessions_control::door(&mutation.harness, verb)
2091 .map_err(session_control_error)?;
2092 let outcome = match door {
2093 #[cfg(not(feature = "adapter-api"))]
2097 crate::SessionDoor::Live(command) => {
2098 return Err(ServiceError::Operation(format!(
2099 "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
2100 session, which needs this build's `adapter-api` feature",
2101 mutation.harness,
2102 verb.as_str()
2103 )));
2104 }
2105 #[cfg(feature = "adapter-api")]
2106 crate::SessionDoor::Live(command) => {
2107 let connection = mutation
2108 .connection
2109 .clone()
2110 .filter(|value| !value.trim().is_empty())
2111 .ok_or_else(|| {
2112 ServiceError::InvalidParams(format!(
2113 "`{}` performs `sessions.{}` by typing `{command}` into a live \
2114 driven session: pass the `connection` of an open runtime \
2115 (`harness.v1.runtimes.start`)",
2116 mutation.harness,
2117 verb.as_str()
2118 ))
2119 })?;
2120 let runtime = self.runtime_mut(&connection)?;
2121 let session = live_session_name(runtime.as_ref(), &mutation);
2122 return type_live_command(runtime.as_mut(), verb, &mutation, command, session)
2128 .await;
2129 }
2130 _ => run_session_mutation(verb, &mutation).await?,
2131 };
2132 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
2133 }
2134
2135 async fn inventory_call(
2139 &self,
2140 method: &str,
2141 params: Value,
2142 ) -> std::result::Result<Value, ServiceError> {
2143 run_inventory(self.inventory_work(method, params)?).await
2144 }
2145
2146 fn inventory_work(
2152 &self,
2153 method: &str,
2154 params: Value,
2155 ) -> std::result::Result<InventoryWork, ServiceError> {
2156 let mut params = decode::<HarnessInventoryParams>(params)?;
2157 if method == "harness.v1.harnesses.probe" {
2158 let harness = params.harness.take().ok_or_else(|| {
2159 ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
2160 })?;
2161 params.harnesses = vec![harness];
2162 }
2163 let selected = params
2164 .harnesses
2165 .iter()
2166 .map(HarnessId::as_str)
2167 .collect::<std::collections::BTreeSet<_>>();
2168 let supported = harness_support_registry()
2169 .harnesses
2170 .into_iter()
2171 .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
2172 .collect::<Vec<_>>();
2173 if !params.harnesses.is_empty() && supported.len() != selected.len() {
2174 let known = supported
2175 .iter()
2176 .map(|harness| harness.id.as_str())
2177 .collect::<std::collections::BTreeSet<_>>();
2178 let missing = params
2179 .harnesses
2180 .iter()
2181 .filter(|id| !known.contains(id.as_str()))
2182 .map(HarnessId::as_str)
2183 .collect::<Vec<_>>();
2184 return Err(ServiceError::InvalidParams(format!(
2185 "unknown harness(es): {}",
2186 missing.join(", ")
2187 )));
2188 }
2189 let global_counts = params
2190 .include_sessions
2191 .then(|| self.session_counts(None, ¶ms.harnesses));
2192 let workspace_counts = params
2193 .include_sessions
2194 .then(|| {
2195 params
2196 .workspace
2197 .as_deref()
2198 .map(|workspace| self.session_counts(Some(workspace), ¶ms.harnesses))
2199 })
2200 .flatten();
2201 Ok(InventoryWork {
2202 params,
2203 supported,
2204 global_counts,
2205 workspace_counts,
2206 })
2207 }
2208
2209 #[cfg(feature = "adapter-api")]
2210 async fn harness_authentication_call(
2211 &self,
2212 method: &str,
2213 params: Value,
2214 ) -> std::result::Result<Value, ServiceError> {
2215 match method {
2216 "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
2217 let params = decode::<HarnessAuthenticationParams>(params)?;
2218 serde_json::to_value(crate::inspect_harness_authentication(¶ms.harness).await)
2219 .map_err(|error| ServiceError::Operation(error.to_string()))
2220 }
2221 "harness.v1.harnesses.auth.begin" => {
2222 let params = decode::<BeginHarnessAuthenticationParams>(params)?;
2223 let cwd = params
2224 .cwd
2225 .or_else(|| std::env::current_dir().ok())
2226 .unwrap_or_else(|| PathBuf::from("."));
2227 let plan = crate::harness_authentication_plan(
2228 ¶ms.harness,
2229 params.environment,
2230 params.method,
2231 &cwd,
2232 )
2233 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
2234 serde_json::to_value(plan)
2235 .map_err(|error| ServiceError::Operation(error.to_string()))
2236 }
2237 _ => Err(ServiceError::MethodNotFound),
2238 }
2239 }
2240
2241 fn session_counts(
2242 &self,
2243 workspace: Option<&Path>,
2244 harnesses: &[HarnessId],
2245 ) -> BTreeMap<String, usize> {
2246 let mut counts = BTreeMap::new();
2247 for session in self
2248 .catalog
2249 .discover(&DiscoveryQuery {
2250 workspace: workspace.map(Path::to_path_buf),
2251 harnesses: harnesses.to_vec(),
2252 ..DiscoveryQuery::default()
2253 })
2254 .unwrap_or_default()
2255 {
2256 *counts
2257 .entry(session.locator.harness.as_str().to_string())
2258 .or_insert(0) += 1;
2259 }
2260 counts
2261 }
2262}
2263
2264#[async_trait::async_trait]
2265impl SdkService for HarnessSessionService {
2266 fn capabilities(&self) -> SdkCapabilities {
2267 SdkCapabilities::default()
2268 }
2269
2270 async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2271 if request.operation == SdkOperation::Events {
2272 let events = self
2273 .poll_sdk_events()
2274 .await
2275 .into_iter()
2276 .map(|(_, event)| event)
2277 .collect::<Vec<_>>();
2278 return serde_json::to_value(events).map_err(|error| {
2279 SdkError::new(
2280 SdkErrorCode::Execution,
2281 request.operation,
2282 error.to_string(),
2283 )
2284 });
2285 }
2286 if self.runtimes.is_empty()
2287 && matches!(
2288 request.operation,
2289 SdkOperation::Input
2290 | SdkOperation::Interrupt
2291 | SdkOperation::Steer
2292 | SdkOperation::Respond
2293 | SdkOperation::Close
2294 )
2295 {
2296 return Err(SdkError::unsupported(request.operation));
2297 }
2298 let method = request
2299 .operation
2300 .method()
2301 .ok_or_else(|| SdkError::unsupported(request.operation))?;
2302 let result = match request.operation {
2303 SdkOperation::Discover
2304 | SdkOperation::Load
2305 | SdkOperation::Export
2306 | SdkOperation::ProfilesList
2307 | SdkOperation::ProfilesGet
2308 | SdkOperation::ProfilesCreate
2309 | SdkOperation::ProfilesDelete
2310 | SdkOperation::SkillsList
2311 | SdkOperation::SkillsInstall
2312 | SdkOperation::SkillsRemove
2313 | SdkOperation::ChannelsList
2314 | SdkOperation::RoutesList
2315 | SdkOperation::TriggersList
2316 | SdkOperation::ChannelsStatus
2317 | SdkOperation::MemoryShow
2318 | SdkOperation::MemorySearch
2319 | SdkOperation::JobsList
2320 | SdkOperation::JobsGet
2321 | SdkOperation::JobsCreate
2322 | SdkOperation::JobsUpdate
2323 | SdkOperation::JobsPause
2324 | SdkOperation::JobsResume
2325 | SdkOperation::JobsRun
2326 | SdkOperation::JobsDelete
2327 | SdkOperation::JobsNotepad
2328 | SdkOperation::JobsNotepadSet
2329 | SdkOperation::JobsNotepadDelete
2330 | SdkOperation::RunsList
2331 | SdkOperation::RunsGet
2332 | SdkOperation::ApprovalsList
2333 | SdkOperation::OrchestrationLoad
2334 | SdkOperation::OrchestrationSave
2335 | SdkOperation::OrchestrationCompile
2336 | SdkOperation::OrchestrationDecompile
2337 | SdkOperation::OrchestrationImport
2338 | SdkOperation::OrchestrationExport
2339 | SdkOperation::WorkflowLoad => self.call(method, request.params),
2340 SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2343 SdkOperation::Start
2344 | SdkOperation::Resume
2345 | SdkOperation::Input
2346 | SdkOperation::Interrupt
2347 | SdkOperation::Steer
2348 | SdkOperation::Respond
2349 | SdkOperation::Close => self.runtime_call(method, request.params).await,
2350 SdkOperation::SessionsNew => {
2355 self.mutate_session(crate::SessionVerb::New, request.params)
2356 .await
2357 }
2358 SdkOperation::SessionsReset => {
2359 self.mutate_session(crate::SessionVerb::Reset, request.params)
2360 .await
2361 }
2362 SdkOperation::SessionsArchive => {
2363 self.mutate_session(crate::SessionVerb::Archive, request.params)
2364 .await
2365 }
2366 SdkOperation::SessionsDelete => {
2367 self.mutate_session(crate::SessionVerb::Delete, request.params)
2368 .await
2369 }
2370 SdkOperation::Events => unreachable!("handled before method dispatch"),
2371 };
2372 result.map_err(|error| sdk_error(request.operation, error))
2373 }
2374
2375 async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2376 Ok(self
2377 .poll_sdk_events()
2378 .await
2379 .into_iter()
2380 .map(|(_, event)| event)
2381 .collect())
2382 }
2383}
2384
2385#[cfg(feature = "adapter-api")]
2386struct HostedRuntimeLease {
2387 connection: HostedHarnessConnection,
2388 _host: std::sync::Arc<HostedHarnessRuntime>,
2389 _registration: LiveRuntimeRegistration,
2390 _server: crate::server::FrontendHttpServer,
2391}
2392
2393#[async_trait::async_trait]
2394#[cfg(feature = "adapter-api")]
2395impl RuntimeConnection for HostedRuntimeLease {
2396 fn handle(&self) -> &crate::RuntimeHandle {
2397 self.connection.handle()
2398 }
2399
2400 async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2401 self.connection.send_input(input).await
2402 }
2403
2404 async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2405 self.connection.next_event().await
2406 }
2407
2408 async fn interrupt(&mut self) -> crate::Result<()> {
2409 self.connection.interrupt().await
2410 }
2411
2412 async fn steer(&mut self, text: String) -> crate::Result<()> {
2415 self.connection.steer(text).await
2416 }
2417
2418 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2419 self.connection.respond(request_id, response).await
2420 }
2421
2422 async fn close(&mut self) -> crate::Result<()> {
2423 self.connection.close().await
2424 }
2425}
2426
2427struct InventoryWork {
2430 params: HarnessInventoryParams,
2431 supported: Vec<crate::HarnessSupportDescriptor>,
2432 global_counts: Option<BTreeMap<String, usize>>,
2433 workspace_counts: Option<BTreeMap<String, usize>>,
2434}
2435
2436async fn run_session_mutation(
2443 verb: crate::SessionVerb,
2444 mutation: &crate::SessionMutation,
2445) -> std::result::Result<crate::SessionMutationOutcome, ServiceError> {
2446 let door =
2453 crate::sessions_control::door(&mutation.harness, verb).map_err(session_control_error)?;
2454 if let crate::SessionDoor::Http = door {
2455 return crate::sessions_control::mutate(verb, mutation)
2456 .await
2457 .map_err(session_control_error);
2458 }
2459 let mutation = mutation.clone();
2460 tokio::task::spawn_blocking(move || crate::sessions_control::mutate_blocking(verb, &mutation))
2461 .await
2462 .map_err(|error| {
2463 ServiceError::Operation(format!("the conversation verb could not be run: {error}"))
2464 })?
2465 .map_err(session_control_error)
2466}
2467
2468async fn run_inventory(work: InventoryWork) -> std::result::Result<Value, ServiceError> {
2471 let InventoryWork {
2472 params,
2473 supported,
2474 global_counts,
2475 workspace_counts,
2476 } = work;
2477 let probes = supported.into_iter().map(|descriptor| {
2478 let global = global_counts
2479 .as_ref()
2480 .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2481 let workspace = workspace_counts
2482 .as_ref()
2483 .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2484 probe_harness(descriptor, ¶ms, global, workspace)
2485 });
2486 let harnesses = futures::future::join_all(probes).await;
2487 serde_json::to_value(HarnessInventoryReport {
2488 probe: params.probe,
2489 workspace: params.workspace,
2490 harnesses,
2491 })
2492 .map_err(|error| ServiceError::Operation(error.to_string()))
2493}
2494
2495async fn probe_harness(
2496 descriptor: crate::HarnessSupportDescriptor,
2497 params: &HarnessInventoryParams,
2498 global: Option<usize>,
2499 workspace: Option<usize>,
2500) -> LocalHarness {
2501 let launch = descriptor.runtime.default_launch.as_ref();
2502 let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
2507 .then(crate::orchestrator::daemon_entry)
2508 .and_then(Result::ok);
2509 let executable = match &orchestrator_entry {
2510 Some(entry) => Some(entry.clone()),
2511 None => launch.and_then(|launch| find_executable(&launch.program)),
2512 };
2513 let installed = executable.is_some();
2514 let version = if params.skip_versions || orchestrator_entry.is_some() {
2515 None
2518 } else {
2519 match executable.as_deref() {
2520 Some(path) => executable_version(path).await,
2521 None => None,
2522 }
2523 };
2524 let configured = auth_evidence(descriptor.id.as_str());
2525 let mut auth = if configured {
2526 HarnessAuthState::Configured
2527 } else if matches!(
2528 descriptor.id.as_str(),
2529 HarnessId::CLAUDE_CODE | HarnessId::CODEX
2530 ) {
2531 HarnessAuthState::Required
2536 } else {
2537 HarnessAuthState::Unknown
2538 };
2539 let mut runtime = if installed {
2540 HarnessRuntimeState::Degraded
2541 } else {
2542 HarnessRuntimeState::Unavailable
2543 };
2544 let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2545 let mut reason = (!installed).then(|| {
2546 if is_orchestrator {
2547 format!(
2548 "{} is supported but its daemon entry `{}` was not found",
2549 descriptor.display_name,
2550 crate::orchestrator::DAEMON_ENTRY
2551 )
2552 } else {
2553 format!(
2554 "{} is supported but `{}` was not found on PATH",
2555 descriptor.display_name,
2556 launch
2557 .map(|launch| launch.program.as_str())
2558 .unwrap_or("executable")
2559 )
2560 }
2561 });
2562 let mut repair = (!installed).then(|| {
2563 if is_orchestrator {
2564 format!(
2565 "Install the `supercode-orchestrator` package so `{}` resolves.",
2566 crate::orchestrator::DAEMON_ENTRY
2567 )
2568 } else {
2569 format!(
2570 "Install {} and ensure `{}` is on PATH.",
2571 descriptor.display_name,
2572 launch
2573 .map(|launch| launch.program.as_str())
2574 .unwrap_or("its executable")
2575 )
2576 }
2577 });
2578
2579 if installed && params.probe == HarnessProbeLevel::Handshake {
2580 let backend_params = RuntimeBackendParams {
2581 harness: descriptor.id.clone(),
2582 protocol: None,
2583 launch: None,
2584 base_url: None,
2585 policy: RuntimePolicy::Default,
2586 };
2587 match runtime_backend(&backend_params) {
2588 Ok(backend) => {
2589 let cwd = params
2590 .workspace
2591 .clone()
2592 .or_else(|| std::env::current_dir().ok())
2593 .unwrap_or_else(|| PathBuf::from("."));
2594 let isolated = descriptor
2595 .runtime
2596 .default_launch
2597 .clone()
2598 .and_then(|launch| IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok());
2599 let Some(isolated) = isolated else {
2600 reason = Some(
2601 "No-prompt runtime handshake could not create its isolated harness home."
2602 .into(),
2603 );
2604 repair = Some(
2605 "Check temporary-directory permissions, then run the handshake probe again."
2606 .into(),
2607 );
2608 let running = probe_running_instance(descriptor.id.as_str());
2609 return LocalHarness {
2610 gateway: gateway_health(
2611 descriptor.id.as_str(),
2612 installed,
2613 running.as_ref(),
2614 version.as_deref(),
2615 ),
2616 id: descriptor.id,
2617 display_name: descriptor.display_name,
2618 supported: true,
2619 installed,
2620 executable: executable.map(|path| path.to_string_lossy().into_owned()),
2621 version,
2622 auth,
2623 runtime,
2624 protocol: descriptor.runtime.protocol,
2625 capabilities: descriptor.runtime.capabilities.clone(),
2626 effective_capabilities: descriptor.runtime.capabilities,
2627 sessions: HarnessSessionCounts { global, workspace },
2628 running,
2629 reason,
2630 repair,
2631 };
2632 };
2633 match tokio::time::timeout(
2634 Duration::from_secs(30),
2635 backend.start(RuntimeStartRequest {
2636 cwd,
2637 launch: Some(isolated.launch.clone()),
2638 mcp_servers: Vec::new(),
2639 approval_policy: None,
2640 }),
2641 )
2642 .await
2643 {
2644 Ok(Ok(mut connection)) => {
2645 match stabilize_handshake(connection.as_mut()).await {
2646 Ok(()) => {
2647 auth = HarnessAuthState::Ready;
2648 runtime = HarnessRuntimeState::Ready;
2649 reason = Some(
2650 "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2651 .into(),
2652 );
2653 repair = None;
2654 }
2655 Err(message) => {
2656 auth = if looks_like_auth_error(&message) {
2657 HarnessAuthState::Required
2658 } else if configured {
2659 HarnessAuthState::Configured
2660 } else {
2661 HarnessAuthState::Unknown
2662 };
2663 reason = Some(format!(
2664 "No-prompt runtime handshake became unhealthy during startup: {message}"
2665 ));
2666 repair = Some(if auth == HarnessAuthState::Required {
2667 format!(
2668 "Run `{}` interactively once and complete sign-in, then probe again.",
2669 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2670 )
2671 } else {
2672 "Run the harness directly to inspect its startup failure, then probe again."
2673 .into()
2674 });
2675 }
2676 }
2677 let _ =
2678 tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2679 }
2680 Ok(Err(error)) => {
2681 let message = truncate_text(&error.to_string(), 500);
2682 auth = if looks_like_auth_error(&message) {
2683 HarnessAuthState::Required
2684 } else if configured {
2685 HarnessAuthState::Configured
2686 } else {
2687 HarnessAuthState::Unknown
2688 };
2689 reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2690 repair = Some(if auth == HarnessAuthState::Required {
2691 format!(
2692 "Run `{}` interactively once and complete sign-in, then probe again.",
2693 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2694 )
2695 } else {
2696 "Check the harness installation and run the handshake probe again."
2697 .into()
2698 });
2699 }
2700 Err(_) => {
2701 reason =
2702 Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2703 repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2704 }
2705 }
2706 let _ = isolated.cleanup();
2715 tokio::time::sleep(Duration::from_millis(250)).await;
2716 if let Err(error) = isolated.cleanup() {
2717 auth = if configured {
2718 HarnessAuthState::Configured
2719 } else {
2720 HarnessAuthState::Unknown
2721 };
2722 runtime = HarnessRuntimeState::Degraded;
2723 reason = Some(format!(
2724 "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2725 ));
2726 repair = Some(
2727 "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2728 .into(),
2729 );
2730 }
2731 }
2732 Err(error) => {
2733 reason = Some(error_message(error));
2734 }
2735 }
2736 } else if installed && configured {
2737 reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2738 } else if installed && auth == HarnessAuthState::Required {
2739 reason = Some("Executable found, but no native authentication evidence is present.".into());
2740 repair = Some(format!(
2741 "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2742 descriptor.id.as_str()
2743 ));
2744 } else if installed {
2745 reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2746 repair = Some(format!(
2747 "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2748 launch
2749 .map(|launch| launch.program.as_str())
2750 .unwrap_or("the harness")
2751 ));
2752 }
2753
2754 let effective_capabilities = if installed {
2755 descriptor.runtime.capabilities.clone()
2756 } else {
2757 unavailable_capabilities()
2758 };
2759 let running = probe_running_instance(descriptor.id.as_str());
2760 LocalHarness {
2761 gateway: gateway_health(
2762 descriptor.id.as_str(),
2763 installed,
2764 running.as_ref(),
2765 version.as_deref(),
2766 ),
2767 id: descriptor.id,
2768 display_name: descriptor.display_name,
2769 supported: true,
2770 installed,
2771 executable: executable.map(|path| path.to_string_lossy().into_owned()),
2772 version,
2773 auth,
2774 runtime,
2775 protocol: descriptor.runtime.protocol,
2776 capabilities: descriptor.runtime.capabilities,
2777 effective_capabilities,
2778 sessions: HarnessSessionCounts { global, workspace },
2779 running,
2780 reason,
2781 repair,
2782 }
2783}
2784
2785async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2786 let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2787 loop {
2788 let now = tokio::time::Instant::now();
2789 if now >= deadline {
2790 return Ok(());
2791 }
2792 match tokio::time::timeout(deadline - now, connection.next_event()).await {
2793 Err(_) => return Ok(()),
2794 Ok(Ok(Some(event))) => {
2795 if let Some(message) = handshake_event_failure(&event) {
2796 return Err(truncate_text(&message, 500));
2797 }
2798 }
2799 Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2800 Ok(Err(error)) => return Err(error.to_string()),
2801 }
2802 }
2803}
2804
2805fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2806 let detail = event
2807 .payload
2808 .get("message")
2809 .or_else(|| event.payload.get("line"))
2810 .and_then(Value::as_str)
2811 .unwrap_or(event.kind.as_str());
2812 match event.kind.as_str() {
2813 "transport_closed" => Some("runtime transport closed during startup".into()),
2814 "transport_error" => Some(format!("runtime transport error: {detail}")),
2815 "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2816 _ => None,
2821 }
2822}
2823
2824fn indexed_claude_window(
2825 locator: &SessionLocator,
2826 options: &SessionLoadOptions,
2827) -> std::result::Result<Option<Value>, ServiceError> {
2828 use supercode_interchange::session::ClaudeReadIndex;
2829 if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2832 || options.include_subagents != Some(false)
2833 {
2834 return Ok(None);
2835 }
2836 let crate::StorageLocator::File { path } = &locator.storage else {
2837 return Ok(None);
2838 };
2839 if !ClaudeReadIndex::supports(path)
2840 .map_err(|error| ServiceError::Operation(error.to_string()))?
2841 {
2842 return Ok(None);
2843 }
2844 let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2845 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2846 let total = index.len();
2847 let (offset, end) = projected_message_window(total, options);
2848 let session = index
2849 .read_messages(offset..end)
2850 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2851 let summary = index
2852 .read_summary()
2853 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2854 let selected_options = SessionLoadOptions {
2855 message_offset: None,
2856 message_limit: None,
2857 message_tail: None,
2858 ..options.clone()
2859 };
2860 let mut selected = projected_session_json(&session, &selected_options);
2861 selected["raw_record_count"] = json!(index.raw_record_count());
2862 Ok(Some(json!({
2863 "session": selected,
2864 "summary": projected_session_summary(&summary, options),
2865 "window": {
2866 "has_more": offset > 0 || end < total, "has_newer": end < total,
2867 "has_older": offset > 0, "newer_items": index.item_count(end..total),
2868 "offset": offset, "older_items": index.item_count(0..offset),
2869 "returned": end - offset, "total_messages": total,
2870 }
2871 })))
2872}
2873
2874fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2875 let total_messages = session.messages.len();
2876 let (offset, end) = projected_message_window(total_messages, options);
2877 json!({
2878 "session": projected_session_json(session, options),
2879 "summary": projected_session_summary(session, options),
2880 "window": {
2881 "has_more": offset > 0 || end < total_messages,
2882 "has_newer": end < total_messages,
2883 "has_older": offset > 0,
2884 "newer_items": normalized_item_count(&session.messages[end..]),
2885 "offset": offset,
2886 "older_items": normalized_item_count(&session.messages[..offset]),
2887 "returned": end.saturating_sub(offset),
2888 "total_messages": total_messages,
2889 }
2890 })
2891}
2892
2893fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2894 messages
2895 .iter()
2896 .map(|message| {
2897 let conversation = usize::from(
2898 matches!(message.role, Role::Assistant | Role::User)
2899 && message_has_content(message),
2900 );
2901 let tool_result =
2902 usize::from(message.role == Role::Tool && message_has_content(message));
2903 conversation + tool_result + message.tool_calls().len()
2904 })
2905 .sum()
2906}
2907
2908fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2909 let mut conversational = session.messages.iter().filter(|message| {
2910 matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2911 });
2912 let first_message = conversational.clone().next();
2913 let last_message = conversational.next_back();
2914 let mut assistant = session
2915 .messages
2916 .iter()
2917 .filter(|message| message.role == Role::Assistant && message_has_content(message));
2918 let first_assistant_message = assistant.clone().next();
2919 let last_assistant_message = assistant.next_back();
2920 let end_of_turn = session
2921 .messages
2922 .iter()
2923 .rev()
2924 .find(|message| message.role != Role::System)
2925 .is_some_and(|message| {
2926 message.role == Role::Assistant
2927 && message_has_content(message)
2928 && message.tool_calls().is_empty()
2929 && message.metadata.get("phase").map(String::as_str) != Some("commentary")
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 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 #[serde(default)]
3091 fidelity: Option<Fidelity>,
3092 #[serde(default)]
3095 view: Option<SessionReadView>,
3096}
3097
3098#[derive(Deserialize)]
3099struct SessionReadView {
3100 #[serde(default)]
3103 tail_messages: Option<usize>,
3104 #[serde(default)]
3107 include_subagents: bool,
3108 #[serde(default)]
3110 display_history: bool,
3111 #[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 #[serde(default)]
3220 from_name: Option<String>,
3221 #[serde(default)]
3223 in_reply_to: Option<String>,
3224 #[serde(default)]
3226 notify_when_idle: bool,
3227 #[serde(default)]
3232 as_user: bool,
3233 #[serde(default)]
3236 homes: crate::HarnessHomes,
3237}
3238
3239#[derive(Deserialize)]
3240#[serde(deny_unknown_fields)]
3241struct InboxParams {
3242 #[serde(default)]
3244 from_name: Option<String>,
3245 #[serde(default)]
3247 address: Option<String>,
3248 #[serde(default)]
3250 all: bool,
3251}
3252
3253#[derive(Deserialize)]
3254#[serde(deny_unknown_fields)]
3255struct HarnessSettingsParams {
3256 harness: String,
3257}
3258
3259#[derive(Deserialize)]
3260#[serde(deny_unknown_fields)]
3261struct ConfigureHarnessParams {
3262 harness: String,
3263 #[serde(default)]
3264 changes: Vec<crate::HarnessSettingChange>,
3265 #[serde(default)]
3266 expected_revision: Option<String>,
3267}
3268
3269fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3270 match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3271 Ok(report) => (
3272 serde_json::to_value(report).unwrap_or(Value::Null),
3273 Value::Null,
3274 ),
3275 Err(error) => (
3276 Value::Null,
3277 Value::String(format!(
3278 "Volter Harness could not inspect Claude Code inbound controls: {error}"
3279 )),
3280 ),
3281 }
3282}
3283
3284async fn message_live_session(params: &MessageSessionParams) -> Value {
3298 use crate::mail_route::{Delivered, NoDoor, Refused};
3299 let (inbound_controls, inbound_controls_error) =
3300 claude_inbound_controls_or_error(¶ms.homes);
3301 let refused = |reason: &str, message: String| {
3302 json!({
3303 "delivered_to_bus": false,
3304 "refusal": {"reason": reason, "message": message},
3305 "inbound_controls": inbound_controls,
3306 "inbound_controls_error": inbound_controls_error,
3307 })
3308 };
3309 if params.text.trim().is_empty() {
3310 return refused(
3311 crate::claude_peer::ClaudePeerRefusal::DeliveryFailed.as_str(),
3312 "refusing to deliver an empty message".into(),
3313 );
3314 }
3315 let sender = match operator_address(params.from_name.as_deref()) {
3316 Ok(sender) => sender,
3317 Err(message) => return refused("invalid_sender", message),
3318 };
3319 let receiver = match crate::mailbox::MailAddress::new(
3320 crate::mailbox::local_machine_name(),
3321 params.locator.harness.as_str(),
3322 ¶ms.locator.session_id,
3323 ) {
3324 Ok(receiver) => receiver,
3325 Err(error) => return refused("delivery_failed", error.to_string()),
3326 };
3327 if params.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3329 && crate::runtime_mail::controlled_runtime(
3330 HarnessId::CLAUDE_CODE,
3331 ¶ms.locator.session_id,
3332 )
3333 .is_none()
3334 {
3335 if let Err(refusal) =
3336 crate::claude_peer::resolve_live_session(¶ms.homes, ¶ms.locator.session_id)
3337 {
3338 return refused(refusal.reason.as_str(), refusal.message);
3339 }
3340 }
3341 if params.as_user {
3342 return message_as_user(
3343 params,
3344 sender,
3345 receiver,
3346 inbound_controls,
3347 inbound_controls_error,
3348 )
3349 .await;
3350 }
3351 let door = match crate::mail_route::door_for(¶ms.homes, &receiver) {
3352 Ok(door) => door,
3353 Err(NoDoor::NotRunning | NoDoor::OtherMachine(_)) => {
3354 return refused(
3355 crate::claude_peer::ClaudePeerRefusal::NotLive.as_str(),
3356 format!(
3357 "no running `{}` session `{}` is reachable; its transcript is persisted only",
3358 params.locator.harness.as_str(),
3359 params.locator.session_id
3360 ),
3361 )
3362 }
3363 };
3364 let mut envelope = match crate::mailbox::Envelope::new(
3365 sender.clone(),
3366 format!("{}@{}", sender.session_id, sender.machine),
3367 crate::mailbox::MailKind::Peer,
3368 crate::mailbox::ReplyVia::Command,
3369 params.text.clone(),
3370 ) {
3371 Ok(envelope) => envelope,
3372 Err(error) => return refused("delivery_failed", error.to_string()),
3373 };
3374 envelope.in_reply_to = params.in_reply_to.clone();
3375 let how = match crate::mail_route::deliver(
3376 &envelope,
3377 &receiver,
3378 &door,
3379 true,
3380 params.notify_when_idle,
3381 )
3382 .await
3383 {
3384 Err(detail) => {
3385 return refused(
3386 crate::claude_peer::ClaudePeerRefusal::DeliveryFailed.as_str(),
3387 detail,
3388 )
3389 }
3390 Ok(Err(Refused::CannotQueueNative)) => unreachable!("sessions.message always wakes"),
3391 Ok(Err(Refused::TooLong(bytes))) => {
3392 return refused(
3393 "too_long",
3394 format!(
3395 "the message is {bytes} bytes; the limit is {}",
3396 crate::mail_route::MAX_RELAYED_BYTES
3397 ),
3398 )
3399 }
3400 Ok(Ok(delivered)) => match delivered {
3401 Delivered::Steered => "steered",
3402 Delivered::Started => "started",
3403 Delivered::Native { busy: true } => "next_tool_call",
3404 Delivered::Native { busy: false } => "started",
3405 Delivered::Hooked => "hook",
3406 Delivered::Queued => "queued",
3407 Delivered::Stored => "stored",
3408 Delivered::Operator => "filed",
3409 },
3410 };
3411 json!({
3412 "delivered_to_bus": !matches!(door, crate::mail_route::Door::Stored),
3413 "message_id": envelope.id,
3414 "reply_to": sender.to_string(),
3415 "target": {
3416 "harness": params.locator.harness.as_str(),
3417 "session_id": params.locator.session_id,
3418 "name": match &door {
3419 crate::mail_route::Door::Native(session) => Some(session.name.clone()),
3420 _ => None,
3421 },
3422 },
3423 "delivery": {"door": door.name(), "how": how},
3424 "inbound_controls": inbound_controls,
3425 "inbound_controls_error": inbound_controls_error,
3426 })
3427}
3428
3429async fn message_as_user(
3433 params: &MessageSessionParams,
3434 sender: crate::mailbox::MailAddress,
3435 receiver: crate::mailbox::MailAddress,
3436 inbound_controls: Value,
3437 inbound_controls_error: Value,
3438) -> Value {
3439 let envelope = match crate::mailbox::Envelope::new(
3440 sender.clone(),
3441 format!("{}@{}", sender.session_id, sender.machine),
3442 crate::mailbox::MailKind::User,
3443 crate::mailbox::ReplyVia::None,
3444 params.text.clone(),
3445 ) {
3446 Ok(envelope) => envelope,
3447 Err(error) => {
3448 return json!({
3449 "delivered_to_bus": false,
3450 "refusal": {"reason": "delivery_failed", "message": error.to_string()},
3451 "inbound_controls": inbound_controls,
3452 "inbound_controls_error": inbound_controls_error,
3453 })
3454 }
3455 };
3456 match crate::mail_route::deliver_user_turn(¶ms.homes, &envelope, &receiver).await {
3457 Ok(turn) => json!({
3458 "delivered_to_bus": turn != crate::mail_route::UserTurn::Waiting,
3459 "message_id": envelope.id,
3460 "target": {
3461 "harness": params.locator.harness.as_str(),
3462 "session_id": params.locator.session_id,
3463 },
3464 "delivery": {
3465 "door": match turn {
3466 crate::mail_route::UserTurn::Steered | crate::mail_route::UserTurn::Started => "runtime",
3467 _ => "pane",
3468 },
3469 "how": turn.as_str(),
3470 },
3471 "inbound_controls": inbound_controls,
3472 "inbound_controls_error": inbound_controls_error,
3473 }),
3474 Err(message) => json!({
3475 "delivered_to_bus": false,
3476 "refusal": {"reason": "no_user_door", "message": message},
3477 "inbound_controls": inbound_controls,
3478 "inbound_controls_error": inbound_controls_error,
3479 }),
3480 }
3481}
3482
3483#[derive(Deserialize)]
3484#[serde(deny_unknown_fields)]
3485struct ActivityUnderParams {
3486 pids: Vec<u32>,
3488 #[serde(default)]
3489 homes: crate::HarnessHomes,
3490}
3491
3492async fn activity_under_call(params: Value) -> std::result::Result<Value, ServiceError> {
3496 let params = decode::<ActivityUnderParams>(params)?;
3497 if params.pids.len() > 1024 {
3498 return Err(ServiceError::InvalidParams(
3499 "sessions.activity_under accepts at most 1024 pids".into(),
3500 ));
3501 }
3502 let found = crate::session_activity::activity_under(¶ms.pids, ¶ms.homes)
3503 .await
3504 .map_err(ServiceError::Sdk)?;
3505 Ok(json!({
3506 "activities": found
3507 .into_iter()
3508 .map(|(pid, activity)| json!({"pid": pid, "activity": activity}))
3509 .collect::<Vec<_>>(),
3510 }))
3511}
3512
3513fn operator_address(
3516 name: Option<&str>,
3517) -> std::result::Result<crate::mailbox::MailAddress, String> {
3518 let name: String = name
3519 .unwrap_or("supercode")
3520 .trim()
3521 .chars()
3522 .map(|character| {
3523 if character.is_whitespace() || character == '@' {
3524 '-'
3525 } else {
3526 character
3527 }
3528 })
3529 .collect();
3530 if name.is_empty() {
3531 return Err("from_name must not be empty".into());
3532 }
3533 crate::mailbox::MailAddress::new(crate::mailbox::local_machine_name(), "operator", name)
3534 .map_err(|error| error.to_string())
3535}
3536
3537fn inbox_call(params: InboxParams) -> std::result::Result<Value, ServiceError> {
3541 let address = match (¶ms.address, ¶ms.from_name) {
3542 (Some(address), None) => crate::mailbox::MailAddress::parse(address)
3543 .map_err(|error| ServiceError::InvalidParams(error.to_string()))?,
3544 (None, name) => operator_address(name.as_deref()).map_err(ServiceError::InvalidParams)?,
3545 (Some(_), Some(_)) => {
3546 return Err(ServiceError::InvalidParams(
3547 "sessions.inbox takes from_name or address, not both".into(),
3548 ))
3549 }
3550 };
3551 let operation = |error: std::io::Error| ServiceError::Operation(error.to_string());
3552 let mailbox =
3553 crate::mailbox::Mailbox::open(&crate::mailbox::mail_root(), &address).map_err(operation)?;
3554 let claimed = mailbox.claim_unread().map_err(operation)?;
3555 let mut messages: Vec<Value> = Vec::new();
3556 if params.all {
3557 for stored in mailbox.list().map_err(operation)? {
3558 if stored.state == crate::mailbox::MailState::Read {
3559 messages.push(json!({"state": "read", "envelope": stored.envelope, "rendered": stored.envelope.render()}));
3560 }
3561 }
3562 }
3563 for stored in &claimed {
3564 messages.push(json!({"state": "unread", "envelope": stored.envelope, "rendered": stored.envelope.render()}));
3565 }
3566 for stored in &claimed {
3567 mailbox.acknowledge(stored).map_err(operation)?;
3568 }
3569 Ok(json!({"address": address.to_string(), "messages": messages}))
3570}
3571
3572#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3577struct FollowedSource {
3578 harness: String,
3579 session_id: String,
3580 reported: Option<String>,
3581}
3582
3583#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3584struct ActivitySubscription {
3585 locators: Vec<SessionLocator>,
3586 homes: crate::HarnessHomes,
3587 reported: BTreeMap<(String, String), crate::SessionActivity>,
3588}
3589
3590fn live_descriptor_value(
3597 session: &SessionDescriptor,
3598 doors: &crate::mail_route::LiveSessions,
3599) -> std::result::Result<Value, ServiceError> {
3600 let mut value = serde_json::to_value(session)
3601 .map_err(|error| ServiceError::Operation(error.to_string()))?;
3602 if let Some(workspace) = &session.cwd {
3603 let source = LiveRuntimeSource {
3604 harness: session.locator.harness.as_str().to_string(),
3605 session_id: session.locator.session_id.clone(),
3606 workspace: workspace.clone(),
3607 };
3608 if let Some(endpoint) = discover_live_runtime(&source)
3609 .map_err(|error| ServiceError::Operation(error.to_string()))?
3610 {
3611 value["live_endpoint"] = json!(endpoint.as_str());
3612 }
3613 }
3614 if let Some(door) = doors.door(
3618 session.locator.harness.as_str(),
3619 &session.locator.session_id,
3620 ) {
3621 value["delivery"] = json!(door);
3622 }
3623 Ok(value)
3624}
3625
3626fn live_index_changes(
3627 changes: Vec<crate::session_index::SessionIndexChange>,
3628 homes: &HarnessHomes,
3629) -> std::result::Result<Vec<Value>, ServiceError> {
3630 use crate::session_index::SessionIndexChange;
3631 let doors = crate::mail_route::LiveSessions::read(homes);
3632 changes
3633 .into_iter()
3634 .map(|change| match change {
3635 SessionIndexChange::Added { descriptor } => Ok(json!({
3636 "kind": "added",
3637 "descriptor": live_descriptor_value(&descriptor, &doors)?,
3638 })),
3639 SessionIndexChange::Updated { descriptor } => Ok(json!({
3640 "kind": "updated",
3641 "descriptor": live_descriptor_value(&descriptor, &doors)?,
3642 })),
3643 SessionIndexChange::Removed { key } => Ok(json!({
3644 "kind": "removed",
3645 "key": key,
3646 })),
3647 })
3648 .collect()
3649}
3650
3651fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3652 use crate::{SessionPresence, SessionTurnState};
3653 match (activity.presence, activity.turn) {
3654 (SessionPresence::Persisted, _) => None,
3655 (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3656 (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3657 (SessionPresence::Running, SessionTurnState::Unknown)
3661 if activity.evidence.native_state.is_none() =>
3662 {
3663 None
3664 }
3665 (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3666 }
3667}
3668
3669#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3670#[serde(rename_all = "kebab-case")]
3671enum TransferFormat {
3672 ClaudeCode,
3673 Codex,
3674 #[serde(rename = "opencode", alias = "open-code")]
3675 OpenCode,
3676 Pi,
3677 Grok,
3678 Gemini,
3679 Goose,
3680 Hermes,
3684}
3685
3686impl TransferFormat {
3687 fn id(self) -> &'static str {
3688 match self {
3689 Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3690 Self::Codex => HarnessId::CODEX,
3691 Self::OpenCode => HarnessId::OPENCODE,
3692 Self::Pi => HarnessId::PI,
3693 Self::Grok => HarnessId::GROK,
3694 Self::Gemini => HarnessId::GEMINI,
3695 Self::Goose => HarnessId::GOOSE,
3696 Self::Hermes => HarnessId::HERMES,
3697 }
3698 }
3699}
3700
3701impl From<TransferFormat> for SessionFormat {
3702 fn from(value: TransferFormat) -> Self {
3703 match value {
3704 TransferFormat::ClaudeCode => Self::ClaudeCode,
3705 TransferFormat::Codex => Self::Codex,
3706 TransferFormat::OpenCode => Self::OpenCode,
3707 TransferFormat::Pi => Self::Pi,
3708 TransferFormat::Grok => Self::Grok,
3709 TransferFormat::Gemini => Self::Gemini,
3710 TransferFormat::Goose => Self::Goose,
3711 TransferFormat::Hermes => Self::Codex,
3713 }
3714 }
3715}
3716
3717#[derive(Deserialize)]
3718struct ImportSessionParams {
3719 source_harness: TransferFormat,
3720 content: String,
3721}
3722
3723#[derive(Deserialize)]
3724struct ExportSessionParams {
3725 locator: SessionLocator,
3726 target_harness: TransferFormat,
3727}
3728
3729#[derive(Deserialize)]
3730struct ReduceSessionParams {
3731 locator: SessionLocator,
3732 target_harness: TransferFormat,
3733 #[serde(default = "default_keep_last")]
3734 keep_last: usize,
3735}
3736
3737fn default_keep_last() -> usize {
3738 6
3739}
3740
3741#[derive(Deserialize)]
3742struct BranchSessionParams {
3743 locator: SessionLocator,
3744 #[serde(default)]
3745 target_harness: Option<TransferFormat>,
3746}
3747
3748#[derive(Deserialize)]
3749struct HandoffSessionParams {
3750 locator: SessionLocator,
3751 target_harness: TransferFormat,
3752 #[serde(default)]
3753 cwd: Option<PathBuf>,
3754}
3755
3756#[derive(Deserialize)]
3757struct MaterializeSessionParams {
3758 artifact: crate::native_materialize::MaterializeArtifact,
3759 cwd: PathBuf,
3760 #[serde(default)]
3762 homes: HarnessHomes,
3763}
3764
3765#[derive(Debug, Clone, Copy, Default, Deserialize)]
3768#[serde(rename_all = "snake_case")]
3769enum ResumePolicy {
3770 Default,
3771 #[default]
3772 Yolo,
3773}
3774
3775#[derive(Deserialize)]
3776struct ResumeInstructionsParams {
3777 locator: SessionLocator,
3778 #[serde(default)]
3779 cwd: Option<PathBuf>,
3780 #[serde(default)]
3781 policy: ResumePolicy,
3782}
3783
3784#[derive(Deserialize)]
3786struct WorkflowLoadParams {
3787 from: crate::workflow_doors::WorkflowHarness,
3788 home: PathBuf,
3789}
3790
3791#[derive(Deserialize)]
3794struct OrchestrationLoadParams {
3795 root: PathBuf,
3796 #[serde(default)]
3797 flavor: crate::orchestration_doors::HomeFlavor,
3798}
3799
3800#[derive(Deserialize)]
3803struct OrchestrationSaveParams {
3804 root: PathBuf,
3805 orchestration: crate::orchestration::Orchestration,
3806 #[serde(default)]
3807 vault: BTreeMap<String, String>,
3808}
3809
3810#[derive(Deserialize)]
3812struct OrchestrationCompileParams {
3813 from: crate::orchestration_doors::OrchestrationHarness,
3814 home: PathBuf,
3815}
3816
3817#[derive(Deserialize)]
3821struct OrchestrationDecompileParams {
3822 to: crate::orchestration_doors::OrchestrationHarness,
3823 orchestration: crate::orchestration::Orchestration,
3824 source: PathBuf,
3825 #[serde(default)]
3826 source_flavor: crate::orchestration_doors::SourceFlavor,
3827 dest: PathBuf,
3828 #[serde(default)]
3829 vault: BTreeMap<String, String>,
3830}
3831
3832#[derive(Deserialize)]
3835struct OrchestrationImportParams {
3836 from: crate::orchestration_doors::OrchestrationHarness,
3837 home: PathBuf,
3838 into: PathBuf,
3839}
3840
3841#[derive(Deserialize)]
3844struct OrchestrationExportParams {
3845 to: crate::orchestration_doors::OrchestrationHarness,
3846 root: PathBuf,
3847 dest: PathBuf,
3848}
3849
3850#[derive(Deserialize)]
3852struct JobsGetParams {
3853 harness: String,
3854 id: String,
3855 #[serde(default)]
3856 homes: crate::HarnessHomes,
3857}
3858
3859fn mutate_job(
3867 verb: crate::jobs_control::JobVerb,
3868 params: Value,
3869) -> std::result::Result<Value, ServiceError> {
3870 let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3871 refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3872 let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3873 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3874}
3875
3876fn mutate_skill(
3883 verb: crate::skills_control::SkillVerb,
3884 params: Value,
3885) -> std::result::Result<Value, ServiceError> {
3886 let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3887 if !crate::skills_control::supports_skill_control(&mutation.harness) {
3888 return Err(ServiceError::UnsupportedAction(format!(
3889 "`{}` has no skills root Volter Harness reads; `skills.{}` is supported for: {}",
3890 mutation.harness,
3891 verb.as_str(),
3892 crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3893 )));
3894 }
3895 let outcome =
3896 crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3897 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3898}
3899
3900fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3902 match error {
3903 crate::skills_control::SkillControlError::Unsupported(message) => {
3904 ServiceError::UnsupportedAction(message)
3905 }
3906 crate::skills_control::SkillControlError::Invalid(message) => {
3907 ServiceError::InvalidParams(message)
3908 }
3909 crate::skills_control::SkillControlError::Failed(message) => {
3910 ServiceError::Operation(message)
3911 }
3912 }
3913}
3914
3915fn mutate_profile(
3923 verb: crate::profiles_control::ProfileVerb,
3924 params: Value,
3925) -> std::result::Result<Value, ServiceError> {
3926 let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3927 let outcome =
3928 crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3929 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3930}
3931
3932fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3934 match error {
3935 crate::profiles_control::ProfileControlError::Unsupported(message) => {
3936 ServiceError::UnsupportedAction(message)
3937 }
3938 crate::profiles_control::ProfileControlError::Invalid(message) => {
3939 ServiceError::InvalidParams(message)
3940 }
3941 crate::profiles_control::ProfileControlError::Failed(message) => {
3942 ServiceError::Operation(message)
3943 }
3944 }
3945}
3946
3947fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3951 match error {
3952 crate::jobs_control::JobControlError::Unsupported(message) => {
3953 ServiceError::UnsupportedAction(message)
3954 }
3955 crate::jobs_control::JobControlError::Invalid(message) => {
3956 ServiceError::InvalidParams(message)
3957 }
3958 crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3959 }
3960}
3961
3962fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3967 match error {
3968 crate::SessionControlError::Unsupported(message) => {
3969 ServiceError::UnsupportedAction(message)
3970 }
3971 crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3972 crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3973 }
3974}
3975
3976fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3981 if crate::jobs::supports_jobs(harness) {
3982 return Ok(());
3983 }
3984 Err(ServiceError::UnsupportedAction(format!(
3985 "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3986 crate::jobs::JOB_HARNESSES.join(", ")
3987 )))
3988}
3989
3990#[derive(Deserialize)]
3992struct RunsGetParams {
3993 harness: String,
3994 id: String,
3995 #[serde(default)]
3996 homes: crate::HarnessHomes,
3997}
3998
3999fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
4004 if crate::runs::supports_runs(harness) {
4005 return Ok(());
4006 }
4007 Err(ServiceError::UnsupportedAction(format!(
4008 "`{harness}` keeps no run store; `{verb}` is supported for: {}",
4009 crate::runs::RUN_HARNESSES.join(", ")
4010 )))
4011}
4012
4013#[derive(Serialize)]
4014struct SessionArtifact {
4015 source_harness: HarnessId,
4016 target_harness: &'static str,
4017 session_id: Option<String>,
4018 content: String,
4019 suggested_filename: String,
4020 files: Vec<SessionArtifactFile>,
4021 fidelity: Fidelity,
4022 residue: Vec<String>,
4023}
4024
4025#[derive(Serialize)]
4026struct SessionArtifactFile {
4027 path: String,
4028 content: String,
4029 role: ArtifactFileRole,
4030}
4031
4032#[derive(Serialize)]
4033#[serde(rename_all = "snake_case")]
4034enum ArtifactFileRole {
4035 Primary,
4036 Subagent,
4037 Bundle,
4038 SourceRecovery,
4039}
4040
4041#[derive(Serialize)]
4042struct StructuredLaunch {
4043 cwd: PathBuf,
4044 program: String,
4045 arguments: Vec<String>,
4046 env: BTreeMap<String, String>,
4047}
4048
4049struct HandoffInstructions {
4050 launch: StructuredLaunch,
4051 materialize: Option<StructuredLaunch>,
4052 requires_materialization: bool,
4053 note: String,
4054}
4055
4056#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4057#[serde(rename_all = "snake_case")]
4058enum HarnessProbeLevel {
4059 #[default]
4060 Passive,
4061 Handshake,
4062}
4063
4064#[derive(Default, Deserialize)]
4065#[serde(default)]
4066struct HarnessInventoryParams {
4067 harness: Option<HarnessId>,
4068 harnesses: Vec<HarnessId>,
4069 workspace: Option<PathBuf>,
4070 probe: HarnessProbeLevel,
4071 include_sessions: bool,
4072 skip_versions: bool,
4074}
4075
4076#[derive(Deserialize)]
4077struct HarnessAuthenticationParams {
4078 harness: HarnessId,
4079}
4080
4081#[derive(Deserialize)]
4082struct BeginHarnessAuthenticationParams {
4083 harness: HarnessId,
4084 #[serde(default = "local_browser_authentication_environment")]
4085 environment: crate::HarnessAuthenticationEnvironment,
4086 #[serde(default)]
4087 method: Option<crate::HarnessAuthenticationMethodId>,
4088 #[serde(default)]
4089 cwd: Option<PathBuf>,
4090}
4091
4092fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
4093 crate::HarnessAuthenticationEnvironment::LocalBrowser
4094}
4095
4096#[derive(Serialize)]
4097struct HarnessInventoryReport {
4098 probe: HarnessProbeLevel,
4099 workspace: Option<PathBuf>,
4100 harnesses: Vec<LocalHarness>,
4101}
4102
4103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4104#[serde(rename_all = "snake_case")]
4105enum HarnessAuthState {
4106 Ready,
4107 Configured,
4108 Required,
4109 Unknown,
4110}
4111
4112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4113#[serde(rename_all = "snake_case")]
4114enum HarnessRuntimeState {
4115 Ready,
4116 Degraded,
4117 Unavailable,
4118}
4119
4120#[derive(Serialize)]
4121struct HarnessSessionCounts {
4122 global: Option<usize>,
4123 workspace: Option<usize>,
4124}
4125
4126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4137#[serde(rename_all = "snake_case")]
4138pub enum GatewayState {
4139 Up,
4140 Down,
4141 Unknown,
4142}
4143
4144#[derive(Debug, Clone, Serialize)]
4146pub struct GatewayHealth {
4147 pub state: GatewayState,
4148 #[serde(skip_serializing_if = "Option::is_none")]
4153 pub endpoint: Option<String>,
4154 #[serde(skip_serializing_if = "Option::is_none")]
4155 pub version: Option<String>,
4156 pub evidence: String,
4158 pub checked_at_ms: u64,
4159}
4160
4161fn openclaw_gateway_endpoint(home: &Path) -> String {
4165 let config_path = home.join(".openclaw/openclaw.json");
4166 let gateway = std::fs::read_to_string(&config_path)
4167 .ok()
4168 .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
4169 .and_then(|config| config.get("gateway").cloned());
4170 if let Some(url) = gateway
4171 .as_ref()
4172 .and_then(|gateway| gateway.get("url"))
4173 .and_then(serde_json::Value::as_str)
4174 {
4175 return url.to_string();
4176 }
4177 let port = gateway
4178 .as_ref()
4179 .and_then(|gateway| gateway.get("port"))
4180 .and_then(serde_json::Value::as_u64)
4181 .unwrap_or(18789);
4182 format!("ws://127.0.0.1:{port}")
4183}
4184
4185fn hermes_gateway_status() -> Option<(GatewayState, String)> {
4192 let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
4193 let output = std::process::Command::new(&program)
4194 .args(["gateway", "status"])
4195 .stdin(std::process::Stdio::null())
4196 .output()
4197 .ok()?;
4198 let text = format!(
4199 "{}{}",
4200 String::from_utf8_lossy(&output.stdout),
4201 String::from_utf8_lossy(&output.stderr)
4202 );
4203 let verdict = text.lines().find_map(|line| {
4204 let l = line.trim();
4205 if l.contains("supervised by launchd (PID")
4206 || l.contains("supervised by systemd (PID")
4207 || l.contains("Gateway is running")
4208 || l.contains("process is running")
4209 {
4210 Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
4211 } else if l.contains("not running") || l.contains("not installed") {
4212 Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
4213 } else {
4214 None
4215 }
4216 });
4217 verdict
4218}
4219
4220fn gateway_health(
4221 id: &str,
4222 installed: bool,
4223 running: Option<&RunningInstance>,
4224 version: Option<&str>,
4225) -> GatewayHealth {
4226 let checked_at_ms = now_epoch_ms();
4227 let home = supercode_interchange::user_home()
4228 .map(std::path::PathBuf::into_os_string)
4229 .map(PathBuf::from);
4230 match id {
4231 HarnessId::HERMES | HarnessId::OPENCLAW => {
4232 let endpoint = (id == HarnessId::OPENCLAW)
4233 .then(|| home.as_deref().map(openclaw_gateway_endpoint))
4234 .flatten();
4235 let (state, evidence) = match running {
4236 Some(instance) => (GatewayState::Up, instance.evidence.clone()),
4237 None if !installed => (
4238 GatewayState::Unknown,
4239 format!("`{id}` is not installed; no gateway to probe"),
4240 ),
4241 None if id == HarnessId::HERMES => match hermes_gateway_status() {
4242 Some((state, evidence)) => (state, evidence),
4245 None => (
4246 GatewayState::Down,
4247 "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
4248 ),
4249 },
4250 None => (
4251 GatewayState::Down,
4252 format!(
4253 "no TCP listener at {}",
4254 endpoint.as_deref().unwrap_or("the gateway endpoint")
4255 ),
4256 ),
4257 };
4258 GatewayHealth {
4259 state,
4260 endpoint,
4261 version: version.map(str::to_string),
4262 evidence,
4263 checked_at_ms,
4264 }
4265 }
4266 HarnessId::ORCHESTRATOR => {
4273 let root = crate::HarnessHomes::default().orchestrator;
4274 let (state, evidence) = match crate::orchestrator::read_lease(&root) {
4275 Some(lease) if lease.is_live() => (
4276 GatewayState::Up,
4277 format!(
4278 "`{}` names pid {} (started {}), which is live",
4279 crate::orchestrator::lock_path(&root).display(),
4280 lease.pid,
4281 lease.started_at
4282 ),
4283 ),
4284 Some(lease) => (
4285 GatewayState::Down,
4286 format!(
4287 "stale lease `{}`: pid {} is gone",
4288 crate::orchestrator::lock_path(&root).display(),
4289 lease.pid
4290 ),
4291 ),
4292 None => (
4293 GatewayState::Down,
4294 format!(
4295 "no lease at `{}`; `supercode orchestrator start` writes one",
4296 crate::orchestrator::lock_path(&root).display()
4297 ),
4298 ),
4299 };
4300 GatewayHealth {
4301 state,
4302 endpoint: None,
4303 version: version.map(str::to_string),
4304 evidence,
4305 checked_at_ms,
4306 }
4307 }
4308 _ => GatewayHealth {
4309 state: GatewayState::Unknown,
4310 endpoint: None,
4311 version: version.map(str::to_string),
4312 evidence: format!("`{id}` runs per session, not as a gateway"),
4313 checked_at_ms,
4314 },
4315 }
4316}
4317
4318#[derive(Debug, Clone, Serialize)]
4319struct RunningInstance {
4320 method: RunningInstanceMethod,
4322 evidence: String,
4324 checked_at_ms: u64,
4326}
4327
4328#[derive(Debug, Clone, Copy, Serialize)]
4329#[serde(rename_all = "snake_case")]
4330enum RunningInstanceMethod {
4331 GatewayConnect,
4334 StoreWalActivity,
4337}
4338
4339fn now_epoch_ms() -> u64 {
4340 std::time::SystemTime::now()
4341 .duration_since(std::time::UNIX_EPOCH)
4342 .map(|elapsed| elapsed.as_millis() as u64)
4343 .unwrap_or(0)
4344}
4345
4346fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4350 let config_path = home.join(".openclaw/openclaw.json");
4351 let text = std::fs::read_to_string(&config_path).ok();
4352 let gateway = text
4353 .as_deref()
4354 .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4355 .and_then(|config| config.get("gateway").cloned());
4356 let address = gateway
4357 .as_ref()
4358 .and_then(|gateway| gateway.get("url"))
4359 .and_then(serde_json::Value::as_str)
4360 .and_then(|url| {
4361 url.split("://").nth(1).map(|rest| {
4362 rest.trim_end_matches('/')
4363 .split('/')
4364 .next()
4365 .unwrap_or(rest)
4366 .to_string()
4367 })
4368 })
4369 .unwrap_or_else(|| {
4370 let port = gateway
4371 .as_ref()
4372 .and_then(|gateway| gateway.get("port"))
4373 .and_then(serde_json::Value::as_u64)
4374 .unwrap_or(18789);
4375 format!("127.0.0.1:{port}")
4376 });
4377 let reachable = std::net::TcpStream::connect_timeout(
4378 &address.parse().ok()?,
4379 std::time::Duration::from_millis(400),
4380 )
4381 .is_ok();
4382 reachable.then(|| RunningInstance {
4383 method: RunningInstanceMethod::GatewayConnect,
4384 evidence: format!(
4385 "gateway endpoint {address} accepted a TCP connect (from {})",
4386 config_path.display()
4387 ),
4388 checked_at_ms: now_epoch_ms(),
4389 })
4390}
4391
4392fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4397 let wal = home.join(".hermes/state.db-wal");
4398 let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4399 let age_ms = std::time::SystemTime::now()
4400 .duration_since(modified)
4401 .map(|age| age.as_millis() as u64)
4402 .unwrap_or(u64::MAX);
4403 (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4404 method: RunningInstanceMethod::StoreWalActivity,
4405 evidence: format!(
4406 "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4407 wal.display()
4408 ),
4409 checked_at_ms: now_epoch_ms(),
4410 })
4411}
4412
4413fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4415 let home = supercode_interchange::user_home()
4416 .map(std::path::PathBuf::into_os_string)
4417 .map(PathBuf::from)?;
4418 match id {
4419 HarnessId::OPENCLAW => probe_openclaw_running(&home),
4420 HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4421 _ => None,
4422 }
4423}
4424
4425#[derive(Serialize)]
4426struct LocalHarness {
4427 id: HarnessId,
4428 display_name: String,
4429 supported: bool,
4430 installed: bool,
4431 executable: Option<String>,
4432 version: Option<String>,
4433 auth: HarnessAuthState,
4434 runtime: HarnessRuntimeState,
4435 protocol: String,
4436 capabilities: crate::RuntimeCapabilities,
4437 effective_capabilities: crate::RuntimeCapabilities,
4438 sessions: HarnessSessionCounts,
4439 #[serde(skip_serializing_if = "Option::is_none")]
4442 running: Option<RunningInstance>,
4443 gateway: GatewayHealth,
4445 reason: Option<String>,
4446 repair: Option<String>,
4447}
4448
4449#[derive(Clone, Deserialize)]
4450struct RuntimeBackendParams {
4451 harness: HarnessId,
4452 #[serde(default)]
4453 protocol: Option<String>,
4454 #[serde(default)]
4455 launch: Option<RuntimeLaunch>,
4456 #[serde(default)]
4457 base_url: Option<String>,
4458 #[serde(default)]
4459 policy: RuntimePolicy,
4460}
4461
4462#[derive(Debug, Clone, Copy, Default, Deserialize)]
4465#[serde(rename_all = "snake_case")]
4466enum RuntimePolicy {
4467 Default,
4468 #[default]
4469 Yolo,
4470}
4471
4472#[derive(Deserialize)]
4473struct RuntimeStartParams {
4474 #[serde(flatten)]
4475 backend: RuntimeBackendParams,
4476 cwd: PathBuf,
4477 #[serde(default)]
4480 mcp_servers: Vec<crate::McpServerLaunch>,
4481 #[serde(default)]
4483 approval_policy: Option<String>,
4484}
4485
4486#[derive(Deserialize)]
4487struct RuntimeAttachParams {
4488 #[serde(flatten)]
4489 backend: RuntimeBackendParams,
4490 runtime_id: String,
4491 #[serde(default)]
4492 cwd: Option<PathBuf>,
4493 #[serde(default)]
4496 mcp_servers: Vec<crate::McpServerLaunch>,
4497 #[serde(default)]
4499 approval_policy: Option<String>,
4500}
4501
4502#[derive(Deserialize)]
4503struct RuntimeConnectionParams {
4504 connection: String,
4505}
4506
4507#[derive(Deserialize)]
4508struct RuntimeInputParams {
4509 connection: String,
4510 text: String,
4511 #[serde(default)]
4512 image_urls: Vec<String>,
4513}
4514
4515const MAX_RUNTIME_IMAGES: usize = 4;
4516const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4517const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4518
4519fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4520 if image_urls.len() > MAX_RUNTIME_IMAGES {
4521 return Err(ServiceError::InvalidParams(format!(
4522 "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4523 )));
4524 }
4525 let mut total = 0usize;
4526 for url in &image_urls {
4527 if !(url.starts_with("data:image/")
4528 || url.starts_with("https://")
4529 || url.starts_with("http://"))
4530 {
4531 return Err(ServiceError::InvalidParams(
4532 "runtime images must be image data URLs or HTTP(S) URLs".into(),
4533 ));
4534 }
4535 if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4536 return Err(ServiceError::InvalidParams(format!(
4537 "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4538 )));
4539 }
4540 total = total.saturating_add(url.len());
4541 }
4542 if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4543 return Err(ServiceError::InvalidParams(format!(
4544 "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4545 )));
4546 }
4547 Ok(image_urls)
4548}
4549
4550#[derive(Deserialize)]
4551struct RuntimeRespondParams {
4552 connection: String,
4553 request_id: Value,
4554 response: Value,
4555}
4556
4557fn default_reduction_store_root() -> PathBuf {
4558 if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4559 return PathBuf::from(root).join("sessions");
4560 }
4561 if let Some(home) = supercode_interchange::user_home().map(std::path::PathBuf::into_os_string) {
4562 return PathBuf::from(home).join(".supercode").join("sessions");
4563 }
4564 PathBuf::from(".supercode").join("sessions")
4565}
4566
4567fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4568 let mut output = String::new();
4569 for message in messages {
4570 output.push_str(
4571 &serde_json::to_string(message)
4572 .map_err(|error| ServiceError::Operation(error.to_string()))?,
4573 );
4574 output.push('\n');
4575 }
4576 Ok(output)
4577}
4578
4579fn parse_messages_jsonl(
4580 content: &str,
4581) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4582 content
4583 .lines()
4584 .enumerate()
4585 .filter(|(_, line)| !line.trim().is_empty())
4586 .map(|(index, line)| {
4587 serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4588 ServiceError::Operation(format!(
4589 "reduced transcript line {} is invalid: {error}",
4590 index + 1
4591 ))
4592 })
4593 })
4594 .collect()
4595}
4596
4597fn reduced_bootstrap_prompt(
4598 source: &SessionLocator,
4599 target: TransferFormat,
4600 view_jsonl: &str,
4601 sidecar_path: &Path,
4602 reduction_log_path: &Path,
4603) -> String {
4604 format!(
4605 "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4606 \n\
4607 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 Volter Harness 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\
4608 \n\
4609 <supercode-reduced-session source-session=\"{source_id}\">\n\
4610 {view_jsonl}\
4611 </supercode-reduced-session>\n\
4612 \n\
4613 Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4614 source_harness = source.harness.as_str(),
4615 target_harness = target.id(),
4616 sidecar = sidecar_path.display(),
4617 log = reduction_log_path.display(),
4618 source_id = source.session_id,
4619 )
4620}
4621
4622fn session_artifact(
4623 locator: &SessionLocator,
4624 session: &Session,
4625 target: TransferFormat,
4626) -> std::result::Result<SessionArtifact, ServiceError> {
4627 session_artifact_with_id(locator, session, target, None)
4628}
4629
4630fn session_artifact_with_id(
4631 locator: &SessionLocator,
4632 session: &Session,
4633 target: TransferFormat,
4634 target_session_id: Option<&str>,
4635) -> std::result::Result<SessionArtifact, ServiceError> {
4636 let format: SessionFormat = target.into();
4637 let diagonal = format.source() == session.meta.source;
4638 crate::residue_store::store_segments(session);
4639 let has_appended_turns = session
4640 .imported_message_count
4641 .is_some_and(|imported| imported < session.messages.len());
4642 let mut restoration = None;
4643 let content = if let Some(id) = target_session_id {
4644 if diagonal && format != SessionFormat::OpenCode {
4645 session
4646 .to_jsonl_spliced(format, Some(id))
4647 .map_err(operation)?
4648 } else {
4649 let mut rewritten = session.clone();
4650 rewritten.meta.session_id = Some(id.to_string());
4651 rewritten.to_jsonl(format).map_err(operation)?
4652 }
4653 } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4654 session.raw_verbatim()
4655 } else if diagonal {
4656 session.to_jsonl_spliced(format, None).map_err(operation)?
4657 } else {
4658 match session
4661 .restore_residue(format, crate::residue_store::lookup)
4662 .map_err(operation)?
4663 {
4664 Some((content, report)) => {
4665 restoration = Some(report);
4666 content
4667 }
4668 None => session.to_jsonl(format).map_err(operation)?,
4669 }
4670 };
4671 let stem = sanitize_filename(
4672 target_session_id
4673 .or(session.meta.session_id.as_deref())
4674 .unwrap_or(&locator.session_id),
4675 );
4676 let suggested_filename = if diagonal && target == TransferFormat::Grok {
4677 "chat_history.jsonl".to_string()
4678 } else if target == TransferFormat::Goose {
4679 format!("{stem}.goose.json")
4680 } else {
4681 format!("{stem}.{}.jsonl", target.id())
4682 };
4683 let mut files = vec![SessionArtifactFile {
4684 path: suggested_filename.clone(),
4685 content: content.clone(),
4686 role: ArtifactFileRole::Primary,
4687 }];
4688 if target == TransferFormat::ClaudeCode {
4689 let bundle_stem = Path::new(&suggested_filename)
4690 .file_stem()
4691 .and_then(|stem| stem.to_str())
4692 .unwrap_or(&stem);
4693 let mut child_paths = BTreeSet::new();
4694 for (index, subagent) in session.subagents.iter().enumerate() {
4695 let agent_id = subagent
4696 .meta
4697 .agent_id
4698 .as_deref()
4699 .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4700 .map(sanitize_filename)
4701 .filter(|id| !id.is_empty())
4702 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4703 let child_has_appended_turns = subagent
4704 .imported_message_count
4705 .is_some_and(|imported| imported < subagent.messages.len());
4706 let child_content = if target_session_id.is_none()
4707 && subagent.meta.source == SessionSource::ClaudeCode
4708 && subagent.raw_is_verbatim
4709 && !child_has_appended_turns
4710 {
4711 subagent.raw_verbatim()
4712 } else if subagent.meta.source == SessionSource::ClaudeCode {
4713 subagent
4714 .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4715 .map_err(operation)?
4716 } else {
4717 let mut child = subagent.clone();
4718 if let Some(id) = target_session_id {
4719 child.meta.session_id = Some(id.to_string());
4720 }
4721 child
4722 .to_jsonl(SessionFormat::ClaudeCode)
4723 .map_err(operation)?
4724 };
4725 let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4726 if !child_paths.insert(path.clone()) {
4727 return Err(ServiceError::Operation(format!(
4728 "Claude subagent ids collide at artifact path `{path}`"
4729 )));
4730 }
4731 files.push(SessionArtifactFile {
4732 path,
4733 content: child_content,
4734 role: ArtifactFileRole::Subagent,
4735 });
4736 }
4737 }
4738 if diagonal && target == TransferFormat::Grok {
4739 append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4740 }
4741 if !diagonal || !session.raw_is_verbatim {
4742 files.push(SessionArtifactFile {
4743 path: "recovery/source.supercode.jsonl".into(),
4744 content: session.to_native_jsonl(),
4745 role: ArtifactFileRole::SourceRecovery,
4746 });
4747 for (index, subagent) in session.subagents.iter().enumerate() {
4748 let id = subagent
4749 .meta
4750 .agent_id
4751 .as_deref()
4752 .map(sanitize_filename)
4753 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4754 files.push(SessionArtifactFile {
4755 path: format!("recovery/subagents/{id}.supercode.jsonl"),
4756 content: subagent.to_native_jsonl(),
4757 role: ArtifactFileRole::SourceRecovery,
4758 });
4759 }
4760 }
4761 if !diagonal && session.meta.source == SessionSource::Grok {
4762 append_grok_bundle_files(
4763 locator,
4764 "recovery/grok/",
4765 ArtifactFileRole::SourceRecovery,
4766 &mut files,
4767 )?;
4768 }
4769 let (fidelity, residue) = if diagonal
4770 && target_session_id.is_none()
4771 && session.raw_is_verbatim
4772 && !has_appended_turns
4773 {
4774 (Fidelity::ByteLossless, Vec::new())
4775 } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4776 (
4777 Fidelity::ValueLossless,
4778 vec![if target_session_id.is_some() {
4779 "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4780 } else {
4781 "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4782 }],
4783 )
4784 } else {
4785 match restoration {
4786 Some(report) if report.rendered_messages == 0 => (
4787 Fidelity::ByteLossless,
4788 vec![format!(
4789 "restored verbatim from this conversation's {} source records in the residue store",
4790 target.id()
4791 )],
4792 ),
4793 Some(report) => (
4794 Fidelity::Semantic,
4795 vec![format!(
4796 "{} of {} messages restored verbatim from the residue store; the other {} written by the {} writer",
4797 report.restored_messages,
4798 report.restored_messages + report.rendered_messages,
4799 report.rendered_messages,
4800 target.id()
4801 )],
4802 ),
4803 None => (
4804 Fidelity::Semantic,
4805 vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4806 ),
4807 }
4808 };
4809 Ok(SessionArtifact {
4810 source_harness: locator.harness.clone(),
4811 target_harness: target.id(),
4812 session_id: target_session_id
4813 .map(str::to_string)
4814 .or_else(|| session.meta.session_id.clone()),
4815 content,
4816 suggested_filename,
4817 files,
4818 fidelity,
4819 residue,
4820 })
4821}
4822
4823fn append_grok_bundle_files(
4824 locator: &SessionLocator,
4825 prefix: &str,
4826 role: ArtifactFileRole,
4827 files: &mut Vec<SessionArtifactFile>,
4828) -> std::result::Result<(), ServiceError> {
4829 let primary = locator.storage.path();
4830 if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4831 return Err(ServiceError::Operation(format!(
4832 "Grok bundle locator must name chat_history.jsonl, got {}",
4833 primary.display()
4834 )));
4835 }
4836 let parent = primary.parent().ok_or_else(|| {
4837 ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4838 })?;
4839 for name in ["summary.json", "updates.jsonl"] {
4840 let path = parent.join(name);
4841 let metadata = match std::fs::symlink_metadata(&path) {
4842 Ok(metadata) => metadata,
4843 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4844 Err(error) => return Err(ServiceError::Operation(error.to_string())),
4845 };
4846 if metadata.file_type().is_symlink() || !metadata.is_file() {
4847 return Err(ServiceError::Operation(format!(
4848 "refusing non-regular Grok bundle member {}",
4849 path.display()
4850 )));
4851 }
4852 let content = std::fs::read_to_string(&path).map_err(|error| {
4853 ServiceError::Operation(format!(
4854 "Grok bundle member {} is not representable as UTF-8: {error}",
4855 path.display()
4856 ))
4857 })?;
4858 files.push(SessionArtifactFile {
4859 path: format!("{prefix}{name}"),
4860 content,
4861 role: match role {
4862 ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4863 _ => ArtifactFileRole::SourceRecovery,
4864 },
4865 });
4866 }
4867 Ok(())
4868}
4869
4870fn handoff_artifact(
4871 locator: &SessionLocator,
4872 session: &Session,
4873 target: TransferFormat,
4874) -> std::result::Result<SessionArtifact, ServiceError> {
4875 let target_session_id = target_session_id(target);
4876 session_artifact_with_id(locator, session, target, Some(&target_session_id))
4877}
4878
4879fn target_session_id(target: TransferFormat) -> String {
4880 let uuid = generated_session_id();
4881 match target {
4882 TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4883 TransferFormat::ClaudeCode
4884 | TransferFormat::Codex
4885 | TransferFormat::Pi
4886 | TransferFormat::Grok
4887 | TransferFormat::Gemini
4888 | TransferFormat::Goose
4889 | TransferFormat::Hermes => uuid,
4890 }
4891}
4892
4893fn sanitize_filename(value: &str) -> String {
4894 let value = value
4895 .chars()
4896 .map(|character| {
4897 if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4898 character
4899 } else {
4900 '-'
4901 }
4902 })
4903 .collect::<String>();
4904 let value = value.trim_matches('-');
4905 if value.is_empty() {
4906 "session".into()
4907 } else {
4908 value.chars().take(100).collect()
4909 }
4910}
4911
4912fn handoff_instructions(
4913 target: TransferFormat,
4914 session_id: &str,
4915 cwd: &Path,
4916) -> HandoffInstructions {
4917 let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4918 cwd: cwd.to_path_buf(),
4919 program: program.into(),
4920 arguments,
4921 env: BTreeMap::new(),
4922 };
4923 match target {
4924 TransferFormat::ClaudeCode => HandoffInstructions {
4925 launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4926 materialize: None,
4927 requires_materialization: true,
4928 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(),
4929 },
4930 TransferFormat::Hermes => HandoffInstructions {
4931 launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4932 materialize: None,
4933 requires_materialization: true,
4934 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(),
4935 },
4936 TransferFormat::Codex => HandoffInstructions {
4937 launch: launch("codex", vec!["resume".into(), session_id.into()]),
4938 materialize: None,
4939 requires_materialization: true,
4940 note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4941 },
4942 TransferFormat::OpenCode => HandoffInstructions {
4943 launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4944 materialize: Some(launch(
4945 "opencode",
4946 vec!["import".into(), "{artifact_path}".into()],
4947 )),
4948 requires_materialization: true,
4949 note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4950 },
4951 TransferFormat::Pi => HandoffInstructions {
4952 launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4953 materialize: None,
4954 requires_materialization: true,
4955 note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4956 },
4957 TransferFormat::Grok => HandoffInstructions {
4958 launch: launch(
4959 "grok",
4960 vec!["--resume".into(), "{materialized_session_id}".into()],
4961 ),
4962 materialize: None,
4963 requires_materialization: true,
4964 note: "Grok has no import command. Materialize the artifact through `harness.v1.sessions.materialize` (target `grok`, `value_lossless`, the destination cwd): it writes Grok's store entry (`chat_history.jsonl` and the `summary.json` `--resume` requires) under a fresh id; replace {materialized_session_id} with the id it returns.".into(),
4965 },
4966 TransferFormat::Gemini => HandoffInstructions {
4967 launch: launch(
4968 "gemini",
4969 vec!["--session-file".into(), "{artifact_path}".into()],
4970 ),
4971 materialize: None,
4972 requires_materialization: true,
4973 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(),
4974 },
4975 TransferFormat::Goose => HandoffInstructions {
4976 launch: launch(
4977 "goose",
4978 vec![
4979 "session".into(),
4980 "--resume".into(),
4981 "--session-id".into(),
4982 "{imported_session_id}".into(),
4983 ],
4984 ),
4985 materialize: Some(launch(
4986 "goose",
4987 vec!["session".into(), "import".into(), "{artifact_path}".into()],
4988 )),
4989 requires_materialization: true,
4990 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(),
4991 },
4992 }
4993}
4994
4995fn resume_launch(
4996 harness: &str,
4997 session_id: &str,
4998 cwd: &Path,
4999 policy: ResumePolicy,
5000) -> std::result::Result<StructuredLaunch, ServiceError> {
5001 let mut arguments = Vec::new();
5002 let program = match harness {
5003 HarnessId::GROK => {
5004 if matches!(policy, ResumePolicy::Yolo) {
5005 if crate::support::self_sandbox_supported() {
5006 arguments.extend(["--sandbox".into(), "workspace".into()]);
5007 }
5008 arguments.push("--always-approve".into());
5009 }
5010 arguments.extend(["--resume".into(), session_id.into()]);
5011 "grok"
5012 }
5013 HarnessId::CODEX => {
5014 let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
5015 .expect("a filesystem path always serializes as JSON text");
5016 arguments.extend([
5017 "-c".into(),
5018 "check_for_update_on_startup=false".into(),
5019 "-c".into(),
5020 format!("projects={{{cwd_key}={{trust_level=\"trusted\"}}}}"),
5023 ]);
5024 if matches!(policy, ResumePolicy::Yolo) {
5025 arguments.extend([
5026 "--dangerously-bypass-approvals-and-sandbox".into(),
5027 "--dangerously-bypass-hook-trust".into(),
5028 ]);
5029 }
5030 arguments.extend(["resume".into(), session_id.into()]);
5031 "codex"
5032 }
5033 HarnessId::CLAUDE_CODE => {
5034 if matches!(policy, ResumePolicy::Yolo) {
5035 arguments.push("--dangerously-skip-permissions".into());
5036 }
5037 arguments.extend(["--resume".into(), session_id.into()]);
5038 "claude"
5039 }
5040 HarnessId::GEMINI => {
5041 if matches!(policy, ResumePolicy::Yolo) {
5042 arguments.push("--yolo".into());
5043 }
5044 arguments.extend(["--resume".into(), session_id.into()]);
5045 "gemini"
5046 }
5047 HarnessId::GOOSE => {
5048 arguments.extend([
5049 "session".into(),
5050 "--resume".into(),
5051 "--session-id".into(),
5052 session_id.into(),
5053 ]);
5054 "goose"
5055 }
5056 HarnessId::PI => {
5057 if matches!(policy, ResumePolicy::Yolo) {
5058 arguments.push("--approve".into());
5059 }
5060 arguments.extend(["--session".into(), session_id.into()]);
5061 "pi"
5062 }
5063 HarnessId::OPENCODE => {
5064 arguments.extend(["--session".into(), session_id.into()]);
5065 "opencode"
5066 }
5067 HarnessId::SUPERCODE => {
5068 if matches!(policy, ResumePolicy::Yolo) {
5069 arguments.push("--dangerous".into());
5070 }
5071 arguments.extend(["resume".into(), session_id.into()]);
5072 "supercode"
5073 }
5074 other => {
5075 return Err(ServiceError::InvalidParams(format!(
5076 "no structured resume launch is registered for harness `{other}`"
5077 )))
5078 }
5079 };
5080 Ok(StructuredLaunch {
5081 cwd: cwd.to_path_buf(),
5082 env: if program == "grok" {
5083 crate::support::grok_home_env()
5084 } else {
5085 BTreeMap::new()
5086 },
5087 program: program.into(),
5088 arguments,
5089 })
5090}
5091
5092fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
5098 let digest = blake3::hash(address.as_bytes()).to_hex();
5099 let path = std::env::temp_dir().join(format!(
5100 "supercode-openclaw-gateway-token-{}",
5101 &digest.as_str()[..16]
5102 ));
5103 #[cfg(unix)]
5104 {
5105 use std::io::Write;
5106 use std::os::unix::fs::OpenOptionsExt;
5107 let mut file = std::fs::OpenOptions::new()
5108 .write(true)
5109 .create(true)
5110 .truncate(true)
5111 .mode(0o600)
5112 .open(&path)?;
5113 file.write_all(secret.as_bytes())?;
5114 }
5115 #[cfg(not(unix))]
5116 std::fs::write(&path, secret)?;
5117 Ok(path)
5118}
5119
5120fn open_connect_descriptor(
5126 descriptor: &crate::HarnessSupportDescriptor,
5127 home: &Path,
5128) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5129 let Some(connect) = &descriptor.runtime.connect_launch else {
5130 return Err(ServiceError::InvalidParams(format!(
5131 "harness `{}` has no registered connect-mode launch",
5132 descriptor.id.as_str()
5133 )));
5134 };
5135 let resolved = connect
5136 .resolve(home)
5137 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
5138 match (descriptor.id.as_str(), connect.protocol.as_str()) {
5139 (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
5140 let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
5141 if let Some(token) = resolved.auth {
5142 backend = backend.with_bearer(token);
5143 }
5144 Ok(Box::new(backend))
5145 }
5146 (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
5147 let mut env = BTreeMap::new();
5158 let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
5159 if let Some(token) = resolved.auth {
5160 let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
5161 .map_err(|error| {
5162 ServiceError::UnsupportedAction(format!(
5163 "could not stage the gateway credential for the bridge: {error}"
5164 ))
5165 })?;
5166 arguments.push("--token-file".into());
5167 arguments.push(token_path.to_string_lossy().into_owned());
5168 env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
5169 }
5170 let program = descriptor
5175 .runtime
5176 .default_launch
5177 .as_ref()
5178 .map(|launch| launch.program.clone())
5179 .unwrap_or_else(|| "openclaw".into());
5180 let launch = RuntimeLaunch {
5181 program,
5182 arguments,
5183 env,
5184 };
5185 Ok(Box::new(
5186 crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
5187 .with_resume_support(descriptor.runtime.capabilities.resume_session),
5188 ))
5189 }
5190 _ => Err(ServiceError::UnsupportedAction(format!(
5191 "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
5192 descriptor.id.as_str(),
5193 connect.protocol
5194 ))),
5195 }
5196}
5197
5198fn registry_connect_descriptor(
5201 params: &RuntimeBackendParams,
5202) -> Option<crate::HarnessSupportDescriptor> {
5203 if params.launch.is_some() || params.base_url.is_some() {
5204 return None;
5205 }
5206 harness_support_registry()
5207 .harnesses
5208 .into_iter()
5209 .find(|descriptor| descriptor.id == params.harness)
5210 .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
5211}
5212
5213fn service_home() -> std::result::Result<PathBuf, ServiceError> {
5214 supercode_interchange::user_home()
5215 .map(std::path::PathBuf::into_os_string)
5216 .map(PathBuf::from)
5217 .ok_or_else(|| {
5218 ServiceError::UnsupportedAction(
5219 "connect-mode launches need HOME to locate the harness config".into(),
5220 )
5221 })
5222}
5223
5224pub const RUNTIME_OPEN_METHODS: &[&str] = &[
5227 "harness.v1.runtimes.start",
5228 "harness.v1.runtimes.resume",
5229 "harness.v1.runtimes.attach",
5230 "harness.v1.runtimes.attach_existing",
5231];
5232
5233pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
5238
5239pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
5245
5246pub const DETACHED_METHODS: &[&str] = &[
5252 "harness.v1.harnesses.list",
5253 "harness.v1.harnesses.probe",
5254 "harness.v1.sessions.message",
5255 "harness.v1.sessions.new",
5256 "harness.v1.sessions.reset",
5257 "harness.v1.sessions.archive",
5258 "harness.v1.sessions.delete",
5259];
5260
5261pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
5267
5268pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
5276
5277async fn within_control_deadline<F: std::future::Future>(
5280 method: &str,
5281 call: F,
5282) -> std::result::Result<F::Output, ServiceError> {
5283 tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
5284 .await
5285 .map_err(|_| {
5286 ServiceError::Operation(format!(
5287 "`{method}` gave up after {}s: the runtime did not answer",
5288 RUNTIME_CONTROL_DEADLINE.as_secs()
5289 ))
5290 })
5291}
5292
5293pub struct RuntimeOpen {
5297 id: Value,
5298 method: String,
5299 params: Value,
5300}
5301
5302impl RuntimeOpen {
5303 pub async fn open(self) -> OpenedRuntime {
5307 let Self { id, method, params } = self;
5308 let outcome = open_runtime(&method, params).await;
5309 OpenedRuntime { id, outcome }
5310 }
5311}
5312
5313pub struct OpenedRuntime {
5316 id: Value,
5317 outcome: std::result::Result<OpenRuntime, ServiceError>,
5318}
5319
5320pub struct DetachedCall {
5325 id: Value,
5326 method: String,
5327 work: std::result::Result<Work, ServiceError>,
5328}
5329
5330impl DetachedCall {
5331 pub async fn run(self) -> DetachedAnswer {
5334 let Self { id, method, work } = self;
5335 match work {
5336 Ok(Work::Runtime(work)) => {
5341 let (result, returned) = work.run().await;
5342 DetachedAnswer {
5343 response: service_response(id, result),
5344 returned,
5345 }
5346 }
5347 Ok(Work::Free(work)) => {
5348 let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5349 Ok(result) => result,
5350 Err(_) => Err(ServiceError::Operation(format!(
5351 "`{method}` gave up after {}s: the harness it waits on did not answer",
5352 DETACHED_CALL_DEADLINE.as_secs()
5353 ))),
5354 };
5355 DetachedAnswer {
5356 response: service_response(id, result),
5357 returned: None,
5358 }
5359 }
5360 Err(error) => DetachedAnswer {
5361 response: service_response(id, Err(error)),
5362 returned: None,
5363 },
5364 }
5365 }
5366}
5367
5368pub struct DetachedAnswer {
5372 response: Value,
5373 returned: Option<ReturnedRuntime>,
5374}
5375
5376impl DetachedAnswer {
5377 pub fn into_response(self) -> Value {
5380 self.response
5381 }
5382}
5383
5384pub struct ReturnedRuntime {
5387 connection: String,
5388 runtime: Box<dyn RuntimeConnection>,
5389}
5390
5391enum Work {
5394 Free(DetachedWork),
5395 Runtime(RuntimeWork),
5396}
5397
5398enum DetachedWork {
5401 Inventory(InventoryWork),
5405 Message(MessageSessionParams),
5407 SessionMutation {
5410 verb: crate::SessionVerb,
5411 mutation: crate::SessionMutation,
5412 },
5413}
5414
5415impl DetachedWork {
5416 async fn run(self) -> std::result::Result<Value, ServiceError> {
5417 match self {
5418 Self::Inventory(work) => run_inventory(work).await,
5419 Self::Message(params) => Ok(message_live_session(¶ms).await),
5420 Self::SessionMutation { verb, mutation } => {
5421 let outcome = run_session_mutation(verb, &mutation).await?;
5422 serde_json::to_value(outcome)
5423 .map_err(|error| ServiceError::Operation(error.to_string()))
5424 }
5425 }
5426 }
5427}
5428
5429enum RuntimeWork {
5431 Close {
5433 runtime: Box<dyn RuntimeConnection>,
5434 process_group: Option<u32>,
5435 },
5436 LiveCommand {
5439 connection: String,
5440 runtime: Box<dyn RuntimeConnection>,
5441 verb: crate::SessionVerb,
5442 mutation: crate::SessionMutation,
5443 command: &'static str,
5444 session: String,
5445 },
5446}
5447
5448type RuntimeWorkAnswer = (
5451 std::result::Result<Value, ServiceError>,
5452 Option<ReturnedRuntime>,
5453);
5454
5455impl RuntimeWork {
5456 async fn run(self) -> RuntimeWorkAnswer {
5457 match self {
5458 Self::Close {
5459 runtime,
5460 process_group,
5461 } => (close_runtime(runtime, process_group).await, None),
5462 Self::LiveCommand {
5463 connection,
5464 mut runtime,
5465 verb,
5466 mutation,
5467 command,
5468 session,
5469 } => {
5470 let result =
5471 type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5472 (
5473 result,
5474 Some(ReturnedRuntime {
5475 connection,
5476 runtime,
5477 }),
5478 )
5479 }
5480 }
5481 }
5482}
5483
5484async fn close_runtime(
5487 mut runtime: Box<dyn RuntimeConnection>,
5488 process_group: Option<u32>,
5489) -> std::result::Result<Value, ServiceError> {
5490 match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5491 Ok(result) => {
5492 result.map_err(operation)?;
5493 Ok(json!({"closed": true}))
5494 }
5495 Err(deadline) => {
5496 let killed = kill_runtime_process_group(process_group);
5501 drop(runtime);
5502 Ok(json!({
5503 "closed": true,
5504 "killed": killed,
5505 "detail": error_message(deadline),
5506 }))
5507 }
5508 }
5509}
5510
5511fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5514 mutation
5515 .session
5516 .clone()
5517 .filter(|value| !value.trim().is_empty())
5518 .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5519}
5520
5521async fn type_live_command(
5525 runtime: &mut dyn RuntimeConnection,
5526 verb: crate::SessionVerb,
5527 mutation: &crate::SessionMutation,
5528 command: &str,
5529 session: String,
5530) -> std::result::Result<Value, ServiceError> {
5531 within_control_deadline(
5532 &format!("sessions.{}", verb.as_str()),
5533 runtime.send_input(RuntimeInput {
5534 text: command.to_string(),
5535 image_urls: Vec::new(),
5536 }),
5537 )
5538 .await?
5539 .map_err(operation)?;
5540 let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5541 .map_err(session_control_error)?;
5542 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5543}
5544
5545enum OpenRuntime {
5548 Hosted {
5551 runtime: Box<dyn RuntimeConnection>,
5552 capabilities: crate::RuntimeCapabilities,
5553 workspace: PathBuf,
5554 },
5555 Joined { runtime: Box<dyn RuntimeConnection> },
5558}
5559
5560async fn open_runtime(
5565 method: &str,
5566 params: Value,
5567) -> std::result::Result<OpenRuntime, ServiceError> {
5568 match tokio::time::timeout(
5569 RUNTIME_OPEN_DEADLINE,
5570 open_runtime_unbounded(method, params),
5571 )
5572 .await
5573 {
5574 Ok(result) => result,
5575 Err(_) => Err(ServiceError::Operation(format!(
5576 "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5577 RUNTIME_OPEN_DEADLINE.as_secs()
5578 ))),
5579 }
5580}
5581
5582async fn open_runtime_unbounded(
5583 method: &str,
5584 params: Value,
5585) -> std::result::Result<OpenRuntime, ServiceError> {
5586 match method {
5587 "harness.v1.runtimes.start" => {
5588 let params = decode::<RuntimeStartParams>(params)?;
5589 let backend = runtime_backend(¶ms.backend)?;
5590 let capabilities = backend.capabilities();
5591 let workspace = params.cwd.clone();
5592 let runtime = backend
5593 .start(RuntimeStartRequest {
5594 cwd: params.cwd,
5595 launch: runtime_launch(¶ms.backend),
5596 mcp_servers: params.mcp_servers,
5597 approval_policy: params.approval_policy,
5598 })
5599 .await
5600 .map_err(operation)?;
5601 Ok(OpenRuntime::Hosted {
5602 runtime,
5603 capabilities,
5604 workspace,
5605 })
5606 }
5607 "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5608 let params = decode::<RuntimeAttachParams>(params)?;
5609 let backend = runtime_backend(¶ms.backend)?;
5610 let capabilities = backend.capabilities();
5611 let workspace = params
5612 .cwd
5613 .clone()
5614 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5615 let runtime = backend
5616 .attach(RuntimeAttachRequest {
5617 runtime_id: params.runtime_id,
5618 cwd: params.cwd,
5619 launch: runtime_launch(¶ms.backend),
5620 mcp_servers: params.mcp_servers,
5621 approval_policy: params.approval_policy,
5622 })
5623 .await
5624 .map_err(operation)?;
5625 Ok(OpenRuntime::Hosted {
5626 runtime,
5627 capabilities,
5628 workspace,
5629 })
5630 }
5631 "harness.v1.runtimes.attach_existing" => {
5632 let params = decode::<RuntimeAttachParams>(params)?;
5633 let backend: Box<dyn RuntimeBackend> = match params
5634 .backend
5635 .base_url
5636 .as_deref()
5637 .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5638 {
5639 Some(endpoint) => {
5640 #[cfg(not(feature = "adapter-api"))]
5641 {
5642 let _ = endpoint;
5643 return Err(ServiceError::UnsupportedAction(
5644 "live HTTP attachment adapter is not compiled".into(),
5645 ));
5646 }
5647 #[cfg(feature = "adapter-api")]
5648 {
5649 let workspace = params.cwd.clone().ok_or_else(|| {
5650 ServiceError::InvalidParams(
5651 "Volter Harness live attach requires the project cwd".into(),
5652 )
5653 })?;
5654 let source = LiveRuntimeSource {
5655 harness: params.backend.harness.as_str().to_string(),
5656 session_id: params.runtime_id.clone(),
5657 workspace,
5658 };
5659 let receipt = resolve_live_runtime(&endpoint, &source)
5660 .map_err(|error| ServiceError::Operation(error.to_string()))?;
5661 Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5662 }
5663 }
5664 None => runtime_backend(¶ms.backend)?,
5665 };
5666 let capabilities = backend.capabilities();
5667 if !capabilities.attach_existing_process {
5668 return Err(ServiceError::Operation(format!(
5669 "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5670 backend.harness().as_str()
5671 )));
5672 }
5673 let runtime = backend
5674 .attach_existing(RuntimeAttachRequest {
5675 runtime_id: params.runtime_id,
5676 cwd: params.cwd,
5677 launch: runtime_launch(¶ms.backend),
5678 mcp_servers: params.mcp_servers,
5679 approval_policy: params.approval_policy,
5680 })
5681 .await
5682 .map_err(operation)?;
5683 Ok(OpenRuntime::Joined { runtime })
5684 }
5685 _ => Err(ServiceError::MethodNotFound),
5686 }
5687}
5688
5689fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5691 match result {
5692 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5693 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5694 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5695 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5696 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5697 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5698 }
5699}
5700
5701fn runtime_backend(
5702 params: &RuntimeBackendParams,
5703) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5704 if let Some(descriptor) = registry_connect_descriptor(params) {
5705 return open_connect_descriptor(&descriptor, &service_home()?);
5706 }
5707 if params.protocol.as_deref() == Some("acp") {
5708 let launch = params
5709 .launch
5710 .clone()
5711 .or_else(|| {
5712 harness_support_registry()
5713 .harnesses
5714 .into_iter()
5715 .find(|harness| harness.id == params.harness)
5716 .filter(|harness| {
5717 harness.runtime.implementation == ImplementationKind::GenericProtocol
5718 && harness.runtime.protocol.starts_with("acp")
5719 })
5720 .and_then(|harness| harness.runtime.default_launch)
5721 })
5722 .ok_or_else(|| {
5723 ServiceError::InvalidParams(
5724 "an ACP runtime requires `launch` unless the harness has a registered default"
5725 .into(),
5726 )
5727 })?;
5728 let resume_session = harness_support_registry()
5729 .harnesses
5730 .into_iter()
5731 .find(|harness| harness.id == params.harness)
5732 .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5733 return Ok(Box::new(
5734 AcpRuntimeBackend::new(params.harness.clone(), launch)
5735 .with_resume_support(resume_session),
5736 ));
5737 }
5738 let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5739 HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5740 HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5741 HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5742 HarnessId::OPENCODE => match ¶ms.base_url {
5743 Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5744 None => Box::new(OpenCodeRuntimeBackend::new()),
5745 },
5746 harness => {
5747 let descriptor = harness_support_registry()
5748 .harnesses
5749 .into_iter()
5750 .find(|descriptor| descriptor.id.as_str() == harness)
5751 .filter(|descriptor| {
5752 descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5753 && descriptor.runtime.protocol.starts_with("acp")
5754 });
5755 let Some(descriptor) = descriptor else {
5756 return Err(ServiceError::InvalidParams(format!(
5757 "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5758 )));
5759 };
5760 let resume = descriptor.runtime.capabilities.resume_session;
5761 Box::new(
5762 AcpRuntimeBackend::new(
5763 descriptor.id,
5764 descriptor
5765 .runtime
5766 .default_launch
5767 .expect("generic ACP registry entry includes its launch"),
5768 )
5769 .with_resume_support(resume),
5770 )
5771 }
5772 };
5773 Ok(backend)
5774}
5775
5776fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5777 if let Some(launch) = ¶ms.launch {
5778 return Some(launch.clone());
5779 }
5780 if !matches!(params.policy, RuntimePolicy::Yolo) {
5781 return None;
5782 }
5783 let launch = match params.harness.as_str() {
5784 HarnessId::GROK => RuntimeLaunch {
5785 program: "grok".into(),
5786 arguments: {
5787 let mut arguments: Vec<String> = Vec::new();
5788 if crate::support::self_sandbox_supported() {
5789 arguments.extend(["--sandbox".into(), "workspace".into()]);
5790 }
5791 arguments.extend([
5792 "--always-approve".into(),
5793 "agent".into(),
5794 "--no-leader".into(),
5795 "stdio".into(),
5796 ]);
5797 arguments
5798 },
5799 env: crate::support::grok_env(),
5800 },
5801 HarnessId::CODEX => RuntimeLaunch {
5802 program: "codex".into(),
5803 arguments: vec![
5804 "--dangerously-bypass-approvals-and-sandbox".into(),
5805 "--dangerously-bypass-hook-trust".into(),
5806 "app-server".into(),
5807 ],
5808 env: BTreeMap::new(),
5809 },
5810 HarnessId::CLAUDE_CODE => RuntimeLaunch {
5811 program: "claude".into(),
5812 arguments: vec![
5813 "--dangerously-skip-permissions".into(),
5814 "--print".into(),
5815 "--input-format".into(),
5816 "stream-json".into(),
5817 "--output-format".into(),
5818 "stream-json".into(),
5819 "--verbose".into(),
5820 ],
5821 env: BTreeMap::new(),
5822 },
5823 HarnessId::PI => RuntimeLaunch {
5824 program: "pi".into(),
5825 arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5826 env: BTreeMap::new(),
5827 },
5828 HarnessId::OPENCODE => RuntimeLaunch {
5829 program: "opencode".into(),
5830 arguments: vec!["serve".into()],
5831 env: BTreeMap::new(),
5832 },
5833 HarnessId::GEMINI => RuntimeLaunch {
5834 program: "gemini".into(),
5835 arguments: vec!["--acp".into(), "--yolo".into()],
5836 env: BTreeMap::new(),
5837 },
5838 HarnessId::GOOSE => RuntimeLaunch {
5839 program: "goose".into(),
5840 arguments: vec!["acp".into()],
5841 env: BTreeMap::new(),
5842 },
5843 HarnessId::SUPERCODE => RuntimeLaunch {
5844 program: "supercode".into(),
5845 arguments: vec!["acp".into(), "--dangerous".into()],
5846 env: BTreeMap::new(),
5847 },
5848 _ => return None,
5849 };
5850 Some(launch)
5851}
5852
5853struct IsolatedProbeHome {
5859 launch: RuntimeLaunch,
5860 root: PathBuf,
5861}
5862
5863impl IsolatedProbeHome {
5864 fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5865 let root = std::env::temp_dir().join(format!(
5866 "supercode-harness-probe-{harness}-{}",
5867 generated_session_id()
5868 ));
5869 std::fs::create_dir_all(&root)?;
5870 set_private_dir_permissions(&root)?;
5871
5872 if let Some(source_home) = supercode_interchange::user_home()
5873 .map(std::path::PathBuf::into_os_string)
5874 .map(PathBuf::from)
5875 {
5876 for relative in probe_auth_files(harness) {
5877 copy_probe_file(&source_home, &root, relative)?;
5878 }
5879 }
5880 if harness == HarnessId::SUPERCODE {
5885 let config_home = crate::agent::global_instructions_dir();
5886 for file in ["config.toml", "credentials.toml"] {
5887 copy_probe_path(
5888 &config_home.join(file),
5889 &root.join(".config/supercode").join(file),
5890 )?;
5891 }
5892 }
5893 configure_isolated_probe_auth(harness, &root)?;
5894
5895 let root_text = root.to_string_lossy().into_owned();
5896 for (key, value) in [
5897 ("HOME", root_text.clone()),
5898 (
5899 "XDG_CACHE_HOME",
5900 root.join(".cache").to_string_lossy().into_owned(),
5901 ),
5902 (
5903 "XDG_CONFIG_HOME",
5904 root.join(".config").to_string_lossy().into_owned(),
5905 ),
5906 (
5907 "XDG_DATA_HOME",
5908 root.join(".local/share").to_string_lossy().into_owned(),
5909 ),
5910 ] {
5911 launch.env.insert(key.into(), value);
5912 }
5913 let scoped = match harness {
5914 HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5915 HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5916 HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5917 HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5918 HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5919 HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5920 _ => None,
5921 };
5922 if let Some((key, value)) = scoped {
5923 launch
5924 .env
5925 .insert(key.into(), value.to_string_lossy().into_owned());
5926 }
5927 Ok(Self { launch, root })
5928 }
5929
5930 fn cleanup(&self) -> std::io::Result<()> {
5931 match std::fs::remove_dir_all(&self.root) {
5932 Ok(()) => Ok(()),
5933 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5934 Err(error) => Err(error),
5935 }
5936 }
5937}
5938
5939impl Drop for IsolatedProbeHome {
5940 fn drop(&mut self) {
5941 let _ = self.cleanup();
5942 }
5943}
5944
5945fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5946 match harness {
5947 HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5948 HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5952 HarnessId::CODEX => &[".codex/auth.json"],
5953 HarnessId::GEMINI => &[
5954 ".gemini/google_accounts.json",
5955 ".gemini/oauth_creds.json",
5956 ".gemini/settings.json",
5957 ],
5958 HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5959 HarnessId::OPENCODE => &[
5960 ".config/opencode/auth.json",
5961 ".local/share/opencode/auth.json",
5962 ],
5963 HarnessId::PI => &[".pi/agent/auth.json"],
5964 HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5969 _ => &[],
5970 }
5971}
5972
5973fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5974 copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5975}
5976
5977fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5978 if !source.is_file() {
5979 return Ok(());
5980 }
5981 if let Some(parent) = destination.parent() {
5982 std::fs::create_dir_all(parent)?;
5983 set_private_dir_permissions(parent)?;
5984 }
5985 std::fs::copy(source, destination)?;
5986 set_private_file_permissions(destination)
5987}
5988
5989fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5990 if harness != HarnessId::GEMINI {
5991 return Ok(());
5992 }
5993 let oauth = probe_home.join(".gemini/oauth_creds.json");
5994 if !oauth.is_file() {
5995 return Ok(());
5996 }
5997 let settings_path = probe_home.join(".gemini/settings.json");
5998 let mut settings = std::fs::read_to_string(&settings_path)
5999 .ok()
6000 .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
6001 .unwrap_or_else(|| json!({}));
6002 settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
6003 std::fs::write(
6004 &settings_path,
6005 serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
6006 )?;
6007 set_private_file_permissions(&settings_path)
6008}
6009
6010#[cfg(unix)]
6011fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
6012 use std::os::unix::fs::PermissionsExt;
6013 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
6014}
6015
6016#[cfg(not(unix))]
6017fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
6018 Ok(())
6019}
6020
6021#[cfg(unix)]
6022fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
6023 use std::os::unix::fs::PermissionsExt;
6024 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
6025}
6026
6027#[cfg(not(unix))]
6028fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
6029 Ok(())
6030}
6031
6032fn find_executable(program: &str) -> Option<PathBuf> {
6033 let candidate = PathBuf::from(program);
6034 if candidate.components().count() > 1 {
6035 return candidate.is_file().then_some(candidate);
6036 }
6037 let path = std::env::var_os("PATH")?;
6038 for directory in std::env::split_paths(&path) {
6039 let candidate = directory.join(program);
6040 if candidate.is_file() {
6041 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
6042 }
6043 #[cfg(windows)]
6044 {
6045 for extension in ["exe", "cmd", "bat"] {
6046 let candidate = directory.join(format!("{program}.{extension}"));
6047 if candidate.is_file() {
6048 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
6049 }
6050 }
6051 }
6052 }
6053 None
6054}
6055
6056async fn executable_version(executable: &Path) -> Option<String> {
6057 let mut command = tokio::process::Command::new(executable);
6058 command
6059 .arg("--version")
6060 .stdin(std::process::Stdio::null())
6061 .stdout(std::process::Stdio::piped())
6062 .stderr(std::process::Stdio::piped())
6063 .kill_on_drop(true);
6064 let output = tokio::time::timeout(Duration::from_secs(3), command.output())
6065 .await
6066 .ok()?
6067 .ok()?;
6068 let stdout = String::from_utf8_lossy(&output.stdout);
6069 let stderr = String::from_utf8_lossy(&output.stderr);
6070 stdout
6071 .lines()
6072 .chain(stderr.lines())
6073 .map(str::trim)
6074 .find(|line| !line.is_empty())
6075 .map(|line| truncate_text(line, 200))
6076}
6077
6078pub(crate) fn auth_evidence(harness: &str) -> bool {
6079 let env_names: &[&str] = match harness {
6080 HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
6081 HarnessId::CODEX => &["OPENAI_API_KEY"],
6082 HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
6083 HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
6084 HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
6085 HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
6086 HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
6087 _ => &[],
6088 };
6089 if env_names
6090 .iter()
6091 .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
6092 {
6093 return true;
6094 }
6095 let Some(home) = supercode_interchange::user_home()
6096 .map(std::path::PathBuf::into_os_string)
6097 .map(PathBuf::from)
6098 else {
6099 return false;
6100 };
6101 let files: Vec<PathBuf> = match harness {
6102 HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
6103 HarnessId::CODEX => vec![home.join(".codex/auth.json")],
6104 HarnessId::OPENCODE => vec![
6105 home.join(".local/share/opencode/auth.json"),
6106 home.join(".config/opencode/auth.json"),
6107 ],
6108 HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
6109 HarnessId::GROK => vec![home.join(".grok/auth.json")],
6110 HarnessId::GEMINI => vec![
6111 home.join(".gemini/oauth_creds.json"),
6112 home.join(".gemini/google_accounts.json"),
6113 ],
6114 HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
6115 HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
6116 _ => Vec::new(),
6117 };
6118 if files.into_iter().any(|path| {
6119 std::fs::metadata(path)
6120 .map(|metadata| metadata.is_file() && metadata.len() > 2)
6121 .unwrap_or(false)
6122 }) {
6123 return true;
6124 }
6125 if harness == HarnessId::CLAUDE_CODE {
6132 return std::fs::read_to_string(home.join(".claude.json"))
6133 .map(|text| text.contains("\"oauthAccount\""))
6134 .unwrap_or(false);
6135 }
6136 false
6137}
6138
6139fn looks_like_auth_error(message: &str) -> bool {
6140 let message = message.to_ascii_lowercase();
6141 [
6142 "auth",
6143 "login",
6144 "sign in",
6145 "sign-in",
6146 "credential",
6147 "unauthorized",
6148 "forbidden",
6149 "token",
6150 ]
6151 .iter()
6152 .any(|needle| message.contains(needle))
6153}
6154
6155fn unavailable_capabilities() -> crate::RuntimeCapabilities {
6156 crate::RuntimeCapabilities {
6157 start_session: false,
6158 resume_session: false,
6159 attach_existing_process: false,
6160 send_input: false,
6161 stream_events: false,
6162 interrupt: false,
6163 steer: false,
6164 respond_to_requests: false,
6165 }
6166}
6167
6168fn truncate_text(text: &str, max_chars: usize) -> String {
6169 let mut chars = text.chars();
6170 let truncated = chars.by_ref().take(max_chars).collect::<String>();
6171 if chars.next().is_some() {
6172 format!("{truncated}…")
6173 } else {
6174 truncated
6175 }
6176}
6177
6178fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
6185 match &handle.endpoint {
6186 crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
6187 crate::RuntimeEndpoint::Http { .. } => None,
6188 }
6189}
6190
6191fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
6197 match process_group {
6198 #[cfg(unix)]
6199 Some(pid) => {
6200 crate::lsp::kill_process_group(pid);
6201 true
6202 }
6203 #[cfg(not(unix))]
6204 Some(_) => false,
6205 None => false,
6206 }
6207}
6208
6209fn error_message(error: ServiceError) -> String {
6210 match error {
6211 ServiceError::InvalidParams(message)
6212 | ServiceError::Operation(message)
6213 | ServiceError::UnsupportedAction(message) => message,
6214 ServiceError::MethodNotFound => "runtime adapter is not available".into(),
6215 ServiceError::Sdk(error) => error.to_string(),
6216 }
6217}
6218
6219#[derive(Debug)]
6220enum ServiceError {
6221 InvalidParams(String),
6222 MethodNotFound,
6223 UnsupportedAction(String),
6224 Operation(String),
6225 Sdk(SdkError),
6226}
6227
6228fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
6229 match error {
6230 ServiceError::InvalidParams(message) => {
6231 SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
6232 }
6233 ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
6234 SdkError::unsupported(operation)
6235 }
6236 ServiceError::Operation(message) => {
6237 let code = if message.contains("already in progress") {
6238 SdkErrorCode::Busy
6239 } else if message.contains("not supported by this runtime") {
6240 SdkErrorCode::UnsupportedAction
6241 } else if message.contains("unknown runtime connection") {
6242 SdkErrorCode::NotFound
6243 } else {
6244 SdkErrorCode::Execution
6245 };
6246 SdkError::new(code, operation, message)
6247 }
6248 ServiceError::Sdk(error) => error,
6249 }
6250}
6251
6252fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
6253 let error_code = error.code();
6254 let code = match error_code {
6255 SdkErrorCode::Unauthenticated => -32030,
6256 SdkErrorCode::Unauthorized => -32031,
6257 SdkErrorCode::ControllerRequired => -32032,
6258 SdkErrorCode::LeaseExpired => -32033,
6259 SdkErrorCode::InvalidArgument => -32602,
6260 SdkErrorCode::NotFound => -32004,
6261 SdkErrorCode::Busy => -32000,
6262 SdkErrorCode::UnsupportedAction => -32020,
6263 SdkErrorCode::Execution => -32002,
6264 SdkErrorCode::Transport => -32003,
6265 };
6266 json!({
6267 "jsonrpc": "2.0",
6268 "id": id,
6269 "error": {
6270 "code": code,
6271 "name": error_code,
6272 "operation": error.operation(),
6273 "message": error.to_string(),
6274 },
6275 })
6276}
6277
6278fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
6279 serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
6280}
6281
6282fn operation(error: impl Into<crate::Error>) -> ServiceError {
6283 let error = error.into();
6284 match error {
6285 crate::Error::Sdk(error) => ServiceError::Sdk(error),
6286 error => ServiceError::Operation(error.to_string()),
6287 }
6288}
6289
6290#[derive(Debug, Clone, Deserialize, Default)]
6294#[serde(default)]
6295struct MemoryRequest {
6296 harness: Option<String>,
6298 query: Option<String>,
6300 profile: Option<String>,
6302 session: Option<String>,
6304 full: bool,
6306 regex: bool,
6308 cwd: Option<std::path::PathBuf>,
6310 homes: crate::HarnessHomes,
6312}
6313
6314fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6317 let request = decode::<MemoryRequest>(params)?;
6318 let harness = request
6319 .harness
6320 .clone()
6321 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6322 let to_service = |error: crate::memory::MemoryError| match error {
6323 crate::memory::MemoryError::UnsupportedHarness { .. }
6324 | crate::memory::MemoryError::SessionNotScoped { .. } => {
6325 ServiceError::UnsupportedAction(error.to_string())
6326 }
6327 other => ServiceError::InvalidParams(other.to_string()),
6328 };
6329 match method {
6330 "harness.v1.memory.show" => {
6331 let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6332 harness,
6333 profile: request.profile,
6334 session: request.session,
6335 full: request.full,
6336 cwd: request.cwd,
6337 homes: request.homes,
6338 })
6339 .map_err(to_service)?;
6340 Ok(json!({
6341 "schema": crate::memory::MEMORY_SCHEMA,
6342 "documents": documents,
6343 }))
6344 }
6345 "harness.v1.memory.search" => {
6346 let query = request
6347 .query
6348 .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6349 let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6350 harness,
6351 query,
6352 profile: request.profile,
6353 regex: request.regex,
6354 cwd: request.cwd,
6355 homes: request.homes,
6356 })
6357 .map_err(to_service)?;
6358 Ok(json!({
6359 "schema": crate::memory::MEMORY_SCHEMA,
6360 "matches": matches,
6361 }))
6362 }
6363 _ => Err(ServiceError::MethodNotFound),
6364 }
6365}
6366
6367#[derive(Debug, Clone, Deserialize)]
6371#[serde(default)]
6372struct ProfilesQuery {
6373 harness: Option<String>,
6375 name: Option<String>,
6377 homes: crate::HarnessHomes,
6379}
6380
6381impl Default for ProfilesQuery {
6382 fn default() -> Self {
6383 Self {
6384 harness: None,
6385 name: None,
6386 homes: crate::HarnessHomes::default(),
6387 }
6388 }
6389}
6390
6391fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6394 let query = decode::<ProfilesQuery>(params)?;
6395 let to_service = |error: crate::profiles::ProfileError| match error {
6396 crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6397 ServiceError::UnsupportedAction(error.to_string())
6398 }
6399 crate::profiles::ProfileError::NotFound { .. } => {
6400 ServiceError::InvalidParams(error.to_string())
6401 }
6402 };
6403 match method {
6404 "harness.v1.profiles.list" => {
6405 let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6406 .map_err(to_service)?;
6407 Ok(json!({
6408 "schema": crate::profiles::PROFILES_SCHEMA,
6409 "profiles": profiles,
6410 }))
6411 }
6412 "harness.v1.profiles.get" => {
6413 let harness = query
6414 .harness
6415 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6416 let name = query
6417 .name
6418 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6419 let profile =
6420 crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6421 Ok(json!({
6422 "schema": crate::profiles::PROFILES_SCHEMA,
6423 "profile": profile,
6424 }))
6425 }
6426 _ => Err(ServiceError::MethodNotFound),
6427 }
6428}
6429
6430#[derive(Debug, Clone, Deserialize)]
6434#[serde(default)]
6435struct ChannelsQuery {
6436 harness: Option<String>,
6438 name: Option<String>,
6440 homes: crate::HarnessHomes,
6442}
6443
6444impl Default for ChannelsQuery {
6445 fn default() -> Self {
6446 Self {
6447 harness: None,
6448 name: None,
6449 homes: crate::HarnessHomes::default(),
6450 }
6451 }
6452}
6453
6454#[derive(Debug, Clone, Deserialize)]
6458#[serde(default)]
6459struct RoutesQuery {
6460 harness: Option<String>,
6461 profile: Option<String>,
6463 homes: crate::HarnessHomes,
6464}
6465
6466impl Default for RoutesQuery {
6467 fn default() -> Self {
6468 Self {
6469 harness: None,
6470 profile: None,
6471 homes: crate::HarnessHomes::default(),
6472 }
6473 }
6474}
6475
6476#[derive(Debug, Clone, Deserialize)]
6477#[serde(default)]
6478struct TriggersQuery {
6479 harness: Option<String>,
6480 homes: crate::HarnessHomes,
6481}
6482
6483impl Default for TriggersQuery {
6484 fn default() -> Self {
6485 Self {
6486 harness: None,
6487 homes: crate::HarnessHomes::default(),
6488 }
6489 }
6490}
6491
6492fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6493 let query = decode::<TriggersQuery>(params)?;
6494 let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6495 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6496 Ok(json!({
6497 "schema": crate::triggers::TRIGGERS_SCHEMA,
6498 "triggers": triggers,
6499 }))
6500}
6501
6502fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6503 let query = decode::<RoutesQuery>(params)?;
6504 let routes = crate::routes::list_routes(
6505 &query.homes,
6506 query.harness.as_deref(),
6507 query.profile.as_deref(),
6508 )
6509 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6510 Ok(json!({
6511 "schema": crate::routes::ROUTES_SCHEMA,
6512 "routes": routes,
6513 }))
6514}
6515
6516fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6517 let query = decode::<ChannelsQuery>(params)?;
6518 let to_service = |error: crate::channels::ChannelError| match error {
6519 crate::channels::ChannelError::UnsupportedHarness { .. } => {
6520 ServiceError::UnsupportedAction(error.to_string())
6521 }
6522 crate::channels::ChannelError::NotFound { .. } => {
6523 ServiceError::InvalidParams(error.to_string())
6524 }
6525 };
6526 match method {
6527 "harness.v1.channels.list" => {
6528 let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6529 .map_err(to_service)?;
6530 Ok(json!({
6531 "schema": crate::channels::CHANNELS_SCHEMA,
6532 "channels": channels,
6533 }))
6534 }
6535 "harness.v1.channels.status" => {
6536 let harness = query
6537 .harness
6538 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6539 let name = query
6540 .name
6541 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6542 let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6543 .map_err(to_service)?;
6544 Ok(json!({
6545 "schema": crate::channels::CHANNELS_SCHEMA,
6546 "channel": channel,
6547 }))
6548 }
6549 _ => Err(ServiceError::MethodNotFound),
6550 }
6551}
6552
6553fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6554 json!({
6555 "jsonrpc": "2.0",
6556 "id": id,
6557 "error": {"code": code, "message": message},
6558 })
6559}
6560
6561#[cfg(test)]
6562mod tests {
6563 use super::*;
6564 use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6565 use async_trait::async_trait;
6566 use std::io::Write;
6567 use std::path::PathBuf;
6568 use std::time::Instant;
6569
6570 #[test]
6571 fn indexed_claude_descriptor_carries_its_delivery_door() {
6572 let descriptor = SessionDescriptor {
6573 locator: SessionLocator {
6574 harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6575 session_id: "live-session".into(),
6576 storage: StorageLocator::File {
6577 path: PathBuf::from("/tmp/live-session.jsonl"),
6578 },
6579 },
6580 cwd: Some(PathBuf::from("/project")),
6581 title: None,
6582 preview_candidates: Vec::new(),
6583 latest_message_candidates: Vec::new(),
6584 updated_at_ms: Some(1),
6585 message_count: None,
6586 model: None,
6587 parent_session_id: None,
6588 child_session_count: 0,
6589 nouns: Default::default(),
6590 };
6591 let doors =
6592 crate::mail_route::LiveSessions::with(&[("claude-code", "live-session", "native")]);
6593
6594 let value = live_descriptor_value(&descriptor, &doors).unwrap();
6595 assert_eq!(value["delivery"], "native");
6596 assert!(value.get("live_endpoint").is_none());
6597 }
6598
6599 struct EndingRuntime {
6600 handle: RuntimeHandle,
6601 event: Option<HarnessEvent>,
6602 close_failures: usize,
6603 }
6604
6605 #[async_trait]
6606 impl RuntimeConnection for EndingRuntime {
6607 fn handle(&self) -> &RuntimeHandle {
6608 &self.handle
6609 }
6610
6611 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6612 unreachable!("ending runtime does not accept input")
6613 }
6614
6615 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6616 Ok(self.event.take())
6617 }
6618
6619 async fn interrupt(&mut self) -> crate::Result<()> {
6620 Ok(())
6621 }
6622
6623 async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6624 Ok(())
6625 }
6626
6627 async fn close(&mut self) -> crate::Result<()> {
6628 if self.close_failures > 0 {
6629 self.close_failures -= 1;
6630 return Err(crate::Error::Other(
6631 "cleanup temporarily unavailable".into(),
6632 ));
6633 }
6634 Ok(())
6635 }
6636 }
6637
6638 fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6639 Box::new(EndingRuntime {
6640 handle: RuntimeHandle {
6641 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6642 runtime_id: "ending-session".into(),
6643 endpoint: RuntimeEndpoint::LocalProcess {
6644 pid: None,
6645 command: vec!["ending-runtime".into()],
6646 protocol: "test".into(),
6647 },
6648 },
6649 event,
6650 close_failures: 0,
6651 })
6652 }
6653
6654 #[tokio::test]
6655 async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6656 let mut service = HarnessSessionService::new();
6657 let handle = ending_runtime(None).handle().clone();
6658 let runtime_id = handle.runtime_id.clone();
6659 let opened = service
6660 .insert_runtime(Box::new(EndingRuntime {
6661 handle,
6662 event: None,
6663 close_failures: 1,
6664 }))
6665 .unwrap();
6666 let connection = opened["connection"].as_str().unwrap().to_string();
6667 service.terminal_launches.insert(
6668 connection.clone(),
6669 StructuredLaunch {
6670 cwd: PathBuf::from("/fixture"),
6671 program: "fixture".into(),
6672 arguments: Vec::new(),
6673 env: BTreeMap::new(),
6674 },
6675 );
6676 let first = service
6677 .handle_async(request(
6678 1,
6679 "harness.v1.runtimes.close",
6680 json!({"connection": connection}),
6681 ))
6682 .await;
6683 assert!(first.get("error").is_some(), "{first}");
6685 assert!(!service.runtimes.contains_key(&connection));
6690 assert!(!service.terminal_launches.contains_key(&connection));
6691 assert!(!service.runtime_sequences.contains_key(&runtime_id));
6692 let again = service
6693 .handle_async(request(
6694 2,
6695 "harness.v1.runtimes.close",
6696 json!({"connection": connection}),
6697 ))
6698 .await;
6699 assert_eq!(again["error"]["code"], -32602, "{again}");
6700 }
6701
6702 fn request(id: u64, method: &str, params: Value) -> Value {
6703 json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6704 }
6705
6706 fn hermes_store() -> PathBuf {
6709 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6710 }
6711
6712 fn hermes_discovery(params: Value) -> Value {
6716 let mut response =
6717 HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6718 let store = hermes_store().display().to_string();
6719 for session in response["result"]["sessions"]
6720 .as_array_mut()
6721 .expect("sessions array")
6722 {
6723 if session["locator"]["storage"]["path"] == json!(store) {
6724 session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6725 }
6726 session.as_object_mut().unwrap().remove("activity");
6730 }
6731 response["result"].take()
6732 }
6733
6734 fn hermes_query() -> Value {
6735 json!({
6736 "harnesses": ["hermes"],
6737 "homes": {"hermes": hermes_store()},
6738 })
6739 }
6740
6741 fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6742 result["sessions"]
6743 .as_array()
6744 .expect("sessions array")
6745 .iter()
6746 .find(|session| session["locator"]["session_id"] == json!(id))
6747 .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6748 }
6749
6750 #[test]
6751 fn orch6_discover_rows_carry_the_conversation_nouns() {
6752 let result = hermes_discovery(hermes_query());
6753
6754 let dm = row(&result, "tg-dm-1");
6757 assert_eq!(dm["trigger"], json!("channel"));
6758 assert_eq!(dm["surface"]["platform"], json!("telegram"));
6759 assert_eq!(dm["surface"]["kind"], json!("dm"));
6760 assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6761 assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6762 assert_eq!(
6763 dm["workspace"],
6764 json!({"kind": "channel", "value": "telegram:123456"})
6765 );
6766 assert!(dm.get("profile").is_none(), "{dm:#}");
6767
6768 let fire = row(&result, "cron_job42_20260902_120000");
6770 assert_eq!(fire["trigger"], json!("cron"));
6771 assert_eq!(
6772 fire["recurrence"],
6773 json!({"job_id": "job42", "kind": "cron"})
6774 );
6775 assert_eq!(fire["workspace"]["kind"], json!("repo"));
6776
6777 let coder = row(&result, "tg-coder-1");
6780 assert_eq!(coder["trigger"], json!("channel"));
6781 assert_eq!(coder["profile"], json!("coder"));
6782 assert_eq!(coder["surface"]["thread_id"], json!("55"));
6783 assert_eq!(
6784 coder["surface"]["key"],
6785 json!("agent:coder:telegram:group:-100777:55")
6786 );
6787 assert_eq!(
6788 coder["workspace"],
6789 json!({"kind": "repo", "value": "/workspace/project"})
6790 );
6791 assert_eq!(
6792 coder["cross_surface"],
6793 json!({"state": "pending", "platform": "discord"})
6794 );
6795
6796 let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6798 assert_eq!(acp["trigger"], json!("human"));
6799 assert!(acp.get("surface").is_none(), "{acp:#}");
6800 assert_eq!(acp["workspace"], json!({"kind": "none"}));
6801 }
6802
6803 #[test]
6804 fn orch6_discover_filters_by_harness_and_profile() {
6805 let mut params = hermes_query();
6806 params["profile"] = json!("coder");
6807 let result = hermes_discovery(params);
6808 let ids: Vec<&str> = result["sessions"]
6809 .as_array()
6810 .expect("sessions array")
6811 .iter()
6812 .map(|session| session["locator"]["session_id"].as_str().unwrap())
6813 .collect();
6814 assert_eq!(ids, vec!["tg-coder-1"]);
6815
6816 let mut missing = hermes_query();
6819 missing["profile"] = json!("nobody");
6820 assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6821
6822 let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6825 assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6826 }
6827
6828 #[test]
6829 fn orch6_load_reports_the_same_nouns_as_discovery() {
6830 let mut service = HarnessSessionService::new();
6831 let loaded = service.handle(request(
6832 1,
6833 "harness.v1.sessions.load",
6834 json!({"locator": {
6835 "harness": "hermes",
6836 "session_id": "tg-coder-1",
6837 "storage": {"kind": "file", "path": hermes_store()},
6838 }}),
6839 ));
6840 let session = &loaded["result"]["session"];
6841 let discovered = hermes_discovery(hermes_query());
6842 let row = row(&discovered, "tg-coder-1");
6843 for noun in [
6844 "trigger",
6845 "surface",
6846 "profile",
6847 "recurrence",
6848 "cross_surface",
6849 "workspace",
6850 ] {
6851 assert_eq!(
6852 session[noun],
6853 row.get(noun).cloned().unwrap_or(Value::Null),
6854 "`{noun}` disagrees between sessions.load and sessions.discover"
6855 );
6856 }
6857 }
6858
6859 fn profile_fixture_homes() -> Value {
6862 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6863 json!({
6864 "hermes": fixtures.join("hermes_home/state.db"),
6865 "openclaw": fixtures.join("openclaw_home"),
6866 })
6867 }
6868
6869 fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6870 response["result"]["profiles"]
6871 .as_array()
6872 .unwrap_or_else(|| panic!("no profiles array in {response}"))
6873 .iter()
6874 .find(|row| row["harness"] == harness && row["name"] == name)
6875 .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6876 }
6877
6878 #[test]
6883 fn profiles_list_reads_every_source_uniformly() {
6884 let mut service = HarnessSessionService::new();
6885 let response = service.handle(request(
6886 1,
6887 "harness.v1.profiles.list",
6888 json!({"homes": profile_fixture_homes()}),
6889 ));
6890 assert_eq!(
6891 response["result"]["schema"],
6892 crate::profiles::PROFILES_SCHEMA
6893 );
6894
6895 let default = profile_row(&response, "hermes", "default");
6896 assert_eq!(default["kind"], "hermes_profile");
6897 assert_eq!(default["default"], true);
6898 assert_eq!(default["routes"], 0);
6899 assert_eq!(default["sessions"], 11);
6900 assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6901
6902 let coder = profile_row(&response, "hermes", "coder");
6903 assert_eq!(coder["kind"], "hermes_profile");
6904 assert_eq!(coder["default"], false);
6905 assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6906 assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6907 assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6908 assert!(coder["home"]
6909 .as_str()
6910 .unwrap()
6911 .ends_with("hermes_home/profiles/coder"));
6912
6913 let main = profile_row(&response, "openclaw", "main");
6914 assert_eq!(main["kind"], "openclaw_agent");
6915 assert_eq!(main["default"], true);
6918 assert_eq!(main["routes"], 0);
6919 assert_eq!(main["sessions"], 4);
6920 assert_eq!(
6921 main["model"],
6922 Value::Null,
6923 "`agents.defaults.model` is an install default, not this agent's pin"
6924 );
6925
6926 let design = profile_row(&response, "openclaw", "design");
6927 assert_eq!(design["default"], false);
6928 assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6929 assert_eq!(design["sessions"], 0);
6930 assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6931
6932 let preset = profile_row(&response, "supercode", "supercode-default");
6933 assert_eq!(preset["kind"], "preset");
6934 assert_eq!(preset["default"], true);
6935 assert_eq!(preset["home"], Value::Null);
6936 assert_eq!(preset["routes"], Value::Null);
6937 }
6938
6939 #[test]
6942 fn profiles_list_reads_codex_profile_tables() {
6943 let codex_home = std::env::temp_dir().join(format!(
6944 "supercode-orch10-codex-{}-{}",
6945 std::process::id(),
6946 std::time::SystemTime::now()
6947 .duration_since(std::time::UNIX_EPOCH)
6948 .unwrap()
6949 .as_nanos()
6950 ));
6951 std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6952 std::fs::write(
6953 codex_home.join("config.toml"),
6954 "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6955 )
6956 .unwrap();
6957
6958 let mut service = HarnessSessionService::new();
6959 let response = service.handle(request(
6960 1,
6961 "harness.v1.profiles.list",
6962 json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6963 ));
6964 let rows = response["result"]["profiles"].as_array().unwrap();
6965 assert_eq!(rows.len(), 2, "{response}");
6966 let review = profile_row(&response, "codex", "review");
6967 assert_eq!(review["kind"], "codex_profile");
6968 assert_eq!(review["default"], true);
6969 assert_eq!(review["model"], "gpt-5.1-codex");
6970 assert_eq!(review["home"], Value::Null);
6971 assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6972
6973 let got = service.handle(request(
6974 2,
6975 "harness.v1.profiles.get",
6976 json!({
6977 "harness": "codex",
6978 "name": "fast",
6979 "homes": {"codex": codex_home.join("sessions")},
6980 }),
6981 ));
6982 assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6983 std::fs::remove_dir_all(&codex_home).ok();
6984 }
6985
6986 #[test]
6989 fn profiles_refuse_harnesses_without_the_concept() {
6990 let mut service = HarnessSessionService::new();
6991 let response = service.handle(request(
6992 1,
6993 "harness.v1.profiles.list",
6994 json!({"harness": "claude-code"}),
6995 ));
6996 assert_eq!(response["error"]["code"], -32020, "{response}");
6997
6998 let missing = service.handle(request(
6999 2,
7000 "harness.v1.profiles.get",
7001 json!({
7002 "harness": "hermes",
7003 "name": "no-such-profile",
7004 "homes": profile_fixture_homes(),
7005 }),
7006 ));
7007 assert_eq!(missing["error"]["code"], -32602, "{missing}");
7008 }
7009
7010 #[test]
7013 fn profiles_methods_are_advertised() {
7014 let mut service = HarnessSessionService::new();
7015 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
7016 let methods = response["result"]["methods"].as_array().unwrap();
7017 for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
7018 assert!(
7019 methods.iter().any(|entry| entry == method),
7020 "{method} is not advertised"
7021 );
7022 }
7023 }
7024
7025 fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
7030 response["result"]["channels"]
7031 .as_array()
7032 .unwrap_or_else(|| panic!("no channels array in {response}"))
7033 .iter()
7034 .find(|row| row["harness"] == harness && row["name"] == name)
7035 .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
7036 }
7037
7038 fn channels_list(harness: Option<&str>) -> Value {
7039 let mut params = json!({"homes": profile_fixture_homes()});
7040 if let Some(harness) = harness {
7041 params["harness"] = json!(harness);
7042 }
7043 HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
7044 }
7045
7046 #[test]
7050 fn channels_list_reads_both_gateway_harnesses_uniformly() {
7051 let response = channels_list(None);
7052 assert_eq!(
7053 response["result"]["schema"],
7054 crate::channels::CHANNELS_SCHEMA
7055 );
7056
7057 let telegram = channel_row(&response, "hermes", "telegram");
7060 assert_eq!(telegram["kind"], "telegram");
7061 assert_eq!(telegram["enabled"], true);
7062 assert_eq!(telegram["configured"], true);
7063 assert_eq!(telegram["sessions"], 2);
7067 let api = channel_row(&response, "hermes", "api_server");
7068 assert_eq!(api["configured"], true, "extra.key is a credential key");
7069 assert_eq!(api["sessions"], 0);
7070 let webhook = channel_row(&response, "hermes", "webhook");
7071 assert_eq!(webhook["enabled"], false);
7072 assert_eq!(webhook["configured"], true);
7075
7076 let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
7078 assert_eq!(linked["kind"], "slack");
7079 assert_eq!(linked["account"], "T0FIXTURE");
7080 assert_eq!(linked["enabled"], true);
7081 assert_eq!(linked["configured"], true);
7082 let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
7083 assert_eq!(unlinked["enabled"], false);
7084 assert_eq!(
7085 unlinked["configured"], false,
7086 "an account with no credential key is not configured"
7087 );
7088 let telegram = channel_row(&response, "openclaw", "telegram");
7091 assert_eq!(telegram["account"], "hermes-fixture-bot");
7092 assert_eq!(telegram["configured"], true);
7093
7094 for row in response["result"]["channels"].as_array().unwrap() {
7096 assert_eq!(row["status"], "unknown", "{row}");
7097 }
7098 }
7099
7100 #[test]
7104 fn channels_rows_never_carry_a_fixture_secret() {
7105 let secrets = [
7106 "FAKE-TOKEN-DO-NOT-EMIT",
7107 "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
7108 "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
7109 "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
7110 "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
7111 ];
7112 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7114 let raw = format!(
7115 "{}{}",
7116 std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
7117 std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
7118 );
7119 for secret in secrets {
7120 assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
7121 }
7122
7123 let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
7124 for secret in secrets {
7125 assert!(
7126 !emitted.contains(secret),
7127 "`{secret}` leaked into a channel row: {emitted}"
7128 );
7129 }
7130 for row in channels_list(None)["result"]["channels"]
7133 .as_array()
7134 .unwrap()
7135 {
7136 for key in row.as_object().unwrap().keys() {
7137 let key = key.to_ascii_lowercase();
7138 assert!(
7139 !["token", "key", "secret", "password", "credential"]
7140 .iter()
7141 .any(|marker| key.ends_with(marker)),
7142 "`{key}` is a credential-shaped field on a channel row"
7143 );
7144 }
7145 }
7146 }
7147
7148 #[test]
7150 fn channels_status_reads_one_row_by_name() {
7151 let mut service = HarnessSessionService::new();
7152 let got = service.handle(request(
7153 1,
7154 "harness.v1.channels.status",
7155 json!({
7156 "harness": "openclaw",
7157 "name": "slack/T0FIXTURE",
7158 "homes": profile_fixture_homes(),
7159 }),
7160 ));
7161 assert_eq!(got["result"]["channel"]["kind"], "slack");
7162 assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
7163 assert_eq!(got["result"]["channel"]["status"], "unknown");
7164
7165 let missing = service.handle(request(
7166 2,
7167 "harness.v1.channels.status",
7168 json!({
7169 "harness": "openclaw",
7170 "name": "no-such-channel",
7171 "homes": profile_fixture_homes(),
7172 }),
7173 ));
7174 assert_eq!(missing["error"]["code"], -32602, "{missing}");
7175 }
7176
7177 #[test]
7181 fn channels_refuse_harnesses_without_the_concept() {
7182 let response = channels_list(Some("claude-code"));
7183 assert_eq!(response["error"]["code"], -32020, "{response}");
7184 let codex = channels_list(Some("codex"));
7185 assert_eq!(codex["error"]["code"], -32020, "{codex}");
7186 }
7187
7188 #[test]
7190 fn channels_list_filters_by_harness() {
7191 let response = channels_list(Some("openclaw"));
7192 let rows = response["result"]["channels"].as_array().unwrap();
7193 assert!(!rows.is_empty(), "{response}");
7194 assert!(
7195 rows.iter().all(|row| row["harness"] == "openclaw"),
7196 "harness filter leaked: {response}"
7197 );
7198 }
7199
7200 #[test]
7203 fn channels_methods_are_advertised() {
7204 let mut service = HarnessSessionService::new();
7205 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
7206 let methods = response["result"]["methods"].as_array().unwrap();
7207 for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
7208 assert!(
7209 methods.iter().any(|entry| entry == method),
7210 "{method} is not advertised"
7211 );
7212 }
7213 }
7214
7215 #[test]
7220 fn orch6_story_fixture_matches_the_live_discovery_response() {
7221 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7222 .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
7223 let mut result = hermes_discovery(hermes_query());
7224 result.as_object_mut().unwrap().remove("next_cursor");
7228 let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
7229 if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
7230 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
7231 std::fs::write(&path, &rendered).unwrap();
7232 }
7233 let committed = std::fs::read_to_string(&path).unwrap_or_default();
7234 assert_eq!(
7235 committed, rendered,
7236 "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
7237 re-run with SUPERCODE_UPDATE_FIXTURES=1"
7238 );
7239 }
7240
7241 fn pi_locator() -> SessionLocator {
7242 SessionLocator {
7243 harness: HarnessId::from(HarnessId::PI),
7244 session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
7245 storage: StorageLocator::File {
7246 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7247 .join("tests/fixtures/pi_session.jsonl"),
7248 },
7249 }
7250 }
7251
7252 fn opencode_locator() -> SessionLocator {
7253 let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
7254 SessionLocator {
7255 harness: HarnessId::from(HarnessId::OPENCODE),
7256 session_id: session_id.into(),
7257 storage: StorageLocator::Sqlite {
7258 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7259 .join("tests/fixtures/opencode_fixture/opencode.db"),
7260 selector: session_id.into(),
7261 },
7262 }
7263 }
7264
7265 fn grok_locator() -> SessionLocator {
7266 SessionLocator {
7267 harness: HarnessId::from(HarnessId::GROK),
7268 session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
7269 storage: StorageLocator::File {
7270 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7271 .join("tests/fixtures/grok_session/chat_history.jsonl"),
7272 },
7273 }
7274 }
7275
7276 fn fixture_homes() -> Value {
7279 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7280 json!({
7281 "claude_code": fixtures.join("__absent__"),
7282 "codex": fixtures.join("__absent__"),
7283 "opencode": fixtures.join("__absent__"),
7284 "pi": fixtures.join("__absent__"),
7285 "agents": fixtures.join("__absent__"),
7286 "hermes": fixtures.join("hermes_home"),
7287 "openclaw": fixtures.join("openclaw_home"),
7288 })
7289 }
7290
7291 #[test]
7292 fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
7293 let root = std::env::temp_dir().join(format!(
7294 "supercode-preview-rpc-{}-{}",
7295 std::process::id(),
7296 std::time::SystemTime::now()
7297 .duration_since(std::time::UNIX_EPOCH)
7298 .unwrap()
7299 .as_nanos()
7300 ));
7301 std::fs::create_dir_all(&root).unwrap();
7302 for id in ["first", "second"] {
7303 std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7304 json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7305 json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7306 )).unwrap();
7307 }
7308 let mut service = HarnessSessionService::new();
7309 let query = json!({
7310 "harnesses": ["codex"], "homes": {"codex": root},
7311 "query": "nebula", "search_previews": true, "limit": 1
7312 });
7313 let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7314 assert!(first.get("error").is_none(), "{first}");
7315 assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7316 assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7317 let mut next_query = query.clone();
7318 next_query["cursor"] = first["result"]["next_cursor"].clone();
7319 let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7320 assert_eq!(next["result"]["receipt"]["returned"], 1);
7321 assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7322 assert_eq!(next["result"]["receipt"]["truncated"], false);
7323 assert_ne!(
7324 first["result"]["sessions"][0]["locator"],
7325 next["result"]["sessions"][0]["locator"]
7326 );
7327 let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7328 assert!(
7329 refused["error"]["message"]
7330 .as_str()
7331 .unwrap()
7332 .contains("use sessions.discover"),
7333 "{refused}"
7334 );
7335 std::fs::remove_dir_all(root).unwrap();
7336 }
7337
7338 #[test]
7339 fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7340 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7341 let root = std::env::temp_dir().join(format!(
7342 "supercode-index-rpc-{}-{}",
7343 std::process::id(),
7344 std::time::SystemTime::now()
7345 .duration_since(std::time::UNIX_EPOCH)
7346 .unwrap()
7347 .as_nanos()
7348 ));
7349 std::fs::create_dir_all(&root).unwrap();
7350 for id in ["first", "second"] {
7351 std::fs::write(root.join(format!("{id}.jsonl")), format!(
7352 "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7353 )).unwrap();
7354 }
7355 let mut service = HarnessSessionService::new();
7356 let opened = service.handle(request(
7357 1,
7358 "harness.v1.sessions.index.subscribe",
7359 json!({
7360 "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7361 }),
7362 ));
7363 assert!(opened.get("error").is_none(), "{opened:#}");
7364 let subscription = opened["result"]["subscription"]
7365 .as_str()
7366 .unwrap()
7367 .to_owned();
7368 assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7369 for params in [
7370 json!({"subscription": subscription, "limit": 0}),
7371 json!({"subscription": subscription, "limit": 2049}),
7372 json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7373 json!({"subscription": "unknown", "limit": 2}),
7374 ] {
7375 let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7376 assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7377 }
7378 for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7379 let response = service.handle(request(
7380 3,
7381 "harness.v1.sessions.index.resize",
7382 json!({
7383 "subscription": subscription, "limit": limit
7384 }),
7385 ));
7386 assert!(response.get("error").is_none(), "{response:#}");
7387 assert_eq!(response["result"]["subscription"], subscription);
7388 assert_eq!(response["result"]["revision"], revision);
7389 assert_eq!(
7390 response["result"]["initial"].as_array().unwrap().len(),
7391 limit
7392 );
7393 assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7394 assert_eq!(service.index_subscriptions.len(), 1);
7395 }
7396 let removed = service.handle(request(
7397 4,
7398 "harness.v1.sessions.index.unsubscribe",
7399 json!({
7400 "subscription": subscription
7401 }),
7402 ));
7403 assert_eq!(removed["result"]["removed"], true);
7404 let stale = service.handle(request(
7405 5,
7406 "harness.v1.sessions.index.resize",
7407 json!({
7408 "subscription": subscription, "limit": 1
7409 }),
7410 ));
7411 assert_eq!(stale["error"]["code"], -32602);
7412 drop(service);
7413 std::fs::remove_dir_all(root).unwrap();
7414 }
7415
7416 fn skills_rows(params: Value) -> Vec<Value> {
7417 let response =
7418 HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7419 assert!(response.get("error").is_none(), "{response:#}");
7420 response["result"].as_array().cloned().unwrap_or_default()
7421 }
7422
7423 #[test]
7426 fn skills_list_reads_the_hermes_and_openclaw_roots() {
7427 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7428 let rows = skills_rows(json!({
7429 "homes": fixture_homes(),
7430 "cwd": fixtures.join("hermes_home"),
7431 }));
7432 let arxiv = rows
7433 .iter()
7434 .find(|row| row["name"] == json!("arxiv-search"))
7435 .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7436 assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7437 assert_eq!(arxiv["scope"], json!("user"));
7438 assert_eq!(arxiv["version"], json!("1.4.0"));
7439 assert!(arxiv["location"]
7440 .as_str()
7441 .unwrap()
7442 .ends_with("hermes_home/skills/research/arxiv"));
7443
7444 let bare = rows
7446 .iter()
7447 .find(|row| row["name"] == json!("bare-skill"))
7448 .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7449 assert_eq!(bare["enabled"], json!(null));
7450 assert!(bare.get("description").is_none());
7451
7452 let demo = rows
7453 .iter()
7454 .find(|row| row["name"] == json!("clawhub-demo"))
7455 .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7456 assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7457 assert_eq!(demo["scope"], json!("managed"));
7458 assert_eq!(demo["enabled"], json!(false));
7459 }
7460
7461 #[test]
7463 fn skills_list_filters_by_harness_and_scope() {
7464 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7465 let hermes = skills_rows(json!({
7466 "homes": fixture_homes(),
7467 "cwd": fixtures.join("hermes_home"),
7468 "harness": HarnessId::HERMES,
7469 }));
7470 assert!(!hermes.is_empty());
7471 assert!(hermes
7472 .iter()
7473 .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7474
7475 let managed = skills_rows(json!({
7476 "homes": fixture_homes(),
7477 "cwd": fixtures.join("openclaw_home"),
7478 "harness": HarnessId::OPENCLAW,
7479 "scope": "managed",
7480 }));
7481 assert_eq!(managed.len(), 1, "{managed:#?}");
7482 assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7483
7484 let bundled = skills_rows(json!({
7485 "homes": fixture_homes(),
7486 "cwd": fixtures.join("openclaw_home"),
7487 "harness": HarnessId::OPENCLAW,
7488 "scope": "bundled",
7489 }));
7490 assert!(bundled.is_empty(), "{bundled:#?}");
7491 }
7492
7493 #[test]
7496 fn skills_list_refuses_an_unknown_harness() {
7497 let response = HarnessSessionService::new().handle(request(
7498 1,
7499 "harness.v1.skills.list",
7500 json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7501 ));
7502 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7503 assert!(response["error"]["message"]
7504 .as_str()
7505 .unwrap()
7506 .contains("not-a-harness"));
7507 }
7508
7509 #[test]
7511 fn skills_list_is_an_advertised_method_and_sdk_operation() {
7512 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7513 assert_eq!(
7514 SdkOperation::from_method("harness.v1.skills.list"),
7515 Some(SdkOperation::SkillsList)
7516 );
7517 }
7518
7519 #[test]
7523 fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7524 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7525 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7526 assert_eq!(
7527 SdkOperation::from_method("harness.v1.skills.install"),
7528 Some(SdkOperation::SkillsInstall)
7529 );
7530 assert_eq!(
7531 SdkOperation::from_method("harness.v1.skills.remove"),
7532 Some(SdkOperation::SkillsRemove)
7533 );
7534 }
7535
7536 #[test]
7540 fn skills_install_and_remove_drive_the_directory_door() {
7541 let root = std::env::temp_dir().join(format!(
7542 "supercode-orch22-rpc-{}-{}",
7543 std::process::id(),
7544 std::time::SystemTime::now()
7545 .duration_since(std::time::UNIX_EPOCH)
7546 .unwrap()
7547 .as_nanos()
7548 ));
7549 let source = root.join("probe-src");
7550 std::fs::create_dir_all(&source).unwrap();
7551 std::fs::write(
7552 source.join("SKILL.md"),
7553 "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7554 )
7555 .unwrap();
7556 let homes = json!({
7557 "claude_code": root.join("claude_home"),
7558 "codex": root.join("__absent__"),
7559 "opencode": root.join("__absent__"),
7560 "pi": root.join("__absent__"),
7561 "hermes": root.join("__absent__"),
7562 "openclaw": root.join("__absent__"),
7563 "agents": root.join("__absent__"),
7564 });
7565
7566 let mut service = HarnessSessionService::new();
7567 let installed = service.handle(request(
7568 1,
7569 "harness.v1.skills.install",
7570 json!({
7571 "harness": HarnessId::CLAUDE_CODE,
7572 "source": source,
7573 "scope": "user",
7574 "cwd": root,
7575 "homes": homes,
7576 }),
7577 ));
7578 let result = &installed["result"];
7579 assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7580 assert_eq!(result["verb"], json!("install"));
7581 assert!(result["ran"]
7582 .as_str()
7583 .is_some_and(|ran| ran.starts_with("cp -R ")));
7584 assert_eq!(result["skill"]["scope"], json!("user"));
7585
7586 let removed = service.handle(request(
7587 2,
7588 "harness.v1.skills.remove",
7589 json!({
7590 "harness": HarnessId::CLAUDE_CODE,
7591 "name": "orch22-rpc",
7592 "scope": "user",
7593 "cwd": root,
7594 "homes": homes,
7595 }),
7596 ));
7597 assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7598 assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7599 std::fs::remove_dir_all(&root).ok();
7600 }
7601
7602 #[test]
7605 fn skills_remove_refuses_openclaw_at_the_pin() {
7606 let response = HarnessSessionService::new().handle(request(
7607 1,
7608 "harness.v1.skills.remove",
7609 json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7610 ));
7611 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7612 assert!(response["error"]["message"]
7613 .as_str()
7614 .unwrap()
7615 .contains("no `skills remove` verb"));
7616 }
7617
7618 #[test]
7621 fn skills_install_refuses_a_harness_without_a_skills_root() {
7622 let response = HarnessSessionService::new().handle(request(
7623 1,
7624 "harness.v1.skills.install",
7625 json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7626 ));
7627 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7628 assert!(response["error"]["message"]
7629 .as_str()
7630 .unwrap()
7631 .contains("not-a-harness"));
7632 }
7633
7634 fn memory_homes() -> Value {
7642 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7643 json!({
7644 "claude_code": fixtures.join("__absent__"),
7645 "codex": fixtures.join("__absent__"),
7646 "opencode": fixtures.join("__absent__"),
7647 "pi": fixtures.join("__absent__"),
7648 "grok": fixtures.join("__absent__"),
7649 "gemini": fixtures.join("__absent__"),
7650 "goose": fixtures.join("__absent__"),
7651 "supercode": fixtures.join("__absent__"),
7652 "hermes": fixtures.join("hermes_home/state.db"),
7653 "openclaw": fixtures.join("openclaw_home"),
7654 })
7655 }
7656
7657 fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7658 let response = HarnessSessionService::new().handle(request(1, method, params));
7659 assert!(response.get("error").is_none(), "{response:#}");
7660 assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7661 response["result"][key]
7662 .as_array()
7663 .cloned()
7664 .unwrap_or_default()
7665 }
7666
7667 fn memory_documents(params: Value) -> Vec<Value> {
7668 memory_call_ok("harness.v1.memory.show", params, "documents")
7669 }
7670
7671 fn memory_matches(params: Value) -> Vec<Value> {
7672 memory_call_ok("harness.v1.memory.search", params, "matches")
7673 }
7674
7675 fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7676 rows.iter()
7677 .find(|row| row["profile"] == profile && row["name"] == name)
7678 .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7679 }
7680
7681 #[test]
7684 fn memory_show_reads_the_hermes_profile_homes() {
7685 let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7686
7687 let notes = find_document(&rows, "default", "MEMORY.md");
7688 assert_eq!(notes["harness"], "hermes");
7689 assert_eq!(notes["scope"], "user");
7690 assert!(notes["size"].as_u64().unwrap() > 0);
7691 assert!(notes["updated_at"].is_string(), "{notes:#?}");
7692 assert!(notes.get("content").is_none(), "{notes:#?}");
7694 assert_eq!(notes["truncated"], true);
7695 assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7696
7697 let user = find_document(&rows, "default", "USER.md");
7698 assert_eq!(user["scope"], "user");
7699 assert!(user["preview"]
7700 .as_array()
7701 .unwrap()
7702 .iter()
7703 .any(|line| line.as_str().unwrap().contains("neovim")));
7704
7705 let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7706 assert!(topic["path"]
7707 .as_str()
7708 .unwrap()
7709 .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7710
7711 let coder = find_document(&rows, "coder", "MEMORY.md");
7713 assert_eq!(coder["scope"], "profile");
7714 assert!(coder["path"]
7715 .as_str()
7716 .unwrap()
7717 .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7718 }
7719
7720 #[test]
7723 fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7724 let rows = memory_documents(json!({
7725 "harness": "hermes",
7726 "profile": "coder",
7727 "full": true,
7728 "homes": memory_homes(),
7729 }));
7730 assert!(
7731 rows.iter().all(|row| row["profile"] == "coder"),
7732 "{rows:#?}"
7733 );
7734 let coder = find_document(&rows, "coder", "MEMORY.md");
7735 assert!(coder["content"]
7736 .as_str()
7737 .expect("full returns the body")
7738 .contains("anthropic/claude-opus-4-8"));
7739 }
7740
7741 #[test]
7745 fn memory_show_reads_the_openclaw_agent_workspaces() {
7746 let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7747
7748 let main = find_document(&rows, "main", "MEMORY.md");
7749 assert_eq!(main["scope"], "agent");
7750 assert!(main["path"]
7751 .as_str()
7752 .unwrap()
7753 .ends_with("openclaw_home/workspace/MEMORY.md"));
7754
7755 let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7756 assert!(topic["path"]
7757 .as_str()
7758 .unwrap()
7759 .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7760
7761 let design = find_document(&rows, "design", "MEMORY.md");
7762 assert!(design["path"]
7763 .as_str()
7764 .unwrap()
7765 .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7766 }
7767
7768 #[test]
7771 fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7772 let scratch = std::env::temp_dir().join(format!(
7773 "supercode-orch12-cc-{}-{}",
7774 std::process::id(),
7775 std::time::SystemTime::now()
7776 .duration_since(std::time::UNIX_EPOCH)
7777 .unwrap()
7778 .as_nanos()
7779 ));
7780 let project = scratch.join("repo");
7781 std::fs::create_dir_all(project.join(".git")).unwrap();
7782 let worktree = project.join("crates/harness");
7785 std::fs::create_dir_all(&worktree).unwrap();
7786 let slug: String = project
7787 .to_string_lossy()
7788 .chars()
7789 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7790 .collect();
7791 let projects = scratch.join("claude/projects");
7792 let memory = projects.join(&slug).join("memory");
7793 std::fs::create_dir_all(&memory).unwrap();
7794 std::fs::write(
7795 memory.join("MEMORY.md"),
7796 "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7797 )
7798 .unwrap();
7799 std::fs::write(
7800 memory.join("build-box.md"),
7801 "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7802 )
7803 .unwrap();
7804
7805 let mut homes = memory_homes();
7806 homes["claude_code"] = json!(projects);
7807 let rows = memory_documents(json!({
7808 "harness": "claude-code",
7809 "cwd": worktree,
7810 "homes": homes,
7811 }));
7812 let index = find_document(&rows, &slug, "MEMORY.md");
7813 assert_eq!(index["harness"], "claude-code");
7814 assert_eq!(index["scope"], "project");
7815 let topic = find_document(&rows, &slug, "build-box.md");
7816 assert!(topic["preview"]
7817 .as_array()
7818 .unwrap()
7819 .iter()
7820 .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7821
7822 let hits = memory_matches(json!({
7823 "harness": "claude-code",
7824 "query": "pinned harnesses",
7825 "cwd": worktree,
7826 "homes": homes,
7827 }));
7828 assert_eq!(hits.len(), 1, "{hits:#?}");
7829 assert_eq!(hits[0]["name"], "MEMORY.md");
7830 assert_eq!(hits[0]["line"], 2);
7831
7832 let _ = std::fs::remove_dir_all(&scratch);
7833 }
7834
7835 #[test]
7841 fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7842 let state = std::env::temp_dir().join(format!(
7843 "supercode-orch12-oc-{}-{}",
7844 std::process::id(),
7845 std::time::SystemTime::now()
7846 .duration_since(std::time::UNIX_EPOCH)
7847 .unwrap()
7848 .as_nanos()
7849 ));
7850 std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7852 std::fs::create_dir_all(state.join("workspace")).unwrap();
7853 std::fs::write(
7854 state.join("workspace/MEMORY.md"),
7855 "the gateway websocket needs credentials\n",
7856 )
7857 .unwrap();
7858
7859 let mut homes = memory_homes();
7860 homes["openclaw"] = json!(state);
7861 let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7862 assert_eq!(rows.len(), 1, "{rows:#?}");
7863 let row = find_document(&rows, "main", "MEMORY.md");
7864 assert_eq!(row["scope"], "agent");
7865 assert!(row["path"]
7866 .as_str()
7867 .unwrap()
7868 .ends_with("workspace/MEMORY.md"));
7869
7870 let _ = std::fs::remove_dir_all(&state);
7871 }
7872
7873 #[test]
7876 fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7877 let hit = memory_matches(json!({
7878 "harness": "hermes",
7879 "query": "NEOVIM",
7880 "homes": memory_homes(),
7881 }));
7882 assert_eq!(hit.len(), 1, "{hit:#?}");
7883 assert_eq!(hit[0]["harness"], "hermes");
7884 assert_eq!(hit[0]["name"], "USER.md");
7885 assert_eq!(hit[0]["scope"], "user");
7886 assert_eq!(hit[0]["line"], 5);
7887 assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7888
7889 let regex = memory_matches(json!({
7891 "harness": "hermes",
7892 "query": "neo(vim|vi)",
7893 "regex": true,
7894 "homes": memory_homes(),
7895 }));
7896 assert_eq!(regex.len(), 1, "{regex:#?}");
7897
7898 let miss = memory_matches(json!({
7899 "harness": "hermes",
7900 "query": "no-memory-line-says-this",
7901 "homes": memory_homes(),
7902 }));
7903 assert!(miss.is_empty(), "{miss:#?}");
7904 }
7905
7906 #[test]
7909 fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7910 for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7911 let response = HarnessSessionService::new().handle(request(
7912 1,
7913 method,
7914 json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7915 ));
7916 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7917 assert!(response["error"]["message"]
7918 .as_str()
7919 .unwrap()
7920 .contains("codex"));
7921 }
7922
7923 let response = HarnessSessionService::new().handle(request(
7924 1,
7925 "harness.v1.memory.show",
7926 json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7927 ));
7928 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7929
7930 let response = HarnessSessionService::new().handle(request(
7932 1,
7933 "harness.v1.memory.show",
7934 json!({"homes": memory_homes()}),
7935 ));
7936 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7937 }
7938
7939 #[test]
7941 fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7942 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7943 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7944 assert_eq!(
7945 SdkOperation::from_method("harness.v1.memory.show"),
7946 Some(SdkOperation::MemoryShow)
7947 );
7948 assert_eq!(
7949 SdkOperation::from_method("harness.v1.memory.search"),
7950 Some(SdkOperation::MemorySearch)
7951 );
7952 }
7953
7954 struct RequestingRuntime {
7959 handle: RuntimeHandle,
7960 events: std::collections::VecDeque<HarnessEvent>,
7961 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7962 }
7963
7964 #[async_trait]
7965 impl RuntimeConnection for RequestingRuntime {
7966 fn handle(&self) -> &RuntimeHandle {
7967 &self.handle
7968 }
7969
7970 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7971 unreachable!("this runtime only raises requests")
7972 }
7973
7974 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7975 match self.events.pop_front() {
7976 Some(event) => Ok(Some(event)),
7977 None => std::future::pending().await,
7981 }
7982 }
7983
7984 async fn interrupt(&mut self) -> crate::Result<()> {
7985 Ok(())
7986 }
7987
7988 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7989 self.answered
7993 .lock()
7994 .unwrap_or_else(std::sync::PoisonError::into_inner)
7995 .push(json!({"request_id": request_id, "response": response}));
7996 Ok(())
7997 }
7998
7999 async fn close(&mut self) -> crate::Result<()> {
8000 Ok(())
8001 }
8002 }
8003
8004 fn requesting_runtime(
8005 harness: &str,
8006 events: Vec<HarnessEvent>,
8007 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
8008 ) -> Box<dyn RuntimeConnection> {
8009 requesting_runtime_named(harness, "hermes-live-session", events, answered)
8010 }
8011
8012 fn requesting_runtime_named(
8013 harness: &str,
8014 runtime_id: &str,
8015 events: Vec<HarnessEvent>,
8016 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
8017 ) -> Box<dyn RuntimeConnection> {
8018 Box::new(RequestingRuntime {
8019 handle: RuntimeHandle {
8020 harness: HarnessId::from(harness),
8021 runtime_id: runtime_id.into(),
8022 endpoint: RuntimeEndpoint::LocalProcess {
8023 pid: None,
8024 command: vec!["hermes-acp".into()],
8025 protocol: "acp".into(),
8026 },
8027 },
8028 events: events.into(),
8029 answered,
8030 })
8031 }
8032
8033 fn permission_event(id: u64, title: &str) -> HarnessEvent {
8034 HarnessEvent {
8035 sequence: None,
8036 kind: "session/request_permission".into(),
8037 payload: json!({
8038 "jsonrpc": "2.0",
8039 "id": id,
8040 "method": "session/request_permission",
8041 "params": {
8042 "sessionId": "hermes-live-session",
8043 "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
8044 "options": [
8045 {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
8046 {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
8047 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8048 ],
8049 },
8050 }),
8051 }
8052 }
8053
8054 fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
8055 let response = service.handle(request(1, "harness.v1.approvals.list", params));
8056 assert!(response.get("error").is_none(), "{response:#}");
8057 response["result"].clone()
8058 }
8059
8060 #[tokio::test]
8068 async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
8069 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8070 let mut service = HarnessSessionService::new();
8071 service.runtimes.insert(
8072 "runtime-cc".into(),
8073 requesting_runtime_named(
8074 HarnessId::CLAUDE_CODE,
8075 "claude-live-session",
8076 vec![HarnessEvent {
8077 sequence: None,
8078 kind: "control_request".into(),
8079 payload: json!({
8080 "type": "control_request",
8081 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
8082 "request": {
8083 "subtype": "can_use_tool",
8084 "tool_name": "Bash",
8085 "display_name": "Bash",
8086 "input": {"command": "touch probe-artifact.txt"},
8087 "tool_use_id": "toolu_mock_1",
8088 },
8089 }),
8090 }],
8091 answered.clone(),
8092 ),
8093 );
8094
8095 let notifications = service.poll_runtimes().await;
8096 assert_eq!(notifications.len(), 1, "{notifications:#?}");
8097
8098 let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
8099 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
8100 let row = &rows[0];
8101 assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
8102 assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
8103 assert_eq!(row["status"], "pending");
8104 assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
8105 assert_eq!(row["runtime_id"], "claude-live-session");
8106 assert_eq!(
8107 row["options"]
8108 .as_array()
8109 .unwrap()
8110 .iter()
8111 .map(|option| option["id"].as_str().unwrap())
8112 .collect::<Vec<_>>(),
8113 vec!["allow", "deny"],
8114 );
8115
8116 let response = resolve(
8117 &mut service,
8118 json!({"id": row["id"], "decision": "allow_once"}),
8119 )
8120 .await;
8121 assert!(response.get("error").is_none(), "{response:#}");
8122 assert_eq!(response["result"]["option_id"], "allow");
8123 assert_eq!(
8124 answered
8125 .lock()
8126 .unwrap_or_else(std::sync::PoisonError::into_inner)
8127 .as_slice(),
8128 &[json!({
8129 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
8130 "response": {"behavior": "allow"},
8131 })],
8132 );
8133 assert_eq!(
8134 approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
8135 .as_array()
8136 .map(Vec::len),
8137 Some(0),
8138 );
8139 }
8140
8141 #[tokio::test]
8145 async fn a_live_permission_request_lists_until_it_is_answered() {
8146 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8147 let mut service = HarnessSessionService::new();
8148 service.runtimes.insert(
8149 "runtime-1".into(),
8150 requesting_runtime(
8151 HarnessId::HERMES,
8152 vec![permission_event(7, "rm -rf build")],
8153 answered.clone(),
8154 ),
8155 );
8156
8157 let notifications = service.poll_runtimes().await;
8158 assert_eq!(notifications.len(), 1, "{notifications:#?}");
8159
8160 let rows = approvals(&mut service, json!({}));
8161 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
8162 let row = &rows[0];
8163 assert_eq!(row["id"], "runtime-1/7");
8164 assert_eq!(row["harness"], HarnessId::HERMES);
8165 assert_eq!(row["kind"], "live");
8166 assert_eq!(row["status"], "pending");
8167 assert_eq!(row["subject"], "rm -rf build");
8168 assert_eq!(row["session_id"], "hermes-live-session");
8169 assert_eq!(row["runtime_id"], "hermes-live-session");
8170 assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
8171 assert!(
8172 row["age_ms"].as_i64().is_some_and(|age| age >= 0),
8173 "{row:#}"
8174 );
8175 assert_eq!(
8176 row["options"]
8177 .as_array()
8178 .unwrap()
8179 .iter()
8180 .map(|option| option["id"].as_str().unwrap())
8181 .collect::<Vec<_>>(),
8182 vec!["allow_once", "allow_for_session", "deny"],
8183 );
8184
8185 assert_eq!(
8187 approvals(&mut service, json!({"harness": HarnessId::HERMES}))
8188 .as_array()
8189 .map(Vec::len),
8190 Some(1),
8191 );
8192 assert_eq!(
8193 approvals(&mut service, json!({"session": "some-other-session"}))
8194 .as_array()
8195 .map(Vec::len),
8196 Some(0),
8197 );
8198
8199 let response = service
8200 .handle_async(request(
8201 2,
8202 "harness.v1.runtimes.respond",
8203 json!({
8204 "connection": "runtime-1",
8205 "request_id": 7,
8206 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8207 }),
8208 ))
8209 .await;
8210 assert!(response.get("error").is_none(), "{response:#}");
8211 assert_eq!(
8212 answered
8213 .lock()
8214 .unwrap_or_else(std::sync::PoisonError::into_inner)
8215 .as_slice(),
8216 &[json!({
8217 "request_id": 7,
8218 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8219 })],
8220 );
8221
8222 let rows = approvals(&mut service, json!({}));
8223 assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
8224 }
8225
8226 #[test]
8229 fn queued_subagent_approvals_list_through_the_same_door() {
8230 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8231 crate::subagents::QueuedApproval {
8232 child_agent_id: "child-7".into(),
8233 tool: "shell".into(),
8234 subject: Some("cargo publish --dry-run".into()),
8235 queued_at_ms: 1,
8236 outcome: None,
8237 },
8238 crate::subagents::QueuedApproval {
8239 child_agent_id: "child-8".into(),
8240 tool: "write_file".into(),
8241 subject: None,
8242 queued_at_ms: 2,
8243 outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
8244 },
8245 ]));
8246 let mut service = HarnessSessionService::new();
8247 service.observe_subagent_approvals(queue);
8248
8249 let rows = approvals(&mut service, json!({}));
8250 assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
8251 assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
8252 assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
8253 assert_eq!(rows[0]["status"], "pending");
8254 assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
8255 assert_eq!(rows[1]["status"], "denied");
8256 assert!(rows[1]["options"].as_array().unwrap().is_empty());
8257
8258 let only = approvals(&mut service, json!({"session": "child-8"}));
8260 assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
8261 assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
8262 }
8263
8264 #[test]
8270 fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
8271 let response = HarnessSessionService::new().handle(request(
8272 1,
8273 "harness.v1.approvals.list",
8274 json!({"harness": "not-a-harness"}),
8275 ));
8276 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
8277 assert!(response["error"]["message"]
8278 .as_str()
8279 .unwrap()
8280 .contains("not-a-harness"));
8281 for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
8282 let response = HarnessSessionService::new().handle(request(
8283 1,
8284 "harness.v1.approvals.list",
8285 json!({"harness": harness}),
8286 ));
8287 assert!(response.get("error").is_none(), "{harness}: {response:#}");
8288 }
8289 }
8290
8291 #[test]
8295 fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
8296 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8297 assert_eq!(
8298 SdkOperation::from_method("harness.v1.approvals.list"),
8299 Some(SdkOperation::ApprovalsList)
8300 );
8301 let registry = harness_support_registry();
8302 for id in [
8303 HarnessId::HERMES,
8304 HarnessId::OPENCLAW,
8305 HarnessId::CODEX,
8306 HarnessId::CLAUDE_CODE,
8309 ] {
8310 let concept = registry
8311 .harnesses
8312 .iter()
8313 .find(|harness| harness.id.as_str() == id)
8314 .unwrap()
8315 .orchestration
8316 .concepts
8317 .iter()
8318 .find(|concept| concept.concept == "pending_request")
8319 .unwrap();
8320 assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8321 assert!(concept
8322 .methods
8323 .iter()
8324 .any(|method| method == "harness.v1.approvals.list"));
8325 }
8326 }
8327
8328 async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8331 service
8332 .handle_async(request(3, "harness.v1.approvals.resolve", params))
8333 .await
8334 }
8335
8336 #[tokio::test]
8340 async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8341 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8342 let mut service = HarnessSessionService::new();
8343 service.runtimes.insert(
8344 "runtime-1".into(),
8345 requesting_runtime(
8346 HarnessId::HERMES,
8347 vec![permission_event(7, "rm -rf build")],
8348 answered.clone(),
8349 ),
8350 );
8351 service.poll_runtimes().await;
8352
8353 let rows = approvals(&mut service, json!({}));
8354 assert_eq!(rows[0]["id"], "runtime-1/7");
8355
8356 let response = resolve(
8357 &mut service,
8358 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8359 )
8360 .await;
8361 assert!(response.get("error").is_none(), "{response:#}");
8362 assert_eq!(
8363 response["result"],
8364 json!({
8365 "id": "runtime-1/7",
8366 "decision": "allow_once",
8367 "option_id": "allow_once",
8368 "resolved": true,
8369 }),
8370 );
8371 assert_eq!(
8373 answered
8374 .lock()
8375 .unwrap_or_else(std::sync::PoisonError::into_inner)
8376 .as_slice(),
8377 &[json!({
8378 "request_id": 7,
8379 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8380 })],
8381 );
8382 assert_eq!(
8384 approvals(&mut service, json!({})).as_array().map(Vec::len),
8385 Some(0),
8386 );
8387 let response = resolve(
8389 &mut service,
8390 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8391 )
8392 .await;
8393 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8394 }
8395
8396 #[tokio::test]
8399 async fn deny_selects_the_requests_own_reject_option() {
8400 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8401 let mut service = HarnessSessionService::new();
8402 service.runtimes.insert(
8403 "runtime-1".into(),
8404 requesting_runtime(
8405 HarnessId::HERMES,
8406 vec![permission_event(11, "git push --force")],
8407 answered.clone(),
8408 ),
8409 );
8410 service.poll_runtimes().await;
8411
8412 let response = resolve(
8413 &mut service,
8414 json!({"id": "runtime-1/11", "decision": "deny"}),
8415 )
8416 .await;
8417 assert!(response.get("error").is_none(), "{response:#}");
8418 assert_eq!(response["result"]["option_id"], "deny");
8420 assert_eq!(
8421 answered
8422 .lock()
8423 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8424 json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8425 );
8426 assert_eq!(
8427 approvals(&mut service, json!({})).as_array().map(Vec::len),
8428 Some(0),
8429 );
8430 }
8431
8432 #[tokio::test]
8435 async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8436 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8437 let mut service = HarnessSessionService::new();
8438 let mut event = permission_event(3, "rm -rf build");
8439 event.payload["params"]["options"] = json!([
8442 {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8443 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8444 ]);
8445 service.runtimes.insert(
8446 "runtime-1".into(),
8447 requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8448 );
8449 service.poll_runtimes().await;
8450
8451 let response = resolve(
8452 &mut service,
8453 json!({"id": "runtime-1/3", "decision": "allow_always"}),
8454 )
8455 .await;
8456 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8457 let message = response["error"]["message"].as_str().unwrap();
8458 assert!(message.contains("allow_always"), "{message}");
8459 assert!(message.contains("allow_once, deny"), "{message}");
8460 assert!(answered
8462 .lock()
8463 .unwrap_or_else(std::sync::PoisonError::into_inner)
8464 .is_empty());
8465 assert_eq!(
8466 approvals(&mut service, json!({})).as_array().map(Vec::len),
8467 Some(1),
8468 );
8469 }
8470
8471 #[tokio::test]
8475 async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8476 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8477 crate::subagents::QueuedApproval {
8478 child_agent_id: "child-7".into(),
8479 tool: "shell".into(),
8480 subject: Some("cargo publish --dry-run".into()),
8481 queued_at_ms: 1,
8482 outcome: None,
8483 },
8484 ]));
8485 let mut service = HarnessSessionService::new();
8486 service.observe_subagent_approvals(queue.clone());
8487 let row = approvals(&mut service, json!({}))[0]["id"]
8488 .as_str()
8489 .unwrap()
8490 .to_string();
8491 assert_eq!(row, "supercode/subagent/child-7/1/0");
8492
8493 let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8494 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8495 let message = response["error"]["message"].as_str().unwrap();
8496 assert!(message.contains("queued subagent record"), "{message}");
8497 assert!(message.contains("request"), "{message}");
8498 assert!(queue
8500 .lock()
8501 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8502 .outcome
8503 .is_none());
8504 }
8505
8506 #[tokio::test]
8509 async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8510 let mut service = HarnessSessionService::new();
8511 let response = resolve(
8512 &mut service,
8513 json!({"id": "runtime-9/4", "decision": "deny"}),
8514 )
8515 .await;
8516 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8517 assert!(response["error"]["message"]
8518 .as_str()
8519 .unwrap()
8520 .contains("runtime-9/4"));
8521
8522 let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8523 let message = response["error"]["message"].as_str().unwrap();
8524 assert!(
8525 message.contains("allow_once | allow_always | deny"),
8526 "{message}"
8527 );
8528
8529 let response = resolve(
8530 &mut service,
8531 json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8532 )
8533 .await;
8534 assert!(response["error"]["message"]
8535 .as_str()
8536 .unwrap()
8537 .contains("not both"));
8538 }
8539
8540 #[test]
8544 fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8545 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8546 assert_eq!(
8547 SdkOperation::from_method("harness.v1.approvals.resolve"),
8548 Some(SdkOperation::ApprovalsResolve)
8549 );
8550 assert_eq!(
8551 SdkOperation::ApprovalsResolve.action_name(),
8552 "approvals_resolve"
8553 );
8554 let registry = harness_support_registry();
8555 for id in [
8556 HarnessId::HERMES,
8557 HarnessId::OPENCLAW,
8558 HarnessId::CODEX,
8559 HarnessId::CLAUDE_CODE,
8562 ] {
8563 let concept = registry
8564 .harnesses
8565 .iter()
8566 .find(|harness| harness.id.as_str() == id)
8567 .unwrap()
8568 .orchestration
8569 .concepts
8570 .iter()
8571 .find(|concept| concept.concept == "pending_request")
8572 .unwrap();
8573 assert_eq!(
8574 concept.controlled,
8575 crate::ImplementationKind::BuiltIn,
8576 "{id}"
8577 );
8578 assert!(
8579 concept
8580 .methods
8581 .iter()
8582 .any(|method| method == "harness.v1.approvals.resolve"),
8583 "{id}"
8584 );
8585 }
8586 }
8587
8588 #[test]
8589 fn capabilities_are_explicit_and_versioned() {
8590 let mut service = HarnessSessionService::new();
8591 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8592 assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8593 assert_eq!(
8594 response["result"]["sdk"]["schema_version"],
8595 crate::SDK_SCHEMA_VERSION
8596 );
8597 assert_eq!(
8598 response["result"]["sdk"]["operations"]
8599 .as_array()
8600 .unwrap()
8601 .len(),
8602 SdkOperation::ALL.len()
8603 );
8604 assert_eq!(
8605 response["result"]["harnesses"].as_array().unwrap().len(),
8606 11
8607 );
8608 assert!(response["result"]["harnesses"]
8609 .as_array()
8610 .unwrap()
8611 .iter()
8612 .any(|harness| harness == HarnessId::GROK));
8613 assert!(response["result"]["harnesses"]
8614 .as_array()
8615 .unwrap()
8616 .iter()
8617 .any(|harness| harness == HarnessId::GOOSE));
8618 }
8619
8620 #[test]
8621 fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8622 let noisy_stderr = crate::HarnessEvent {
8623 sequence: None,
8624 kind: "transport_stderr".into(),
8625 payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8626 };
8627 assert_eq!(handshake_event_failure(&noisy_stderr), None);
8628
8629 let closed = crate::HarnessEvent {
8630 sequence: None,
8631 kind: "transport_closed".into(),
8632 payload: json!({}),
8633 };
8634 assert!(handshake_event_failure(&closed).is_some());
8635 }
8636
8637 #[tokio::test]
8638 async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8639 let mut service = HarnessSessionService::new();
8640 service
8641 .runtimes
8642 .insert("raw-eof".into(), ending_runtime(None));
8643 service.runtimes.insert(
8644 "explicit-close".into(),
8645 ending_runtime(Some(HarnessEvent {
8646 sequence: None,
8647 kind: "transport_closed".into(),
8648 payload: json!({"message": "native transport exited"}),
8649 })),
8650 );
8651
8652 let notifications = service.poll_runtimes().await;
8653
8654 assert_eq!(notifications.len(), 2);
8655 assert!(notifications
8656 .iter()
8657 .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8658 assert!(notifications.iter().all(|notification| {
8659 notification["params"]["session_id"] == "ending-session"
8660 && notification["params"]["connection"].is_string()
8661 }));
8662 let mut sequences = notifications
8663 .iter()
8664 .filter_map(|notification| notification["params"]["sequence"].as_u64())
8665 .collect::<Vec<_>>();
8666 sequences.sort_unstable();
8667 assert_eq!(sequences, vec![1, 2]);
8668 assert!(service.runtimes.is_empty());
8669 }
8670
8671 #[test]
8672 fn support_report_and_grok_default_binding_share_the_registry() {
8673 let mut service = HarnessSessionService::new();
8674 let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8675 assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8676 let params = RuntimeBackendParams {
8677 harness: HarnessId::from(HarnessId::GROK),
8678 protocol: None,
8679 launch: None,
8680 base_url: None,
8681 policy: RuntimePolicy::Default,
8682 };
8683 let backend = match runtime_backend(¶ms) {
8684 Ok(backend) => backend,
8685 Err(_) => panic!("Grok should bind through its registered ACP launch"),
8686 };
8687 assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8688 assert!(backend.capabilities().start_session);
8689 let registered = harness_support_registry()
8690 .harnesses
8691 .into_iter()
8692 .find(|harness| harness.id.as_str() == HarnessId::GROK)
8693 .and_then(|harness| harness.runtime.default_launch)
8694 .unwrap();
8695 assert!(!registered
8696 .arguments
8697 .iter()
8698 .any(|argument| argument == "--always-approve"));
8699 assert!(runtime_launch(¶ms).is_none());
8700
8701 let yolo = RuntimeBackendParams {
8702 policy: RuntimePolicy::Yolo,
8703 ..params
8704 };
8705 assert!(runtime_launch(&yolo)
8706 .unwrap()
8707 .arguments
8708 .iter()
8709 .any(|argument| argument == "--always-approve"));
8710
8711 let mismatched_protocol = RuntimeBackendParams {
8712 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8713 protocol: Some("acp".into()),
8714 launch: None,
8715 base_url: None,
8716 policy: RuntimePolicy::Default,
8717 };
8718 assert!(runtime_backend(&mismatched_protocol).is_err());
8719 }
8720
8721 #[test]
8722 fn load_follow_and_unfollow_share_the_same_locator() {
8723 let mut service = HarnessSessionService::new();
8724 let locator = pi_locator();
8725 let loaded = service.handle(request(
8726 1,
8727 "harness.v1.sessions.load",
8728 json!({"locator": locator}),
8729 ));
8730 assert_eq!(
8731 loaded["result"]["session"]["session_id"],
8732 locator.session_id
8733 );
8734
8735 let followed = service.handle(request(
8736 2,
8737 "harness.v1.sessions.follow",
8738 json!({"locator": locator}),
8739 ));
8740 assert_eq!(followed["result"]["subscription"], "sub-1");
8741 assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8742 assert!(service.poll().is_empty());
8743
8744 let unfollowed = service.handle(request(
8745 3,
8746 "harness.v1.sessions.unfollow",
8747 json!({"subscription": "sub-1"}),
8748 ));
8749 assert_eq!(unfollowed["result"]["removed"], true);
8750 }
8751
8752 #[test]
8753 fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8754 let temp = std::env::temp_dir().join(format!(
8755 "supercode-bounded-view-{}-{}",
8756 std::process::id(),
8757 generated_session_id()
8758 ));
8759 let path = temp.join("parent.jsonl");
8760 let subagents = temp.join("parent/subagents");
8761 std::fs::create_dir_all(&subagents).unwrap();
8762 let long_last = "x".repeat(300);
8763 let parent_records = [
8764 json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8765 json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8766 json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8767 ];
8768 std::fs::write(
8769 &path,
8770 format!(
8771 "{}\n",
8772 parent_records
8773 .iter()
8774 .map(Value::to_string)
8775 .collect::<Vec<_>>()
8776 .join("\n")
8777 ),
8778 )
8779 .unwrap();
8780 std::fs::write(
8781 subagents.join("agent-child.jsonl"),
8782 concat!(
8783 r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8784 "\n",
8785 ),
8786 )
8787 .unwrap();
8788 let locator = SessionLocator {
8789 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8790 session_id: "parent".into(),
8791 storage: StorageLocator::File { path },
8792 };
8793 let mut service = HarnessSessionService::new();
8794
8795 let complete = service.handle(request(
8796 1,
8797 "harness.v1.sessions.load",
8798 json!({"locator": locator}),
8799 ));
8800 assert_eq!(
8801 complete["result"]["session"]["subagents"]
8802 .as_array()
8803 .unwrap()
8804 .len(),
8805 1
8806 );
8807
8808 let bounded = service.handle(request(
8809 2,
8810 "harness.v1.sessions.load",
8811 json!({
8812 "locator": locator,
8813 "view": {
8814 "tail_messages": 1,
8815 "max_message_chars": 256,
8816 "include_subagents": false
8817 },
8818 }),
8819 ));
8820 let session = &bounded["result"]["session"];
8821 assert!(session["subagents"].as_array().unwrap().is_empty());
8822 assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8823 assert_eq!(
8824 session["messages"][0]["content"],
8825 format!("{}\n…", "x".repeat(256))
8826 );
8827
8828 let followed = service.handle(request(
8829 3,
8830 "harness.v1.sessions.follow",
8831 json!({
8832 "locator": locator,
8833 "view": {
8834 "tail_messages": 1,
8835 "max_message_chars": 256,
8836 "include_subagents": false
8837 },
8838 }),
8839 ));
8840 let initial = &followed["result"]["initial"]["session"];
8841 assert!(initial["subagents"].as_array().unwrap().is_empty());
8842 assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8843
8844 let _ = std::fs::remove_dir_all(&temp);
8845 }
8846
8847 #[test]
8848 fn forty_megabyte_display_load_is_bounded_and_prompt() {
8849 let temp = std::env::temp_dir().join(format!(
8850 "supercode-large-display-view-{}-{}",
8851 std::process::id(),
8852 generated_session_id()
8853 ));
8854 std::fs::create_dir_all(&temp).unwrap();
8855 let path = temp.join("rollout.jsonl");
8856 let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8857 writeln!(
8858 file,
8859 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8860 )
8861 .unwrap();
8862 let padding = "x".repeat(80 * 1024);
8863 for index in 0..512 {
8864 let marker = if index == 0 {
8865 "OLDEST-SHOULD-NOT-LOAD"
8866 } else if index == 511 {
8867 "LATEST-MUST-LOAD"
8868 } else {
8869 "bulk"
8870 };
8871 writeln!(
8872 file,
8873 "{}",
8874 json!({
8875 "timestamp": "2026-01-01T00:00:01Z",
8876 "type": "response_item",
8877 "payload": {
8878 "type": "message",
8879 "role": "assistant",
8880 "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8881 },
8882 })
8883 )
8884 .unwrap();
8885 }
8886 file.flush().unwrap();
8887 drop(file);
8888 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8889
8890 let locator = SessionLocator {
8891 harness: HarnessId::from(HarnessId::CODEX),
8892 session_id: "large-display".into(),
8893 storage: StorageLocator::File { path },
8894 };
8895 let started = Instant::now();
8896 let response = HarnessSessionService::new().handle(request(
8897 1,
8898 "harness.v1.sessions.load",
8899 json!({
8900 "locator": locator,
8901 "view": {
8902 "tail_messages": 500,
8903 "max_message_chars": 1024,
8904 "include_subagents": false,
8905 "display_history": true,
8906 },
8907 }),
8908 ));
8909 let elapsed = started.elapsed();
8910 let wire = response.to_string();
8911 eprintln!(
8912 "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8913 wire.len()
8914 );
8915 assert!(response.get("error").is_none(), "{response:#}");
8916 assert!(wire.contains("LATEST-MUST-LOAD"));
8917 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8918 assert!(
8919 wire.len() < 2 * 1024 * 1024,
8920 "bounded wire was {} bytes",
8921 wire.len()
8922 );
8923 assert!(
8924 elapsed.as_secs_f64() < 3.0,
8925 "bounded 40 MiB load took {elapsed:?}"
8926 );
8927
8928 let anchored = temp.join("anchored.jsonl");
8932 let mut file = std::io::BufWriter::new(std::fs::File::create(&anchored).unwrap());
8933 writeln!(
8934 file,
8935 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8936 )
8937 .unwrap();
8938 for index in 0..512 {
8939 let marker = if index == 0 {
8940 "OLDEST-SHOULD-NOT-LOAD"
8941 } else if index == 511 {
8942 "LATEST-MUST-LOAD"
8943 } else {
8944 "bulk"
8945 };
8946 let (role, kind) = if index % 8 == 0 {
8947 ("user", "input_text")
8948 } else {
8949 ("assistant", "output_text")
8950 };
8951 writeln!(
8952 file,
8953 "{}",
8954 json!({
8955 "timestamp": "2026-01-01T00:00:01Z",
8956 "type": "response_item",
8957 "payload": {
8958 "type": "message",
8959 "role": role,
8960 "content": [{"type": kind, "text": format!("{marker}:{padding}")}],
8961 },
8962 })
8963 )
8964 .unwrap();
8965 }
8966 file.flush().unwrap();
8967 drop(file);
8968 let short = HarnessSessionService::new().handle(request(
8969 2,
8970 "harness.v1.sessions.load",
8971 json!({
8972 "locator": SessionLocator {
8973 harness: HarnessId::from(HarnessId::CODEX),
8974 session_id: "large-display".into(),
8975 storage: StorageLocator::File { path: anchored },
8976 },
8977 "view": {
8978 "tail_messages": 20,
8979 "max_message_chars": 1024,
8980 "include_subagents": false,
8981 "display_history": true,
8982 },
8983 }),
8984 ));
8985 let records = short["result"]["session"]["raw_record_count"].as_u64();
8986 assert!(
8987 records.is_some_and(|records| records < 128),
8988 "{records:?} records read"
8989 );
8990 let short = short.to_string();
8991 assert!(short.contains("LATEST-MUST-LOAD"));
8992 assert!(short.contains("older native records remain outside this bounded display window"));
8993
8994 let _ = std::fs::remove_dir_all(&temp);
8995 }
8996
8997 #[test]
8998 fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8999 let temp = std::env::temp_dir().join(format!(
9000 "supercode-large-goose-view-{}-{}",
9001 std::process::id(),
9002 generated_session_id()
9003 ));
9004 std::fs::create_dir_all(&temp).unwrap();
9005 let path = temp.join("sessions.db");
9006 let connection = rusqlite::Connection::open(&path).unwrap();
9007 connection
9008 .execute_batch(
9009 "CREATE TABLE sessions (
9010 id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
9011 created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
9012 session_type TEXT NOT NULL, extension_data TEXT,
9013 goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
9014 archived_at TEXT
9015 );
9016 CREATE TABLE messages (
9017 id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
9018 role TEXT NOT NULL, content_json TEXT NOT NULL,
9019 created_timestamp INTEGER NOT NULL, metadata_json TEXT
9020 );",
9021 )
9022 .unwrap();
9023 connection
9024 .execute(
9025 "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
9026 rusqlite::params![
9027 "goose-large",
9028 "Large Goose session",
9029 "/tmp",
9030 "2026-01-01 00:00:00",
9031 "2026-01-01 00:00:02",
9032 "user",
9033 "{}",
9034 "auto",
9035 "anthropic",
9036 r#"{"model_name":"claude-sonnet"}"#,
9037 ],
9038 )
9039 .unwrap();
9040 let old_content = serde_json::to_string(&vec![json!({
9041 "type": "text",
9042 "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
9043 })])
9044 .unwrap();
9045 connection
9046 .execute(
9047 "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
9048 rusqlite::params!["goose-large", old_content],
9049 )
9050 .unwrap();
9051 connection
9052 .execute(
9053 "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
9054 rusqlite::params![
9055 "goose-large",
9056 r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
9057 ],
9058 )
9059 .unwrap();
9060 drop(connection);
9061 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
9062
9063 let locator = SessionLocator {
9064 harness: HarnessId::from(HarnessId::GOOSE),
9065 session_id: "goose-large".into(),
9066 storage: StorageLocator::Sqlite {
9067 path,
9068 selector: "goose-large".into(),
9069 },
9070 };
9071 let started = Instant::now();
9072 let response = HarnessSessionService::new().handle(request(
9073 1,
9074 "harness.v1.sessions.load",
9075 json!({
9076 "locator": locator,
9077 "view": {
9078 "tail_messages": 1,
9079 "max_message_chars": 1024,
9080 "include_subagents": false,
9081 "display_history": true,
9082 },
9083 }),
9084 ));
9085 let elapsed = started.elapsed();
9086 let wire = response.to_string();
9087 eprintln!(
9088 "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
9089 wire.len()
9090 );
9091 assert!(response.get("error").is_none(), "{response:#}");
9092 assert!(wire.contains("LATEST-MUST-LOAD"));
9093 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
9094 assert!(
9095 wire.len() < 64 * 1024,
9096 "bounded wire was {} bytes",
9097 wire.len()
9098 );
9099 assert!(
9100 elapsed.as_secs_f64() < 1.0,
9101 "bounded Goose load took {elapsed:?}"
9102 );
9103
9104 let _ = std::fs::remove_dir_all(&temp);
9105 }
9106
9107 #[test]
9108 fn display_view_keeps_codex_assistant_history_across_compaction() {
9109 let temp = std::env::temp_dir().join(format!(
9110 "supercode-codex-display-view-{}-{}",
9111 std::process::id(),
9112 generated_session_id()
9113 ));
9114 std::fs::create_dir_all(&temp).unwrap();
9115 let path = temp.join("rollout.jsonl");
9116 std::fs::write(
9117 &path,
9118 concat!(
9119 r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
9120 "\n",
9121 r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
9122 "\n",
9123 r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
9124 "\n",
9125 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"}]}}"#,
9126 "\n",
9127 r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
9128 "\n",
9129 r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
9130 "\n",
9131 ),
9132 )
9133 .unwrap();
9134 let locator = SessionLocator {
9135 harness: HarnessId::from(HarnessId::CODEX),
9136 session_id: "codex-display".into(),
9137 storage: StorageLocator::File { path },
9138 };
9139 let mut service = HarnessSessionService::new();
9140
9141 let continuation = service.handle(request(
9142 1,
9143 "harness.v1.sessions.load",
9144 json!({"locator": locator}),
9145 ));
9146 let continuation_text = continuation["result"]["session"]["messages"].to_string();
9147 assert!(!continuation_text.contains("old answer"));
9148
9149 let display = service.handle(request(
9150 2,
9151 "harness.v1.sessions.load",
9152 json!({
9153 "locator": locator,
9154 "view": {
9155 "tail_messages": 10,
9156 "include_subagents": false,
9157 "display_history": true,
9158 },
9159 }),
9160 ));
9161 let display_text = display["result"]["session"]["messages"].to_string();
9162 assert!(display_text.contains("old prompt"));
9163 assert!(display_text.contains("old answer"));
9164 assert!(display_text.contains("new prompt"));
9165 assert!(display_text.contains("new answer"));
9166
9167 let _ = std::fs::remove_dir_all(&temp);
9168 }
9169
9170 #[test]
9171 fn indexed_claude_windows_match_the_existing_wire_projection() {
9172 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9173 .join("tests/fixtures/claude_code_session.jsonl");
9174 let locator = SessionLocator {
9175 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9176 session_id: "fixture".into(),
9177 storage: StorageLocator::File { path },
9178 };
9179 let full = load_session(&locator).unwrap();
9180 for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
9181 for offset in [0, 1, full.messages.len(), usize::MAX] {
9182 for limit in [0, 1, 3, usize::MAX] {
9183 let options = SessionLoadOptions {
9184 include_subagents: Some(false),
9185 inline_media,
9186 message_offset: Some(offset),
9187 message_limit: Some(limit),
9188 ..Default::default()
9189 };
9190 let expected = projected_session_result(&full, &options);
9191 assert_eq!(
9192 indexed_claude_window(&locator, &options).unwrap().unwrap(),
9193 expected
9194 );
9195 }
9196 }
9197 for tail in [0, 1, 3, usize::MAX] {
9198 let options = SessionLoadOptions {
9199 include_subagents: Some(false),
9200 inline_media,
9201 message_tail: Some(tail),
9202 ..Default::default()
9203 };
9204 assert_eq!(
9205 indexed_claude_window(&locator, &options).unwrap().unwrap(),
9206 projected_session_result(&full, &options)
9207 );
9208 }
9209 }
9210 }
9211
9212 #[test]
9213 fn load_supports_bounded_windows_and_media_metadata() {
9214 let mut service = HarnessSessionService::new();
9215 let locator = pi_locator();
9216 let bounded = service.handle(request(
9217 1,
9218 "harness.v1.sessions.load",
9219 json!({
9220 "locator": locator,
9221 "options": {
9222 "include_subagents": false,
9223 "message_limit": 2,
9224 "message_offset": 1
9225 }
9226 }),
9227 ));
9228 assert_eq!(bounded["result"]["window"]["offset"], 1);
9229 assert_eq!(bounded["result"]["window"]["returned"], 2);
9230 assert!(bounded["result"]["summary"]["first_message"].is_object());
9231 assert!(bounded["result"]["summary"]["last_message"].is_object());
9232 assert_eq!(
9233 bounded["result"]["session"]["messages"]
9234 .as_array()
9235 .unwrap()
9236 .len(),
9237 2
9238 );
9239 assert!(bounded["result"]["session"]["subagents"]
9240 .as_array()
9241 .unwrap()
9242 .is_empty());
9243
9244 let tail = service.handle(request(
9245 2,
9246 "harness.v1.sessions.load",
9247 json!({"locator": locator, "options": {"message_tail": 1}}),
9248 ));
9249 assert_eq!(tail["result"]["window"]["returned"], 1);
9250 assert_eq!(tail["result"]["window"]["has_more"], true);
9251 assert_eq!(tail["result"]["window"]["has_older"], true);
9252 assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
9253 assert!(tail["result"]["summary"]["first_message"].is_object());
9254
9255 let metadata_only = service.handle(request(
9256 3,
9257 "harness.v1.sessions.load",
9258 json!({"locator": locator, "options": {"inline_media": "metadata"}}),
9259 ));
9260 assert!(metadata_only["result"]["session"]
9261 .to_string()
9262 .contains("media_reference"));
9263 assert!(!metadata_only["result"]["session"]
9264 .to_string()
9265 .contains("data:image/"));
9266 }
9267
9268 #[test]
9269 fn import_translate_branch_and_handoff_use_typed_artifacts() {
9270 let mut service = HarnessSessionService::new();
9271 let locator = pi_locator();
9272 let translated = service.handle(request(
9273 1,
9274 "harness.v1.sessions.translate",
9275 json!({"locator": locator, "target_harness": "grok"}),
9276 ));
9277 assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
9278 assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
9279 assert!(translated["result"]["artifact"]["content"]
9280 .as_str()
9281 .is_some_and(|content| !content.is_empty()));
9282
9283 for target in ["opencode", "open-code"] {
9284 let opencode = service.handle(request(
9285 6,
9286 "harness.v1.sessions.translate",
9287 json!({"locator": locator, "target_harness": target}),
9288 ));
9289 assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
9290 }
9291 let goose = service.handle(request(
9292 7,
9293 "harness.v1.sessions.translate",
9294 json!({"locator": locator, "target_harness": "goose"}),
9295 ));
9296 assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
9297 assert!(serde_json::from_str::<Value>(
9298 goose["result"]["artifact"]["content"].as_str().unwrap()
9299 )
9300 .unwrap()["conversation"]
9301 .is_array());
9302
9303 let imported = service.handle(request(
9304 2,
9305 "harness.v1.sessions.import",
9306 json!({
9307 "source_harness": "grok",
9308 "content": translated["result"]["artifact"]["content"],
9309 }),
9310 ));
9311 assert_eq!(imported["result"]["session"]["source"], "grok");
9312
9313 let branched = service.handle(request(
9314 3,
9315 "harness.v1.sessions.branch",
9316 json!({"locator": locator, "target_harness": "codex"}),
9317 ));
9318 assert_eq!(branched["result"]["parent"]["harness"], "pi");
9319 assert!(branched["result"]["bootstrap_prompt"]
9320 .as_str()
9321 .unwrap()
9322 .contains("frozen parent transcript"));
9323 assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
9324
9325 let handoff = service.handle(request(
9326 4,
9327 "harness.v1.sessions.handoff",
9328 json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
9329 ));
9330 assert_eq!(handoff["result"]["launch"]["program"], "pi");
9331 assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
9332 assert_eq!(handoff["result"]["requires_materialization"], true);
9333
9334 let goose_handoff = service.handle(request(
9335 8,
9336 "harness.v1.sessions.handoff",
9337 json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
9338 ));
9339 assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
9340 assert_eq!(
9341 goose_handoff["result"]["materialize"]["arguments"],
9342 json!(["session", "import", "{artifact_path}"])
9343 );
9344
9345 let resumed = service.handle(request(
9346 5,
9347 "harness.v1.sessions.resume_instructions",
9348 json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
9349 ));
9350 assert_eq!(resumed["result"]["launch"]["program"], "pi");
9351 assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
9352 }
9353
9354 #[test]
9355 fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
9356 let temp = std::env::temp_dir().join(format!(
9357 "supercode-service-reduce-{}-{}",
9358 std::process::id(),
9359 generated_session_id()
9360 ));
9361 let source_path = temp.join("source.jsonl");
9362 let store_root = temp.join("store");
9363 std::fs::create_dir_all(&temp).unwrap();
9364
9365 let mut records = vec![json!({
9366 "timestamp": "2026-01-01T00:00:00Z",
9367 "type": "session_meta",
9368 "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9369 })];
9370 for turn in 0..16 {
9371 records.push(json!({
9372 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9373 "type": "response_item",
9374 "payload": {
9375 "type": "message",
9376 "role": "user",
9377 "content": [{
9378 "type": "input_text",
9379 "text": format!("request {turn}: {}", "context ".repeat(80)),
9380 }],
9381 },
9382 }));
9383 records.push(json!({
9384 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9385 "type": "response_item",
9386 "payload": {
9387 "type": "message",
9388 "role": "assistant",
9389 "content": [{
9390 "type": "output_text",
9391 "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9392 }],
9393 },
9394 }));
9395 }
9396 let source = format!(
9397 "{}\n",
9398 records
9399 .iter()
9400 .map(Value::to_string)
9401 .collect::<Vec<_>>()
9402 .join("\n")
9403 );
9404 std::fs::write(&source_path, &source).unwrap();
9405 let locator = SessionLocator {
9406 harness: HarnessId::from(HarnessId::CODEX),
9407 session_id: "codex-reduce".into(),
9408 storage: StorageLocator::File {
9409 path: source_path.clone(),
9410 },
9411 };
9412 let original = load_session(&locator).unwrap();
9413 let mut service =
9414 HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9415
9416 let response = service.handle(request(
9417 1,
9418 "harness.v1.sessions.reduce",
9419 json!({
9420 "locator": locator,
9421 "target_harness": "claude-code",
9422 "keep_last": 4,
9423 }),
9424 ));
9425 assert!(response.get("error").is_none(), "{response:#}");
9426 let receipt = &response["result"]["receipt"];
9427 assert_eq!(receipt["source_harness"], "codex");
9428 assert_eq!(receipt["target_harness"], "claude-code");
9429 assert_eq!(receipt["verified"], true);
9430 assert_eq!(receipt["reversible"], true);
9431 assert!(receipt["reductions"].as_u64().unwrap() > 0);
9432 assert!(
9433 receipt["source_tokens"].as_u64().unwrap()
9434 > receipt["reduced_tokens"].as_u64().unwrap()
9435 );
9436 assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9437 assert!(response["result"]["bootstrap_prompt"]
9438 .as_str()
9439 .unwrap()
9440 .contains("Do not guess hidden content"));
9441
9442 let rescue_id = receipt["id"].as_str().unwrap();
9443 let store = crate::SessionStore::open(&store_root).unwrap();
9444 let sidecar =
9445 Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9446 let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9447 let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9448 let policy = reduce::ReductionPolicy {
9449 clear_turns_older_than: Some(4),
9450 ..Default::default()
9451 };
9452 let (restamped_view, reapplied_log) =
9453 reduce::project_messages(&sidecar.messages, &policy, &log);
9454 assert_eq!(
9455 messages_jsonl(&persisted_view).unwrap(),
9456 messages_jsonl(&restamped_view).unwrap()
9457 );
9458 assert_eq!(reapplied_log, log);
9459 reduce::verify_log(&log, &sidecar).unwrap();
9460 assert_eq!(
9461 reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9462 original.messages
9463 );
9464 assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9465
9466 std::fs::remove_dir_all(temp).ok();
9467 }
9468
9469 #[test]
9470 fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9471 let temp = std::env::temp_dir().join(format!(
9472 "supercode-severed-view-{}-{}",
9473 std::process::id(),
9474 generated_session_id()
9475 ));
9476 std::fs::create_dir_all(&temp).unwrap();
9477 let path = temp.join("severed.jsonl");
9478 std::fs::write(
9481 &path,
9482 concat!(
9483 r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9484 "\n",
9485 r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9486 "\n",
9487 ),
9488 )
9489 .unwrap();
9490 let locator = SessionLocator {
9491 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9492 session_id: "severed".into(),
9493 storage: StorageLocator::File { path },
9494 };
9495 let mut service = HarnessSessionService::new();
9496
9497 let viewed = service.handle(request(
9498 1,
9499 "harness.v1.sessions.load",
9500 json!({"locator": locator}),
9501 ));
9502 let session = &viewed["result"]["session"];
9503 assert_eq!(session["fidelity"], "semantic");
9504 assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9505 assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9506 entry
9507 .as_str()
9508 .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9509 }));
9510
9511 let strict = service.handle(request(
9514 2,
9515 "harness.v1.sessions.load",
9516 json!({"locator": locator, "fidelity": "byte_lossless"}),
9517 ));
9518 assert!(strict["error"]["message"]
9519 .as_str()
9520 .unwrap()
9521 .contains("cannot reconstruct lossless Claude continuation"));
9522
9523 let translated = service.handle(request(
9525 3,
9526 "harness.v1.sessions.translate",
9527 json!({"locator": locator, "target_harness": "codex"}),
9528 ));
9529 assert!(translated["error"]["message"]
9530 .as_str()
9531 .unwrap()
9532 .contains("cannot reconstruct lossless Claude continuation"));
9533 let resumed = service.handle(request(
9534 4,
9535 "harness.v1.sessions.resume_instructions",
9536 json!({"locator": locator}),
9537 ));
9538 assert!(resumed["error"]["message"]
9539 .as_str()
9540 .unwrap()
9541 .contains("cannot reconstruct lossless Claude continuation"));
9542
9543 let _ = std::fs::remove_dir_all(&temp);
9544 }
9545
9546 #[test]
9547 fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9548 let codex = resume_launch(
9549 HarnessId::CODEX,
9550 "codex-session",
9551 Path::new("/tmp/project"),
9552 ResumePolicy::Yolo,
9553 )
9554 .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9555 assert_eq!(codex.program, "codex");
9556 assert_eq!(
9557 codex.arguments,
9558 [
9559 "-c",
9560 "check_for_update_on_startup=false",
9561 "-c",
9562 "projects={\"/tmp/project\"={trust_level=\"trusted\"}}",
9563 "--dangerously-bypass-approvals-and-sandbox",
9564 "--dangerously-bypass-hook-trust",
9565 "resume",
9566 "codex-session",
9567 ]
9568 );
9569
9570 let gemini = resume_launch(
9571 HarnessId::GEMINI,
9572 "gemini-session",
9573 Path::new("/tmp/project"),
9574 ResumePolicy::Yolo,
9575 )
9576 .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9577 assert_eq!(gemini.program, "gemini");
9578 assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9579
9580 let goose = resume_launch(
9581 HarnessId::GOOSE,
9582 "goose-session",
9583 Path::new("/tmp/project"),
9584 ResumePolicy::Yolo,
9585 )
9586 .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9587 assert_eq!(goose.program, "goose");
9588 assert_eq!(
9589 goose.arguments,
9590 ["session", "--resume", "--session-id", "goose-session"]
9591 );
9592
9593 let supercode = resume_launch(
9594 HarnessId::SUPERCODE,
9595 "supercode-session",
9596 Path::new("/tmp/project"),
9597 ResumePolicy::Yolo,
9598 )
9599 .unwrap_or_else(|_| panic!("Volter Harness resume launch must be registered"));
9600 assert_eq!(supercode.program, "supercode");
9601 assert_eq!(
9602 supercode.arguments,
9603 ["--dangerous", "resume", "supercode-session"]
9604 );
9605 }
9606
9607 #[test]
9608 fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9609 let temp = std::env::temp_dir().join(format!(
9610 "supercode-harness-artifact-{}-{}",
9611 std::process::id(),
9612 generated_session_id()
9613 ));
9614 let main_path = temp.join("parent.jsonl");
9615 let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9616 std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9617 let fixture = std::fs::read_to_string(
9618 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9619 .join("tests/fixtures/claude_code_session.jsonl"),
9620 )
9621 .unwrap();
9622 let parent = fixture.trim_end_matches('\n');
9623 let child = fixture.trim_end_matches('\n');
9624 std::fs::write(&main_path, parent).unwrap();
9625 std::fs::write(&subagent_path, child).unwrap();
9626 let locator = SessionLocator {
9627 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9628 session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9629 storage: StorageLocator::File {
9630 path: main_path.clone(),
9631 },
9632 };
9633 let mut service = HarnessSessionService::new();
9634 let claude = service.handle(request(
9635 1,
9636 "harness.v1.sessions.translate",
9637 json!({"locator": locator, "target_harness": "claude-code"}),
9638 ));
9639 let artifact = &claude["result"]["artifact"];
9640 assert_eq!(artifact["fidelity"], "byte_lossless");
9641 assert_eq!(artifact["content"], parent);
9642 let files = artifact["files"].as_array().unwrap();
9643 assert!(files.iter().any(|file| {
9644 file["role"] == "subagent"
9645 && file["path"]
9646 .as_str()
9647 .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9648 && file["content"] == child
9649 }));
9650 assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9651
9652 let grok = service.handle(request(
9653 2,
9654 "harness.v1.sessions.translate",
9655 json!({"locator": grok_locator(), "target_harness": "grok"}),
9656 ));
9657 let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9658 for name in ["summary.json", "updates.jsonl"] {
9659 let expected = std::fs::read_to_string(
9660 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9661 .join("tests/fixtures/grok_session")
9662 .join(name),
9663 )
9664 .unwrap();
9665 assert!(files.iter().any(|file| {
9666 file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9667 }));
9668 }
9669 std::fs::remove_dir_all(temp).ok();
9670 }
9671
9672 #[test]
9673 fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9674 let mut service = HarnessSessionService::new();
9675 let source = pi_locator();
9676 for (target, format) in [
9677 ("claude-code", SessionFormat::ClaudeCode),
9678 ("codex", SessionFormat::Codex),
9679 ("opencode", SessionFormat::OpenCode),
9680 ("pi", SessionFormat::Pi),
9681 ] {
9682 let result = service.handle(request(
9683 1,
9684 "harness.v1.sessions.handoff",
9685 json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9686 ));
9687 let artifact = &result["result"]["artifact"];
9688 let target_id = artifact["session_id"].as_str().unwrap();
9689 assert_ne!(target_id, source.session_id, "{target}");
9690 let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9691 assert_eq!(
9692 parsed.meta.session_id.as_deref(),
9693 Some(target_id),
9694 "{target}"
9695 );
9696 if target != "pi" {
9697 assert!(result["result"]["launch"]["arguments"]
9698 .as_array()
9699 .unwrap()
9700 .iter()
9701 .any(|argument| argument == target_id));
9702 }
9703 if target == "opencode" {
9704 assert!(target_id.starts_with("ses_"));
9705 fn assert_session_ids(value: &Value, target_id: &str) {
9706 match value {
9707 Value::Object(fields) => {
9708 if let Some(session_id) = fields.get("sessionID") {
9709 assert_eq!(session_id, target_id);
9710 }
9711 for child in fields.values() {
9712 assert_session_ids(child, target_id);
9713 }
9714 }
9715 Value::Array(values) => {
9716 for child in values {
9717 assert_session_ids(child, target_id);
9718 }
9719 }
9720 _ => {}
9721 }
9722 }
9723 let document: Value =
9724 serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9725 assert_session_ids(&document, target_id);
9726 }
9727 }
9728
9729 let first = service.handle(request(
9730 2,
9731 "harness.v1.sessions.handoff",
9732 json!({"locator": source, "target_harness": "codex"}),
9733 ));
9734 let second = service.handle(request(
9735 3,
9736 "harness.v1.sessions.handoff",
9737 json!({"locator": source, "target_harness": "codex"}),
9738 ));
9739 assert_ne!(
9740 first["result"]["artifact"]["session_id"],
9741 second["result"]["artifact"]["session_id"]
9742 );
9743 }
9744
9745 #[test]
9746 fn grok_handoff_materializes_through_the_core_door() {
9747 let mut service = HarnessSessionService::new();
9748 let source = opencode_locator();
9749 let response = service.handle(request(
9750 1,
9751 "harness.v1.sessions.handoff",
9752 json!({
9753 "locator": source,
9754 "target_harness": "grok",
9755 "cwd": "/tmp/grok-handoff-project",
9756 }),
9757 ));
9758 let result = &response["result"];
9759
9760 assert_eq!(result["artifact"]["target_harness"], "grok");
9763 let artifact = Session::load_str(
9764 result["artifact"]["content"].as_str().unwrap(),
9765 SessionFormat::Grok,
9766 )
9767 .unwrap();
9768 assert!(!artifact.messages.is_empty());
9769 let target_session_id = result["artifact"]["session_id"].as_str().unwrap();
9770 assert_eq!(target_session_id.len(), 36);
9771 assert_ne!(target_session_id, opencode_locator().session_id);
9772 assert!(result["materialize"].is_null());
9773 assert_eq!(
9774 result["launch"]["arguments"],
9775 json!(["--resume", "{materialized_session_id}"])
9776 );
9777 assert!(result["note"]
9778 .as_str()
9779 .unwrap()
9780 .contains("harness.v1.sessions.materialize"));
9781 }
9782
9783 #[tokio::test]
9784 async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9785 let mut service = HarnessSessionService::new();
9786 let inventory = service
9787 .handle_async(request(
9788 1,
9789 "harness.v1.harnesses.list",
9790 json!({"harnesses": ["missing"]}),
9791 ))
9792 .await;
9793 assert_eq!(inventory["error"]["code"], -32602);
9794
9795 let attached = service
9796 .handle_async(request(
9797 2,
9798 "harness.v1.runtimes.attach_existing",
9799 json!({"harness": "codex", "runtime_id": "thread-1"}),
9800 ))
9801 .await;
9802 assert_eq!(attached["error"]["code"], -32000);
9803 assert!(attached["error"]["message"]
9804 .as_str()
9805 .unwrap()
9806 .contains("runtimes.resume"));
9807 }
9808
9809 #[test]
9810 fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9811 let mut service = HarnessSessionService::new();
9812 let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9813 assert_eq!(invalid["error"]["code"], -32602);
9814 let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9815 assert_eq!(unknown["error"]["code"], -32601);
9816 }
9817
9818 #[cfg(unix)]
9819 #[tokio::test]
9820 #[allow(clippy::await_holding_lock)]
9823 async fn async_service_drives_a_generic_acp_runtime() {
9824 let _environment_guard = crate::live_runtime::test_environment_lock();
9825 let script = r#"
9826 i=0
9827 while IFS= read -r line; do
9828 i=$((i + 1))
9829 case "$i" in
9830 1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9831 2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9832 3)
9833 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9834 printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9835 ;;
9836 4)
9837 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9838 printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9839 ;;
9840 esac
9841 done
9842 "#;
9843 let mut service = HarnessSessionService::new();
9844 let started = service
9845 .handle_async(request(
9846 1,
9847 "harness.v1.runtimes.start",
9848 json!({
9849 "harness": "codex",
9850 "protocol": "acp",
9851 "cwd": std::env::current_dir().unwrap(),
9852 "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9853 }),
9854 ))
9855 .await;
9856 assert_eq!(started["result"]["connection"], "runtime-1");
9857 assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9858
9859 let terminal = service
9860 .handle_async(request(
9861 9,
9862 "harness.v1.runtimes.terminal_instructions",
9863 json!({"connection":"runtime-1"}),
9864 ))
9865 .await;
9866 let arguments = terminal["result"]["launch"]["arguments"]
9867 .as_array()
9868 .expect("hosted runtime should return terminal arguments");
9869 let endpoint_index = arguments
9870 .iter()
9871 .position(|value| value == "--endpoint")
9872 .expect("terminal command should use an opaque endpoint");
9873 let endpoint = LiveRuntimeEndpoint::parse(
9874 arguments[endpoint_index + 1]
9875 .as_str()
9876 .expect("endpoint argument should be text"),
9877 )
9878 .unwrap();
9879 assert!(!terminal.to_string().contains("Bearer"));
9880 let workspace = std::env::current_dir().unwrap();
9881 let receipt = resolve_live_runtime(
9882 &endpoint,
9883 &LiveRuntimeSource {
9884 harness: "codex".into(),
9885 session_id: "svc_acp".into(),
9886 workspace,
9887 },
9888 )
9889 .unwrap();
9890 let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9891 .await
9892 .unwrap();
9893 let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9894 .await
9895 .unwrap();
9896
9897 let sent = service
9898 .handle_async(request(
9899 2,
9900 "harness.v1.runtimes.send_input",
9901 json!({"connection": "runtime-1", "text": "hi"}),
9902 ))
9903 .await;
9904 assert_eq!(sent["result"]["turn_id"], "3");
9905
9906 let mut events = Vec::new();
9907 for _ in 0..20 {
9908 events.extend(service.poll_runtimes().await);
9909 if events.len() >= 2 {
9910 break;
9911 }
9912 tokio::time::sleep(Duration::from_millis(2)).await;
9913 }
9914 assert!(events
9915 .iter()
9916 .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9917 assert!(events.iter().any(|event| {
9918 event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9919 }));
9920
9921 let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9922 loop {
9923 let event = attachment.next_event().await.unwrap();
9924 if event.kind == "text_delta" && event.payload["text"] == "ok" {
9925 break;
9926 }
9927 }
9928 })
9929 .await;
9930 assert!(
9931 saw_editor_reply.is_ok(),
9932 "terminal should observe the editor-driven turn"
9933 );
9934
9935 crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9936 .await
9937 .unwrap();
9938 let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9939 loop {
9940 let event = attachment.next_event().await.unwrap();
9941 if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9942 break;
9943 }
9944 }
9945 })
9946 .await;
9947 assert!(
9948 saw_terminal_reply.is_ok(),
9949 "terminal should drive the same runtime"
9950 );
9951
9952 let closed = service
9953 .handle_async(request(
9954 3,
9955 "harness.v1.runtimes.close",
9956 json!({"connection": "runtime-1"}),
9957 ))
9958 .await;
9959 assert_eq!(closed["result"]["closed"], true);
9960 }
9961
9962 #[test]
9967 fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9968 let home = connect_scratch_home("uni7-running");
9969
9970 assert!(probe_hermes_running(&home, 300_000).is_none());
9977
9978 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9981 let port = listener.local_addr().unwrap().port();
9982 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9983 std::fs::write(
9984 home.join(".openclaw/openclaw.json"),
9985 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9986 )
9987 .unwrap();
9988 let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9989 assert!(matches!(
9990 running.method,
9991 RunningInstanceMethod::GatewayConnect
9992 ));
9993 assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9994 drop(listener);
9995 let mut closed_detected = probe_openclaw_running(&home).is_some();
10000 for _ in 0..3 {
10001 if !closed_detected {
10002 break;
10003 }
10004 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
10005 let port = listener.local_addr().unwrap().port();
10006 drop(listener);
10007 std::fs::write(
10008 home.join(".openclaw/openclaw.json"),
10009 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
10010 )
10011 .unwrap();
10012 closed_detected = probe_openclaw_running(&home).is_some();
10013 }
10014 assert!(
10015 !closed_detected,
10016 "a closed gateway must not read as running"
10017 );
10018
10019 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
10021 let port = listener.local_addr().unwrap().port();
10022 std::fs::write(
10023 home.join(".openclaw/openclaw.json"),
10024 format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
10025 )
10026 .unwrap();
10027 assert!(probe_openclaw_running(&home).is_some());
10028 drop(listener);
10029
10030 std::fs::create_dir_all(home.join(".hermes")).unwrap();
10032 let wal = home.join(".hermes/state.db-wal");
10033 std::fs::write(&wal, b"wal").unwrap();
10034 let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
10035 assert!(matches!(
10036 running.method,
10037 RunningInstanceMethod::StoreWalActivity
10038 ));
10039 assert!(running.evidence.contains("state.db-wal"));
10040 let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
10041 std::fs::File::options()
10042 .append(true)
10043 .open(&wal)
10044 .unwrap()
10045 .set_modified(stale)
10046 .unwrap();
10047 assert!(
10048 probe_hermes_running(&home, 300_000).is_none(),
10049 "a stale WAL (crash leftover) must not read as running"
10050 );
10051 }
10052
10053 fn connect_scratch_home(tag: &str) -> PathBuf {
10054 let dir = std::env::temp_dir().join(format!(
10055 "supercode-connect-service-{tag}-{}-{}",
10056 std::process::id(),
10057 std::time::SystemTime::now()
10058 .duration_since(std::time::UNIX_EPOCH)
10059 .unwrap()
10060 .as_nanos()
10061 ));
10062 std::fs::create_dir_all(&dir).unwrap();
10063 dir
10064 }
10065
10066 async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
10070 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
10071 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
10072 let address = listener.local_addr().unwrap();
10073 let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
10074 tokio::spawn(async move {
10075 loop {
10076 let Ok((mut stream, _)) = listener.accept().await else {
10077 break;
10078 };
10079 let request_sender = request_sender.clone();
10080 tokio::spawn(async move {
10081 let (reader, mut writer) = stream.split();
10082 let mut reader = BufReader::new(reader);
10083 let mut request_line = String::new();
10084 if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
10085 return;
10086 }
10087 let request_line = request_line.trim_end().to_string();
10088 let mut authorization = String::new();
10089 let mut content_length = 0usize;
10090 loop {
10091 let mut line = String::new();
10092 if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
10093 return;
10094 }
10095 let line = line.trim_end();
10096 if line.is_empty() {
10097 break;
10098 }
10099 let lower = line.to_ascii_lowercase();
10100 if let Some(value) = lower.strip_prefix("authorization:") {
10101 authorization = value.trim().to_string();
10102 }
10103 if let Some(value) = lower.strip_prefix("content-length:") {
10104 content_length = value.trim().parse().unwrap_or(0);
10105 }
10106 }
10107 if content_length > 0 {
10108 let mut body = vec![0u8; content_length];
10109 let _ = reader.read_exact(&mut body).await;
10110 }
10111 let _ = request_sender.send(format!("{request_line} :: {authorization}"));
10112 if request_line.starts_with("GET /event") {
10113 let _ = writer
10114 .write_all(
10115 b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
10116 )
10117 .await;
10118 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
10119 return;
10120 }
10121 let body = if request_line.starts_with("POST /session") {
10122 r#"{"id":"mock-session"}"#
10123 } else {
10124 r#"{"status":"ok"}"#
10125 };
10126 let response = format!(
10127 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
10128 body.len(),
10129 body
10130 );
10131 let _ = writer.write_all(response.as_bytes()).await;
10132 });
10133 }
10134 });
10135 (format!("http://{address}"), request_receiver)
10136 }
10137
10138 fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
10139 crate::HarnessSupportDescriptor {
10140 orchestration: Default::default(),
10141 id: HarnessId::from(HarnessId::OPENCODE),
10142 display_name: "OpenCode".into(),
10143 native: crate::NativeSupport {
10144 discover: crate::ImplementationKind::Absent,
10145 load: crate::ImplementationKind::Absent,
10146 follow: crate::ImplementationKind::Absent,
10147 import: crate::ImplementationKind::Absent,
10148 export: crate::ImplementationKind::Absent,
10149 },
10150 runtime: crate::RuntimeSupport {
10151 implementation: crate::ImplementationKind::BuiltIn,
10152 protocol: protocol.into(),
10153 default_launch: None,
10154 connect_launch: Some(crate::RuntimeConnectLaunch {
10155 config_path: "~/opencode-tui.json".into(),
10156 address_pointer: "/server/url".into(),
10157 port_pointer: None,
10158 default_address: None,
10159 auth_pointer: Some("/server/token".into()),
10160 protocol: protocol.into(),
10161 }),
10162 capabilities: crate::RuntimeCapabilities {
10163 start_session: true,
10164 resume_session: true,
10165 attach_existing_process: true,
10166 send_input: true,
10167 stream_events: true,
10168 interrupt: true,
10169 steer: false,
10170 respond_to_requests: true,
10171 },
10172 },
10173 }
10174 }
10175
10176 #[tokio::test]
10177 async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
10178 let (base_url, mut requests) = mock_opencode_endpoint().await;
10179 let home = connect_scratch_home("open");
10180 std::fs::write(
10181 home.join("opencode-tui.json"),
10182 format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
10183 )
10184 .unwrap();
10185
10186 let descriptor = connect_descriptor("opencode-http-sse");
10187 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10188 assert!(backend.capabilities().attach_existing_process);
10189
10190 let connection = backend
10191 .start(crate::RuntimeStartRequest {
10192 cwd: home.clone(),
10193 launch: None,
10194 mcp_servers: Vec::new(),
10195 approval_policy: None,
10196 })
10197 .await
10198 .unwrap();
10199 let handle = connection.handle();
10200 assert_eq!(handle.runtime_id, "mock-session");
10201 match &handle.endpoint {
10202 crate::RuntimeEndpoint::Http {
10203 base_url: endpoint, ..
10204 } => assert_eq!(endpoint, &base_url),
10205 other => panic!("connect mode must join the running endpoint, got {other:?}"),
10206 }
10207
10208 let mut seen = Vec::new();
10209 while let Ok(line) = requests.try_recv() {
10210 seen.push(line);
10211 }
10212 assert!(seen
10213 .iter()
10214 .any(|line| line.starts_with("GET /global/health")
10215 && line.contains("bearer connect-secret")));
10216 assert!(seen.iter().any(
10217 |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
10218 ));
10219 }
10220
10221 #[cfg(unix)] #[tokio::test]
10233 async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
10234 let home = connect_scratch_home("openclaw");
10235 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
10236 std::fs::write(
10237 home.join(".openclaw/openclaw.json"),
10238 r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
10239 )
10240 .unwrap();
10241 let script = home.join("openclaw");
10242 std::fs::write(
10243 &script,
10244 r#"#!/bin/sh
10245# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
10246[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
10247[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
10248[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
10249[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
10250while IFS= read -r line; do
10251 case "$line" in
10252 *'"initialize"'*)
10253 printf '%s
10254' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
10255 *'"session/resume"'*)
10256 printf '%s
10257' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
10258 *'"session/new"'*)
10259 printf '%s
10260' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
10261 *'"session/prompt"'*)
10262 printf '%s
10263' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
10264 printf '%s
10265' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
10266 esac
10267done
10268"#,
10269 )
10270 .unwrap();
10271 use std::os::unix::fs::PermissionsExt;
10272 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
10273
10274 let mut descriptor = crate::harness_support_registry()
10275 .harnesses
10276 .into_iter()
10277 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
10278 .expect("openclaw must be registered");
10279 descriptor
10280 .runtime
10281 .connect_launch
10282 .as_mut()
10283 .unwrap()
10284 .config_path = "~/.openclaw/openclaw.json".into();
10285 descriptor.runtime.default_launch.as_mut().unwrap().program =
10286 script.to_string_lossy().into_owned();
10287 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10288 assert!(backend.capabilities().resume_session);
10289
10290 let joined = backend
10291 .attach(crate::RuntimeAttachRequest {
10292 runtime_id: "agent:main:main".into(),
10293 cwd: Some(home.clone()),
10294 launch: None,
10295 mcp_servers: Vec::new(),
10296 approval_policy: None,
10297 })
10298 .await;
10299 let mut connection = joined.expect("mock bridge attach must succeed");
10300 assert_eq!(connection.handle().runtime_id, "agent:main:main");
10301 let turn = connection
10302 .send_input(crate::RuntimeInput {
10303 text: "hello".into(),
10304 image_urls: Vec::new(),
10305 })
10306 .await;
10307 assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
10308 connection.close().await.unwrap();
10309 }
10310
10311 #[tokio::test]
10312 async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
10313 let home = connect_scratch_home("fail");
10314 std::fs::write(
10315 home.join("opencode-tui.json"),
10316 r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
10317 )
10318 .unwrap();
10319
10320 let gateway_only = connect_descriptor("acp-v1-jsonrpc");
10321 let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
10322 panic!("an ACP connect endpoint has no gateway client yet");
10323 };
10324 let message = format!("{error:?}");
10325 assert!(message.contains("acp-v1-jsonrpc"));
10326 assert!(!message.contains("connect-secret"));
10327
10328 let unreadable = connect_descriptor("opencode-http-sse");
10329 let missing_home = connect_scratch_home("missing");
10330 let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
10331 panic!("an unreadable connect config must fail closed");
10332 };
10333 let message = format!("{error:?}");
10334 assert!(message.contains("opencode-tui.json"));
10335 assert!(!message.contains("connect-secret"));
10336 }
10337
10338 fn jobs_fixture_root() -> PathBuf {
10343 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10344 }
10345
10346 fn jobs_fixture_homes() -> Value {
10349 let root = jobs_fixture_root();
10350 json!({
10351 "claude_code": root.join("claude_jobs_home/projects"),
10352 "hermes": root.join("hermes_home/state.db"),
10353 "openclaw": root.join("openclaw_home"),
10354 })
10355 }
10356
10357 fn jobs_list(params: Value) -> Value {
10358 let mut service = HarnessSessionService::new();
10359 service.handle(request(1, "harness.v1.jobs.list", params))
10360 }
10361
10362 fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10363 result["jobs"]
10364 .as_array()
10365 .expect("jobs is an array")
10366 .iter()
10367 .find(|job| job["id"] == id)
10368 .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10369 }
10370
10371 #[test]
10372 fn gateway_health_derives_from_running_probe_and_install_state() {
10373 let running = RunningInstance {
10374 method: RunningInstanceMethod::GatewayConnect,
10375 evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10376 checked_at_ms: 1,
10377 };
10378 let up = gateway_health(
10379 HarnessId::OPENCLAW,
10380 true,
10381 Some(&running),
10382 Some("2026.7.1-2"),
10383 );
10384 assert_eq!(up.state, GatewayState::Up);
10385 assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10386 assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10387 let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10391 std::fs::create_dir_all(&dir).unwrap();
10392 let fake = dir.join("hermes");
10393 let write_fake = |body: &str| {
10394 std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10395 #[cfg(unix)]
10396 {
10397 use std::os::unix::fs::PermissionsExt;
10398 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10399 }
10400 };
10401 write_fake("echo '✗ Gateway service is not installed'");
10402 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10403 *slot.borrow_mut() = Some((
10404 HarnessId::HERMES.to_string(),
10405 fake.to_string_lossy().into_owned(),
10406 ))
10407 });
10408 let down = gateway_health(HarnessId::HERMES, true, None, None);
10409 assert_eq!(down.state, GatewayState::Down, "{down:?}");
10410 assert!(down.endpoint.is_none());
10411 assert!(down.evidence.contains("not installed"));
10412 write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10413 let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10414 assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10415 assert!(idle_but_up.evidence.contains("PID 4242"));
10416 write_fake("echo 'something unparseable'");
10417 let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10418 assert_eq!(no_verdict.state, GatewayState::Down);
10419 assert!(no_verdict.evidence.contains("no verdict"));
10420 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10421 let absent = gateway_health(HarnessId::HERMES, false, None, None);
10422 assert_eq!(absent.state, GatewayState::Unknown);
10423 let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10424 assert_eq!(core.state, GatewayState::Unknown);
10425 assert!(core.evidence.contains("per session"));
10426 }
10427
10428 #[test]
10429 fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10430 let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10431 let rows = response["result"]["triggers"]
10432 .as_array()
10433 .expect("triggers")
10434 .clone();
10435 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10436 assert!(
10437 hermes.iter().any(|r| r["name"] == "deploys"
10438 && r["route"] == "/webhooks/deploys"
10439 && r["kind"] == "webhook"),
10440 "{rows:#?}"
10441 );
10442 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10443 assert!(openclaw
10444 .iter()
10445 .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10446 assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10447 && r["kind"] == "hook_mapping"
10448 && r["target"]["action"] == "agent"));
10449 let rendered = response.to_string();
10450 for secret in [
10451 "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10452 "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10453 ] {
10454 assert!(!rendered.contains(secret), "{rendered}");
10455 }
10456 let refused =
10457 triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10458 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10459 }
10460
10461 fn triggers_list(params: Value) -> Value {
10462 let mut service = HarnessSessionService::new();
10463 service.handle(request(1, "harness.v1.triggers.list", params))
10464 }
10465
10466 #[test]
10467 fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10468 let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10469 let rows = response["result"]["routes"]
10470 .as_array()
10471 .expect("routes")
10472 .clone();
10473 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10474 assert_eq!(hermes.len(), 2, "{rows:#?}");
10475 assert_eq!(hermes[0]["target"], "coder");
10476 assert_eq!(hermes[0]["match"]["platform"], "slack");
10477 assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10478 assert_eq!(hermes[0]["specificity"], 4);
10479 assert_eq!(hermes[1]["default"], true);
10480 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10481 assert!(
10482 openclaw.iter().any(|r| r["target"] == "design"
10483 && r["match"]["platform"] == "slack"
10484 && r["specificity"] == 1),
10485 "{openclaw:#?}"
10486 );
10487 assert!(openclaw.iter().any(|r| r["default"] == true));
10488 let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10490 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10491 }
10492
10493 fn routes_list(params: Value) -> Value {
10494 let mut service = HarnessSessionService::new();
10495 service.handle(request(1, "harness.v1.routes.list", params))
10496 }
10497
10498 #[test]
10499 fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10500 let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10501 let result = &response["result"];
10502 let ids: Vec<&str> = result["jobs"]
10503 .as_array()
10504 .unwrap()
10505 .iter()
10506 .map(|job| job["id"].as_str().unwrap())
10507 .collect();
10508 assert_eq!(
10509 ids,
10510 vec![
10511 "release-watch",
10512 "toolu_wake_recheck",
10513 "digest-15m",
10514 "nightly-audit",
10515 "coder-standup",
10516 "ops-once-boot",
10517 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10518 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10519 "cron_standup",
10520 "cron_reindex",
10521 ],
10522 "{result}"
10523 );
10524
10525 let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10529 assert_eq!(health["harness"], "openclaw");
10530 assert_eq!(health["schedule"]["kind"], "interval");
10531 assert_eq!(health["schedule"]["minutes"], 10.0);
10532 assert_eq!(health["session_target"], "isolated");
10533 assert_eq!(health["payload"]["kind"], "prompt");
10534 assert_eq!(health["payload"]["text"], "nightly health check");
10535 assert_eq!(health["deliver"]["mode"], "announce");
10539 assert_eq!(health["deliver"]["target"], "last");
10540 assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10541 let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10542 assert_eq!(digest["schedule"]["kind"], "cron");
10543 assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10544 assert_eq!(digest["session_target"], "main");
10545 assert_eq!(digest["payload"]["kind"], "system_event");
10546
10547 let cron = job_row(result, "release-watch");
10549 assert_eq!(cron["harness"], "claude-code");
10550 assert_eq!(cron["scope"], "session");
10551 assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10552 assert_eq!(cron["schedule"]["kind"], "cron");
10553 assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10554 assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10555 assert_eq!(cron["payload"]["kind"], "prompt");
10556 assert_eq!(cron["recurring"], true);
10557 assert_eq!(cron["deliver"]["target"], "session");
10558 let wakeup = job_row(result, "toolu_wake_recheck");
10559 assert_eq!(wakeup["payload"]["kind"], "wakeup");
10560 assert_eq!(wakeup["schedule"]["kind"], "once");
10561 assert_eq!(wakeup["recurring"], false);
10562 assert_eq!(wakeup["state"], "pending");
10563
10564 let interval = job_row(result, "digest-15m");
10566 assert_eq!(interval["harness"], "hermes");
10567 assert_eq!(interval["scope"], "install");
10568 assert_eq!(interval["profile"], Value::Null);
10569 assert_eq!(interval["schedule"]["kind"], "interval");
10570 assert_eq!(interval["schedule"]["minutes"], 15.0);
10571 assert_eq!(interval["schedule"]["display"], "every 15 min");
10572 assert_eq!(interval["deliver"]["target"], "origin");
10573 assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10574 assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10575 assert_eq!(interval["last_status"], "ok");
10576 let nightly = job_row(result, "nightly-audit");
10577 assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10578 assert_eq!(nightly["deliver"]["target"], "local");
10579 assert_eq!(nightly["enabled"], false);
10580 assert_eq!(nightly["state"], "paused");
10581 let profiled = job_row(result, "ops-once-boot");
10583 assert_eq!(profiled["profile"], "ops");
10584 assert_eq!(profiled["schedule"]["kind"], "once");
10585 assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10586 assert_eq!(profiled["payload"]["kind"], "script");
10587 assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10589 assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10590 assert_eq!(profiled["recurring"], false);
10591
10592 let standup_to_group = job_row(result, "coder-standup");
10595 assert_eq!(standup_to_group["deliver"]["target"], "origin");
10596 assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10597 assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10598 assert!(standup_to_group["deliver"]["mode"].is_null());
10600 assert!(standup_to_group["deliver"]["account"].is_null());
10601
10602 let standup = job_row(result, "cron_standup");
10605 assert_eq!(standup["harness"], "openclaw");
10606 assert_eq!(standup["session_target"], "isolated");
10607 assert_eq!(standup["deliver"]["mode"], "announce");
10608 assert_eq!(standup["deliver"]["target"], "slack");
10609 assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10610 assert_eq!(standup["payload"]["kind"], "prompt");
10611 assert_eq!(standup["profile"], "main");
10612 let reindex = job_row(result, "cron_reindex");
10613 assert_eq!(reindex["session_target"], "main");
10614 assert_eq!(reindex["payload"]["kind"], "system_event");
10615 assert_eq!(reindex["schedule"]["kind"], "interval");
10616 assert_eq!(reindex["schedule"]["display"], "every 240 min");
10617 assert_eq!(reindex["enabled"], false);
10618
10619 let states: Vec<(&str, &str)> = result["sources"]
10621 .as_array()
10622 .unwrap()
10623 .iter()
10624 .map(|source| {
10625 (
10626 source["harness"].as_str().unwrap(),
10627 source["state"].as_str().unwrap(),
10628 )
10629 })
10630 .collect();
10631 assert_eq!(
10635 states,
10636 vec![
10637 ("claude-code", "scanned"),
10638 ("hermes", "read"),
10639 ("hermes", "absent_store"),
10640 ("hermes", "read"),
10641 ("openclaw", "read"),
10642 ("openclaw", "read"),
10643 ],
10644 "{result}"
10645 );
10646 }
10647
10648 #[test]
10649 fn jobs_list_filters_by_harness_session_and_profile() {
10650 let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10651 let ids: Vec<&str> = by_harness["result"]["jobs"]
10652 .as_array()
10653 .unwrap()
10654 .iter()
10655 .map(|job| job["id"].as_str().unwrap())
10656 .collect();
10657 assert_eq!(
10658 ids,
10659 vec![
10660 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10661 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10662 "cron_standup",
10663 "cron_reindex",
10664 ]
10665 );
10666
10667 let by_session = jobs_list(json!({
10668 "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10669 "homes": jobs_fixture_homes(),
10670 }));
10671 let jobs = by_session["result"]["jobs"].as_array().unwrap();
10672 assert_eq!(jobs.len(), 2, "{by_session}");
10673 assert!(jobs
10674 .iter()
10675 .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10676
10677 let by_profile = jobs_list(json!({
10678 "harness": "hermes",
10679 "profile": "ops",
10680 "homes": jobs_fixture_homes(),
10681 }));
10682 let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10683 assert_eq!(jobs.len(), 1, "{by_profile}");
10684 assert_eq!(jobs[0]["id"], "ops-once-boot");
10685 }
10686
10687 #[test]
10688 fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10689 let mut service = HarnessSessionService::new();
10690 let hermes = service.handle(request(
10691 1,
10692 "harness.v1.jobs.get",
10693 json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10694 ));
10695 assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10696 assert_eq!(hermes["result"]["source"]["provider"], "nous");
10698 assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10699
10700 let claude = service.handle(request(
10701 2,
10702 "harness.v1.jobs.get",
10703 json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10704 ));
10705 assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10706 assert_eq!(
10707 claude["result"]["source"]["tool_use_id"],
10708 "toolu_cron_release_watch"
10709 );
10710
10711 let missing = service.handle(request(
10712 3,
10713 "harness.v1.jobs.get",
10714 json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10715 ));
10716 assert!(missing["error"]["message"]
10717 .as_str()
10718 .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10719 }
10720
10721 #[test]
10722 fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10723 let mut service = HarnessSessionService::new();
10724 for (id, method, params) in [
10725 (
10726 1,
10727 "harness.v1.jobs.list",
10728 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10729 ),
10730 (
10731 2,
10732 "harness.v1.jobs.get",
10733 json!({"harness": "codex", "id": "anything"}),
10734 ),
10735 ] {
10736 let response = service.handle(request(id, method, params));
10737 assert_eq!(response["error"]["code"], -32020, "{response}");
10738 assert!(response["error"]["message"]
10739 .as_str()
10740 .is_some_and(|message| message.contains("has no scheduled jobs")));
10741 assert!(response.get("result").is_none());
10742 }
10743 }
10744
10745 #[test]
10746 fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10747 let scratch = std::env::temp_dir().join(format!(
10748 "supercode-jobs-migrated-{}-{}",
10749 std::process::id(),
10750 generated_session_id()
10751 ));
10752 std::fs::create_dir_all(&scratch).unwrap();
10753 let response = jobs_list(json!({
10754 "harness": "openclaw",
10755 "homes": {"openclaw": scratch.clone()},
10756 }));
10757 let result = &response["result"];
10758 assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10759 assert_eq!(result["sources"][0]["state"], "absent_store");
10760 assert_eq!(result["sources"][0]["harness"], "openclaw");
10761 std::fs::remove_dir_all(&scratch).ok();
10762 }
10763
10764 const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10773 const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10775
10776 fn runs_list(params: Value) -> Value {
10777 let mut service = HarnessSessionService::new();
10778 service.handle(request(1, "harness.v1.runs.list", params))
10779 }
10780
10781 fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10782 result["runs"]
10783 .as_array()
10784 .expect("runs is an array")
10785 .iter()
10786 .find(|run| run["id"] == id)
10787 .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10788 }
10789
10790 #[test]
10791 fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10792 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10793 let result = &response["result"];
10794 let ids: Vec<&str> = result["runs"]
10795 .as_array()
10796 .expect("runs is an array")
10797 .iter()
10798 .map(|run| run["id"].as_str().unwrap())
10799 .collect();
10800 let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10801 assert_eq!(
10802 ids,
10803 vec![
10804 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10806 "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10807 "c3d4e5f60718293a4b5c6d7e8f901234",
10808 "f60718293a4b5c6d7e8f901234567890",
10809 "e5f60718293a4b5c6d7e8f9012345678",
10810 "d4e5f60718293a4b5c6d7e8f90123456",
10811 "run_health_0002",
10813 digest_fire.as_str(),
10814 "run_health_0001",
10815 ],
10816 "{result}"
10817 );
10818
10819 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10822 assert_eq!(failed["harness"], "hermes");
10823 assert_eq!(failed["job_id"], "job42");
10824 assert_eq!(failed["status"], "failed");
10825 assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10826 assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10827
10828 let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10831 assert_eq!(abandoned["status"], "unknown");
10832 assert_eq!(abandoned["job_id"], "ops-once-boot");
10833
10834 let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10836 assert_eq!(running["status"], "running");
10837 assert!(running["finished_at"].is_null(), "{running}");
10838 assert!(running["session_id"].is_null(), "{running}");
10839
10840 let ok = run_row(result, "run_health_0001");
10843 assert_eq!(ok["harness"], "openclaw");
10844 assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10845 assert_eq!(ok["status"], "ok");
10846 assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10847 assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10848 assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10849 assert!(ok["claimed_at"].is_null(), "{ok}");
10851
10852 assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10855
10856 for id in [
10861 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10862 "d4e5f60718293a4b5c6d7e8f90123456",
10863 ] {
10864 assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10865 }
10866
10867 let sources = result["sources"].as_array().unwrap();
10870 let states: Vec<(&str, &str)> = sources
10871 .iter()
10872 .map(|source| {
10873 (
10874 source["harness"].as_str().unwrap(),
10875 source["state"].as_str().unwrap(),
10876 )
10877 })
10878 .collect();
10879 assert_eq!(
10880 states,
10881 vec![
10882 ("hermes", "read"),
10883 ("hermes", "absent_store"),
10884 ("hermes", "read"),
10885 ("openclaw", "read"),
10886 ],
10887 "{result}"
10888 );
10889 assert_eq!(sources[2]["profile"], "ops");
10890 assert!(sources[3]["path"]
10891 .as_str()
10892 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10893 }
10894
10895 #[test]
10896 fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10897 let response = runs_list(json!({
10898 "harness": "hermes",
10899 "job": "job42",
10900 "homes": jobs_fixture_homes(),
10901 }));
10902 let result = &response["result"];
10903 assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10904
10905 let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10909 assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10910
10911 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10915 assert!(failed["session_id"].is_null(), "{failed}");
10916 }
10917
10918 #[test]
10922 fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10923 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10924 let result = &response["result"];
10925
10926 let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10930 assert_eq!(delivered["status"], "completed");
10931 assert_eq!(delivered["delivery"]["state"], "delivered");
10932 assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10933 assert_eq!(delivered["delivery"]["attempts"], 1);
10934 assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10935 assert_eq!(
10936 delivered["delivery"]["delivered_at"],
10937 "2026-09-02T09:00:30.400Z"
10938 );
10939
10940 let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10943 assert_eq!(undelivered["status"], "completed");
10944 assert_eq!(undelivered["delivery"]["state"], "failed");
10945 assert_eq!(undelivered["delivery"]["attempts"], 3);
10946 assert_eq!(
10947 undelivered["delivery"]["last_error"],
10948 "telegram send failed: Bad Request: chat not found"
10949 );
10950 assert!(
10953 undelivered["delivery"]["delivered_at"].is_null(),
10954 "{undelivered}"
10955 );
10956
10957 let announced = run_row(result, "run_health_0001");
10960 assert_eq!(announced["delivery"]["state"], "delivered");
10961 assert_eq!(announced["delivery"]["target"], "last");
10962 assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10964 assert!(
10965 announced["delivery"]["delivered_at"].is_null(),
10966 "{announced}"
10967 );
10968 let refused = run_row(result, "run_health_0002");
10969 assert_eq!(refused["delivery"]["state"], "not-delivered");
10970 assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10971
10972 let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10975 assert!(skipped["delivery"].is_null(), "{skipped}");
10976 }
10977
10978 #[test]
10983 fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10984 let scratch = std::env::temp_dir().join(format!(
10985 "supercode-runs-delivery-{}-{}",
10986 std::process::id(),
10987 generated_session_id()
10988 ));
10989 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10990 let fixture = jobs_fixture_root().join("hermes_home");
10991 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10992 for name in ["cron/executions.db", "cron/jobs.json"] {
10993 std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10994 }
10995 {
10996 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10997 connection
11000 .execute(
11001 "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
11002 [],
11003 )
11004 .unwrap();
11005 connection
11008 .execute(
11009 "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
11010 ('cron_coder-standup_20260902_090010', 'cron', \
11011 'agent:coder:telegram:group:-100777:55', 1788339610.0)",
11012 [],
11013 )
11014 .unwrap();
11015 }
11016 let response = runs_list(json!({
11017 "harness": "hermes",
11018 "job": "coder-standup",
11019 "homes": {"hermes": scratch.join("state.db")},
11020 }));
11021 let result = &response["result"];
11022 let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
11023 assert_eq!(
11024 matched["session_id"], "cron_coder-standup_20260902_090010",
11025 "{result}"
11026 );
11027 assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
11028 assert_eq!(
11029 matched["delivery"]["target"], "slack:C0FALLBACK:55",
11030 "{result}"
11031 );
11032 std::fs::remove_dir_all(&scratch).ok();
11033 }
11034
11035 #[test]
11036 fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
11037 let scratch = std::env::temp_dir().join(format!(
11042 "supercode-runs-compressed-{}-{}",
11043 std::process::id(),
11044 generated_session_id()
11045 ));
11046 std::fs::create_dir_all(scratch.join("cron")).unwrap();
11047 let fixture = jobs_fixture_root().join("hermes_home");
11048 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
11049 std::fs::copy(
11050 fixture.join("cron/executions.db"),
11051 scratch.join("cron/executions.db"),
11052 )
11053 .unwrap();
11054 {
11055 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
11056 connection
11057 .execute(
11058 "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
11059 ["cron_job42_20260902_120000"],
11060 )
11061 .unwrap();
11062 connection
11063 .execute(
11064 "INSERT INTO sessions (id, source, parent_session_id, started_at) \
11065 VALUES ('job42-after-compaction', 'cron', \
11066 'cron_job42_20260902_120000', 1788350000.0)",
11067 [],
11068 )
11069 .unwrap();
11070 }
11071 let response = runs_list(json!({
11072 "harness": "hermes",
11073 "job": "job42",
11074 "homes": {"hermes": scratch.join("state.db")},
11075 }));
11076 let result = &response["result"];
11077 assert_eq!(
11078 run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
11079 "job42-after-compaction",
11080 "{result}"
11081 );
11082 std::fs::remove_dir_all(&scratch).ok();
11083 }
11084
11085 #[test]
11086 fn runs_list_filters_by_job_and_caps_by_limit() {
11087 let by_job = runs_list(json!({
11088 "harness": "openclaw",
11089 "job": OPENCLAW_HEALTH_JOB,
11090 "homes": jobs_fixture_homes(),
11091 }));
11092 let ids: Vec<&str> = by_job["result"]["runs"]
11093 .as_array()
11094 .unwrap()
11095 .iter()
11096 .map(|run| run["id"].as_str().unwrap())
11097 .collect();
11098 assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
11099
11100 let capped = runs_list(json!({
11101 "harness": "openclaw",
11102 "limit": 1,
11103 "homes": jobs_fixture_homes(),
11104 }));
11105 let runs = capped["result"]["runs"].as_array().unwrap();
11106 assert_eq!(runs.len(), 1, "{capped}");
11107 assert_eq!(runs[0]["id"], "run_health_0002");
11109 }
11110
11111 #[test]
11112 fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
11113 let mut service = HarnessSessionService::new();
11114 let hermes = service.handle(request(
11115 1,
11116 "harness.v1.runs.get",
11117 json!({
11118 "harness": "hermes",
11119 "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
11120 "homes": jobs_fixture_homes(),
11121 }),
11122 ));
11123 assert_eq!(hermes["result"]["run"]["status"], "completed");
11124 assert_eq!(
11125 hermes["result"]["run"]["session_id"],
11126 "cron_job42_20260902_120000"
11127 );
11128 assert_eq!(hermes["result"]["source"]["source"], "scheduler");
11130 assert_eq!(hermes["result"]["source"]["pid"], 4242);
11131 assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
11132
11133 let openclaw = service.handle(request(
11134 2,
11135 "harness.v1.runs.get",
11136 json!({
11137 "harness": "openclaw",
11138 "id": "run_health_0002",
11139 "homes": jobs_fixture_homes(),
11140 }),
11141 ));
11142 assert_eq!(openclaw["result"]["run"]["status"], "error");
11143 assert_eq!(
11146 openclaw["result"]["source"]["delivery_status"],
11147 "not-delivered"
11148 );
11149 assert_eq!(
11150 openclaw["result"]["source"]["delivery_error"],
11151 "channel_not_found"
11152 );
11153 assert_eq!(openclaw["result"]["source"]["delivered"], 0);
11154 assert_eq!(
11155 openclaw["result"]["run"]["delivery"]["state"],
11156 "not-delivered"
11157 );
11158 assert_eq!(
11159 openclaw["result"]["run"]["delivery"]["last_error"],
11160 "channel_not_found"
11161 );
11162
11163 let missing = service.handle(request(
11164 3,
11165 "harness.v1.runs.get",
11166 json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
11167 ));
11168 assert!(missing["error"]["message"]
11169 .as_str()
11170 .is_some_and(|message| message.contains("no run `no-such-run`")));
11171 }
11172
11173 #[test]
11174 fn runs_refuse_a_harness_that_keeps_no_run_store() {
11175 let mut service = HarnessSessionService::new();
11176 for (id, method, params) in [
11177 (
11180 1,
11181 "harness.v1.runs.list",
11182 json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
11183 ),
11184 (
11185 2,
11186 "harness.v1.runs.get",
11187 json!({"harness": "claude-code", "id": "anything"}),
11188 ),
11189 (
11190 3,
11191 "harness.v1.runs.list",
11192 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
11193 ),
11194 ] {
11195 let response = service.handle(request(id, method, params));
11196 assert_eq!(response["error"]["code"], -32020, "{response}");
11197 assert!(response["error"]["message"]
11198 .as_str()
11199 .is_some_and(|message| message.contains("keeps no run store")));
11200 assert!(response.get("result").is_none());
11201 }
11202 }
11203
11204 #[test]
11205 fn runs_list_reports_an_install_with_no_run_store_as_absent() {
11206 let scratch = std::env::temp_dir().join(format!(
11207 "supercode-runs-empty-{}-{}",
11208 std::process::id(),
11209 generated_session_id()
11210 ));
11211 std::fs::create_dir_all(&scratch).unwrap();
11212 let response = runs_list(json!({
11213 "harness": "openclaw",
11214 "homes": {"openclaw": scratch.clone()},
11215 }));
11216 let result = &response["result"];
11217 assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
11218 assert_eq!(result["sources"][0]["state"], "absent_store");
11219 assert!(result["sources"][0]["path"]
11220 .as_str()
11221 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
11222 std::fs::remove_dir_all(&scratch).ok();
11223 }
11224}