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)]
3766#[serde(rename_all = "snake_case")]
3767enum ResumePolicy {
3768 #[default]
3769 Default,
3770 Yolo,
3771}
3772
3773#[derive(Deserialize)]
3774struct ResumeInstructionsParams {
3775 locator: SessionLocator,
3776 #[serde(default)]
3777 cwd: Option<PathBuf>,
3778 #[serde(default)]
3779 policy: ResumePolicy,
3780}
3781
3782#[derive(Deserialize)]
3784struct WorkflowLoadParams {
3785 from: crate::workflow_doors::WorkflowHarness,
3786 home: PathBuf,
3787}
3788
3789#[derive(Deserialize)]
3792struct OrchestrationLoadParams {
3793 root: PathBuf,
3794 #[serde(default)]
3795 flavor: crate::orchestration_doors::HomeFlavor,
3796}
3797
3798#[derive(Deserialize)]
3801struct OrchestrationSaveParams {
3802 root: PathBuf,
3803 orchestration: crate::orchestration::Orchestration,
3804 #[serde(default)]
3805 vault: BTreeMap<String, String>,
3806}
3807
3808#[derive(Deserialize)]
3810struct OrchestrationCompileParams {
3811 from: crate::orchestration_doors::OrchestrationHarness,
3812 home: PathBuf,
3813}
3814
3815#[derive(Deserialize)]
3819struct OrchestrationDecompileParams {
3820 to: crate::orchestration_doors::OrchestrationHarness,
3821 orchestration: crate::orchestration::Orchestration,
3822 source: PathBuf,
3823 #[serde(default)]
3824 source_flavor: crate::orchestration_doors::SourceFlavor,
3825 dest: PathBuf,
3826 #[serde(default)]
3827 vault: BTreeMap<String, String>,
3828}
3829
3830#[derive(Deserialize)]
3833struct OrchestrationImportParams {
3834 from: crate::orchestration_doors::OrchestrationHarness,
3835 home: PathBuf,
3836 into: PathBuf,
3837}
3838
3839#[derive(Deserialize)]
3842struct OrchestrationExportParams {
3843 to: crate::orchestration_doors::OrchestrationHarness,
3844 root: PathBuf,
3845 dest: PathBuf,
3846}
3847
3848#[derive(Deserialize)]
3850struct JobsGetParams {
3851 harness: String,
3852 id: String,
3853 #[serde(default)]
3854 homes: crate::HarnessHomes,
3855}
3856
3857fn mutate_job(
3865 verb: crate::jobs_control::JobVerb,
3866 params: Value,
3867) -> std::result::Result<Value, ServiceError> {
3868 let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3869 refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3870 let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3871 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3872}
3873
3874fn mutate_skill(
3881 verb: crate::skills_control::SkillVerb,
3882 params: Value,
3883) -> std::result::Result<Value, ServiceError> {
3884 let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3885 if !crate::skills_control::supports_skill_control(&mutation.harness) {
3886 return Err(ServiceError::UnsupportedAction(format!(
3887 "`{}` has no skills root Volter Harness reads; `skills.{}` is supported for: {}",
3888 mutation.harness,
3889 verb.as_str(),
3890 crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3891 )));
3892 }
3893 let outcome =
3894 crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3895 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3896}
3897
3898fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3900 match error {
3901 crate::skills_control::SkillControlError::Unsupported(message) => {
3902 ServiceError::UnsupportedAction(message)
3903 }
3904 crate::skills_control::SkillControlError::Invalid(message) => {
3905 ServiceError::InvalidParams(message)
3906 }
3907 crate::skills_control::SkillControlError::Failed(message) => {
3908 ServiceError::Operation(message)
3909 }
3910 }
3911}
3912
3913fn mutate_profile(
3921 verb: crate::profiles_control::ProfileVerb,
3922 params: Value,
3923) -> std::result::Result<Value, ServiceError> {
3924 let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3925 let outcome =
3926 crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3927 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3928}
3929
3930fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3932 match error {
3933 crate::profiles_control::ProfileControlError::Unsupported(message) => {
3934 ServiceError::UnsupportedAction(message)
3935 }
3936 crate::profiles_control::ProfileControlError::Invalid(message) => {
3937 ServiceError::InvalidParams(message)
3938 }
3939 crate::profiles_control::ProfileControlError::Failed(message) => {
3940 ServiceError::Operation(message)
3941 }
3942 }
3943}
3944
3945fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3949 match error {
3950 crate::jobs_control::JobControlError::Unsupported(message) => {
3951 ServiceError::UnsupportedAction(message)
3952 }
3953 crate::jobs_control::JobControlError::Invalid(message) => {
3954 ServiceError::InvalidParams(message)
3955 }
3956 crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3957 }
3958}
3959
3960fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3965 match error {
3966 crate::SessionControlError::Unsupported(message) => {
3967 ServiceError::UnsupportedAction(message)
3968 }
3969 crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3970 crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3971 }
3972}
3973
3974fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3979 if crate::jobs::supports_jobs(harness) {
3980 return Ok(());
3981 }
3982 Err(ServiceError::UnsupportedAction(format!(
3983 "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3984 crate::jobs::JOB_HARNESSES.join(", ")
3985 )))
3986}
3987
3988#[derive(Deserialize)]
3990struct RunsGetParams {
3991 harness: String,
3992 id: String,
3993 #[serde(default)]
3994 homes: crate::HarnessHomes,
3995}
3996
3997fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
4002 if crate::runs::supports_runs(harness) {
4003 return Ok(());
4004 }
4005 Err(ServiceError::UnsupportedAction(format!(
4006 "`{harness}` keeps no run store; `{verb}` is supported for: {}",
4007 crate::runs::RUN_HARNESSES.join(", ")
4008 )))
4009}
4010
4011#[derive(Serialize)]
4012struct SessionArtifact {
4013 source_harness: HarnessId,
4014 target_harness: &'static str,
4015 session_id: Option<String>,
4016 content: String,
4017 suggested_filename: String,
4018 files: Vec<SessionArtifactFile>,
4019 fidelity: Fidelity,
4020 residue: Vec<String>,
4021}
4022
4023#[derive(Serialize)]
4024struct SessionArtifactFile {
4025 path: String,
4026 content: String,
4027 role: ArtifactFileRole,
4028}
4029
4030#[derive(Serialize)]
4031#[serde(rename_all = "snake_case")]
4032enum ArtifactFileRole {
4033 Primary,
4034 Subagent,
4035 Bundle,
4036 SourceRecovery,
4037}
4038
4039#[derive(Serialize)]
4040struct StructuredLaunch {
4041 cwd: PathBuf,
4042 program: String,
4043 arguments: Vec<String>,
4044 env: BTreeMap<String, String>,
4045}
4046
4047struct HandoffInstructions {
4048 launch: StructuredLaunch,
4049 materialize: Option<StructuredLaunch>,
4050 requires_materialization: bool,
4051 note: String,
4052}
4053
4054#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4055#[serde(rename_all = "snake_case")]
4056enum HarnessProbeLevel {
4057 #[default]
4058 Passive,
4059 Handshake,
4060}
4061
4062#[derive(Default, Deserialize)]
4063#[serde(default)]
4064struct HarnessInventoryParams {
4065 harness: Option<HarnessId>,
4066 harnesses: Vec<HarnessId>,
4067 workspace: Option<PathBuf>,
4068 probe: HarnessProbeLevel,
4069 include_sessions: bool,
4070 skip_versions: bool,
4072}
4073
4074#[derive(Deserialize)]
4075struct HarnessAuthenticationParams {
4076 harness: HarnessId,
4077}
4078
4079#[derive(Deserialize)]
4080struct BeginHarnessAuthenticationParams {
4081 harness: HarnessId,
4082 #[serde(default = "local_browser_authentication_environment")]
4083 environment: crate::HarnessAuthenticationEnvironment,
4084 #[serde(default)]
4085 method: Option<crate::HarnessAuthenticationMethodId>,
4086 #[serde(default)]
4087 cwd: Option<PathBuf>,
4088}
4089
4090fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
4091 crate::HarnessAuthenticationEnvironment::LocalBrowser
4092}
4093
4094#[derive(Serialize)]
4095struct HarnessInventoryReport {
4096 probe: HarnessProbeLevel,
4097 workspace: Option<PathBuf>,
4098 harnesses: Vec<LocalHarness>,
4099}
4100
4101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4102#[serde(rename_all = "snake_case")]
4103enum HarnessAuthState {
4104 Ready,
4105 Configured,
4106 Required,
4107 Unknown,
4108}
4109
4110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4111#[serde(rename_all = "snake_case")]
4112enum HarnessRuntimeState {
4113 Ready,
4114 Degraded,
4115 Unavailable,
4116}
4117
4118#[derive(Serialize)]
4119struct HarnessSessionCounts {
4120 global: Option<usize>,
4121 workspace: Option<usize>,
4122}
4123
4124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4135#[serde(rename_all = "snake_case")]
4136pub enum GatewayState {
4137 Up,
4138 Down,
4139 Unknown,
4140}
4141
4142#[derive(Debug, Clone, Serialize)]
4144pub struct GatewayHealth {
4145 pub state: GatewayState,
4146 #[serde(skip_serializing_if = "Option::is_none")]
4151 pub endpoint: Option<String>,
4152 #[serde(skip_serializing_if = "Option::is_none")]
4153 pub version: Option<String>,
4154 pub evidence: String,
4156 pub checked_at_ms: u64,
4157}
4158
4159fn openclaw_gateway_endpoint(home: &Path) -> String {
4163 let config_path = home.join(".openclaw/openclaw.json");
4164 let gateway = std::fs::read_to_string(&config_path)
4165 .ok()
4166 .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
4167 .and_then(|config| config.get("gateway").cloned());
4168 if let Some(url) = gateway
4169 .as_ref()
4170 .and_then(|gateway| gateway.get("url"))
4171 .and_then(serde_json::Value::as_str)
4172 {
4173 return url.to_string();
4174 }
4175 let port = gateway
4176 .as_ref()
4177 .and_then(|gateway| gateway.get("port"))
4178 .and_then(serde_json::Value::as_u64)
4179 .unwrap_or(18789);
4180 format!("ws://127.0.0.1:{port}")
4181}
4182
4183fn hermes_gateway_status() -> Option<(GatewayState, String)> {
4190 let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
4191 let output = std::process::Command::new(&program)
4192 .args(["gateway", "status"])
4193 .stdin(std::process::Stdio::null())
4194 .output()
4195 .ok()?;
4196 let text = format!(
4197 "{}{}",
4198 String::from_utf8_lossy(&output.stdout),
4199 String::from_utf8_lossy(&output.stderr)
4200 );
4201 let verdict = text.lines().find_map(|line| {
4202 let l = line.trim();
4203 if l.contains("supervised by launchd (PID")
4204 || l.contains("supervised by systemd (PID")
4205 || l.contains("Gateway is running")
4206 || l.contains("process is running")
4207 {
4208 Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
4209 } else if l.contains("not running") || l.contains("not installed") {
4210 Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
4211 } else {
4212 None
4213 }
4214 });
4215 verdict
4216}
4217
4218fn gateway_health(
4219 id: &str,
4220 installed: bool,
4221 running: Option<&RunningInstance>,
4222 version: Option<&str>,
4223) -> GatewayHealth {
4224 let checked_at_ms = now_epoch_ms();
4225 let home = supercode_interchange::user_home()
4226 .map(std::path::PathBuf::into_os_string)
4227 .map(PathBuf::from);
4228 match id {
4229 HarnessId::HERMES | HarnessId::OPENCLAW => {
4230 let endpoint = (id == HarnessId::OPENCLAW)
4231 .then(|| home.as_deref().map(openclaw_gateway_endpoint))
4232 .flatten();
4233 let (state, evidence) = match running {
4234 Some(instance) => (GatewayState::Up, instance.evidence.clone()),
4235 None if !installed => (
4236 GatewayState::Unknown,
4237 format!("`{id}` is not installed; no gateway to probe"),
4238 ),
4239 None if id == HarnessId::HERMES => match hermes_gateway_status() {
4240 Some((state, evidence)) => (state, evidence),
4243 None => (
4244 GatewayState::Down,
4245 "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
4246 ),
4247 },
4248 None => (
4249 GatewayState::Down,
4250 format!(
4251 "no TCP listener at {}",
4252 endpoint.as_deref().unwrap_or("the gateway endpoint")
4253 ),
4254 ),
4255 };
4256 GatewayHealth {
4257 state,
4258 endpoint,
4259 version: version.map(str::to_string),
4260 evidence,
4261 checked_at_ms,
4262 }
4263 }
4264 HarnessId::ORCHESTRATOR => {
4271 let root = crate::HarnessHomes::default().orchestrator;
4272 let (state, evidence) = match crate::orchestrator::read_lease(&root) {
4273 Some(lease) if lease.is_live() => (
4274 GatewayState::Up,
4275 format!(
4276 "`{}` names pid {} (started {}), which is live",
4277 crate::orchestrator::lock_path(&root).display(),
4278 lease.pid,
4279 lease.started_at
4280 ),
4281 ),
4282 Some(lease) => (
4283 GatewayState::Down,
4284 format!(
4285 "stale lease `{}`: pid {} is gone",
4286 crate::orchestrator::lock_path(&root).display(),
4287 lease.pid
4288 ),
4289 ),
4290 None => (
4291 GatewayState::Down,
4292 format!(
4293 "no lease at `{}`; `supercode orchestrator start` writes one",
4294 crate::orchestrator::lock_path(&root).display()
4295 ),
4296 ),
4297 };
4298 GatewayHealth {
4299 state,
4300 endpoint: None,
4301 version: version.map(str::to_string),
4302 evidence,
4303 checked_at_ms,
4304 }
4305 }
4306 _ => GatewayHealth {
4307 state: GatewayState::Unknown,
4308 endpoint: None,
4309 version: version.map(str::to_string),
4310 evidence: format!("`{id}` runs per session, not as a gateway"),
4311 checked_at_ms,
4312 },
4313 }
4314}
4315
4316#[derive(Debug, Clone, Serialize)]
4317struct RunningInstance {
4318 method: RunningInstanceMethod,
4320 evidence: String,
4322 checked_at_ms: u64,
4324}
4325
4326#[derive(Debug, Clone, Copy, Serialize)]
4327#[serde(rename_all = "snake_case")]
4328enum RunningInstanceMethod {
4329 GatewayConnect,
4332 StoreWalActivity,
4335}
4336
4337fn now_epoch_ms() -> u64 {
4338 std::time::SystemTime::now()
4339 .duration_since(std::time::UNIX_EPOCH)
4340 .map(|elapsed| elapsed.as_millis() as u64)
4341 .unwrap_or(0)
4342}
4343
4344fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4348 let config_path = home.join(".openclaw/openclaw.json");
4349 let text = std::fs::read_to_string(&config_path).ok();
4350 let gateway = text
4351 .as_deref()
4352 .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4353 .and_then(|config| config.get("gateway").cloned());
4354 let address = gateway
4355 .as_ref()
4356 .and_then(|gateway| gateway.get("url"))
4357 .and_then(serde_json::Value::as_str)
4358 .and_then(|url| {
4359 url.split("://").nth(1).map(|rest| {
4360 rest.trim_end_matches('/')
4361 .split('/')
4362 .next()
4363 .unwrap_or(rest)
4364 .to_string()
4365 })
4366 })
4367 .unwrap_or_else(|| {
4368 let port = gateway
4369 .as_ref()
4370 .and_then(|gateway| gateway.get("port"))
4371 .and_then(serde_json::Value::as_u64)
4372 .unwrap_or(18789);
4373 format!("127.0.0.1:{port}")
4374 });
4375 let reachable = std::net::TcpStream::connect_timeout(
4376 &address.parse().ok()?,
4377 std::time::Duration::from_millis(400),
4378 )
4379 .is_ok();
4380 reachable.then(|| RunningInstance {
4381 method: RunningInstanceMethod::GatewayConnect,
4382 evidence: format!(
4383 "gateway endpoint {address} accepted a TCP connect (from {})",
4384 config_path.display()
4385 ),
4386 checked_at_ms: now_epoch_ms(),
4387 })
4388}
4389
4390fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4395 let wal = home.join(".hermes/state.db-wal");
4396 let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4397 let age_ms = std::time::SystemTime::now()
4398 .duration_since(modified)
4399 .map(|age| age.as_millis() as u64)
4400 .unwrap_or(u64::MAX);
4401 (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4402 method: RunningInstanceMethod::StoreWalActivity,
4403 evidence: format!(
4404 "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4405 wal.display()
4406 ),
4407 checked_at_ms: now_epoch_ms(),
4408 })
4409}
4410
4411fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4413 let home = supercode_interchange::user_home()
4414 .map(std::path::PathBuf::into_os_string)
4415 .map(PathBuf::from)?;
4416 match id {
4417 HarnessId::OPENCLAW => probe_openclaw_running(&home),
4418 HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4419 _ => None,
4420 }
4421}
4422
4423#[derive(Serialize)]
4424struct LocalHarness {
4425 id: HarnessId,
4426 display_name: String,
4427 supported: bool,
4428 installed: bool,
4429 executable: Option<String>,
4430 version: Option<String>,
4431 auth: HarnessAuthState,
4432 runtime: HarnessRuntimeState,
4433 protocol: String,
4434 capabilities: crate::RuntimeCapabilities,
4435 effective_capabilities: crate::RuntimeCapabilities,
4436 sessions: HarnessSessionCounts,
4437 #[serde(skip_serializing_if = "Option::is_none")]
4440 running: Option<RunningInstance>,
4441 gateway: GatewayHealth,
4443 reason: Option<String>,
4444 repair: Option<String>,
4445}
4446
4447#[derive(Clone, Deserialize)]
4448struct RuntimeBackendParams {
4449 harness: HarnessId,
4450 #[serde(default)]
4451 protocol: Option<String>,
4452 #[serde(default)]
4453 launch: Option<RuntimeLaunch>,
4454 #[serde(default)]
4455 base_url: Option<String>,
4456 #[serde(default)]
4457 policy: RuntimePolicy,
4458}
4459
4460#[derive(Debug, Clone, Copy, Default, Deserialize)]
4461#[serde(rename_all = "snake_case")]
4462enum RuntimePolicy {
4463 #[default]
4464 Default,
4465 Yolo,
4466}
4467
4468#[derive(Deserialize)]
4469struct RuntimeStartParams {
4470 #[serde(flatten)]
4471 backend: RuntimeBackendParams,
4472 cwd: PathBuf,
4473 #[serde(default)]
4476 mcp_servers: Vec<crate::McpServerLaunch>,
4477 #[serde(default)]
4479 approval_policy: Option<String>,
4480}
4481
4482#[derive(Deserialize)]
4483struct RuntimeAttachParams {
4484 #[serde(flatten)]
4485 backend: RuntimeBackendParams,
4486 runtime_id: String,
4487 #[serde(default)]
4488 cwd: Option<PathBuf>,
4489 #[serde(default)]
4492 mcp_servers: Vec<crate::McpServerLaunch>,
4493 #[serde(default)]
4495 approval_policy: Option<String>,
4496}
4497
4498#[derive(Deserialize)]
4499struct RuntimeConnectionParams {
4500 connection: String,
4501}
4502
4503#[derive(Deserialize)]
4504struct RuntimeInputParams {
4505 connection: String,
4506 text: String,
4507 #[serde(default)]
4508 image_urls: Vec<String>,
4509}
4510
4511const MAX_RUNTIME_IMAGES: usize = 4;
4512const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4513const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4514
4515fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4516 if image_urls.len() > MAX_RUNTIME_IMAGES {
4517 return Err(ServiceError::InvalidParams(format!(
4518 "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4519 )));
4520 }
4521 let mut total = 0usize;
4522 for url in &image_urls {
4523 if !(url.starts_with("data:image/")
4524 || url.starts_with("https://")
4525 || url.starts_with("http://"))
4526 {
4527 return Err(ServiceError::InvalidParams(
4528 "runtime images must be image data URLs or HTTP(S) URLs".into(),
4529 ));
4530 }
4531 if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4532 return Err(ServiceError::InvalidParams(format!(
4533 "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4534 )));
4535 }
4536 total = total.saturating_add(url.len());
4537 }
4538 if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4539 return Err(ServiceError::InvalidParams(format!(
4540 "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4541 )));
4542 }
4543 Ok(image_urls)
4544}
4545
4546#[derive(Deserialize)]
4547struct RuntimeRespondParams {
4548 connection: String,
4549 request_id: Value,
4550 response: Value,
4551}
4552
4553fn default_reduction_store_root() -> PathBuf {
4554 if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4555 return PathBuf::from(root).join("sessions");
4556 }
4557 if let Some(home) = supercode_interchange::user_home().map(std::path::PathBuf::into_os_string) {
4558 return PathBuf::from(home).join(".supercode").join("sessions");
4559 }
4560 PathBuf::from(".supercode").join("sessions")
4561}
4562
4563fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4564 let mut output = String::new();
4565 for message in messages {
4566 output.push_str(
4567 &serde_json::to_string(message)
4568 .map_err(|error| ServiceError::Operation(error.to_string()))?,
4569 );
4570 output.push('\n');
4571 }
4572 Ok(output)
4573}
4574
4575fn parse_messages_jsonl(
4576 content: &str,
4577) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4578 content
4579 .lines()
4580 .enumerate()
4581 .filter(|(_, line)| !line.trim().is_empty())
4582 .map(|(index, line)| {
4583 serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4584 ServiceError::Operation(format!(
4585 "reduced transcript line {} is invalid: {error}",
4586 index + 1
4587 ))
4588 })
4589 })
4590 .collect()
4591}
4592
4593fn reduced_bootstrap_prompt(
4594 source: &SessionLocator,
4595 target: TransferFormat,
4596 view_jsonl: &str,
4597 sidecar_path: &Path,
4598 reduction_log_path: &Path,
4599) -> String {
4600 format!(
4601 "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4602 \n\
4603 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\
4604 \n\
4605 <supercode-reduced-session source-session=\"{source_id}\">\n\
4606 {view_jsonl}\
4607 </supercode-reduced-session>\n\
4608 \n\
4609 Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4610 source_harness = source.harness.as_str(),
4611 target_harness = target.id(),
4612 sidecar = sidecar_path.display(),
4613 log = reduction_log_path.display(),
4614 source_id = source.session_id,
4615 )
4616}
4617
4618fn session_artifact(
4619 locator: &SessionLocator,
4620 session: &Session,
4621 target: TransferFormat,
4622) -> std::result::Result<SessionArtifact, ServiceError> {
4623 session_artifact_with_id(locator, session, target, None)
4624}
4625
4626fn session_artifact_with_id(
4627 locator: &SessionLocator,
4628 session: &Session,
4629 target: TransferFormat,
4630 target_session_id: Option<&str>,
4631) -> std::result::Result<SessionArtifact, ServiceError> {
4632 let format: SessionFormat = target.into();
4633 let diagonal = format.source() == session.meta.source;
4634 crate::residue_store::store_segments(session);
4635 let has_appended_turns = session
4636 .imported_message_count
4637 .is_some_and(|imported| imported < session.messages.len());
4638 let mut restoration = None;
4639 let content = if let Some(id) = target_session_id {
4640 if diagonal && format != SessionFormat::OpenCode {
4641 session
4642 .to_jsonl_spliced(format, Some(id))
4643 .map_err(operation)?
4644 } else {
4645 let mut rewritten = session.clone();
4646 rewritten.meta.session_id = Some(id.to_string());
4647 rewritten.to_jsonl(format).map_err(operation)?
4648 }
4649 } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4650 session.raw_verbatim()
4651 } else if diagonal {
4652 session.to_jsonl_spliced(format, None).map_err(operation)?
4653 } else {
4654 match session
4657 .restore_residue(format, crate::residue_store::lookup)
4658 .map_err(operation)?
4659 {
4660 Some((content, report)) => {
4661 restoration = Some(report);
4662 content
4663 }
4664 None => session.to_jsonl(format).map_err(operation)?,
4665 }
4666 };
4667 let stem = sanitize_filename(
4668 target_session_id
4669 .or(session.meta.session_id.as_deref())
4670 .unwrap_or(&locator.session_id),
4671 );
4672 let suggested_filename = if diagonal && target == TransferFormat::Grok {
4673 "chat_history.jsonl".to_string()
4674 } else if target == TransferFormat::Goose {
4675 format!("{stem}.goose.json")
4676 } else {
4677 format!("{stem}.{}.jsonl", target.id())
4678 };
4679 let mut files = vec![SessionArtifactFile {
4680 path: suggested_filename.clone(),
4681 content: content.clone(),
4682 role: ArtifactFileRole::Primary,
4683 }];
4684 if target == TransferFormat::ClaudeCode {
4685 let bundle_stem = Path::new(&suggested_filename)
4686 .file_stem()
4687 .and_then(|stem| stem.to_str())
4688 .unwrap_or(&stem);
4689 let mut child_paths = BTreeSet::new();
4690 for (index, subagent) in session.subagents.iter().enumerate() {
4691 let agent_id = subagent
4692 .meta
4693 .agent_id
4694 .as_deref()
4695 .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4696 .map(sanitize_filename)
4697 .filter(|id| !id.is_empty())
4698 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4699 let child_has_appended_turns = subagent
4700 .imported_message_count
4701 .is_some_and(|imported| imported < subagent.messages.len());
4702 let child_content = if target_session_id.is_none()
4703 && subagent.meta.source == SessionSource::ClaudeCode
4704 && subagent.raw_is_verbatim
4705 && !child_has_appended_turns
4706 {
4707 subagent.raw_verbatim()
4708 } else if subagent.meta.source == SessionSource::ClaudeCode {
4709 subagent
4710 .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4711 .map_err(operation)?
4712 } else {
4713 let mut child = subagent.clone();
4714 if let Some(id) = target_session_id {
4715 child.meta.session_id = Some(id.to_string());
4716 }
4717 child
4718 .to_jsonl(SessionFormat::ClaudeCode)
4719 .map_err(operation)?
4720 };
4721 let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4722 if !child_paths.insert(path.clone()) {
4723 return Err(ServiceError::Operation(format!(
4724 "Claude subagent ids collide at artifact path `{path}`"
4725 )));
4726 }
4727 files.push(SessionArtifactFile {
4728 path,
4729 content: child_content,
4730 role: ArtifactFileRole::Subagent,
4731 });
4732 }
4733 }
4734 if diagonal && target == TransferFormat::Grok {
4735 append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4736 }
4737 if !diagonal || !session.raw_is_verbatim {
4738 files.push(SessionArtifactFile {
4739 path: "recovery/source.supercode.jsonl".into(),
4740 content: session.to_native_jsonl(),
4741 role: ArtifactFileRole::SourceRecovery,
4742 });
4743 for (index, subagent) in session.subagents.iter().enumerate() {
4744 let id = subagent
4745 .meta
4746 .agent_id
4747 .as_deref()
4748 .map(sanitize_filename)
4749 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4750 files.push(SessionArtifactFile {
4751 path: format!("recovery/subagents/{id}.supercode.jsonl"),
4752 content: subagent.to_native_jsonl(),
4753 role: ArtifactFileRole::SourceRecovery,
4754 });
4755 }
4756 }
4757 if !diagonal && session.meta.source == SessionSource::Grok {
4758 append_grok_bundle_files(
4759 locator,
4760 "recovery/grok/",
4761 ArtifactFileRole::SourceRecovery,
4762 &mut files,
4763 )?;
4764 }
4765 let (fidelity, residue) = if diagonal
4766 && target_session_id.is_none()
4767 && session.raw_is_verbatim
4768 && !has_appended_turns
4769 {
4770 (Fidelity::ByteLossless, Vec::new())
4771 } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4772 (
4773 Fidelity::ValueLossless,
4774 vec![if target_session_id.is_some() {
4775 "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4776 } else {
4777 "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4778 }],
4779 )
4780 } else {
4781 match restoration {
4782 Some(report) if report.rendered_messages == 0 => (
4783 Fidelity::ByteLossless,
4784 vec![format!(
4785 "restored verbatim from this conversation's {} source records in the residue store",
4786 target.id()
4787 )],
4788 ),
4789 Some(report) => (
4790 Fidelity::Semantic,
4791 vec![format!(
4792 "{} of {} messages restored verbatim from the residue store; the other {} written by the {} writer",
4793 report.restored_messages,
4794 report.restored_messages + report.rendered_messages,
4795 report.rendered_messages,
4796 target.id()
4797 )],
4798 ),
4799 None => (
4800 Fidelity::Semantic,
4801 vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4802 ),
4803 }
4804 };
4805 Ok(SessionArtifact {
4806 source_harness: locator.harness.clone(),
4807 target_harness: target.id(),
4808 session_id: target_session_id
4809 .map(str::to_string)
4810 .or_else(|| session.meta.session_id.clone()),
4811 content,
4812 suggested_filename,
4813 files,
4814 fidelity,
4815 residue,
4816 })
4817}
4818
4819fn append_grok_bundle_files(
4820 locator: &SessionLocator,
4821 prefix: &str,
4822 role: ArtifactFileRole,
4823 files: &mut Vec<SessionArtifactFile>,
4824) -> std::result::Result<(), ServiceError> {
4825 let primary = locator.storage.path();
4826 if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4827 return Err(ServiceError::Operation(format!(
4828 "Grok bundle locator must name chat_history.jsonl, got {}",
4829 primary.display()
4830 )));
4831 }
4832 let parent = primary.parent().ok_or_else(|| {
4833 ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4834 })?;
4835 for name in ["summary.json", "updates.jsonl"] {
4836 let path = parent.join(name);
4837 let metadata = match std::fs::symlink_metadata(&path) {
4838 Ok(metadata) => metadata,
4839 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4840 Err(error) => return Err(ServiceError::Operation(error.to_string())),
4841 };
4842 if metadata.file_type().is_symlink() || !metadata.is_file() {
4843 return Err(ServiceError::Operation(format!(
4844 "refusing non-regular Grok bundle member {}",
4845 path.display()
4846 )));
4847 }
4848 let content = std::fs::read_to_string(&path).map_err(|error| {
4849 ServiceError::Operation(format!(
4850 "Grok bundle member {} is not representable as UTF-8: {error}",
4851 path.display()
4852 ))
4853 })?;
4854 files.push(SessionArtifactFile {
4855 path: format!("{prefix}{name}"),
4856 content,
4857 role: match role {
4858 ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4859 _ => ArtifactFileRole::SourceRecovery,
4860 },
4861 });
4862 }
4863 Ok(())
4864}
4865
4866fn handoff_artifact(
4867 locator: &SessionLocator,
4868 session: &Session,
4869 target: TransferFormat,
4870) -> std::result::Result<SessionArtifact, ServiceError> {
4871 let target_session_id = target_session_id(target);
4872 session_artifact_with_id(locator, session, target, Some(&target_session_id))
4873}
4874
4875fn target_session_id(target: TransferFormat) -> String {
4876 let uuid = generated_session_id();
4877 match target {
4878 TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4879 TransferFormat::ClaudeCode
4880 | TransferFormat::Codex
4881 | TransferFormat::Pi
4882 | TransferFormat::Grok
4883 | TransferFormat::Gemini
4884 | TransferFormat::Goose
4885 | TransferFormat::Hermes => uuid,
4886 }
4887}
4888
4889fn sanitize_filename(value: &str) -> String {
4890 let value = value
4891 .chars()
4892 .map(|character| {
4893 if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4894 character
4895 } else {
4896 '-'
4897 }
4898 })
4899 .collect::<String>();
4900 let value = value.trim_matches('-');
4901 if value.is_empty() {
4902 "session".into()
4903 } else {
4904 value.chars().take(100).collect()
4905 }
4906}
4907
4908fn handoff_instructions(
4909 target: TransferFormat,
4910 session_id: &str,
4911 cwd: &Path,
4912) -> HandoffInstructions {
4913 let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4914 cwd: cwd.to_path_buf(),
4915 program: program.into(),
4916 arguments,
4917 env: BTreeMap::new(),
4918 };
4919 match target {
4920 TransferFormat::ClaudeCode => HandoffInstructions {
4921 launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4922 materialize: None,
4923 requires_materialization: true,
4924 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(),
4925 },
4926 TransferFormat::Hermes => HandoffInstructions {
4927 launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4928 materialize: None,
4929 requires_materialization: true,
4930 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(),
4931 },
4932 TransferFormat::Codex => HandoffInstructions {
4933 launch: launch("codex", vec!["resume".into(), session_id.into()]),
4934 materialize: None,
4935 requires_materialization: true,
4936 note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4937 },
4938 TransferFormat::OpenCode => HandoffInstructions {
4939 launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4940 materialize: Some(launch(
4941 "opencode",
4942 vec!["import".into(), "{artifact_path}".into()],
4943 )),
4944 requires_materialization: true,
4945 note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4946 },
4947 TransferFormat::Pi => HandoffInstructions {
4948 launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4949 materialize: None,
4950 requires_materialization: true,
4951 note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4952 },
4953 TransferFormat::Grok => HandoffInstructions {
4954 launch: launch(
4955 "grok",
4956 vec!["--resume".into(), "{materialized_session_id}".into()],
4957 ),
4958 materialize: None,
4959 requires_materialization: true,
4960 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(),
4961 },
4962 TransferFormat::Gemini => HandoffInstructions {
4963 launch: launch(
4964 "gemini",
4965 vec!["--session-file".into(), "{artifact_path}".into()],
4966 ),
4967 materialize: None,
4968 requires_materialization: true,
4969 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(),
4970 },
4971 TransferFormat::Goose => HandoffInstructions {
4972 launch: launch(
4973 "goose",
4974 vec![
4975 "session".into(),
4976 "--resume".into(),
4977 "--session-id".into(),
4978 "{imported_session_id}".into(),
4979 ],
4980 ),
4981 materialize: Some(launch(
4982 "goose",
4983 vec!["session".into(), "import".into(), "{artifact_path}".into()],
4984 )),
4985 requires_materialization: true,
4986 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(),
4987 },
4988 }
4989}
4990
4991fn resume_launch(
4992 harness: &str,
4993 session_id: &str,
4994 cwd: &Path,
4995 policy: ResumePolicy,
4996) -> std::result::Result<StructuredLaunch, ServiceError> {
4997 let mut arguments = Vec::new();
4998 let program = match harness {
4999 HarnessId::GROK => {
5000 if matches!(policy, ResumePolicy::Yolo) {
5001 if crate::support::self_sandbox_supported() {
5002 arguments.extend(["--sandbox".into(), "workspace".into()]);
5003 }
5004 arguments.push("--always-approve".into());
5005 }
5006 arguments.extend(["--resume".into(), session_id.into()]);
5007 "grok"
5008 }
5009 HarnessId::CODEX => {
5010 let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
5011 .expect("a filesystem path always serializes as JSON text");
5012 arguments.extend([
5013 "-c".into(),
5014 "check_for_update_on_startup=false".into(),
5015 "-c".into(),
5016 format!("projects.{cwd_key}.trust_level=\"trusted\""),
5017 ]);
5018 if matches!(policy, ResumePolicy::Yolo) {
5019 arguments.extend([
5020 "--dangerously-bypass-approvals-and-sandbox".into(),
5021 "--dangerously-bypass-hook-trust".into(),
5022 ]);
5023 }
5024 arguments.extend(["resume".into(), session_id.into()]);
5025 "codex"
5026 }
5027 HarnessId::CLAUDE_CODE => {
5028 if matches!(policy, ResumePolicy::Yolo) {
5029 arguments.push("--dangerously-skip-permissions".into());
5030 }
5031 arguments.extend(["--resume".into(), session_id.into()]);
5032 "claude"
5033 }
5034 HarnessId::GEMINI => {
5035 if matches!(policy, ResumePolicy::Yolo) {
5036 arguments.push("--yolo".into());
5037 }
5038 arguments.extend(["--resume".into(), session_id.into()]);
5039 "gemini"
5040 }
5041 HarnessId::GOOSE => {
5042 arguments.extend([
5043 "session".into(),
5044 "--resume".into(),
5045 "--session-id".into(),
5046 session_id.into(),
5047 ]);
5048 "goose"
5049 }
5050 HarnessId::PI => {
5051 if matches!(policy, ResumePolicy::Yolo) {
5052 arguments.push("--approve".into());
5053 }
5054 arguments.extend(["--session".into(), session_id.into()]);
5055 "pi"
5056 }
5057 HarnessId::OPENCODE => {
5058 arguments.extend(["--session".into(), session_id.into()]);
5059 "opencode"
5060 }
5061 HarnessId::SUPERCODE => {
5062 if matches!(policy, ResumePolicy::Yolo) {
5063 arguments.push("--dangerous".into());
5064 }
5065 arguments.extend(["resume".into(), session_id.into()]);
5066 "supercode"
5067 }
5068 other => {
5069 return Err(ServiceError::InvalidParams(format!(
5070 "no structured resume launch is registered for harness `{other}`"
5071 )))
5072 }
5073 };
5074 Ok(StructuredLaunch {
5075 cwd: cwd.to_path_buf(),
5076 env: if program == "grok" {
5077 crate::support::grok_home_env()
5078 } else {
5079 BTreeMap::new()
5080 },
5081 program: program.into(),
5082 arguments,
5083 })
5084}
5085
5086fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
5092 let digest = blake3::hash(address.as_bytes()).to_hex();
5093 let path = std::env::temp_dir().join(format!(
5094 "supercode-openclaw-gateway-token-{}",
5095 &digest.as_str()[..16]
5096 ));
5097 #[cfg(unix)]
5098 {
5099 use std::io::Write;
5100 use std::os::unix::fs::OpenOptionsExt;
5101 let mut file = std::fs::OpenOptions::new()
5102 .write(true)
5103 .create(true)
5104 .truncate(true)
5105 .mode(0o600)
5106 .open(&path)?;
5107 file.write_all(secret.as_bytes())?;
5108 }
5109 #[cfg(not(unix))]
5110 std::fs::write(&path, secret)?;
5111 Ok(path)
5112}
5113
5114fn open_connect_descriptor(
5120 descriptor: &crate::HarnessSupportDescriptor,
5121 home: &Path,
5122) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5123 let Some(connect) = &descriptor.runtime.connect_launch else {
5124 return Err(ServiceError::InvalidParams(format!(
5125 "harness `{}` has no registered connect-mode launch",
5126 descriptor.id.as_str()
5127 )));
5128 };
5129 let resolved = connect
5130 .resolve(home)
5131 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
5132 match (descriptor.id.as_str(), connect.protocol.as_str()) {
5133 (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
5134 let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
5135 if let Some(token) = resolved.auth {
5136 backend = backend.with_bearer(token);
5137 }
5138 Ok(Box::new(backend))
5139 }
5140 (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
5141 let mut env = BTreeMap::new();
5152 let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
5153 if let Some(token) = resolved.auth {
5154 let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
5155 .map_err(|error| {
5156 ServiceError::UnsupportedAction(format!(
5157 "could not stage the gateway credential for the bridge: {error}"
5158 ))
5159 })?;
5160 arguments.push("--token-file".into());
5161 arguments.push(token_path.to_string_lossy().into_owned());
5162 env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
5163 }
5164 let program = descriptor
5169 .runtime
5170 .default_launch
5171 .as_ref()
5172 .map(|launch| launch.program.clone())
5173 .unwrap_or_else(|| "openclaw".into());
5174 let launch = RuntimeLaunch {
5175 program,
5176 arguments,
5177 env,
5178 };
5179 Ok(Box::new(
5180 crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
5181 .with_resume_support(descriptor.runtime.capabilities.resume_session),
5182 ))
5183 }
5184 _ => Err(ServiceError::UnsupportedAction(format!(
5185 "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
5186 descriptor.id.as_str(),
5187 connect.protocol
5188 ))),
5189 }
5190}
5191
5192fn registry_connect_descriptor(
5195 params: &RuntimeBackendParams,
5196) -> Option<crate::HarnessSupportDescriptor> {
5197 if params.launch.is_some() || params.base_url.is_some() {
5198 return None;
5199 }
5200 harness_support_registry()
5201 .harnesses
5202 .into_iter()
5203 .find(|descriptor| descriptor.id == params.harness)
5204 .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
5205}
5206
5207fn service_home() -> std::result::Result<PathBuf, ServiceError> {
5208 supercode_interchange::user_home()
5209 .map(std::path::PathBuf::into_os_string)
5210 .map(PathBuf::from)
5211 .ok_or_else(|| {
5212 ServiceError::UnsupportedAction(
5213 "connect-mode launches need HOME to locate the harness config".into(),
5214 )
5215 })
5216}
5217
5218pub const RUNTIME_OPEN_METHODS: &[&str] = &[
5221 "harness.v1.runtimes.start",
5222 "harness.v1.runtimes.resume",
5223 "harness.v1.runtimes.attach",
5224 "harness.v1.runtimes.attach_existing",
5225];
5226
5227pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
5232
5233pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
5239
5240pub const DETACHED_METHODS: &[&str] = &[
5246 "harness.v1.harnesses.list",
5247 "harness.v1.harnesses.probe",
5248 "harness.v1.sessions.message",
5249 "harness.v1.sessions.new",
5250 "harness.v1.sessions.reset",
5251 "harness.v1.sessions.archive",
5252 "harness.v1.sessions.delete",
5253];
5254
5255pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
5261
5262pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
5270
5271async fn within_control_deadline<F: std::future::Future>(
5274 method: &str,
5275 call: F,
5276) -> std::result::Result<F::Output, ServiceError> {
5277 tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
5278 .await
5279 .map_err(|_| {
5280 ServiceError::Operation(format!(
5281 "`{method}` gave up after {}s: the runtime did not answer",
5282 RUNTIME_CONTROL_DEADLINE.as_secs()
5283 ))
5284 })
5285}
5286
5287pub struct RuntimeOpen {
5291 id: Value,
5292 method: String,
5293 params: Value,
5294}
5295
5296impl RuntimeOpen {
5297 pub async fn open(self) -> OpenedRuntime {
5301 let Self { id, method, params } = self;
5302 let outcome = open_runtime(&method, params).await;
5303 OpenedRuntime { id, outcome }
5304 }
5305}
5306
5307pub struct OpenedRuntime {
5310 id: Value,
5311 outcome: std::result::Result<OpenRuntime, ServiceError>,
5312}
5313
5314pub struct DetachedCall {
5319 id: Value,
5320 method: String,
5321 work: std::result::Result<Work, ServiceError>,
5322}
5323
5324impl DetachedCall {
5325 pub async fn run(self) -> DetachedAnswer {
5328 let Self { id, method, work } = self;
5329 match work {
5330 Ok(Work::Runtime(work)) => {
5335 let (result, returned) = work.run().await;
5336 DetachedAnswer {
5337 response: service_response(id, result),
5338 returned,
5339 }
5340 }
5341 Ok(Work::Free(work)) => {
5342 let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5343 Ok(result) => result,
5344 Err(_) => Err(ServiceError::Operation(format!(
5345 "`{method}` gave up after {}s: the harness it waits on did not answer",
5346 DETACHED_CALL_DEADLINE.as_secs()
5347 ))),
5348 };
5349 DetachedAnswer {
5350 response: service_response(id, result),
5351 returned: None,
5352 }
5353 }
5354 Err(error) => DetachedAnswer {
5355 response: service_response(id, Err(error)),
5356 returned: None,
5357 },
5358 }
5359 }
5360}
5361
5362pub struct DetachedAnswer {
5366 response: Value,
5367 returned: Option<ReturnedRuntime>,
5368}
5369
5370impl DetachedAnswer {
5371 pub fn into_response(self) -> Value {
5374 self.response
5375 }
5376}
5377
5378pub struct ReturnedRuntime {
5381 connection: String,
5382 runtime: Box<dyn RuntimeConnection>,
5383}
5384
5385enum Work {
5388 Free(DetachedWork),
5389 Runtime(RuntimeWork),
5390}
5391
5392enum DetachedWork {
5395 Inventory(InventoryWork),
5399 Message(MessageSessionParams),
5401 SessionMutation {
5404 verb: crate::SessionVerb,
5405 mutation: crate::SessionMutation,
5406 },
5407}
5408
5409impl DetachedWork {
5410 async fn run(self) -> std::result::Result<Value, ServiceError> {
5411 match self {
5412 Self::Inventory(work) => run_inventory(work).await,
5413 Self::Message(params) => Ok(message_live_session(¶ms).await),
5414 Self::SessionMutation { verb, mutation } => {
5415 let outcome = run_session_mutation(verb, &mutation).await?;
5416 serde_json::to_value(outcome)
5417 .map_err(|error| ServiceError::Operation(error.to_string()))
5418 }
5419 }
5420 }
5421}
5422
5423enum RuntimeWork {
5425 Close {
5427 runtime: Box<dyn RuntimeConnection>,
5428 process_group: Option<u32>,
5429 },
5430 LiveCommand {
5433 connection: String,
5434 runtime: Box<dyn RuntimeConnection>,
5435 verb: crate::SessionVerb,
5436 mutation: crate::SessionMutation,
5437 command: &'static str,
5438 session: String,
5439 },
5440}
5441
5442type RuntimeWorkAnswer = (
5445 std::result::Result<Value, ServiceError>,
5446 Option<ReturnedRuntime>,
5447);
5448
5449impl RuntimeWork {
5450 async fn run(self) -> RuntimeWorkAnswer {
5451 match self {
5452 Self::Close {
5453 runtime,
5454 process_group,
5455 } => (close_runtime(runtime, process_group).await, None),
5456 Self::LiveCommand {
5457 connection,
5458 mut runtime,
5459 verb,
5460 mutation,
5461 command,
5462 session,
5463 } => {
5464 let result =
5465 type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5466 (
5467 result,
5468 Some(ReturnedRuntime {
5469 connection,
5470 runtime,
5471 }),
5472 )
5473 }
5474 }
5475 }
5476}
5477
5478async fn close_runtime(
5481 mut runtime: Box<dyn RuntimeConnection>,
5482 process_group: Option<u32>,
5483) -> std::result::Result<Value, ServiceError> {
5484 match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5485 Ok(result) => {
5486 result.map_err(operation)?;
5487 Ok(json!({"closed": true}))
5488 }
5489 Err(deadline) => {
5490 let killed = kill_runtime_process_group(process_group);
5495 drop(runtime);
5496 Ok(json!({
5497 "closed": true,
5498 "killed": killed,
5499 "detail": error_message(deadline),
5500 }))
5501 }
5502 }
5503}
5504
5505fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5508 mutation
5509 .session
5510 .clone()
5511 .filter(|value| !value.trim().is_empty())
5512 .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5513}
5514
5515async fn type_live_command(
5519 runtime: &mut dyn RuntimeConnection,
5520 verb: crate::SessionVerb,
5521 mutation: &crate::SessionMutation,
5522 command: &str,
5523 session: String,
5524) -> std::result::Result<Value, ServiceError> {
5525 within_control_deadline(
5526 &format!("sessions.{}", verb.as_str()),
5527 runtime.send_input(RuntimeInput {
5528 text: command.to_string(),
5529 image_urls: Vec::new(),
5530 }),
5531 )
5532 .await?
5533 .map_err(operation)?;
5534 let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5535 .map_err(session_control_error)?;
5536 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5537}
5538
5539enum OpenRuntime {
5542 Hosted {
5545 runtime: Box<dyn RuntimeConnection>,
5546 capabilities: crate::RuntimeCapabilities,
5547 workspace: PathBuf,
5548 },
5549 Joined { runtime: Box<dyn RuntimeConnection> },
5552}
5553
5554async fn open_runtime(
5559 method: &str,
5560 params: Value,
5561) -> std::result::Result<OpenRuntime, ServiceError> {
5562 match tokio::time::timeout(
5563 RUNTIME_OPEN_DEADLINE,
5564 open_runtime_unbounded(method, params),
5565 )
5566 .await
5567 {
5568 Ok(result) => result,
5569 Err(_) => Err(ServiceError::Operation(format!(
5570 "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5571 RUNTIME_OPEN_DEADLINE.as_secs()
5572 ))),
5573 }
5574}
5575
5576async fn open_runtime_unbounded(
5577 method: &str,
5578 params: Value,
5579) -> std::result::Result<OpenRuntime, ServiceError> {
5580 match method {
5581 "harness.v1.runtimes.start" => {
5582 let params = decode::<RuntimeStartParams>(params)?;
5583 let backend = runtime_backend(¶ms.backend)?;
5584 let capabilities = backend.capabilities();
5585 let workspace = params.cwd.clone();
5586 let runtime = backend
5587 .start(RuntimeStartRequest {
5588 cwd: params.cwd,
5589 launch: runtime_launch(¶ms.backend),
5590 mcp_servers: params.mcp_servers,
5591 approval_policy: params.approval_policy,
5592 })
5593 .await
5594 .map_err(operation)?;
5595 Ok(OpenRuntime::Hosted {
5596 runtime,
5597 capabilities,
5598 workspace,
5599 })
5600 }
5601 "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5602 let params = decode::<RuntimeAttachParams>(params)?;
5603 let backend = runtime_backend(¶ms.backend)?;
5604 let capabilities = backend.capabilities();
5605 let workspace = params
5606 .cwd
5607 .clone()
5608 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5609 let runtime = backend
5610 .attach(RuntimeAttachRequest {
5611 runtime_id: params.runtime_id,
5612 cwd: params.cwd,
5613 launch: runtime_launch(¶ms.backend),
5614 mcp_servers: params.mcp_servers,
5615 approval_policy: params.approval_policy,
5616 })
5617 .await
5618 .map_err(operation)?;
5619 Ok(OpenRuntime::Hosted {
5620 runtime,
5621 capabilities,
5622 workspace,
5623 })
5624 }
5625 "harness.v1.runtimes.attach_existing" => {
5626 let params = decode::<RuntimeAttachParams>(params)?;
5627 let backend: Box<dyn RuntimeBackend> = match params
5628 .backend
5629 .base_url
5630 .as_deref()
5631 .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5632 {
5633 Some(endpoint) => {
5634 #[cfg(not(feature = "adapter-api"))]
5635 {
5636 let _ = endpoint;
5637 return Err(ServiceError::UnsupportedAction(
5638 "live HTTP attachment adapter is not compiled".into(),
5639 ));
5640 }
5641 #[cfg(feature = "adapter-api")]
5642 {
5643 let workspace = params.cwd.clone().ok_or_else(|| {
5644 ServiceError::InvalidParams(
5645 "Volter Harness live attach requires the project cwd".into(),
5646 )
5647 })?;
5648 let source = LiveRuntimeSource {
5649 harness: params.backend.harness.as_str().to_string(),
5650 session_id: params.runtime_id.clone(),
5651 workspace,
5652 };
5653 let receipt = resolve_live_runtime(&endpoint, &source)
5654 .map_err(|error| ServiceError::Operation(error.to_string()))?;
5655 Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5656 }
5657 }
5658 None => runtime_backend(¶ms.backend)?,
5659 };
5660 let capabilities = backend.capabilities();
5661 if !capabilities.attach_existing_process {
5662 return Err(ServiceError::Operation(format!(
5663 "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5664 backend.harness().as_str()
5665 )));
5666 }
5667 let runtime = backend
5668 .attach_existing(RuntimeAttachRequest {
5669 runtime_id: params.runtime_id,
5670 cwd: params.cwd,
5671 launch: runtime_launch(¶ms.backend),
5672 mcp_servers: params.mcp_servers,
5673 approval_policy: params.approval_policy,
5674 })
5675 .await
5676 .map_err(operation)?;
5677 Ok(OpenRuntime::Joined { runtime })
5678 }
5679 _ => Err(ServiceError::MethodNotFound),
5680 }
5681}
5682
5683fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5685 match result {
5686 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5687 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5688 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5689 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5690 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5691 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5692 }
5693}
5694
5695fn runtime_backend(
5696 params: &RuntimeBackendParams,
5697) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5698 if let Some(descriptor) = registry_connect_descriptor(params) {
5699 return open_connect_descriptor(&descriptor, &service_home()?);
5700 }
5701 if params.protocol.as_deref() == Some("acp") {
5702 let launch = params
5703 .launch
5704 .clone()
5705 .or_else(|| {
5706 harness_support_registry()
5707 .harnesses
5708 .into_iter()
5709 .find(|harness| harness.id == params.harness)
5710 .filter(|harness| {
5711 harness.runtime.implementation == ImplementationKind::GenericProtocol
5712 && harness.runtime.protocol.starts_with("acp")
5713 })
5714 .and_then(|harness| harness.runtime.default_launch)
5715 })
5716 .ok_or_else(|| {
5717 ServiceError::InvalidParams(
5718 "an ACP runtime requires `launch` unless the harness has a registered default"
5719 .into(),
5720 )
5721 })?;
5722 let resume_session = harness_support_registry()
5723 .harnesses
5724 .into_iter()
5725 .find(|harness| harness.id == params.harness)
5726 .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5727 return Ok(Box::new(
5728 AcpRuntimeBackend::new(params.harness.clone(), launch)
5729 .with_resume_support(resume_session),
5730 ));
5731 }
5732 let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5733 HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5734 HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5735 HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5736 HarnessId::OPENCODE => match ¶ms.base_url {
5737 Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5738 None => Box::new(OpenCodeRuntimeBackend::new()),
5739 },
5740 harness => {
5741 let descriptor = harness_support_registry()
5742 .harnesses
5743 .into_iter()
5744 .find(|descriptor| descriptor.id.as_str() == harness)
5745 .filter(|descriptor| {
5746 descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5747 && descriptor.runtime.protocol.starts_with("acp")
5748 });
5749 let Some(descriptor) = descriptor else {
5750 return Err(ServiceError::InvalidParams(format!(
5751 "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5752 )));
5753 };
5754 let resume = descriptor.runtime.capabilities.resume_session;
5755 Box::new(
5756 AcpRuntimeBackend::new(
5757 descriptor.id,
5758 descriptor
5759 .runtime
5760 .default_launch
5761 .expect("generic ACP registry entry includes its launch"),
5762 )
5763 .with_resume_support(resume),
5764 )
5765 }
5766 };
5767 Ok(backend)
5768}
5769
5770fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5771 if let Some(launch) = ¶ms.launch {
5772 return Some(launch.clone());
5773 }
5774 if !matches!(params.policy, RuntimePolicy::Yolo) {
5775 return None;
5776 }
5777 let launch = match params.harness.as_str() {
5778 HarnessId::GROK => RuntimeLaunch {
5779 program: "grok".into(),
5780 arguments: {
5781 let mut arguments: Vec<String> = Vec::new();
5782 if crate::support::self_sandbox_supported() {
5783 arguments.extend(["--sandbox".into(), "workspace".into()]);
5784 }
5785 arguments.extend([
5786 "--always-approve".into(),
5787 "agent".into(),
5788 "--no-leader".into(),
5789 "stdio".into(),
5790 ]);
5791 arguments
5792 },
5793 env: crate::support::grok_env(),
5794 },
5795 HarnessId::CODEX => RuntimeLaunch {
5796 program: "codex".into(),
5797 arguments: vec![
5798 "--dangerously-bypass-approvals-and-sandbox".into(),
5799 "--dangerously-bypass-hook-trust".into(),
5800 "app-server".into(),
5801 ],
5802 env: BTreeMap::new(),
5803 },
5804 HarnessId::CLAUDE_CODE => RuntimeLaunch {
5805 program: "claude".into(),
5806 arguments: vec![
5807 "--dangerously-skip-permissions".into(),
5808 "--print".into(),
5809 "--input-format".into(),
5810 "stream-json".into(),
5811 "--output-format".into(),
5812 "stream-json".into(),
5813 "--verbose".into(),
5814 ],
5815 env: BTreeMap::new(),
5816 },
5817 HarnessId::PI => RuntimeLaunch {
5818 program: "pi".into(),
5819 arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5820 env: BTreeMap::new(),
5821 },
5822 HarnessId::OPENCODE => RuntimeLaunch {
5823 program: "opencode".into(),
5824 arguments: vec!["serve".into()],
5825 env: BTreeMap::new(),
5826 },
5827 HarnessId::GEMINI => RuntimeLaunch {
5828 program: "gemini".into(),
5829 arguments: vec!["--acp".into(), "--yolo".into()],
5830 env: BTreeMap::new(),
5831 },
5832 HarnessId::GOOSE => RuntimeLaunch {
5833 program: "goose".into(),
5834 arguments: vec!["acp".into()],
5835 env: BTreeMap::new(),
5836 },
5837 HarnessId::SUPERCODE => RuntimeLaunch {
5838 program: "supercode".into(),
5839 arguments: vec!["acp".into(), "--dangerous".into()],
5840 env: BTreeMap::new(),
5841 },
5842 _ => return None,
5843 };
5844 Some(launch)
5845}
5846
5847struct IsolatedProbeHome {
5853 launch: RuntimeLaunch,
5854 root: PathBuf,
5855}
5856
5857impl IsolatedProbeHome {
5858 fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5859 let root = std::env::temp_dir().join(format!(
5860 "supercode-harness-probe-{harness}-{}",
5861 generated_session_id()
5862 ));
5863 std::fs::create_dir_all(&root)?;
5864 set_private_dir_permissions(&root)?;
5865
5866 if let Some(source_home) = supercode_interchange::user_home()
5867 .map(std::path::PathBuf::into_os_string)
5868 .map(PathBuf::from)
5869 {
5870 for relative in probe_auth_files(harness) {
5871 copy_probe_file(&source_home, &root, relative)?;
5872 }
5873 }
5874 if harness == HarnessId::SUPERCODE {
5879 let config_home = crate::agent::global_instructions_dir();
5880 for file in ["config.toml", "credentials.toml"] {
5881 copy_probe_path(
5882 &config_home.join(file),
5883 &root.join(".config/supercode").join(file),
5884 )?;
5885 }
5886 }
5887 configure_isolated_probe_auth(harness, &root)?;
5888
5889 let root_text = root.to_string_lossy().into_owned();
5890 for (key, value) in [
5891 ("HOME", root_text.clone()),
5892 (
5893 "XDG_CACHE_HOME",
5894 root.join(".cache").to_string_lossy().into_owned(),
5895 ),
5896 (
5897 "XDG_CONFIG_HOME",
5898 root.join(".config").to_string_lossy().into_owned(),
5899 ),
5900 (
5901 "XDG_DATA_HOME",
5902 root.join(".local/share").to_string_lossy().into_owned(),
5903 ),
5904 ] {
5905 launch.env.insert(key.into(), value);
5906 }
5907 let scoped = match harness {
5908 HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5909 HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5910 HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5911 HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5912 HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5913 HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5914 _ => None,
5915 };
5916 if let Some((key, value)) = scoped {
5917 launch
5918 .env
5919 .insert(key.into(), value.to_string_lossy().into_owned());
5920 }
5921 Ok(Self { launch, root })
5922 }
5923
5924 fn cleanup(&self) -> std::io::Result<()> {
5925 match std::fs::remove_dir_all(&self.root) {
5926 Ok(()) => Ok(()),
5927 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5928 Err(error) => Err(error),
5929 }
5930 }
5931}
5932
5933impl Drop for IsolatedProbeHome {
5934 fn drop(&mut self) {
5935 let _ = self.cleanup();
5936 }
5937}
5938
5939fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5940 match harness {
5941 HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5942 HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5946 HarnessId::CODEX => &[".codex/auth.json"],
5947 HarnessId::GEMINI => &[
5948 ".gemini/google_accounts.json",
5949 ".gemini/oauth_creds.json",
5950 ".gemini/settings.json",
5951 ],
5952 HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5953 HarnessId::OPENCODE => &[
5954 ".config/opencode/auth.json",
5955 ".local/share/opencode/auth.json",
5956 ],
5957 HarnessId::PI => &[".pi/agent/auth.json"],
5958 HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5963 _ => &[],
5964 }
5965}
5966
5967fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5968 copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5969}
5970
5971fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5972 if !source.is_file() {
5973 return Ok(());
5974 }
5975 if let Some(parent) = destination.parent() {
5976 std::fs::create_dir_all(parent)?;
5977 set_private_dir_permissions(parent)?;
5978 }
5979 std::fs::copy(source, destination)?;
5980 set_private_file_permissions(destination)
5981}
5982
5983fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5984 if harness != HarnessId::GEMINI {
5985 return Ok(());
5986 }
5987 let oauth = probe_home.join(".gemini/oauth_creds.json");
5988 if !oauth.is_file() {
5989 return Ok(());
5990 }
5991 let settings_path = probe_home.join(".gemini/settings.json");
5992 let mut settings = std::fs::read_to_string(&settings_path)
5993 .ok()
5994 .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5995 .unwrap_or_else(|| json!({}));
5996 settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5997 std::fs::write(
5998 &settings_path,
5999 serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
6000 )?;
6001 set_private_file_permissions(&settings_path)
6002}
6003
6004#[cfg(unix)]
6005fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
6006 use std::os::unix::fs::PermissionsExt;
6007 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
6008}
6009
6010#[cfg(not(unix))]
6011fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
6012 Ok(())
6013}
6014
6015#[cfg(unix)]
6016fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
6017 use std::os::unix::fs::PermissionsExt;
6018 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
6019}
6020
6021#[cfg(not(unix))]
6022fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
6023 Ok(())
6024}
6025
6026fn find_executable(program: &str) -> Option<PathBuf> {
6027 let candidate = PathBuf::from(program);
6028 if candidate.components().count() > 1 {
6029 return candidate.is_file().then_some(candidate);
6030 }
6031 let path = std::env::var_os("PATH")?;
6032 for directory in std::env::split_paths(&path) {
6033 let candidate = directory.join(program);
6034 if candidate.is_file() {
6035 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
6036 }
6037 #[cfg(windows)]
6038 {
6039 for extension in ["exe", "cmd", "bat"] {
6040 let candidate = directory.join(format!("{program}.{extension}"));
6041 if candidate.is_file() {
6042 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
6043 }
6044 }
6045 }
6046 }
6047 None
6048}
6049
6050async fn executable_version(executable: &Path) -> Option<String> {
6051 let mut command = tokio::process::Command::new(executable);
6052 command
6053 .arg("--version")
6054 .stdin(std::process::Stdio::null())
6055 .stdout(std::process::Stdio::piped())
6056 .stderr(std::process::Stdio::piped())
6057 .kill_on_drop(true);
6058 let output = tokio::time::timeout(Duration::from_secs(3), command.output())
6059 .await
6060 .ok()?
6061 .ok()?;
6062 let stdout = String::from_utf8_lossy(&output.stdout);
6063 let stderr = String::from_utf8_lossy(&output.stderr);
6064 stdout
6065 .lines()
6066 .chain(stderr.lines())
6067 .map(str::trim)
6068 .find(|line| !line.is_empty())
6069 .map(|line| truncate_text(line, 200))
6070}
6071
6072pub(crate) fn auth_evidence(harness: &str) -> bool {
6073 let env_names: &[&str] = match harness {
6074 HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
6075 HarnessId::CODEX => &["OPENAI_API_KEY"],
6076 HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
6077 HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
6078 HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
6079 HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
6080 HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
6081 _ => &[],
6082 };
6083 if env_names
6084 .iter()
6085 .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
6086 {
6087 return true;
6088 }
6089 let Some(home) = supercode_interchange::user_home()
6090 .map(std::path::PathBuf::into_os_string)
6091 .map(PathBuf::from)
6092 else {
6093 return false;
6094 };
6095 let files: Vec<PathBuf> = match harness {
6096 HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
6097 HarnessId::CODEX => vec![home.join(".codex/auth.json")],
6098 HarnessId::OPENCODE => vec![
6099 home.join(".local/share/opencode/auth.json"),
6100 home.join(".config/opencode/auth.json"),
6101 ],
6102 HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
6103 HarnessId::GROK => vec![home.join(".grok/auth.json")],
6104 HarnessId::GEMINI => vec![
6105 home.join(".gemini/oauth_creds.json"),
6106 home.join(".gemini/google_accounts.json"),
6107 ],
6108 HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
6109 HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
6110 _ => Vec::new(),
6111 };
6112 if files.into_iter().any(|path| {
6113 std::fs::metadata(path)
6114 .map(|metadata| metadata.is_file() && metadata.len() > 2)
6115 .unwrap_or(false)
6116 }) {
6117 return true;
6118 }
6119 if harness == HarnessId::CLAUDE_CODE {
6126 return std::fs::read_to_string(home.join(".claude.json"))
6127 .map(|text| text.contains("\"oauthAccount\""))
6128 .unwrap_or(false);
6129 }
6130 false
6131}
6132
6133fn looks_like_auth_error(message: &str) -> bool {
6134 let message = message.to_ascii_lowercase();
6135 [
6136 "auth",
6137 "login",
6138 "sign in",
6139 "sign-in",
6140 "credential",
6141 "unauthorized",
6142 "forbidden",
6143 "token",
6144 ]
6145 .iter()
6146 .any(|needle| message.contains(needle))
6147}
6148
6149fn unavailable_capabilities() -> crate::RuntimeCapabilities {
6150 crate::RuntimeCapabilities {
6151 start_session: false,
6152 resume_session: false,
6153 attach_existing_process: false,
6154 send_input: false,
6155 stream_events: false,
6156 interrupt: false,
6157 steer: false,
6158 respond_to_requests: false,
6159 }
6160}
6161
6162fn truncate_text(text: &str, max_chars: usize) -> String {
6163 let mut chars = text.chars();
6164 let truncated = chars.by_ref().take(max_chars).collect::<String>();
6165 if chars.next().is_some() {
6166 format!("{truncated}…")
6167 } else {
6168 truncated
6169 }
6170}
6171
6172fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
6179 match &handle.endpoint {
6180 crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
6181 crate::RuntimeEndpoint::Http { .. } => None,
6182 }
6183}
6184
6185fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
6191 match process_group {
6192 #[cfg(unix)]
6193 Some(pid) => {
6194 crate::lsp::kill_process_group(pid);
6195 true
6196 }
6197 #[cfg(not(unix))]
6198 Some(_) => false,
6199 None => false,
6200 }
6201}
6202
6203fn error_message(error: ServiceError) -> String {
6204 match error {
6205 ServiceError::InvalidParams(message)
6206 | ServiceError::Operation(message)
6207 | ServiceError::UnsupportedAction(message) => message,
6208 ServiceError::MethodNotFound => "runtime adapter is not available".into(),
6209 ServiceError::Sdk(error) => error.to_string(),
6210 }
6211}
6212
6213#[derive(Debug)]
6214enum ServiceError {
6215 InvalidParams(String),
6216 MethodNotFound,
6217 UnsupportedAction(String),
6218 Operation(String),
6219 Sdk(SdkError),
6220}
6221
6222fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
6223 match error {
6224 ServiceError::InvalidParams(message) => {
6225 SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
6226 }
6227 ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
6228 SdkError::unsupported(operation)
6229 }
6230 ServiceError::Operation(message) => {
6231 let code = if message.contains("already in progress") {
6232 SdkErrorCode::Busy
6233 } else if message.contains("not supported by this runtime") {
6234 SdkErrorCode::UnsupportedAction
6235 } else if message.contains("unknown runtime connection") {
6236 SdkErrorCode::NotFound
6237 } else {
6238 SdkErrorCode::Execution
6239 };
6240 SdkError::new(code, operation, message)
6241 }
6242 ServiceError::Sdk(error) => error,
6243 }
6244}
6245
6246fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
6247 let error_code = error.code();
6248 let code = match error_code {
6249 SdkErrorCode::Unauthenticated => -32030,
6250 SdkErrorCode::Unauthorized => -32031,
6251 SdkErrorCode::ControllerRequired => -32032,
6252 SdkErrorCode::LeaseExpired => -32033,
6253 SdkErrorCode::InvalidArgument => -32602,
6254 SdkErrorCode::NotFound => -32004,
6255 SdkErrorCode::Busy => -32000,
6256 SdkErrorCode::UnsupportedAction => -32020,
6257 SdkErrorCode::Execution => -32002,
6258 SdkErrorCode::Transport => -32003,
6259 };
6260 json!({
6261 "jsonrpc": "2.0",
6262 "id": id,
6263 "error": {
6264 "code": code,
6265 "name": error_code,
6266 "operation": error.operation(),
6267 "message": error.to_string(),
6268 },
6269 })
6270}
6271
6272fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
6273 serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
6274}
6275
6276fn operation(error: impl Into<crate::Error>) -> ServiceError {
6277 let error = error.into();
6278 match error {
6279 crate::Error::Sdk(error) => ServiceError::Sdk(error),
6280 error => ServiceError::Operation(error.to_string()),
6281 }
6282}
6283
6284#[derive(Debug, Clone, Deserialize, Default)]
6288#[serde(default)]
6289struct MemoryRequest {
6290 harness: Option<String>,
6292 query: Option<String>,
6294 profile: Option<String>,
6296 session: Option<String>,
6298 full: bool,
6300 regex: bool,
6302 cwd: Option<std::path::PathBuf>,
6304 homes: crate::HarnessHomes,
6306}
6307
6308fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6311 let request = decode::<MemoryRequest>(params)?;
6312 let harness = request
6313 .harness
6314 .clone()
6315 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6316 let to_service = |error: crate::memory::MemoryError| match error {
6317 crate::memory::MemoryError::UnsupportedHarness { .. }
6318 | crate::memory::MemoryError::SessionNotScoped { .. } => {
6319 ServiceError::UnsupportedAction(error.to_string())
6320 }
6321 other => ServiceError::InvalidParams(other.to_string()),
6322 };
6323 match method {
6324 "harness.v1.memory.show" => {
6325 let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6326 harness,
6327 profile: request.profile,
6328 session: request.session,
6329 full: request.full,
6330 cwd: request.cwd,
6331 homes: request.homes,
6332 })
6333 .map_err(to_service)?;
6334 Ok(json!({
6335 "schema": crate::memory::MEMORY_SCHEMA,
6336 "documents": documents,
6337 }))
6338 }
6339 "harness.v1.memory.search" => {
6340 let query = request
6341 .query
6342 .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6343 let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6344 harness,
6345 query,
6346 profile: request.profile,
6347 regex: request.regex,
6348 cwd: request.cwd,
6349 homes: request.homes,
6350 })
6351 .map_err(to_service)?;
6352 Ok(json!({
6353 "schema": crate::memory::MEMORY_SCHEMA,
6354 "matches": matches,
6355 }))
6356 }
6357 _ => Err(ServiceError::MethodNotFound),
6358 }
6359}
6360
6361#[derive(Debug, Clone, Deserialize)]
6365#[serde(default)]
6366struct ProfilesQuery {
6367 harness: Option<String>,
6369 name: Option<String>,
6371 homes: crate::HarnessHomes,
6373}
6374
6375impl Default for ProfilesQuery {
6376 fn default() -> Self {
6377 Self {
6378 harness: None,
6379 name: None,
6380 homes: crate::HarnessHomes::default(),
6381 }
6382 }
6383}
6384
6385fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6388 let query = decode::<ProfilesQuery>(params)?;
6389 let to_service = |error: crate::profiles::ProfileError| match error {
6390 crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6391 ServiceError::UnsupportedAction(error.to_string())
6392 }
6393 crate::profiles::ProfileError::NotFound { .. } => {
6394 ServiceError::InvalidParams(error.to_string())
6395 }
6396 };
6397 match method {
6398 "harness.v1.profiles.list" => {
6399 let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6400 .map_err(to_service)?;
6401 Ok(json!({
6402 "schema": crate::profiles::PROFILES_SCHEMA,
6403 "profiles": profiles,
6404 }))
6405 }
6406 "harness.v1.profiles.get" => {
6407 let harness = query
6408 .harness
6409 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6410 let name = query
6411 .name
6412 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6413 let profile =
6414 crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6415 Ok(json!({
6416 "schema": crate::profiles::PROFILES_SCHEMA,
6417 "profile": profile,
6418 }))
6419 }
6420 _ => Err(ServiceError::MethodNotFound),
6421 }
6422}
6423
6424#[derive(Debug, Clone, Deserialize)]
6428#[serde(default)]
6429struct ChannelsQuery {
6430 harness: Option<String>,
6432 name: Option<String>,
6434 homes: crate::HarnessHomes,
6436}
6437
6438impl Default for ChannelsQuery {
6439 fn default() -> Self {
6440 Self {
6441 harness: None,
6442 name: None,
6443 homes: crate::HarnessHomes::default(),
6444 }
6445 }
6446}
6447
6448#[derive(Debug, Clone, Deserialize)]
6452#[serde(default)]
6453struct RoutesQuery {
6454 harness: Option<String>,
6455 profile: Option<String>,
6457 homes: crate::HarnessHomes,
6458}
6459
6460impl Default for RoutesQuery {
6461 fn default() -> Self {
6462 Self {
6463 harness: None,
6464 profile: None,
6465 homes: crate::HarnessHomes::default(),
6466 }
6467 }
6468}
6469
6470#[derive(Debug, Clone, Deserialize)]
6471#[serde(default)]
6472struct TriggersQuery {
6473 harness: Option<String>,
6474 homes: crate::HarnessHomes,
6475}
6476
6477impl Default for TriggersQuery {
6478 fn default() -> Self {
6479 Self {
6480 harness: None,
6481 homes: crate::HarnessHomes::default(),
6482 }
6483 }
6484}
6485
6486fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6487 let query = decode::<TriggersQuery>(params)?;
6488 let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6489 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6490 Ok(json!({
6491 "schema": crate::triggers::TRIGGERS_SCHEMA,
6492 "triggers": triggers,
6493 }))
6494}
6495
6496fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6497 let query = decode::<RoutesQuery>(params)?;
6498 let routes = crate::routes::list_routes(
6499 &query.homes,
6500 query.harness.as_deref(),
6501 query.profile.as_deref(),
6502 )
6503 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6504 Ok(json!({
6505 "schema": crate::routes::ROUTES_SCHEMA,
6506 "routes": routes,
6507 }))
6508}
6509
6510fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6511 let query = decode::<ChannelsQuery>(params)?;
6512 let to_service = |error: crate::channels::ChannelError| match error {
6513 crate::channels::ChannelError::UnsupportedHarness { .. } => {
6514 ServiceError::UnsupportedAction(error.to_string())
6515 }
6516 crate::channels::ChannelError::NotFound { .. } => {
6517 ServiceError::InvalidParams(error.to_string())
6518 }
6519 };
6520 match method {
6521 "harness.v1.channels.list" => {
6522 let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6523 .map_err(to_service)?;
6524 Ok(json!({
6525 "schema": crate::channels::CHANNELS_SCHEMA,
6526 "channels": channels,
6527 }))
6528 }
6529 "harness.v1.channels.status" => {
6530 let harness = query
6531 .harness
6532 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6533 let name = query
6534 .name
6535 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6536 let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6537 .map_err(to_service)?;
6538 Ok(json!({
6539 "schema": crate::channels::CHANNELS_SCHEMA,
6540 "channel": channel,
6541 }))
6542 }
6543 _ => Err(ServiceError::MethodNotFound),
6544 }
6545}
6546
6547fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6548 json!({
6549 "jsonrpc": "2.0",
6550 "id": id,
6551 "error": {"code": code, "message": message},
6552 })
6553}
6554
6555#[cfg(test)]
6556mod tests {
6557 use super::*;
6558 use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6559 use async_trait::async_trait;
6560 use std::io::Write;
6561 use std::path::PathBuf;
6562 use std::time::Instant;
6563
6564 #[test]
6565 fn indexed_claude_descriptor_carries_its_delivery_door() {
6566 let descriptor = SessionDescriptor {
6567 locator: SessionLocator {
6568 harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6569 session_id: "live-session".into(),
6570 storage: StorageLocator::File {
6571 path: PathBuf::from("/tmp/live-session.jsonl"),
6572 },
6573 },
6574 cwd: Some(PathBuf::from("/project")),
6575 title: None,
6576 preview_candidates: Vec::new(),
6577 latest_message_candidates: Vec::new(),
6578 updated_at_ms: Some(1),
6579 message_count: None,
6580 model: None,
6581 parent_session_id: None,
6582 child_session_count: 0,
6583 nouns: Default::default(),
6584 };
6585 let doors =
6586 crate::mail_route::LiveSessions::with(&[("claude-code", "live-session", "native")]);
6587
6588 let value = live_descriptor_value(&descriptor, &doors).unwrap();
6589 assert_eq!(value["delivery"], "native");
6590 assert!(value.get("live_endpoint").is_none());
6591 }
6592
6593 struct EndingRuntime {
6594 handle: RuntimeHandle,
6595 event: Option<HarnessEvent>,
6596 close_failures: usize,
6597 }
6598
6599 #[async_trait]
6600 impl RuntimeConnection for EndingRuntime {
6601 fn handle(&self) -> &RuntimeHandle {
6602 &self.handle
6603 }
6604
6605 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6606 unreachable!("ending runtime does not accept input")
6607 }
6608
6609 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6610 Ok(self.event.take())
6611 }
6612
6613 async fn interrupt(&mut self) -> crate::Result<()> {
6614 Ok(())
6615 }
6616
6617 async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6618 Ok(())
6619 }
6620
6621 async fn close(&mut self) -> crate::Result<()> {
6622 if self.close_failures > 0 {
6623 self.close_failures -= 1;
6624 return Err(crate::Error::Other(
6625 "cleanup temporarily unavailable".into(),
6626 ));
6627 }
6628 Ok(())
6629 }
6630 }
6631
6632 fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6633 Box::new(EndingRuntime {
6634 handle: RuntimeHandle {
6635 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6636 runtime_id: "ending-session".into(),
6637 endpoint: RuntimeEndpoint::LocalProcess {
6638 pid: None,
6639 command: vec!["ending-runtime".into()],
6640 protocol: "test".into(),
6641 },
6642 },
6643 event,
6644 close_failures: 0,
6645 })
6646 }
6647
6648 #[tokio::test]
6649 async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6650 let mut service = HarnessSessionService::new();
6651 let handle = ending_runtime(None).handle().clone();
6652 let runtime_id = handle.runtime_id.clone();
6653 let opened = service
6654 .insert_runtime(Box::new(EndingRuntime {
6655 handle,
6656 event: None,
6657 close_failures: 1,
6658 }))
6659 .unwrap();
6660 let connection = opened["connection"].as_str().unwrap().to_string();
6661 service.terminal_launches.insert(
6662 connection.clone(),
6663 StructuredLaunch {
6664 cwd: PathBuf::from("/fixture"),
6665 program: "fixture".into(),
6666 arguments: Vec::new(),
6667 env: BTreeMap::new(),
6668 },
6669 );
6670 let first = service
6671 .handle_async(request(
6672 1,
6673 "harness.v1.runtimes.close",
6674 json!({"connection": connection}),
6675 ))
6676 .await;
6677 assert!(first.get("error").is_some(), "{first}");
6679 assert!(!service.runtimes.contains_key(&connection));
6684 assert!(!service.terminal_launches.contains_key(&connection));
6685 assert!(!service.runtime_sequences.contains_key(&runtime_id));
6686 let again = service
6687 .handle_async(request(
6688 2,
6689 "harness.v1.runtimes.close",
6690 json!({"connection": connection}),
6691 ))
6692 .await;
6693 assert_eq!(again["error"]["code"], -32602, "{again}");
6694 }
6695
6696 fn request(id: u64, method: &str, params: Value) -> Value {
6697 json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6698 }
6699
6700 fn hermes_store() -> PathBuf {
6703 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6704 }
6705
6706 fn hermes_discovery(params: Value) -> Value {
6710 let mut response =
6711 HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6712 let store = hermes_store().display().to_string();
6713 for session in response["result"]["sessions"]
6714 .as_array_mut()
6715 .expect("sessions array")
6716 {
6717 if session["locator"]["storage"]["path"] == json!(store) {
6718 session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6719 }
6720 session.as_object_mut().unwrap().remove("activity");
6724 }
6725 response["result"].take()
6726 }
6727
6728 fn hermes_query() -> Value {
6729 json!({
6730 "harnesses": ["hermes"],
6731 "homes": {"hermes": hermes_store()},
6732 })
6733 }
6734
6735 fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6736 result["sessions"]
6737 .as_array()
6738 .expect("sessions array")
6739 .iter()
6740 .find(|session| session["locator"]["session_id"] == json!(id))
6741 .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6742 }
6743
6744 #[test]
6745 fn orch6_discover_rows_carry_the_conversation_nouns() {
6746 let result = hermes_discovery(hermes_query());
6747
6748 let dm = row(&result, "tg-dm-1");
6751 assert_eq!(dm["trigger"], json!("channel"));
6752 assert_eq!(dm["surface"]["platform"], json!("telegram"));
6753 assert_eq!(dm["surface"]["kind"], json!("dm"));
6754 assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6755 assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6756 assert_eq!(
6757 dm["workspace"],
6758 json!({"kind": "channel", "value": "telegram:123456"})
6759 );
6760 assert!(dm.get("profile").is_none(), "{dm:#}");
6761
6762 let fire = row(&result, "cron_job42_20260902_120000");
6764 assert_eq!(fire["trigger"], json!("cron"));
6765 assert_eq!(
6766 fire["recurrence"],
6767 json!({"job_id": "job42", "kind": "cron"})
6768 );
6769 assert_eq!(fire["workspace"]["kind"], json!("repo"));
6770
6771 let coder = row(&result, "tg-coder-1");
6774 assert_eq!(coder["trigger"], json!("channel"));
6775 assert_eq!(coder["profile"], json!("coder"));
6776 assert_eq!(coder["surface"]["thread_id"], json!("55"));
6777 assert_eq!(
6778 coder["surface"]["key"],
6779 json!("agent:coder:telegram:group:-100777:55")
6780 );
6781 assert_eq!(
6782 coder["workspace"],
6783 json!({"kind": "repo", "value": "/workspace/project"})
6784 );
6785 assert_eq!(
6786 coder["cross_surface"],
6787 json!({"state": "pending", "platform": "discord"})
6788 );
6789
6790 let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6792 assert_eq!(acp["trigger"], json!("human"));
6793 assert!(acp.get("surface").is_none(), "{acp:#}");
6794 assert_eq!(acp["workspace"], json!({"kind": "none"}));
6795 }
6796
6797 #[test]
6798 fn orch6_discover_filters_by_harness_and_profile() {
6799 let mut params = hermes_query();
6800 params["profile"] = json!("coder");
6801 let result = hermes_discovery(params);
6802 let ids: Vec<&str> = result["sessions"]
6803 .as_array()
6804 .expect("sessions array")
6805 .iter()
6806 .map(|session| session["locator"]["session_id"].as_str().unwrap())
6807 .collect();
6808 assert_eq!(ids, vec!["tg-coder-1"]);
6809
6810 let mut missing = hermes_query();
6813 missing["profile"] = json!("nobody");
6814 assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6815
6816 let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6819 assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6820 }
6821
6822 #[test]
6823 fn orch6_load_reports_the_same_nouns_as_discovery() {
6824 let mut service = HarnessSessionService::new();
6825 let loaded = service.handle(request(
6826 1,
6827 "harness.v1.sessions.load",
6828 json!({"locator": {
6829 "harness": "hermes",
6830 "session_id": "tg-coder-1",
6831 "storage": {"kind": "file", "path": hermes_store()},
6832 }}),
6833 ));
6834 let session = &loaded["result"]["session"];
6835 let discovered = hermes_discovery(hermes_query());
6836 let row = row(&discovered, "tg-coder-1");
6837 for noun in [
6838 "trigger",
6839 "surface",
6840 "profile",
6841 "recurrence",
6842 "cross_surface",
6843 "workspace",
6844 ] {
6845 assert_eq!(
6846 session[noun],
6847 row.get(noun).cloned().unwrap_or(Value::Null),
6848 "`{noun}` disagrees between sessions.load and sessions.discover"
6849 );
6850 }
6851 }
6852
6853 fn profile_fixture_homes() -> Value {
6856 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6857 json!({
6858 "hermes": fixtures.join("hermes_home/state.db"),
6859 "openclaw": fixtures.join("openclaw_home"),
6860 })
6861 }
6862
6863 fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6864 response["result"]["profiles"]
6865 .as_array()
6866 .unwrap_or_else(|| panic!("no profiles array in {response}"))
6867 .iter()
6868 .find(|row| row["harness"] == harness && row["name"] == name)
6869 .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6870 }
6871
6872 #[test]
6877 fn profiles_list_reads_every_source_uniformly() {
6878 let mut service = HarnessSessionService::new();
6879 let response = service.handle(request(
6880 1,
6881 "harness.v1.profiles.list",
6882 json!({"homes": profile_fixture_homes()}),
6883 ));
6884 assert_eq!(
6885 response["result"]["schema"],
6886 crate::profiles::PROFILES_SCHEMA
6887 );
6888
6889 let default = profile_row(&response, "hermes", "default");
6890 assert_eq!(default["kind"], "hermes_profile");
6891 assert_eq!(default["default"], true);
6892 assert_eq!(default["routes"], 0);
6893 assert_eq!(default["sessions"], 11);
6894 assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6895
6896 let coder = profile_row(&response, "hermes", "coder");
6897 assert_eq!(coder["kind"], "hermes_profile");
6898 assert_eq!(coder["default"], false);
6899 assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6900 assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6901 assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6902 assert!(coder["home"]
6903 .as_str()
6904 .unwrap()
6905 .ends_with("hermes_home/profiles/coder"));
6906
6907 let main = profile_row(&response, "openclaw", "main");
6908 assert_eq!(main["kind"], "openclaw_agent");
6909 assert_eq!(main["default"], true);
6912 assert_eq!(main["routes"], 0);
6913 assert_eq!(main["sessions"], 4);
6914 assert_eq!(
6915 main["model"],
6916 Value::Null,
6917 "`agents.defaults.model` is an install default, not this agent's pin"
6918 );
6919
6920 let design = profile_row(&response, "openclaw", "design");
6921 assert_eq!(design["default"], false);
6922 assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6923 assert_eq!(design["sessions"], 0);
6924 assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6925
6926 let preset = profile_row(&response, "supercode", "supercode-default");
6927 assert_eq!(preset["kind"], "preset");
6928 assert_eq!(preset["default"], true);
6929 assert_eq!(preset["home"], Value::Null);
6930 assert_eq!(preset["routes"], Value::Null);
6931 }
6932
6933 #[test]
6936 fn profiles_list_reads_codex_profile_tables() {
6937 let codex_home = std::env::temp_dir().join(format!(
6938 "supercode-orch10-codex-{}-{}",
6939 std::process::id(),
6940 std::time::SystemTime::now()
6941 .duration_since(std::time::UNIX_EPOCH)
6942 .unwrap()
6943 .as_nanos()
6944 ));
6945 std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6946 std::fs::write(
6947 codex_home.join("config.toml"),
6948 "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6949 )
6950 .unwrap();
6951
6952 let mut service = HarnessSessionService::new();
6953 let response = service.handle(request(
6954 1,
6955 "harness.v1.profiles.list",
6956 json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6957 ));
6958 let rows = response["result"]["profiles"].as_array().unwrap();
6959 assert_eq!(rows.len(), 2, "{response}");
6960 let review = profile_row(&response, "codex", "review");
6961 assert_eq!(review["kind"], "codex_profile");
6962 assert_eq!(review["default"], true);
6963 assert_eq!(review["model"], "gpt-5.1-codex");
6964 assert_eq!(review["home"], Value::Null);
6965 assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6966
6967 let got = service.handle(request(
6968 2,
6969 "harness.v1.profiles.get",
6970 json!({
6971 "harness": "codex",
6972 "name": "fast",
6973 "homes": {"codex": codex_home.join("sessions")},
6974 }),
6975 ));
6976 assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6977 std::fs::remove_dir_all(&codex_home).ok();
6978 }
6979
6980 #[test]
6983 fn profiles_refuse_harnesses_without_the_concept() {
6984 let mut service = HarnessSessionService::new();
6985 let response = service.handle(request(
6986 1,
6987 "harness.v1.profiles.list",
6988 json!({"harness": "claude-code"}),
6989 ));
6990 assert_eq!(response["error"]["code"], -32020, "{response}");
6991
6992 let missing = service.handle(request(
6993 2,
6994 "harness.v1.profiles.get",
6995 json!({
6996 "harness": "hermes",
6997 "name": "no-such-profile",
6998 "homes": profile_fixture_homes(),
6999 }),
7000 ));
7001 assert_eq!(missing["error"]["code"], -32602, "{missing}");
7002 }
7003
7004 #[test]
7007 fn profiles_methods_are_advertised() {
7008 let mut service = HarnessSessionService::new();
7009 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
7010 let methods = response["result"]["methods"].as_array().unwrap();
7011 for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
7012 assert!(
7013 methods.iter().any(|entry| entry == method),
7014 "{method} is not advertised"
7015 );
7016 }
7017 }
7018
7019 fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
7024 response["result"]["channels"]
7025 .as_array()
7026 .unwrap_or_else(|| panic!("no channels array in {response}"))
7027 .iter()
7028 .find(|row| row["harness"] == harness && row["name"] == name)
7029 .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
7030 }
7031
7032 fn channels_list(harness: Option<&str>) -> Value {
7033 let mut params = json!({"homes": profile_fixture_homes()});
7034 if let Some(harness) = harness {
7035 params["harness"] = json!(harness);
7036 }
7037 HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
7038 }
7039
7040 #[test]
7044 fn channels_list_reads_both_gateway_harnesses_uniformly() {
7045 let response = channels_list(None);
7046 assert_eq!(
7047 response["result"]["schema"],
7048 crate::channels::CHANNELS_SCHEMA
7049 );
7050
7051 let telegram = channel_row(&response, "hermes", "telegram");
7054 assert_eq!(telegram["kind"], "telegram");
7055 assert_eq!(telegram["enabled"], true);
7056 assert_eq!(telegram["configured"], true);
7057 assert_eq!(telegram["sessions"], 2);
7061 let api = channel_row(&response, "hermes", "api_server");
7062 assert_eq!(api["configured"], true, "extra.key is a credential key");
7063 assert_eq!(api["sessions"], 0);
7064 let webhook = channel_row(&response, "hermes", "webhook");
7065 assert_eq!(webhook["enabled"], false);
7066 assert_eq!(webhook["configured"], true);
7069
7070 let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
7072 assert_eq!(linked["kind"], "slack");
7073 assert_eq!(linked["account"], "T0FIXTURE");
7074 assert_eq!(linked["enabled"], true);
7075 assert_eq!(linked["configured"], true);
7076 let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
7077 assert_eq!(unlinked["enabled"], false);
7078 assert_eq!(
7079 unlinked["configured"], false,
7080 "an account with no credential key is not configured"
7081 );
7082 let telegram = channel_row(&response, "openclaw", "telegram");
7085 assert_eq!(telegram["account"], "hermes-fixture-bot");
7086 assert_eq!(telegram["configured"], true);
7087
7088 for row in response["result"]["channels"].as_array().unwrap() {
7090 assert_eq!(row["status"], "unknown", "{row}");
7091 }
7092 }
7093
7094 #[test]
7098 fn channels_rows_never_carry_a_fixture_secret() {
7099 let secrets = [
7100 "FAKE-TOKEN-DO-NOT-EMIT",
7101 "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
7102 "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
7103 "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
7104 "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
7105 ];
7106 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7108 let raw = format!(
7109 "{}{}",
7110 std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
7111 std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
7112 );
7113 for secret in secrets {
7114 assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
7115 }
7116
7117 let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
7118 for secret in secrets {
7119 assert!(
7120 !emitted.contains(secret),
7121 "`{secret}` leaked into a channel row: {emitted}"
7122 );
7123 }
7124 for row in channels_list(None)["result"]["channels"]
7127 .as_array()
7128 .unwrap()
7129 {
7130 for key in row.as_object().unwrap().keys() {
7131 let key = key.to_ascii_lowercase();
7132 assert!(
7133 !["token", "key", "secret", "password", "credential"]
7134 .iter()
7135 .any(|marker| key.ends_with(marker)),
7136 "`{key}` is a credential-shaped field on a channel row"
7137 );
7138 }
7139 }
7140 }
7141
7142 #[test]
7144 fn channels_status_reads_one_row_by_name() {
7145 let mut service = HarnessSessionService::new();
7146 let got = service.handle(request(
7147 1,
7148 "harness.v1.channels.status",
7149 json!({
7150 "harness": "openclaw",
7151 "name": "slack/T0FIXTURE",
7152 "homes": profile_fixture_homes(),
7153 }),
7154 ));
7155 assert_eq!(got["result"]["channel"]["kind"], "slack");
7156 assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
7157 assert_eq!(got["result"]["channel"]["status"], "unknown");
7158
7159 let missing = service.handle(request(
7160 2,
7161 "harness.v1.channels.status",
7162 json!({
7163 "harness": "openclaw",
7164 "name": "no-such-channel",
7165 "homes": profile_fixture_homes(),
7166 }),
7167 ));
7168 assert_eq!(missing["error"]["code"], -32602, "{missing}");
7169 }
7170
7171 #[test]
7175 fn channels_refuse_harnesses_without_the_concept() {
7176 let response = channels_list(Some("claude-code"));
7177 assert_eq!(response["error"]["code"], -32020, "{response}");
7178 let codex = channels_list(Some("codex"));
7179 assert_eq!(codex["error"]["code"], -32020, "{codex}");
7180 }
7181
7182 #[test]
7184 fn channels_list_filters_by_harness() {
7185 let response = channels_list(Some("openclaw"));
7186 let rows = response["result"]["channels"].as_array().unwrap();
7187 assert!(!rows.is_empty(), "{response}");
7188 assert!(
7189 rows.iter().all(|row| row["harness"] == "openclaw"),
7190 "harness filter leaked: {response}"
7191 );
7192 }
7193
7194 #[test]
7197 fn channels_methods_are_advertised() {
7198 let mut service = HarnessSessionService::new();
7199 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
7200 let methods = response["result"]["methods"].as_array().unwrap();
7201 for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
7202 assert!(
7203 methods.iter().any(|entry| entry == method),
7204 "{method} is not advertised"
7205 );
7206 }
7207 }
7208
7209 #[test]
7214 fn orch6_story_fixture_matches_the_live_discovery_response() {
7215 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7216 .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
7217 let mut result = hermes_discovery(hermes_query());
7218 result.as_object_mut().unwrap().remove("next_cursor");
7222 let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
7223 if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
7224 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
7225 std::fs::write(&path, &rendered).unwrap();
7226 }
7227 let committed = std::fs::read_to_string(&path).unwrap_or_default();
7228 assert_eq!(
7229 committed, rendered,
7230 "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
7231 re-run with SUPERCODE_UPDATE_FIXTURES=1"
7232 );
7233 }
7234
7235 fn pi_locator() -> SessionLocator {
7236 SessionLocator {
7237 harness: HarnessId::from(HarnessId::PI),
7238 session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
7239 storage: StorageLocator::File {
7240 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7241 .join("tests/fixtures/pi_session.jsonl"),
7242 },
7243 }
7244 }
7245
7246 fn opencode_locator() -> SessionLocator {
7247 let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
7248 SessionLocator {
7249 harness: HarnessId::from(HarnessId::OPENCODE),
7250 session_id: session_id.into(),
7251 storage: StorageLocator::Sqlite {
7252 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7253 .join("tests/fixtures/opencode_fixture/opencode.db"),
7254 selector: session_id.into(),
7255 },
7256 }
7257 }
7258
7259 fn grok_locator() -> SessionLocator {
7260 SessionLocator {
7261 harness: HarnessId::from(HarnessId::GROK),
7262 session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
7263 storage: StorageLocator::File {
7264 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7265 .join("tests/fixtures/grok_session/chat_history.jsonl"),
7266 },
7267 }
7268 }
7269
7270 fn fixture_homes() -> Value {
7273 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7274 json!({
7275 "claude_code": fixtures.join("__absent__"),
7276 "codex": fixtures.join("__absent__"),
7277 "opencode": fixtures.join("__absent__"),
7278 "pi": fixtures.join("__absent__"),
7279 "agents": fixtures.join("__absent__"),
7280 "hermes": fixtures.join("hermes_home"),
7281 "openclaw": fixtures.join("openclaw_home"),
7282 })
7283 }
7284
7285 #[test]
7286 fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
7287 let root = std::env::temp_dir().join(format!(
7288 "supercode-preview-rpc-{}-{}",
7289 std::process::id(),
7290 std::time::SystemTime::now()
7291 .duration_since(std::time::UNIX_EPOCH)
7292 .unwrap()
7293 .as_nanos()
7294 ));
7295 std::fs::create_dir_all(&root).unwrap();
7296 for id in ["first", "second"] {
7297 std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7298 json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7299 json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7300 )).unwrap();
7301 }
7302 let mut service = HarnessSessionService::new();
7303 let query = json!({
7304 "harnesses": ["codex"], "homes": {"codex": root},
7305 "query": "nebula", "search_previews": true, "limit": 1
7306 });
7307 let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7308 assert!(first.get("error").is_none(), "{first}");
7309 assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7310 assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7311 let mut next_query = query.clone();
7312 next_query["cursor"] = first["result"]["next_cursor"].clone();
7313 let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7314 assert_eq!(next["result"]["receipt"]["returned"], 1);
7315 assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7316 assert_eq!(next["result"]["receipt"]["truncated"], false);
7317 assert_ne!(
7318 first["result"]["sessions"][0]["locator"],
7319 next["result"]["sessions"][0]["locator"]
7320 );
7321 let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7322 assert!(
7323 refused["error"]["message"]
7324 .as_str()
7325 .unwrap()
7326 .contains("use sessions.discover"),
7327 "{refused}"
7328 );
7329 std::fs::remove_dir_all(root).unwrap();
7330 }
7331
7332 #[test]
7333 fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7334 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7335 let root = std::env::temp_dir().join(format!(
7336 "supercode-index-rpc-{}-{}",
7337 std::process::id(),
7338 std::time::SystemTime::now()
7339 .duration_since(std::time::UNIX_EPOCH)
7340 .unwrap()
7341 .as_nanos()
7342 ));
7343 std::fs::create_dir_all(&root).unwrap();
7344 for id in ["first", "second"] {
7345 std::fs::write(root.join(format!("{id}.jsonl")), format!(
7346 "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7347 )).unwrap();
7348 }
7349 let mut service = HarnessSessionService::new();
7350 let opened = service.handle(request(
7351 1,
7352 "harness.v1.sessions.index.subscribe",
7353 json!({
7354 "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7355 }),
7356 ));
7357 assert!(opened.get("error").is_none(), "{opened:#}");
7358 let subscription = opened["result"]["subscription"]
7359 .as_str()
7360 .unwrap()
7361 .to_owned();
7362 assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7363 for params in [
7364 json!({"subscription": subscription, "limit": 0}),
7365 json!({"subscription": subscription, "limit": 2049}),
7366 json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7367 json!({"subscription": "unknown", "limit": 2}),
7368 ] {
7369 let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7370 assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7371 }
7372 for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7373 let response = service.handle(request(
7374 3,
7375 "harness.v1.sessions.index.resize",
7376 json!({
7377 "subscription": subscription, "limit": limit
7378 }),
7379 ));
7380 assert!(response.get("error").is_none(), "{response:#}");
7381 assert_eq!(response["result"]["subscription"], subscription);
7382 assert_eq!(response["result"]["revision"], revision);
7383 assert_eq!(
7384 response["result"]["initial"].as_array().unwrap().len(),
7385 limit
7386 );
7387 assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7388 assert_eq!(service.index_subscriptions.len(), 1);
7389 }
7390 let removed = service.handle(request(
7391 4,
7392 "harness.v1.sessions.index.unsubscribe",
7393 json!({
7394 "subscription": subscription
7395 }),
7396 ));
7397 assert_eq!(removed["result"]["removed"], true);
7398 let stale = service.handle(request(
7399 5,
7400 "harness.v1.sessions.index.resize",
7401 json!({
7402 "subscription": subscription, "limit": 1
7403 }),
7404 ));
7405 assert_eq!(stale["error"]["code"], -32602);
7406 drop(service);
7407 std::fs::remove_dir_all(root).unwrap();
7408 }
7409
7410 fn skills_rows(params: Value) -> Vec<Value> {
7411 let response =
7412 HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7413 assert!(response.get("error").is_none(), "{response:#}");
7414 response["result"].as_array().cloned().unwrap_or_default()
7415 }
7416
7417 #[test]
7420 fn skills_list_reads_the_hermes_and_openclaw_roots() {
7421 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7422 let rows = skills_rows(json!({
7423 "homes": fixture_homes(),
7424 "cwd": fixtures.join("hermes_home"),
7425 }));
7426 let arxiv = rows
7427 .iter()
7428 .find(|row| row["name"] == json!("arxiv-search"))
7429 .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7430 assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7431 assert_eq!(arxiv["scope"], json!("user"));
7432 assert_eq!(arxiv["version"], json!("1.4.0"));
7433 assert!(arxiv["location"]
7434 .as_str()
7435 .unwrap()
7436 .ends_with("hermes_home/skills/research/arxiv"));
7437
7438 let bare = rows
7440 .iter()
7441 .find(|row| row["name"] == json!("bare-skill"))
7442 .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7443 assert_eq!(bare["enabled"], json!(null));
7444 assert!(bare.get("description").is_none());
7445
7446 let demo = rows
7447 .iter()
7448 .find(|row| row["name"] == json!("clawhub-demo"))
7449 .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7450 assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7451 assert_eq!(demo["scope"], json!("managed"));
7452 assert_eq!(demo["enabled"], json!(false));
7453 }
7454
7455 #[test]
7457 fn skills_list_filters_by_harness_and_scope() {
7458 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7459 let hermes = skills_rows(json!({
7460 "homes": fixture_homes(),
7461 "cwd": fixtures.join("hermes_home"),
7462 "harness": HarnessId::HERMES,
7463 }));
7464 assert!(!hermes.is_empty());
7465 assert!(hermes
7466 .iter()
7467 .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7468
7469 let managed = skills_rows(json!({
7470 "homes": fixture_homes(),
7471 "cwd": fixtures.join("openclaw_home"),
7472 "harness": HarnessId::OPENCLAW,
7473 "scope": "managed",
7474 }));
7475 assert_eq!(managed.len(), 1, "{managed:#?}");
7476 assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7477
7478 let bundled = skills_rows(json!({
7479 "homes": fixture_homes(),
7480 "cwd": fixtures.join("openclaw_home"),
7481 "harness": HarnessId::OPENCLAW,
7482 "scope": "bundled",
7483 }));
7484 assert!(bundled.is_empty(), "{bundled:#?}");
7485 }
7486
7487 #[test]
7490 fn skills_list_refuses_an_unknown_harness() {
7491 let response = HarnessSessionService::new().handle(request(
7492 1,
7493 "harness.v1.skills.list",
7494 json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7495 ));
7496 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7497 assert!(response["error"]["message"]
7498 .as_str()
7499 .unwrap()
7500 .contains("not-a-harness"));
7501 }
7502
7503 #[test]
7505 fn skills_list_is_an_advertised_method_and_sdk_operation() {
7506 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7507 assert_eq!(
7508 SdkOperation::from_method("harness.v1.skills.list"),
7509 Some(SdkOperation::SkillsList)
7510 );
7511 }
7512
7513 #[test]
7517 fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7518 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7519 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7520 assert_eq!(
7521 SdkOperation::from_method("harness.v1.skills.install"),
7522 Some(SdkOperation::SkillsInstall)
7523 );
7524 assert_eq!(
7525 SdkOperation::from_method("harness.v1.skills.remove"),
7526 Some(SdkOperation::SkillsRemove)
7527 );
7528 }
7529
7530 #[test]
7534 fn skills_install_and_remove_drive_the_directory_door() {
7535 let root = std::env::temp_dir().join(format!(
7536 "supercode-orch22-rpc-{}-{}",
7537 std::process::id(),
7538 std::time::SystemTime::now()
7539 .duration_since(std::time::UNIX_EPOCH)
7540 .unwrap()
7541 .as_nanos()
7542 ));
7543 let source = root.join("probe-src");
7544 std::fs::create_dir_all(&source).unwrap();
7545 std::fs::write(
7546 source.join("SKILL.md"),
7547 "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7548 )
7549 .unwrap();
7550 let homes = json!({
7551 "claude_code": root.join("claude_home"),
7552 "codex": root.join("__absent__"),
7553 "opencode": root.join("__absent__"),
7554 "pi": root.join("__absent__"),
7555 "hermes": root.join("__absent__"),
7556 "openclaw": root.join("__absent__"),
7557 "agents": root.join("__absent__"),
7558 });
7559
7560 let mut service = HarnessSessionService::new();
7561 let installed = service.handle(request(
7562 1,
7563 "harness.v1.skills.install",
7564 json!({
7565 "harness": HarnessId::CLAUDE_CODE,
7566 "source": source,
7567 "scope": "user",
7568 "cwd": root,
7569 "homes": homes,
7570 }),
7571 ));
7572 let result = &installed["result"];
7573 assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7574 assert_eq!(result["verb"], json!("install"));
7575 assert!(result["ran"]
7576 .as_str()
7577 .is_some_and(|ran| ran.starts_with("cp -R ")));
7578 assert_eq!(result["skill"]["scope"], json!("user"));
7579
7580 let removed = service.handle(request(
7581 2,
7582 "harness.v1.skills.remove",
7583 json!({
7584 "harness": HarnessId::CLAUDE_CODE,
7585 "name": "orch22-rpc",
7586 "scope": "user",
7587 "cwd": root,
7588 "homes": homes,
7589 }),
7590 ));
7591 assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7592 assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7593 std::fs::remove_dir_all(&root).ok();
7594 }
7595
7596 #[test]
7599 fn skills_remove_refuses_openclaw_at_the_pin() {
7600 let response = HarnessSessionService::new().handle(request(
7601 1,
7602 "harness.v1.skills.remove",
7603 json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7604 ));
7605 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7606 assert!(response["error"]["message"]
7607 .as_str()
7608 .unwrap()
7609 .contains("no `skills remove` verb"));
7610 }
7611
7612 #[test]
7615 fn skills_install_refuses_a_harness_without_a_skills_root() {
7616 let response = HarnessSessionService::new().handle(request(
7617 1,
7618 "harness.v1.skills.install",
7619 json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7620 ));
7621 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7622 assert!(response["error"]["message"]
7623 .as_str()
7624 .unwrap()
7625 .contains("not-a-harness"));
7626 }
7627
7628 fn memory_homes() -> Value {
7636 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7637 json!({
7638 "claude_code": fixtures.join("__absent__"),
7639 "codex": fixtures.join("__absent__"),
7640 "opencode": fixtures.join("__absent__"),
7641 "pi": fixtures.join("__absent__"),
7642 "grok": fixtures.join("__absent__"),
7643 "gemini": fixtures.join("__absent__"),
7644 "goose": fixtures.join("__absent__"),
7645 "supercode": fixtures.join("__absent__"),
7646 "hermes": fixtures.join("hermes_home/state.db"),
7647 "openclaw": fixtures.join("openclaw_home"),
7648 })
7649 }
7650
7651 fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7652 let response = HarnessSessionService::new().handle(request(1, method, params));
7653 assert!(response.get("error").is_none(), "{response:#}");
7654 assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7655 response["result"][key]
7656 .as_array()
7657 .cloned()
7658 .unwrap_or_default()
7659 }
7660
7661 fn memory_documents(params: Value) -> Vec<Value> {
7662 memory_call_ok("harness.v1.memory.show", params, "documents")
7663 }
7664
7665 fn memory_matches(params: Value) -> Vec<Value> {
7666 memory_call_ok("harness.v1.memory.search", params, "matches")
7667 }
7668
7669 fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7670 rows.iter()
7671 .find(|row| row["profile"] == profile && row["name"] == name)
7672 .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7673 }
7674
7675 #[test]
7678 fn memory_show_reads_the_hermes_profile_homes() {
7679 let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7680
7681 let notes = find_document(&rows, "default", "MEMORY.md");
7682 assert_eq!(notes["harness"], "hermes");
7683 assert_eq!(notes["scope"], "user");
7684 assert!(notes["size"].as_u64().unwrap() > 0);
7685 assert!(notes["updated_at"].is_string(), "{notes:#?}");
7686 assert!(notes.get("content").is_none(), "{notes:#?}");
7688 assert_eq!(notes["truncated"], true);
7689 assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7690
7691 let user = find_document(&rows, "default", "USER.md");
7692 assert_eq!(user["scope"], "user");
7693 assert!(user["preview"]
7694 .as_array()
7695 .unwrap()
7696 .iter()
7697 .any(|line| line.as_str().unwrap().contains("neovim")));
7698
7699 let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7700 assert!(topic["path"]
7701 .as_str()
7702 .unwrap()
7703 .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7704
7705 let coder = find_document(&rows, "coder", "MEMORY.md");
7707 assert_eq!(coder["scope"], "profile");
7708 assert!(coder["path"]
7709 .as_str()
7710 .unwrap()
7711 .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7712 }
7713
7714 #[test]
7717 fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7718 let rows = memory_documents(json!({
7719 "harness": "hermes",
7720 "profile": "coder",
7721 "full": true,
7722 "homes": memory_homes(),
7723 }));
7724 assert!(
7725 rows.iter().all(|row| row["profile"] == "coder"),
7726 "{rows:#?}"
7727 );
7728 let coder = find_document(&rows, "coder", "MEMORY.md");
7729 assert!(coder["content"]
7730 .as_str()
7731 .expect("full returns the body")
7732 .contains("anthropic/claude-opus-4-8"));
7733 }
7734
7735 #[test]
7739 fn memory_show_reads_the_openclaw_agent_workspaces() {
7740 let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7741
7742 let main = find_document(&rows, "main", "MEMORY.md");
7743 assert_eq!(main["scope"], "agent");
7744 assert!(main["path"]
7745 .as_str()
7746 .unwrap()
7747 .ends_with("openclaw_home/workspace/MEMORY.md"));
7748
7749 let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7750 assert!(topic["path"]
7751 .as_str()
7752 .unwrap()
7753 .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7754
7755 let design = find_document(&rows, "design", "MEMORY.md");
7756 assert!(design["path"]
7757 .as_str()
7758 .unwrap()
7759 .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7760 }
7761
7762 #[test]
7765 fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7766 let scratch = std::env::temp_dir().join(format!(
7767 "supercode-orch12-cc-{}-{}",
7768 std::process::id(),
7769 std::time::SystemTime::now()
7770 .duration_since(std::time::UNIX_EPOCH)
7771 .unwrap()
7772 .as_nanos()
7773 ));
7774 let project = scratch.join("repo");
7775 std::fs::create_dir_all(project.join(".git")).unwrap();
7776 let worktree = project.join("crates/harness");
7779 std::fs::create_dir_all(&worktree).unwrap();
7780 let slug: String = project
7781 .to_string_lossy()
7782 .chars()
7783 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7784 .collect();
7785 let projects = scratch.join("claude/projects");
7786 let memory = projects.join(&slug).join("memory");
7787 std::fs::create_dir_all(&memory).unwrap();
7788 std::fs::write(
7789 memory.join("MEMORY.md"),
7790 "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7791 )
7792 .unwrap();
7793 std::fs::write(
7794 memory.join("build-box.md"),
7795 "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7796 )
7797 .unwrap();
7798
7799 let mut homes = memory_homes();
7800 homes["claude_code"] = json!(projects);
7801 let rows = memory_documents(json!({
7802 "harness": "claude-code",
7803 "cwd": worktree,
7804 "homes": homes,
7805 }));
7806 let index = find_document(&rows, &slug, "MEMORY.md");
7807 assert_eq!(index["harness"], "claude-code");
7808 assert_eq!(index["scope"], "project");
7809 let topic = find_document(&rows, &slug, "build-box.md");
7810 assert!(topic["preview"]
7811 .as_array()
7812 .unwrap()
7813 .iter()
7814 .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7815
7816 let hits = memory_matches(json!({
7817 "harness": "claude-code",
7818 "query": "pinned harnesses",
7819 "cwd": worktree,
7820 "homes": homes,
7821 }));
7822 assert_eq!(hits.len(), 1, "{hits:#?}");
7823 assert_eq!(hits[0]["name"], "MEMORY.md");
7824 assert_eq!(hits[0]["line"], 2);
7825
7826 let _ = std::fs::remove_dir_all(&scratch);
7827 }
7828
7829 #[test]
7835 fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7836 let state = std::env::temp_dir().join(format!(
7837 "supercode-orch12-oc-{}-{}",
7838 std::process::id(),
7839 std::time::SystemTime::now()
7840 .duration_since(std::time::UNIX_EPOCH)
7841 .unwrap()
7842 .as_nanos()
7843 ));
7844 std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7846 std::fs::create_dir_all(state.join("workspace")).unwrap();
7847 std::fs::write(
7848 state.join("workspace/MEMORY.md"),
7849 "the gateway websocket needs credentials\n",
7850 )
7851 .unwrap();
7852
7853 let mut homes = memory_homes();
7854 homes["openclaw"] = json!(state);
7855 let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7856 assert_eq!(rows.len(), 1, "{rows:#?}");
7857 let row = find_document(&rows, "main", "MEMORY.md");
7858 assert_eq!(row["scope"], "agent");
7859 assert!(row["path"]
7860 .as_str()
7861 .unwrap()
7862 .ends_with("workspace/MEMORY.md"));
7863
7864 let _ = std::fs::remove_dir_all(&state);
7865 }
7866
7867 #[test]
7870 fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7871 let hit = memory_matches(json!({
7872 "harness": "hermes",
7873 "query": "NEOVIM",
7874 "homes": memory_homes(),
7875 }));
7876 assert_eq!(hit.len(), 1, "{hit:#?}");
7877 assert_eq!(hit[0]["harness"], "hermes");
7878 assert_eq!(hit[0]["name"], "USER.md");
7879 assert_eq!(hit[0]["scope"], "user");
7880 assert_eq!(hit[0]["line"], 5);
7881 assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7882
7883 let regex = memory_matches(json!({
7885 "harness": "hermes",
7886 "query": "neo(vim|vi)",
7887 "regex": true,
7888 "homes": memory_homes(),
7889 }));
7890 assert_eq!(regex.len(), 1, "{regex:#?}");
7891
7892 let miss = memory_matches(json!({
7893 "harness": "hermes",
7894 "query": "no-memory-line-says-this",
7895 "homes": memory_homes(),
7896 }));
7897 assert!(miss.is_empty(), "{miss:#?}");
7898 }
7899
7900 #[test]
7903 fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7904 for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7905 let response = HarnessSessionService::new().handle(request(
7906 1,
7907 method,
7908 json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7909 ));
7910 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7911 assert!(response["error"]["message"]
7912 .as_str()
7913 .unwrap()
7914 .contains("codex"));
7915 }
7916
7917 let response = HarnessSessionService::new().handle(request(
7918 1,
7919 "harness.v1.memory.show",
7920 json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7921 ));
7922 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7923
7924 let response = HarnessSessionService::new().handle(request(
7926 1,
7927 "harness.v1.memory.show",
7928 json!({"homes": memory_homes()}),
7929 ));
7930 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7931 }
7932
7933 #[test]
7935 fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7936 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7937 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7938 assert_eq!(
7939 SdkOperation::from_method("harness.v1.memory.show"),
7940 Some(SdkOperation::MemoryShow)
7941 );
7942 assert_eq!(
7943 SdkOperation::from_method("harness.v1.memory.search"),
7944 Some(SdkOperation::MemorySearch)
7945 );
7946 }
7947
7948 struct RequestingRuntime {
7953 handle: RuntimeHandle,
7954 events: std::collections::VecDeque<HarnessEvent>,
7955 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7956 }
7957
7958 #[async_trait]
7959 impl RuntimeConnection for RequestingRuntime {
7960 fn handle(&self) -> &RuntimeHandle {
7961 &self.handle
7962 }
7963
7964 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7965 unreachable!("this runtime only raises requests")
7966 }
7967
7968 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7969 match self.events.pop_front() {
7970 Some(event) => Ok(Some(event)),
7971 None => std::future::pending().await,
7975 }
7976 }
7977
7978 async fn interrupt(&mut self) -> crate::Result<()> {
7979 Ok(())
7980 }
7981
7982 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7983 self.answered
7987 .lock()
7988 .unwrap_or_else(std::sync::PoisonError::into_inner)
7989 .push(json!({"request_id": request_id, "response": response}));
7990 Ok(())
7991 }
7992
7993 async fn close(&mut self) -> crate::Result<()> {
7994 Ok(())
7995 }
7996 }
7997
7998 fn requesting_runtime(
7999 harness: &str,
8000 events: Vec<HarnessEvent>,
8001 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
8002 ) -> Box<dyn RuntimeConnection> {
8003 requesting_runtime_named(harness, "hermes-live-session", events, answered)
8004 }
8005
8006 fn requesting_runtime_named(
8007 harness: &str,
8008 runtime_id: &str,
8009 events: Vec<HarnessEvent>,
8010 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
8011 ) -> Box<dyn RuntimeConnection> {
8012 Box::new(RequestingRuntime {
8013 handle: RuntimeHandle {
8014 harness: HarnessId::from(harness),
8015 runtime_id: runtime_id.into(),
8016 endpoint: RuntimeEndpoint::LocalProcess {
8017 pid: None,
8018 command: vec!["hermes-acp".into()],
8019 protocol: "acp".into(),
8020 },
8021 },
8022 events: events.into(),
8023 answered,
8024 })
8025 }
8026
8027 fn permission_event(id: u64, title: &str) -> HarnessEvent {
8028 HarnessEvent {
8029 sequence: None,
8030 kind: "session/request_permission".into(),
8031 payload: json!({
8032 "jsonrpc": "2.0",
8033 "id": id,
8034 "method": "session/request_permission",
8035 "params": {
8036 "sessionId": "hermes-live-session",
8037 "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
8038 "options": [
8039 {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
8040 {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
8041 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8042 ],
8043 },
8044 }),
8045 }
8046 }
8047
8048 fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
8049 let response = service.handle(request(1, "harness.v1.approvals.list", params));
8050 assert!(response.get("error").is_none(), "{response:#}");
8051 response["result"].clone()
8052 }
8053
8054 #[tokio::test]
8062 async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
8063 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8064 let mut service = HarnessSessionService::new();
8065 service.runtimes.insert(
8066 "runtime-cc".into(),
8067 requesting_runtime_named(
8068 HarnessId::CLAUDE_CODE,
8069 "claude-live-session",
8070 vec![HarnessEvent {
8071 sequence: None,
8072 kind: "control_request".into(),
8073 payload: json!({
8074 "type": "control_request",
8075 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
8076 "request": {
8077 "subtype": "can_use_tool",
8078 "tool_name": "Bash",
8079 "display_name": "Bash",
8080 "input": {"command": "touch probe-artifact.txt"},
8081 "tool_use_id": "toolu_mock_1",
8082 },
8083 }),
8084 }],
8085 answered.clone(),
8086 ),
8087 );
8088
8089 let notifications = service.poll_runtimes().await;
8090 assert_eq!(notifications.len(), 1, "{notifications:#?}");
8091
8092 let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
8093 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
8094 let row = &rows[0];
8095 assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
8096 assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
8097 assert_eq!(row["status"], "pending");
8098 assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
8099 assert_eq!(row["runtime_id"], "claude-live-session");
8100 assert_eq!(
8101 row["options"]
8102 .as_array()
8103 .unwrap()
8104 .iter()
8105 .map(|option| option["id"].as_str().unwrap())
8106 .collect::<Vec<_>>(),
8107 vec!["allow", "deny"],
8108 );
8109
8110 let response = resolve(
8111 &mut service,
8112 json!({"id": row["id"], "decision": "allow_once"}),
8113 )
8114 .await;
8115 assert!(response.get("error").is_none(), "{response:#}");
8116 assert_eq!(response["result"]["option_id"], "allow");
8117 assert_eq!(
8118 answered
8119 .lock()
8120 .unwrap_or_else(std::sync::PoisonError::into_inner)
8121 .as_slice(),
8122 &[json!({
8123 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
8124 "response": {"behavior": "allow"},
8125 })],
8126 );
8127 assert_eq!(
8128 approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
8129 .as_array()
8130 .map(Vec::len),
8131 Some(0),
8132 );
8133 }
8134
8135 #[tokio::test]
8139 async fn a_live_permission_request_lists_until_it_is_answered() {
8140 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8141 let mut service = HarnessSessionService::new();
8142 service.runtimes.insert(
8143 "runtime-1".into(),
8144 requesting_runtime(
8145 HarnessId::HERMES,
8146 vec![permission_event(7, "rm -rf build")],
8147 answered.clone(),
8148 ),
8149 );
8150
8151 let notifications = service.poll_runtimes().await;
8152 assert_eq!(notifications.len(), 1, "{notifications:#?}");
8153
8154 let rows = approvals(&mut service, json!({}));
8155 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
8156 let row = &rows[0];
8157 assert_eq!(row["id"], "runtime-1/7");
8158 assert_eq!(row["harness"], HarnessId::HERMES);
8159 assert_eq!(row["kind"], "live");
8160 assert_eq!(row["status"], "pending");
8161 assert_eq!(row["subject"], "rm -rf build");
8162 assert_eq!(row["session_id"], "hermes-live-session");
8163 assert_eq!(row["runtime_id"], "hermes-live-session");
8164 assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
8165 assert!(
8166 row["age_ms"].as_i64().is_some_and(|age| age >= 0),
8167 "{row:#}"
8168 );
8169 assert_eq!(
8170 row["options"]
8171 .as_array()
8172 .unwrap()
8173 .iter()
8174 .map(|option| option["id"].as_str().unwrap())
8175 .collect::<Vec<_>>(),
8176 vec!["allow_once", "allow_for_session", "deny"],
8177 );
8178
8179 assert_eq!(
8181 approvals(&mut service, json!({"harness": HarnessId::HERMES}))
8182 .as_array()
8183 .map(Vec::len),
8184 Some(1),
8185 );
8186 assert_eq!(
8187 approvals(&mut service, json!({"session": "some-other-session"}))
8188 .as_array()
8189 .map(Vec::len),
8190 Some(0),
8191 );
8192
8193 let response = service
8194 .handle_async(request(
8195 2,
8196 "harness.v1.runtimes.respond",
8197 json!({
8198 "connection": "runtime-1",
8199 "request_id": 7,
8200 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8201 }),
8202 ))
8203 .await;
8204 assert!(response.get("error").is_none(), "{response:#}");
8205 assert_eq!(
8206 answered
8207 .lock()
8208 .unwrap_or_else(std::sync::PoisonError::into_inner)
8209 .as_slice(),
8210 &[json!({
8211 "request_id": 7,
8212 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8213 })],
8214 );
8215
8216 let rows = approvals(&mut service, json!({}));
8217 assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
8218 }
8219
8220 #[test]
8223 fn queued_subagent_approvals_list_through_the_same_door() {
8224 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8225 crate::subagents::QueuedApproval {
8226 child_agent_id: "child-7".into(),
8227 tool: "shell".into(),
8228 subject: Some("cargo publish --dry-run".into()),
8229 queued_at_ms: 1,
8230 outcome: None,
8231 },
8232 crate::subagents::QueuedApproval {
8233 child_agent_id: "child-8".into(),
8234 tool: "write_file".into(),
8235 subject: None,
8236 queued_at_ms: 2,
8237 outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
8238 },
8239 ]));
8240 let mut service = HarnessSessionService::new();
8241 service.observe_subagent_approvals(queue);
8242
8243 let rows = approvals(&mut service, json!({}));
8244 assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
8245 assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
8246 assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
8247 assert_eq!(rows[0]["status"], "pending");
8248 assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
8249 assert_eq!(rows[1]["status"], "denied");
8250 assert!(rows[1]["options"].as_array().unwrap().is_empty());
8251
8252 let only = approvals(&mut service, json!({"session": "child-8"}));
8254 assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
8255 assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
8256 }
8257
8258 #[test]
8264 fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
8265 let response = HarnessSessionService::new().handle(request(
8266 1,
8267 "harness.v1.approvals.list",
8268 json!({"harness": "not-a-harness"}),
8269 ));
8270 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
8271 assert!(response["error"]["message"]
8272 .as_str()
8273 .unwrap()
8274 .contains("not-a-harness"));
8275 for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
8276 let response = HarnessSessionService::new().handle(request(
8277 1,
8278 "harness.v1.approvals.list",
8279 json!({"harness": harness}),
8280 ));
8281 assert!(response.get("error").is_none(), "{harness}: {response:#}");
8282 }
8283 }
8284
8285 #[test]
8289 fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
8290 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8291 assert_eq!(
8292 SdkOperation::from_method("harness.v1.approvals.list"),
8293 Some(SdkOperation::ApprovalsList)
8294 );
8295 let registry = harness_support_registry();
8296 for id in [
8297 HarnessId::HERMES,
8298 HarnessId::OPENCLAW,
8299 HarnessId::CODEX,
8300 HarnessId::CLAUDE_CODE,
8303 ] {
8304 let concept = registry
8305 .harnesses
8306 .iter()
8307 .find(|harness| harness.id.as_str() == id)
8308 .unwrap()
8309 .orchestration
8310 .concepts
8311 .iter()
8312 .find(|concept| concept.concept == "pending_request")
8313 .unwrap();
8314 assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8315 assert!(concept
8316 .methods
8317 .iter()
8318 .any(|method| method == "harness.v1.approvals.list"));
8319 }
8320 }
8321
8322 async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8325 service
8326 .handle_async(request(3, "harness.v1.approvals.resolve", params))
8327 .await
8328 }
8329
8330 #[tokio::test]
8334 async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8335 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8336 let mut service = HarnessSessionService::new();
8337 service.runtimes.insert(
8338 "runtime-1".into(),
8339 requesting_runtime(
8340 HarnessId::HERMES,
8341 vec![permission_event(7, "rm -rf build")],
8342 answered.clone(),
8343 ),
8344 );
8345 service.poll_runtimes().await;
8346
8347 let rows = approvals(&mut service, json!({}));
8348 assert_eq!(rows[0]["id"], "runtime-1/7");
8349
8350 let response = resolve(
8351 &mut service,
8352 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8353 )
8354 .await;
8355 assert!(response.get("error").is_none(), "{response:#}");
8356 assert_eq!(
8357 response["result"],
8358 json!({
8359 "id": "runtime-1/7",
8360 "decision": "allow_once",
8361 "option_id": "allow_once",
8362 "resolved": true,
8363 }),
8364 );
8365 assert_eq!(
8367 answered
8368 .lock()
8369 .unwrap_or_else(std::sync::PoisonError::into_inner)
8370 .as_slice(),
8371 &[json!({
8372 "request_id": 7,
8373 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8374 })],
8375 );
8376 assert_eq!(
8378 approvals(&mut service, json!({})).as_array().map(Vec::len),
8379 Some(0),
8380 );
8381 let response = resolve(
8383 &mut service,
8384 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8385 )
8386 .await;
8387 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8388 }
8389
8390 #[tokio::test]
8393 async fn deny_selects_the_requests_own_reject_option() {
8394 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8395 let mut service = HarnessSessionService::new();
8396 service.runtimes.insert(
8397 "runtime-1".into(),
8398 requesting_runtime(
8399 HarnessId::HERMES,
8400 vec![permission_event(11, "git push --force")],
8401 answered.clone(),
8402 ),
8403 );
8404 service.poll_runtimes().await;
8405
8406 let response = resolve(
8407 &mut service,
8408 json!({"id": "runtime-1/11", "decision": "deny"}),
8409 )
8410 .await;
8411 assert!(response.get("error").is_none(), "{response:#}");
8412 assert_eq!(response["result"]["option_id"], "deny");
8414 assert_eq!(
8415 answered
8416 .lock()
8417 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8418 json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8419 );
8420 assert_eq!(
8421 approvals(&mut service, json!({})).as_array().map(Vec::len),
8422 Some(0),
8423 );
8424 }
8425
8426 #[tokio::test]
8429 async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8430 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8431 let mut service = HarnessSessionService::new();
8432 let mut event = permission_event(3, "rm -rf build");
8433 event.payload["params"]["options"] = json!([
8436 {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8437 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8438 ]);
8439 service.runtimes.insert(
8440 "runtime-1".into(),
8441 requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8442 );
8443 service.poll_runtimes().await;
8444
8445 let response = resolve(
8446 &mut service,
8447 json!({"id": "runtime-1/3", "decision": "allow_always"}),
8448 )
8449 .await;
8450 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8451 let message = response["error"]["message"].as_str().unwrap();
8452 assert!(message.contains("allow_always"), "{message}");
8453 assert!(message.contains("allow_once, deny"), "{message}");
8454 assert!(answered
8456 .lock()
8457 .unwrap_or_else(std::sync::PoisonError::into_inner)
8458 .is_empty());
8459 assert_eq!(
8460 approvals(&mut service, json!({})).as_array().map(Vec::len),
8461 Some(1),
8462 );
8463 }
8464
8465 #[tokio::test]
8469 async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8470 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8471 crate::subagents::QueuedApproval {
8472 child_agent_id: "child-7".into(),
8473 tool: "shell".into(),
8474 subject: Some("cargo publish --dry-run".into()),
8475 queued_at_ms: 1,
8476 outcome: None,
8477 },
8478 ]));
8479 let mut service = HarnessSessionService::new();
8480 service.observe_subagent_approvals(queue.clone());
8481 let row = approvals(&mut service, json!({}))[0]["id"]
8482 .as_str()
8483 .unwrap()
8484 .to_string();
8485 assert_eq!(row, "supercode/subagent/child-7/1/0");
8486
8487 let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8488 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8489 let message = response["error"]["message"].as_str().unwrap();
8490 assert!(message.contains("queued subagent record"), "{message}");
8491 assert!(message.contains("request"), "{message}");
8492 assert!(queue
8494 .lock()
8495 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8496 .outcome
8497 .is_none());
8498 }
8499
8500 #[tokio::test]
8503 async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8504 let mut service = HarnessSessionService::new();
8505 let response = resolve(
8506 &mut service,
8507 json!({"id": "runtime-9/4", "decision": "deny"}),
8508 )
8509 .await;
8510 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8511 assert!(response["error"]["message"]
8512 .as_str()
8513 .unwrap()
8514 .contains("runtime-9/4"));
8515
8516 let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8517 let message = response["error"]["message"].as_str().unwrap();
8518 assert!(
8519 message.contains("allow_once | allow_always | deny"),
8520 "{message}"
8521 );
8522
8523 let response = resolve(
8524 &mut service,
8525 json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8526 )
8527 .await;
8528 assert!(response["error"]["message"]
8529 .as_str()
8530 .unwrap()
8531 .contains("not both"));
8532 }
8533
8534 #[test]
8538 fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8539 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8540 assert_eq!(
8541 SdkOperation::from_method("harness.v1.approvals.resolve"),
8542 Some(SdkOperation::ApprovalsResolve)
8543 );
8544 assert_eq!(
8545 SdkOperation::ApprovalsResolve.action_name(),
8546 "approvals_resolve"
8547 );
8548 let registry = harness_support_registry();
8549 for id in [
8550 HarnessId::HERMES,
8551 HarnessId::OPENCLAW,
8552 HarnessId::CODEX,
8553 HarnessId::CLAUDE_CODE,
8556 ] {
8557 let concept = registry
8558 .harnesses
8559 .iter()
8560 .find(|harness| harness.id.as_str() == id)
8561 .unwrap()
8562 .orchestration
8563 .concepts
8564 .iter()
8565 .find(|concept| concept.concept == "pending_request")
8566 .unwrap();
8567 assert_eq!(
8568 concept.controlled,
8569 crate::ImplementationKind::BuiltIn,
8570 "{id}"
8571 );
8572 assert!(
8573 concept
8574 .methods
8575 .iter()
8576 .any(|method| method == "harness.v1.approvals.resolve"),
8577 "{id}"
8578 );
8579 }
8580 }
8581
8582 #[test]
8583 fn capabilities_are_explicit_and_versioned() {
8584 let mut service = HarnessSessionService::new();
8585 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8586 assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8587 assert_eq!(
8588 response["result"]["sdk"]["schema_version"],
8589 crate::SDK_SCHEMA_VERSION
8590 );
8591 assert_eq!(
8592 response["result"]["sdk"]["operations"]
8593 .as_array()
8594 .unwrap()
8595 .len(),
8596 SdkOperation::ALL.len()
8597 );
8598 assert_eq!(
8599 response["result"]["harnesses"].as_array().unwrap().len(),
8600 11
8601 );
8602 assert!(response["result"]["harnesses"]
8603 .as_array()
8604 .unwrap()
8605 .iter()
8606 .any(|harness| harness == HarnessId::GROK));
8607 assert!(response["result"]["harnesses"]
8608 .as_array()
8609 .unwrap()
8610 .iter()
8611 .any(|harness| harness == HarnessId::GOOSE));
8612 }
8613
8614 #[test]
8615 fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8616 let noisy_stderr = crate::HarnessEvent {
8617 sequence: None,
8618 kind: "transport_stderr".into(),
8619 payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8620 };
8621 assert_eq!(handshake_event_failure(&noisy_stderr), None);
8622
8623 let closed = crate::HarnessEvent {
8624 sequence: None,
8625 kind: "transport_closed".into(),
8626 payload: json!({}),
8627 };
8628 assert!(handshake_event_failure(&closed).is_some());
8629 }
8630
8631 #[tokio::test]
8632 async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8633 let mut service = HarnessSessionService::new();
8634 service
8635 .runtimes
8636 .insert("raw-eof".into(), ending_runtime(None));
8637 service.runtimes.insert(
8638 "explicit-close".into(),
8639 ending_runtime(Some(HarnessEvent {
8640 sequence: None,
8641 kind: "transport_closed".into(),
8642 payload: json!({"message": "native transport exited"}),
8643 })),
8644 );
8645
8646 let notifications = service.poll_runtimes().await;
8647
8648 assert_eq!(notifications.len(), 2);
8649 assert!(notifications
8650 .iter()
8651 .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8652 assert!(notifications.iter().all(|notification| {
8653 notification["params"]["session_id"] == "ending-session"
8654 && notification["params"]["connection"].is_string()
8655 }));
8656 let mut sequences = notifications
8657 .iter()
8658 .filter_map(|notification| notification["params"]["sequence"].as_u64())
8659 .collect::<Vec<_>>();
8660 sequences.sort_unstable();
8661 assert_eq!(sequences, vec![1, 2]);
8662 assert!(service.runtimes.is_empty());
8663 }
8664
8665 #[test]
8666 fn support_report_and_grok_default_binding_share_the_registry() {
8667 let mut service = HarnessSessionService::new();
8668 let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8669 assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8670 let params = RuntimeBackendParams {
8671 harness: HarnessId::from(HarnessId::GROK),
8672 protocol: None,
8673 launch: None,
8674 base_url: None,
8675 policy: RuntimePolicy::Default,
8676 };
8677 let backend = match runtime_backend(¶ms) {
8678 Ok(backend) => backend,
8679 Err(_) => panic!("Grok should bind through its registered ACP launch"),
8680 };
8681 assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8682 assert!(backend.capabilities().start_session);
8683 let registered = harness_support_registry()
8684 .harnesses
8685 .into_iter()
8686 .find(|harness| harness.id.as_str() == HarnessId::GROK)
8687 .and_then(|harness| harness.runtime.default_launch)
8688 .unwrap();
8689 assert!(!registered
8690 .arguments
8691 .iter()
8692 .any(|argument| argument == "--always-approve"));
8693 assert!(runtime_launch(¶ms).is_none());
8694
8695 let yolo = RuntimeBackendParams {
8696 policy: RuntimePolicy::Yolo,
8697 ..params
8698 };
8699 assert!(runtime_launch(&yolo)
8700 .unwrap()
8701 .arguments
8702 .iter()
8703 .any(|argument| argument == "--always-approve"));
8704
8705 let mismatched_protocol = RuntimeBackendParams {
8706 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8707 protocol: Some("acp".into()),
8708 launch: None,
8709 base_url: None,
8710 policy: RuntimePolicy::Default,
8711 };
8712 assert!(runtime_backend(&mismatched_protocol).is_err());
8713 }
8714
8715 #[test]
8716 fn load_follow_and_unfollow_share_the_same_locator() {
8717 let mut service = HarnessSessionService::new();
8718 let locator = pi_locator();
8719 let loaded = service.handle(request(
8720 1,
8721 "harness.v1.sessions.load",
8722 json!({"locator": locator}),
8723 ));
8724 assert_eq!(
8725 loaded["result"]["session"]["session_id"],
8726 locator.session_id
8727 );
8728
8729 let followed = service.handle(request(
8730 2,
8731 "harness.v1.sessions.follow",
8732 json!({"locator": locator}),
8733 ));
8734 assert_eq!(followed["result"]["subscription"], "sub-1");
8735 assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8736 assert!(service.poll().is_empty());
8737
8738 let unfollowed = service.handle(request(
8739 3,
8740 "harness.v1.sessions.unfollow",
8741 json!({"subscription": "sub-1"}),
8742 ));
8743 assert_eq!(unfollowed["result"]["removed"], true);
8744 }
8745
8746 #[test]
8747 fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8748 let temp = std::env::temp_dir().join(format!(
8749 "supercode-bounded-view-{}-{}",
8750 std::process::id(),
8751 generated_session_id()
8752 ));
8753 let path = temp.join("parent.jsonl");
8754 let subagents = temp.join("parent/subagents");
8755 std::fs::create_dir_all(&subagents).unwrap();
8756 let long_last = "x".repeat(300);
8757 let parent_records = [
8758 json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8759 json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8760 json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8761 ];
8762 std::fs::write(
8763 &path,
8764 format!(
8765 "{}\n",
8766 parent_records
8767 .iter()
8768 .map(Value::to_string)
8769 .collect::<Vec<_>>()
8770 .join("\n")
8771 ),
8772 )
8773 .unwrap();
8774 std::fs::write(
8775 subagents.join("agent-child.jsonl"),
8776 concat!(
8777 r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8778 "\n",
8779 ),
8780 )
8781 .unwrap();
8782 let locator = SessionLocator {
8783 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8784 session_id: "parent".into(),
8785 storage: StorageLocator::File { path },
8786 };
8787 let mut service = HarnessSessionService::new();
8788
8789 let complete = service.handle(request(
8790 1,
8791 "harness.v1.sessions.load",
8792 json!({"locator": locator}),
8793 ));
8794 assert_eq!(
8795 complete["result"]["session"]["subagents"]
8796 .as_array()
8797 .unwrap()
8798 .len(),
8799 1
8800 );
8801
8802 let bounded = service.handle(request(
8803 2,
8804 "harness.v1.sessions.load",
8805 json!({
8806 "locator": locator,
8807 "view": {
8808 "tail_messages": 1,
8809 "max_message_chars": 256,
8810 "include_subagents": false
8811 },
8812 }),
8813 ));
8814 let session = &bounded["result"]["session"];
8815 assert!(session["subagents"].as_array().unwrap().is_empty());
8816 assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8817 assert_eq!(
8818 session["messages"][0]["content"],
8819 format!("{}\n…", "x".repeat(256))
8820 );
8821
8822 let followed = service.handle(request(
8823 3,
8824 "harness.v1.sessions.follow",
8825 json!({
8826 "locator": locator,
8827 "view": {
8828 "tail_messages": 1,
8829 "max_message_chars": 256,
8830 "include_subagents": false
8831 },
8832 }),
8833 ));
8834 let initial = &followed["result"]["initial"]["session"];
8835 assert!(initial["subagents"].as_array().unwrap().is_empty());
8836 assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8837
8838 let _ = std::fs::remove_dir_all(&temp);
8839 }
8840
8841 #[test]
8842 fn forty_megabyte_display_load_is_bounded_and_prompt() {
8843 let temp = std::env::temp_dir().join(format!(
8844 "supercode-large-display-view-{}-{}",
8845 std::process::id(),
8846 generated_session_id()
8847 ));
8848 std::fs::create_dir_all(&temp).unwrap();
8849 let path = temp.join("rollout.jsonl");
8850 let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8851 writeln!(
8852 file,
8853 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8854 )
8855 .unwrap();
8856 let padding = "x".repeat(80 * 1024);
8857 for index in 0..512 {
8858 let marker = if index == 0 {
8859 "OLDEST-SHOULD-NOT-LOAD"
8860 } else if index == 511 {
8861 "LATEST-MUST-LOAD"
8862 } else {
8863 "bulk"
8864 };
8865 writeln!(
8866 file,
8867 "{}",
8868 json!({
8869 "timestamp": "2026-01-01T00:00:01Z",
8870 "type": "response_item",
8871 "payload": {
8872 "type": "message",
8873 "role": "assistant",
8874 "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8875 },
8876 })
8877 )
8878 .unwrap();
8879 }
8880 file.flush().unwrap();
8881 drop(file);
8882 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8883
8884 let locator = SessionLocator {
8885 harness: HarnessId::from(HarnessId::CODEX),
8886 session_id: "large-display".into(),
8887 storage: StorageLocator::File { path },
8888 };
8889 let started = Instant::now();
8890 let response = HarnessSessionService::new().handle(request(
8891 1,
8892 "harness.v1.sessions.load",
8893 json!({
8894 "locator": locator,
8895 "view": {
8896 "tail_messages": 500,
8897 "max_message_chars": 1024,
8898 "include_subagents": false,
8899 "display_history": true,
8900 },
8901 }),
8902 ));
8903 let elapsed = started.elapsed();
8904 let wire = response.to_string();
8905 eprintln!(
8906 "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8907 wire.len()
8908 );
8909 assert!(response.get("error").is_none(), "{response:#}");
8910 assert!(wire.contains("LATEST-MUST-LOAD"));
8911 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8912 assert!(
8913 wire.len() < 2 * 1024 * 1024,
8914 "bounded wire was {} bytes",
8915 wire.len()
8916 );
8917 assert!(
8918 elapsed.as_secs_f64() < 3.0,
8919 "bounded 40 MiB load took {elapsed:?}"
8920 );
8921
8922 let anchored = temp.join("anchored.jsonl");
8926 let mut file = std::io::BufWriter::new(std::fs::File::create(&anchored).unwrap());
8927 writeln!(
8928 file,
8929 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8930 )
8931 .unwrap();
8932 for index in 0..512 {
8933 let marker = if index == 0 {
8934 "OLDEST-SHOULD-NOT-LOAD"
8935 } else if index == 511 {
8936 "LATEST-MUST-LOAD"
8937 } else {
8938 "bulk"
8939 };
8940 let (role, kind) = if index % 8 == 0 {
8941 ("user", "input_text")
8942 } else {
8943 ("assistant", "output_text")
8944 };
8945 writeln!(
8946 file,
8947 "{}",
8948 json!({
8949 "timestamp": "2026-01-01T00:00:01Z",
8950 "type": "response_item",
8951 "payload": {
8952 "type": "message",
8953 "role": role,
8954 "content": [{"type": kind, "text": format!("{marker}:{padding}")}],
8955 },
8956 })
8957 )
8958 .unwrap();
8959 }
8960 file.flush().unwrap();
8961 drop(file);
8962 let short = HarnessSessionService::new().handle(request(
8963 2,
8964 "harness.v1.sessions.load",
8965 json!({
8966 "locator": SessionLocator {
8967 harness: HarnessId::from(HarnessId::CODEX),
8968 session_id: "large-display".into(),
8969 storage: StorageLocator::File { path: anchored },
8970 },
8971 "view": {
8972 "tail_messages": 20,
8973 "max_message_chars": 1024,
8974 "include_subagents": false,
8975 "display_history": true,
8976 },
8977 }),
8978 ));
8979 let records = short["result"]["session"]["raw_record_count"].as_u64();
8980 assert!(
8981 records.is_some_and(|records| records < 128),
8982 "{records:?} records read"
8983 );
8984 let short = short.to_string();
8985 assert!(short.contains("LATEST-MUST-LOAD"));
8986 assert!(short.contains("older native records remain outside this bounded display window"));
8987
8988 let _ = std::fs::remove_dir_all(&temp);
8989 }
8990
8991 #[test]
8992 fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8993 let temp = std::env::temp_dir().join(format!(
8994 "supercode-large-goose-view-{}-{}",
8995 std::process::id(),
8996 generated_session_id()
8997 ));
8998 std::fs::create_dir_all(&temp).unwrap();
8999 let path = temp.join("sessions.db");
9000 let connection = rusqlite::Connection::open(&path).unwrap();
9001 connection
9002 .execute_batch(
9003 "CREATE TABLE sessions (
9004 id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
9005 created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
9006 session_type TEXT NOT NULL, extension_data TEXT,
9007 goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
9008 archived_at TEXT
9009 );
9010 CREATE TABLE messages (
9011 id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
9012 role TEXT NOT NULL, content_json TEXT NOT NULL,
9013 created_timestamp INTEGER NOT NULL, metadata_json TEXT
9014 );",
9015 )
9016 .unwrap();
9017 connection
9018 .execute(
9019 "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
9020 rusqlite::params![
9021 "goose-large",
9022 "Large Goose session",
9023 "/tmp",
9024 "2026-01-01 00:00:00",
9025 "2026-01-01 00:00:02",
9026 "user",
9027 "{}",
9028 "auto",
9029 "anthropic",
9030 r#"{"model_name":"claude-sonnet"}"#,
9031 ],
9032 )
9033 .unwrap();
9034 let old_content = serde_json::to_string(&vec![json!({
9035 "type": "text",
9036 "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
9037 })])
9038 .unwrap();
9039 connection
9040 .execute(
9041 "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
9042 rusqlite::params!["goose-large", old_content],
9043 )
9044 .unwrap();
9045 connection
9046 .execute(
9047 "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
9048 rusqlite::params![
9049 "goose-large",
9050 r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
9051 ],
9052 )
9053 .unwrap();
9054 drop(connection);
9055 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
9056
9057 let locator = SessionLocator {
9058 harness: HarnessId::from(HarnessId::GOOSE),
9059 session_id: "goose-large".into(),
9060 storage: StorageLocator::Sqlite {
9061 path,
9062 selector: "goose-large".into(),
9063 },
9064 };
9065 let started = Instant::now();
9066 let response = HarnessSessionService::new().handle(request(
9067 1,
9068 "harness.v1.sessions.load",
9069 json!({
9070 "locator": locator,
9071 "view": {
9072 "tail_messages": 1,
9073 "max_message_chars": 1024,
9074 "include_subagents": false,
9075 "display_history": true,
9076 },
9077 }),
9078 ));
9079 let elapsed = started.elapsed();
9080 let wire = response.to_string();
9081 eprintln!(
9082 "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
9083 wire.len()
9084 );
9085 assert!(response.get("error").is_none(), "{response:#}");
9086 assert!(wire.contains("LATEST-MUST-LOAD"));
9087 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
9088 assert!(
9089 wire.len() < 64 * 1024,
9090 "bounded wire was {} bytes",
9091 wire.len()
9092 );
9093 assert!(
9094 elapsed.as_secs_f64() < 1.0,
9095 "bounded Goose load took {elapsed:?}"
9096 );
9097
9098 let _ = std::fs::remove_dir_all(&temp);
9099 }
9100
9101 #[test]
9102 fn display_view_keeps_codex_assistant_history_across_compaction() {
9103 let temp = std::env::temp_dir().join(format!(
9104 "supercode-codex-display-view-{}-{}",
9105 std::process::id(),
9106 generated_session_id()
9107 ));
9108 std::fs::create_dir_all(&temp).unwrap();
9109 let path = temp.join("rollout.jsonl");
9110 std::fs::write(
9111 &path,
9112 concat!(
9113 r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
9114 "\n",
9115 r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
9116 "\n",
9117 r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
9118 "\n",
9119 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"}]}}"#,
9120 "\n",
9121 r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
9122 "\n",
9123 r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
9124 "\n",
9125 ),
9126 )
9127 .unwrap();
9128 let locator = SessionLocator {
9129 harness: HarnessId::from(HarnessId::CODEX),
9130 session_id: "codex-display".into(),
9131 storage: StorageLocator::File { path },
9132 };
9133 let mut service = HarnessSessionService::new();
9134
9135 let continuation = service.handle(request(
9136 1,
9137 "harness.v1.sessions.load",
9138 json!({"locator": locator}),
9139 ));
9140 let continuation_text = continuation["result"]["session"]["messages"].to_string();
9141 assert!(!continuation_text.contains("old answer"));
9142
9143 let display = service.handle(request(
9144 2,
9145 "harness.v1.sessions.load",
9146 json!({
9147 "locator": locator,
9148 "view": {
9149 "tail_messages": 10,
9150 "include_subagents": false,
9151 "display_history": true,
9152 },
9153 }),
9154 ));
9155 let display_text = display["result"]["session"]["messages"].to_string();
9156 assert!(display_text.contains("old prompt"));
9157 assert!(display_text.contains("old answer"));
9158 assert!(display_text.contains("new prompt"));
9159 assert!(display_text.contains("new answer"));
9160
9161 let _ = std::fs::remove_dir_all(&temp);
9162 }
9163
9164 #[test]
9165 fn indexed_claude_windows_match_the_existing_wire_projection() {
9166 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9167 .join("tests/fixtures/claude_code_session.jsonl");
9168 let locator = SessionLocator {
9169 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9170 session_id: "fixture".into(),
9171 storage: StorageLocator::File { path },
9172 };
9173 let full = load_session(&locator).unwrap();
9174 for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
9175 for offset in [0, 1, full.messages.len(), usize::MAX] {
9176 for limit in [0, 1, 3, usize::MAX] {
9177 let options = SessionLoadOptions {
9178 include_subagents: Some(false),
9179 inline_media,
9180 message_offset: Some(offset),
9181 message_limit: Some(limit),
9182 ..Default::default()
9183 };
9184 let expected = projected_session_result(&full, &options);
9185 assert_eq!(
9186 indexed_claude_window(&locator, &options).unwrap().unwrap(),
9187 expected
9188 );
9189 }
9190 }
9191 for tail in [0, 1, 3, usize::MAX] {
9192 let options = SessionLoadOptions {
9193 include_subagents: Some(false),
9194 inline_media,
9195 message_tail: Some(tail),
9196 ..Default::default()
9197 };
9198 assert_eq!(
9199 indexed_claude_window(&locator, &options).unwrap().unwrap(),
9200 projected_session_result(&full, &options)
9201 );
9202 }
9203 }
9204 }
9205
9206 #[test]
9207 fn load_supports_bounded_windows_and_media_metadata() {
9208 let mut service = HarnessSessionService::new();
9209 let locator = pi_locator();
9210 let bounded = service.handle(request(
9211 1,
9212 "harness.v1.sessions.load",
9213 json!({
9214 "locator": locator,
9215 "options": {
9216 "include_subagents": false,
9217 "message_limit": 2,
9218 "message_offset": 1
9219 }
9220 }),
9221 ));
9222 assert_eq!(bounded["result"]["window"]["offset"], 1);
9223 assert_eq!(bounded["result"]["window"]["returned"], 2);
9224 assert!(bounded["result"]["summary"]["first_message"].is_object());
9225 assert!(bounded["result"]["summary"]["last_message"].is_object());
9226 assert_eq!(
9227 bounded["result"]["session"]["messages"]
9228 .as_array()
9229 .unwrap()
9230 .len(),
9231 2
9232 );
9233 assert!(bounded["result"]["session"]["subagents"]
9234 .as_array()
9235 .unwrap()
9236 .is_empty());
9237
9238 let tail = service.handle(request(
9239 2,
9240 "harness.v1.sessions.load",
9241 json!({"locator": locator, "options": {"message_tail": 1}}),
9242 ));
9243 assert_eq!(tail["result"]["window"]["returned"], 1);
9244 assert_eq!(tail["result"]["window"]["has_more"], true);
9245 assert_eq!(tail["result"]["window"]["has_older"], true);
9246 assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
9247 assert!(tail["result"]["summary"]["first_message"].is_object());
9248
9249 let metadata_only = service.handle(request(
9250 3,
9251 "harness.v1.sessions.load",
9252 json!({"locator": locator, "options": {"inline_media": "metadata"}}),
9253 ));
9254 assert!(metadata_only["result"]["session"]
9255 .to_string()
9256 .contains("media_reference"));
9257 assert!(!metadata_only["result"]["session"]
9258 .to_string()
9259 .contains("data:image/"));
9260 }
9261
9262 #[test]
9263 fn import_translate_branch_and_handoff_use_typed_artifacts() {
9264 let mut service = HarnessSessionService::new();
9265 let locator = pi_locator();
9266 let translated = service.handle(request(
9267 1,
9268 "harness.v1.sessions.translate",
9269 json!({"locator": locator, "target_harness": "grok"}),
9270 ));
9271 assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
9272 assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
9273 assert!(translated["result"]["artifact"]["content"]
9274 .as_str()
9275 .is_some_and(|content| !content.is_empty()));
9276
9277 for target in ["opencode", "open-code"] {
9278 let opencode = service.handle(request(
9279 6,
9280 "harness.v1.sessions.translate",
9281 json!({"locator": locator, "target_harness": target}),
9282 ));
9283 assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
9284 }
9285 let goose = service.handle(request(
9286 7,
9287 "harness.v1.sessions.translate",
9288 json!({"locator": locator, "target_harness": "goose"}),
9289 ));
9290 assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
9291 assert!(serde_json::from_str::<Value>(
9292 goose["result"]["artifact"]["content"].as_str().unwrap()
9293 )
9294 .unwrap()["conversation"]
9295 .is_array());
9296
9297 let imported = service.handle(request(
9298 2,
9299 "harness.v1.sessions.import",
9300 json!({
9301 "source_harness": "grok",
9302 "content": translated["result"]["artifact"]["content"],
9303 }),
9304 ));
9305 assert_eq!(imported["result"]["session"]["source"], "grok");
9306
9307 let branched = service.handle(request(
9308 3,
9309 "harness.v1.sessions.branch",
9310 json!({"locator": locator, "target_harness": "codex"}),
9311 ));
9312 assert_eq!(branched["result"]["parent"]["harness"], "pi");
9313 assert!(branched["result"]["bootstrap_prompt"]
9314 .as_str()
9315 .unwrap()
9316 .contains("frozen parent transcript"));
9317 assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
9318
9319 let handoff = service.handle(request(
9320 4,
9321 "harness.v1.sessions.handoff",
9322 json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
9323 ));
9324 assert_eq!(handoff["result"]["launch"]["program"], "pi");
9325 assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
9326 assert_eq!(handoff["result"]["requires_materialization"], true);
9327
9328 let goose_handoff = service.handle(request(
9329 8,
9330 "harness.v1.sessions.handoff",
9331 json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
9332 ));
9333 assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
9334 assert_eq!(
9335 goose_handoff["result"]["materialize"]["arguments"],
9336 json!(["session", "import", "{artifact_path}"])
9337 );
9338
9339 let resumed = service.handle(request(
9340 5,
9341 "harness.v1.sessions.resume_instructions",
9342 json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
9343 ));
9344 assert_eq!(resumed["result"]["launch"]["program"], "pi");
9345 assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
9346 }
9347
9348 #[test]
9349 fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
9350 let temp = std::env::temp_dir().join(format!(
9351 "supercode-service-reduce-{}-{}",
9352 std::process::id(),
9353 generated_session_id()
9354 ));
9355 let source_path = temp.join("source.jsonl");
9356 let store_root = temp.join("store");
9357 std::fs::create_dir_all(&temp).unwrap();
9358
9359 let mut records = vec![json!({
9360 "timestamp": "2026-01-01T00:00:00Z",
9361 "type": "session_meta",
9362 "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9363 })];
9364 for turn in 0..16 {
9365 records.push(json!({
9366 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9367 "type": "response_item",
9368 "payload": {
9369 "type": "message",
9370 "role": "user",
9371 "content": [{
9372 "type": "input_text",
9373 "text": format!("request {turn}: {}", "context ".repeat(80)),
9374 }],
9375 },
9376 }));
9377 records.push(json!({
9378 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9379 "type": "response_item",
9380 "payload": {
9381 "type": "message",
9382 "role": "assistant",
9383 "content": [{
9384 "type": "output_text",
9385 "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9386 }],
9387 },
9388 }));
9389 }
9390 let source = format!(
9391 "{}\n",
9392 records
9393 .iter()
9394 .map(Value::to_string)
9395 .collect::<Vec<_>>()
9396 .join("\n")
9397 );
9398 std::fs::write(&source_path, &source).unwrap();
9399 let locator = SessionLocator {
9400 harness: HarnessId::from(HarnessId::CODEX),
9401 session_id: "codex-reduce".into(),
9402 storage: StorageLocator::File {
9403 path: source_path.clone(),
9404 },
9405 };
9406 let original = load_session(&locator).unwrap();
9407 let mut service =
9408 HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9409
9410 let response = service.handle(request(
9411 1,
9412 "harness.v1.sessions.reduce",
9413 json!({
9414 "locator": locator,
9415 "target_harness": "claude-code",
9416 "keep_last": 4,
9417 }),
9418 ));
9419 assert!(response.get("error").is_none(), "{response:#}");
9420 let receipt = &response["result"]["receipt"];
9421 assert_eq!(receipt["source_harness"], "codex");
9422 assert_eq!(receipt["target_harness"], "claude-code");
9423 assert_eq!(receipt["verified"], true);
9424 assert_eq!(receipt["reversible"], true);
9425 assert!(receipt["reductions"].as_u64().unwrap() > 0);
9426 assert!(
9427 receipt["source_tokens"].as_u64().unwrap()
9428 > receipt["reduced_tokens"].as_u64().unwrap()
9429 );
9430 assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9431 assert!(response["result"]["bootstrap_prompt"]
9432 .as_str()
9433 .unwrap()
9434 .contains("Do not guess hidden content"));
9435
9436 let rescue_id = receipt["id"].as_str().unwrap();
9437 let store = crate::SessionStore::open(&store_root).unwrap();
9438 let sidecar =
9439 Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9440 let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9441 let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9442 let policy = reduce::ReductionPolicy {
9443 clear_turns_older_than: Some(4),
9444 ..Default::default()
9445 };
9446 let (restamped_view, reapplied_log) =
9447 reduce::project_messages(&sidecar.messages, &policy, &log);
9448 assert_eq!(
9449 messages_jsonl(&persisted_view).unwrap(),
9450 messages_jsonl(&restamped_view).unwrap()
9451 );
9452 assert_eq!(reapplied_log, log);
9453 reduce::verify_log(&log, &sidecar).unwrap();
9454 assert_eq!(
9455 reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9456 original.messages
9457 );
9458 assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9459
9460 std::fs::remove_dir_all(temp).ok();
9461 }
9462
9463 #[test]
9464 fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9465 let temp = std::env::temp_dir().join(format!(
9466 "supercode-severed-view-{}-{}",
9467 std::process::id(),
9468 generated_session_id()
9469 ));
9470 std::fs::create_dir_all(&temp).unwrap();
9471 let path = temp.join("severed.jsonl");
9472 std::fs::write(
9475 &path,
9476 concat!(
9477 r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9478 "\n",
9479 r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9480 "\n",
9481 ),
9482 )
9483 .unwrap();
9484 let locator = SessionLocator {
9485 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9486 session_id: "severed".into(),
9487 storage: StorageLocator::File { path },
9488 };
9489 let mut service = HarnessSessionService::new();
9490
9491 let viewed = service.handle(request(
9492 1,
9493 "harness.v1.sessions.load",
9494 json!({"locator": locator}),
9495 ));
9496 let session = &viewed["result"]["session"];
9497 assert_eq!(session["fidelity"], "semantic");
9498 assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9499 assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9500 entry
9501 .as_str()
9502 .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9503 }));
9504
9505 let strict = service.handle(request(
9508 2,
9509 "harness.v1.sessions.load",
9510 json!({"locator": locator, "fidelity": "byte_lossless"}),
9511 ));
9512 assert!(strict["error"]["message"]
9513 .as_str()
9514 .unwrap()
9515 .contains("cannot reconstruct lossless Claude continuation"));
9516
9517 let translated = service.handle(request(
9519 3,
9520 "harness.v1.sessions.translate",
9521 json!({"locator": locator, "target_harness": "codex"}),
9522 ));
9523 assert!(translated["error"]["message"]
9524 .as_str()
9525 .unwrap()
9526 .contains("cannot reconstruct lossless Claude continuation"));
9527 let resumed = service.handle(request(
9528 4,
9529 "harness.v1.sessions.resume_instructions",
9530 json!({"locator": locator}),
9531 ));
9532 assert!(resumed["error"]["message"]
9533 .as_str()
9534 .unwrap()
9535 .contains("cannot reconstruct lossless Claude continuation"));
9536
9537 let _ = std::fs::remove_dir_all(&temp);
9538 }
9539
9540 #[test]
9541 fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9542 let codex = resume_launch(
9543 HarnessId::CODEX,
9544 "codex-session",
9545 Path::new("/tmp/project"),
9546 ResumePolicy::Yolo,
9547 )
9548 .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9549 assert_eq!(codex.program, "codex");
9550 assert_eq!(
9551 codex.arguments,
9552 [
9553 "-c",
9554 "check_for_update_on_startup=false",
9555 "-c",
9556 "projects.\"/tmp/project\".trust_level=\"trusted\"",
9557 "--dangerously-bypass-approvals-and-sandbox",
9558 "--dangerously-bypass-hook-trust",
9559 "resume",
9560 "codex-session",
9561 ]
9562 );
9563
9564 let gemini = resume_launch(
9565 HarnessId::GEMINI,
9566 "gemini-session",
9567 Path::new("/tmp/project"),
9568 ResumePolicy::Yolo,
9569 )
9570 .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9571 assert_eq!(gemini.program, "gemini");
9572 assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9573
9574 let goose = resume_launch(
9575 HarnessId::GOOSE,
9576 "goose-session",
9577 Path::new("/tmp/project"),
9578 ResumePolicy::Yolo,
9579 )
9580 .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9581 assert_eq!(goose.program, "goose");
9582 assert_eq!(
9583 goose.arguments,
9584 ["session", "--resume", "--session-id", "goose-session"]
9585 );
9586
9587 let supercode = resume_launch(
9588 HarnessId::SUPERCODE,
9589 "supercode-session",
9590 Path::new("/tmp/project"),
9591 ResumePolicy::Yolo,
9592 )
9593 .unwrap_or_else(|_| panic!("Volter Harness resume launch must be registered"));
9594 assert_eq!(supercode.program, "supercode");
9595 assert_eq!(
9596 supercode.arguments,
9597 ["--dangerous", "resume", "supercode-session"]
9598 );
9599 }
9600
9601 #[test]
9602 fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9603 let temp = std::env::temp_dir().join(format!(
9604 "supercode-harness-artifact-{}-{}",
9605 std::process::id(),
9606 generated_session_id()
9607 ));
9608 let main_path = temp.join("parent.jsonl");
9609 let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9610 std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9611 let fixture = std::fs::read_to_string(
9612 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9613 .join("tests/fixtures/claude_code_session.jsonl"),
9614 )
9615 .unwrap();
9616 let parent = fixture.trim_end_matches('\n');
9617 let child = fixture.trim_end_matches('\n');
9618 std::fs::write(&main_path, parent).unwrap();
9619 std::fs::write(&subagent_path, child).unwrap();
9620 let locator = SessionLocator {
9621 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9622 session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9623 storage: StorageLocator::File {
9624 path: main_path.clone(),
9625 },
9626 };
9627 let mut service = HarnessSessionService::new();
9628 let claude = service.handle(request(
9629 1,
9630 "harness.v1.sessions.translate",
9631 json!({"locator": locator, "target_harness": "claude-code"}),
9632 ));
9633 let artifact = &claude["result"]["artifact"];
9634 assert_eq!(artifact["fidelity"], "byte_lossless");
9635 assert_eq!(artifact["content"], parent);
9636 let files = artifact["files"].as_array().unwrap();
9637 assert!(files.iter().any(|file| {
9638 file["role"] == "subagent"
9639 && file["path"]
9640 .as_str()
9641 .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9642 && file["content"] == child
9643 }));
9644 assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9645
9646 let grok = service.handle(request(
9647 2,
9648 "harness.v1.sessions.translate",
9649 json!({"locator": grok_locator(), "target_harness": "grok"}),
9650 ));
9651 let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9652 for name in ["summary.json", "updates.jsonl"] {
9653 let expected = std::fs::read_to_string(
9654 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9655 .join("tests/fixtures/grok_session")
9656 .join(name),
9657 )
9658 .unwrap();
9659 assert!(files.iter().any(|file| {
9660 file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9661 }));
9662 }
9663 std::fs::remove_dir_all(temp).ok();
9664 }
9665
9666 #[test]
9667 fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9668 let mut service = HarnessSessionService::new();
9669 let source = pi_locator();
9670 for (target, format) in [
9671 ("claude-code", SessionFormat::ClaudeCode),
9672 ("codex", SessionFormat::Codex),
9673 ("opencode", SessionFormat::OpenCode),
9674 ("pi", SessionFormat::Pi),
9675 ] {
9676 let result = service.handle(request(
9677 1,
9678 "harness.v1.sessions.handoff",
9679 json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9680 ));
9681 let artifact = &result["result"]["artifact"];
9682 let target_id = artifact["session_id"].as_str().unwrap();
9683 assert_ne!(target_id, source.session_id, "{target}");
9684 let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9685 assert_eq!(
9686 parsed.meta.session_id.as_deref(),
9687 Some(target_id),
9688 "{target}"
9689 );
9690 if target != "pi" {
9691 assert!(result["result"]["launch"]["arguments"]
9692 .as_array()
9693 .unwrap()
9694 .iter()
9695 .any(|argument| argument == target_id));
9696 }
9697 if target == "opencode" {
9698 assert!(target_id.starts_with("ses_"));
9699 fn assert_session_ids(value: &Value, target_id: &str) {
9700 match value {
9701 Value::Object(fields) => {
9702 if let Some(session_id) = fields.get("sessionID") {
9703 assert_eq!(session_id, target_id);
9704 }
9705 for child in fields.values() {
9706 assert_session_ids(child, target_id);
9707 }
9708 }
9709 Value::Array(values) => {
9710 for child in values {
9711 assert_session_ids(child, target_id);
9712 }
9713 }
9714 _ => {}
9715 }
9716 }
9717 let document: Value =
9718 serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9719 assert_session_ids(&document, target_id);
9720 }
9721 }
9722
9723 let first = service.handle(request(
9724 2,
9725 "harness.v1.sessions.handoff",
9726 json!({"locator": source, "target_harness": "codex"}),
9727 ));
9728 let second = service.handle(request(
9729 3,
9730 "harness.v1.sessions.handoff",
9731 json!({"locator": source, "target_harness": "codex"}),
9732 ));
9733 assert_ne!(
9734 first["result"]["artifact"]["session_id"],
9735 second["result"]["artifact"]["session_id"]
9736 );
9737 }
9738
9739 #[test]
9740 fn grok_handoff_materializes_through_the_core_door() {
9741 let mut service = HarnessSessionService::new();
9742 let source = opencode_locator();
9743 let response = service.handle(request(
9744 1,
9745 "harness.v1.sessions.handoff",
9746 json!({
9747 "locator": source,
9748 "target_harness": "grok",
9749 "cwd": "/tmp/grok-handoff-project",
9750 }),
9751 ));
9752 let result = &response["result"];
9753
9754 assert_eq!(result["artifact"]["target_harness"], "grok");
9757 let artifact = Session::load_str(
9758 result["artifact"]["content"].as_str().unwrap(),
9759 SessionFormat::Grok,
9760 )
9761 .unwrap();
9762 assert!(!artifact.messages.is_empty());
9763 let target_session_id = result["artifact"]["session_id"].as_str().unwrap();
9764 assert_eq!(target_session_id.len(), 36);
9765 assert_ne!(target_session_id, opencode_locator().session_id);
9766 assert!(result["materialize"].is_null());
9767 assert_eq!(
9768 result["launch"]["arguments"],
9769 json!(["--resume", "{materialized_session_id}"])
9770 );
9771 assert!(result["note"]
9772 .as_str()
9773 .unwrap()
9774 .contains("harness.v1.sessions.materialize"));
9775 }
9776
9777 #[tokio::test]
9778 async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9779 let mut service = HarnessSessionService::new();
9780 let inventory = service
9781 .handle_async(request(
9782 1,
9783 "harness.v1.harnesses.list",
9784 json!({"harnesses": ["missing"]}),
9785 ))
9786 .await;
9787 assert_eq!(inventory["error"]["code"], -32602);
9788
9789 let attached = service
9790 .handle_async(request(
9791 2,
9792 "harness.v1.runtimes.attach_existing",
9793 json!({"harness": "codex", "runtime_id": "thread-1"}),
9794 ))
9795 .await;
9796 assert_eq!(attached["error"]["code"], -32000);
9797 assert!(attached["error"]["message"]
9798 .as_str()
9799 .unwrap()
9800 .contains("runtimes.resume"));
9801 }
9802
9803 #[test]
9804 fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9805 let mut service = HarnessSessionService::new();
9806 let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9807 assert_eq!(invalid["error"]["code"], -32602);
9808 let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9809 assert_eq!(unknown["error"]["code"], -32601);
9810 }
9811
9812 #[cfg(unix)]
9813 #[tokio::test]
9814 #[allow(clippy::await_holding_lock)]
9817 async fn async_service_drives_a_generic_acp_runtime() {
9818 let _environment_guard = crate::live_runtime::test_environment_lock();
9819 let script = r#"
9820 i=0
9821 while IFS= read -r line; do
9822 i=$((i + 1))
9823 case "$i" in
9824 1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9825 2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9826 3)
9827 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9828 printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9829 ;;
9830 4)
9831 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9832 printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9833 ;;
9834 esac
9835 done
9836 "#;
9837 let mut service = HarnessSessionService::new();
9838 let started = service
9839 .handle_async(request(
9840 1,
9841 "harness.v1.runtimes.start",
9842 json!({
9843 "harness": "codex",
9844 "protocol": "acp",
9845 "cwd": std::env::current_dir().unwrap(),
9846 "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9847 }),
9848 ))
9849 .await;
9850 assert_eq!(started["result"]["connection"], "runtime-1");
9851 assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9852
9853 let terminal = service
9854 .handle_async(request(
9855 9,
9856 "harness.v1.runtimes.terminal_instructions",
9857 json!({"connection":"runtime-1"}),
9858 ))
9859 .await;
9860 let arguments = terminal["result"]["launch"]["arguments"]
9861 .as_array()
9862 .expect("hosted runtime should return terminal arguments");
9863 let endpoint_index = arguments
9864 .iter()
9865 .position(|value| value == "--endpoint")
9866 .expect("terminal command should use an opaque endpoint");
9867 let endpoint = LiveRuntimeEndpoint::parse(
9868 arguments[endpoint_index + 1]
9869 .as_str()
9870 .expect("endpoint argument should be text"),
9871 )
9872 .unwrap();
9873 assert!(!terminal.to_string().contains("Bearer"));
9874 let workspace = std::env::current_dir().unwrap();
9875 let receipt = resolve_live_runtime(
9876 &endpoint,
9877 &LiveRuntimeSource {
9878 harness: "codex".into(),
9879 session_id: "svc_acp".into(),
9880 workspace,
9881 },
9882 )
9883 .unwrap();
9884 let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9885 .await
9886 .unwrap();
9887 let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9888 .await
9889 .unwrap();
9890
9891 let sent = service
9892 .handle_async(request(
9893 2,
9894 "harness.v1.runtimes.send_input",
9895 json!({"connection": "runtime-1", "text": "hi"}),
9896 ))
9897 .await;
9898 assert_eq!(sent["result"]["turn_id"], "3");
9899
9900 let mut events = Vec::new();
9901 for _ in 0..20 {
9902 events.extend(service.poll_runtimes().await);
9903 if events.len() >= 2 {
9904 break;
9905 }
9906 tokio::time::sleep(Duration::from_millis(2)).await;
9907 }
9908 assert!(events
9909 .iter()
9910 .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9911 assert!(events.iter().any(|event| {
9912 event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9913 }));
9914
9915 let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9916 loop {
9917 let event = attachment.next_event().await.unwrap();
9918 if event.kind == "text_delta" && event.payload["text"] == "ok" {
9919 break;
9920 }
9921 }
9922 })
9923 .await;
9924 assert!(
9925 saw_editor_reply.is_ok(),
9926 "terminal should observe the editor-driven turn"
9927 );
9928
9929 crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9930 .await
9931 .unwrap();
9932 let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9933 loop {
9934 let event = attachment.next_event().await.unwrap();
9935 if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9936 break;
9937 }
9938 }
9939 })
9940 .await;
9941 assert!(
9942 saw_terminal_reply.is_ok(),
9943 "terminal should drive the same runtime"
9944 );
9945
9946 let closed = service
9947 .handle_async(request(
9948 3,
9949 "harness.v1.runtimes.close",
9950 json!({"connection": "runtime-1"}),
9951 ))
9952 .await;
9953 assert_eq!(closed["result"]["closed"], true);
9954 }
9955
9956 #[test]
9961 fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9962 let home = connect_scratch_home("uni7-running");
9963
9964 assert!(probe_hermes_running(&home, 300_000).is_none());
9971
9972 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9975 let port = listener.local_addr().unwrap().port();
9976 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9977 std::fs::write(
9978 home.join(".openclaw/openclaw.json"),
9979 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9980 )
9981 .unwrap();
9982 let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9983 assert!(matches!(
9984 running.method,
9985 RunningInstanceMethod::GatewayConnect
9986 ));
9987 assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9988 drop(listener);
9989 let mut closed_detected = probe_openclaw_running(&home).is_some();
9994 for _ in 0..3 {
9995 if !closed_detected {
9996 break;
9997 }
9998 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9999 let port = listener.local_addr().unwrap().port();
10000 drop(listener);
10001 std::fs::write(
10002 home.join(".openclaw/openclaw.json"),
10003 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
10004 )
10005 .unwrap();
10006 closed_detected = probe_openclaw_running(&home).is_some();
10007 }
10008 assert!(
10009 !closed_detected,
10010 "a closed gateway must not read as running"
10011 );
10012
10013 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
10015 let port = listener.local_addr().unwrap().port();
10016 std::fs::write(
10017 home.join(".openclaw/openclaw.json"),
10018 format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
10019 )
10020 .unwrap();
10021 assert!(probe_openclaw_running(&home).is_some());
10022 drop(listener);
10023
10024 std::fs::create_dir_all(home.join(".hermes")).unwrap();
10026 let wal = home.join(".hermes/state.db-wal");
10027 std::fs::write(&wal, b"wal").unwrap();
10028 let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
10029 assert!(matches!(
10030 running.method,
10031 RunningInstanceMethod::StoreWalActivity
10032 ));
10033 assert!(running.evidence.contains("state.db-wal"));
10034 let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
10035 std::fs::File::options()
10036 .append(true)
10037 .open(&wal)
10038 .unwrap()
10039 .set_modified(stale)
10040 .unwrap();
10041 assert!(
10042 probe_hermes_running(&home, 300_000).is_none(),
10043 "a stale WAL (crash leftover) must not read as running"
10044 );
10045 }
10046
10047 fn connect_scratch_home(tag: &str) -> PathBuf {
10048 let dir = std::env::temp_dir().join(format!(
10049 "supercode-connect-service-{tag}-{}-{}",
10050 std::process::id(),
10051 std::time::SystemTime::now()
10052 .duration_since(std::time::UNIX_EPOCH)
10053 .unwrap()
10054 .as_nanos()
10055 ));
10056 std::fs::create_dir_all(&dir).unwrap();
10057 dir
10058 }
10059
10060 async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
10064 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
10065 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
10066 let address = listener.local_addr().unwrap();
10067 let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
10068 tokio::spawn(async move {
10069 loop {
10070 let Ok((mut stream, _)) = listener.accept().await else {
10071 break;
10072 };
10073 let request_sender = request_sender.clone();
10074 tokio::spawn(async move {
10075 let (reader, mut writer) = stream.split();
10076 let mut reader = BufReader::new(reader);
10077 let mut request_line = String::new();
10078 if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
10079 return;
10080 }
10081 let request_line = request_line.trim_end().to_string();
10082 let mut authorization = String::new();
10083 let mut content_length = 0usize;
10084 loop {
10085 let mut line = String::new();
10086 if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
10087 return;
10088 }
10089 let line = line.trim_end();
10090 if line.is_empty() {
10091 break;
10092 }
10093 let lower = line.to_ascii_lowercase();
10094 if let Some(value) = lower.strip_prefix("authorization:") {
10095 authorization = value.trim().to_string();
10096 }
10097 if let Some(value) = lower.strip_prefix("content-length:") {
10098 content_length = value.trim().parse().unwrap_or(0);
10099 }
10100 }
10101 if content_length > 0 {
10102 let mut body = vec![0u8; content_length];
10103 let _ = reader.read_exact(&mut body).await;
10104 }
10105 let _ = request_sender.send(format!("{request_line} :: {authorization}"));
10106 if request_line.starts_with("GET /event") {
10107 let _ = writer
10108 .write_all(
10109 b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
10110 )
10111 .await;
10112 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
10113 return;
10114 }
10115 let body = if request_line.starts_with("POST /session") {
10116 r#"{"id":"mock-session"}"#
10117 } else {
10118 r#"{"status":"ok"}"#
10119 };
10120 let response = format!(
10121 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
10122 body.len(),
10123 body
10124 );
10125 let _ = writer.write_all(response.as_bytes()).await;
10126 });
10127 }
10128 });
10129 (format!("http://{address}"), request_receiver)
10130 }
10131
10132 fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
10133 crate::HarnessSupportDescriptor {
10134 orchestration: Default::default(),
10135 id: HarnessId::from(HarnessId::OPENCODE),
10136 display_name: "OpenCode".into(),
10137 native: crate::NativeSupport {
10138 discover: crate::ImplementationKind::Absent,
10139 load: crate::ImplementationKind::Absent,
10140 follow: crate::ImplementationKind::Absent,
10141 import: crate::ImplementationKind::Absent,
10142 export: crate::ImplementationKind::Absent,
10143 },
10144 runtime: crate::RuntimeSupport {
10145 implementation: crate::ImplementationKind::BuiltIn,
10146 protocol: protocol.into(),
10147 default_launch: None,
10148 connect_launch: Some(crate::RuntimeConnectLaunch {
10149 config_path: "~/opencode-tui.json".into(),
10150 address_pointer: "/server/url".into(),
10151 port_pointer: None,
10152 default_address: None,
10153 auth_pointer: Some("/server/token".into()),
10154 protocol: protocol.into(),
10155 }),
10156 capabilities: crate::RuntimeCapabilities {
10157 start_session: true,
10158 resume_session: true,
10159 attach_existing_process: true,
10160 send_input: true,
10161 stream_events: true,
10162 interrupt: true,
10163 steer: false,
10164 respond_to_requests: true,
10165 },
10166 },
10167 }
10168 }
10169
10170 #[tokio::test]
10171 async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
10172 let (base_url, mut requests) = mock_opencode_endpoint().await;
10173 let home = connect_scratch_home("open");
10174 std::fs::write(
10175 home.join("opencode-tui.json"),
10176 format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
10177 )
10178 .unwrap();
10179
10180 let descriptor = connect_descriptor("opencode-http-sse");
10181 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10182 assert!(backend.capabilities().attach_existing_process);
10183
10184 let connection = backend
10185 .start(crate::RuntimeStartRequest {
10186 cwd: home.clone(),
10187 launch: None,
10188 mcp_servers: Vec::new(),
10189 approval_policy: None,
10190 })
10191 .await
10192 .unwrap();
10193 let handle = connection.handle();
10194 assert_eq!(handle.runtime_id, "mock-session");
10195 match &handle.endpoint {
10196 crate::RuntimeEndpoint::Http {
10197 base_url: endpoint, ..
10198 } => assert_eq!(endpoint, &base_url),
10199 other => panic!("connect mode must join the running endpoint, got {other:?}"),
10200 }
10201
10202 let mut seen = Vec::new();
10203 while let Ok(line) = requests.try_recv() {
10204 seen.push(line);
10205 }
10206 assert!(seen
10207 .iter()
10208 .any(|line| line.starts_with("GET /global/health")
10209 && line.contains("bearer connect-secret")));
10210 assert!(seen.iter().any(
10211 |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
10212 ));
10213 }
10214
10215 #[tokio::test]
10226 async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
10227 let home = connect_scratch_home("openclaw");
10228 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
10229 std::fs::write(
10230 home.join(".openclaw/openclaw.json"),
10231 r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
10232 )
10233 .unwrap();
10234 let script = home.join("openclaw");
10235 std::fs::write(
10236 &script,
10237 r#"#!/bin/sh
10238# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
10239[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
10240[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
10241[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
10242[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
10243while IFS= read -r line; do
10244 case "$line" in
10245 *'"initialize"'*)
10246 printf '%s
10247' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
10248 *'"session/resume"'*)
10249 printf '%s
10250' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
10251 *'"session/new"'*)
10252 printf '%s
10253' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
10254 *'"session/prompt"'*)
10255 printf '%s
10256' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
10257 printf '%s
10258' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
10259 esac
10260done
10261"#,
10262 )
10263 .unwrap();
10264 use std::os::unix::fs::PermissionsExt;
10265 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
10266
10267 let mut descriptor = crate::harness_support_registry()
10268 .harnesses
10269 .into_iter()
10270 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
10271 .expect("openclaw must be registered");
10272 descriptor
10273 .runtime
10274 .connect_launch
10275 .as_mut()
10276 .unwrap()
10277 .config_path = "~/.openclaw/openclaw.json".into();
10278 descriptor.runtime.default_launch.as_mut().unwrap().program =
10279 script.to_string_lossy().into_owned();
10280 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10281 assert!(backend.capabilities().resume_session);
10282
10283 let joined = backend
10284 .attach(crate::RuntimeAttachRequest {
10285 runtime_id: "agent:main:main".into(),
10286 cwd: Some(home.clone()),
10287 launch: None,
10288 mcp_servers: Vec::new(),
10289 approval_policy: None,
10290 })
10291 .await;
10292 let mut connection = joined.expect("mock bridge attach must succeed");
10293 assert_eq!(connection.handle().runtime_id, "agent:main:main");
10294 let turn = connection
10295 .send_input(crate::RuntimeInput {
10296 text: "hello".into(),
10297 image_urls: Vec::new(),
10298 })
10299 .await;
10300 assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
10301 connection.close().await.unwrap();
10302 }
10303
10304 #[tokio::test]
10305 async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
10306 let home = connect_scratch_home("fail");
10307 std::fs::write(
10308 home.join("opencode-tui.json"),
10309 r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
10310 )
10311 .unwrap();
10312
10313 let gateway_only = connect_descriptor("acp-v1-jsonrpc");
10314 let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
10315 panic!("an ACP connect endpoint has no gateway client yet");
10316 };
10317 let message = format!("{error:?}");
10318 assert!(message.contains("acp-v1-jsonrpc"));
10319 assert!(!message.contains("connect-secret"));
10320
10321 let unreadable = connect_descriptor("opencode-http-sse");
10322 let missing_home = connect_scratch_home("missing");
10323 let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
10324 panic!("an unreadable connect config must fail closed");
10325 };
10326 let message = format!("{error:?}");
10327 assert!(message.contains("opencode-tui.json"));
10328 assert!(!message.contains("connect-secret"));
10329 }
10330
10331 fn jobs_fixture_root() -> PathBuf {
10336 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10337 }
10338
10339 fn jobs_fixture_homes() -> Value {
10342 let root = jobs_fixture_root();
10343 json!({
10344 "claude_code": root.join("claude_jobs_home/projects"),
10345 "hermes": root.join("hermes_home/state.db"),
10346 "openclaw": root.join("openclaw_home"),
10347 })
10348 }
10349
10350 fn jobs_list(params: Value) -> Value {
10351 let mut service = HarnessSessionService::new();
10352 service.handle(request(1, "harness.v1.jobs.list", params))
10353 }
10354
10355 fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10356 result["jobs"]
10357 .as_array()
10358 .expect("jobs is an array")
10359 .iter()
10360 .find(|job| job["id"] == id)
10361 .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10362 }
10363
10364 #[test]
10365 fn gateway_health_derives_from_running_probe_and_install_state() {
10366 let running = RunningInstance {
10367 method: RunningInstanceMethod::GatewayConnect,
10368 evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10369 checked_at_ms: 1,
10370 };
10371 let up = gateway_health(
10372 HarnessId::OPENCLAW,
10373 true,
10374 Some(&running),
10375 Some("2026.7.1-2"),
10376 );
10377 assert_eq!(up.state, GatewayState::Up);
10378 assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10379 assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10380 let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10384 std::fs::create_dir_all(&dir).unwrap();
10385 let fake = dir.join("hermes");
10386 let write_fake = |body: &str| {
10387 std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10388 #[cfg(unix)]
10389 {
10390 use std::os::unix::fs::PermissionsExt;
10391 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10392 }
10393 };
10394 write_fake("echo '✗ Gateway service is not installed'");
10395 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10396 *slot.borrow_mut() = Some((
10397 HarnessId::HERMES.to_string(),
10398 fake.to_string_lossy().into_owned(),
10399 ))
10400 });
10401 let down = gateway_health(HarnessId::HERMES, true, None, None);
10402 assert_eq!(down.state, GatewayState::Down, "{down:?}");
10403 assert!(down.endpoint.is_none());
10404 assert!(down.evidence.contains("not installed"));
10405 write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10406 let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10407 assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10408 assert!(idle_but_up.evidence.contains("PID 4242"));
10409 write_fake("echo 'something unparseable'");
10410 let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10411 assert_eq!(no_verdict.state, GatewayState::Down);
10412 assert!(no_verdict.evidence.contains("no verdict"));
10413 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10414 let absent = gateway_health(HarnessId::HERMES, false, None, None);
10415 assert_eq!(absent.state, GatewayState::Unknown);
10416 let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10417 assert_eq!(core.state, GatewayState::Unknown);
10418 assert!(core.evidence.contains("per session"));
10419 }
10420
10421 #[test]
10422 fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10423 let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10424 let rows = response["result"]["triggers"]
10425 .as_array()
10426 .expect("triggers")
10427 .clone();
10428 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10429 assert!(
10430 hermes.iter().any(|r| r["name"] == "deploys"
10431 && r["route"] == "/webhooks/deploys"
10432 && r["kind"] == "webhook"),
10433 "{rows:#?}"
10434 );
10435 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10436 assert!(openclaw
10437 .iter()
10438 .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10439 assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10440 && r["kind"] == "hook_mapping"
10441 && r["target"]["action"] == "agent"));
10442 let rendered = response.to_string();
10443 for secret in [
10444 "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10445 "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10446 ] {
10447 assert!(!rendered.contains(secret), "{rendered}");
10448 }
10449 let refused =
10450 triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10451 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10452 }
10453
10454 fn triggers_list(params: Value) -> Value {
10455 let mut service = HarnessSessionService::new();
10456 service.handle(request(1, "harness.v1.triggers.list", params))
10457 }
10458
10459 #[test]
10460 fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10461 let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10462 let rows = response["result"]["routes"]
10463 .as_array()
10464 .expect("routes")
10465 .clone();
10466 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10467 assert_eq!(hermes.len(), 2, "{rows:#?}");
10468 assert_eq!(hermes[0]["target"], "coder");
10469 assert_eq!(hermes[0]["match"]["platform"], "slack");
10470 assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10471 assert_eq!(hermes[0]["specificity"], 4);
10472 assert_eq!(hermes[1]["default"], true);
10473 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10474 assert!(
10475 openclaw.iter().any(|r| r["target"] == "design"
10476 && r["match"]["platform"] == "slack"
10477 && r["specificity"] == 1),
10478 "{openclaw:#?}"
10479 );
10480 assert!(openclaw.iter().any(|r| r["default"] == true));
10481 let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10483 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10484 }
10485
10486 fn routes_list(params: Value) -> Value {
10487 let mut service = HarnessSessionService::new();
10488 service.handle(request(1, "harness.v1.routes.list", params))
10489 }
10490
10491 #[test]
10492 fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10493 let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10494 let result = &response["result"];
10495 let ids: Vec<&str> = result["jobs"]
10496 .as_array()
10497 .unwrap()
10498 .iter()
10499 .map(|job| job["id"].as_str().unwrap())
10500 .collect();
10501 assert_eq!(
10502 ids,
10503 vec![
10504 "release-watch",
10505 "toolu_wake_recheck",
10506 "digest-15m",
10507 "nightly-audit",
10508 "coder-standup",
10509 "ops-once-boot",
10510 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10511 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10512 "cron_standup",
10513 "cron_reindex",
10514 ],
10515 "{result}"
10516 );
10517
10518 let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10522 assert_eq!(health["harness"], "openclaw");
10523 assert_eq!(health["schedule"]["kind"], "interval");
10524 assert_eq!(health["schedule"]["minutes"], 10.0);
10525 assert_eq!(health["session_target"], "isolated");
10526 assert_eq!(health["payload"]["kind"], "prompt");
10527 assert_eq!(health["payload"]["text"], "nightly health check");
10528 assert_eq!(health["deliver"]["mode"], "announce");
10532 assert_eq!(health["deliver"]["target"], "last");
10533 assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10534 let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10535 assert_eq!(digest["schedule"]["kind"], "cron");
10536 assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10537 assert_eq!(digest["session_target"], "main");
10538 assert_eq!(digest["payload"]["kind"], "system_event");
10539
10540 let cron = job_row(result, "release-watch");
10542 assert_eq!(cron["harness"], "claude-code");
10543 assert_eq!(cron["scope"], "session");
10544 assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10545 assert_eq!(cron["schedule"]["kind"], "cron");
10546 assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10547 assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10548 assert_eq!(cron["payload"]["kind"], "prompt");
10549 assert_eq!(cron["recurring"], true);
10550 assert_eq!(cron["deliver"]["target"], "session");
10551 let wakeup = job_row(result, "toolu_wake_recheck");
10552 assert_eq!(wakeup["payload"]["kind"], "wakeup");
10553 assert_eq!(wakeup["schedule"]["kind"], "once");
10554 assert_eq!(wakeup["recurring"], false);
10555 assert_eq!(wakeup["state"], "pending");
10556
10557 let interval = job_row(result, "digest-15m");
10559 assert_eq!(interval["harness"], "hermes");
10560 assert_eq!(interval["scope"], "install");
10561 assert_eq!(interval["profile"], Value::Null);
10562 assert_eq!(interval["schedule"]["kind"], "interval");
10563 assert_eq!(interval["schedule"]["minutes"], 15.0);
10564 assert_eq!(interval["schedule"]["display"], "every 15 min");
10565 assert_eq!(interval["deliver"]["target"], "origin");
10566 assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10567 assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10568 assert_eq!(interval["last_status"], "ok");
10569 let nightly = job_row(result, "nightly-audit");
10570 assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10571 assert_eq!(nightly["deliver"]["target"], "local");
10572 assert_eq!(nightly["enabled"], false);
10573 assert_eq!(nightly["state"], "paused");
10574 let profiled = job_row(result, "ops-once-boot");
10576 assert_eq!(profiled["profile"], "ops");
10577 assert_eq!(profiled["schedule"]["kind"], "once");
10578 assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10579 assert_eq!(profiled["payload"]["kind"], "script");
10580 assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10582 assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10583 assert_eq!(profiled["recurring"], false);
10584
10585 let standup_to_group = job_row(result, "coder-standup");
10588 assert_eq!(standup_to_group["deliver"]["target"], "origin");
10589 assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10590 assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10591 assert!(standup_to_group["deliver"]["mode"].is_null());
10593 assert!(standup_to_group["deliver"]["account"].is_null());
10594
10595 let standup = job_row(result, "cron_standup");
10598 assert_eq!(standup["harness"], "openclaw");
10599 assert_eq!(standup["session_target"], "isolated");
10600 assert_eq!(standup["deliver"]["mode"], "announce");
10601 assert_eq!(standup["deliver"]["target"], "slack");
10602 assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10603 assert_eq!(standup["payload"]["kind"], "prompt");
10604 assert_eq!(standup["profile"], "main");
10605 let reindex = job_row(result, "cron_reindex");
10606 assert_eq!(reindex["session_target"], "main");
10607 assert_eq!(reindex["payload"]["kind"], "system_event");
10608 assert_eq!(reindex["schedule"]["kind"], "interval");
10609 assert_eq!(reindex["schedule"]["display"], "every 240 min");
10610 assert_eq!(reindex["enabled"], false);
10611
10612 let states: Vec<(&str, &str)> = result["sources"]
10614 .as_array()
10615 .unwrap()
10616 .iter()
10617 .map(|source| {
10618 (
10619 source["harness"].as_str().unwrap(),
10620 source["state"].as_str().unwrap(),
10621 )
10622 })
10623 .collect();
10624 assert_eq!(
10628 states,
10629 vec![
10630 ("claude-code", "scanned"),
10631 ("hermes", "read"),
10632 ("hermes", "absent_store"),
10633 ("hermes", "read"),
10634 ("openclaw", "read"),
10635 ("openclaw", "read"),
10636 ],
10637 "{result}"
10638 );
10639 }
10640
10641 #[test]
10642 fn jobs_list_filters_by_harness_session_and_profile() {
10643 let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10644 let ids: Vec<&str> = by_harness["result"]["jobs"]
10645 .as_array()
10646 .unwrap()
10647 .iter()
10648 .map(|job| job["id"].as_str().unwrap())
10649 .collect();
10650 assert_eq!(
10651 ids,
10652 vec![
10653 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10654 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10655 "cron_standup",
10656 "cron_reindex",
10657 ]
10658 );
10659
10660 let by_session = jobs_list(json!({
10661 "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10662 "homes": jobs_fixture_homes(),
10663 }));
10664 let jobs = by_session["result"]["jobs"].as_array().unwrap();
10665 assert_eq!(jobs.len(), 2, "{by_session}");
10666 assert!(jobs
10667 .iter()
10668 .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10669
10670 let by_profile = jobs_list(json!({
10671 "harness": "hermes",
10672 "profile": "ops",
10673 "homes": jobs_fixture_homes(),
10674 }));
10675 let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10676 assert_eq!(jobs.len(), 1, "{by_profile}");
10677 assert_eq!(jobs[0]["id"], "ops-once-boot");
10678 }
10679
10680 #[test]
10681 fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10682 let mut service = HarnessSessionService::new();
10683 let hermes = service.handle(request(
10684 1,
10685 "harness.v1.jobs.get",
10686 json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10687 ));
10688 assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10689 assert_eq!(hermes["result"]["source"]["provider"], "nous");
10691 assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10692
10693 let claude = service.handle(request(
10694 2,
10695 "harness.v1.jobs.get",
10696 json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10697 ));
10698 assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10699 assert_eq!(
10700 claude["result"]["source"]["tool_use_id"],
10701 "toolu_cron_release_watch"
10702 );
10703
10704 let missing = service.handle(request(
10705 3,
10706 "harness.v1.jobs.get",
10707 json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10708 ));
10709 assert!(missing["error"]["message"]
10710 .as_str()
10711 .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10712 }
10713
10714 #[test]
10715 fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10716 let mut service = HarnessSessionService::new();
10717 for (id, method, params) in [
10718 (
10719 1,
10720 "harness.v1.jobs.list",
10721 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10722 ),
10723 (
10724 2,
10725 "harness.v1.jobs.get",
10726 json!({"harness": "codex", "id": "anything"}),
10727 ),
10728 ] {
10729 let response = service.handle(request(id, method, params));
10730 assert_eq!(response["error"]["code"], -32020, "{response}");
10731 assert!(response["error"]["message"]
10732 .as_str()
10733 .is_some_and(|message| message.contains("has no scheduled jobs")));
10734 assert!(response.get("result").is_none());
10735 }
10736 }
10737
10738 #[test]
10739 fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10740 let scratch = std::env::temp_dir().join(format!(
10741 "supercode-jobs-migrated-{}-{}",
10742 std::process::id(),
10743 generated_session_id()
10744 ));
10745 std::fs::create_dir_all(&scratch).unwrap();
10746 let response = jobs_list(json!({
10747 "harness": "openclaw",
10748 "homes": {"openclaw": scratch.clone()},
10749 }));
10750 let result = &response["result"];
10751 assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10752 assert_eq!(result["sources"][0]["state"], "absent_store");
10753 assert_eq!(result["sources"][0]["harness"], "openclaw");
10754 std::fs::remove_dir_all(&scratch).ok();
10755 }
10756
10757 const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10766 const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10768
10769 fn runs_list(params: Value) -> Value {
10770 let mut service = HarnessSessionService::new();
10771 service.handle(request(1, "harness.v1.runs.list", params))
10772 }
10773
10774 fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10775 result["runs"]
10776 .as_array()
10777 .expect("runs is an array")
10778 .iter()
10779 .find(|run| run["id"] == id)
10780 .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10781 }
10782
10783 #[test]
10784 fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10785 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10786 let result = &response["result"];
10787 let ids: Vec<&str> = result["runs"]
10788 .as_array()
10789 .expect("runs is an array")
10790 .iter()
10791 .map(|run| run["id"].as_str().unwrap())
10792 .collect();
10793 let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10794 assert_eq!(
10795 ids,
10796 vec![
10797 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10799 "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10800 "c3d4e5f60718293a4b5c6d7e8f901234",
10801 "f60718293a4b5c6d7e8f901234567890",
10802 "e5f60718293a4b5c6d7e8f9012345678",
10803 "d4e5f60718293a4b5c6d7e8f90123456",
10804 "run_health_0002",
10806 digest_fire.as_str(),
10807 "run_health_0001",
10808 ],
10809 "{result}"
10810 );
10811
10812 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10815 assert_eq!(failed["harness"], "hermes");
10816 assert_eq!(failed["job_id"], "job42");
10817 assert_eq!(failed["status"], "failed");
10818 assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10819 assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10820
10821 let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10824 assert_eq!(abandoned["status"], "unknown");
10825 assert_eq!(abandoned["job_id"], "ops-once-boot");
10826
10827 let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10829 assert_eq!(running["status"], "running");
10830 assert!(running["finished_at"].is_null(), "{running}");
10831 assert!(running["session_id"].is_null(), "{running}");
10832
10833 let ok = run_row(result, "run_health_0001");
10836 assert_eq!(ok["harness"], "openclaw");
10837 assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10838 assert_eq!(ok["status"], "ok");
10839 assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10840 assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10841 assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10842 assert!(ok["claimed_at"].is_null(), "{ok}");
10844
10845 assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10848
10849 for id in [
10854 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10855 "d4e5f60718293a4b5c6d7e8f90123456",
10856 ] {
10857 assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10858 }
10859
10860 let sources = result["sources"].as_array().unwrap();
10863 let states: Vec<(&str, &str)> = sources
10864 .iter()
10865 .map(|source| {
10866 (
10867 source["harness"].as_str().unwrap(),
10868 source["state"].as_str().unwrap(),
10869 )
10870 })
10871 .collect();
10872 assert_eq!(
10873 states,
10874 vec![
10875 ("hermes", "read"),
10876 ("hermes", "absent_store"),
10877 ("hermes", "read"),
10878 ("openclaw", "read"),
10879 ],
10880 "{result}"
10881 );
10882 assert_eq!(sources[2]["profile"], "ops");
10883 assert!(sources[3]["path"]
10884 .as_str()
10885 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10886 }
10887
10888 #[test]
10889 fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10890 let response = runs_list(json!({
10891 "harness": "hermes",
10892 "job": "job42",
10893 "homes": jobs_fixture_homes(),
10894 }));
10895 let result = &response["result"];
10896 assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10897
10898 let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10902 assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10903
10904 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10908 assert!(failed["session_id"].is_null(), "{failed}");
10909 }
10910
10911 #[test]
10915 fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10916 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10917 let result = &response["result"];
10918
10919 let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10923 assert_eq!(delivered["status"], "completed");
10924 assert_eq!(delivered["delivery"]["state"], "delivered");
10925 assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10926 assert_eq!(delivered["delivery"]["attempts"], 1);
10927 assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10928 assert_eq!(
10929 delivered["delivery"]["delivered_at"],
10930 "2026-09-02T09:00:30.400Z"
10931 );
10932
10933 let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10936 assert_eq!(undelivered["status"], "completed");
10937 assert_eq!(undelivered["delivery"]["state"], "failed");
10938 assert_eq!(undelivered["delivery"]["attempts"], 3);
10939 assert_eq!(
10940 undelivered["delivery"]["last_error"],
10941 "telegram send failed: Bad Request: chat not found"
10942 );
10943 assert!(
10946 undelivered["delivery"]["delivered_at"].is_null(),
10947 "{undelivered}"
10948 );
10949
10950 let announced = run_row(result, "run_health_0001");
10953 assert_eq!(announced["delivery"]["state"], "delivered");
10954 assert_eq!(announced["delivery"]["target"], "last");
10955 assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10957 assert!(
10958 announced["delivery"]["delivered_at"].is_null(),
10959 "{announced}"
10960 );
10961 let refused = run_row(result, "run_health_0002");
10962 assert_eq!(refused["delivery"]["state"], "not-delivered");
10963 assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10964
10965 let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10968 assert!(skipped["delivery"].is_null(), "{skipped}");
10969 }
10970
10971 #[test]
10976 fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10977 let scratch = std::env::temp_dir().join(format!(
10978 "supercode-runs-delivery-{}-{}",
10979 std::process::id(),
10980 generated_session_id()
10981 ));
10982 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10983 let fixture = jobs_fixture_root().join("hermes_home");
10984 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10985 for name in ["cron/executions.db", "cron/jobs.json"] {
10986 std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10987 }
10988 {
10989 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10990 connection
10993 .execute(
10994 "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10995 [],
10996 )
10997 .unwrap();
10998 connection
11001 .execute(
11002 "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
11003 ('cron_coder-standup_20260902_090010', 'cron', \
11004 'agent:coder:telegram:group:-100777:55', 1788339610.0)",
11005 [],
11006 )
11007 .unwrap();
11008 }
11009 let response = runs_list(json!({
11010 "harness": "hermes",
11011 "job": "coder-standup",
11012 "homes": {"hermes": scratch.join("state.db")},
11013 }));
11014 let result = &response["result"];
11015 let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
11016 assert_eq!(
11017 matched["session_id"], "cron_coder-standup_20260902_090010",
11018 "{result}"
11019 );
11020 assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
11021 assert_eq!(
11022 matched["delivery"]["target"], "slack:C0FALLBACK:55",
11023 "{result}"
11024 );
11025 std::fs::remove_dir_all(&scratch).ok();
11026 }
11027
11028 #[test]
11029 fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
11030 let scratch = std::env::temp_dir().join(format!(
11035 "supercode-runs-compressed-{}-{}",
11036 std::process::id(),
11037 generated_session_id()
11038 ));
11039 std::fs::create_dir_all(scratch.join("cron")).unwrap();
11040 let fixture = jobs_fixture_root().join("hermes_home");
11041 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
11042 std::fs::copy(
11043 fixture.join("cron/executions.db"),
11044 scratch.join("cron/executions.db"),
11045 )
11046 .unwrap();
11047 {
11048 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
11049 connection
11050 .execute(
11051 "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
11052 ["cron_job42_20260902_120000"],
11053 )
11054 .unwrap();
11055 connection
11056 .execute(
11057 "INSERT INTO sessions (id, source, parent_session_id, started_at) \
11058 VALUES ('job42-after-compaction', 'cron', \
11059 'cron_job42_20260902_120000', 1788350000.0)",
11060 [],
11061 )
11062 .unwrap();
11063 }
11064 let response = runs_list(json!({
11065 "harness": "hermes",
11066 "job": "job42",
11067 "homes": {"hermes": scratch.join("state.db")},
11068 }));
11069 let result = &response["result"];
11070 assert_eq!(
11071 run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
11072 "job42-after-compaction",
11073 "{result}"
11074 );
11075 std::fs::remove_dir_all(&scratch).ok();
11076 }
11077
11078 #[test]
11079 fn runs_list_filters_by_job_and_caps_by_limit() {
11080 let by_job = runs_list(json!({
11081 "harness": "openclaw",
11082 "job": OPENCLAW_HEALTH_JOB,
11083 "homes": jobs_fixture_homes(),
11084 }));
11085 let ids: Vec<&str> = by_job["result"]["runs"]
11086 .as_array()
11087 .unwrap()
11088 .iter()
11089 .map(|run| run["id"].as_str().unwrap())
11090 .collect();
11091 assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
11092
11093 let capped = runs_list(json!({
11094 "harness": "openclaw",
11095 "limit": 1,
11096 "homes": jobs_fixture_homes(),
11097 }));
11098 let runs = capped["result"]["runs"].as_array().unwrap();
11099 assert_eq!(runs.len(), 1, "{capped}");
11100 assert_eq!(runs[0]["id"], "run_health_0002");
11102 }
11103
11104 #[test]
11105 fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
11106 let mut service = HarnessSessionService::new();
11107 let hermes = service.handle(request(
11108 1,
11109 "harness.v1.runs.get",
11110 json!({
11111 "harness": "hermes",
11112 "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
11113 "homes": jobs_fixture_homes(),
11114 }),
11115 ));
11116 assert_eq!(hermes["result"]["run"]["status"], "completed");
11117 assert_eq!(
11118 hermes["result"]["run"]["session_id"],
11119 "cron_job42_20260902_120000"
11120 );
11121 assert_eq!(hermes["result"]["source"]["source"], "scheduler");
11123 assert_eq!(hermes["result"]["source"]["pid"], 4242);
11124 assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
11125
11126 let openclaw = service.handle(request(
11127 2,
11128 "harness.v1.runs.get",
11129 json!({
11130 "harness": "openclaw",
11131 "id": "run_health_0002",
11132 "homes": jobs_fixture_homes(),
11133 }),
11134 ));
11135 assert_eq!(openclaw["result"]["run"]["status"], "error");
11136 assert_eq!(
11139 openclaw["result"]["source"]["delivery_status"],
11140 "not-delivered"
11141 );
11142 assert_eq!(
11143 openclaw["result"]["source"]["delivery_error"],
11144 "channel_not_found"
11145 );
11146 assert_eq!(openclaw["result"]["source"]["delivered"], 0);
11147 assert_eq!(
11148 openclaw["result"]["run"]["delivery"]["state"],
11149 "not-delivered"
11150 );
11151 assert_eq!(
11152 openclaw["result"]["run"]["delivery"]["last_error"],
11153 "channel_not_found"
11154 );
11155
11156 let missing = service.handle(request(
11157 3,
11158 "harness.v1.runs.get",
11159 json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
11160 ));
11161 assert!(missing["error"]["message"]
11162 .as_str()
11163 .is_some_and(|message| message.contains("no run `no-such-run`")));
11164 }
11165
11166 #[test]
11167 fn runs_refuse_a_harness_that_keeps_no_run_store() {
11168 let mut service = HarnessSessionService::new();
11169 for (id, method, params) in [
11170 (
11173 1,
11174 "harness.v1.runs.list",
11175 json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
11176 ),
11177 (
11178 2,
11179 "harness.v1.runs.get",
11180 json!({"harness": "claude-code", "id": "anything"}),
11181 ),
11182 (
11183 3,
11184 "harness.v1.runs.list",
11185 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
11186 ),
11187 ] {
11188 let response = service.handle(request(id, method, params));
11189 assert_eq!(response["error"]["code"], -32020, "{response}");
11190 assert!(response["error"]["message"]
11191 .as_str()
11192 .is_some_and(|message| message.contains("keeps no run store")));
11193 assert!(response.get("result").is_none());
11194 }
11195 }
11196
11197 #[test]
11198 fn runs_list_reports_an_install_with_no_run_store_as_absent() {
11199 let scratch = std::env::temp_dir().join(format!(
11200 "supercode-runs-empty-{}-{}",
11201 std::process::id(),
11202 generated_session_id()
11203 ));
11204 std::fs::create_dir_all(&scratch).unwrap();
11205 let response = runs_list(json!({
11206 "harness": "openclaw",
11207 "homes": {"openclaw": scratch.clone()},
11208 }));
11209 let result = &response["result"];
11210 assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
11211 assert_eq!(result["sources"][0]["state"], "absent_store");
11212 assert!(result["sources"][0]["path"]
11213 .as_str()
11214 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
11215 std::fs::remove_dir_all(&scratch).ok();
11216 }
11217}