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