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::runtime::generated_session_id;
17#[cfg(feature = "adapter-api")]
18use crate::runtime::{HostedHarnessConnection, HostedHarnessRuntime};
19use crate::sdk::{
20 discover_session_page, load_session, load_session_with_fidelity, SdkCapabilities, SdkError,
21 SdkErrorCode, SdkEvent, SdkOperation, SdkRequest, SdkRuntimeEvent, SdkService,
22};
23use crate::watch::{bound_session_view, message_json, normalized_session_json};
24use crate::Fidelity;
25#[cfg(feature = "adapter-api")]
26use crate::SupercodeHttpRuntimeBackend;
27use crate::{
28 discover_live_runtime, harness_support_registry, AcpRuntimeBackend, ClaudeCodeRuntimeBackend,
29 CodexRuntimeBackend, DiscoveryQuery, HarnessCatalog, HarnessHomes, HarnessId,
30 ImplementationKind, LiveRuntimeEndpoint, LiveRuntimeSource, OpenCodeRuntimeBackend,
31 PiRuntimeBackend, Role, RuntimeAttachRequest, RuntimeBackend, RuntimeConnection, RuntimeInput,
32 RuntimeLaunch, RuntimeStartRequest, Session, SessionDescriptor, SessionFollower, SessionFormat,
33 SessionLocator, SessionSource,
34};
35use crate::{reduce, tokens};
36#[cfg(feature = "adapter-api")]
37use crate::{register_live_runtime, resolve_live_runtime, LiveRuntimeRegistration};
38
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.index.subscribe",
57 "harness.v1.sessions.index.resize",
58 "harness.v1.sessions.index.unsubscribe",
59 "harness.v1.sessions.message",
60 "harness.v1.sessions.import",
61 "harness.v1.sessions.export",
62 "harness.v1.sessions.translate",
63 "harness.v1.sessions.reduce",
64 "harness.v1.sessions.branch",
65 "harness.v1.sessions.handoff",
66 "harness.v1.sessions.resume_instructions",
67 "harness.v1.skills.list",
68 "harness.v1.skills.install",
69 "harness.v1.skills.remove",
70 "harness.v1.memory.show",
71 "harness.v1.memory.search",
72 "harness.v1.jobs.list",
73 "harness.v1.jobs.get",
74 "harness.v1.jobs.create",
75 "harness.v1.jobs.update",
76 "harness.v1.jobs.pause",
77 "harness.v1.jobs.resume",
78 "harness.v1.jobs.run",
79 "harness.v1.jobs.delete",
80 "harness.v1.sessions.new",
81 "harness.v1.sessions.reset",
82 "harness.v1.sessions.archive",
83 "harness.v1.sessions.delete",
84 "harness.v1.runs.list",
85 "harness.v1.runs.get",
86 "harness.v1.approvals.list",
87 "harness.v1.approvals.resolve",
88 "harness.v1.runtimes.capabilities",
89 "harness.v1.runtimes.start",
90 "harness.v1.runtimes.resume",
91 "harness.v1.runtimes.attach_existing",
92 "harness.v1.runtimes.attach",
93 "harness.v1.runtimes.send_input",
94 "harness.v1.runtimes.interrupt",
95 "harness.v1.runtimes.steer",
96 "harness.v1.runtimes.respond",
97 "harness.v1.runtimes.terminal_instructions",
98 "harness.v1.runtimes.close",
99 "harness.v1.profiles.list",
100 "harness.v1.profiles.get",
101 "harness.v1.profiles.create",
102 "harness.v1.profiles.delete",
103 "harness.v1.channels.list",
104 "harness.v1.routes.list",
105 "harness.v1.triggers.list",
106 "harness.v1.channels.status",
107 "harness.v1.orchestration.load",
108 "harness.v1.orchestration.save",
109 "harness.v1.orchestration.compile",
110 "harness.v1.orchestration.decompile",
111 "harness.v1.orchestration.import",
112 "harness.v1.orchestration.export",
113 "harness.v1.workflow.load",
114];
115
116pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
118pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
120pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
122pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
124pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
126
127pub struct HarnessSessionService {
130 catalog: HarnessCatalog,
131 followers: BTreeMap<String, SessionFollower>,
132 followed_sources: BTreeMap<String, FollowedSource>,
133 activity_subscriptions: BTreeMap<String, ActivitySubscription>,
134 index_subscriptions: BTreeMap<String, crate::session_index::SessionIndexSubscription>,
135 index_notifier: Arc<Notify>,
136 #[cfg(feature = "adapter-api")]
137 activity_monitor: crate::session_activity::SessionActivityMonitor,
138 next_subscription: u64,
139 runtimes: BTreeMap<String, Box<dyn RuntimeConnection>>,
140 terminal_launches: BTreeMap<String, StructuredLaunch>,
141 runtime_sequences: BTreeMap<String, u64>,
142 next_runtime: u64,
143 reduction_store_root: Option<PathBuf>,
144 approvals: crate::approvals::ApprovalRegistry,
148 subagent_approvals: Option<Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>>,
151}
152
153impl Default for HarnessSessionService {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159impl HarnessSessionService {
160 pub fn new() -> Self {
162 Self {
163 catalog: HarnessCatalog::new(),
164 followers: BTreeMap::new(),
165 followed_sources: BTreeMap::new(),
166 activity_subscriptions: BTreeMap::new(),
167 index_subscriptions: BTreeMap::new(),
168 index_notifier: Arc::new(Notify::new()),
169 #[cfg(feature = "adapter-api")]
170 activity_monitor: Default::default(),
171 next_subscription: 1,
172 runtimes: BTreeMap::new(),
173 terminal_launches: BTreeMap::new(),
174 runtime_sequences: BTreeMap::new(),
175 next_runtime: 1,
176 reduction_store_root: None,
177 approvals: crate::approvals::ApprovalRegistry::new(),
178 subagent_approvals: None,
179 }
180 }
181
182 pub fn with_reduction_store_root(mut self, root: impl Into<PathBuf>) -> Self {
187 self.reduction_store_root = Some(root.into());
188 self
189 }
190
191 pub fn observe_subagent_approvals(
199 &mut self,
200 queue: Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>,
201 ) {
202 self.subagent_approvals = Some(queue);
203 }
204
205 pub fn approvals(&self, query: &crate::approvals::ApprovalsQuery) -> Vec<crate::ApprovalRow> {
212 let now = crate::approvals::now_ms();
213 let mut rows = self.approvals.rows(now);
214 if let Some(queue) = self.subagent_approvals.as_ref() {
215 let queued = queue
216 .lock()
217 .unwrap_or_else(std::sync::PoisonError::into_inner)
218 .clone();
219 rows.extend(crate::approvals::subagent_rows(&queued, now));
220 }
221 rows.retain(|row| query.matches(row));
222 rows.sort_by(|left, right| {
223 left.requested_at_ms
224 .cmp(&right.requested_at_ms)
225 .then_with(|| left.id.cmp(&right.id))
226 });
227 rows
228 }
229
230 async fn approvals_resolve(
240 &mut self,
241 params: Value,
242 ) -> std::result::Result<Value, ServiceError> {
243 let params = decode::<crate::approvals::ApprovalsResolveParams>(params)?;
244 if params.id.trim().is_empty() {
245 return Err(ServiceError::InvalidParams(
246 "approvals resolve requires the `id` of a listed approval row".into(),
247 ));
248 }
249 let choice = match (params.decision, params.option_id.as_deref()) {
250 (Some(_), Some(_)) => {
251 return Err(ServiceError::InvalidParams(
252 "approvals resolve takes either `decision` or `option_id`, not both".into(),
253 ))
254 }
255 (Some(decision), None) => crate::approvals::ApprovalChoice::Decision(decision),
256 (None, Some(option)) => crate::approvals::ApprovalChoice::Option(option.to_string()),
257 (None, None) => {
258 return Err(ServiceError::InvalidParams(format!(
259 "approvals resolve requires `decision` ({}) or an explicit `option_id`",
260 crate::approvals::ApprovalDecision::ALL
261 .map(|decision| decision.as_str())
262 .join(" | "),
263 )))
264 }
265 };
266 let resolution = self
267 .approvals
268 .resolution(¶ms.id, &choice)
269 .map_err(|error| ServiceError::InvalidParams(error.to_string()))?;
270 self.runtime_call(
274 "harness.v1.runtimes.respond",
275 json!({
276 "connection": resolution.connection,
277 "request_id": resolution.request_id,
278 "response": resolution.response,
279 }),
280 )
281 .await?;
282 Ok(json!({
283 "id": params.id,
284 "decision": params.decision.map(|decision| decision.as_str()),
285 "option_id": resolution.option_id,
286 "resolved": true,
287 }))
288 }
289
290 #[cfg(feature = "adapter-api")]
293 pub fn session_index_notifier(&self) -> Arc<Notify> {
294 Arc::clone(&self.index_notifier)
295 }
296
297 #[cfg(feature = "adapter-api")]
299 pub fn handle(&mut self, request: Value) -> Value {
300 let id = request.get("id").cloned().unwrap_or(Value::Null);
301 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
302 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
303 }
304 let Some(method) = request.get("method").and_then(Value::as_str) else {
305 return rpc_error(id, -32600, "request is missing `method`");
306 };
307 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
308 match self.call(method, params) {
309 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
310 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
311 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
312 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
313 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
314 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
315 }
316 }
317
318 #[cfg(feature = "adapter-api")]
321 pub async fn handle_async(&mut self, request: Value) -> Value {
322 let method = request
323 .get("method")
324 .and_then(Value::as_str)
325 .unwrap_or_default();
326 if matches!(
327 method,
328 "harness.v1.harnesses.list" | "harness.v1.harnesses.probe"
329 ) {
330 let id = request.get("id").cloned().unwrap_or(Value::Null);
331 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
332 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
333 }
334 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
335 return match self.inventory_call(method, params).await {
336 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
337 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
338 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
339 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
340 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
341 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
342 };
343 }
344 if matches!(
345 method,
346 "harness.v1.harnesses.auth.methods"
347 | "harness.v1.harnesses.auth.begin"
348 | "harness.v1.harnesses.auth.verify"
349 ) {
350 let id = request.get("id").cloned().unwrap_or(Value::Null);
351 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
352 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
353 }
354 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
355 return match self.harness_authentication_call(method, params).await {
356 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
357 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
358 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
359 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
360 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
361 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
362 };
363 }
364 if matches!(
370 method,
371 "harness.v1.sessions.new"
372 | "harness.v1.sessions.reset"
373 | "harness.v1.sessions.archive"
374 | "harness.v1.sessions.delete"
375 ) {
376 let id = request.get("id").cloned().unwrap_or(Value::Null);
377 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
378 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
379 }
380 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
381 let verb = match method {
382 "harness.v1.sessions.new" => crate::SessionVerb::New,
383 "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
384 "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
385 _ => crate::SessionVerb::Delete,
386 };
387 return match self.mutate_session(verb, params).await {
388 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
389 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
390 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
391 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
392 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
393 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
394 };
395 }
396 if method == "harness.v1.sessions.message" {
397 let id = request.get("id").cloned().unwrap_or(Value::Null);
398 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
399 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
400 }
401 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
402 return match self.message_call(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 matches!(
412 method,
413 "harness.v1.harnesses.settings" | "harness.v1.harnesses.configure"
414 ) {
415 let id = request.get("id").cloned().unwrap_or(Value::Null);
416 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
417 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
418 }
419 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
420 return match self.harness_settings_call(method, params) {
421 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
422 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
423 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
424 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
425 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
426 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
427 };
428 }
429 if method == "harness.v1.sessions.activity.subscribe" {
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.subscribe_session_activity(params).await {
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 let Some(operation) = SdkOperation::from_method(method) {
445 let id = request.get("id").cloned().unwrap_or(Value::Null);
446 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
447 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
448 }
449 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
450 return match self.execute(SdkRequest { operation, params }).await {
451 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
452 Err(error) => sdk_rpc_error(id, &error),
453 };
454 }
455 if !method.starts_with("harness.v1.runtimes.") {
456 return self.handle(request);
457 }
458 let id = request.get("id").cloned().unwrap_or(Value::Null);
459 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
460 return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
461 }
462 let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
463 match self.runtime_call(method, params).await {
464 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
465 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
466 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
467 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
468 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
469 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
470 }
471 }
472
473 #[cfg(feature = "adapter-api")]
476 pub fn poll(&mut self) -> Vec<Value> {
477 let mut notifications = Vec::new();
478 for (subscription, follower) in &mut self.followers {
479 match follower.poll() {
480 Ok(Some(event)) => notifications.push(json!({
481 "jsonrpc": "2.0",
482 "method": SESSION_EVENT_METHOD,
483 "params": {
484 "subscription": subscription,
485 "event": event.to_json(),
486 }
487 })),
488 Ok(None) => {}
489 Err(error) => notifications.push(json!({
490 "jsonrpc": "2.0",
491 "method": SESSION_EVENT_METHOD,
492 "params": {
493 "subscription": subscription,
494 "event": {
495 "type": "watch_error",
496 "recoverable": true,
497 "message": error.to_string(),
498 },
499 }
500 })),
501 }
502 }
503 notifications
504 }
505
506 #[cfg(feature = "adapter-api")]
517 pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
518 let registry = crate::LocalRuntimeRegistry::new();
519 let authorization = crate::RuntimeAuthorization::observer();
520 let mut notifications = Vec::new();
521 for (subscription, source) in &mut self.followed_sources {
522 let state = match registry
523 .source_state(&source.harness, &source.session_id, &authorization)
524 .await
525 {
526 Ok(Some(state)) => state,
527 Ok(None) => crate::RuntimeRegistryState::Persisted,
528 Err(_) => continue,
530 };
531 if source.reported.as_deref() == Some(state.as_str()) {
532 continue;
533 }
534 source.reported = Some(state.as_str().to_string());
535 notifications.push(json!({
536 "jsonrpc": "2.0",
537 "method": SESSION_EVENT_METHOD,
538 "params": {
539 "subscription": subscription,
540 "event": {"type": "runtime_state", "state": state.as_str()},
541 },
542 }));
543 }
544 notifications
545 }
546
547 #[cfg(feature = "adapter-api")]
551 pub async fn poll_session_activities(&mut self) -> Vec<Value> {
552 let subscriptions = self
553 .activity_subscriptions
554 .iter()
555 .map(|(id, subscription)| {
556 (
557 id.clone(),
558 subscription.locators.clone(),
559 subscription.homes.clone(),
560 )
561 })
562 .collect::<Vec<_>>();
563 let mut notifications = Vec::new();
564 for (subscription_id, locators, homes) in subscriptions {
565 let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
566 continue;
569 };
570 let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
571 continue;
572 };
573 let mut changed = Vec::new();
574 for activity in activities {
575 let key = activity.key();
576 if subscription
577 .reported
578 .get(&key)
579 .is_some_and(|previous| previous.same_state(&activity))
580 {
581 continue;
582 }
583 subscription.reported.insert(key, activity.clone());
584 changed.push(activity);
585 }
586 if !changed.is_empty() {
587 notifications.push(json!({
588 "jsonrpc": "2.0",
589 "method": SESSION_ACTIVITY_EVENT_METHOD,
590 "params": {
591 "subscription": subscription_id,
592 "activities": changed,
593 },
594 }));
595 }
596 }
597 notifications
598 }
599
600 #[cfg(feature = "adapter-api")]
604 pub fn poll_session_indexes(&mut self) -> Vec<Value> {
605 let mut notifications = Vec::new();
606 for (subscription, index) in &mut self.index_subscriptions {
607 let homes = index.homes().clone();
608 match index.poll() {
609 Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
610 Ok(changes) => notifications.push(json!({
611 "jsonrpc": "2.0",
612 "method": SESSION_INDEX_EVENT_METHOD,
613 "params": {
614 "subscription": subscription,
615 "revision": delta.revision,
616 "changes": changes,
617 },
618 })),
619 Err(error) => notifications.push(json!({
620 "jsonrpc": "2.0",
621 "method": SESSION_INDEX_EVENT_METHOD,
622 "params": {
623 "subscription": subscription,
624 "error": {"recoverable": true, "message": error_message(error)},
625 },
626 })),
627 },
628 Ok(None) => {}
629 Err(error) => notifications.push(json!({
630 "jsonrpc": "2.0",
631 "method": SESSION_INDEX_EVENT_METHOD,
632 "params": {
633 "subscription": subscription,
634 "error": {"recoverable": true, "message": error},
635 },
636 })),
637 }
638 }
639 notifications
640 }
641
642 #[cfg(feature = "adapter-api")]
643 async fn subscribe_session_activity(
644 &mut self,
645 params: Value,
646 ) -> std::result::Result<Value, ServiceError> {
647 let params = decode::<ActivitySubscribeParams>(params)?;
648 if params.locators.is_empty() {
649 return Err(ServiceError::InvalidParams(
650 "sessions.activity.subscribe requires at least one locator".into(),
651 ));
652 }
653 if params.locators.len() > 2_048 {
654 return Err(ServiceError::InvalidParams(
655 "sessions.activity.subscribe accepts at most 2048 locators".into(),
656 ));
657 }
658 let initial = self
659 .activity_monitor
660 .resolve(¶ms.locators, ¶ms.homes)
661 .await
662 .map_err(ServiceError::Sdk)?;
663 let subscription = format!("activity-sub-{}", self.next_subscription);
664 self.next_subscription += 1;
665 let reported = initial
666 .iter()
667 .cloned()
668 .map(|activity| (activity.key(), activity))
669 .collect();
670 self.activity_subscriptions.insert(
671 subscription.clone(),
672 ActivitySubscription {
673 locators: params.locators,
674 homes: params.homes,
675 reported,
676 },
677 );
678 Ok(json!({"subscription": subscription, "initial": initial}))
679 }
680
681 #[cfg(feature = "adapter-api")]
683 pub async fn poll_runtimes(&mut self) -> Vec<Value> {
684 self.poll_sdk_events()
685 .await
686 .into_iter()
687 .map(|(connection, runtime_event)| {
688 json!({
689 "jsonrpc": "2.0",
690 "method": RUNTIME_EVENT_METHOD,
691 "params": {
692 "connection": connection,
693 "session_id": runtime_event.session_id,
694 "sequence": runtime_event.event.sequence,
695 "event": {
696 "kind": runtime_event.event.kind,
697 "payload": runtime_event.event.payload,
698 },
699 },
700 })
701 })
702 .collect()
703 }
704
705 async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
706 let mut events = Vec::new();
707 let mut closed = Vec::new();
708 let now_ms = crate::approvals::now_ms();
709 for (connection, runtime) in &mut self.runtimes {
710 let session_id = runtime.handle().runtime_id.clone();
711 let harness = runtime.handle().harness.clone();
712 for _ in 0..256 {
717 match tokio::time::timeout(Duration::ZERO, runtime.next_event()).await {
718 Ok(Ok(Some(event))) => {
719 let terminal = event.kind == "transport_closed";
720 self.approvals
724 .observe(connection, &harness, &session_id, &event, now_ms);
725 let next_sequence = self
726 .runtime_sequences
727 .entry(session_id.clone())
728 .or_insert(0);
729 let sequence = event.sequence.unwrap_or_else(|| {
730 *next_sequence = next_sequence.saturating_add(1);
731 *next_sequence
732 });
733 *next_sequence = (*next_sequence).max(sequence);
734 events.push((
735 connection.clone(),
736 SdkRuntimeEvent {
737 session_id: session_id.clone(),
738 event: SdkEvent {
739 sequence,
740 kind: event.kind,
741 payload: event.payload,
742 },
743 },
744 ));
745 if terminal {
746 closed.push(connection.clone());
747 break;
748 }
749 }
750 Ok(Ok(None)) => {
751 let sequence = self
752 .runtime_sequences
753 .entry(session_id.clone())
754 .or_insert(0);
755 *sequence = sequence.saturating_add(1);
756 events.push((
757 connection.clone(),
758 SdkRuntimeEvent {
759 session_id,
760 event: SdkEvent {
761 sequence: *sequence,
762 kind: "transport_closed".into(),
763 payload: json!({"message": "Harness runtime transport closed."}),
764 },
765 },
766 ));
767 closed.push(connection.clone());
768 break;
769 }
770 Err(_) => break,
771 Ok(Err(error)) => {
772 let sequence = self
773 .runtime_sequences
774 .entry(session_id.clone())
775 .or_insert(0);
776 *sequence = sequence.saturating_add(1);
777 events.push((
778 connection.clone(),
779 SdkRuntimeEvent {
780 session_id,
781 event: SdkEvent {
782 sequence: *sequence,
783 kind: "transport_error".into(),
784 payload: json!({"message": error.to_string(), "terminal": true}),
785 },
786 },
787 ));
788 closed.push(connection.clone());
789 break;
790 }
791 }
792 }
793 }
794 for connection in closed {
795 if let Some(runtime) = self.runtimes.remove(&connection) {
796 self.runtime_sequences.remove(&runtime.handle().runtime_id);
797 }
798 self.terminal_launches.remove(&connection);
799 self.approvals.forget(&connection);
802 }
803 events
804 }
805
806 fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
807 match method {
808 "harness.v1.capabilities" => Ok(json!({
809 "version": HARNESS_SERVICE_VERSION,
810 "sdk": self.capabilities(),
811 "methods": HARNESS_SERVICE_METHODS,
812 "notifications": [
813 SESSION_EVENT_METHOD,
814 SESSION_ACTIVITY_EVENT_METHOD,
815 SESSION_INDEX_EVENT_METHOD,
816 RUNTIME_EVENT_METHOD
817 ],
818 "harnesses": harness_support_registry()
819 .harnesses
820 .into_iter()
821 .map(|harness| harness.id)
822 .collect::<Vec<_>>(),
823 })),
824 "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
825 .map_err(|error| ServiceError::Operation(error.to_string())),
826 "harness.v1.profiles.list" | "harness.v1.profiles.get" => profiles_call(method, params),
827 "harness.v1.profiles.create" => {
833 mutate_profile(crate::profiles_control::ProfileVerb::Create, params)
834 }
835 "harness.v1.profiles.delete" => {
836 mutate_profile(crate::profiles_control::ProfileVerb::Delete, params)
837 }
838 "harness.v1.channels.list" | "harness.v1.channels.status" => {
839 channels_call(method, params)
840 }
841 "harness.v1.routes.list" => routes_call(params),
844 "harness.v1.triggers.list" => triggers_call(params),
846 "harness.v1.workflow.load" => {
856 let params = decode::<WorkflowLoadParams>(params)?;
857 let read =
858 crate::workflow_doors::load(params.from, ¶ms.home).map_err(operation)?;
859 serde_json::to_value(read)
860 .map_err(|error| ServiceError::Operation(error.to_string()))
861 }
862 "harness.v1.orchestration.load" => {
863 let params = decode::<OrchestrationLoadParams>(params)?;
864 let read = crate::orchestration_doors::load(¶ms.root, params.flavor)
865 .map_err(operation)?;
866 serde_json::to_value(read)
867 .map_err(|error| ServiceError::Operation(error.to_string()))
868 }
869 "harness.v1.orchestration.save" => {
870 let params = decode::<OrchestrationSaveParams>(params)?;
871 let saved = crate::orchestration_doors::save(
872 ¶ms.root,
873 params.orchestration,
874 params.vault,
875 )
876 .map_err(operation)?;
877 serde_json::to_value(saved)
878 .map_err(|error| ServiceError::Operation(error.to_string()))
879 }
880 "harness.v1.orchestration.compile" => {
881 let params = decode::<OrchestrationCompileParams>(params)?;
882 let read = crate::orchestration_doors::compile(params.from, ¶ms.home)
883 .map_err(operation)?;
884 serde_json::to_value(read)
885 .map_err(|error| ServiceError::Operation(error.to_string()))
886 }
887 "harness.v1.orchestration.decompile" => {
888 let params = decode::<OrchestrationDecompileParams>(params)?;
889 let report = crate::orchestration_doors::decompile(
890 params.to,
891 params.orchestration,
892 ¶ms.source,
893 params.source_flavor,
894 ¶ms.dest,
895 params.vault,
896 )
897 .map_err(operation)?;
898 serde_json::to_value(report)
899 .map_err(|error| ServiceError::Operation(error.to_string()))
900 }
901 "harness.v1.orchestration.import" => {
906 let params = decode::<OrchestrationImportParams>(params)?;
907 let imported =
908 crate::orchestration_doors::import(params.from, ¶ms.home, ¶ms.into)
909 .map_err(operation)?;
910 serde_json::to_value(imported)
911 .map_err(|error| ServiceError::Operation(error.to_string()))
912 }
913 "harness.v1.orchestration.export" => {
914 let params = decode::<OrchestrationExportParams>(params)?;
915 let report =
916 crate::orchestration_doors::export(params.to, ¶ms.root, ¶ms.dest)
917 .map_err(operation)?;
918 serde_json::to_value(report)
919 .map_err(|error| ServiceError::Operation(error.to_string()))
920 }
921 "harness.v1.memory.show" | "harness.v1.memory.search" => memory_call(method, params),
927 "harness.v1.skills.list" => {
932 let query = decode::<crate::skills::SkillsQuery>(params)?;
933 if let Some(harness) = query.harness.as_deref() {
934 if !crate::skills::SKILL_HARNESSES.contains(&harness) {
935 return Err(ServiceError::UnsupportedAction(format!(
936 "`{harness}` has no skills root supercode reads"
937 )));
938 }
939 }
940 serde_json::to_value(crate::skills::list_skills(&query))
941 .map_err(|error| ServiceError::Operation(error.to_string()))
942 }
943 "harness.v1.skills.install" => {
951 mutate_skill(crate::skills_control::SkillVerb::Install, params)
952 }
953 "harness.v1.skills.remove" => {
954 mutate_skill(crate::skills_control::SkillVerb::Remove, params)
955 }
956 "harness.v1.approvals.list" => {
964 let query = decode::<crate::approvals::ApprovalsQuery>(params)?;
965 if let Some(harness) = query.harness.as_deref() {
966 if !crate::approvals::lists_approvals(harness) {
967 return Err(ServiceError::UnsupportedAction(format!(
968 "`{harness}` has no runtime door that carries an approval request"
969 )));
970 }
971 }
972 serde_json::to_value(self.approvals(&query))
973 .map_err(|error| ServiceError::Operation(error.to_string()))
974 }
975 "harness.v1.sessions.discover" => {
976 let query = decode::<DiscoveryQuery>(params)?;
977 let page = discover_session_page(&query).map_err(operation)?;
978 let peers = if page
983 .sessions
984 .iter()
985 .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
986 {
987 crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(
988 &query.homes,
989 ))
990 } else {
991 Vec::new()
992 };
993 let activities = crate::session_activity::resolve_stock_session_activities(
994 &page
995 .sessions
996 .iter()
997 .map(|session| session.locator.clone())
998 .collect::<Vec<_>>(),
999 &query.homes,
1000 )
1001 .into_iter()
1002 .map(|activity| (activity.key(), activity))
1003 .collect::<BTreeMap<_, _>>();
1004 let sessions = page
1005 .sessions
1006 .into_iter()
1007 .map(|session| {
1008 let mut value = live_descriptor_value(&session, &peers)?;
1009 let activity_key = (
1010 session.locator.harness.as_str().to_string(),
1011 session.locator.session_id.clone(),
1012 );
1013 if let Some(activity) = activities.get(&activity_key) {
1014 value["activity"] = serde_json::to_value(activity)
1015 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1016 if let Some(status) = legacy_live_status(activity) {
1017 value["live_status"] = json!(status);
1018 }
1019 }
1020 Ok(value)
1021 })
1022 .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1023 let mut result = json!({"sessions": sessions, "next_cursor": page.next_cursor});
1024 if query.search_previews {
1027 result["receipt"] = serde_json::to_value(page.receipt)
1028 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1029 }
1030 Ok(result)
1031 }
1032 "harness.v1.sessions.load" => {
1033 let params = decode::<LoadSessionParams>(params)?;
1034 if let Some(options) = ¶ms.options {
1035 options.validate()?;
1036 if let Some(result) = indexed_claude_window(¶ms.read.locator, options)? {
1037 return Ok(result);
1038 }
1039 return load_session(¶ms.read.locator)
1040 .map(|session| projected_session_result(&session, options))
1041 .map_err(operation);
1042 }
1043 let mut session = if params.read.display_history() {
1044 self.catalog
1045 .load_display_view(
1046 ¶ms.read.locator,
1047 params.read.read_fidelity(),
1048 params.read.tail_messages().unwrap_or(500),
1049 )
1050 .map_err(crate::Error::from)
1051 } else if params.read.include_subagents() {
1052 load_session_with_fidelity(¶ms.read.locator, params.read.read_fidelity())
1053 } else {
1054 self.catalog
1055 .load_parent_with_fidelity(
1056 ¶ms.read.locator,
1057 params.read.read_fidelity(),
1058 )
1059 .map_err(crate::Error::from)
1060 }
1061 .map_err(operation)?;
1062 params.read.bound_session(&mut session);
1063 Ok(json!({"session": normalized_session_json(&session)}))
1064 }
1065 "harness.v1.sessions.follow" => {
1066 let params = decode::<LocatorParams>(params)?;
1067 let mut follower = self
1068 .catalog
1069 .follow_read_view(
1070 ¶ms.locator,
1071 params.read_fidelity(),
1072 params.include_subagents(),
1073 params.tail_messages(),
1074 params.max_message_chars(),
1075 params.display_history(),
1076 )
1077 .map_err(operation)?;
1078 let initial = follower
1079 .poll()
1080 .map_err(operation)?
1081 .map(|event| event.to_json());
1082 let subscription = format!("sub-{}", self.next_subscription);
1083 self.next_subscription += 1;
1084 self.followers.insert(subscription.clone(), follower);
1085 self.followed_sources.insert(
1086 subscription.clone(),
1087 FollowedSource {
1088 harness: params.locator.harness.as_str().to_string(),
1089 session_id: params.locator.session_id.clone(),
1090 reported: None,
1091 },
1092 );
1093 Ok(json!({"subscription": subscription, "initial": initial}))
1094 }
1095 "harness.v1.sessions.unfollow" => {
1096 let params = decode::<UnfollowParams>(params)?;
1097 self.followed_sources.remove(¶ms.subscription);
1098 Ok(json!({
1099 "removed": self.followers.remove(¶ms.subscription).is_some()
1100 }))
1101 }
1102 "harness.v1.sessions.activity.unsubscribe" => {
1103 let params = decode::<UnfollowParams>(params)?;
1104 Ok(json!({
1105 "removed": self.activity_subscriptions.remove(¶ms.subscription).is_some()
1106 }))
1107 }
1108 "harness.v1.sessions.index.subscribe" => {
1109 let query = decode::<DiscoveryQuery>(params)?;
1110 crate::session_index::validate_query(&query)
1111 .map_err(ServiceError::InvalidParams)?;
1112 let homes = query.homes.clone();
1113 let (index, initial) = crate::session_index::SessionIndexSubscription::open(
1114 query,
1115 Arc::clone(&self.index_notifier),
1116 )
1117 .map_err(ServiceError::Operation)?;
1118 let peers = peers_for_descriptors(&initial, &homes);
1119 let initial = initial
1120 .iter()
1121 .map(|descriptor| live_descriptor_value(descriptor, &peers))
1122 .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1123 let subscription = format!("index-sub-{}", self.next_subscription);
1124 self.next_subscription += 1;
1125 self.index_subscriptions.insert(subscription.clone(), index);
1126 Ok(json!({
1127 "subscription": subscription,
1128 "revision": 1,
1129 "initial": initial,
1130 }))
1131 }
1132 "harness.v1.sessions.index.resize" => {
1133 let params = decode::<IndexResizeParams>(params)?;
1134 crate::session_index::validate_limit(params.limit)
1135 .map_err(ServiceError::InvalidParams)?;
1136 let index = self
1137 .index_subscriptions
1138 .get_mut(¶ms.subscription)
1139 .ok_or_else(|| {
1140 ServiceError::InvalidParams("unknown session index subscription".into())
1141 })?;
1142 let prepared = index
1143 .prepare_resize(params.limit)
1144 .map_err(ServiceError::Operation)?;
1145 let peers = peers_for_descriptors(&prepared.page.sessions, index.homes());
1146 let initial = prepared
1147 .page
1148 .sessions
1149 .iter()
1150 .map(|descriptor| live_descriptor_value(descriptor, &peers))
1151 .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1152 let response = json!({
1153 "subscription": params.subscription,
1154 "revision": prepared.revision,
1155 "initial": initial,
1156 "receipt": prepared.page.receipt,
1157 });
1158 index.commit_resize(prepared);
1159 Ok(response)
1160 }
1161 "harness.v1.sessions.index.unsubscribe" => {
1162 let params = decode::<UnfollowParams>(params)?;
1163 Ok(json!({
1164 "removed": self.index_subscriptions.remove(¶ms.subscription).is_some()
1165 }))
1166 }
1167 "harness.v1.sessions.import" => {
1168 let params = decode::<ImportSessionParams>(params)?;
1169 let session = Session::load_str(¶ms.content, params.source_harness.into())
1170 .map_err(operation)?;
1171 Ok(json!({"session": normalized_session_json(&session)}))
1172 }
1173 "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
1174 let params = decode::<ExportSessionParams>(params)?;
1175 let session = load_session(¶ms.locator).map_err(operation)?;
1176 let artifact = session_artifact(¶ms.locator, &session, params.target_harness)?;
1177 if method == "harness.v1.sessions.export"
1178 && params.target_harness == TransferFormat::Hermes
1179 {
1180 let imported = crate::hermes_import::import_into_hermes(&session, None)
1182 .map_err(operation)?;
1183 return Ok(json!({"artifact": artifact, "imported": imported}));
1184 }
1185 Ok(json!({"artifact": artifact}))
1186 }
1187 "harness.v1.sessions.reduce" => {
1188 let params = decode::<ReduceSessionParams>(params)?;
1189 self.reduce_session(params)
1190 }
1191 "harness.v1.sessions.branch" => {
1192 let params = decode::<BranchSessionParams>(params)?;
1193 let session = load_session(¶ms.locator).map_err(operation)?;
1194 let storage = params.locator.storage.path().display().to_string();
1195 let bootstrap_prompt = format!(
1196 "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.",
1197 params.locator.harness.as_str(), params.locator.session_id, storage
1198 );
1199 let artifact = params
1200 .target_harness
1201 .map(|target| session_artifact(¶ms.locator, &session, target))
1202 .transpose()?;
1203 Ok(json!({
1204 "parent": params.locator,
1205 "session": normalized_session_json(&session),
1206 "bootstrap_prompt": bootstrap_prompt,
1207 "artifact": artifact,
1208 }))
1209 }
1210 "harness.v1.sessions.handoff" => {
1211 let params = decode::<HandoffSessionParams>(params)?;
1212 let session = load_session(¶ms.locator).map_err(operation)?;
1213 let cwd = params
1214 .cwd
1215 .or_else(|| session.meta.cwd.clone())
1216 .unwrap_or_else(|| PathBuf::from("."));
1217 let artifact =
1218 handoff_artifact(¶ms.locator, &session, params.target_harness, &cwd)?;
1219 let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
1220 ServiceError::Operation(
1221 "handoff artifact omitted target session identity".into(),
1222 )
1223 })?;
1224 let instructions =
1225 handoff_instructions(params.target_harness, target_session_id, &cwd);
1226 Ok(json!({
1227 "artifact": artifact,
1228 "launch": instructions.launch,
1229 "materialize": instructions.materialize,
1230 "requires_materialization": instructions.requires_materialization,
1231 "note": instructions.note,
1232 }))
1233 }
1234 "harness.v1.jobs.list" => {
1238 let query = decode::<crate::jobs::JobsQuery>(params)?;
1239 if let Some(harness) = query.harness.as_deref() {
1240 refuse_harness_without_jobs(harness, "jobs.list")?;
1241 }
1242 let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1243 serde_json::to_value(listing)
1244 .map_err(|error| ServiceError::Operation(error.to_string()))
1245 }
1246 "harness.v1.jobs.get" => {
1247 let params = decode::<JobsGetParams>(params)?;
1248 refuse_harness_without_jobs(¶ms.harness, "jobs.get")?;
1249 match crate::jobs::get_job(¶ms.harness, ¶ms.id, ¶ms.homes)
1250 .map_err(operation)?
1251 {
1252 Some((job, source)) => Ok(json!({"job": job, "source": source})),
1253 None => Err(ServiceError::Operation(format!(
1254 "`{}` has no scheduled job `{}`",
1255 params.harness, params.id
1256 ))),
1257 }
1258 }
1259 "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1265 "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1266 "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1267 "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1268 "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1269 "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1270 "harness.v1.runs.list" => {
1274 let query = decode::<crate::runs::RunsQuery>(params)?;
1275 if let Some(harness) = query.harness.as_deref() {
1276 refuse_harness_without_runs(harness, "runs.list")?;
1277 }
1278 let listing = crate::runs::list_runs(&query).map_err(operation)?;
1279 serde_json::to_value(listing)
1280 .map_err(|error| ServiceError::Operation(error.to_string()))
1281 }
1282 "harness.v1.runs.get" => {
1283 let params = decode::<RunsGetParams>(params)?;
1284 refuse_harness_without_runs(¶ms.harness, "runs.get")?;
1285 match crate::runs::get_run(¶ms.harness, ¶ms.id, ¶ms.homes)
1286 .map_err(operation)?
1287 {
1288 Some((run, source)) => Ok(json!({"run": run, "source": source})),
1289 None => Err(ServiceError::Operation(format!(
1290 "`{}` has no run `{}`",
1291 params.harness, params.id
1292 ))),
1293 }
1294 }
1295 "harness.v1.sessions.resume_instructions" => {
1296 let params = decode::<ResumeInstructionsParams>(params)?;
1297 let session = load_session(¶ms.locator).map_err(operation)?;
1298 let cwd = params
1299 .cwd
1300 .or(session.meta.cwd)
1301 .unwrap_or_else(|| PathBuf::from("."));
1302 let launch = resume_launch(
1303 params.locator.harness.as_str(),
1304 ¶ms.locator.session_id,
1305 &cwd,
1306 params.policy,
1307 )?;
1308 Ok(json!({"launch": launch}))
1309 }
1310 _ => Err(ServiceError::MethodNotFound),
1311 }
1312 }
1313
1314 fn reduce_session(
1315 &self,
1316 params: ReduceSessionParams,
1317 ) -> std::result::Result<Value, ServiceError> {
1318 let session = load_session(¶ms.locator).map_err(operation)?;
1319 if session.messages.is_empty() {
1320 return Err(ServiceError::InvalidParams(
1321 "cannot reduce an empty session".into(),
1322 ));
1323 }
1324 let keep_last = params.keep_last.clamp(1, 128);
1325 let policy = reduce::ReductionPolicy {
1326 clear_turns_older_than: Some(keep_last),
1327 ..Default::default()
1328 };
1329 let (view, log) =
1330 reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1331 if log.reductions.is_empty() {
1332 return Err(ServiceError::UnsupportedAction(format!(
1333 "session `{}` is already too small for a meaningful reversible reduction",
1334 params.locator.session_id
1335 )));
1336 }
1337 let source_tokens = tokens::estimate_view_tokens(&session.messages);
1338 let reduced_tokens = tokens::estimate_view_tokens(&view);
1339 if reduced_tokens >= source_tokens {
1340 return Err(ServiceError::UnsupportedAction(format!(
1341 "session `{}` has no token-reducing reversible projection",
1342 params.locator.session_id
1343 )));
1344 }
1345
1346 let store_root = self
1347 .reduction_store_root
1348 .clone()
1349 .unwrap_or_else(default_reduction_store_root);
1350 let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1351 let rescue_id = format!("rescue-{}", generated_session_id());
1352 let imported = session
1353 .imported_message_count
1354 .unwrap_or(session.messages.len())
1355 .min(session.messages.len());
1356 let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1357 let view_jsonl = messages_jsonl(&view)?;
1358 let title = format!(
1359 "Reduced {} continuation from {}",
1360 params.target_harness.id(),
1361 params.locator.session_id
1362 );
1363
1364 store
1369 .save_sidecar(&rescue_id, &sidecar_jsonl)
1370 .map_err(operation)?;
1371 store
1372 .save_reduction_log(&rescue_id, &log)
1373 .map_err(operation)?;
1374 store
1375 .save(&rescue_id, &title, &view_jsonl)
1376 .map_err(operation)?;
1377
1378 let source_bytes = serde_json::to_vec(&session.messages)
1379 .map_err(|error| ServiceError::Operation(error.to_string()))?
1380 .len() as u64;
1381 let reduced_bytes = serde_json::to_vec(&view)
1382 .map_err(|error| ServiceError::Operation(error.to_string()))?
1383 .len() as u64;
1384 store
1385 .set_reduction_stats(
1386 &rescue_id,
1387 &title,
1388 source_bytes,
1389 reduced_bytes,
1390 log.reductions.len() as u32,
1391 )
1392 .map_err(operation)?;
1393
1394 let reloaded_sidecar = store
1398 .load_sidecar(&rescue_id)
1399 .map_err(operation)?
1400 .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1401 let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1402 let reloaded_log = store
1403 .load_reduction_log(&rescue_id)
1404 .map_err(operation)?
1405 .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1406 let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1407 reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1408 let (restamped_view, restamped_log) =
1415 reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1416 if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1417 return Err(ServiceError::Operation(
1418 "persisted reduction view does not match its durable log and sidecar".into(),
1419 ));
1420 }
1421 if restamped_log != reloaded_log {
1422 return Err(ServiceError::Operation(
1423 "reapplying the durable reduction log changed its identity".into(),
1424 ));
1425 }
1426 let inverted =
1427 reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1428 if inverted != session.messages {
1429 return Err(ServiceError::Operation(
1430 "reduction inversion did not restore the source messages byte-exactly".into(),
1431 ));
1432 }
1433
1434 let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1435 let sidecar_path = store.sidecar_path(&rescue_id);
1436 let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1437 let bootstrap_prompt = reduced_bootstrap_prompt(
1438 ¶ms.locator,
1439 params.target_harness,
1440 &view_jsonl,
1441 &sidecar_path,
1442 &reduction_log_path,
1443 );
1444 let mut reduced_session = session.clone();
1445 reduced_session.meta.session_id = Some(rescue_id.clone());
1446 reduced_session.messages = view;
1447
1448 Ok(json!({
1449 "session": normalized_session_json(&reduced_session),
1450 "bootstrap_prompt": bootstrap_prompt,
1451 "receipt": {
1452 "id": rescue_id,
1453 "sidecar_id": rescue_id,
1454 "source_harness": params.locator.harness,
1455 "target_harness": params.target_harness.id(),
1456 "source_tokens": source_tokens,
1457 "reduced_tokens": reduced_tokens,
1458 "ratio": ratio,
1459 "source_bytes": source_bytes,
1460 "reduced_bytes": reduced_bytes,
1461 "reductions": reloaded_log.reductions.len(),
1462 "sidecar_path": sidecar_path,
1463 "reduction_log_path": reduction_log_path,
1464 "verified": true,
1465 "reversible": true,
1466 }
1467 }))
1468 }
1469
1470 pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1488 if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1489 return None;
1490 }
1491 let method = request.get("method").and_then(Value::as_str)?;
1492 if !RUNTIME_OPEN_METHODS.contains(&method) {
1493 return None;
1494 }
1495 Some(RuntimeOpen {
1496 id: request.get("id").cloned().unwrap_or(Value::Null),
1497 method: method.to_string(),
1498 params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1499 })
1500 }
1501
1502 pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1506 let OpenedRuntime { id, outcome } = opened;
1507 let result = match outcome {
1508 Ok(open) => self.register_open_runtime(open).await,
1509 Err(error) => Err(error),
1510 };
1511 service_response(id, result)
1512 }
1513
1514 async fn register_open_runtime(
1516 &mut self,
1517 open: OpenRuntime,
1518 ) -> std::result::Result<Value, ServiceError> {
1519 match open {
1520 OpenRuntime::Hosted {
1521 runtime,
1522 capabilities,
1523 workspace,
1524 } => {
1525 self.insert_hosted_runtime(runtime, capabilities, workspace)
1526 .await
1527 }
1528 OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1529 }
1530 }
1531
1532 async fn runtime_call(
1533 &mut self,
1534 method: &str,
1535 params: Value,
1536 ) -> std::result::Result<Value, ServiceError> {
1537 match method {
1538 "harness.v1.runtimes.capabilities" => {
1539 let params = decode::<RuntimeBackendParams>(params)?;
1540 let backend = runtime_backend(¶ms)?;
1541 Ok(json!({
1542 "harness": backend.harness(),
1543 "capabilities": backend.capabilities(),
1544 }))
1545 }
1546 method if RUNTIME_OPEN_METHODS.contains(&method) => {
1547 self.register_open_runtime(open_runtime(method, params).await?)
1548 .await
1549 }
1550 "harness.v1.runtimes.send_input" => {
1551 let params = decode::<RuntimeInputParams>(params)?;
1552 let image_urls = validate_runtime_image_urls(params.image_urls)?;
1553 let runtime = self.runtime_mut(¶ms.connection)?;
1554 let turn_id = within_control_deadline(
1555 method,
1556 runtime.send_input(RuntimeInput {
1557 text: params.text,
1558 image_urls,
1559 }),
1560 )
1561 .await?
1562 .map_err(operation)?;
1563 Ok(json!({"turn_id": turn_id}))
1564 }
1565 "harness.v1.runtimes.interrupt" => {
1566 let params = decode::<RuntimeConnectionParams>(params)?;
1567 within_control_deadline(method, self.runtime_mut(¶ms.connection)?.interrupt())
1568 .await?
1569 .map_err(operation)?;
1570 Ok(json!({}))
1571 }
1572 "harness.v1.runtimes.steer" => {
1573 let params = decode::<RuntimeInputParams>(params)?;
1574 if !params.image_urls.is_empty() {
1575 return Err(ServiceError::InvalidParams(
1576 "runtime steering accepts text only".into(),
1577 ));
1578 }
1579 let text = params.text.trim();
1580 if text.is_empty() || text.chars().count() > 50_000 {
1581 return Err(ServiceError::InvalidParams(
1582 "runtime steering requires 1 to 50,000 text characters".into(),
1583 ));
1584 }
1585 within_control_deadline(
1586 method,
1587 self.runtime_mut(¶ms.connection)?
1588 .steer(text.to_string()),
1589 )
1590 .await?
1591 .map_err(operation)?;
1592 Ok(json!({}))
1593 }
1594 "harness.v1.runtimes.respond" => {
1595 let params = decode::<RuntimeRespondParams>(params)?;
1596 let request_id = params.request_id.clone();
1597 within_control_deadline(
1598 method,
1599 self.runtime_mut(¶ms.connection)?
1600 .respond(params.request_id, params.response),
1601 )
1602 .await?
1603 .map_err(operation)?;
1604 self.approvals.answered(¶ms.connection, &request_id);
1606 Ok(json!({}))
1607 }
1608 "harness.v1.runtimes.terminal_instructions" => {
1609 let params = decode::<RuntimeConnectionParams>(params)?;
1610 let launch = self
1611 .terminal_launches
1612 .get(¶ms.connection)
1613 .ok_or_else(|| {
1614 ServiceError::Operation(
1615 "this runtime is not hosted for terminal attachment".into(),
1616 )
1617 })?;
1618 Ok(json!({"launch":launch}))
1619 }
1620 "harness.v1.runtimes.close" => {
1621 let params = decode::<RuntimeConnectionParams>(params)?;
1622 let Some(runtime) = self.runtimes.get_mut(¶ms.connection) else {
1623 return Err(ServiceError::InvalidParams(format!(
1624 "unknown runtime connection `{}`",
1625 params.connection
1626 )));
1627 };
1628 let runtime_id = runtime.handle().runtime_id.clone();
1631 within_control_deadline(method, runtime.close())
1632 .await?
1633 .map_err(operation)?;
1634 self.runtimes.remove(¶ms.connection);
1635 self.terminal_launches.remove(¶ms.connection);
1636 self.runtime_sequences.remove(&runtime_id);
1637 self.approvals.forget(¶ms.connection);
1638 Ok(json!({"closed": true}))
1639 }
1640 _ => Err(ServiceError::MethodNotFound),
1641 }
1642 }
1643
1644 #[cfg(feature = "adapter-api")]
1646 async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1647 let params = decode::<MessageSessionParams>(params)?;
1648 Ok(message_live_session(¶ms, &crate::claude_peer::ProcessCourierRunner).await)
1649 }
1650
1651 #[cfg(feature = "adapter-api")]
1652 fn harness_settings_call(
1653 &self,
1654 method: &str,
1655 params: Value,
1656 ) -> std::result::Result<Value, ServiceError> {
1657 let homes = crate::HarnessHomes::default();
1658 match method {
1659 "harness.v1.harnesses.settings" => {
1660 let params = decode::<HarnessSettingsParams>(params)?;
1661 let report = crate::inspect_harness_interop_settings(&homes, ¶ms.harness)
1662 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1663 serde_json::to_value(report)
1664 .map_err(|error| ServiceError::Operation(error.to_string()))
1665 }
1666 "harness.v1.harnesses.configure" => {
1667 let params = decode::<ConfigureHarnessParams>(params)?;
1668 let report = crate::configure_harness_interop_settings(
1669 &homes,
1670 ¶ms.harness,
1671 ¶ms.changes,
1672 params.expected_revision.as_deref(),
1673 )
1674 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1675 serde_json::to_value(report)
1676 .map_err(|error| ServiceError::Operation(error.to_string()))
1677 }
1678 _ => Err(ServiceError::MethodNotFound),
1679 }
1680 }
1681
1682 fn insert_runtime(
1683 &mut self,
1684 runtime: Box<dyn RuntimeConnection>,
1685 ) -> std::result::Result<Value, ServiceError> {
1686 let connection = format!("runtime-{}", self.next_runtime);
1687 self.next_runtime += 1;
1688 let handle = runtime.handle().clone();
1689 self.runtime_sequences
1690 .entry(handle.runtime_id.clone())
1691 .or_insert(0);
1692 self.runtimes.insert(connection.clone(), runtime);
1693 Ok(json!({"connection": connection, "handle": handle}))
1694 }
1695
1696 #[cfg(feature = "adapter-api")]
1697 async fn insert_hosted_runtime(
1698 &mut self,
1699 runtime: Box<dyn RuntimeConnection>,
1700 capabilities: crate::RuntimeCapabilities,
1701 workspace: PathBuf,
1702 ) -> std::result::Result<Value, ServiceError> {
1703 let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1704 let token: std::sync::Arc<str> = crate::server::generate_token().into();
1705 let server = crate::server::run_frontend_http(
1706 host.clone(),
1707 host.frontend_sender(),
1708 "127.0.0.1:0",
1709 token.clone(),
1710 )
1711 .await
1712 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1713 let source = LiveRuntimeSource {
1714 harness: connection.handle().harness.as_str().to_string(),
1715 session_id: connection.handle().runtime_id.clone(),
1716 workspace: workspace.clone(),
1717 };
1718 let registration = register_live_runtime(
1719 connection.handle().runtime_id.clone(),
1720 source.clone(),
1721 format!("http://{}", server.address()),
1722 token.to_string(),
1723 )
1724 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1725 let endpoint = registration.endpoint().to_string();
1726 let launch = StructuredLaunch {
1727 cwd: workspace,
1728 program: std::env::current_exe()
1732 .ok()
1733 .map(|path| path.to_string_lossy().into_owned())
1734 .unwrap_or_else(|| "supercode".into()),
1735 arguments: vec![
1736 "harness".into(),
1737 "attach".into(),
1738 "--endpoint".into(),
1739 endpoint,
1740 "--harness".into(),
1741 source.harness,
1742 "--session".into(),
1743 source.session_id,
1744 ],
1745 env: BTreeMap::new(),
1746 };
1747 let lease = HostedRuntimeLease {
1748 connection,
1749 _host: host,
1750 _registration: registration,
1751 _server: server,
1752 };
1753 let opened = self.insert_runtime(Box::new(lease))?;
1754 let connection_id = opened["connection"]
1755 .as_str()
1756 .expect("insert_runtime returns a connection id")
1757 .to_string();
1758 self.terminal_launches.insert(connection_id, launch);
1759 Ok(opened)
1760 }
1761
1762 #[cfg(not(feature = "adapter-api"))]
1763 async fn insert_hosted_runtime(
1764 &mut self,
1765 runtime: Box<dyn RuntimeConnection>,
1766 _capabilities: crate::RuntimeCapabilities,
1767 _workspace: PathBuf,
1768 ) -> std::result::Result<Value, ServiceError> {
1769 self.insert_runtime(runtime)
1770 }
1771
1772 fn runtime_mut(
1773 &mut self,
1774 connection: &str,
1775 ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1776 self.runtimes.get_mut(connection).ok_or_else(|| {
1777 ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1778 })
1779 }
1780
1781 async fn mutate_session(
1792 &mut self,
1793 verb: crate::SessionVerb,
1794 params: Value,
1795 ) -> std::result::Result<Value, ServiceError> {
1796 let mutation = decode::<crate::SessionMutation>(params)?;
1797 let door = crate::sessions_control::door(&mutation.harness, verb)
1798 .map_err(session_control_error)?;
1799 let outcome = match door {
1800 #[cfg(not(feature = "adapter-api"))]
1804 crate::SessionDoor::Live(command) => {
1805 return Err(ServiceError::Operation(format!(
1806 "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
1807 session, which needs this build's `adapter-api` feature",
1808 mutation.harness,
1809 verb.as_str()
1810 )));
1811 }
1812 #[cfg(feature = "adapter-api")]
1813 crate::SessionDoor::Live(command) => {
1814 let connection = mutation
1815 .connection
1816 .clone()
1817 .filter(|value| !value.trim().is_empty())
1818 .ok_or_else(|| {
1819 ServiceError::InvalidParams(format!(
1820 "`{}` performs `sessions.{}` by typing `{command}` into a live \
1821 driven session: pass the `connection` of an open runtime \
1822 (`harness.v1.runtimes.start`)",
1823 mutation.harness,
1824 verb.as_str()
1825 ))
1826 })?;
1827 let runtime = self.runtime_mut(&connection)?;
1828 let session = mutation
1829 .session
1830 .clone()
1831 .filter(|value| !value.trim().is_empty())
1832 .unwrap_or_else(|| runtime.handle().runtime_id.clone());
1833 runtime
1834 .send_input(RuntimeInput {
1835 text: command.to_string(),
1836 image_urls: Vec::new(),
1837 })
1838 .await
1839 .map_err(operation)?;
1840 crate::sessions_control::live_outcome(verb, &mutation, command, session)
1841 .map_err(session_control_error)?
1842 }
1843 _ => crate::sessions_control::mutate(verb, &mutation)
1844 .await
1845 .map_err(session_control_error)?,
1846 };
1847 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
1848 }
1849
1850 async fn inventory_call(
1851 &self,
1852 method: &str,
1853 params: Value,
1854 ) -> std::result::Result<Value, ServiceError> {
1855 let mut params = decode::<HarnessInventoryParams>(params)?;
1856 if method == "harness.v1.harnesses.probe" {
1857 let harness = params.harness.take().ok_or_else(|| {
1858 ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
1859 })?;
1860 params.harnesses = vec![harness];
1861 }
1862 let selected = params
1863 .harnesses
1864 .iter()
1865 .map(HarnessId::as_str)
1866 .collect::<std::collections::BTreeSet<_>>();
1867 let supported = harness_support_registry()
1868 .harnesses
1869 .into_iter()
1870 .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
1871 .collect::<Vec<_>>();
1872 if !params.harnesses.is_empty() && supported.len() != selected.len() {
1873 let known = supported
1874 .iter()
1875 .map(|harness| harness.id.as_str())
1876 .collect::<std::collections::BTreeSet<_>>();
1877 let missing = params
1878 .harnesses
1879 .iter()
1880 .filter(|id| !known.contains(id.as_str()))
1881 .map(HarnessId::as_str)
1882 .collect::<Vec<_>>();
1883 return Err(ServiceError::InvalidParams(format!(
1884 "unknown harness(es): {}",
1885 missing.join(", ")
1886 )));
1887 }
1888 let global_counts = params
1889 .include_sessions
1890 .then(|| self.session_counts(None, ¶ms.harnesses));
1891 let workspace_counts = params.include_sessions.then(|| {
1892 params
1893 .workspace
1894 .as_deref()
1895 .map(|workspace| self.session_counts(Some(workspace), ¶ms.harnesses))
1896 });
1897 let probes = supported.into_iter().map(|descriptor| {
1898 let global = global_counts
1899 .as_ref()
1900 .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
1901 let workspace = workspace_counts
1902 .as_ref()
1903 .and_then(Option::as_ref)
1904 .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
1905 self.probe_harness(descriptor, ¶ms, global, workspace)
1906 });
1907 let harnesses = futures::future::join_all(probes).await;
1908 serde_json::to_value(HarnessInventoryReport {
1909 probe: params.probe,
1910 workspace: params.workspace,
1911 harnesses,
1912 })
1913 .map_err(|error| ServiceError::Operation(error.to_string()))
1914 }
1915
1916 #[cfg(feature = "adapter-api")]
1917 async fn harness_authentication_call(
1918 &self,
1919 method: &str,
1920 params: Value,
1921 ) -> std::result::Result<Value, ServiceError> {
1922 match method {
1923 "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
1924 let params = decode::<HarnessAuthenticationParams>(params)?;
1925 serde_json::to_value(crate::inspect_harness_authentication(¶ms.harness).await)
1926 .map_err(|error| ServiceError::Operation(error.to_string()))
1927 }
1928 "harness.v1.harnesses.auth.begin" => {
1929 let params = decode::<BeginHarnessAuthenticationParams>(params)?;
1930 let cwd = params
1931 .cwd
1932 .or_else(|| std::env::current_dir().ok())
1933 .unwrap_or_else(|| PathBuf::from("."));
1934 let plan = crate::harness_authentication_plan(
1935 ¶ms.harness,
1936 params.environment,
1937 params.method,
1938 &cwd,
1939 )
1940 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
1941 serde_json::to_value(plan)
1942 .map_err(|error| ServiceError::Operation(error.to_string()))
1943 }
1944 _ => Err(ServiceError::MethodNotFound),
1945 }
1946 }
1947
1948 async fn probe_harness(
1949 &self,
1950 descriptor: crate::HarnessSupportDescriptor,
1951 params: &HarnessInventoryParams,
1952 global: Option<usize>,
1953 workspace: Option<usize>,
1954 ) -> LocalHarness {
1955 let launch = descriptor.runtime.default_launch.as_ref();
1956 let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
1961 .then(crate::orchestrator::daemon_entry)
1962 .and_then(Result::ok);
1963 let executable = match &orchestrator_entry {
1964 Some(entry) => Some(entry.clone()),
1965 None => launch.and_then(|launch| find_executable(&launch.program)),
1966 };
1967 let installed = executable.is_some();
1968 let version = if params.skip_versions || orchestrator_entry.is_some() {
1969 None
1972 } else {
1973 match executable.as_deref() {
1974 Some(path) => executable_version(path).await,
1975 None => None,
1976 }
1977 };
1978 let configured = auth_evidence(descriptor.id.as_str());
1979 let mut auth = if configured {
1980 HarnessAuthState::Configured
1981 } else if matches!(
1982 descriptor.id.as_str(),
1983 HarnessId::CLAUDE_CODE | HarnessId::CODEX
1984 ) {
1985 HarnessAuthState::Required
1990 } else {
1991 HarnessAuthState::Unknown
1992 };
1993 let mut runtime = if installed {
1994 HarnessRuntimeState::Degraded
1995 } else {
1996 HarnessRuntimeState::Unavailable
1997 };
1998 let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
1999 let mut reason = (!installed).then(|| {
2000 if is_orchestrator {
2001 format!(
2002 "{} is supported but its daemon entry `{}` was not found",
2003 descriptor.display_name,
2004 crate::orchestrator::DAEMON_ENTRY
2005 )
2006 } else {
2007 format!(
2008 "{} is supported but `{}` was not found on PATH",
2009 descriptor.display_name,
2010 launch
2011 .map(|launch| launch.program.as_str())
2012 .unwrap_or("executable")
2013 )
2014 }
2015 });
2016 let mut repair = (!installed).then(|| {
2017 if is_orchestrator {
2018 format!(
2019 "Install the `supercode-orchestrator` package so `{}` resolves.",
2020 crate::orchestrator::DAEMON_ENTRY
2021 )
2022 } else {
2023 format!(
2024 "Install {} and ensure `{}` is on PATH.",
2025 descriptor.display_name,
2026 launch
2027 .map(|launch| launch.program.as_str())
2028 .unwrap_or("its executable")
2029 )
2030 }
2031 });
2032
2033 if installed && params.probe == HarnessProbeLevel::Handshake {
2034 let backend_params = RuntimeBackendParams {
2035 harness: descriptor.id.clone(),
2036 protocol: None,
2037 launch: None,
2038 base_url: None,
2039 policy: RuntimePolicy::Default,
2040 };
2041 match runtime_backend(&backend_params) {
2042 Ok(backend) => {
2043 let cwd = params
2044 .workspace
2045 .clone()
2046 .or_else(|| std::env::current_dir().ok())
2047 .unwrap_or_else(|| PathBuf::from("."));
2048 let isolated = descriptor
2049 .runtime
2050 .default_launch
2051 .clone()
2052 .and_then(|launch| {
2053 IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok()
2054 });
2055 let Some(isolated) = isolated else {
2056 reason = Some(
2057 "No-prompt runtime handshake could not create its isolated harness home."
2058 .into(),
2059 );
2060 repair = Some(
2061 "Check temporary-directory permissions, then run the handshake probe again."
2062 .into(),
2063 );
2064 let running = probe_running_instance(descriptor.id.as_str());
2065 return LocalHarness {
2066 gateway: gateway_health(
2067 descriptor.id.as_str(),
2068 installed,
2069 running.as_ref(),
2070 version.as_deref(),
2071 ),
2072 id: descriptor.id,
2073 display_name: descriptor.display_name,
2074 supported: true,
2075 installed,
2076 executable: executable.map(|path| path.to_string_lossy().into_owned()),
2077 version,
2078 auth,
2079 runtime,
2080 protocol: descriptor.runtime.protocol,
2081 capabilities: descriptor.runtime.capabilities.clone(),
2082 effective_capabilities: descriptor.runtime.capabilities,
2083 sessions: HarnessSessionCounts { global, workspace },
2084 running,
2085 reason,
2086 repair,
2087 };
2088 };
2089 match tokio::time::timeout(
2090 Duration::from_secs(30),
2091 backend.start(RuntimeStartRequest {
2092 cwd,
2093 launch: Some(isolated.launch.clone()),
2094 mcp_servers: Vec::new(),
2095 }),
2096 )
2097 .await
2098 {
2099 Ok(Ok(mut connection)) => {
2100 match stabilize_handshake(connection.as_mut()).await {
2101 Ok(()) => {
2102 auth = HarnessAuthState::Ready;
2103 runtime = HarnessRuntimeState::Ready;
2104 reason = Some(
2105 "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2106 .into(),
2107 );
2108 repair = None;
2109 }
2110 Err(message) => {
2111 auth = if looks_like_auth_error(&message) {
2112 HarnessAuthState::Required
2113 } else if configured {
2114 HarnessAuthState::Configured
2115 } else {
2116 HarnessAuthState::Unknown
2117 };
2118 reason = Some(format!(
2119 "No-prompt runtime handshake became unhealthy during startup: {message}"
2120 ));
2121 repair = Some(if auth == HarnessAuthState::Required {
2122 format!(
2123 "Run `{}` interactively once and complete sign-in, then probe again.",
2124 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2125 )
2126 } else {
2127 "Run the harness directly to inspect its startup failure, then probe again."
2128 .into()
2129 });
2130 }
2131 }
2132 let _ =
2133 tokio::time::timeout(Duration::from_secs(3), connection.close())
2134 .await;
2135 }
2136 Ok(Err(error)) => {
2137 let message = truncate_text(&error.to_string(), 500);
2138 auth = if looks_like_auth_error(&message) {
2139 HarnessAuthState::Required
2140 } else if configured {
2141 HarnessAuthState::Configured
2142 } else {
2143 HarnessAuthState::Unknown
2144 };
2145 reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2146 repair = Some(if auth == HarnessAuthState::Required {
2147 format!(
2148 "Run `{}` interactively once and complete sign-in, then probe again.",
2149 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2150 )
2151 } else {
2152 "Check the harness installation and run the handshake probe again."
2153 .into()
2154 });
2155 }
2156 Err(_) => {
2157 reason = Some(
2158 "No-prompt runtime handshake timed out after 30 seconds.".into(),
2159 );
2160 repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2161 }
2162 }
2163 let _ = isolated.cleanup();
2172 tokio::time::sleep(Duration::from_millis(250)).await;
2173 if let Err(error) = isolated.cleanup() {
2174 auth = if configured {
2175 HarnessAuthState::Configured
2176 } else {
2177 HarnessAuthState::Unknown
2178 };
2179 runtime = HarnessRuntimeState::Degraded;
2180 reason = Some(format!(
2181 "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2182 ));
2183 repair = Some(
2184 "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2185 .into(),
2186 );
2187 }
2188 }
2189 Err(error) => {
2190 reason = Some(error_message(error));
2191 }
2192 }
2193 } else if installed && configured {
2194 reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2195 } else if installed && auth == HarnessAuthState::Required {
2196 reason =
2197 Some("Executable found, but no native authentication evidence is present.".into());
2198 repair = Some(format!(
2199 "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2200 descriptor.id.as_str()
2201 ));
2202 } else if installed {
2203 reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2204 repair =
2205 Some(format!(
2206 "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2207 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2208 ));
2209 }
2210
2211 let effective_capabilities = if installed {
2212 descriptor.runtime.capabilities.clone()
2213 } else {
2214 unavailable_capabilities()
2215 };
2216 let running = probe_running_instance(descriptor.id.as_str());
2217 LocalHarness {
2218 gateway: gateway_health(
2219 descriptor.id.as_str(),
2220 installed,
2221 running.as_ref(),
2222 version.as_deref(),
2223 ),
2224 id: descriptor.id,
2225 display_name: descriptor.display_name,
2226 supported: true,
2227 installed,
2228 executable: executable.map(|path| path.to_string_lossy().into_owned()),
2229 version,
2230 auth,
2231 runtime,
2232 protocol: descriptor.runtime.protocol,
2233 capabilities: descriptor.runtime.capabilities,
2234 effective_capabilities,
2235 sessions: HarnessSessionCounts { global, workspace },
2236 running,
2237 reason,
2238 repair,
2239 }
2240 }
2241
2242 fn session_counts(
2243 &self,
2244 workspace: Option<&Path>,
2245 harnesses: &[HarnessId],
2246 ) -> BTreeMap<String, usize> {
2247 let mut counts = BTreeMap::new();
2248 for session in self
2249 .catalog
2250 .discover(&DiscoveryQuery {
2251 workspace: workspace.map(Path::to_path_buf),
2252 harnesses: harnesses.to_vec(),
2253 ..DiscoveryQuery::default()
2254 })
2255 .unwrap_or_default()
2256 {
2257 *counts
2258 .entry(session.locator.harness.as_str().to_string())
2259 .or_insert(0) += 1;
2260 }
2261 counts
2262 }
2263}
2264
2265#[async_trait::async_trait]
2266impl SdkService for HarnessSessionService {
2267 fn capabilities(&self) -> SdkCapabilities {
2268 SdkCapabilities::default()
2269 }
2270
2271 async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2272 if request.operation == SdkOperation::Events {
2273 let events = self
2274 .poll_sdk_events()
2275 .await
2276 .into_iter()
2277 .map(|(_, event)| event)
2278 .collect::<Vec<_>>();
2279 return serde_json::to_value(events).map_err(|error| {
2280 SdkError::new(
2281 SdkErrorCode::Execution,
2282 request.operation,
2283 error.to_string(),
2284 )
2285 });
2286 }
2287 if self.runtimes.is_empty()
2288 && matches!(
2289 request.operation,
2290 SdkOperation::Input
2291 | SdkOperation::Interrupt
2292 | SdkOperation::Steer
2293 | SdkOperation::Respond
2294 | SdkOperation::Close
2295 )
2296 {
2297 return Err(SdkError::unsupported(request.operation));
2298 }
2299 let method = request
2300 .operation
2301 .method()
2302 .ok_or_else(|| SdkError::unsupported(request.operation))?;
2303 let result = match request.operation {
2304 SdkOperation::Discover
2305 | SdkOperation::Load
2306 | SdkOperation::Export
2307 | SdkOperation::ProfilesList
2308 | SdkOperation::ProfilesGet
2309 | SdkOperation::ProfilesCreate
2310 | SdkOperation::ProfilesDelete
2311 | SdkOperation::SkillsList
2312 | SdkOperation::SkillsInstall
2313 | SdkOperation::SkillsRemove
2314 | SdkOperation::ChannelsList
2315 | SdkOperation::RoutesList
2316 | SdkOperation::TriggersList
2317 | SdkOperation::ChannelsStatus
2318 | SdkOperation::MemoryShow
2319 | SdkOperation::MemorySearch
2320 | SdkOperation::JobsList
2321 | SdkOperation::JobsGet
2322 | SdkOperation::JobsCreate
2323 | SdkOperation::JobsUpdate
2324 | SdkOperation::JobsPause
2325 | SdkOperation::JobsResume
2326 | SdkOperation::JobsRun
2327 | SdkOperation::JobsDelete
2328 | SdkOperation::RunsList
2329 | SdkOperation::RunsGet
2330 | SdkOperation::ApprovalsList
2331 | SdkOperation::OrchestrationLoad
2332 | SdkOperation::OrchestrationSave
2333 | SdkOperation::OrchestrationCompile
2334 | SdkOperation::OrchestrationDecompile
2335 | SdkOperation::OrchestrationImport
2336 | SdkOperation::OrchestrationExport
2337 | SdkOperation::WorkflowLoad => self.call(method, request.params),
2338 SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2341 SdkOperation::Start
2342 | SdkOperation::Resume
2343 | SdkOperation::Input
2344 | SdkOperation::Interrupt
2345 | SdkOperation::Steer
2346 | SdkOperation::Respond
2347 | SdkOperation::Close => self.runtime_call(method, request.params).await,
2348 SdkOperation::SessionsNew => {
2353 self.mutate_session(crate::SessionVerb::New, request.params)
2354 .await
2355 }
2356 SdkOperation::SessionsReset => {
2357 self.mutate_session(crate::SessionVerb::Reset, request.params)
2358 .await
2359 }
2360 SdkOperation::SessionsArchive => {
2361 self.mutate_session(crate::SessionVerb::Archive, request.params)
2362 .await
2363 }
2364 SdkOperation::SessionsDelete => {
2365 self.mutate_session(crate::SessionVerb::Delete, request.params)
2366 .await
2367 }
2368 SdkOperation::Events => unreachable!("handled before method dispatch"),
2369 };
2370 result.map_err(|error| sdk_error(request.operation, error))
2371 }
2372
2373 async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2374 Ok(self
2375 .poll_sdk_events()
2376 .await
2377 .into_iter()
2378 .map(|(_, event)| event)
2379 .collect())
2380 }
2381}
2382
2383#[cfg(feature = "adapter-api")]
2384struct HostedRuntimeLease {
2385 connection: HostedHarnessConnection,
2386 _host: std::sync::Arc<HostedHarnessRuntime>,
2387 _registration: LiveRuntimeRegistration,
2388 _server: crate::server::FrontendHttpServer,
2389}
2390
2391#[async_trait::async_trait]
2392#[cfg(feature = "adapter-api")]
2393impl RuntimeConnection for HostedRuntimeLease {
2394 fn handle(&self) -> &crate::RuntimeHandle {
2395 self.connection.handle()
2396 }
2397
2398 async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2399 self.connection.send_input(input).await
2400 }
2401
2402 async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2403 self.connection.next_event().await
2404 }
2405
2406 async fn interrupt(&mut self) -> crate::Result<()> {
2407 self.connection.interrupt().await
2408 }
2409
2410 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2411 self.connection.respond(request_id, response).await
2412 }
2413
2414 async fn close(&mut self) -> crate::Result<()> {
2415 self.connection.close().await
2416 }
2417}
2418
2419async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2420 let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2421 loop {
2422 let now = tokio::time::Instant::now();
2423 if now >= deadline {
2424 return Ok(());
2425 }
2426 match tokio::time::timeout(deadline - now, connection.next_event()).await {
2427 Err(_) => return Ok(()),
2428 Ok(Ok(Some(event))) => {
2429 if let Some(message) = handshake_event_failure(&event) {
2430 return Err(truncate_text(&message, 500));
2431 }
2432 }
2433 Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2434 Ok(Err(error)) => return Err(error.to_string()),
2435 }
2436 }
2437}
2438
2439fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2440 let detail = event
2441 .payload
2442 .get("message")
2443 .or_else(|| event.payload.get("line"))
2444 .and_then(Value::as_str)
2445 .unwrap_or(event.kind.as_str());
2446 match event.kind.as_str() {
2447 "transport_closed" => Some("runtime transport closed during startup".into()),
2448 "transport_error" => Some(format!("runtime transport error: {detail}")),
2449 "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2450 _ => None,
2455 }
2456}
2457
2458fn indexed_claude_window(
2459 locator: &SessionLocator,
2460 options: &SessionLoadOptions,
2461) -> std::result::Result<Option<Value>, ServiceError> {
2462 use supercode_interchange::session::ClaudeReadIndex;
2463 if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2466 || options.include_subagents != Some(false)
2467 {
2468 return Ok(None);
2469 }
2470 let crate::StorageLocator::File { path } = &locator.storage else {
2471 return Ok(None);
2472 };
2473 if !ClaudeReadIndex::supports(path)
2474 .map_err(|error| ServiceError::Operation(error.to_string()))?
2475 {
2476 return Ok(None);
2477 }
2478 let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2479 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2480 let total = index.len();
2481 let (offset, end) = projected_message_window(total, options);
2482 let session = index
2483 .read_messages(offset..end)
2484 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2485 let summary = index
2486 .read_summary()
2487 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2488 let selected_options = SessionLoadOptions {
2489 message_offset: None,
2490 message_limit: None,
2491 message_tail: None,
2492 ..options.clone()
2493 };
2494 let mut selected = projected_session_json(&session, &selected_options);
2495 selected["raw_record_count"] = json!(index.raw_record_count());
2496 Ok(Some(json!({
2497 "session": selected,
2498 "summary": projected_session_summary(&summary, options),
2499 "window": {
2500 "has_more": offset > 0 || end < total, "has_newer": end < total,
2501 "has_older": offset > 0, "newer_items": index.item_count(end..total),
2502 "offset": offset, "older_items": index.item_count(0..offset),
2503 "returned": end - offset, "total_messages": total,
2504 }
2505 })))
2506}
2507
2508fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2509 let total_messages = session.messages.len();
2510 let (offset, end) = projected_message_window(total_messages, options);
2511 json!({
2512 "session": projected_session_json(session, options),
2513 "summary": projected_session_summary(session, options),
2514 "window": {
2515 "has_more": offset > 0 || end < total_messages,
2516 "has_newer": end < total_messages,
2517 "has_older": offset > 0,
2518 "newer_items": normalized_item_count(&session.messages[end..]),
2519 "offset": offset,
2520 "older_items": normalized_item_count(&session.messages[..offset]),
2521 "returned": end.saturating_sub(offset),
2522 "total_messages": total_messages,
2523 }
2524 })
2525}
2526
2527fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2528 messages
2529 .iter()
2530 .map(|message| {
2531 let conversation = usize::from(
2532 matches!(message.role, Role::Assistant | Role::User)
2533 && message_has_content(message),
2534 );
2535 let tool_result =
2536 usize::from(message.role == Role::Tool && message_has_content(message));
2537 conversation + tool_result + message.tool_calls().len()
2538 })
2539 .sum()
2540}
2541
2542fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2543 let mut conversational = session.messages.iter().filter(|message| {
2544 matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2545 });
2546 let first_message = conversational.clone().next();
2547 let last_message = conversational.next_back();
2548 let mut assistant = session
2549 .messages
2550 .iter()
2551 .filter(|message| message.role == Role::Assistant && message_has_content(message));
2552 let first_assistant_message = assistant.clone().next();
2553 let last_assistant_message = assistant.next_back();
2554 let end_of_turn = session
2555 .messages
2556 .iter()
2557 .rev()
2558 .find(|message| message.role != Role::System)
2559 .is_some_and(|message| {
2560 message.role == Role::Assistant
2561 && message_has_content(message)
2562 && message.tool_calls().is_empty()
2563 });
2564 let project = |message: Option<&crate::ChatMessage>| {
2565 message.map(|message| project_inline_media(message_json(message), options))
2566 };
2567 json!({
2568 "end_of_turn": end_of_turn,
2569 "first_assistant_message": project(first_assistant_message),
2570 "first_message": project(first_message),
2571 "last_assistant_message": project(last_assistant_message),
2572 "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2573 "last_message": project(last_message),
2574 })
2575}
2576
2577fn message_has_content(message: &crate::ChatMessage) -> bool {
2578 message
2579 .content
2580 .as_deref()
2581 .is_some_and(|content| !content.trim().is_empty())
2582 || message
2583 .content_parts
2584 .as_ref()
2585 .is_some_and(|parts| !parts.is_empty())
2586}
2587
2588fn message_text(message: &crate::ChatMessage) -> String {
2589 if let Some(content) = &message.content {
2590 return content.clone();
2591 }
2592 message
2593 .content_parts
2594 .as_ref()
2595 .into_iter()
2596 .flatten()
2597 .filter_map(|part| part.get("text").and_then(Value::as_str))
2598 .collect::<Vec<_>>()
2599 .join("\n")
2600}
2601
2602fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2603 let (offset, end) = projected_message_window(session.messages.len(), options);
2604 let messages = session.messages[offset..end]
2605 .iter()
2606 .map(|message| project_inline_media(message_json(message), options))
2607 .collect::<Vec<_>>();
2608 let subagents = if options.include_subagents.unwrap_or(true) {
2609 let subagent_options = SessionLoadOptions {
2614 message_limit: None,
2615 message_offset: None,
2616 message_tail: None,
2617 ..options.clone()
2618 };
2619 session
2620 .subagents
2621 .iter()
2622 .map(|subagent| projected_session_json(subagent, &subagent_options))
2623 .collect::<Vec<_>>()
2624 } else {
2625 Vec::new()
2626 };
2627 json!({
2628 "source": match session.meta.source {
2629 SessionSource::ClaudeCode => "claude_code",
2630 SessionSource::Codex => "codex",
2631 SessionSource::Gemini => "gemini",
2632 SessionSource::Goose => "goose",
2633 SessionSource::Grok => "grok",
2634 SessionSource::Native => "native",
2635 SessionSource::OpenClaw => "openclaw",
2636 SessionSource::Hermes => "hermes",
2637 SessionSource::OpenCode => "opencode",
2638 SessionSource::Pi => "pi",
2639 },
2640 "session_id": session.meta.session_id,
2641 "ended_at": session.meta.ended_at,
2642 "end_reason": session.meta.end_reason,
2643 "model": session.meta.model,
2644 "cwd": session.meta.cwd,
2645 "system_prompt": session.meta.system_prompt,
2646 "agent_id": session.meta.agent_id,
2647 "parent_tool_use_id": session.meta.parent_tool_use_id,
2648 "lineage": session.meta.lineage,
2649 "messages": messages,
2650 "subagents": subagents,
2651 "raw_record_count": session.raw.len(),
2652 "parse_error_lines": session.parse_error_lines,
2653 })
2654}
2655
2656fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
2657 if let Some(tail) = options.message_tail {
2658 return (total.saturating_sub(tail), total);
2659 }
2660 let offset = options.message_offset.unwrap_or(0).min(total);
2661 let end = options
2662 .message_limit
2663 .map(|limit| offset.saturating_add(limit).min(total))
2664 .unwrap_or(total);
2665 (offset, end)
2666}
2667
2668fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
2669 let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
2670 return message;
2671 };
2672 for part in parts {
2673 let Some(url) = part
2674 .get("image_url")
2675 .and_then(|image| image.get("url"))
2676 .and_then(Value::as_str)
2677 else {
2678 continue;
2679 };
2680 let Some(rest) = url.strip_prefix("data:") else {
2681 continue;
2682 };
2683 let Some((media_type, encoded)) = rest.split_once(";base64,") else {
2684 continue;
2685 };
2686 let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
2687 let decoded_bytes = encoded.len().saturating_mul(3) / 4;
2688 let decoded_bytes = decoded_bytes.saturating_sub(padding);
2689 let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
2690 || options
2691 .max_inline_media_bytes
2692 .is_some_and(|limit| decoded_bytes > limit);
2693 if should_elide {
2694 *part = json!({
2695 "type": "media_reference",
2696 "media_type": media_type,
2697 "encoding": "base64",
2698 "encoded_bytes": encoded.len(),
2699 "decoded_bytes": decoded_bytes,
2700 "omitted": true,
2701 });
2702 }
2703 }
2704 message
2705}
2706
2707#[derive(Deserialize)]
2708struct LocatorParams {
2709 locator: SessionLocator,
2710 #[serde(default)]
2723 fidelity: Option<Fidelity>,
2724 #[serde(default)]
2727 view: Option<SessionReadView>,
2728}
2729
2730#[derive(Deserialize)]
2731struct SessionReadView {
2732 #[serde(default)]
2735 tail_messages: Option<usize>,
2736 #[serde(default)]
2739 include_subagents: bool,
2740 #[serde(default)]
2742 display_history: bool,
2743 #[serde(default)]
2746 max_message_chars: Option<usize>,
2747}
2748
2749impl LocatorParams {
2750 fn read_fidelity(&self) -> Fidelity {
2751 self.fidelity.unwrap_or(Fidelity::Semantic)
2752 }
2753
2754 fn include_subagents(&self) -> bool {
2755 self.view
2756 .as_ref()
2757 .map(|view| view.include_subagents)
2758 .unwrap_or(true)
2759 }
2760
2761 fn tail_messages(&self) -> Option<usize> {
2762 self.view
2763 .as_ref()
2764 .and_then(|view| view.tail_messages)
2765 .map(|limit| limit.clamp(1, 5_000))
2766 }
2767
2768 fn display_history(&self) -> bool {
2769 self.view.as_ref().is_some_and(|view| view.display_history)
2770 }
2771
2772 fn max_message_chars(&self) -> Option<usize> {
2773 self.view
2774 .as_ref()
2775 .and_then(|view| view.max_message_chars)
2776 .map(|limit| limit.clamp(256, 64_000))
2777 }
2778
2779 fn bound_session(&self, session: &mut Session) {
2780 bound_session_view(session, self.tail_messages(), self.max_message_chars());
2781 }
2782}
2783
2784#[derive(Debug, Clone, Copy, Default, Deserialize)]
2785#[serde(rename_all = "snake_case")]
2786enum InlineMediaMode {
2787 #[default]
2788 Full,
2789 Metadata,
2790}
2791
2792#[derive(Debug, Clone, Default, Deserialize)]
2793#[serde(default)]
2794struct SessionLoadOptions {
2795 include_subagents: Option<bool>,
2796 inline_media: InlineMediaMode,
2797 max_inline_media_bytes: Option<usize>,
2798 message_limit: Option<usize>,
2799 message_offset: Option<usize>,
2800 message_tail: Option<usize>,
2801}
2802
2803impl SessionLoadOptions {
2804 fn validate(&self) -> std::result::Result<(), ServiceError> {
2805 if self.message_tail.is_some()
2806 && (self.message_limit.is_some() || self.message_offset.is_some())
2807 {
2808 return Err(ServiceError::InvalidParams(
2809 "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
2810 .into(),
2811 ));
2812 }
2813 Ok(())
2814 }
2815}
2816
2817#[derive(Deserialize)]
2818struct LoadSessionParams {
2819 #[serde(flatten)]
2820 read: LocatorParams,
2821 #[serde(default)]
2822 options: Option<SessionLoadOptions>,
2823}
2824
2825#[derive(Deserialize)]
2826struct UnfollowParams {
2827 subscription: String,
2828}
2829
2830#[derive(Debug, Deserialize)]
2831#[serde(deny_unknown_fields)]
2832struct IndexResizeParams {
2833 subscription: String,
2834 limit: usize,
2835}
2836
2837#[derive(Deserialize)]
2838struct ActivitySubscribeParams {
2839 locators: Vec<SessionLocator>,
2840 #[serde(default)]
2841 homes: crate::HarnessHomes,
2842}
2843
2844#[derive(Deserialize)]
2845struct MessageSessionParams {
2846 locator: SessionLocator,
2847 text: String,
2848 #[serde(default)]
2851 homes: crate::HarnessHomes,
2852}
2853
2854#[derive(Deserialize)]
2855#[serde(deny_unknown_fields)]
2856struct HarnessSettingsParams {
2857 harness: String,
2858}
2859
2860#[derive(Deserialize)]
2861#[serde(deny_unknown_fields)]
2862struct ConfigureHarnessParams {
2863 harness: String,
2864 #[serde(default)]
2865 changes: Vec<crate::HarnessSettingChange>,
2866 #[serde(default)]
2867 expected_revision: Option<String>,
2868}
2869
2870fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
2871 match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
2872 Ok(report) => (
2873 serde_json::to_value(report).unwrap_or(Value::Null),
2874 Value::Null,
2875 ),
2876 Err(error) => (
2877 Value::Null,
2878 Value::String(format!(
2879 "Supercode could not inspect Claude Code inbound controls: {error}"
2880 )),
2881 ),
2882 }
2883}
2884
2885#[cfg(feature = "adapter-api")]
2897async fn message_live_session(
2898 params: &MessageSessionParams,
2899 runner: &dyn crate::claude_peer::CourierRunner,
2900) -> Value {
2901 if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
2902 return json!({
2903 "delivered_to_bus": false,
2904 "refusal": {
2905 "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
2906 "message": format!(
2907 "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
2908 params.locator.harness.as_str()
2909 ),
2910 },
2911 });
2912 }
2913 let (inbound_controls, inbound_controls_error) =
2914 claude_inbound_controls_or_error(¶ms.homes);
2915 match crate::claude_peer::message_claude_peer(
2916 ¶ms.homes,
2917 ¶ms.locator.session_id,
2918 ¶ms.text,
2919 runner,
2920 )
2921 .await
2922 {
2923 Ok(delivery) => json!({
2924 "delivered_to_bus": true,
2925 "target": {
2926 "session_id": delivery.target.session_id,
2927 "name": delivery.target.name,
2928 "pid": delivery.target.pid,
2929 "cwd": delivery.target.cwd,
2930 "status": delivery.target.status.map(|status| status.as_str()),
2931 },
2932 "courier": {
2933 "model": crate::claude_peer::COURIER_MODEL,
2934 "report": delivery.courier_report,
2935 },
2936 "inbound_controls": inbound_controls,
2937 "inbound_controls_error": inbound_controls_error,
2938 }),
2939 Err(refusal) => json!({
2940 "delivered_to_bus": false,
2941 "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
2942 "inbound_controls": inbound_controls,
2943 "inbound_controls_error": inbound_controls_error,
2944 }),
2945 }
2946}
2947
2948#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
2953struct FollowedSource {
2954 harness: String,
2955 session_id: String,
2956 reported: Option<String>,
2957}
2958
2959#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
2960struct ActivitySubscription {
2961 locators: Vec<SessionLocator>,
2962 homes: crate::HarnessHomes,
2963 reported: BTreeMap<(String, String), crate::SessionActivity>,
2964}
2965
2966fn peers_for_descriptors(
2967 descriptors: &[SessionDescriptor],
2968 homes: &HarnessHomes,
2969) -> Vec<crate::claude_peer::ClaudePeerSession> {
2970 if descriptors
2971 .iter()
2972 .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
2973 {
2974 crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
2975 } else {
2976 Vec::new()
2977 }
2978}
2979
2980fn live_descriptor_value(
2986 session: &SessionDescriptor,
2987 peers: &[crate::claude_peer::ClaudePeerSession],
2988) -> std::result::Result<Value, ServiceError> {
2989 let mut value = serde_json::to_value(session)
2990 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2991 if let Some(workspace) = &session.cwd {
2992 let source = LiveRuntimeSource {
2993 harness: session.locator.harness.as_str().to_string(),
2994 session_id: session.locator.session_id.clone(),
2995 workspace: workspace.clone(),
2996 };
2997 if let Some(endpoint) = discover_live_runtime(&source)
2998 .map_err(|error| ServiceError::Operation(error.to_string()))?
2999 {
3000 value["live_endpoint"] = json!(endpoint.as_str());
3001 }
3002 }
3003 if value.get("live_endpoint").is_none() {
3004 if let Some(peer) = peers.iter().find(|peer| {
3005 session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3006 && peer.session_id == session.locator.session_id
3007 }) {
3008 value["live_endpoint"] = json!(peer.endpoint().as_str());
3009 }
3010 }
3011 Ok(value)
3012}
3013
3014fn live_index_changes(
3015 changes: Vec<crate::session_index::SessionIndexChange>,
3016 homes: &HarnessHomes,
3017) -> std::result::Result<Vec<Value>, ServiceError> {
3018 use crate::session_index::SessionIndexChange;
3019 let has_claude = changes.iter().any(|change| match change {
3020 SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3021 descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3022 }
3023 SessionIndexChange::Removed { .. } => false,
3024 });
3025 let peers = if has_claude {
3026 crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3027 } else {
3028 Vec::new()
3029 };
3030 changes
3031 .into_iter()
3032 .map(|change| match change {
3033 SessionIndexChange::Added { descriptor } => Ok(json!({
3034 "kind": "added",
3035 "descriptor": live_descriptor_value(&descriptor, &peers)?,
3036 })),
3037 SessionIndexChange::Updated { descriptor } => Ok(json!({
3038 "kind": "updated",
3039 "descriptor": live_descriptor_value(&descriptor, &peers)?,
3040 })),
3041 SessionIndexChange::Removed { key } => Ok(json!({
3042 "kind": "removed",
3043 "key": key,
3044 })),
3045 })
3046 .collect()
3047}
3048
3049fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3050 use crate::{SessionPresence, SessionTurnState};
3051 match (activity.presence, activity.turn) {
3052 (SessionPresence::Persisted, _) => None,
3053 (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3054 (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3055 (SessionPresence::Running, SessionTurnState::Unknown)
3059 if activity.evidence.native_state.is_none() =>
3060 {
3061 None
3062 }
3063 (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3064 }
3065}
3066
3067#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3068#[serde(rename_all = "kebab-case")]
3069enum TransferFormat {
3070 ClaudeCode,
3071 Codex,
3072 #[serde(rename = "opencode", alias = "open-code")]
3073 OpenCode,
3074 Pi,
3075 Grok,
3076 Gemini,
3077 Goose,
3078 Hermes,
3082}
3083
3084impl TransferFormat {
3085 fn id(self) -> &'static str {
3086 match self {
3087 Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3088 Self::Codex => HarnessId::CODEX,
3089 Self::OpenCode => HarnessId::OPENCODE,
3090 Self::Pi => HarnessId::PI,
3091 Self::Grok => HarnessId::GROK,
3092 Self::Gemini => HarnessId::GEMINI,
3093 Self::Goose => HarnessId::GOOSE,
3094 Self::Hermes => HarnessId::HERMES,
3095 }
3096 }
3097}
3098
3099impl From<TransferFormat> for SessionFormat {
3100 fn from(value: TransferFormat) -> Self {
3101 match value {
3102 TransferFormat::ClaudeCode => Self::ClaudeCode,
3103 TransferFormat::Codex => Self::Codex,
3104 TransferFormat::OpenCode => Self::OpenCode,
3105 TransferFormat::Pi => Self::Pi,
3106 TransferFormat::Grok => Self::Grok,
3107 TransferFormat::Gemini => Self::Gemini,
3108 TransferFormat::Goose => Self::Goose,
3109 TransferFormat::Hermes => Self::Codex,
3111 }
3112 }
3113}
3114
3115#[derive(Deserialize)]
3116struct ImportSessionParams {
3117 source_harness: TransferFormat,
3118 content: String,
3119}
3120
3121#[derive(Deserialize)]
3122struct ExportSessionParams {
3123 locator: SessionLocator,
3124 target_harness: TransferFormat,
3125}
3126
3127#[derive(Deserialize)]
3128struct ReduceSessionParams {
3129 locator: SessionLocator,
3130 target_harness: TransferFormat,
3131 #[serde(default = "default_keep_last")]
3132 keep_last: usize,
3133}
3134
3135fn default_keep_last() -> usize {
3136 6
3137}
3138
3139#[derive(Deserialize)]
3140struct BranchSessionParams {
3141 locator: SessionLocator,
3142 #[serde(default)]
3143 target_harness: Option<TransferFormat>,
3144}
3145
3146#[derive(Deserialize)]
3147struct HandoffSessionParams {
3148 locator: SessionLocator,
3149 target_harness: TransferFormat,
3150 #[serde(default)]
3151 cwd: Option<PathBuf>,
3152}
3153
3154#[derive(Debug, Clone, Copy, Default, Deserialize)]
3155#[serde(rename_all = "snake_case")]
3156enum ResumePolicy {
3157 #[default]
3158 Default,
3159 Yolo,
3160}
3161
3162#[derive(Deserialize)]
3163struct ResumeInstructionsParams {
3164 locator: SessionLocator,
3165 #[serde(default)]
3166 cwd: Option<PathBuf>,
3167 #[serde(default)]
3168 policy: ResumePolicy,
3169}
3170
3171#[derive(Deserialize)]
3173struct WorkflowLoadParams {
3174 from: crate::workflow_doors::WorkflowHarness,
3175 home: PathBuf,
3176}
3177
3178#[derive(Deserialize)]
3181struct OrchestrationLoadParams {
3182 root: PathBuf,
3183 #[serde(default)]
3184 flavor: crate::orchestration_doors::HomeFlavor,
3185}
3186
3187#[derive(Deserialize)]
3190struct OrchestrationSaveParams {
3191 root: PathBuf,
3192 orchestration: crate::orchestration::Orchestration,
3193 #[serde(default)]
3194 vault: BTreeMap<String, String>,
3195}
3196
3197#[derive(Deserialize)]
3199struct OrchestrationCompileParams {
3200 from: crate::orchestration_doors::OrchestrationHarness,
3201 home: PathBuf,
3202}
3203
3204#[derive(Deserialize)]
3208struct OrchestrationDecompileParams {
3209 to: crate::orchestration_doors::OrchestrationHarness,
3210 orchestration: crate::orchestration::Orchestration,
3211 source: PathBuf,
3212 #[serde(default)]
3213 source_flavor: crate::orchestration_doors::SourceFlavor,
3214 dest: PathBuf,
3215 #[serde(default)]
3216 vault: BTreeMap<String, String>,
3217}
3218
3219#[derive(Deserialize)]
3222struct OrchestrationImportParams {
3223 from: crate::orchestration_doors::OrchestrationHarness,
3224 home: PathBuf,
3225 into: PathBuf,
3226}
3227
3228#[derive(Deserialize)]
3231struct OrchestrationExportParams {
3232 to: crate::orchestration_doors::OrchestrationHarness,
3233 root: PathBuf,
3234 dest: PathBuf,
3235}
3236
3237#[derive(Deserialize)]
3239struct JobsGetParams {
3240 harness: String,
3241 id: String,
3242 #[serde(default)]
3243 homes: crate::HarnessHomes,
3244}
3245
3246fn mutate_job(
3254 verb: crate::jobs_control::JobVerb,
3255 params: Value,
3256) -> std::result::Result<Value, ServiceError> {
3257 let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3258 refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3259 let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3260 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3261}
3262
3263fn mutate_skill(
3270 verb: crate::skills_control::SkillVerb,
3271 params: Value,
3272) -> std::result::Result<Value, ServiceError> {
3273 let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3274 if !crate::skills_control::supports_skill_control(&mutation.harness) {
3275 return Err(ServiceError::UnsupportedAction(format!(
3276 "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3277 mutation.harness,
3278 verb.as_str(),
3279 crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3280 )));
3281 }
3282 let outcome =
3283 crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3284 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3285}
3286
3287fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3289 match error {
3290 crate::skills_control::SkillControlError::Unsupported(message) => {
3291 ServiceError::UnsupportedAction(message)
3292 }
3293 crate::skills_control::SkillControlError::Invalid(message) => {
3294 ServiceError::InvalidParams(message)
3295 }
3296 crate::skills_control::SkillControlError::Failed(message) => {
3297 ServiceError::Operation(message)
3298 }
3299 }
3300}
3301
3302fn mutate_profile(
3310 verb: crate::profiles_control::ProfileVerb,
3311 params: Value,
3312) -> std::result::Result<Value, ServiceError> {
3313 let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3314 let outcome =
3315 crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3316 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3317}
3318
3319fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3321 match error {
3322 crate::profiles_control::ProfileControlError::Unsupported(message) => {
3323 ServiceError::UnsupportedAction(message)
3324 }
3325 crate::profiles_control::ProfileControlError::Invalid(message) => {
3326 ServiceError::InvalidParams(message)
3327 }
3328 crate::profiles_control::ProfileControlError::Failed(message) => {
3329 ServiceError::Operation(message)
3330 }
3331 }
3332}
3333
3334fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3338 match error {
3339 crate::jobs_control::JobControlError::Unsupported(message) => {
3340 ServiceError::UnsupportedAction(message)
3341 }
3342 crate::jobs_control::JobControlError::Invalid(message) => {
3343 ServiceError::InvalidParams(message)
3344 }
3345 crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3346 }
3347}
3348
3349fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3354 match error {
3355 crate::SessionControlError::Unsupported(message) => {
3356 ServiceError::UnsupportedAction(message)
3357 }
3358 crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3359 crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3360 }
3361}
3362
3363fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3368 if crate::jobs::supports_jobs(harness) {
3369 return Ok(());
3370 }
3371 Err(ServiceError::UnsupportedAction(format!(
3372 "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3373 crate::jobs::JOB_HARNESSES.join(", ")
3374 )))
3375}
3376
3377#[derive(Deserialize)]
3379struct RunsGetParams {
3380 harness: String,
3381 id: String,
3382 #[serde(default)]
3383 homes: crate::HarnessHomes,
3384}
3385
3386fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3391 if crate::runs::supports_runs(harness) {
3392 return Ok(());
3393 }
3394 Err(ServiceError::UnsupportedAction(format!(
3395 "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3396 crate::runs::RUN_HARNESSES.join(", ")
3397 )))
3398}
3399
3400#[derive(Serialize)]
3401struct SessionArtifact {
3402 source_harness: HarnessId,
3403 target_harness: &'static str,
3404 session_id: Option<String>,
3405 content: String,
3406 suggested_filename: String,
3407 files: Vec<SessionArtifactFile>,
3408 fidelity: Fidelity,
3409 residue: Vec<String>,
3410}
3411
3412#[derive(Serialize)]
3413struct SessionArtifactFile {
3414 path: String,
3415 content: String,
3416 role: ArtifactFileRole,
3417}
3418
3419#[derive(Serialize)]
3420#[serde(rename_all = "snake_case")]
3421enum ArtifactFileRole {
3422 Primary,
3423 Subagent,
3424 Bundle,
3425 SourceRecovery,
3426}
3427
3428#[derive(Serialize)]
3429struct StructuredLaunch {
3430 cwd: PathBuf,
3431 program: String,
3432 arguments: Vec<String>,
3433 env: BTreeMap<String, String>,
3434}
3435
3436struct HandoffInstructions {
3437 launch: StructuredLaunch,
3438 materialize: Option<StructuredLaunch>,
3439 requires_materialization: bool,
3440 note: String,
3441}
3442
3443#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3444#[serde(rename_all = "snake_case")]
3445enum HarnessProbeLevel {
3446 #[default]
3447 Passive,
3448 Handshake,
3449}
3450
3451#[derive(Default, Deserialize)]
3452#[serde(default)]
3453struct HarnessInventoryParams {
3454 harness: Option<HarnessId>,
3455 harnesses: Vec<HarnessId>,
3456 workspace: Option<PathBuf>,
3457 probe: HarnessProbeLevel,
3458 include_sessions: bool,
3459 skip_versions: bool,
3461}
3462
3463#[derive(Deserialize)]
3464struct HarnessAuthenticationParams {
3465 harness: HarnessId,
3466}
3467
3468#[derive(Deserialize)]
3469struct BeginHarnessAuthenticationParams {
3470 harness: HarnessId,
3471 #[serde(default = "local_browser_authentication_environment")]
3472 environment: crate::HarnessAuthenticationEnvironment,
3473 #[serde(default)]
3474 method: Option<crate::HarnessAuthenticationMethodId>,
3475 #[serde(default)]
3476 cwd: Option<PathBuf>,
3477}
3478
3479fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3480 crate::HarnessAuthenticationEnvironment::LocalBrowser
3481}
3482
3483#[derive(Serialize)]
3484struct HarnessInventoryReport {
3485 probe: HarnessProbeLevel,
3486 workspace: Option<PathBuf>,
3487 harnesses: Vec<LocalHarness>,
3488}
3489
3490#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3491#[serde(rename_all = "snake_case")]
3492enum HarnessAuthState {
3493 Ready,
3494 Configured,
3495 Required,
3496 Unknown,
3497}
3498
3499#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3500#[serde(rename_all = "snake_case")]
3501enum HarnessRuntimeState {
3502 Ready,
3503 Degraded,
3504 Unavailable,
3505}
3506
3507#[derive(Serialize)]
3508struct HarnessSessionCounts {
3509 global: Option<usize>,
3510 workspace: Option<usize>,
3511}
3512
3513#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3524#[serde(rename_all = "snake_case")]
3525pub enum GatewayState {
3526 Up,
3527 Down,
3528 Unknown,
3529}
3530
3531#[derive(Debug, Clone, Serialize)]
3533pub struct GatewayHealth {
3534 pub state: GatewayState,
3535 #[serde(skip_serializing_if = "Option::is_none")]
3540 pub endpoint: Option<String>,
3541 #[serde(skip_serializing_if = "Option::is_none")]
3542 pub version: Option<String>,
3543 pub evidence: String,
3545 pub checked_at_ms: u64,
3546}
3547
3548fn openclaw_gateway_endpoint(home: &Path) -> String {
3552 let config_path = home.join(".openclaw/openclaw.json");
3553 let gateway = std::fs::read_to_string(&config_path)
3554 .ok()
3555 .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3556 .and_then(|config| config.get("gateway").cloned());
3557 if let Some(url) = gateway
3558 .as_ref()
3559 .and_then(|gateway| gateway.get("url"))
3560 .and_then(serde_json::Value::as_str)
3561 {
3562 return url.to_string();
3563 }
3564 let port = gateway
3565 .as_ref()
3566 .and_then(|gateway| gateway.get("port"))
3567 .and_then(serde_json::Value::as_u64)
3568 .unwrap_or(18789);
3569 format!("ws://127.0.0.1:{port}")
3570}
3571
3572fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3579 let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3580 let output = std::process::Command::new(&program)
3581 .args(["gateway", "status"])
3582 .stdin(std::process::Stdio::null())
3583 .output()
3584 .ok()?;
3585 let text = format!(
3586 "{}{}",
3587 String::from_utf8_lossy(&output.stdout),
3588 String::from_utf8_lossy(&output.stderr)
3589 );
3590 let verdict = text.lines().find_map(|line| {
3591 let l = line.trim();
3592 if l.contains("supervised by launchd (PID")
3593 || l.contains("supervised by systemd (PID")
3594 || l.contains("Gateway is running")
3595 || l.contains("process is running")
3596 {
3597 Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3598 } else if l.contains("not running") || l.contains("not installed") {
3599 Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3600 } else {
3601 None
3602 }
3603 });
3604 verdict
3605}
3606
3607fn gateway_health(
3608 id: &str,
3609 installed: bool,
3610 running: Option<&RunningInstance>,
3611 version: Option<&str>,
3612) -> GatewayHealth {
3613 let checked_at_ms = now_epoch_ms();
3614 let home = std::env::var_os("HOME").map(PathBuf::from);
3615 match id {
3616 HarnessId::HERMES | HarnessId::OPENCLAW => {
3617 let endpoint = (id == HarnessId::OPENCLAW)
3618 .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3619 .flatten();
3620 let (state, evidence) = match running {
3621 Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3622 None if !installed => (
3623 GatewayState::Unknown,
3624 format!("`{id}` is not installed; no gateway to probe"),
3625 ),
3626 None if id == HarnessId::HERMES => match hermes_gateway_status() {
3627 Some((state, evidence)) => (state, evidence),
3630 None => (
3631 GatewayState::Down,
3632 "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
3633 ),
3634 },
3635 None => (
3636 GatewayState::Down,
3637 format!(
3638 "no TCP listener at {}",
3639 endpoint.as_deref().unwrap_or("the gateway endpoint")
3640 ),
3641 ),
3642 };
3643 GatewayHealth {
3644 state,
3645 endpoint,
3646 version: version.map(str::to_string),
3647 evidence,
3648 checked_at_ms,
3649 }
3650 }
3651 HarnessId::ORCHESTRATOR => {
3658 let root = crate::HarnessHomes::default().orchestrator;
3659 let (state, evidence) = match crate::orchestrator::read_lease(&root) {
3660 Some(lease) if crate::orchestrator::pid_is_live(lease.pid) => (
3661 GatewayState::Up,
3662 format!(
3663 "`{}` names pid {} (started {}), which is live",
3664 crate::orchestrator::lock_path(&root).display(),
3665 lease.pid,
3666 lease.started_at
3667 ),
3668 ),
3669 Some(lease) => (
3670 GatewayState::Down,
3671 format!(
3672 "stale lease `{}`: pid {} is gone",
3673 crate::orchestrator::lock_path(&root).display(),
3674 lease.pid
3675 ),
3676 ),
3677 None => (
3678 GatewayState::Down,
3679 format!(
3680 "no lease at `{}`; `supercode orchestrator start` writes one",
3681 crate::orchestrator::lock_path(&root).display()
3682 ),
3683 ),
3684 };
3685 GatewayHealth {
3686 state,
3687 endpoint: None,
3688 version: version.map(str::to_string),
3689 evidence,
3690 checked_at_ms,
3691 }
3692 }
3693 _ => GatewayHealth {
3694 state: GatewayState::Unknown,
3695 endpoint: None,
3696 version: version.map(str::to_string),
3697 evidence: format!("`{id}` runs per session, not as a gateway"),
3698 checked_at_ms,
3699 },
3700 }
3701}
3702
3703#[derive(Debug, Clone, Serialize)]
3704struct RunningInstance {
3705 method: RunningInstanceMethod,
3707 evidence: String,
3709 checked_at_ms: u64,
3711}
3712
3713#[derive(Debug, Clone, Copy, Serialize)]
3714#[serde(rename_all = "snake_case")]
3715enum RunningInstanceMethod {
3716 GatewayConnect,
3719 StoreWalActivity,
3722}
3723
3724fn now_epoch_ms() -> u64 {
3725 std::time::SystemTime::now()
3726 .duration_since(std::time::UNIX_EPOCH)
3727 .map(|elapsed| elapsed.as_millis() as u64)
3728 .unwrap_or(0)
3729}
3730
3731fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
3735 let config_path = home.join(".openclaw/openclaw.json");
3736 let text = std::fs::read_to_string(&config_path).ok();
3737 let gateway = text
3738 .as_deref()
3739 .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
3740 .and_then(|config| config.get("gateway").cloned());
3741 let address = gateway
3742 .as_ref()
3743 .and_then(|gateway| gateway.get("url"))
3744 .and_then(serde_json::Value::as_str)
3745 .and_then(|url| {
3746 url.split("://").nth(1).map(|rest| {
3747 rest.trim_end_matches('/')
3748 .split('/')
3749 .next()
3750 .unwrap_or(rest)
3751 .to_string()
3752 })
3753 })
3754 .unwrap_or_else(|| {
3755 let port = gateway
3756 .as_ref()
3757 .and_then(|gateway| gateway.get("port"))
3758 .and_then(serde_json::Value::as_u64)
3759 .unwrap_or(18789);
3760 format!("127.0.0.1:{port}")
3761 });
3762 let reachable = std::net::TcpStream::connect_timeout(
3763 &address.parse().ok()?,
3764 std::time::Duration::from_millis(400),
3765 )
3766 .is_ok();
3767 reachable.then(|| RunningInstance {
3768 method: RunningInstanceMethod::GatewayConnect,
3769 evidence: format!(
3770 "gateway endpoint {address} accepted a TCP connect (from {})",
3771 config_path.display()
3772 ),
3773 checked_at_ms: now_epoch_ms(),
3774 })
3775}
3776
3777fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
3782 let wal = home.join(".hermes/state.db-wal");
3783 let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
3784 let age_ms = std::time::SystemTime::now()
3785 .duration_since(modified)
3786 .map(|age| age.as_millis() as u64)
3787 .unwrap_or(u64::MAX);
3788 (age_ms <= max_wal_age_ms).then(|| RunningInstance {
3789 method: RunningInstanceMethod::StoreWalActivity,
3790 evidence: format!(
3791 "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
3792 wal.display()
3793 ),
3794 checked_at_ms: now_epoch_ms(),
3795 })
3796}
3797
3798fn probe_running_instance(id: &str) -> Option<RunningInstance> {
3800 let home = std::env::var_os("HOME").map(PathBuf::from)?;
3801 match id {
3802 HarnessId::OPENCLAW => probe_openclaw_running(&home),
3803 HarnessId::HERMES => probe_hermes_running(&home, 300_000),
3804 _ => None,
3805 }
3806}
3807
3808#[derive(Serialize)]
3809struct LocalHarness {
3810 id: HarnessId,
3811 display_name: String,
3812 supported: bool,
3813 installed: bool,
3814 executable: Option<String>,
3815 version: Option<String>,
3816 auth: HarnessAuthState,
3817 runtime: HarnessRuntimeState,
3818 protocol: String,
3819 capabilities: crate::RuntimeCapabilities,
3820 effective_capabilities: crate::RuntimeCapabilities,
3821 sessions: HarnessSessionCounts,
3822 #[serde(skip_serializing_if = "Option::is_none")]
3825 running: Option<RunningInstance>,
3826 gateway: GatewayHealth,
3828 reason: Option<String>,
3829 repair: Option<String>,
3830}
3831
3832#[derive(Clone, Deserialize)]
3833struct RuntimeBackendParams {
3834 harness: HarnessId,
3835 #[serde(default)]
3836 protocol: Option<String>,
3837 #[serde(default)]
3838 launch: Option<RuntimeLaunch>,
3839 #[serde(default)]
3840 base_url: Option<String>,
3841 #[serde(default)]
3842 policy: RuntimePolicy,
3843}
3844
3845#[derive(Debug, Clone, Copy, Default, Deserialize)]
3846#[serde(rename_all = "snake_case")]
3847enum RuntimePolicy {
3848 #[default]
3849 Default,
3850 Yolo,
3851}
3852
3853#[derive(Deserialize)]
3854struct RuntimeStartParams {
3855 #[serde(flatten)]
3856 backend: RuntimeBackendParams,
3857 cwd: PathBuf,
3858 #[serde(default)]
3861 mcp_servers: Vec<crate::McpServerLaunch>,
3862}
3863
3864#[derive(Deserialize)]
3865struct RuntimeAttachParams {
3866 #[serde(flatten)]
3867 backend: RuntimeBackendParams,
3868 runtime_id: String,
3869 #[serde(default)]
3870 cwd: Option<PathBuf>,
3871}
3872
3873#[derive(Deserialize)]
3874struct RuntimeConnectionParams {
3875 connection: String,
3876}
3877
3878#[derive(Deserialize)]
3879struct RuntimeInputParams {
3880 connection: String,
3881 text: String,
3882 #[serde(default)]
3883 image_urls: Vec<String>,
3884}
3885
3886const MAX_RUNTIME_IMAGES: usize = 4;
3887const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
3888const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
3889
3890fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
3891 if image_urls.len() > MAX_RUNTIME_IMAGES {
3892 return Err(ServiceError::InvalidParams(format!(
3893 "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
3894 )));
3895 }
3896 let mut total = 0usize;
3897 for url in &image_urls {
3898 if !(url.starts_with("data:image/")
3899 || url.starts_with("https://")
3900 || url.starts_with("http://"))
3901 {
3902 return Err(ServiceError::InvalidParams(
3903 "runtime images must be image data URLs or HTTP(S) URLs".into(),
3904 ));
3905 }
3906 if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
3907 return Err(ServiceError::InvalidParams(format!(
3908 "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
3909 )));
3910 }
3911 total = total.saturating_add(url.len());
3912 }
3913 if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
3914 return Err(ServiceError::InvalidParams(format!(
3915 "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
3916 )));
3917 }
3918 Ok(image_urls)
3919}
3920
3921#[derive(Deserialize)]
3922struct RuntimeRespondParams {
3923 connection: String,
3924 request_id: Value,
3925 response: Value,
3926}
3927
3928fn default_reduction_store_root() -> PathBuf {
3929 if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
3930 return PathBuf::from(root).join("sessions");
3931 }
3932 if let Some(home) = std::env::var_os("HOME") {
3933 return PathBuf::from(home).join(".supercode").join("sessions");
3934 }
3935 PathBuf::from(".supercode").join("sessions")
3936}
3937
3938fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
3939 let mut output = String::new();
3940 for message in messages {
3941 output.push_str(
3942 &serde_json::to_string(message)
3943 .map_err(|error| ServiceError::Operation(error.to_string()))?,
3944 );
3945 output.push('\n');
3946 }
3947 Ok(output)
3948}
3949
3950fn parse_messages_jsonl(
3951 content: &str,
3952) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
3953 content
3954 .lines()
3955 .enumerate()
3956 .filter(|(_, line)| !line.trim().is_empty())
3957 .map(|(index, line)| {
3958 serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
3959 ServiceError::Operation(format!(
3960 "reduced transcript line {} is invalid: {error}",
3961 index + 1
3962 ))
3963 })
3964 })
3965 .collect()
3966}
3967
3968fn reduced_bootstrap_prompt(
3969 source: &SessionLocator,
3970 target: TransferFormat,
3971 view_jsonl: &str,
3972 sidecar_path: &Path,
3973 reduction_log_path: &Path,
3974) -> String {
3975 format!(
3976 "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
3977 \n\
3978 The bounded working transcript is below. Treat reduction markers as transparent placeholders, not missing work. If a detail behind a marker is needed, use ordinary file-reading/search tools against the full Supercode sidecar at `{sidecar}` and its reduction index at `{log}`. Do not guess hidden content. Both files were reloaded and verified before this continuation was issued.\n\
3979 \n\
3980 <supercode-reduced-session source-session=\"{source_id}\">\n\
3981 {view_jsonl}\
3982 </supercode-reduced-session>\n\
3983 \n\
3984 Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
3985 source_harness = source.harness.as_str(),
3986 target_harness = target.id(),
3987 sidecar = sidecar_path.display(),
3988 log = reduction_log_path.display(),
3989 source_id = source.session_id,
3990 )
3991}
3992
3993fn session_artifact(
3994 locator: &SessionLocator,
3995 session: &Session,
3996 target: TransferFormat,
3997) -> std::result::Result<SessionArtifact, ServiceError> {
3998 session_artifact_with_id(locator, session, target, None)
3999}
4000
4001fn session_artifact_with_id(
4002 locator: &SessionLocator,
4003 session: &Session,
4004 target: TransferFormat,
4005 target_session_id: Option<&str>,
4006) -> std::result::Result<SessionArtifact, ServiceError> {
4007 let format: SessionFormat = target.into();
4008 let diagonal = format.source() == session.meta.source;
4009 let has_appended_turns = session
4010 .imported_message_count
4011 .is_some_and(|imported| imported < session.messages.len());
4012 let content = if let Some(id) = target_session_id {
4013 if diagonal && format != SessionFormat::OpenCode {
4014 session
4015 .to_jsonl_spliced(format, Some(id))
4016 .map_err(operation)?
4017 } else {
4018 let mut rewritten = session.clone();
4019 rewritten.meta.session_id = Some(id.to_string());
4020 rewritten.to_jsonl(format).map_err(operation)?
4021 }
4022 } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4023 session.raw_verbatim()
4024 } else if diagonal {
4025 session.to_jsonl_spliced(format, None).map_err(operation)?
4026 } else {
4027 session.to_jsonl(format).map_err(operation)?
4028 };
4029 let stem = sanitize_filename(
4030 target_session_id
4031 .or(session.meta.session_id.as_deref())
4032 .unwrap_or(&locator.session_id),
4033 );
4034 let suggested_filename = if diagonal && target == TransferFormat::Grok {
4035 "chat_history.jsonl".to_string()
4036 } else if target == TransferFormat::Goose {
4037 format!("{stem}.goose.json")
4038 } else {
4039 format!("{stem}.{}.jsonl", target.id())
4040 };
4041 let mut files = vec![SessionArtifactFile {
4042 path: suggested_filename.clone(),
4043 content: content.clone(),
4044 role: ArtifactFileRole::Primary,
4045 }];
4046 if target == TransferFormat::ClaudeCode {
4047 let bundle_stem = Path::new(&suggested_filename)
4048 .file_stem()
4049 .and_then(|stem| stem.to_str())
4050 .unwrap_or(&stem);
4051 let mut child_paths = BTreeSet::new();
4052 for (index, subagent) in session.subagents.iter().enumerate() {
4053 let agent_id = subagent
4054 .meta
4055 .agent_id
4056 .as_deref()
4057 .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4058 .map(sanitize_filename)
4059 .filter(|id| !id.is_empty())
4060 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4061 let child_has_appended_turns = subagent
4062 .imported_message_count
4063 .is_some_and(|imported| imported < subagent.messages.len());
4064 let child_content = if target_session_id.is_none()
4065 && subagent.meta.source == SessionSource::ClaudeCode
4066 && subagent.raw_is_verbatim
4067 && !child_has_appended_turns
4068 {
4069 subagent.raw_verbatim()
4070 } else if subagent.meta.source == SessionSource::ClaudeCode {
4071 subagent
4072 .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4073 .map_err(operation)?
4074 } else {
4075 let mut child = subagent.clone();
4076 if let Some(id) = target_session_id {
4077 child.meta.session_id = Some(id.to_string());
4078 }
4079 child
4080 .to_jsonl(SessionFormat::ClaudeCode)
4081 .map_err(operation)?
4082 };
4083 let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4084 if !child_paths.insert(path.clone()) {
4085 return Err(ServiceError::Operation(format!(
4086 "Claude subagent ids collide at artifact path `{path}`"
4087 )));
4088 }
4089 files.push(SessionArtifactFile {
4090 path,
4091 content: child_content,
4092 role: ArtifactFileRole::Subagent,
4093 });
4094 }
4095 }
4096 if diagonal && target == TransferFormat::Grok {
4097 append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4098 }
4099 if !diagonal || !session.raw_is_verbatim {
4100 files.push(SessionArtifactFile {
4101 path: "recovery/source.supercode.jsonl".into(),
4102 content: session.to_native_jsonl(),
4103 role: ArtifactFileRole::SourceRecovery,
4104 });
4105 for (index, subagent) in session.subagents.iter().enumerate() {
4106 let id = subagent
4107 .meta
4108 .agent_id
4109 .as_deref()
4110 .map(sanitize_filename)
4111 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4112 files.push(SessionArtifactFile {
4113 path: format!("recovery/subagents/{id}.supercode.jsonl"),
4114 content: subagent.to_native_jsonl(),
4115 role: ArtifactFileRole::SourceRecovery,
4116 });
4117 }
4118 }
4119 if !diagonal && session.meta.source == SessionSource::Grok {
4120 append_grok_bundle_files(
4121 locator,
4122 "recovery/grok/",
4123 ArtifactFileRole::SourceRecovery,
4124 &mut files,
4125 )?;
4126 }
4127 let (fidelity, residue) = if diagonal
4128 && target_session_id.is_none()
4129 && session.raw_is_verbatim
4130 && !has_appended_turns
4131 {
4132 (Fidelity::ByteLossless, Vec::new())
4133 } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4134 (
4135 Fidelity::ValueLossless,
4136 vec![if target_session_id.is_some() {
4137 "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4138 } else {
4139 "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4140 }],
4141 )
4142 } else {
4143 (
4144 Fidelity::Semantic,
4145 vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4146 )
4147 };
4148 Ok(SessionArtifact {
4149 source_harness: locator.harness.clone(),
4150 target_harness: target.id(),
4151 session_id: target_session_id
4152 .map(str::to_string)
4153 .or_else(|| session.meta.session_id.clone()),
4154 content,
4155 suggested_filename,
4156 files,
4157 fidelity,
4158 residue,
4159 })
4160}
4161
4162fn append_grok_bundle_files(
4163 locator: &SessionLocator,
4164 prefix: &str,
4165 role: ArtifactFileRole,
4166 files: &mut Vec<SessionArtifactFile>,
4167) -> std::result::Result<(), ServiceError> {
4168 let primary = locator.storage.path();
4169 if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4170 return Err(ServiceError::Operation(format!(
4171 "Grok bundle locator must name chat_history.jsonl, got {}",
4172 primary.display()
4173 )));
4174 }
4175 let parent = primary.parent().ok_or_else(|| {
4176 ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4177 })?;
4178 for name in ["summary.json", "updates.jsonl"] {
4179 let path = parent.join(name);
4180 let metadata = match std::fs::symlink_metadata(&path) {
4181 Ok(metadata) => metadata,
4182 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4183 Err(error) => return Err(ServiceError::Operation(error.to_string())),
4184 };
4185 if metadata.file_type().is_symlink() || !metadata.is_file() {
4186 return Err(ServiceError::Operation(format!(
4187 "refusing non-regular Grok bundle member {}",
4188 path.display()
4189 )));
4190 }
4191 let content = std::fs::read_to_string(&path).map_err(|error| {
4192 ServiceError::Operation(format!(
4193 "Grok bundle member {} is not representable as UTF-8: {error}",
4194 path.display()
4195 ))
4196 })?;
4197 files.push(SessionArtifactFile {
4198 path: format!("{prefix}{name}"),
4199 content,
4200 role: match role {
4201 ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4202 _ => ArtifactFileRole::SourceRecovery,
4203 },
4204 });
4205 }
4206 Ok(())
4207}
4208
4209fn handoff_artifact(
4210 locator: &SessionLocator,
4211 session: &Session,
4212 target: TransferFormat,
4213 cwd: &Path,
4214) -> std::result::Result<SessionArtifact, ServiceError> {
4215 if target != TransferFormat::Grok {
4216 let target_session_id = target_session_id(target);
4217 return session_artifact_with_id(locator, session, target, Some(&target_session_id));
4218 }
4219
4220 let mut importable = session.clone();
4224 importable.meta.session_id = Some(target_session_id(TransferFormat::ClaudeCode));
4230 importable.meta.cwd = Some(if cwd.is_absolute() {
4231 cwd.to_path_buf()
4232 } else {
4233 std::env::current_dir()
4234 .map_err(|error| ServiceError::Operation(error.to_string()))?
4235 .join(cwd)
4236 });
4237 let content = importable
4238 .to_jsonl(SessionFormat::ClaudeCode)
4239 .map_err(operation)?;
4240 let stem = sanitize_filename(
4241 importable
4242 .meta
4243 .session_id
4244 .as_deref()
4245 .unwrap_or(&locator.session_id),
4246 );
4247 let suggested_filename = format!("{stem}.grok-import.claude-code.jsonl");
4248 Ok(SessionArtifact {
4249 source_harness: locator.harness.clone(),
4250 target_harness: TransferFormat::ClaudeCode.id(),
4253 session_id: importable.meta.session_id.clone(),
4254 content: content.clone(),
4255 suggested_filename: suggested_filename.clone(),
4256 files: vec![SessionArtifactFile {
4257 path: suggested_filename,
4258 content,
4259 role: ArtifactFileRole::Primary,
4260 }],
4261 fidelity: Fidelity::Semantic,
4262 residue: vec!["Grok's stock importer accepts a Claude Code transcript, not a complete Grok updates/session bundle".into()],
4263 })
4264}
4265
4266fn target_session_id(target: TransferFormat) -> String {
4267 let uuid = generated_session_id();
4268 match target {
4269 TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4270 TransferFormat::ClaudeCode
4271 | TransferFormat::Codex
4272 | TransferFormat::Pi
4273 | TransferFormat::Grok
4274 | TransferFormat::Gemini
4275 | TransferFormat::Goose
4276 | TransferFormat::Hermes => uuid,
4277 }
4278}
4279
4280fn sanitize_filename(value: &str) -> String {
4281 let value = value
4282 .chars()
4283 .map(|character| {
4284 if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4285 character
4286 } else {
4287 '-'
4288 }
4289 })
4290 .collect::<String>();
4291 let value = value.trim_matches('-');
4292 if value.is_empty() {
4293 "session".into()
4294 } else {
4295 value.chars().take(100).collect()
4296 }
4297}
4298
4299fn handoff_instructions(
4300 target: TransferFormat,
4301 session_id: &str,
4302 cwd: &Path,
4303) -> HandoffInstructions {
4304 let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4305 cwd: cwd.to_path_buf(),
4306 program: program.into(),
4307 arguments,
4308 env: BTreeMap::new(),
4309 };
4310 match target {
4311 TransferFormat::ClaudeCode => HandoffInstructions {
4312 launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4313 materialize: None,
4314 requires_materialization: true,
4315 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(),
4316 },
4317 TransferFormat::Hermes => HandoffInstructions {
4318 launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4319 materialize: None,
4320 requires_materialization: true,
4321 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(),
4322 },
4323 TransferFormat::Codex => HandoffInstructions {
4324 launch: launch("codex", vec!["resume".into(), session_id.into()]),
4325 materialize: None,
4326 requires_materialization: true,
4327 note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4328 },
4329 TransferFormat::OpenCode => HandoffInstructions {
4330 launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4331 materialize: Some(launch(
4332 "opencode",
4333 vec!["import".into(), "{artifact_path}".into()],
4334 )),
4335 requires_materialization: true,
4336 note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4337 },
4338 TransferFormat::Pi => HandoffInstructions {
4339 launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4340 materialize: None,
4341 requires_materialization: true,
4342 note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4343 },
4344 TransferFormat::Grok => HandoffInstructions {
4345 launch: launch(
4346 "grok",
4347 vec![
4348 "--resume".into(),
4349 "{imported_session_id}".into(),
4350 "--fork-session".into(),
4351 ],
4352 ),
4353 materialize: Some(launch(
4354 "grok",
4355 vec!["import".into(), "--json".into(), "{artifact_path}".into()],
4356 )),
4357 requires_materialization: true,
4358 note: "The artifact is Claude Code JSONL for Grok's official importer. Write it to a file, run the materialize command, read sessionId from its NDJSON outcome=imported record, replace {imported_session_id} in the launch arguments, then launch a writable fork of the imported session.".into(),
4359 },
4360 TransferFormat::Gemini => HandoffInstructions {
4361 launch: launch(
4362 "gemini",
4363 vec!["--session-file".into(), "{artifact_path}".into()],
4364 ),
4365 materialize: None,
4366 requires_materialization: true,
4367 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(),
4368 },
4369 TransferFormat::Goose => HandoffInstructions {
4370 launch: launch(
4371 "goose",
4372 vec![
4373 "session".into(),
4374 "--resume".into(),
4375 "--session-id".into(),
4376 "{imported_session_id}".into(),
4377 ],
4378 ),
4379 materialize: Some(launch(
4380 "goose",
4381 vec!["session".into(), "import".into(), "{artifact_path}".into()],
4382 )),
4383 requires_materialization: true,
4384 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(),
4385 },
4386 }
4387}
4388
4389fn resume_launch(
4390 harness: &str,
4391 session_id: &str,
4392 cwd: &Path,
4393 policy: ResumePolicy,
4394) -> std::result::Result<StructuredLaunch, ServiceError> {
4395 let mut arguments = Vec::new();
4396 let program = match harness {
4397 HarnessId::GROK => {
4398 if matches!(policy, ResumePolicy::Yolo) {
4399 if crate::support::self_sandbox_supported() {
4400 arguments.extend(["--sandbox".into(), "workspace".into()]);
4401 }
4402 arguments.push("--always-approve".into());
4403 }
4404 arguments.extend(["--resume".into(), session_id.into()]);
4405 "grok"
4406 }
4407 HarnessId::CODEX => {
4408 let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4409 .expect("a filesystem path always serializes as JSON text");
4410 arguments.extend([
4411 "-c".into(),
4412 "check_for_update_on_startup=false".into(),
4413 "-c".into(),
4414 format!("projects.{cwd_key}.trust_level=\"trusted\""),
4415 ]);
4416 if matches!(policy, ResumePolicy::Yolo) {
4417 arguments.extend([
4418 "--dangerously-bypass-approvals-and-sandbox".into(),
4419 "--dangerously-bypass-hook-trust".into(),
4420 ]);
4421 }
4422 arguments.extend(["resume".into(), session_id.into()]);
4423 "codex"
4424 }
4425 HarnessId::CLAUDE_CODE => {
4426 if matches!(policy, ResumePolicy::Yolo) {
4427 arguments.push("--dangerously-skip-permissions".into());
4428 }
4429 arguments.extend(["--resume".into(), session_id.into()]);
4430 "claude"
4431 }
4432 HarnessId::GEMINI => {
4433 if matches!(policy, ResumePolicy::Yolo) {
4434 arguments.push("--yolo".into());
4435 }
4436 arguments.extend(["--resume".into(), session_id.into()]);
4437 "gemini"
4438 }
4439 HarnessId::GOOSE => {
4440 arguments.extend([
4441 "session".into(),
4442 "--resume".into(),
4443 "--session-id".into(),
4444 session_id.into(),
4445 ]);
4446 "goose"
4447 }
4448 HarnessId::PI => {
4449 if matches!(policy, ResumePolicy::Yolo) {
4450 arguments.push("--approve".into());
4451 }
4452 arguments.extend(["--session".into(), session_id.into()]);
4453 "pi"
4454 }
4455 HarnessId::OPENCODE => {
4456 arguments.extend(["--session".into(), session_id.into()]);
4457 "opencode"
4458 }
4459 HarnessId::SUPERCODE => {
4460 if matches!(policy, ResumePolicy::Yolo) {
4461 arguments.push("--dangerous".into());
4462 }
4463 arguments.extend(["resume".into(), session_id.into()]);
4464 "supercode"
4465 }
4466 other => {
4467 return Err(ServiceError::InvalidParams(format!(
4468 "no structured resume launch is registered for harness `{other}`"
4469 )))
4470 }
4471 };
4472 Ok(StructuredLaunch {
4473 cwd: cwd.to_path_buf(),
4474 program: program.into(),
4475 arguments,
4476 env: BTreeMap::new(),
4477 })
4478}
4479
4480fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4486 let digest = blake3::hash(address.as_bytes()).to_hex();
4487 let path = std::env::temp_dir().join(format!(
4488 "supercode-openclaw-gateway-token-{}",
4489 &digest.as_str()[..16]
4490 ));
4491 #[cfg(unix)]
4492 {
4493 use std::io::Write;
4494 use std::os::unix::fs::OpenOptionsExt;
4495 let mut file = std::fs::OpenOptions::new()
4496 .write(true)
4497 .create(true)
4498 .truncate(true)
4499 .mode(0o600)
4500 .open(&path)?;
4501 file.write_all(secret.as_bytes())?;
4502 }
4503 #[cfg(not(unix))]
4504 std::fs::write(&path, secret)?;
4505 Ok(path)
4506}
4507
4508fn open_connect_descriptor(
4514 descriptor: &crate::HarnessSupportDescriptor,
4515 home: &Path,
4516) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4517 let Some(connect) = &descriptor.runtime.connect_launch else {
4518 return Err(ServiceError::InvalidParams(format!(
4519 "harness `{}` has no registered connect-mode launch",
4520 descriptor.id.as_str()
4521 )));
4522 };
4523 let resolved = connect
4524 .resolve(home)
4525 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4526 match (descriptor.id.as_str(), connect.protocol.as_str()) {
4527 (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4528 let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4529 if let Some(token) = resolved.auth {
4530 backend = backend.with_bearer(token);
4531 }
4532 Ok(Box::new(backend))
4533 }
4534 (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4535 let mut env = BTreeMap::new();
4546 let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4547 if let Some(token) = resolved.auth {
4548 let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4549 .map_err(|error| {
4550 ServiceError::UnsupportedAction(format!(
4551 "could not stage the gateway credential for the bridge: {error}"
4552 ))
4553 })?;
4554 arguments.push("--token-file".into());
4555 arguments.push(token_path.to_string_lossy().into_owned());
4556 env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4557 }
4558 let program = descriptor
4563 .runtime
4564 .default_launch
4565 .as_ref()
4566 .map(|launch| launch.program.clone())
4567 .unwrap_or_else(|| "openclaw".into());
4568 let launch = RuntimeLaunch {
4569 program,
4570 arguments,
4571 env,
4572 };
4573 Ok(Box::new(
4574 crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4575 .with_resume_support(descriptor.runtime.capabilities.resume_session),
4576 ))
4577 }
4578 _ => Err(ServiceError::UnsupportedAction(format!(
4579 "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4580 descriptor.id.as_str(),
4581 connect.protocol
4582 ))),
4583 }
4584}
4585
4586fn registry_connect_descriptor(
4589 params: &RuntimeBackendParams,
4590) -> Option<crate::HarnessSupportDescriptor> {
4591 if params.launch.is_some() || params.base_url.is_some() {
4592 return None;
4593 }
4594 harness_support_registry()
4595 .harnesses
4596 .into_iter()
4597 .find(|descriptor| descriptor.id == params.harness)
4598 .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4599}
4600
4601fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4602 std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4603 ServiceError::UnsupportedAction(
4604 "connect-mode launches need HOME to locate the harness config".into(),
4605 )
4606 })
4607}
4608
4609pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4612 "harness.v1.runtimes.start",
4613 "harness.v1.runtimes.resume",
4614 "harness.v1.runtimes.attach",
4615 "harness.v1.runtimes.attach_existing",
4616];
4617
4618pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
4623
4624pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
4630
4631async fn within_control_deadline<F: std::future::Future>(
4634 method: &str,
4635 call: F,
4636) -> std::result::Result<F::Output, ServiceError> {
4637 tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
4638 .await
4639 .map_err(|_| {
4640 ServiceError::Operation(format!(
4641 "`{method}` gave up after {}s: the runtime did not answer",
4642 RUNTIME_CONTROL_DEADLINE.as_secs()
4643 ))
4644 })
4645}
4646
4647pub struct RuntimeOpen {
4651 id: Value,
4652 method: String,
4653 params: Value,
4654}
4655
4656impl RuntimeOpen {
4657 pub async fn open(self) -> OpenedRuntime {
4661 let Self { id, method, params } = self;
4662 let outcome = open_runtime(&method, params).await;
4663 OpenedRuntime { id, outcome }
4664 }
4665}
4666
4667pub struct OpenedRuntime {
4670 id: Value,
4671 outcome: std::result::Result<OpenRuntime, ServiceError>,
4672}
4673
4674enum OpenRuntime {
4677 Hosted {
4680 runtime: Box<dyn RuntimeConnection>,
4681 capabilities: crate::RuntimeCapabilities,
4682 workspace: PathBuf,
4683 },
4684 Joined { runtime: Box<dyn RuntimeConnection> },
4687}
4688
4689async fn open_runtime(
4694 method: &str,
4695 params: Value,
4696) -> std::result::Result<OpenRuntime, ServiceError> {
4697 match tokio::time::timeout(
4698 RUNTIME_OPEN_DEADLINE,
4699 open_runtime_unbounded(method, params),
4700 )
4701 .await
4702 {
4703 Ok(result) => result,
4704 Err(_) => Err(ServiceError::Operation(format!(
4705 "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
4706 RUNTIME_OPEN_DEADLINE.as_secs()
4707 ))),
4708 }
4709}
4710
4711async fn open_runtime_unbounded(
4712 method: &str,
4713 params: Value,
4714) -> std::result::Result<OpenRuntime, ServiceError> {
4715 match method {
4716 "harness.v1.runtimes.start" => {
4717 let params = decode::<RuntimeStartParams>(params)?;
4718 let backend = runtime_backend(¶ms.backend)?;
4719 let capabilities = backend.capabilities();
4720 let workspace = params.cwd.clone();
4721 let runtime = backend
4722 .start(RuntimeStartRequest {
4723 cwd: params.cwd,
4724 launch: runtime_launch(¶ms.backend),
4725 mcp_servers: params.mcp_servers,
4726 })
4727 .await
4728 .map_err(operation)?;
4729 Ok(OpenRuntime::Hosted {
4730 runtime,
4731 capabilities,
4732 workspace,
4733 })
4734 }
4735 "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
4736 let params = decode::<RuntimeAttachParams>(params)?;
4737 let backend = runtime_backend(¶ms.backend)?;
4738 let capabilities = backend.capabilities();
4739 let workspace = params
4740 .cwd
4741 .clone()
4742 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
4743 let runtime = backend
4744 .attach(RuntimeAttachRequest {
4745 runtime_id: params.runtime_id,
4746 cwd: params.cwd,
4747 launch: runtime_launch(¶ms.backend),
4748 })
4749 .await
4750 .map_err(operation)?;
4751 Ok(OpenRuntime::Hosted {
4752 runtime,
4753 capabilities,
4754 workspace,
4755 })
4756 }
4757 "harness.v1.runtimes.attach_existing" => {
4758 let params = decode::<RuntimeAttachParams>(params)?;
4759 let backend: Box<dyn RuntimeBackend> = match params
4760 .backend
4761 .base_url
4762 .as_deref()
4763 .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
4764 {
4765 Some(endpoint) => {
4766 #[cfg(not(feature = "adapter-api"))]
4767 {
4768 let _ = endpoint;
4769 return Err(ServiceError::UnsupportedAction(
4770 "live HTTP attachment adapter is not compiled".into(),
4771 ));
4772 }
4773 #[cfg(feature = "adapter-api")]
4774 {
4775 let workspace = params.cwd.clone().ok_or_else(|| {
4776 ServiceError::InvalidParams(
4777 "Supercode live attach requires the project cwd".into(),
4778 )
4779 })?;
4780 let source = LiveRuntimeSource {
4781 harness: params.backend.harness.as_str().to_string(),
4782 session_id: params.runtime_id.clone(),
4783 workspace,
4784 };
4785 let receipt = resolve_live_runtime(&endpoint, &source)
4786 .map_err(|error| ServiceError::Operation(error.to_string()))?;
4787 Box::new(SupercodeHttpRuntimeBackend::new(receipt))
4788 }
4789 }
4790 None => runtime_backend(¶ms.backend)?,
4791 };
4792 let capabilities = backend.capabilities();
4793 if !capabilities.attach_existing_process {
4794 return Err(ServiceError::Operation(format!(
4795 "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
4796 backend.harness().as_str()
4797 )));
4798 }
4799 let runtime = backend
4800 .attach_existing(RuntimeAttachRequest {
4801 runtime_id: params.runtime_id,
4802 cwd: params.cwd,
4803 launch: runtime_launch(¶ms.backend),
4804 })
4805 .await
4806 .map_err(operation)?;
4807 Ok(OpenRuntime::Joined { runtime })
4808 }
4809 _ => Err(ServiceError::MethodNotFound),
4810 }
4811}
4812
4813fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
4815 match result {
4816 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
4817 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
4818 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
4819 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
4820 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
4821 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
4822 }
4823}
4824
4825fn runtime_backend(
4826 params: &RuntimeBackendParams,
4827) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4828 if let Some(descriptor) = registry_connect_descriptor(params) {
4829 return open_connect_descriptor(&descriptor, &service_home()?);
4830 }
4831 if params.protocol.as_deref() == Some("acp") {
4832 let launch = params
4833 .launch
4834 .clone()
4835 .or_else(|| {
4836 harness_support_registry()
4837 .harnesses
4838 .into_iter()
4839 .find(|harness| harness.id == params.harness)
4840 .filter(|harness| {
4841 harness.runtime.implementation == ImplementationKind::GenericProtocol
4842 && harness.runtime.protocol.starts_with("acp")
4843 })
4844 .and_then(|harness| harness.runtime.default_launch)
4845 })
4846 .ok_or_else(|| {
4847 ServiceError::InvalidParams(
4848 "an ACP runtime requires `launch` unless the harness has a registered default"
4849 .into(),
4850 )
4851 })?;
4852 let resume_session = harness_support_registry()
4853 .harnesses
4854 .into_iter()
4855 .find(|harness| harness.id == params.harness)
4856 .is_some_and(|harness| harness.runtime.capabilities.resume_session);
4857 return Ok(Box::new(
4858 AcpRuntimeBackend::new(params.harness.clone(), launch)
4859 .with_resume_support(resume_session),
4860 ));
4861 }
4862 let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
4863 HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
4864 HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
4865 HarnessId::PI => Box::new(PiRuntimeBackend::new()),
4866 HarnessId::OPENCODE => match ¶ms.base_url {
4867 Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
4868 None => Box::new(OpenCodeRuntimeBackend::new()),
4869 },
4870 harness => {
4871 let descriptor = harness_support_registry()
4872 .harnesses
4873 .into_iter()
4874 .find(|descriptor| descriptor.id.as_str() == harness)
4875 .filter(|descriptor| {
4876 descriptor.runtime.implementation == ImplementationKind::GenericProtocol
4877 && descriptor.runtime.protocol.starts_with("acp")
4878 });
4879 let Some(descriptor) = descriptor else {
4880 return Err(ServiceError::InvalidParams(format!(
4881 "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
4882 )));
4883 };
4884 let resume = descriptor.runtime.capabilities.resume_session;
4885 Box::new(
4886 AcpRuntimeBackend::new(
4887 descriptor.id,
4888 descriptor
4889 .runtime
4890 .default_launch
4891 .expect("generic ACP registry entry includes its launch"),
4892 )
4893 .with_resume_support(resume),
4894 )
4895 }
4896 };
4897 Ok(backend)
4898}
4899
4900fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
4901 if let Some(launch) = ¶ms.launch {
4902 return Some(launch.clone());
4903 }
4904 if !matches!(params.policy, RuntimePolicy::Yolo) {
4905 return None;
4906 }
4907 let launch = match params.harness.as_str() {
4908 HarnessId::GROK => RuntimeLaunch {
4909 program: "grok".into(),
4910 arguments: {
4911 let mut arguments: Vec<String> = Vec::new();
4912 if crate::support::self_sandbox_supported() {
4913 arguments.extend(["--sandbox".into(), "workspace".into()]);
4914 }
4915 arguments.extend([
4916 "--always-approve".into(),
4917 "agent".into(),
4918 "--no-leader".into(),
4919 "stdio".into(),
4920 ]);
4921 arguments
4922 },
4923 env: BTreeMap::from([("GROK_AGENT_DASHBOARD".into(), "0".into())]),
4924 },
4925 HarnessId::CODEX => RuntimeLaunch {
4926 program: "codex".into(),
4927 arguments: vec![
4928 "--dangerously-bypass-approvals-and-sandbox".into(),
4929 "--dangerously-bypass-hook-trust".into(),
4930 "app-server".into(),
4931 ],
4932 env: BTreeMap::new(),
4933 },
4934 HarnessId::CLAUDE_CODE => RuntimeLaunch {
4935 program: "claude".into(),
4936 arguments: vec![
4937 "--dangerously-skip-permissions".into(),
4938 "--print".into(),
4939 "--input-format".into(),
4940 "stream-json".into(),
4941 "--output-format".into(),
4942 "stream-json".into(),
4943 "--verbose".into(),
4944 ],
4945 env: BTreeMap::new(),
4946 },
4947 HarnessId::PI => RuntimeLaunch {
4948 program: "pi".into(),
4949 arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
4950 env: BTreeMap::new(),
4951 },
4952 HarnessId::OPENCODE => RuntimeLaunch {
4953 program: "opencode".into(),
4954 arguments: vec!["serve".into()],
4955 env: BTreeMap::new(),
4956 },
4957 HarnessId::GEMINI => RuntimeLaunch {
4958 program: "gemini".into(),
4959 arguments: vec!["--acp".into(), "--yolo".into()],
4960 env: BTreeMap::new(),
4961 },
4962 HarnessId::GOOSE => RuntimeLaunch {
4963 program: "goose".into(),
4964 arguments: vec!["acp".into()],
4965 env: BTreeMap::new(),
4966 },
4967 HarnessId::SUPERCODE => RuntimeLaunch {
4968 program: "supercode".into(),
4969 arguments: vec!["acp".into(), "--dangerous".into()],
4970 env: BTreeMap::new(),
4971 },
4972 _ => return None,
4973 };
4974 Some(launch)
4975}
4976
4977struct IsolatedProbeHome {
4983 launch: RuntimeLaunch,
4984 root: PathBuf,
4985}
4986
4987impl IsolatedProbeHome {
4988 fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
4989 let root = std::env::temp_dir().join(format!(
4990 "supercode-harness-probe-{harness}-{}",
4991 generated_session_id()
4992 ));
4993 std::fs::create_dir_all(&root)?;
4994 set_private_dir_permissions(&root)?;
4995
4996 if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
4997 for relative in probe_auth_files(harness) {
4998 copy_probe_file(&source_home, &root, relative)?;
4999 }
5000 }
5001 configure_isolated_probe_auth(harness, &root)?;
5002
5003 let root_text = root.to_string_lossy().into_owned();
5004 for (key, value) in [
5005 ("HOME", root_text.clone()),
5006 (
5007 "XDG_CACHE_HOME",
5008 root.join(".cache").to_string_lossy().into_owned(),
5009 ),
5010 (
5011 "XDG_CONFIG_HOME",
5012 root.join(".config").to_string_lossy().into_owned(),
5013 ),
5014 (
5015 "XDG_DATA_HOME",
5016 root.join(".local/share").to_string_lossy().into_owned(),
5017 ),
5018 ] {
5019 launch.env.insert(key.into(), value);
5020 }
5021 let scoped = match harness {
5022 HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5023 HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5024 HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5025 HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5026 HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5027 HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5028 _ => None,
5029 };
5030 if let Some((key, value)) = scoped {
5031 launch
5032 .env
5033 .insert(key.into(), value.to_string_lossy().into_owned());
5034 }
5035 Ok(Self { launch, root })
5036 }
5037
5038 fn cleanup(&self) -> std::io::Result<()> {
5039 match std::fs::remove_dir_all(&self.root) {
5040 Ok(()) => Ok(()),
5041 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5042 Err(error) => Err(error),
5043 }
5044 }
5045}
5046
5047impl Drop for IsolatedProbeHome {
5048 fn drop(&mut self) {
5049 let _ = self.cleanup();
5050 }
5051}
5052
5053fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5054 match harness {
5055 HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5056 HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5060 HarnessId::CODEX => &[".codex/auth.json"],
5061 HarnessId::GEMINI => &[
5062 ".gemini/google_accounts.json",
5063 ".gemini/oauth_creds.json",
5064 ".gemini/settings.json",
5065 ],
5066 HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5067 HarnessId::OPENCODE => &[
5068 ".config/opencode/auth.json",
5069 ".local/share/opencode/auth.json",
5070 ],
5071 HarnessId::PI => &[".pi/agent/auth.json"],
5072 HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5077 HarnessId::SUPERCODE => &[
5078 ".config/supercode/config.toml",
5079 ".config/supercode/credentials.toml",
5080 ],
5081 _ => &[],
5082 }
5083}
5084
5085fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5086 let source = source_home.join(relative);
5087 if !source.is_file() {
5088 return Ok(());
5089 }
5090 let destination = probe_home.join(relative);
5091 if let Some(parent) = destination.parent() {
5092 std::fs::create_dir_all(parent)?;
5093 set_private_dir_permissions(parent)?;
5094 }
5095 std::fs::copy(source, &destination)?;
5096 set_private_file_permissions(&destination)
5097}
5098
5099fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5100 if harness != HarnessId::GEMINI {
5101 return Ok(());
5102 }
5103 let oauth = probe_home.join(".gemini/oauth_creds.json");
5104 if !oauth.is_file() {
5105 return Ok(());
5106 }
5107 let settings_path = probe_home.join(".gemini/settings.json");
5108 let mut settings = std::fs::read_to_string(&settings_path)
5109 .ok()
5110 .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5111 .unwrap_or_else(|| json!({}));
5112 settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5113 std::fs::write(
5114 &settings_path,
5115 serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5116 )?;
5117 set_private_file_permissions(&settings_path)
5118}
5119
5120#[cfg(unix)]
5121fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5122 use std::os::unix::fs::PermissionsExt;
5123 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5124}
5125
5126#[cfg(not(unix))]
5127fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5128 Ok(())
5129}
5130
5131#[cfg(unix)]
5132fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5133 use std::os::unix::fs::PermissionsExt;
5134 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5135}
5136
5137#[cfg(not(unix))]
5138fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5139 Ok(())
5140}
5141
5142fn find_executable(program: &str) -> Option<PathBuf> {
5143 let candidate = PathBuf::from(program);
5144 if candidate.components().count() > 1 {
5145 return candidate.is_file().then_some(candidate);
5146 }
5147 let path = std::env::var_os("PATH")?;
5148 for directory in std::env::split_paths(&path) {
5149 let candidate = directory.join(program);
5150 if candidate.is_file() {
5151 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5152 }
5153 #[cfg(windows)]
5154 {
5155 for extension in ["exe", "cmd", "bat"] {
5156 let candidate = directory.join(format!("{program}.{extension}"));
5157 if candidate.is_file() {
5158 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5159 }
5160 }
5161 }
5162 }
5163 None
5164}
5165
5166async fn executable_version(executable: &Path) -> Option<String> {
5167 let mut command = tokio::process::Command::new(executable);
5168 command
5169 .arg("--version")
5170 .stdin(std::process::Stdio::null())
5171 .stdout(std::process::Stdio::piped())
5172 .stderr(std::process::Stdio::piped())
5173 .kill_on_drop(true);
5174 let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5175 .await
5176 .ok()?
5177 .ok()?;
5178 let stdout = String::from_utf8_lossy(&output.stdout);
5179 let stderr = String::from_utf8_lossy(&output.stderr);
5180 stdout
5181 .lines()
5182 .chain(stderr.lines())
5183 .map(str::trim)
5184 .find(|line| !line.is_empty())
5185 .map(|line| truncate_text(line, 200))
5186}
5187
5188pub(crate) fn auth_evidence(harness: &str) -> bool {
5189 let env_names: &[&str] = match harness {
5190 HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5191 HarnessId::CODEX => &["OPENAI_API_KEY"],
5192 HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5193 HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5194 HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5195 HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5196 HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5197 _ => &[],
5198 };
5199 if env_names
5200 .iter()
5201 .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5202 {
5203 return true;
5204 }
5205 let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5206 return false;
5207 };
5208 let files: Vec<PathBuf> = match harness {
5209 HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5210 HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5211 HarnessId::OPENCODE => vec![
5212 home.join(".local/share/opencode/auth.json"),
5213 home.join(".config/opencode/auth.json"),
5214 ],
5215 HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5216 HarnessId::GROK => vec![home.join(".grok/auth.json")],
5217 HarnessId::GEMINI => vec![
5218 home.join(".gemini/oauth_creds.json"),
5219 home.join(".gemini/google_accounts.json"),
5220 ],
5221 HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5222 HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5223 _ => Vec::new(),
5224 };
5225 if files.into_iter().any(|path| {
5226 std::fs::metadata(path)
5227 .map(|metadata| metadata.is_file() && metadata.len() > 2)
5228 .unwrap_or(false)
5229 }) {
5230 return true;
5231 }
5232 if harness == HarnessId::CLAUDE_CODE {
5239 return std::fs::read_to_string(home.join(".claude.json"))
5240 .map(|text| text.contains("\"oauthAccount\""))
5241 .unwrap_or(false);
5242 }
5243 false
5244}
5245
5246fn looks_like_auth_error(message: &str) -> bool {
5247 let message = message.to_ascii_lowercase();
5248 [
5249 "auth",
5250 "login",
5251 "sign in",
5252 "sign-in",
5253 "credential",
5254 "unauthorized",
5255 "forbidden",
5256 "token",
5257 ]
5258 .iter()
5259 .any(|needle| message.contains(needle))
5260}
5261
5262fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5263 crate::RuntimeCapabilities {
5264 start_session: false,
5265 resume_session: false,
5266 attach_existing_process: false,
5267 send_input: false,
5268 stream_events: false,
5269 interrupt: false,
5270 steer: false,
5271 respond_to_requests: false,
5272 }
5273}
5274
5275fn truncate_text(text: &str, max_chars: usize) -> String {
5276 let mut chars = text.chars();
5277 let truncated = chars.by_ref().take(max_chars).collect::<String>();
5278 if chars.next().is_some() {
5279 format!("{truncated}…")
5280 } else {
5281 truncated
5282 }
5283}
5284
5285fn error_message(error: ServiceError) -> String {
5286 match error {
5287 ServiceError::InvalidParams(message)
5288 | ServiceError::Operation(message)
5289 | ServiceError::UnsupportedAction(message) => message,
5290 ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5291 ServiceError::Sdk(error) => error.to_string(),
5292 }
5293}
5294
5295#[derive(Debug)]
5296enum ServiceError {
5297 InvalidParams(String),
5298 MethodNotFound,
5299 UnsupportedAction(String),
5300 Operation(String),
5301 Sdk(SdkError),
5302}
5303
5304fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5305 match error {
5306 ServiceError::InvalidParams(message) => {
5307 SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5308 }
5309 ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5310 SdkError::unsupported(operation)
5311 }
5312 ServiceError::Operation(message) => {
5313 let code = if message.contains("already in progress") {
5314 SdkErrorCode::Busy
5315 } else if message.contains("not supported by this runtime") {
5316 SdkErrorCode::UnsupportedAction
5317 } else if message.contains("unknown runtime connection") {
5318 SdkErrorCode::NotFound
5319 } else {
5320 SdkErrorCode::Execution
5321 };
5322 SdkError::new(code, operation, message)
5323 }
5324 ServiceError::Sdk(error) => error,
5325 }
5326}
5327
5328fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5329 let error_code = error.code();
5330 let code = match error_code {
5331 SdkErrorCode::Unauthenticated => -32030,
5332 SdkErrorCode::Unauthorized => -32031,
5333 SdkErrorCode::ControllerRequired => -32032,
5334 SdkErrorCode::LeaseExpired => -32033,
5335 SdkErrorCode::InvalidArgument => -32602,
5336 SdkErrorCode::NotFound => -32004,
5337 SdkErrorCode::Busy => -32000,
5338 SdkErrorCode::UnsupportedAction => -32020,
5339 SdkErrorCode::Execution => -32002,
5340 SdkErrorCode::Transport => -32003,
5341 };
5342 json!({
5343 "jsonrpc": "2.0",
5344 "id": id,
5345 "error": {
5346 "code": code,
5347 "name": error_code,
5348 "operation": error.operation(),
5349 "message": error.to_string(),
5350 },
5351 })
5352}
5353
5354fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
5355 serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
5356}
5357
5358fn operation(error: impl Into<crate::Error>) -> ServiceError {
5359 let error = error.into();
5360 match error {
5361 crate::Error::Sdk(error) => ServiceError::Sdk(error),
5362 error => ServiceError::Operation(error.to_string()),
5363 }
5364}
5365
5366#[derive(Debug, Clone, Deserialize, Default)]
5370#[serde(default)]
5371struct MemoryRequest {
5372 harness: Option<String>,
5374 query: Option<String>,
5376 profile: Option<String>,
5378 session: Option<String>,
5380 full: bool,
5382 regex: bool,
5384 cwd: Option<std::path::PathBuf>,
5386 homes: crate::HarnessHomes,
5388}
5389
5390fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
5393 let request = decode::<MemoryRequest>(params)?;
5394 let harness = request
5395 .harness
5396 .clone()
5397 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
5398 let to_service = |error: crate::memory::MemoryError| match error {
5399 crate::memory::MemoryError::UnsupportedHarness { .. }
5400 | crate::memory::MemoryError::SessionNotScoped { .. } => {
5401 ServiceError::UnsupportedAction(error.to_string())
5402 }
5403 other => ServiceError::InvalidParams(other.to_string()),
5404 };
5405 match method {
5406 "harness.v1.memory.show" => {
5407 let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
5408 harness,
5409 profile: request.profile,
5410 session: request.session,
5411 full: request.full,
5412 cwd: request.cwd,
5413 homes: request.homes,
5414 })
5415 .map_err(to_service)?;
5416 Ok(json!({
5417 "schema": crate::memory::MEMORY_SCHEMA,
5418 "documents": documents,
5419 }))
5420 }
5421 "harness.v1.memory.search" => {
5422 let query = request
5423 .query
5424 .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
5425 let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
5426 harness,
5427 query,
5428 profile: request.profile,
5429 regex: request.regex,
5430 cwd: request.cwd,
5431 homes: request.homes,
5432 })
5433 .map_err(to_service)?;
5434 Ok(json!({
5435 "schema": crate::memory::MEMORY_SCHEMA,
5436 "matches": matches,
5437 }))
5438 }
5439 _ => Err(ServiceError::MethodNotFound),
5440 }
5441}
5442
5443#[derive(Debug, Clone, Deserialize)]
5447#[serde(default)]
5448struct ProfilesQuery {
5449 harness: Option<String>,
5451 name: Option<String>,
5453 homes: crate::HarnessHomes,
5455}
5456
5457impl Default for ProfilesQuery {
5458 fn default() -> Self {
5459 Self {
5460 harness: None,
5461 name: None,
5462 homes: crate::HarnessHomes::default(),
5463 }
5464 }
5465}
5466
5467fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
5470 let query = decode::<ProfilesQuery>(params)?;
5471 let to_service = |error: crate::profiles::ProfileError| match error {
5472 crate::profiles::ProfileError::UnsupportedHarness { .. } => {
5473 ServiceError::UnsupportedAction(error.to_string())
5474 }
5475 crate::profiles::ProfileError::NotFound { .. } => {
5476 ServiceError::InvalidParams(error.to_string())
5477 }
5478 };
5479 match method {
5480 "harness.v1.profiles.list" => {
5481 let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
5482 .map_err(to_service)?;
5483 Ok(json!({
5484 "schema": crate::profiles::PROFILES_SCHEMA,
5485 "profiles": profiles,
5486 }))
5487 }
5488 "harness.v1.profiles.get" => {
5489 let harness = query
5490 .harness
5491 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
5492 let name = query
5493 .name
5494 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
5495 let profile =
5496 crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
5497 Ok(json!({
5498 "schema": crate::profiles::PROFILES_SCHEMA,
5499 "profile": profile,
5500 }))
5501 }
5502 _ => Err(ServiceError::MethodNotFound),
5503 }
5504}
5505
5506#[derive(Debug, Clone, Deserialize)]
5510#[serde(default)]
5511struct ChannelsQuery {
5512 harness: Option<String>,
5514 name: Option<String>,
5516 homes: crate::HarnessHomes,
5518}
5519
5520impl Default for ChannelsQuery {
5521 fn default() -> Self {
5522 Self {
5523 harness: None,
5524 name: None,
5525 homes: crate::HarnessHomes::default(),
5526 }
5527 }
5528}
5529
5530#[derive(Debug, Clone, Deserialize)]
5534#[serde(default)]
5535struct RoutesQuery {
5536 harness: Option<String>,
5537 profile: Option<String>,
5539 homes: crate::HarnessHomes,
5540}
5541
5542impl Default for RoutesQuery {
5543 fn default() -> Self {
5544 Self {
5545 harness: None,
5546 profile: None,
5547 homes: crate::HarnessHomes::default(),
5548 }
5549 }
5550}
5551
5552#[derive(Debug, Clone, Deserialize)]
5553#[serde(default)]
5554struct TriggersQuery {
5555 harness: Option<String>,
5556 homes: crate::HarnessHomes,
5557}
5558
5559impl Default for TriggersQuery {
5560 fn default() -> Self {
5561 Self {
5562 harness: None,
5563 homes: crate::HarnessHomes::default(),
5564 }
5565 }
5566}
5567
5568fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
5569 let query = decode::<TriggersQuery>(params)?;
5570 let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
5571 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
5572 Ok(json!({
5573 "schema": crate::triggers::TRIGGERS_SCHEMA,
5574 "triggers": triggers,
5575 }))
5576}
5577
5578fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
5579 let query = decode::<RoutesQuery>(params)?;
5580 let routes = crate::routes::list_routes(
5581 &query.homes,
5582 query.harness.as_deref(),
5583 query.profile.as_deref(),
5584 )
5585 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
5586 Ok(json!({
5587 "schema": crate::routes::ROUTES_SCHEMA,
5588 "routes": routes,
5589 }))
5590}
5591
5592fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
5593 let query = decode::<ChannelsQuery>(params)?;
5594 let to_service = |error: crate::channels::ChannelError| match error {
5595 crate::channels::ChannelError::UnsupportedHarness { .. } => {
5596 ServiceError::UnsupportedAction(error.to_string())
5597 }
5598 crate::channels::ChannelError::NotFound { .. } => {
5599 ServiceError::InvalidParams(error.to_string())
5600 }
5601 };
5602 match method {
5603 "harness.v1.channels.list" => {
5604 let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
5605 .map_err(to_service)?;
5606 Ok(json!({
5607 "schema": crate::channels::CHANNELS_SCHEMA,
5608 "channels": channels,
5609 }))
5610 }
5611 "harness.v1.channels.status" => {
5612 let harness = query
5613 .harness
5614 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
5615 let name = query
5616 .name
5617 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
5618 let channel = crate::channels::channel_status(&query.homes, &harness, &name)
5619 .map_err(to_service)?;
5620 Ok(json!({
5621 "schema": crate::channels::CHANNELS_SCHEMA,
5622 "channel": channel,
5623 }))
5624 }
5625 _ => Err(ServiceError::MethodNotFound),
5626 }
5627}
5628
5629fn rpc_error(id: Value, code: i64, message: &str) -> Value {
5630 json!({
5631 "jsonrpc": "2.0",
5632 "id": id,
5633 "error": {"code": code, "message": message},
5634 })
5635}
5636
5637#[cfg(test)]
5638mod tests {
5639 use super::*;
5640 use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
5641 use async_trait::async_trait;
5642 use std::io::Write;
5643 use std::path::PathBuf;
5644 use std::time::Instant;
5645
5646 #[test]
5647 fn indexed_claude_descriptor_keeps_the_live_peer_address() {
5648 let descriptor = SessionDescriptor {
5649 locator: SessionLocator {
5650 harness: HarnessId::new(HarnessId::CLAUDE_CODE),
5651 session_id: "live-session".into(),
5652 storage: StorageLocator::File {
5653 path: PathBuf::from("/tmp/live-session.jsonl"),
5654 },
5655 },
5656 cwd: Some(PathBuf::from("/project")),
5657 title: None,
5658 preview_candidates: Vec::new(),
5659 latest_message_candidates: Vec::new(),
5660 updated_at_ms: Some(1),
5661 message_count: None,
5662 model: None,
5663 parent_session_id: None,
5664 child_session_count: 0,
5665 nouns: Default::default(),
5666 };
5667 let peer = crate::claude_peer::ClaudePeerSession {
5668 pid: 42,
5669 session_id: "live-session".into(),
5670 cwd: Some(PathBuf::from("/project")),
5671 name: "peer".into(),
5672 socket_path: PathBuf::from("/tmp/peer.sock"),
5673 status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
5674 updated_at_ms: Some(1),
5675 version: Some("test".into()),
5676 };
5677
5678 let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
5679 assert!(value["live_endpoint"]
5680 .as_str()
5681 .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
5682 }
5683
5684 struct EndingRuntime {
5685 handle: RuntimeHandle,
5686 event: Option<HarnessEvent>,
5687 close_failures: usize,
5688 }
5689
5690 #[async_trait]
5691 impl RuntimeConnection for EndingRuntime {
5692 fn handle(&self) -> &RuntimeHandle {
5693 &self.handle
5694 }
5695
5696 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
5697 unreachable!("ending runtime does not accept input")
5698 }
5699
5700 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
5701 Ok(self.event.take())
5702 }
5703
5704 async fn interrupt(&mut self) -> crate::Result<()> {
5705 Ok(())
5706 }
5707
5708 async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
5709 Ok(())
5710 }
5711
5712 async fn close(&mut self) -> crate::Result<()> {
5713 if self.close_failures > 0 {
5714 self.close_failures -= 1;
5715 return Err(crate::Error::Other(
5716 "cleanup temporarily unavailable".into(),
5717 ));
5718 }
5719 Ok(())
5720 }
5721 }
5722
5723 fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
5724 Box::new(EndingRuntime {
5725 handle: RuntimeHandle {
5726 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
5727 runtime_id: "ending-session".into(),
5728 endpoint: RuntimeEndpoint::LocalProcess {
5729 pid: None,
5730 command: vec!["ending-runtime".into()],
5731 protocol: "test".into(),
5732 },
5733 },
5734 event,
5735 close_failures: 0,
5736 })
5737 }
5738
5739 #[tokio::test]
5740 async fn failed_runtime_close_retains_ownership_until_retry_succeeds() {
5741 let mut service = HarnessSessionService::new();
5742 let handle = ending_runtime(None).handle().clone();
5743 let runtime_id = handle.runtime_id.clone();
5744 let opened = service
5745 .insert_runtime(Box::new(EndingRuntime {
5746 handle,
5747 event: None,
5748 close_failures: 1,
5749 }))
5750 .unwrap();
5751 let connection = opened["connection"].as_str().unwrap().to_string();
5752 service.terminal_launches.insert(
5753 connection.clone(),
5754 StructuredLaunch {
5755 cwd: PathBuf::from("/fixture"),
5756 program: "fixture".into(),
5757 arguments: Vec::new(),
5758 env: BTreeMap::new(),
5759 },
5760 );
5761 let first = service
5762 .handle_async(request(
5763 1,
5764 "harness.v1.runtimes.close",
5765 json!({"connection": connection}),
5766 ))
5767 .await;
5768 assert!(first.get("error").is_some(), "{first}");
5769 assert!(service.runtimes.contains_key(&connection));
5770 assert!(service.terminal_launches.contains_key(&connection));
5771 assert!(service.runtime_sequences.contains_key(&runtime_id));
5772 let retry = service
5773 .handle_async(request(
5774 2,
5775 "harness.v1.runtimes.close",
5776 json!({"connection": connection}),
5777 ))
5778 .await;
5779 assert_eq!(retry["result"]["closed"], true, "{retry}");
5780 assert!(!service.runtimes.contains_key(&connection));
5781 assert!(!service.terminal_launches.contains_key(&connection));
5782 assert!(!service.runtime_sequences.contains_key(&runtime_id));
5783 }
5784
5785 fn request(id: u64, method: &str, params: Value) -> Value {
5786 json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
5787 }
5788
5789 fn hermes_store() -> PathBuf {
5792 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
5793 }
5794
5795 fn hermes_discovery(params: Value) -> Value {
5799 let mut response =
5800 HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
5801 let store = hermes_store().display().to_string();
5802 for session in response["result"]["sessions"]
5803 .as_array_mut()
5804 .expect("sessions array")
5805 {
5806 if session["locator"]["storage"]["path"] == json!(store) {
5807 session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
5808 }
5809 session.as_object_mut().unwrap().remove("activity");
5813 }
5814 response["result"].take()
5815 }
5816
5817 fn hermes_query() -> Value {
5818 json!({
5819 "harnesses": ["hermes"],
5820 "homes": {"hermes": hermes_store()},
5821 })
5822 }
5823
5824 fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
5825 result["sessions"]
5826 .as_array()
5827 .expect("sessions array")
5828 .iter()
5829 .find(|session| session["locator"]["session_id"] == json!(id))
5830 .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
5831 }
5832
5833 #[test]
5834 fn orch6_discover_rows_carry_the_conversation_nouns() {
5835 let result = hermes_discovery(hermes_query());
5836
5837 let dm = row(&result, "tg-dm-1");
5840 assert_eq!(dm["trigger"], json!("channel"));
5841 assert_eq!(dm["surface"]["platform"], json!("telegram"));
5842 assert_eq!(dm["surface"]["kind"], json!("dm"));
5843 assert_eq!(dm["surface"]["chat_id"], json!("123456"));
5844 assert_eq!(dm["surface"]["participant_id"], json!("u1"));
5845 assert_eq!(
5846 dm["workspace"],
5847 json!({"kind": "channel", "value": "telegram:123456"})
5848 );
5849 assert!(dm.get("profile").is_none(), "{dm:#}");
5850
5851 let fire = row(&result, "cron_job42_20260902_120000");
5853 assert_eq!(fire["trigger"], json!("cron"));
5854 assert_eq!(
5855 fire["recurrence"],
5856 json!({"job_id": "job42", "kind": "cron"})
5857 );
5858 assert_eq!(fire["workspace"]["kind"], json!("repo"));
5859
5860 let coder = row(&result, "tg-coder-1");
5863 assert_eq!(coder["trigger"], json!("channel"));
5864 assert_eq!(coder["profile"], json!("coder"));
5865 assert_eq!(coder["surface"]["thread_id"], json!("55"));
5866 assert_eq!(
5867 coder["surface"]["key"],
5868 json!("agent:coder:telegram:group:-100777:55")
5869 );
5870 assert_eq!(
5871 coder["workspace"],
5872 json!({"kind": "repo", "value": "/workspace/project"})
5873 );
5874 assert_eq!(
5875 coder["cross_surface"],
5876 json!({"state": "pending", "platform": "discord"})
5877 );
5878
5879 let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
5881 assert_eq!(acp["trigger"], json!("human"));
5882 assert!(acp.get("surface").is_none(), "{acp:#}");
5883 assert_eq!(acp["workspace"], json!({"kind": "none"}));
5884 }
5885
5886 #[test]
5887 fn orch6_discover_filters_by_harness_and_profile() {
5888 let mut params = hermes_query();
5889 params["profile"] = json!("coder");
5890 let result = hermes_discovery(params);
5891 let ids: Vec<&str> = result["sessions"]
5892 .as_array()
5893 .expect("sessions array")
5894 .iter()
5895 .map(|session| session["locator"]["session_id"].as_str().unwrap())
5896 .collect();
5897 assert_eq!(ids, vec!["tg-coder-1"]);
5898
5899 let mut missing = hermes_query();
5902 missing["profile"] = json!("nobody");
5903 assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
5904
5905 let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
5908 assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
5909 }
5910
5911 #[test]
5912 fn orch6_load_reports_the_same_nouns_as_discovery() {
5913 let mut service = HarnessSessionService::new();
5914 let loaded = service.handle(request(
5915 1,
5916 "harness.v1.sessions.load",
5917 json!({"locator": {
5918 "harness": "hermes",
5919 "session_id": "tg-coder-1",
5920 "storage": {"kind": "file", "path": hermes_store()},
5921 }}),
5922 ));
5923 let session = &loaded["result"]["session"];
5924 let discovered = hermes_discovery(hermes_query());
5925 let row = row(&discovered, "tg-coder-1");
5926 for noun in [
5927 "trigger",
5928 "surface",
5929 "profile",
5930 "recurrence",
5931 "cross_surface",
5932 "workspace",
5933 ] {
5934 assert_eq!(
5935 session[noun],
5936 row.get(noun).cloned().unwrap_or(Value::Null),
5937 "`{noun}` disagrees between sessions.load and sessions.discover"
5938 );
5939 }
5940 }
5941
5942 fn profile_fixture_homes() -> Value {
5945 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
5946 json!({
5947 "hermes": fixtures.join("hermes_home/state.db"),
5948 "openclaw": fixtures.join("openclaw_home"),
5949 })
5950 }
5951
5952 fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
5953 response["result"]["profiles"]
5954 .as_array()
5955 .unwrap_or_else(|| panic!("no profiles array in {response}"))
5956 .iter()
5957 .find(|row| row["harness"] == harness && row["name"] == name)
5958 .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
5959 }
5960
5961 #[test]
5966 fn profiles_list_reads_every_source_uniformly() {
5967 let mut service = HarnessSessionService::new();
5968 let response = service.handle(request(
5969 1,
5970 "harness.v1.profiles.list",
5971 json!({"homes": profile_fixture_homes()}),
5972 ));
5973 assert_eq!(
5974 response["result"]["schema"],
5975 crate::profiles::PROFILES_SCHEMA
5976 );
5977
5978 let default = profile_row(&response, "hermes", "default");
5979 assert_eq!(default["kind"], "hermes_profile");
5980 assert_eq!(default["default"], true);
5981 assert_eq!(default["routes"], 0);
5982 assert_eq!(default["sessions"], 11);
5983 assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
5984
5985 let coder = profile_row(&response, "hermes", "coder");
5986 assert_eq!(coder["kind"], "hermes_profile");
5987 assert_eq!(coder["default"], false);
5988 assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
5989 assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
5990 assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
5991 assert!(coder["home"]
5992 .as_str()
5993 .unwrap()
5994 .ends_with("hermes_home/profiles/coder"));
5995
5996 let main = profile_row(&response, "openclaw", "main");
5997 assert_eq!(main["kind"], "openclaw_agent");
5998 assert_eq!(main["default"], true);
6001 assert_eq!(main["routes"], 0);
6002 assert_eq!(main["sessions"], 4);
6003 assert_eq!(
6004 main["model"],
6005 Value::Null,
6006 "`agents.defaults.model` is an install default, not this agent's pin"
6007 );
6008
6009 let design = profile_row(&response, "openclaw", "design");
6010 assert_eq!(design["default"], false);
6011 assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6012 assert_eq!(design["sessions"], 0);
6013 assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6014
6015 let preset = profile_row(&response, "supercode", "supercode-default");
6016 assert_eq!(preset["kind"], "preset");
6017 assert_eq!(preset["default"], true);
6018 assert_eq!(preset["home"], Value::Null);
6019 assert_eq!(preset["routes"], Value::Null);
6020 }
6021
6022 #[test]
6025 fn profiles_list_reads_codex_profile_tables() {
6026 let codex_home = std::env::temp_dir().join(format!(
6027 "supercode-orch10-codex-{}-{}",
6028 std::process::id(),
6029 std::time::SystemTime::now()
6030 .duration_since(std::time::UNIX_EPOCH)
6031 .unwrap()
6032 .as_nanos()
6033 ));
6034 std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6035 std::fs::write(
6036 codex_home.join("config.toml"),
6037 "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6038 )
6039 .unwrap();
6040
6041 let mut service = HarnessSessionService::new();
6042 let response = service.handle(request(
6043 1,
6044 "harness.v1.profiles.list",
6045 json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6046 ));
6047 let rows = response["result"]["profiles"].as_array().unwrap();
6048 assert_eq!(rows.len(), 2, "{response}");
6049 let review = profile_row(&response, "codex", "review");
6050 assert_eq!(review["kind"], "codex_profile");
6051 assert_eq!(review["default"], true);
6052 assert_eq!(review["model"], "gpt-5.1-codex");
6053 assert_eq!(review["home"], Value::Null);
6054 assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6055
6056 let got = service.handle(request(
6057 2,
6058 "harness.v1.profiles.get",
6059 json!({
6060 "harness": "codex",
6061 "name": "fast",
6062 "homes": {"codex": codex_home.join("sessions")},
6063 }),
6064 ));
6065 assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6066 std::fs::remove_dir_all(&codex_home).ok();
6067 }
6068
6069 #[test]
6072 fn profiles_refuse_harnesses_without_the_concept() {
6073 let mut service = HarnessSessionService::new();
6074 let response = service.handle(request(
6075 1,
6076 "harness.v1.profiles.list",
6077 json!({"harness": "claude-code"}),
6078 ));
6079 assert_eq!(response["error"]["code"], -32020, "{response}");
6080
6081 let missing = service.handle(request(
6082 2,
6083 "harness.v1.profiles.get",
6084 json!({
6085 "harness": "hermes",
6086 "name": "no-such-profile",
6087 "homes": profile_fixture_homes(),
6088 }),
6089 ));
6090 assert_eq!(missing["error"]["code"], -32602, "{missing}");
6091 }
6092
6093 #[test]
6096 fn profiles_methods_are_advertised() {
6097 let mut service = HarnessSessionService::new();
6098 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6099 let methods = response["result"]["methods"].as_array().unwrap();
6100 for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6101 assert!(
6102 methods.iter().any(|entry| entry == method),
6103 "{method} is not advertised"
6104 );
6105 }
6106 }
6107
6108 fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6113 response["result"]["channels"]
6114 .as_array()
6115 .unwrap_or_else(|| panic!("no channels array in {response}"))
6116 .iter()
6117 .find(|row| row["harness"] == harness && row["name"] == name)
6118 .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6119 }
6120
6121 fn channels_list(harness: Option<&str>) -> Value {
6122 let mut params = json!({"homes": profile_fixture_homes()});
6123 if let Some(harness) = harness {
6124 params["harness"] = json!(harness);
6125 }
6126 HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6127 }
6128
6129 #[test]
6133 fn channels_list_reads_both_gateway_harnesses_uniformly() {
6134 let response = channels_list(None);
6135 assert_eq!(
6136 response["result"]["schema"],
6137 crate::channels::CHANNELS_SCHEMA
6138 );
6139
6140 let telegram = channel_row(&response, "hermes", "telegram");
6143 assert_eq!(telegram["kind"], "telegram");
6144 assert_eq!(telegram["enabled"], true);
6145 assert_eq!(telegram["configured"], true);
6146 assert_eq!(telegram["sessions"], 2);
6150 let api = channel_row(&response, "hermes", "api_server");
6151 assert_eq!(api["configured"], true, "extra.key is a credential key");
6152 assert_eq!(api["sessions"], 0);
6153 let webhook = channel_row(&response, "hermes", "webhook");
6154 assert_eq!(webhook["enabled"], false);
6155 assert_eq!(webhook["configured"], true);
6158
6159 let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6161 assert_eq!(linked["kind"], "slack");
6162 assert_eq!(linked["account"], "T0FIXTURE");
6163 assert_eq!(linked["enabled"], true);
6164 assert_eq!(linked["configured"], true);
6165 let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6166 assert_eq!(unlinked["enabled"], false);
6167 assert_eq!(
6168 unlinked["configured"], false,
6169 "an account with no credential key is not configured"
6170 );
6171 let telegram = channel_row(&response, "openclaw", "telegram");
6174 assert_eq!(telegram["account"], "hermes-fixture-bot");
6175 assert_eq!(telegram["configured"], true);
6176
6177 for row in response["result"]["channels"].as_array().unwrap() {
6179 assert_eq!(row["status"], "unknown", "{row}");
6180 }
6181 }
6182
6183 #[test]
6187 fn channels_rows_never_carry_a_fixture_secret() {
6188 let secrets = [
6189 "FAKE-TOKEN-DO-NOT-EMIT",
6190 "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6191 "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6192 "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6193 "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6194 ];
6195 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6197 let raw = format!(
6198 "{}{}",
6199 std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6200 std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6201 );
6202 for secret in secrets {
6203 assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6204 }
6205
6206 let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6207 for secret in secrets {
6208 assert!(
6209 !emitted.contains(secret),
6210 "`{secret}` leaked into a channel row: {emitted}"
6211 );
6212 }
6213 for row in channels_list(None)["result"]["channels"]
6216 .as_array()
6217 .unwrap()
6218 {
6219 for key in row.as_object().unwrap().keys() {
6220 let key = key.to_ascii_lowercase();
6221 assert!(
6222 !["token", "key", "secret", "password", "credential"]
6223 .iter()
6224 .any(|marker| key.ends_with(marker)),
6225 "`{key}` is a credential-shaped field on a channel row"
6226 );
6227 }
6228 }
6229 }
6230
6231 #[test]
6233 fn channels_status_reads_one_row_by_name() {
6234 let mut service = HarnessSessionService::new();
6235 let got = service.handle(request(
6236 1,
6237 "harness.v1.channels.status",
6238 json!({
6239 "harness": "openclaw",
6240 "name": "slack/T0FIXTURE",
6241 "homes": profile_fixture_homes(),
6242 }),
6243 ));
6244 assert_eq!(got["result"]["channel"]["kind"], "slack");
6245 assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6246 assert_eq!(got["result"]["channel"]["status"], "unknown");
6247
6248 let missing = service.handle(request(
6249 2,
6250 "harness.v1.channels.status",
6251 json!({
6252 "harness": "openclaw",
6253 "name": "no-such-channel",
6254 "homes": profile_fixture_homes(),
6255 }),
6256 ));
6257 assert_eq!(missing["error"]["code"], -32602, "{missing}");
6258 }
6259
6260 #[test]
6264 fn channels_refuse_harnesses_without_the_concept() {
6265 let response = channels_list(Some("claude-code"));
6266 assert_eq!(response["error"]["code"], -32020, "{response}");
6267 let codex = channels_list(Some("codex"));
6268 assert_eq!(codex["error"]["code"], -32020, "{codex}");
6269 }
6270
6271 #[test]
6273 fn channels_list_filters_by_harness() {
6274 let response = channels_list(Some("openclaw"));
6275 let rows = response["result"]["channels"].as_array().unwrap();
6276 assert!(!rows.is_empty(), "{response}");
6277 assert!(
6278 rows.iter().all(|row| row["harness"] == "openclaw"),
6279 "harness filter leaked: {response}"
6280 );
6281 }
6282
6283 #[test]
6286 fn channels_methods_are_advertised() {
6287 let mut service = HarnessSessionService::new();
6288 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6289 let methods = response["result"]["methods"].as_array().unwrap();
6290 for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6291 assert!(
6292 methods.iter().any(|entry| entry == method),
6293 "{method} is not advertised"
6294 );
6295 }
6296 }
6297
6298 #[test]
6303 fn orch6_story_fixture_matches_the_live_discovery_response() {
6304 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6305 .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6306 let mut result = hermes_discovery(hermes_query());
6307 result.as_object_mut().unwrap().remove("next_cursor");
6311 let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6312 if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6313 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6314 std::fs::write(&path, &rendered).unwrap();
6315 }
6316 let committed = std::fs::read_to_string(&path).unwrap_or_default();
6317 assert_eq!(
6318 committed, rendered,
6319 "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6320 re-run with SUPERCODE_UPDATE_FIXTURES=1"
6321 );
6322 }
6323
6324 fn pi_locator() -> SessionLocator {
6325 SessionLocator {
6326 harness: HarnessId::from(HarnessId::PI),
6327 session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6328 storage: StorageLocator::File {
6329 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6330 .join("tests/fixtures/pi_session.jsonl"),
6331 },
6332 }
6333 }
6334
6335 fn opencode_locator() -> SessionLocator {
6336 let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
6337 SessionLocator {
6338 harness: HarnessId::from(HarnessId::OPENCODE),
6339 session_id: session_id.into(),
6340 storage: StorageLocator::Sqlite {
6341 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6342 .join("tests/fixtures/opencode_fixture/opencode.db"),
6343 selector: session_id.into(),
6344 },
6345 }
6346 }
6347
6348 fn grok_locator() -> SessionLocator {
6349 SessionLocator {
6350 harness: HarnessId::from(HarnessId::GROK),
6351 session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
6352 storage: StorageLocator::File {
6353 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6354 .join("tests/fixtures/grok_session/chat_history.jsonl"),
6355 },
6356 }
6357 }
6358
6359 fn fixture_homes() -> Value {
6362 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6363 json!({
6364 "claude_code": fixtures.join("__absent__"),
6365 "codex": fixtures.join("__absent__"),
6366 "opencode": fixtures.join("__absent__"),
6367 "pi": fixtures.join("__absent__"),
6368 "agents": fixtures.join("__absent__"),
6369 "hermes": fixtures.join("hermes_home"),
6370 "openclaw": fixtures.join("openclaw_home"),
6371 })
6372 }
6373
6374 #[test]
6375 fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
6376 let root = std::env::temp_dir().join(format!(
6377 "supercode-preview-rpc-{}-{}",
6378 std::process::id(),
6379 std::time::SystemTime::now()
6380 .duration_since(std::time::UNIX_EPOCH)
6381 .unwrap()
6382 .as_nanos()
6383 ));
6384 std::fs::create_dir_all(&root).unwrap();
6385 for id in ["first", "second"] {
6386 std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
6387 json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
6388 json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
6389 )).unwrap();
6390 }
6391 let mut service = HarnessSessionService::new();
6392 let query = json!({
6393 "harnesses": ["codex"], "homes": {"codex": root},
6394 "query": "nebula", "search_previews": true, "limit": 1
6395 });
6396 let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
6397 assert!(first.get("error").is_none(), "{first}");
6398 assert_eq!(first["result"]["receipt"]["searched_previews"], true);
6399 assert_eq!(first["result"]["receipt"]["total_matched"], 2);
6400 let mut next_query = query.clone();
6401 next_query["cursor"] = first["result"]["next_cursor"].clone();
6402 let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
6403 assert_eq!(next["result"]["receipt"]["returned"], 1);
6404 assert_eq!(next["result"]["receipt"]["total_matched"], 2);
6405 assert_eq!(next["result"]["receipt"]["truncated"], false);
6406 assert_ne!(
6407 first["result"]["sessions"][0]["locator"],
6408 next["result"]["sessions"][0]["locator"]
6409 );
6410 let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
6411 assert!(
6412 refused["error"]["message"]
6413 .as_str()
6414 .unwrap()
6415 .contains("use sessions.discover"),
6416 "{refused}"
6417 );
6418 std::fs::remove_dir_all(root).unwrap();
6419 }
6420
6421 #[test]
6422 fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
6423 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
6424 let root = std::env::temp_dir().join(format!(
6425 "supercode-index-rpc-{}-{}",
6426 std::process::id(),
6427 std::time::SystemTime::now()
6428 .duration_since(std::time::UNIX_EPOCH)
6429 .unwrap()
6430 .as_nanos()
6431 ));
6432 std::fs::create_dir_all(&root).unwrap();
6433 for id in ["first", "second"] {
6434 std::fs::write(root.join(format!("{id}.jsonl")), format!(
6435 "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
6436 )).unwrap();
6437 }
6438 let mut service = HarnessSessionService::new();
6439 let opened = service.handle(request(
6440 1,
6441 "harness.v1.sessions.index.subscribe",
6442 json!({
6443 "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
6444 }),
6445 ));
6446 assert!(opened.get("error").is_none(), "{opened:#}");
6447 let subscription = opened["result"]["subscription"]
6448 .as_str()
6449 .unwrap()
6450 .to_owned();
6451 assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
6452 for params in [
6453 json!({"subscription": subscription, "limit": 0}),
6454 json!({"subscription": subscription, "limit": 2049}),
6455 json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
6456 json!({"subscription": "unknown", "limit": 2}),
6457 ] {
6458 let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
6459 assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
6460 }
6461 for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
6462 let response = service.handle(request(
6463 3,
6464 "harness.v1.sessions.index.resize",
6465 json!({
6466 "subscription": subscription, "limit": limit
6467 }),
6468 ));
6469 assert!(response.get("error").is_none(), "{response:#}");
6470 assert_eq!(response["result"]["subscription"], subscription);
6471 assert_eq!(response["result"]["revision"], revision);
6472 assert_eq!(
6473 response["result"]["initial"].as_array().unwrap().len(),
6474 limit
6475 );
6476 assert_eq!(response["result"]["receipt"]["total_matched"], 2);
6477 assert_eq!(service.index_subscriptions.len(), 1);
6478 }
6479 let removed = service.handle(request(
6480 4,
6481 "harness.v1.sessions.index.unsubscribe",
6482 json!({
6483 "subscription": subscription
6484 }),
6485 ));
6486 assert_eq!(removed["result"]["removed"], true);
6487 let stale = service.handle(request(
6488 5,
6489 "harness.v1.sessions.index.resize",
6490 json!({
6491 "subscription": subscription, "limit": 1
6492 }),
6493 ));
6494 assert_eq!(stale["error"]["code"], -32602);
6495 drop(service);
6496 std::fs::remove_dir_all(root).unwrap();
6497 }
6498
6499 fn skills_rows(params: Value) -> Vec<Value> {
6500 let response =
6501 HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
6502 assert!(response.get("error").is_none(), "{response:#}");
6503 response["result"].as_array().cloned().unwrap_or_default()
6504 }
6505
6506 #[test]
6509 fn skills_list_reads_the_hermes_and_openclaw_roots() {
6510 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6511 let rows = skills_rows(json!({
6512 "homes": fixture_homes(),
6513 "cwd": fixtures.join("hermes_home"),
6514 }));
6515 let arxiv = rows
6516 .iter()
6517 .find(|row| row["name"] == json!("arxiv-search"))
6518 .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
6519 assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
6520 assert_eq!(arxiv["scope"], json!("user"));
6521 assert_eq!(arxiv["version"], json!("1.4.0"));
6522 assert!(arxiv["location"]
6523 .as_str()
6524 .unwrap()
6525 .ends_with("hermes_home/skills/research/arxiv"));
6526
6527 let bare = rows
6529 .iter()
6530 .find(|row| row["name"] == json!("bare-skill"))
6531 .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
6532 assert_eq!(bare["enabled"], json!(null));
6533 assert!(bare.get("description").is_none());
6534
6535 let demo = rows
6536 .iter()
6537 .find(|row| row["name"] == json!("clawhub-demo"))
6538 .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
6539 assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
6540 assert_eq!(demo["scope"], json!("managed"));
6541 assert_eq!(demo["enabled"], json!(false));
6542 }
6543
6544 #[test]
6546 fn skills_list_filters_by_harness_and_scope() {
6547 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6548 let hermes = skills_rows(json!({
6549 "homes": fixture_homes(),
6550 "cwd": fixtures.join("hermes_home"),
6551 "harness": HarnessId::HERMES,
6552 }));
6553 assert!(!hermes.is_empty());
6554 assert!(hermes
6555 .iter()
6556 .all(|row| row["harness"] == json!(HarnessId::HERMES)));
6557
6558 let managed = skills_rows(json!({
6559 "homes": fixture_homes(),
6560 "cwd": fixtures.join("openclaw_home"),
6561 "harness": HarnessId::OPENCLAW,
6562 "scope": "managed",
6563 }));
6564 assert_eq!(managed.len(), 1, "{managed:#?}");
6565 assert_eq!(managed[0]["name"], json!("clawhub-demo"));
6566
6567 let bundled = skills_rows(json!({
6568 "homes": fixture_homes(),
6569 "cwd": fixtures.join("openclaw_home"),
6570 "harness": HarnessId::OPENCLAW,
6571 "scope": "bundled",
6572 }));
6573 assert!(bundled.is_empty(), "{bundled:#?}");
6574 }
6575
6576 #[test]
6579 fn skills_list_refuses_an_unknown_harness() {
6580 let response = HarnessSessionService::new().handle(request(
6581 1,
6582 "harness.v1.skills.list",
6583 json!({"harness": "not-a-harness", "homes": fixture_homes()}),
6584 ));
6585 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6586 assert!(response["error"]["message"]
6587 .as_str()
6588 .unwrap()
6589 .contains("not-a-harness"));
6590 }
6591
6592 #[test]
6594 fn skills_list_is_an_advertised_method_and_sdk_operation() {
6595 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
6596 assert_eq!(
6597 SdkOperation::from_method("harness.v1.skills.list"),
6598 Some(SdkOperation::SkillsList)
6599 );
6600 }
6601
6602 #[test]
6606 fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
6607 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
6608 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
6609 assert_eq!(
6610 SdkOperation::from_method("harness.v1.skills.install"),
6611 Some(SdkOperation::SkillsInstall)
6612 );
6613 assert_eq!(
6614 SdkOperation::from_method("harness.v1.skills.remove"),
6615 Some(SdkOperation::SkillsRemove)
6616 );
6617 }
6618
6619 #[test]
6623 fn skills_install_and_remove_drive_the_directory_door() {
6624 let root = std::env::temp_dir().join(format!(
6625 "supercode-orch22-rpc-{}-{}",
6626 std::process::id(),
6627 std::time::SystemTime::now()
6628 .duration_since(std::time::UNIX_EPOCH)
6629 .unwrap()
6630 .as_nanos()
6631 ));
6632 let source = root.join("probe-src");
6633 std::fs::create_dir_all(&source).unwrap();
6634 std::fs::write(
6635 source.join("SKILL.md"),
6636 "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
6637 )
6638 .unwrap();
6639 let homes = json!({
6640 "claude_code": root.join("claude_home"),
6641 "codex": root.join("__absent__"),
6642 "opencode": root.join("__absent__"),
6643 "pi": root.join("__absent__"),
6644 "hermes": root.join("__absent__"),
6645 "openclaw": root.join("__absent__"),
6646 "agents": root.join("__absent__"),
6647 });
6648
6649 let mut service = HarnessSessionService::new();
6650 let installed = service.handle(request(
6651 1,
6652 "harness.v1.skills.install",
6653 json!({
6654 "harness": HarnessId::CLAUDE_CODE,
6655 "source": source,
6656 "scope": "user",
6657 "cwd": root,
6658 "homes": homes,
6659 }),
6660 ));
6661 let result = &installed["result"];
6662 assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
6663 assert_eq!(result["verb"], json!("install"));
6664 assert!(result["ran"]
6665 .as_str()
6666 .is_some_and(|ran| ran.starts_with("cp -R ")));
6667 assert_eq!(result["skill"]["scope"], json!("user"));
6668
6669 let removed = service.handle(request(
6670 2,
6671 "harness.v1.skills.remove",
6672 json!({
6673 "harness": HarnessId::CLAUDE_CODE,
6674 "name": "orch22-rpc",
6675 "scope": "user",
6676 "cwd": root,
6677 "homes": homes,
6678 }),
6679 ));
6680 assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
6681 assert!(!root.join("claude_home/skills/orch22-rpc").exists());
6682 std::fs::remove_dir_all(&root).ok();
6683 }
6684
6685 #[test]
6688 fn skills_remove_refuses_openclaw_at_the_pin() {
6689 let response = HarnessSessionService::new().handle(request(
6690 1,
6691 "harness.v1.skills.remove",
6692 json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
6693 ));
6694 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6695 assert!(response["error"]["message"]
6696 .as_str()
6697 .unwrap()
6698 .contains("no `skills remove` verb"));
6699 }
6700
6701 #[test]
6704 fn skills_install_refuses_a_harness_without_a_skills_root() {
6705 let response = HarnessSessionService::new().handle(request(
6706 1,
6707 "harness.v1.skills.install",
6708 json!({"harness": "not-a-harness", "source": "/tmp/x"}),
6709 ));
6710 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6711 assert!(response["error"]["message"]
6712 .as_str()
6713 .unwrap()
6714 .contains("not-a-harness"));
6715 }
6716
6717 fn memory_homes() -> Value {
6725 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6726 json!({
6727 "claude_code": fixtures.join("__absent__"),
6728 "codex": fixtures.join("__absent__"),
6729 "opencode": fixtures.join("__absent__"),
6730 "pi": fixtures.join("__absent__"),
6731 "grok": fixtures.join("__absent__"),
6732 "gemini": fixtures.join("__absent__"),
6733 "goose": fixtures.join("__absent__"),
6734 "supercode": fixtures.join("__absent__"),
6735 "hermes": fixtures.join("hermes_home/state.db"),
6736 "openclaw": fixtures.join("openclaw_home"),
6737 })
6738 }
6739
6740 fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
6741 let response = HarnessSessionService::new().handle(request(1, method, params));
6742 assert!(response.get("error").is_none(), "{response:#}");
6743 assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
6744 response["result"][key]
6745 .as_array()
6746 .cloned()
6747 .unwrap_or_default()
6748 }
6749
6750 fn memory_documents(params: Value) -> Vec<Value> {
6751 memory_call_ok("harness.v1.memory.show", params, "documents")
6752 }
6753
6754 fn memory_matches(params: Value) -> Vec<Value> {
6755 memory_call_ok("harness.v1.memory.search", params, "matches")
6756 }
6757
6758 fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
6759 rows.iter()
6760 .find(|row| row["profile"] == profile && row["name"] == name)
6761 .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
6762 }
6763
6764 #[test]
6767 fn memory_show_reads_the_hermes_profile_homes() {
6768 let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
6769
6770 let notes = find_document(&rows, "default", "MEMORY.md");
6771 assert_eq!(notes["harness"], "hermes");
6772 assert_eq!(notes["scope"], "user");
6773 assert!(notes["size"].as_u64().unwrap() > 0);
6774 assert!(notes["updated_at"].is_string(), "{notes:#?}");
6775 assert!(notes.get("content").is_none(), "{notes:#?}");
6777 assert_eq!(notes["truncated"], true);
6778 assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
6779
6780 let user = find_document(&rows, "default", "USER.md");
6781 assert_eq!(user["scope"], "user");
6782 assert!(user["preview"]
6783 .as_array()
6784 .unwrap()
6785 .iter()
6786 .any(|line| line.as_str().unwrap().contains("neovim")));
6787
6788 let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
6789 assert!(topic["path"]
6790 .as_str()
6791 .unwrap()
6792 .ends_with("hermes_home/memories/2026-09-01-notes.md"));
6793
6794 let coder = find_document(&rows, "coder", "MEMORY.md");
6796 assert_eq!(coder["scope"], "profile");
6797 assert!(coder["path"]
6798 .as_str()
6799 .unwrap()
6800 .ends_with("hermes_home/profiles/coder/MEMORY.md"));
6801 }
6802
6803 #[test]
6806 fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
6807 let rows = memory_documents(json!({
6808 "harness": "hermes",
6809 "profile": "coder",
6810 "full": true,
6811 "homes": memory_homes(),
6812 }));
6813 assert!(
6814 rows.iter().all(|row| row["profile"] == "coder"),
6815 "{rows:#?}"
6816 );
6817 let coder = find_document(&rows, "coder", "MEMORY.md");
6818 assert!(coder["content"]
6819 .as_str()
6820 .expect("full returns the body")
6821 .contains("anthropic/claude-opus-4-8"));
6822 }
6823
6824 #[test]
6828 fn memory_show_reads_the_openclaw_agent_workspaces() {
6829 let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
6830
6831 let main = find_document(&rows, "main", "MEMORY.md");
6832 assert_eq!(main["scope"], "agent");
6833 assert!(main["path"]
6834 .as_str()
6835 .unwrap()
6836 .ends_with("openclaw_home/workspace/MEMORY.md"));
6837
6838 let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
6839 assert!(topic["path"]
6840 .as_str()
6841 .unwrap()
6842 .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
6843
6844 let design = find_document(&rows, "design", "MEMORY.md");
6845 assert!(design["path"]
6846 .as_str()
6847 .unwrap()
6848 .ends_with("openclaw_home/workspace-design/MEMORY.md"));
6849 }
6850
6851 #[test]
6854 fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
6855 let scratch = std::env::temp_dir().join(format!(
6856 "supercode-orch12-cc-{}-{}",
6857 std::process::id(),
6858 std::time::SystemTime::now()
6859 .duration_since(std::time::UNIX_EPOCH)
6860 .unwrap()
6861 .as_nanos()
6862 ));
6863 let project = scratch.join("repo");
6864 std::fs::create_dir_all(project.join(".git")).unwrap();
6865 let worktree = project.join("crates/harness");
6868 std::fs::create_dir_all(&worktree).unwrap();
6869 let slug: String = project
6870 .to_string_lossy()
6871 .chars()
6872 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
6873 .collect();
6874 let projects = scratch.join("claude/projects");
6875 let memory = projects.join(&slug).join("memory");
6876 std::fs::create_dir_all(&memory).unwrap();
6877 std::fs::write(
6878 memory.join("MEMORY.md"),
6879 "# index\n- [build box](build-box.md) — the pinned harnesses\n",
6880 )
6881 .unwrap();
6882 std::fs::write(
6883 memory.join("build-box.md"),
6884 "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
6885 )
6886 .unwrap();
6887
6888 let mut homes = memory_homes();
6889 homes["claude_code"] = json!(projects);
6890 let rows = memory_documents(json!({
6891 "harness": "claude-code",
6892 "cwd": worktree,
6893 "homes": homes,
6894 }));
6895 let index = find_document(&rows, &slug, "MEMORY.md");
6896 assert_eq!(index["harness"], "claude-code");
6897 assert_eq!(index["scope"], "project");
6898 let topic = find_document(&rows, &slug, "build-box.md");
6899 assert!(topic["preview"]
6900 .as_array()
6901 .unwrap()
6902 .iter()
6903 .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
6904
6905 let hits = memory_matches(json!({
6906 "harness": "claude-code",
6907 "query": "pinned harnesses",
6908 "cwd": worktree,
6909 "homes": homes,
6910 }));
6911 assert_eq!(hits.len(), 1, "{hits:#?}");
6912 assert_eq!(hits[0]["name"], "MEMORY.md");
6913 assert_eq!(hits[0]["line"], 2);
6914
6915 let _ = std::fs::remove_dir_all(&scratch);
6916 }
6917
6918 #[test]
6924 fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
6925 let state = std::env::temp_dir().join(format!(
6926 "supercode-orch12-oc-{}-{}",
6927 std::process::id(),
6928 std::time::SystemTime::now()
6929 .duration_since(std::time::UNIX_EPOCH)
6930 .unwrap()
6931 .as_nanos()
6932 ));
6933 std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
6935 std::fs::create_dir_all(state.join("workspace")).unwrap();
6936 std::fs::write(
6937 state.join("workspace/MEMORY.md"),
6938 "the gateway websocket needs credentials\n",
6939 )
6940 .unwrap();
6941
6942 let mut homes = memory_homes();
6943 homes["openclaw"] = json!(state);
6944 let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
6945 assert_eq!(rows.len(), 1, "{rows:#?}");
6946 let row = find_document(&rows, "main", "MEMORY.md");
6947 assert_eq!(row["scope"], "agent");
6948 assert!(row["path"]
6949 .as_str()
6950 .unwrap()
6951 .ends_with("workspace/MEMORY.md"));
6952
6953 let _ = std::fs::remove_dir_all(&state);
6954 }
6955
6956 #[test]
6959 fn memory_search_reports_hits_by_line_and_misses_as_empty() {
6960 let hit = memory_matches(json!({
6961 "harness": "hermes",
6962 "query": "NEOVIM",
6963 "homes": memory_homes(),
6964 }));
6965 assert_eq!(hit.len(), 1, "{hit:#?}");
6966 assert_eq!(hit[0]["harness"], "hermes");
6967 assert_eq!(hit[0]["name"], "USER.md");
6968 assert_eq!(hit[0]["scope"], "user");
6969 assert_eq!(hit[0]["line"], 5);
6970 assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
6971
6972 let regex = memory_matches(json!({
6974 "harness": "hermes",
6975 "query": "neo(vim|vi)",
6976 "regex": true,
6977 "homes": memory_homes(),
6978 }));
6979 assert_eq!(regex.len(), 1, "{regex:#?}");
6980
6981 let miss = memory_matches(json!({
6982 "harness": "hermes",
6983 "query": "no-memory-line-says-this",
6984 "homes": memory_homes(),
6985 }));
6986 assert!(miss.is_empty(), "{miss:#?}");
6987 }
6988
6989 #[test]
6992 fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
6993 for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
6994 let response = HarnessSessionService::new().handle(request(
6995 1,
6996 method,
6997 json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
6998 ));
6999 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7000 assert!(response["error"]["message"]
7001 .as_str()
7002 .unwrap()
7003 .contains("codex"));
7004 }
7005
7006 let response = HarnessSessionService::new().handle(request(
7007 1,
7008 "harness.v1.memory.show",
7009 json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7010 ));
7011 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7012
7013 let response = HarnessSessionService::new().handle(request(
7015 1,
7016 "harness.v1.memory.show",
7017 json!({"homes": memory_homes()}),
7018 ));
7019 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7020 }
7021
7022 #[test]
7024 fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7025 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7026 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7027 assert_eq!(
7028 SdkOperation::from_method("harness.v1.memory.show"),
7029 Some(SdkOperation::MemoryShow)
7030 );
7031 assert_eq!(
7032 SdkOperation::from_method("harness.v1.memory.search"),
7033 Some(SdkOperation::MemorySearch)
7034 );
7035 }
7036
7037 struct RequestingRuntime {
7042 handle: RuntimeHandle,
7043 events: std::collections::VecDeque<HarnessEvent>,
7044 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7045 }
7046
7047 #[async_trait]
7048 impl RuntimeConnection for RequestingRuntime {
7049 fn handle(&self) -> &RuntimeHandle {
7050 &self.handle
7051 }
7052
7053 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7054 unreachable!("this runtime only raises requests")
7055 }
7056
7057 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7058 match self.events.pop_front() {
7059 Some(event) => Ok(Some(event)),
7060 None => std::future::pending().await,
7064 }
7065 }
7066
7067 async fn interrupt(&mut self) -> crate::Result<()> {
7068 Ok(())
7069 }
7070
7071 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7072 self.answered
7076 .lock()
7077 .unwrap_or_else(std::sync::PoisonError::into_inner)
7078 .push(json!({"request_id": request_id, "response": response}));
7079 Ok(())
7080 }
7081
7082 async fn close(&mut self) -> crate::Result<()> {
7083 Ok(())
7084 }
7085 }
7086
7087 fn requesting_runtime(
7088 harness: &str,
7089 events: Vec<HarnessEvent>,
7090 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7091 ) -> Box<dyn RuntimeConnection> {
7092 requesting_runtime_named(harness, "hermes-live-session", events, answered)
7093 }
7094
7095 fn requesting_runtime_named(
7096 harness: &str,
7097 runtime_id: &str,
7098 events: Vec<HarnessEvent>,
7099 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7100 ) -> Box<dyn RuntimeConnection> {
7101 Box::new(RequestingRuntime {
7102 handle: RuntimeHandle {
7103 harness: HarnessId::from(harness),
7104 runtime_id: runtime_id.into(),
7105 endpoint: RuntimeEndpoint::LocalProcess {
7106 pid: None,
7107 command: vec!["hermes-acp".into()],
7108 protocol: "acp".into(),
7109 },
7110 },
7111 events: events.into(),
7112 answered,
7113 })
7114 }
7115
7116 fn permission_event(id: u64, title: &str) -> HarnessEvent {
7117 HarnessEvent {
7118 sequence: None,
7119 kind: "session/request_permission".into(),
7120 payload: json!({
7121 "jsonrpc": "2.0",
7122 "id": id,
7123 "method": "session/request_permission",
7124 "params": {
7125 "sessionId": "hermes-live-session",
7126 "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7127 "options": [
7128 {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7129 {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7130 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7131 ],
7132 },
7133 }),
7134 }
7135 }
7136
7137 fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7138 let response = service.handle(request(1, "harness.v1.approvals.list", params));
7139 assert!(response.get("error").is_none(), "{response:#}");
7140 response["result"].clone()
7141 }
7142
7143 #[tokio::test]
7151 async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7152 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7153 let mut service = HarnessSessionService::new();
7154 service.runtimes.insert(
7155 "runtime-cc".into(),
7156 requesting_runtime_named(
7157 HarnessId::CLAUDE_CODE,
7158 "claude-live-session",
7159 vec![HarnessEvent {
7160 sequence: None,
7161 kind: "control_request".into(),
7162 payload: json!({
7163 "type": "control_request",
7164 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7165 "request": {
7166 "subtype": "can_use_tool",
7167 "tool_name": "Bash",
7168 "display_name": "Bash",
7169 "input": {"command": "touch probe-artifact.txt"},
7170 "tool_use_id": "toolu_mock_1",
7171 },
7172 }),
7173 }],
7174 answered.clone(),
7175 ),
7176 );
7177
7178 let notifications = service.poll_runtimes().await;
7179 assert_eq!(notifications.len(), 1, "{notifications:#?}");
7180
7181 let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7182 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7183 let row = &rows[0];
7184 assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7185 assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7186 assert_eq!(row["status"], "pending");
7187 assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7188 assert_eq!(row["runtime_id"], "claude-live-session");
7189 assert_eq!(
7190 row["options"]
7191 .as_array()
7192 .unwrap()
7193 .iter()
7194 .map(|option| option["id"].as_str().unwrap())
7195 .collect::<Vec<_>>(),
7196 vec!["allow", "deny"],
7197 );
7198
7199 let response = resolve(
7200 &mut service,
7201 json!({"id": row["id"], "decision": "allow_once"}),
7202 )
7203 .await;
7204 assert!(response.get("error").is_none(), "{response:#}");
7205 assert_eq!(response["result"]["option_id"], "allow");
7206 assert_eq!(
7207 answered
7208 .lock()
7209 .unwrap_or_else(std::sync::PoisonError::into_inner)
7210 .as_slice(),
7211 &[json!({
7212 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7213 "response": {"behavior": "allow"},
7214 })],
7215 );
7216 assert_eq!(
7217 approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7218 .as_array()
7219 .map(Vec::len),
7220 Some(0),
7221 );
7222 }
7223
7224 #[tokio::test]
7228 async fn a_live_permission_request_lists_until_it_is_answered() {
7229 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7230 let mut service = HarnessSessionService::new();
7231 service.runtimes.insert(
7232 "runtime-1".into(),
7233 requesting_runtime(
7234 HarnessId::HERMES,
7235 vec![permission_event(7, "rm -rf build")],
7236 answered.clone(),
7237 ),
7238 );
7239
7240 let notifications = service.poll_runtimes().await;
7241 assert_eq!(notifications.len(), 1, "{notifications:#?}");
7242
7243 let rows = approvals(&mut service, json!({}));
7244 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7245 let row = &rows[0];
7246 assert_eq!(row["id"], "runtime-1/7");
7247 assert_eq!(row["harness"], HarnessId::HERMES);
7248 assert_eq!(row["kind"], "live");
7249 assert_eq!(row["status"], "pending");
7250 assert_eq!(row["subject"], "rm -rf build");
7251 assert_eq!(row["session_id"], "hermes-live-session");
7252 assert_eq!(row["runtime_id"], "hermes-live-session");
7253 assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7254 assert!(
7255 row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7256 "{row:#}"
7257 );
7258 assert_eq!(
7259 row["options"]
7260 .as_array()
7261 .unwrap()
7262 .iter()
7263 .map(|option| option["id"].as_str().unwrap())
7264 .collect::<Vec<_>>(),
7265 vec!["allow_once", "allow_for_session", "deny"],
7266 );
7267
7268 assert_eq!(
7270 approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7271 .as_array()
7272 .map(Vec::len),
7273 Some(1),
7274 );
7275 assert_eq!(
7276 approvals(&mut service, json!({"session": "some-other-session"}))
7277 .as_array()
7278 .map(Vec::len),
7279 Some(0),
7280 );
7281
7282 let response = service
7283 .handle_async(request(
7284 2,
7285 "harness.v1.runtimes.respond",
7286 json!({
7287 "connection": "runtime-1",
7288 "request_id": 7,
7289 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7290 }),
7291 ))
7292 .await;
7293 assert!(response.get("error").is_none(), "{response:#}");
7294 assert_eq!(
7295 answered
7296 .lock()
7297 .unwrap_or_else(std::sync::PoisonError::into_inner)
7298 .as_slice(),
7299 &[json!({
7300 "request_id": 7,
7301 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7302 })],
7303 );
7304
7305 let rows = approvals(&mut service, json!({}));
7306 assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7307 }
7308
7309 #[test]
7312 fn queued_subagent_approvals_list_through_the_same_door() {
7313 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7314 crate::subagents::QueuedApproval {
7315 child_agent_id: "child-7".into(),
7316 tool: "shell".into(),
7317 subject: Some("cargo publish --dry-run".into()),
7318 queued_at_ms: 1,
7319 outcome: None,
7320 },
7321 crate::subagents::QueuedApproval {
7322 child_agent_id: "child-8".into(),
7323 tool: "write_file".into(),
7324 subject: None,
7325 queued_at_ms: 2,
7326 outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7327 },
7328 ]));
7329 let mut service = HarnessSessionService::new();
7330 service.observe_subagent_approvals(queue);
7331
7332 let rows = approvals(&mut service, json!({}));
7333 assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
7334 assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
7335 assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
7336 assert_eq!(rows[0]["status"], "pending");
7337 assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
7338 assert_eq!(rows[1]["status"], "denied");
7339 assert!(rows[1]["options"].as_array().unwrap().is_empty());
7340
7341 let only = approvals(&mut service, json!({"session": "child-8"}));
7343 assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
7344 assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
7345 }
7346
7347 #[test]
7353 fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
7354 let response = HarnessSessionService::new().handle(request(
7355 1,
7356 "harness.v1.approvals.list",
7357 json!({"harness": "not-a-harness"}),
7358 ));
7359 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7360 assert!(response["error"]["message"]
7361 .as_str()
7362 .unwrap()
7363 .contains("not-a-harness"));
7364 for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
7365 let response = HarnessSessionService::new().handle(request(
7366 1,
7367 "harness.v1.approvals.list",
7368 json!({"harness": harness}),
7369 ));
7370 assert!(response.get("error").is_none(), "{harness}: {response:#}");
7371 }
7372 }
7373
7374 #[test]
7378 fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
7379 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
7380 assert_eq!(
7381 SdkOperation::from_method("harness.v1.approvals.list"),
7382 Some(SdkOperation::ApprovalsList)
7383 );
7384 let registry = harness_support_registry();
7385 for id in [
7386 HarnessId::HERMES,
7387 HarnessId::OPENCLAW,
7388 HarnessId::CODEX,
7389 HarnessId::CLAUDE_CODE,
7392 ] {
7393 let concept = registry
7394 .harnesses
7395 .iter()
7396 .find(|harness| harness.id.as_str() == id)
7397 .unwrap()
7398 .orchestration
7399 .concepts
7400 .iter()
7401 .find(|concept| concept.concept == "pending_request")
7402 .unwrap();
7403 assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
7404 assert!(concept
7405 .methods
7406 .iter()
7407 .any(|method| method == "harness.v1.approvals.list"));
7408 }
7409 }
7410
7411 async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
7414 service
7415 .handle_async(request(3, "harness.v1.approvals.resolve", params))
7416 .await
7417 }
7418
7419 #[tokio::test]
7423 async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
7424 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7425 let mut service = HarnessSessionService::new();
7426 service.runtimes.insert(
7427 "runtime-1".into(),
7428 requesting_runtime(
7429 HarnessId::HERMES,
7430 vec![permission_event(7, "rm -rf build")],
7431 answered.clone(),
7432 ),
7433 );
7434 service.poll_runtimes().await;
7435
7436 let rows = approvals(&mut service, json!({}));
7437 assert_eq!(rows[0]["id"], "runtime-1/7");
7438
7439 let response = resolve(
7440 &mut service,
7441 json!({"id": "runtime-1/7", "decision": "allow_once"}),
7442 )
7443 .await;
7444 assert!(response.get("error").is_none(), "{response:#}");
7445 assert_eq!(
7446 response["result"],
7447 json!({
7448 "id": "runtime-1/7",
7449 "decision": "allow_once",
7450 "option_id": "allow_once",
7451 "resolved": true,
7452 }),
7453 );
7454 assert_eq!(
7456 answered
7457 .lock()
7458 .unwrap_or_else(std::sync::PoisonError::into_inner)
7459 .as_slice(),
7460 &[json!({
7461 "request_id": 7,
7462 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7463 })],
7464 );
7465 assert_eq!(
7467 approvals(&mut service, json!({})).as_array().map(Vec::len),
7468 Some(0),
7469 );
7470 let response = resolve(
7472 &mut service,
7473 json!({"id": "runtime-1/7", "decision": "allow_once"}),
7474 )
7475 .await;
7476 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7477 }
7478
7479 #[tokio::test]
7482 async fn deny_selects_the_requests_own_reject_option() {
7483 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7484 let mut service = HarnessSessionService::new();
7485 service.runtimes.insert(
7486 "runtime-1".into(),
7487 requesting_runtime(
7488 HarnessId::HERMES,
7489 vec![permission_event(11, "git push --force")],
7490 answered.clone(),
7491 ),
7492 );
7493 service.poll_runtimes().await;
7494
7495 let response = resolve(
7496 &mut service,
7497 json!({"id": "runtime-1/11", "decision": "deny"}),
7498 )
7499 .await;
7500 assert!(response.get("error").is_none(), "{response:#}");
7501 assert_eq!(response["result"]["option_id"], "deny");
7503 assert_eq!(
7504 answered
7505 .lock()
7506 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
7507 json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
7508 );
7509 assert_eq!(
7510 approvals(&mut service, json!({})).as_array().map(Vec::len),
7511 Some(0),
7512 );
7513 }
7514
7515 #[tokio::test]
7518 async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
7519 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7520 let mut service = HarnessSessionService::new();
7521 let mut event = permission_event(3, "rm -rf build");
7522 event.payload["params"]["options"] = json!([
7525 {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
7526 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7527 ]);
7528 service.runtimes.insert(
7529 "runtime-1".into(),
7530 requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
7531 );
7532 service.poll_runtimes().await;
7533
7534 let response = resolve(
7535 &mut service,
7536 json!({"id": "runtime-1/3", "decision": "allow_always"}),
7537 )
7538 .await;
7539 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7540 let message = response["error"]["message"].as_str().unwrap();
7541 assert!(message.contains("allow_always"), "{message}");
7542 assert!(message.contains("allow_once, deny"), "{message}");
7543 assert!(answered
7545 .lock()
7546 .unwrap_or_else(std::sync::PoisonError::into_inner)
7547 .is_empty());
7548 assert_eq!(
7549 approvals(&mut service, json!({})).as_array().map(Vec::len),
7550 Some(1),
7551 );
7552 }
7553
7554 #[tokio::test]
7558 async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
7559 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7560 crate::subagents::QueuedApproval {
7561 child_agent_id: "child-7".into(),
7562 tool: "shell".into(),
7563 subject: Some("cargo publish --dry-run".into()),
7564 queued_at_ms: 1,
7565 outcome: None,
7566 },
7567 ]));
7568 let mut service = HarnessSessionService::new();
7569 service.observe_subagent_approvals(queue.clone());
7570 let row = approvals(&mut service, json!({}))[0]["id"]
7571 .as_str()
7572 .unwrap()
7573 .to_string();
7574 assert_eq!(row, "supercode/subagent/child-7/1/0");
7575
7576 let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
7577 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7578 let message = response["error"]["message"].as_str().unwrap();
7579 assert!(message.contains("queued subagent record"), "{message}");
7580 assert!(message.contains("request"), "{message}");
7581 assert!(queue
7583 .lock()
7584 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
7585 .outcome
7586 .is_none());
7587 }
7588
7589 #[tokio::test]
7592 async fn an_unknown_row_and_a_missing_decision_are_both_named() {
7593 let mut service = HarnessSessionService::new();
7594 let response = resolve(
7595 &mut service,
7596 json!({"id": "runtime-9/4", "decision": "deny"}),
7597 )
7598 .await;
7599 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7600 assert!(response["error"]["message"]
7601 .as_str()
7602 .unwrap()
7603 .contains("runtime-9/4"));
7604
7605 let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
7606 let message = response["error"]["message"].as_str().unwrap();
7607 assert!(
7608 message.contains("allow_once | allow_always | deny"),
7609 "{message}"
7610 );
7611
7612 let response = resolve(
7613 &mut service,
7614 json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
7615 )
7616 .await;
7617 assert!(response["error"]["message"]
7618 .as_str()
7619 .unwrap()
7620 .contains("not both"));
7621 }
7622
7623 #[test]
7627 fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
7628 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
7629 assert_eq!(
7630 SdkOperation::from_method("harness.v1.approvals.resolve"),
7631 Some(SdkOperation::ApprovalsResolve)
7632 );
7633 assert_eq!(
7634 SdkOperation::ApprovalsResolve.action_name(),
7635 "approvals_resolve"
7636 );
7637 let registry = harness_support_registry();
7638 for id in [
7639 HarnessId::HERMES,
7640 HarnessId::OPENCLAW,
7641 HarnessId::CODEX,
7642 HarnessId::CLAUDE_CODE,
7645 ] {
7646 let concept = registry
7647 .harnesses
7648 .iter()
7649 .find(|harness| harness.id.as_str() == id)
7650 .unwrap()
7651 .orchestration
7652 .concepts
7653 .iter()
7654 .find(|concept| concept.concept == "pending_request")
7655 .unwrap();
7656 assert_eq!(
7657 concept.controlled,
7658 crate::ImplementationKind::BuiltIn,
7659 "{id}"
7660 );
7661 assert!(
7662 concept
7663 .methods
7664 .iter()
7665 .any(|method| method == "harness.v1.approvals.resolve"),
7666 "{id}"
7667 );
7668 }
7669 }
7670
7671 #[test]
7672 fn capabilities_are_explicit_and_versioned() {
7673 let mut service = HarnessSessionService::new();
7674 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
7675 assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
7676 assert_eq!(
7677 response["result"]["sdk"]["schema_version"],
7678 crate::SDK_SCHEMA_VERSION
7679 );
7680 assert_eq!(
7681 response["result"]["sdk"]["operations"]
7682 .as_array()
7683 .unwrap()
7684 .len(),
7685 SdkOperation::ALL.len()
7686 );
7687 assert_eq!(
7688 response["result"]["harnesses"].as_array().unwrap().len(),
7689 11
7690 );
7691 assert!(response["result"]["harnesses"]
7692 .as_array()
7693 .unwrap()
7694 .iter()
7695 .any(|harness| harness == HarnessId::GROK));
7696 assert!(response["result"]["harnesses"]
7697 .as_array()
7698 .unwrap()
7699 .iter()
7700 .any(|harness| harness == HarnessId::GOOSE));
7701 }
7702
7703 #[test]
7704 fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
7705 let noisy_stderr = crate::HarnessEvent {
7706 sequence: None,
7707 kind: "transport_stderr".into(),
7708 payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
7709 };
7710 assert_eq!(handshake_event_failure(&noisy_stderr), None);
7711
7712 let closed = crate::HarnessEvent {
7713 sequence: None,
7714 kind: "transport_closed".into(),
7715 payload: json!({}),
7716 };
7717 assert!(handshake_event_failure(&closed).is_some());
7718 }
7719
7720 #[tokio::test]
7721 async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
7722 let mut service = HarnessSessionService::new();
7723 service
7724 .runtimes
7725 .insert("raw-eof".into(), ending_runtime(None));
7726 service.runtimes.insert(
7727 "explicit-close".into(),
7728 ending_runtime(Some(HarnessEvent {
7729 sequence: None,
7730 kind: "transport_closed".into(),
7731 payload: json!({"message": "native transport exited"}),
7732 })),
7733 );
7734
7735 let notifications = service.poll_runtimes().await;
7736
7737 assert_eq!(notifications.len(), 2);
7738 assert!(notifications
7739 .iter()
7740 .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
7741 assert!(notifications.iter().all(|notification| {
7742 notification["params"]["session_id"] == "ending-session"
7743 && notification["params"]["connection"].is_string()
7744 }));
7745 let mut sequences = notifications
7746 .iter()
7747 .filter_map(|notification| notification["params"]["sequence"].as_u64())
7748 .collect::<Vec<_>>();
7749 sequences.sort_unstable();
7750 assert_eq!(sequences, vec![1, 2]);
7751 assert!(service.runtimes.is_empty());
7752 }
7753
7754 #[test]
7755 fn support_report_and_grok_default_binding_share_the_registry() {
7756 let mut service = HarnessSessionService::new();
7757 let response = service.handle(request(1, "harness.v1.support.report", json!({})));
7758 assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
7759 let params = RuntimeBackendParams {
7760 harness: HarnessId::from(HarnessId::GROK),
7761 protocol: None,
7762 launch: None,
7763 base_url: None,
7764 policy: RuntimePolicy::Default,
7765 };
7766 let backend = match runtime_backend(¶ms) {
7767 Ok(backend) => backend,
7768 Err(_) => panic!("Grok should bind through its registered ACP launch"),
7769 };
7770 assert_eq!(backend.harness().as_str(), HarnessId::GROK);
7771 assert!(backend.capabilities().start_session);
7772 let registered = harness_support_registry()
7773 .harnesses
7774 .into_iter()
7775 .find(|harness| harness.id.as_str() == HarnessId::GROK)
7776 .and_then(|harness| harness.runtime.default_launch)
7777 .unwrap();
7778 assert!(!registered
7779 .arguments
7780 .iter()
7781 .any(|argument| argument == "--always-approve"));
7782 assert!(runtime_launch(¶ms).is_none());
7783
7784 let yolo = RuntimeBackendParams {
7785 policy: RuntimePolicy::Yolo,
7786 ..params
7787 };
7788 assert!(runtime_launch(&yolo)
7789 .unwrap()
7790 .arguments
7791 .iter()
7792 .any(|argument| argument == "--always-approve"));
7793
7794 let mismatched_protocol = RuntimeBackendParams {
7795 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
7796 protocol: Some("acp".into()),
7797 launch: None,
7798 base_url: None,
7799 policy: RuntimePolicy::Default,
7800 };
7801 assert!(runtime_backend(&mismatched_protocol).is_err());
7802 }
7803
7804 #[test]
7805 fn load_follow_and_unfollow_share_the_same_locator() {
7806 let mut service = HarnessSessionService::new();
7807 let locator = pi_locator();
7808 let loaded = service.handle(request(
7809 1,
7810 "harness.v1.sessions.load",
7811 json!({"locator": locator}),
7812 ));
7813 assert_eq!(
7814 loaded["result"]["session"]["session_id"],
7815 locator.session_id
7816 );
7817
7818 let followed = service.handle(request(
7819 2,
7820 "harness.v1.sessions.follow",
7821 json!({"locator": locator}),
7822 ));
7823 assert_eq!(followed["result"]["subscription"], "sub-1");
7824 assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
7825 assert!(service.poll().is_empty());
7826
7827 let unfollowed = service.handle(request(
7828 3,
7829 "harness.v1.sessions.unfollow",
7830 json!({"subscription": "sub-1"}),
7831 ));
7832 assert_eq!(unfollowed["result"]["removed"], true);
7833 }
7834
7835 #[test]
7836 fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
7837 let temp = std::env::temp_dir().join(format!(
7838 "supercode-bounded-view-{}-{}",
7839 std::process::id(),
7840 generated_session_id()
7841 ));
7842 let path = temp.join("parent.jsonl");
7843 let subagents = temp.join("parent/subagents");
7844 std::fs::create_dir_all(&subagents).unwrap();
7845 let long_last = "x".repeat(300);
7846 let parent_records = [
7847 json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
7848 json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
7849 json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
7850 ];
7851 std::fs::write(
7852 &path,
7853 format!(
7854 "{}\n",
7855 parent_records
7856 .iter()
7857 .map(Value::to_string)
7858 .collect::<Vec<_>>()
7859 .join("\n")
7860 ),
7861 )
7862 .unwrap();
7863 std::fs::write(
7864 subagents.join("agent-child.jsonl"),
7865 concat!(
7866 r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
7867 "\n",
7868 ),
7869 )
7870 .unwrap();
7871 let locator = SessionLocator {
7872 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
7873 session_id: "parent".into(),
7874 storage: StorageLocator::File { path },
7875 };
7876 let mut service = HarnessSessionService::new();
7877
7878 let complete = service.handle(request(
7879 1,
7880 "harness.v1.sessions.load",
7881 json!({"locator": locator}),
7882 ));
7883 assert_eq!(
7884 complete["result"]["session"]["subagents"]
7885 .as_array()
7886 .unwrap()
7887 .len(),
7888 1
7889 );
7890
7891 let bounded = service.handle(request(
7892 2,
7893 "harness.v1.sessions.load",
7894 json!({
7895 "locator": locator,
7896 "view": {
7897 "tail_messages": 1,
7898 "max_message_chars": 256,
7899 "include_subagents": false
7900 },
7901 }),
7902 ));
7903 let session = &bounded["result"]["session"];
7904 assert!(session["subagents"].as_array().unwrap().is_empty());
7905 assert_eq!(session["messages"].as_array().unwrap().len(), 1);
7906 assert_eq!(
7907 session["messages"][0]["content"],
7908 format!("{}\n…", "x".repeat(256))
7909 );
7910
7911 let followed = service.handle(request(
7912 3,
7913 "harness.v1.sessions.follow",
7914 json!({
7915 "locator": locator,
7916 "view": {
7917 "tail_messages": 1,
7918 "max_message_chars": 256,
7919 "include_subagents": false
7920 },
7921 }),
7922 ));
7923 let initial = &followed["result"]["initial"]["session"];
7924 assert!(initial["subagents"].as_array().unwrap().is_empty());
7925 assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
7926
7927 let _ = std::fs::remove_dir_all(&temp);
7928 }
7929
7930 #[test]
7931 fn forty_megabyte_display_load_is_bounded_and_prompt() {
7932 let temp = std::env::temp_dir().join(format!(
7933 "supercode-large-display-view-{}-{}",
7934 std::process::id(),
7935 generated_session_id()
7936 ));
7937 std::fs::create_dir_all(&temp).unwrap();
7938 let path = temp.join("rollout.jsonl");
7939 let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
7940 writeln!(
7941 file,
7942 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
7943 )
7944 .unwrap();
7945 let padding = "x".repeat(80 * 1024);
7946 for index in 0..512 {
7947 let marker = if index == 0 {
7948 "OLDEST-SHOULD-NOT-LOAD"
7949 } else if index == 511 {
7950 "LATEST-MUST-LOAD"
7951 } else {
7952 "bulk"
7953 };
7954 writeln!(
7955 file,
7956 "{}",
7957 json!({
7958 "timestamp": "2026-01-01T00:00:01Z",
7959 "type": "response_item",
7960 "payload": {
7961 "type": "message",
7962 "role": "assistant",
7963 "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
7964 },
7965 })
7966 )
7967 .unwrap();
7968 }
7969 file.flush().unwrap();
7970 drop(file);
7971 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
7972
7973 let locator = SessionLocator {
7974 harness: HarnessId::from(HarnessId::CODEX),
7975 session_id: "large-display".into(),
7976 storage: StorageLocator::File { path },
7977 };
7978 let started = Instant::now();
7979 let response = HarnessSessionService::new().handle(request(
7980 1,
7981 "harness.v1.sessions.load",
7982 json!({
7983 "locator": locator,
7984 "view": {
7985 "tail_messages": 500,
7986 "max_message_chars": 1024,
7987 "include_subagents": false,
7988 "display_history": true,
7989 },
7990 }),
7991 ));
7992 let elapsed = started.elapsed();
7993 let wire = response.to_string();
7994 eprintln!(
7995 "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
7996 wire.len()
7997 );
7998 assert!(response.get("error").is_none(), "{response:#}");
7999 assert!(wire.contains("LATEST-MUST-LOAD"));
8000 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8001 assert!(
8002 wire.len() < 2 * 1024 * 1024,
8003 "bounded wire was {} bytes",
8004 wire.len()
8005 );
8006 assert!(
8007 elapsed.as_secs_f64() < 3.0,
8008 "bounded 40 MiB load took {elapsed:?}"
8009 );
8010
8011 let _ = std::fs::remove_dir_all(&temp);
8012 }
8013
8014 #[test]
8015 fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8016 let temp = std::env::temp_dir().join(format!(
8017 "supercode-large-goose-view-{}-{}",
8018 std::process::id(),
8019 generated_session_id()
8020 ));
8021 std::fs::create_dir_all(&temp).unwrap();
8022 let path = temp.join("sessions.db");
8023 let connection = rusqlite::Connection::open(&path).unwrap();
8024 connection
8025 .execute_batch(
8026 "CREATE TABLE sessions (
8027 id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8028 created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8029 session_type TEXT NOT NULL, extension_data TEXT,
8030 goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8031 archived_at TEXT
8032 );
8033 CREATE TABLE messages (
8034 id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8035 role TEXT NOT NULL, content_json TEXT NOT NULL,
8036 created_timestamp INTEGER NOT NULL, metadata_json TEXT
8037 );",
8038 )
8039 .unwrap();
8040 connection
8041 .execute(
8042 "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8043 rusqlite::params![
8044 "goose-large",
8045 "Large Goose session",
8046 "/tmp",
8047 "2026-01-01 00:00:00",
8048 "2026-01-01 00:00:02",
8049 "user",
8050 "{}",
8051 "auto",
8052 "anthropic",
8053 r#"{"model_name":"claude-sonnet"}"#,
8054 ],
8055 )
8056 .unwrap();
8057 let old_content = serde_json::to_string(&vec![json!({
8058 "type": "text",
8059 "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8060 })])
8061 .unwrap();
8062 connection
8063 .execute(
8064 "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8065 rusqlite::params!["goose-large", old_content],
8066 )
8067 .unwrap();
8068 connection
8069 .execute(
8070 "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8071 rusqlite::params![
8072 "goose-large",
8073 r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8074 ],
8075 )
8076 .unwrap();
8077 drop(connection);
8078 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8079
8080 let locator = SessionLocator {
8081 harness: HarnessId::from(HarnessId::GOOSE),
8082 session_id: "goose-large".into(),
8083 storage: StorageLocator::Sqlite {
8084 path,
8085 selector: "goose-large".into(),
8086 },
8087 };
8088 let started = Instant::now();
8089 let response = HarnessSessionService::new().handle(request(
8090 1,
8091 "harness.v1.sessions.load",
8092 json!({
8093 "locator": locator,
8094 "view": {
8095 "tail_messages": 1,
8096 "max_message_chars": 1024,
8097 "include_subagents": false,
8098 "display_history": true,
8099 },
8100 }),
8101 ));
8102 let elapsed = started.elapsed();
8103 let wire = response.to_string();
8104 eprintln!(
8105 "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8106 wire.len()
8107 );
8108 assert!(response.get("error").is_none(), "{response:#}");
8109 assert!(wire.contains("LATEST-MUST-LOAD"));
8110 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8111 assert!(
8112 wire.len() < 64 * 1024,
8113 "bounded wire was {} bytes",
8114 wire.len()
8115 );
8116 assert!(
8117 elapsed.as_secs_f64() < 1.0,
8118 "bounded Goose load took {elapsed:?}"
8119 );
8120
8121 let _ = std::fs::remove_dir_all(&temp);
8122 }
8123
8124 #[test]
8125 fn display_view_keeps_codex_assistant_history_across_compaction() {
8126 let temp = std::env::temp_dir().join(format!(
8127 "supercode-codex-display-view-{}-{}",
8128 std::process::id(),
8129 generated_session_id()
8130 ));
8131 std::fs::create_dir_all(&temp).unwrap();
8132 let path = temp.join("rollout.jsonl");
8133 std::fs::write(
8134 &path,
8135 concat!(
8136 r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8137 "\n",
8138 r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8139 "\n",
8140 r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8141 "\n",
8142 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"}]}}"#,
8143 "\n",
8144 r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8145 "\n",
8146 r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8147 "\n",
8148 ),
8149 )
8150 .unwrap();
8151 let locator = SessionLocator {
8152 harness: HarnessId::from(HarnessId::CODEX),
8153 session_id: "codex-display".into(),
8154 storage: StorageLocator::File { path },
8155 };
8156 let mut service = HarnessSessionService::new();
8157
8158 let continuation = service.handle(request(
8159 1,
8160 "harness.v1.sessions.load",
8161 json!({"locator": locator}),
8162 ));
8163 let continuation_text = continuation["result"]["session"]["messages"].to_string();
8164 assert!(!continuation_text.contains("old answer"));
8165
8166 let display = service.handle(request(
8167 2,
8168 "harness.v1.sessions.load",
8169 json!({
8170 "locator": locator,
8171 "view": {
8172 "tail_messages": 10,
8173 "include_subagents": false,
8174 "display_history": true,
8175 },
8176 }),
8177 ));
8178 let display_text = display["result"]["session"]["messages"].to_string();
8179 assert!(display_text.contains("old prompt"));
8180 assert!(display_text.contains("old answer"));
8181 assert!(display_text.contains("new prompt"));
8182 assert!(display_text.contains("new answer"));
8183
8184 let _ = std::fs::remove_dir_all(&temp);
8185 }
8186
8187 #[test]
8188 fn indexed_claude_windows_match_the_existing_wire_projection() {
8189 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8190 .join("tests/fixtures/claude_code_session.jsonl");
8191 let locator = SessionLocator {
8192 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8193 session_id: "fixture".into(),
8194 storage: StorageLocator::File { path },
8195 };
8196 let full = load_session(&locator).unwrap();
8197 for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8198 for offset in [0, 1, full.messages.len(), usize::MAX] {
8199 for limit in [0, 1, 3, usize::MAX] {
8200 let options = SessionLoadOptions {
8201 include_subagents: Some(false),
8202 inline_media,
8203 message_offset: Some(offset),
8204 message_limit: Some(limit),
8205 ..Default::default()
8206 };
8207 let expected = projected_session_result(&full, &options);
8208 assert_eq!(
8209 indexed_claude_window(&locator, &options).unwrap().unwrap(),
8210 expected
8211 );
8212 }
8213 }
8214 for tail in [0, 1, 3, usize::MAX] {
8215 let options = SessionLoadOptions {
8216 include_subagents: Some(false),
8217 inline_media,
8218 message_tail: Some(tail),
8219 ..Default::default()
8220 };
8221 assert_eq!(
8222 indexed_claude_window(&locator, &options).unwrap().unwrap(),
8223 projected_session_result(&full, &options)
8224 );
8225 }
8226 }
8227 }
8228
8229 #[test]
8230 fn load_supports_bounded_windows_and_media_metadata() {
8231 let mut service = HarnessSessionService::new();
8232 let locator = pi_locator();
8233 let bounded = service.handle(request(
8234 1,
8235 "harness.v1.sessions.load",
8236 json!({
8237 "locator": locator,
8238 "options": {
8239 "include_subagents": false,
8240 "message_limit": 2,
8241 "message_offset": 1
8242 }
8243 }),
8244 ));
8245 assert_eq!(bounded["result"]["window"]["offset"], 1);
8246 assert_eq!(bounded["result"]["window"]["returned"], 2);
8247 assert!(bounded["result"]["summary"]["first_message"].is_object());
8248 assert!(bounded["result"]["summary"]["last_message"].is_object());
8249 assert_eq!(
8250 bounded["result"]["session"]["messages"]
8251 .as_array()
8252 .unwrap()
8253 .len(),
8254 2
8255 );
8256 assert!(bounded["result"]["session"]["subagents"]
8257 .as_array()
8258 .unwrap()
8259 .is_empty());
8260
8261 let tail = service.handle(request(
8262 2,
8263 "harness.v1.sessions.load",
8264 json!({"locator": locator, "options": {"message_tail": 1}}),
8265 ));
8266 assert_eq!(tail["result"]["window"]["returned"], 1);
8267 assert_eq!(tail["result"]["window"]["has_more"], true);
8268 assert_eq!(tail["result"]["window"]["has_older"], true);
8269 assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8270 assert!(tail["result"]["summary"]["first_message"].is_object());
8271
8272 let metadata_only = service.handle(request(
8273 3,
8274 "harness.v1.sessions.load",
8275 json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8276 ));
8277 assert!(metadata_only["result"]["session"]
8278 .to_string()
8279 .contains("media_reference"));
8280 assert!(!metadata_only["result"]["session"]
8281 .to_string()
8282 .contains("data:image/"));
8283 }
8284
8285 #[test]
8286 fn import_translate_branch_and_handoff_use_typed_artifacts() {
8287 let mut service = HarnessSessionService::new();
8288 let locator = pi_locator();
8289 let translated = service.handle(request(
8290 1,
8291 "harness.v1.sessions.translate",
8292 json!({"locator": locator, "target_harness": "grok"}),
8293 ));
8294 assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8295 assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8296 assert!(translated["result"]["artifact"]["content"]
8297 .as_str()
8298 .is_some_and(|content| !content.is_empty()));
8299
8300 for target in ["opencode", "open-code"] {
8301 let opencode = service.handle(request(
8302 6,
8303 "harness.v1.sessions.translate",
8304 json!({"locator": locator, "target_harness": target}),
8305 ));
8306 assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8307 }
8308 let goose = service.handle(request(
8309 7,
8310 "harness.v1.sessions.translate",
8311 json!({"locator": locator, "target_harness": "goose"}),
8312 ));
8313 assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8314 assert!(serde_json::from_str::<Value>(
8315 goose["result"]["artifact"]["content"].as_str().unwrap()
8316 )
8317 .unwrap()["conversation"]
8318 .is_array());
8319
8320 let imported = service.handle(request(
8321 2,
8322 "harness.v1.sessions.import",
8323 json!({
8324 "source_harness": "grok",
8325 "content": translated["result"]["artifact"]["content"],
8326 }),
8327 ));
8328 assert_eq!(imported["result"]["session"]["source"], "grok");
8329
8330 let branched = service.handle(request(
8331 3,
8332 "harness.v1.sessions.branch",
8333 json!({"locator": locator, "target_harness": "codex"}),
8334 ));
8335 assert_eq!(branched["result"]["parent"]["harness"], "pi");
8336 assert!(branched["result"]["bootstrap_prompt"]
8337 .as_str()
8338 .unwrap()
8339 .contains("frozen parent transcript"));
8340 assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
8341
8342 let handoff = service.handle(request(
8343 4,
8344 "harness.v1.sessions.handoff",
8345 json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
8346 ));
8347 assert_eq!(handoff["result"]["launch"]["program"], "pi");
8348 assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
8349 assert_eq!(handoff["result"]["requires_materialization"], true);
8350
8351 let goose_handoff = service.handle(request(
8352 8,
8353 "harness.v1.sessions.handoff",
8354 json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
8355 ));
8356 assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
8357 assert_eq!(
8358 goose_handoff["result"]["materialize"]["arguments"],
8359 json!(["session", "import", "{artifact_path}"])
8360 );
8361
8362 let resumed = service.handle(request(
8363 5,
8364 "harness.v1.sessions.resume_instructions",
8365 json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
8366 ));
8367 assert_eq!(resumed["result"]["launch"]["program"], "pi");
8368 assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
8369 }
8370
8371 #[test]
8372 fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
8373 let temp = std::env::temp_dir().join(format!(
8374 "supercode-service-reduce-{}-{}",
8375 std::process::id(),
8376 generated_session_id()
8377 ));
8378 let source_path = temp.join("source.jsonl");
8379 let store_root = temp.join("store");
8380 std::fs::create_dir_all(&temp).unwrap();
8381
8382 let mut records = vec![json!({
8383 "timestamp": "2026-01-01T00:00:00Z",
8384 "type": "session_meta",
8385 "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
8386 })];
8387 for turn in 0..16 {
8388 records.push(json!({
8389 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
8390 "type": "response_item",
8391 "payload": {
8392 "type": "message",
8393 "role": "user",
8394 "content": [{
8395 "type": "input_text",
8396 "text": format!("request {turn}: {}", "context ".repeat(80)),
8397 }],
8398 },
8399 }));
8400 records.push(json!({
8401 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
8402 "type": "response_item",
8403 "payload": {
8404 "type": "message",
8405 "role": "assistant",
8406 "content": [{
8407 "type": "output_text",
8408 "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
8409 }],
8410 },
8411 }));
8412 }
8413 let source = format!(
8414 "{}\n",
8415 records
8416 .iter()
8417 .map(Value::to_string)
8418 .collect::<Vec<_>>()
8419 .join("\n")
8420 );
8421 std::fs::write(&source_path, &source).unwrap();
8422 let locator = SessionLocator {
8423 harness: HarnessId::from(HarnessId::CODEX),
8424 session_id: "codex-reduce".into(),
8425 storage: StorageLocator::File {
8426 path: source_path.clone(),
8427 },
8428 };
8429 let original = load_session(&locator).unwrap();
8430 let mut service =
8431 HarnessSessionService::new().with_reduction_store_root(store_root.clone());
8432
8433 let response = service.handle(request(
8434 1,
8435 "harness.v1.sessions.reduce",
8436 json!({
8437 "locator": locator,
8438 "target_harness": "claude-code",
8439 "keep_last": 4,
8440 }),
8441 ));
8442 assert!(response.get("error").is_none(), "{response:#}");
8443 let receipt = &response["result"]["receipt"];
8444 assert_eq!(receipt["source_harness"], "codex");
8445 assert_eq!(receipt["target_harness"], "claude-code");
8446 assert_eq!(receipt["verified"], true);
8447 assert_eq!(receipt["reversible"], true);
8448 assert!(receipt["reductions"].as_u64().unwrap() > 0);
8449 assert!(
8450 receipt["source_tokens"].as_u64().unwrap()
8451 > receipt["reduced_tokens"].as_u64().unwrap()
8452 );
8453 assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
8454 assert!(response["result"]["bootstrap_prompt"]
8455 .as_str()
8456 .unwrap()
8457 .contains("Do not guess hidden content"));
8458
8459 let rescue_id = receipt["id"].as_str().unwrap();
8460 let store = crate::SessionStore::open(&store_root).unwrap();
8461 let sidecar =
8462 Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
8463 let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
8464 let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
8465 let policy = reduce::ReductionPolicy {
8466 clear_turns_older_than: Some(4),
8467 ..Default::default()
8468 };
8469 let (restamped_view, reapplied_log) =
8470 reduce::project_messages(&sidecar.messages, &policy, &log);
8471 assert_eq!(
8472 messages_jsonl(&persisted_view).unwrap(),
8473 messages_jsonl(&restamped_view).unwrap()
8474 );
8475 assert_eq!(reapplied_log, log);
8476 reduce::verify_log(&log, &sidecar).unwrap();
8477 assert_eq!(
8478 reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
8479 original.messages
8480 );
8481 assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
8482
8483 std::fs::remove_dir_all(temp).ok();
8484 }
8485
8486 #[test]
8487 fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
8488 let temp = std::env::temp_dir().join(format!(
8489 "supercode-severed-view-{}-{}",
8490 std::process::id(),
8491 generated_session_id()
8492 ));
8493 std::fs::create_dir_all(&temp).unwrap();
8494 let path = temp.join("severed.jsonl");
8495 std::fs::write(
8498 &path,
8499 concat!(
8500 r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
8501 "\n",
8502 r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
8503 "\n",
8504 ),
8505 )
8506 .unwrap();
8507 let locator = SessionLocator {
8508 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8509 session_id: "severed".into(),
8510 storage: StorageLocator::File { path },
8511 };
8512 let mut service = HarnessSessionService::new();
8513
8514 let viewed = service.handle(request(
8515 1,
8516 "harness.v1.sessions.load",
8517 json!({"locator": locator}),
8518 ));
8519 let session = &viewed["result"]["session"];
8520 assert_eq!(session["fidelity"], "semantic");
8521 assert_eq!(session["messages"].as_array().unwrap().len(), 2);
8522 assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
8523 entry
8524 .as_str()
8525 .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
8526 }));
8527
8528 let strict = service.handle(request(
8531 2,
8532 "harness.v1.sessions.load",
8533 json!({"locator": locator, "fidelity": "byte_lossless"}),
8534 ));
8535 assert!(strict["error"]["message"]
8536 .as_str()
8537 .unwrap()
8538 .contains("cannot reconstruct lossless Claude continuation"));
8539
8540 let translated = service.handle(request(
8542 3,
8543 "harness.v1.sessions.translate",
8544 json!({"locator": locator, "target_harness": "codex"}),
8545 ));
8546 assert!(translated["error"]["message"]
8547 .as_str()
8548 .unwrap()
8549 .contains("cannot reconstruct lossless Claude continuation"));
8550 let resumed = service.handle(request(
8551 4,
8552 "harness.v1.sessions.resume_instructions",
8553 json!({"locator": locator}),
8554 ));
8555 assert!(resumed["error"]["message"]
8556 .as_str()
8557 .unwrap()
8558 .contains("cannot reconstruct lossless Claude continuation"));
8559
8560 let _ = std::fs::remove_dir_all(&temp);
8561 }
8562
8563 #[test]
8564 fn structured_resume_launches_cover_gemini_goose_and_supercode() {
8565 let codex = resume_launch(
8566 HarnessId::CODEX,
8567 "codex-session",
8568 Path::new("/tmp/project"),
8569 ResumePolicy::Yolo,
8570 )
8571 .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
8572 assert_eq!(codex.program, "codex");
8573 assert_eq!(
8574 codex.arguments,
8575 [
8576 "-c",
8577 "check_for_update_on_startup=false",
8578 "-c",
8579 "projects.\"/tmp/project\".trust_level=\"trusted\"",
8580 "--dangerously-bypass-approvals-and-sandbox",
8581 "--dangerously-bypass-hook-trust",
8582 "resume",
8583 "codex-session",
8584 ]
8585 );
8586
8587 let gemini = resume_launch(
8588 HarnessId::GEMINI,
8589 "gemini-session",
8590 Path::new("/tmp/project"),
8591 ResumePolicy::Yolo,
8592 )
8593 .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
8594 assert_eq!(gemini.program, "gemini");
8595 assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
8596
8597 let goose = resume_launch(
8598 HarnessId::GOOSE,
8599 "goose-session",
8600 Path::new("/tmp/project"),
8601 ResumePolicy::Yolo,
8602 )
8603 .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
8604 assert_eq!(goose.program, "goose");
8605 assert_eq!(
8606 goose.arguments,
8607 ["session", "--resume", "--session-id", "goose-session"]
8608 );
8609
8610 let supercode = resume_launch(
8611 HarnessId::SUPERCODE,
8612 "supercode-session",
8613 Path::new("/tmp/project"),
8614 ResumePolicy::Yolo,
8615 )
8616 .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
8617 assert_eq!(supercode.program, "supercode");
8618 assert_eq!(
8619 supercode.arguments,
8620 ["--dangerous", "resume", "supercode-session"]
8621 );
8622 }
8623
8624 #[test]
8625 fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
8626 let temp = std::env::temp_dir().join(format!(
8627 "supercode-harness-artifact-{}-{}",
8628 std::process::id(),
8629 generated_session_id()
8630 ));
8631 let main_path = temp.join("parent.jsonl");
8632 let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
8633 std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
8634 let fixture = std::fs::read_to_string(
8635 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8636 .join("tests/fixtures/claude_code_session.jsonl"),
8637 )
8638 .unwrap();
8639 let parent = fixture.trim_end_matches('\n');
8640 let child = fixture.trim_end_matches('\n');
8641 std::fs::write(&main_path, parent).unwrap();
8642 std::fs::write(&subagent_path, child).unwrap();
8643 let locator = SessionLocator {
8644 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8645 session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
8646 storage: StorageLocator::File {
8647 path: main_path.clone(),
8648 },
8649 };
8650 let mut service = HarnessSessionService::new();
8651 let claude = service.handle(request(
8652 1,
8653 "harness.v1.sessions.translate",
8654 json!({"locator": locator, "target_harness": "claude-code"}),
8655 ));
8656 let artifact = &claude["result"]["artifact"];
8657 assert_eq!(artifact["fidelity"], "byte_lossless");
8658 assert_eq!(artifact["content"], parent);
8659 let files = artifact["files"].as_array().unwrap();
8660 assert!(files.iter().any(|file| {
8661 file["role"] == "subagent"
8662 && file["path"]
8663 .as_str()
8664 .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
8665 && file["content"] == child
8666 }));
8667 assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
8668
8669 let grok = service.handle(request(
8670 2,
8671 "harness.v1.sessions.translate",
8672 json!({"locator": grok_locator(), "target_harness": "grok"}),
8673 ));
8674 let files = grok["result"]["artifact"]["files"].as_array().unwrap();
8675 for name in ["summary.json", "updates.jsonl"] {
8676 let expected = std::fs::read_to_string(
8677 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8678 .join("tests/fixtures/grok_session")
8679 .join(name),
8680 )
8681 .unwrap();
8682 assert!(files.iter().any(|file| {
8683 file["path"] == name && file["role"] == "bundle" && file["content"] == expected
8684 }));
8685 }
8686 std::fs::remove_dir_all(temp).ok();
8687 }
8688
8689 #[test]
8690 fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
8691 let mut service = HarnessSessionService::new();
8692 let source = pi_locator();
8693 for (target, format) in [
8694 ("claude-code", SessionFormat::ClaudeCode),
8695 ("codex", SessionFormat::Codex),
8696 ("opencode", SessionFormat::OpenCode),
8697 ("pi", SessionFormat::Pi),
8698 ] {
8699 let result = service.handle(request(
8700 1,
8701 "harness.v1.sessions.handoff",
8702 json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
8703 ));
8704 let artifact = &result["result"]["artifact"];
8705 let target_id = artifact["session_id"].as_str().unwrap();
8706 assert_ne!(target_id, source.session_id, "{target}");
8707 let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
8708 assert_eq!(
8709 parsed.meta.session_id.as_deref(),
8710 Some(target_id),
8711 "{target}"
8712 );
8713 if target != "pi" {
8714 assert!(result["result"]["launch"]["arguments"]
8715 .as_array()
8716 .unwrap()
8717 .iter()
8718 .any(|argument| argument == target_id));
8719 }
8720 if target == "opencode" {
8721 assert!(target_id.starts_with("ses_"));
8722 fn assert_session_ids(value: &Value, target_id: &str) {
8723 match value {
8724 Value::Object(fields) => {
8725 if let Some(session_id) = fields.get("sessionID") {
8726 assert_eq!(session_id, target_id);
8727 }
8728 for child in fields.values() {
8729 assert_session_ids(child, target_id);
8730 }
8731 }
8732 Value::Array(values) => {
8733 for child in values {
8734 assert_session_ids(child, target_id);
8735 }
8736 }
8737 _ => {}
8738 }
8739 }
8740 let document: Value =
8741 serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
8742 assert_session_ids(&document, target_id);
8743 }
8744 }
8745
8746 let first = service.handle(request(
8747 2,
8748 "harness.v1.sessions.handoff",
8749 json!({"locator": source, "target_harness": "codex"}),
8750 ));
8751 let second = service.handle(request(
8752 3,
8753 "harness.v1.sessions.handoff",
8754 json!({"locator": source, "target_harness": "codex"}),
8755 ));
8756 assert_ne!(
8757 first["result"]["artifact"]["session_id"],
8758 second["result"]["artifact"]["session_id"]
8759 );
8760 }
8761
8762 #[test]
8763 fn grok_handoff_uses_the_official_importer_contract() {
8764 let mut service = HarnessSessionService::new();
8765 let source = opencode_locator();
8766 let response = service.handle(request(
8767 1,
8768 "harness.v1.sessions.handoff",
8769 json!({
8770 "locator": source,
8771 "target_harness": "grok",
8772 "cwd": "/tmp/grok-handoff-project",
8773 }),
8774 ));
8775 let result = &response["result"];
8776
8777 assert_eq!(result["artifact"]["target_harness"], "claude-code");
8781 assert!(result["artifact"]["suggested_filename"]
8782 .as_str()
8783 .unwrap()
8784 .ends_with(".grok-import.claude-code.jsonl"));
8785 let artifact = Session::load_str(
8786 result["artifact"]["content"].as_str().unwrap(),
8787 SessionFormat::ClaudeCode,
8788 )
8789 .unwrap();
8790 assert_eq!(
8791 artifact.meta.cwd.as_deref(),
8792 Some(Path::new("/tmp/grok-handoff-project"))
8793 );
8794 let target_session_id = artifact.meta.session_id.as_deref().unwrap();
8795 assert_eq!(target_session_id.len(), 36);
8796 assert_eq!(target_session_id.as_bytes()[14], b'4');
8797 assert_ne!(target_session_id, opencode_locator().session_id);
8798 assert_eq!(
8799 result["artifact"]["session_id"],
8800 artifact.meta.session_id.as_deref().unwrap()
8801 );
8802
8803 assert_eq!(
8804 result["materialize"]["arguments"],
8805 json!(["import", "--json", "{artifact_path}"])
8806 );
8807 assert_eq!(
8808 result["launch"]["arguments"],
8809 json!(["--resume", "{imported_session_id}", "--fork-session"])
8810 );
8811 assert!(result["note"]
8812 .as_str()
8813 .unwrap()
8814 .contains("outcome=imported"));
8815 assert!(!result["launch"]["arguments"]
8816 .as_array()
8817 .unwrap()
8818 .iter()
8819 .any(|argument| argument == &opencode_locator().session_id));
8820 }
8821
8822 #[tokio::test]
8823 async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
8824 let mut service = HarnessSessionService::new();
8825 let inventory = service
8826 .handle_async(request(
8827 1,
8828 "harness.v1.harnesses.list",
8829 json!({"harnesses": ["missing"]}),
8830 ))
8831 .await;
8832 assert_eq!(inventory["error"]["code"], -32602);
8833
8834 let attached = service
8835 .handle_async(request(
8836 2,
8837 "harness.v1.runtimes.attach_existing",
8838 json!({"harness": "codex", "runtime_id": "thread-1"}),
8839 ))
8840 .await;
8841 assert_eq!(attached["error"]["code"], -32000);
8842 assert!(attached["error"]["message"]
8843 .as_str()
8844 .unwrap()
8845 .contains("runtimes.resume"));
8846 }
8847
8848 #[test]
8849 fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
8850 let mut service = HarnessSessionService::new();
8851 let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
8852 assert_eq!(invalid["error"]["code"], -32602);
8853 let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
8854 assert_eq!(unknown["error"]["code"], -32601);
8855 }
8856
8857 #[cfg(unix)]
8858 #[tokio::test]
8859 #[allow(clippy::await_holding_lock)]
8862 async fn async_service_drives_a_generic_acp_runtime() {
8863 let _environment_guard = crate::live_runtime::test_environment_lock();
8864 let script = r#"
8865 i=0
8866 while IFS= read -r line; do
8867 i=$((i + 1))
8868 case "$i" in
8869 1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
8870 2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
8871 3)
8872 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
8873 printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
8874 ;;
8875 4)
8876 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
8877 printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
8878 ;;
8879 esac
8880 done
8881 "#;
8882 let mut service = HarnessSessionService::new();
8883 let started = service
8884 .handle_async(request(
8885 1,
8886 "harness.v1.runtimes.start",
8887 json!({
8888 "harness": "codex",
8889 "protocol": "acp",
8890 "cwd": std::env::current_dir().unwrap(),
8891 "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
8892 }),
8893 ))
8894 .await;
8895 assert_eq!(started["result"]["connection"], "runtime-1");
8896 assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
8897
8898 let terminal = service
8899 .handle_async(request(
8900 9,
8901 "harness.v1.runtimes.terminal_instructions",
8902 json!({"connection":"runtime-1"}),
8903 ))
8904 .await;
8905 let arguments = terminal["result"]["launch"]["arguments"]
8906 .as_array()
8907 .expect("hosted runtime should return terminal arguments");
8908 let endpoint_index = arguments
8909 .iter()
8910 .position(|value| value == "--endpoint")
8911 .expect("terminal command should use an opaque endpoint");
8912 let endpoint = LiveRuntimeEndpoint::parse(
8913 arguments[endpoint_index + 1]
8914 .as_str()
8915 .expect("endpoint argument should be text"),
8916 )
8917 .unwrap();
8918 assert!(!terminal.to_string().contains("Bearer"));
8919 let workspace = std::env::current_dir().unwrap();
8920 let receipt = resolve_live_runtime(
8921 &endpoint,
8922 &LiveRuntimeSource {
8923 harness: "codex".into(),
8924 session_id: "svc_acp".into(),
8925 workspace,
8926 },
8927 )
8928 .unwrap();
8929 let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
8930 .await
8931 .unwrap();
8932 let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
8933 .await
8934 .unwrap();
8935
8936 let sent = service
8937 .handle_async(request(
8938 2,
8939 "harness.v1.runtimes.send_input",
8940 json!({"connection": "runtime-1", "text": "hi"}),
8941 ))
8942 .await;
8943 assert_eq!(sent["result"]["turn_id"], "3");
8944
8945 let mut events = Vec::new();
8946 for _ in 0..20 {
8947 events.extend(service.poll_runtimes().await);
8948 if events.len() >= 2 {
8949 break;
8950 }
8951 tokio::time::sleep(Duration::from_millis(2)).await;
8952 }
8953 assert!(events
8954 .iter()
8955 .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
8956 assert!(events.iter().any(|event| {
8957 event["params"]["event"]["kind"] == "supercode/acp_request_completed"
8958 }));
8959
8960 let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
8961 loop {
8962 let event = attachment.next_event().await.unwrap();
8963 if event.kind == "text_delta" && event.payload["text"] == "ok" {
8964 break;
8965 }
8966 }
8967 })
8968 .await;
8969 assert!(
8970 saw_editor_reply.is_ok(),
8971 "terminal should observe the editor-driven turn"
8972 );
8973
8974 crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
8975 .await
8976 .unwrap();
8977 let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
8978 loop {
8979 let event = attachment.next_event().await.unwrap();
8980 if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
8981 break;
8982 }
8983 }
8984 })
8985 .await;
8986 assert!(
8987 saw_terminal_reply.is_ok(),
8988 "terminal should drive the same runtime"
8989 );
8990
8991 let closed = service
8992 .handle_async(request(
8993 3,
8994 "harness.v1.runtimes.close",
8995 json!({"connection": "runtime-1"}),
8996 ))
8997 .await;
8998 assert_eq!(closed["result"]["closed"], true);
8999 }
9000
9001 #[test]
9006 fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9007 let home = connect_scratch_home("uni7-running");
9008
9009 assert!(probe_hermes_running(&home, 300_000).is_none());
9016
9017 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9020 let port = listener.local_addr().unwrap().port();
9021 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9022 std::fs::write(
9023 home.join(".openclaw/openclaw.json"),
9024 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9025 )
9026 .unwrap();
9027 let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9028 assert!(matches!(
9029 running.method,
9030 RunningInstanceMethod::GatewayConnect
9031 ));
9032 assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9033 drop(listener);
9034 let mut closed_detected = probe_openclaw_running(&home).is_some();
9039 for _ in 0..3 {
9040 if !closed_detected {
9041 break;
9042 }
9043 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9044 let port = listener.local_addr().unwrap().port();
9045 drop(listener);
9046 std::fs::write(
9047 home.join(".openclaw/openclaw.json"),
9048 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9049 )
9050 .unwrap();
9051 closed_detected = probe_openclaw_running(&home).is_some();
9052 }
9053 assert!(
9054 !closed_detected,
9055 "a closed gateway must not read as running"
9056 );
9057
9058 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9060 let port = listener.local_addr().unwrap().port();
9061 std::fs::write(
9062 home.join(".openclaw/openclaw.json"),
9063 format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9064 )
9065 .unwrap();
9066 assert!(probe_openclaw_running(&home).is_some());
9067 drop(listener);
9068
9069 std::fs::create_dir_all(home.join(".hermes")).unwrap();
9071 let wal = home.join(".hermes/state.db-wal");
9072 std::fs::write(&wal, b"wal").unwrap();
9073 let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9074 assert!(matches!(
9075 running.method,
9076 RunningInstanceMethod::StoreWalActivity
9077 ));
9078 assert!(running.evidence.contains("state.db-wal"));
9079 let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9080 std::fs::File::options()
9081 .append(true)
9082 .open(&wal)
9083 .unwrap()
9084 .set_modified(stale)
9085 .unwrap();
9086 assert!(
9087 probe_hermes_running(&home, 300_000).is_none(),
9088 "a stale WAL (crash leftover) must not read as running"
9089 );
9090 }
9091
9092 fn connect_scratch_home(tag: &str) -> PathBuf {
9093 let dir = std::env::temp_dir().join(format!(
9094 "supercode-connect-service-{tag}-{}-{}",
9095 std::process::id(),
9096 std::time::SystemTime::now()
9097 .duration_since(std::time::UNIX_EPOCH)
9098 .unwrap()
9099 .as_nanos()
9100 ));
9101 std::fs::create_dir_all(&dir).unwrap();
9102 dir
9103 }
9104
9105 async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9109 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9110 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9111 let address = listener.local_addr().unwrap();
9112 let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9113 tokio::spawn(async move {
9114 loop {
9115 let Ok((mut stream, _)) = listener.accept().await else {
9116 break;
9117 };
9118 let request_sender = request_sender.clone();
9119 tokio::spawn(async move {
9120 let (reader, mut writer) = stream.split();
9121 let mut reader = BufReader::new(reader);
9122 let mut request_line = String::new();
9123 if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9124 return;
9125 }
9126 let request_line = request_line.trim_end().to_string();
9127 let mut authorization = String::new();
9128 let mut content_length = 0usize;
9129 loop {
9130 let mut line = String::new();
9131 if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9132 return;
9133 }
9134 let line = line.trim_end();
9135 if line.is_empty() {
9136 break;
9137 }
9138 let lower = line.to_ascii_lowercase();
9139 if let Some(value) = lower.strip_prefix("authorization:") {
9140 authorization = value.trim().to_string();
9141 }
9142 if let Some(value) = lower.strip_prefix("content-length:") {
9143 content_length = value.trim().parse().unwrap_or(0);
9144 }
9145 }
9146 if content_length > 0 {
9147 let mut body = vec![0u8; content_length];
9148 let _ = reader.read_exact(&mut body).await;
9149 }
9150 let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9151 if request_line.starts_with("GET /event") {
9152 let _ = writer
9153 .write_all(
9154 b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9155 )
9156 .await;
9157 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9158 return;
9159 }
9160 let body = if request_line.starts_with("POST /session") {
9161 r#"{"id":"mock-session"}"#
9162 } else {
9163 r#"{"status":"ok"}"#
9164 };
9165 let response = format!(
9166 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9167 body.len(),
9168 body
9169 );
9170 let _ = writer.write_all(response.as_bytes()).await;
9171 });
9172 }
9173 });
9174 (format!("http://{address}"), request_receiver)
9175 }
9176
9177 fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9178 crate::HarnessSupportDescriptor {
9179 orchestration: Default::default(),
9180 id: HarnessId::from(HarnessId::OPENCODE),
9181 display_name: "OpenCode".into(),
9182 native: crate::NativeSupport {
9183 discover: crate::ImplementationKind::Absent,
9184 load: crate::ImplementationKind::Absent,
9185 follow: crate::ImplementationKind::Absent,
9186 import: crate::ImplementationKind::Absent,
9187 export: crate::ImplementationKind::Absent,
9188 },
9189 runtime: crate::RuntimeSupport {
9190 implementation: crate::ImplementationKind::BuiltIn,
9191 protocol: protocol.into(),
9192 default_launch: None,
9193 connect_launch: Some(crate::RuntimeConnectLaunch {
9194 config_path: "~/opencode-tui.json".into(),
9195 address_pointer: "/server/url".into(),
9196 port_pointer: None,
9197 default_address: None,
9198 auth_pointer: Some("/server/token".into()),
9199 protocol: protocol.into(),
9200 }),
9201 capabilities: crate::RuntimeCapabilities {
9202 start_session: true,
9203 resume_session: true,
9204 attach_existing_process: true,
9205 send_input: true,
9206 stream_events: true,
9207 interrupt: true,
9208 steer: false,
9209 respond_to_requests: true,
9210 },
9211 },
9212 }
9213 }
9214
9215 #[tokio::test]
9216 async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9217 let (base_url, mut requests) = mock_opencode_endpoint().await;
9218 let home = connect_scratch_home("open");
9219 std::fs::write(
9220 home.join("opencode-tui.json"),
9221 format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9222 )
9223 .unwrap();
9224
9225 let descriptor = connect_descriptor("opencode-http-sse");
9226 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9227 assert!(backend.capabilities().attach_existing_process);
9228
9229 let connection = backend
9230 .start(crate::RuntimeStartRequest {
9231 cwd: home.clone(),
9232 launch: None,
9233 mcp_servers: Vec::new(),
9234 })
9235 .await
9236 .unwrap();
9237 let handle = connection.handle();
9238 assert_eq!(handle.runtime_id, "mock-session");
9239 match &handle.endpoint {
9240 crate::RuntimeEndpoint::Http {
9241 base_url: endpoint, ..
9242 } => assert_eq!(endpoint, &base_url),
9243 other => panic!("connect mode must join the running endpoint, got {other:?}"),
9244 }
9245
9246 let mut seen = Vec::new();
9247 while let Ok(line) = requests.try_recv() {
9248 seen.push(line);
9249 }
9250 assert!(seen
9251 .iter()
9252 .any(|line| line.starts_with("GET /global/health")
9253 && line.contains("bearer connect-secret")));
9254 assert!(seen.iter().any(
9255 |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9256 ));
9257 }
9258
9259 #[tokio::test]
9270 async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9271 let home = connect_scratch_home("openclaw");
9272 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9273 std::fs::write(
9274 home.join(".openclaw/openclaw.json"),
9275 r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9276 )
9277 .unwrap();
9278 let script = home.join("openclaw");
9279 std::fs::write(
9280 &script,
9281 r#"#!/bin/sh
9282# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9283[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9284[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9285[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9286[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9287while IFS= read -r line; do
9288 case "$line" in
9289 *'"initialize"'*)
9290 printf '%s
9291' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9292 *'"session/resume"'*)
9293 printf '%s
9294' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9295 *'"session/new"'*)
9296 printf '%s
9297' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9298 *'"session/prompt"'*)
9299 printf '%s
9300' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9301 printf '%s
9302' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9303 esac
9304done
9305"#,
9306 )
9307 .unwrap();
9308 use std::os::unix::fs::PermissionsExt;
9309 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9310
9311 let mut descriptor = crate::harness_support_registry()
9312 .harnesses
9313 .into_iter()
9314 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9315 .expect("openclaw must be registered");
9316 descriptor
9317 .runtime
9318 .connect_launch
9319 .as_mut()
9320 .unwrap()
9321 .config_path = "~/.openclaw/openclaw.json".into();
9322 descriptor.runtime.default_launch.as_mut().unwrap().program =
9323 script.to_string_lossy().into_owned();
9324 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9325 assert!(backend.capabilities().resume_session);
9326
9327 let joined = backend
9328 .attach(crate::RuntimeAttachRequest {
9329 runtime_id: "agent:main:main".into(),
9330 cwd: Some(home.clone()),
9331 launch: None,
9332 })
9333 .await;
9334 let mut connection = joined.expect("mock bridge attach must succeed");
9335 assert_eq!(connection.handle().runtime_id, "agent:main:main");
9336 let turn = connection
9337 .send_input(crate::RuntimeInput {
9338 text: "hello".into(),
9339 image_urls: Vec::new(),
9340 })
9341 .await;
9342 assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
9343 connection.close().await.unwrap();
9344 }
9345
9346 #[tokio::test]
9347 async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
9348 let home = connect_scratch_home("fail");
9349 std::fs::write(
9350 home.join("opencode-tui.json"),
9351 r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
9352 )
9353 .unwrap();
9354
9355 let gateway_only = connect_descriptor("acp-v1-jsonrpc");
9356 let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
9357 panic!("an ACP connect endpoint has no gateway client yet");
9358 };
9359 let message = format!("{error:?}");
9360 assert!(message.contains("acp-v1-jsonrpc"));
9361 assert!(!message.contains("connect-secret"));
9362
9363 let unreadable = connect_descriptor("opencode-http-sse");
9364 let missing_home = connect_scratch_home("missing");
9365 let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
9366 panic!("an unreadable connect config must fail closed");
9367 };
9368 let message = format!("{error:?}");
9369 assert!(message.contains("opencode-tui.json"));
9370 assert!(!message.contains("connect-secret"));
9371 }
9372
9373 fn jobs_fixture_root() -> PathBuf {
9378 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
9379 }
9380
9381 fn jobs_fixture_homes() -> Value {
9384 let root = jobs_fixture_root();
9385 json!({
9386 "claude_code": root.join("claude_jobs_home/projects"),
9387 "hermes": root.join("hermes_home/state.db"),
9388 "openclaw": root.join("openclaw_home"),
9389 })
9390 }
9391
9392 fn jobs_list(params: Value) -> Value {
9393 let mut service = HarnessSessionService::new();
9394 service.handle(request(1, "harness.v1.jobs.list", params))
9395 }
9396
9397 fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
9398 result["jobs"]
9399 .as_array()
9400 .expect("jobs is an array")
9401 .iter()
9402 .find(|job| job["id"] == id)
9403 .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
9404 }
9405
9406 #[test]
9407 fn gateway_health_derives_from_running_probe_and_install_state() {
9408 let running = RunningInstance {
9409 method: RunningInstanceMethod::GatewayConnect,
9410 evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
9411 checked_at_ms: 1,
9412 };
9413 let up = gateway_health(
9414 HarnessId::OPENCLAW,
9415 true,
9416 Some(&running),
9417 Some("2026.7.1-2"),
9418 );
9419 assert_eq!(up.state, GatewayState::Up);
9420 assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
9421 assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
9422 let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
9426 std::fs::create_dir_all(&dir).unwrap();
9427 let fake = dir.join("hermes");
9428 let write_fake = |body: &str| {
9429 std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
9430 #[cfg(unix)]
9431 {
9432 use std::os::unix::fs::PermissionsExt;
9433 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
9434 }
9435 };
9436 write_fake("echo '✗ Gateway service is not installed'");
9437 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
9438 *slot.borrow_mut() = Some((
9439 HarnessId::HERMES.to_string(),
9440 fake.to_string_lossy().into_owned(),
9441 ))
9442 });
9443 let down = gateway_health(HarnessId::HERMES, true, None, None);
9444 assert_eq!(down.state, GatewayState::Down, "{down:?}");
9445 assert!(down.endpoint.is_none());
9446 assert!(down.evidence.contains("not installed"));
9447 write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
9448 let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
9449 assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
9450 assert!(idle_but_up.evidence.contains("PID 4242"));
9451 write_fake("echo 'something unparseable'");
9452 let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
9453 assert_eq!(no_verdict.state, GatewayState::Down);
9454 assert!(no_verdict.evidence.contains("no verdict"));
9455 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
9456 let absent = gateway_health(HarnessId::HERMES, false, None, None);
9457 assert_eq!(absent.state, GatewayState::Unknown);
9458 let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
9459 assert_eq!(core.state, GatewayState::Unknown);
9460 assert!(core.evidence.contains("per session"));
9461 }
9462
9463 #[test]
9464 fn triggers_list_reads_both_stores_and_never_emits_secrets() {
9465 let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
9466 let rows = response["result"]["triggers"]
9467 .as_array()
9468 .expect("triggers")
9469 .clone();
9470 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
9471 assert!(
9472 hermes.iter().any(|r| r["name"] == "deploys"
9473 && r["route"] == "/webhooks/deploys"
9474 && r["kind"] == "webhook"),
9475 "{rows:#?}"
9476 );
9477 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
9478 assert!(openclaw
9479 .iter()
9480 .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
9481 assert!(openclaw.iter().any(|r| r["name"] == "gmail"
9482 && r["kind"] == "hook_mapping"
9483 && r["target"]["action"] == "agent"));
9484 let rendered = response.to_string();
9485 for secret in [
9486 "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
9487 "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
9488 ] {
9489 assert!(!rendered.contains(secret), "{rendered}");
9490 }
9491 let refused =
9492 triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
9493 assert_eq!(refused["error"]["code"], -32020, "{refused}");
9494 }
9495
9496 fn triggers_list(params: Value) -> Value {
9497 let mut service = HarnessSessionService::new();
9498 service.handle(request(1, "harness.v1.triggers.list", params))
9499 }
9500
9501 #[test]
9502 fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
9503 let response = routes_list(json!({"homes": jobs_fixture_homes()}));
9504 let rows = response["result"]["routes"]
9505 .as_array()
9506 .expect("routes")
9507 .clone();
9508 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
9509 assert_eq!(hermes.len(), 2, "{rows:#?}");
9510 assert_eq!(hermes[0]["target"], "coder");
9511 assert_eq!(hermes[0]["match"]["platform"], "slack");
9512 assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
9513 assert_eq!(hermes[0]["specificity"], 4);
9514 assert_eq!(hermes[1]["default"], true);
9515 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
9516 assert!(
9517 openclaw.iter().any(|r| r["target"] == "design"
9518 && r["match"]["platform"] == "slack"
9519 && r["specificity"] == 1),
9520 "{openclaw:#?}"
9521 );
9522 assert!(openclaw.iter().any(|r| r["default"] == true));
9523 let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
9525 assert_eq!(refused["error"]["code"], -32020, "{refused}");
9526 }
9527
9528 fn routes_list(params: Value) -> Value {
9529 let mut service = HarnessSessionService::new();
9530 service.handle(request(1, "harness.v1.routes.list", params))
9531 }
9532
9533 #[test]
9534 fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
9535 let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
9536 let result = &response["result"];
9537 let ids: Vec<&str> = result["jobs"]
9538 .as_array()
9539 .unwrap()
9540 .iter()
9541 .map(|job| job["id"].as_str().unwrap())
9542 .collect();
9543 assert_eq!(
9544 ids,
9545 vec![
9546 "release-watch",
9547 "toolu_wake_recheck",
9548 "digest-15m",
9549 "nightly-audit",
9550 "coder-standup",
9551 "ops-once-boot",
9552 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
9553 "8bb7d938-ca46-4a6d-90eb-c92331155566",
9554 "cron_standup",
9555 "cron_reindex",
9556 ],
9557 "{result}"
9558 );
9559
9560 let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
9564 assert_eq!(health["harness"], "openclaw");
9565 assert_eq!(health["schedule"]["kind"], "interval");
9566 assert_eq!(health["schedule"]["minutes"], 10.0);
9567 assert_eq!(health["session_target"], "isolated");
9568 assert_eq!(health["payload"]["kind"], "prompt");
9569 assert_eq!(health["payload"]["text"], "nightly health check");
9570 assert_eq!(health["deliver"]["mode"], "announce");
9574 assert_eq!(health["deliver"]["target"], "last");
9575 assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
9576 let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
9577 assert_eq!(digest["schedule"]["kind"], "cron");
9578 assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
9579 assert_eq!(digest["session_target"], "main");
9580 assert_eq!(digest["payload"]["kind"], "system_event");
9581
9582 let cron = job_row(result, "release-watch");
9584 assert_eq!(cron["harness"], "claude-code");
9585 assert_eq!(cron["scope"], "session");
9586 assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
9587 assert_eq!(cron["schedule"]["kind"], "cron");
9588 assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
9589 assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
9590 assert_eq!(cron["payload"]["kind"], "prompt");
9591 assert_eq!(cron["recurring"], true);
9592 assert_eq!(cron["deliver"]["target"], "session");
9593 let wakeup = job_row(result, "toolu_wake_recheck");
9594 assert_eq!(wakeup["payload"]["kind"], "wakeup");
9595 assert_eq!(wakeup["schedule"]["kind"], "once");
9596 assert_eq!(wakeup["recurring"], false);
9597 assert_eq!(wakeup["state"], "pending");
9598
9599 let interval = job_row(result, "digest-15m");
9601 assert_eq!(interval["harness"], "hermes");
9602 assert_eq!(interval["scope"], "install");
9603 assert_eq!(interval["profile"], Value::Null);
9604 assert_eq!(interval["schedule"]["kind"], "interval");
9605 assert_eq!(interval["schedule"]["minutes"], 15.0);
9606 assert_eq!(interval["schedule"]["display"], "every 15 min");
9607 assert_eq!(interval["deliver"]["target"], "origin");
9608 assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
9609 assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
9610 assert_eq!(interval["last_status"], "ok");
9611 let nightly = job_row(result, "nightly-audit");
9612 assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
9613 assert_eq!(nightly["deliver"]["target"], "local");
9614 assert_eq!(nightly["enabled"], false);
9615 assert_eq!(nightly["state"], "paused");
9616 let profiled = job_row(result, "ops-once-boot");
9618 assert_eq!(profiled["profile"], "ops");
9619 assert_eq!(profiled["schedule"]["kind"], "once");
9620 assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
9621 assert_eq!(profiled["payload"]["kind"], "script");
9622 assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
9624 assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
9625 assert_eq!(profiled["recurring"], false);
9626
9627 let standup_to_group = job_row(result, "coder-standup");
9630 assert_eq!(standup_to_group["deliver"]["target"], "origin");
9631 assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
9632 assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
9633 assert!(standup_to_group["deliver"]["mode"].is_null());
9635 assert!(standup_to_group["deliver"]["account"].is_null());
9636
9637 let standup = job_row(result, "cron_standup");
9640 assert_eq!(standup["harness"], "openclaw");
9641 assert_eq!(standup["session_target"], "isolated");
9642 assert_eq!(standup["deliver"]["mode"], "announce");
9643 assert_eq!(standup["deliver"]["target"], "slack");
9644 assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
9645 assert_eq!(standup["payload"]["kind"], "prompt");
9646 assert_eq!(standup["profile"], "main");
9647 let reindex = job_row(result, "cron_reindex");
9648 assert_eq!(reindex["session_target"], "main");
9649 assert_eq!(reindex["payload"]["kind"], "system_event");
9650 assert_eq!(reindex["schedule"]["kind"], "interval");
9651 assert_eq!(reindex["schedule"]["display"], "every 240 min");
9652 assert_eq!(reindex["enabled"], false);
9653
9654 let states: Vec<(&str, &str)> = result["sources"]
9656 .as_array()
9657 .unwrap()
9658 .iter()
9659 .map(|source| {
9660 (
9661 source["harness"].as_str().unwrap(),
9662 source["state"].as_str().unwrap(),
9663 )
9664 })
9665 .collect();
9666 assert_eq!(
9670 states,
9671 vec![
9672 ("claude-code", "scanned"),
9673 ("hermes", "read"),
9674 ("hermes", "absent_store"),
9675 ("hermes", "read"),
9676 ("openclaw", "read"),
9677 ("openclaw", "read"),
9678 ],
9679 "{result}"
9680 );
9681 }
9682
9683 #[test]
9684 fn jobs_list_filters_by_harness_session_and_profile() {
9685 let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
9686 let ids: Vec<&str> = by_harness["result"]["jobs"]
9687 .as_array()
9688 .unwrap()
9689 .iter()
9690 .map(|job| job["id"].as_str().unwrap())
9691 .collect();
9692 assert_eq!(
9693 ids,
9694 vec![
9695 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
9696 "8bb7d938-ca46-4a6d-90eb-c92331155566",
9697 "cron_standup",
9698 "cron_reindex",
9699 ]
9700 );
9701
9702 let by_session = jobs_list(json!({
9703 "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
9704 "homes": jobs_fixture_homes(),
9705 }));
9706 let jobs = by_session["result"]["jobs"].as_array().unwrap();
9707 assert_eq!(jobs.len(), 2, "{by_session}");
9708 assert!(jobs
9709 .iter()
9710 .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
9711
9712 let by_profile = jobs_list(json!({
9713 "harness": "hermes",
9714 "profile": "ops",
9715 "homes": jobs_fixture_homes(),
9716 }));
9717 let jobs = by_profile["result"]["jobs"].as_array().unwrap();
9718 assert_eq!(jobs.len(), 1, "{by_profile}");
9719 assert_eq!(jobs[0]["id"], "ops-once-boot");
9720 }
9721
9722 #[test]
9723 fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
9724 let mut service = HarnessSessionService::new();
9725 let hermes = service.handle(request(
9726 1,
9727 "harness.v1.jobs.get",
9728 json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
9729 ));
9730 assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
9731 assert_eq!(hermes["result"]["source"]["provider"], "nous");
9733 assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
9734
9735 let claude = service.handle(request(
9736 2,
9737 "harness.v1.jobs.get",
9738 json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
9739 ));
9740 assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
9741 assert_eq!(
9742 claude["result"]["source"]["tool_use_id"],
9743 "toolu_cron_release_watch"
9744 );
9745
9746 let missing = service.handle(request(
9747 3,
9748 "harness.v1.jobs.get",
9749 json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
9750 ));
9751 assert!(missing["error"]["message"]
9752 .as_str()
9753 .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
9754 }
9755
9756 #[test]
9757 fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
9758 let mut service = HarnessSessionService::new();
9759 for (id, method, params) in [
9760 (
9761 1,
9762 "harness.v1.jobs.list",
9763 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
9764 ),
9765 (
9766 2,
9767 "harness.v1.jobs.get",
9768 json!({"harness": "codex", "id": "anything"}),
9769 ),
9770 ] {
9771 let response = service.handle(request(id, method, params));
9772 assert_eq!(response["error"]["code"], -32020, "{response}");
9773 assert!(response["error"]["message"]
9774 .as_str()
9775 .is_some_and(|message| message.contains("has no scheduled jobs")));
9776 assert!(response.get("result").is_none());
9777 }
9778 }
9779
9780 #[test]
9781 fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
9782 let scratch = std::env::temp_dir().join(format!(
9783 "supercode-jobs-migrated-{}-{}",
9784 std::process::id(),
9785 generated_session_id()
9786 ));
9787 std::fs::create_dir_all(&scratch).unwrap();
9788 let response = jobs_list(json!({
9789 "harness": "openclaw",
9790 "homes": {"openclaw": scratch.clone()},
9791 }));
9792 let result = &response["result"];
9793 assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
9794 assert_eq!(result["sources"][0]["state"], "absent_store");
9795 assert_eq!(result["sources"][0]["harness"], "openclaw");
9796 std::fs::remove_dir_all(&scratch).ok();
9797 }
9798
9799 const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
9808 const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
9810
9811 fn runs_list(params: Value) -> Value {
9812 let mut service = HarnessSessionService::new();
9813 service.handle(request(1, "harness.v1.runs.list", params))
9814 }
9815
9816 fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
9817 result["runs"]
9818 .as_array()
9819 .expect("runs is an array")
9820 .iter()
9821 .find(|run| run["id"] == id)
9822 .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
9823 }
9824
9825 #[test]
9826 fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
9827 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
9828 let result = &response["result"];
9829 let ids: Vec<&str> = result["runs"]
9830 .as_array()
9831 .expect("runs is an array")
9832 .iter()
9833 .map(|run| run["id"].as_str().unwrap())
9834 .collect();
9835 let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
9836 assert_eq!(
9837 ids,
9838 vec![
9839 "b2c3d4e5f60718293a4b5c6d7e8f9012",
9841 "a1b2c3d4e5f60718293a4b5c6d7e8f90",
9842 "c3d4e5f60718293a4b5c6d7e8f901234",
9843 "f60718293a4b5c6d7e8f901234567890",
9844 "e5f60718293a4b5c6d7e8f9012345678",
9845 "d4e5f60718293a4b5c6d7e8f90123456",
9846 "run_health_0002",
9848 digest_fire.as_str(),
9849 "run_health_0001",
9850 ],
9851 "{result}"
9852 );
9853
9854 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
9857 assert_eq!(failed["harness"], "hermes");
9858 assert_eq!(failed["job_id"], "job42");
9859 assert_eq!(failed["status"], "failed");
9860 assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
9861 assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
9862
9863 let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
9866 assert_eq!(abandoned["status"], "unknown");
9867 assert_eq!(abandoned["job_id"], "ops-once-boot");
9868
9869 let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
9871 assert_eq!(running["status"], "running");
9872 assert!(running["finished_at"].is_null(), "{running}");
9873 assert!(running["session_id"].is_null(), "{running}");
9874
9875 let ok = run_row(result, "run_health_0001");
9878 assert_eq!(ok["harness"], "openclaw");
9879 assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
9880 assert_eq!(ok["status"], "ok");
9881 assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
9882 assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
9883 assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
9884 assert!(ok["claimed_at"].is_null(), "{ok}");
9886
9887 assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
9890
9891 for id in [
9896 "b2c3d4e5f60718293a4b5c6d7e8f9012",
9897 "d4e5f60718293a4b5c6d7e8f90123456",
9898 ] {
9899 assert!(run_row(result, id)["delivery"].is_null(), "{id}");
9900 }
9901
9902 let sources = result["sources"].as_array().unwrap();
9905 let states: Vec<(&str, &str)> = sources
9906 .iter()
9907 .map(|source| {
9908 (
9909 source["harness"].as_str().unwrap(),
9910 source["state"].as_str().unwrap(),
9911 )
9912 })
9913 .collect();
9914 assert_eq!(
9915 states,
9916 vec![
9917 ("hermes", "read"),
9918 ("hermes", "absent_store"),
9919 ("hermes", "read"),
9920 ("openclaw", "read"),
9921 ],
9922 "{result}"
9923 );
9924 assert_eq!(sources[2]["profile"], "ops");
9925 assert!(sources[3]["path"]
9926 .as_str()
9927 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
9928 }
9929
9930 #[test]
9931 fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
9932 let response = runs_list(json!({
9933 "harness": "hermes",
9934 "job": "job42",
9935 "homes": jobs_fixture_homes(),
9936 }));
9937 let result = &response["result"];
9938 assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
9939
9940 let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
9944 assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
9945
9946 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
9950 assert!(failed["session_id"].is_null(), "{failed}");
9951 }
9952
9953 #[test]
9957 fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
9958 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
9959 let result = &response["result"];
9960
9961 let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
9965 assert_eq!(delivered["status"], "completed");
9966 assert_eq!(delivered["delivery"]["state"], "delivered");
9967 assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
9968 assert_eq!(delivered["delivery"]["attempts"], 1);
9969 assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
9970 assert_eq!(
9971 delivered["delivery"]["delivered_at"],
9972 "2026-09-02T09:00:30.400Z"
9973 );
9974
9975 let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
9978 assert_eq!(undelivered["status"], "completed");
9979 assert_eq!(undelivered["delivery"]["state"], "failed");
9980 assert_eq!(undelivered["delivery"]["attempts"], 3);
9981 assert_eq!(
9982 undelivered["delivery"]["last_error"],
9983 "telegram send failed: Bad Request: chat not found"
9984 );
9985 assert!(
9988 undelivered["delivery"]["delivered_at"].is_null(),
9989 "{undelivered}"
9990 );
9991
9992 let announced = run_row(result, "run_health_0001");
9995 assert_eq!(announced["delivery"]["state"], "delivered");
9996 assert_eq!(announced["delivery"]["target"], "last");
9997 assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
9999 assert!(
10000 announced["delivery"]["delivered_at"].is_null(),
10001 "{announced}"
10002 );
10003 let refused = run_row(result, "run_health_0002");
10004 assert_eq!(refused["delivery"]["state"], "not-delivered");
10005 assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10006
10007 let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10010 assert!(skipped["delivery"].is_null(), "{skipped}");
10011 }
10012
10013 #[test]
10018 fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10019 let scratch = std::env::temp_dir().join(format!(
10020 "supercode-runs-delivery-{}-{}",
10021 std::process::id(),
10022 generated_session_id()
10023 ));
10024 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10025 let fixture = jobs_fixture_root().join("hermes_home");
10026 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10027 for name in ["cron/executions.db", "cron/jobs.json"] {
10028 std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10029 }
10030 {
10031 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10032 connection
10035 .execute(
10036 "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10037 [],
10038 )
10039 .unwrap();
10040 connection
10043 .execute(
10044 "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10045 ('cron_coder-standup_20260902_090010', 'cron', \
10046 'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10047 [],
10048 )
10049 .unwrap();
10050 }
10051 let response = runs_list(json!({
10052 "harness": "hermes",
10053 "job": "coder-standup",
10054 "homes": {"hermes": scratch.join("state.db")},
10055 }));
10056 let result = &response["result"];
10057 let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10058 assert_eq!(
10059 matched["session_id"], "cron_coder-standup_20260902_090010",
10060 "{result}"
10061 );
10062 assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10063 assert_eq!(
10064 matched["delivery"]["target"], "slack:C0FALLBACK:55",
10065 "{result}"
10066 );
10067 std::fs::remove_dir_all(&scratch).ok();
10068 }
10069
10070 #[test]
10071 fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10072 let scratch = std::env::temp_dir().join(format!(
10077 "supercode-runs-compressed-{}-{}",
10078 std::process::id(),
10079 generated_session_id()
10080 ));
10081 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10082 let fixture = jobs_fixture_root().join("hermes_home");
10083 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10084 std::fs::copy(
10085 fixture.join("cron/executions.db"),
10086 scratch.join("cron/executions.db"),
10087 )
10088 .unwrap();
10089 {
10090 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10091 connection
10092 .execute(
10093 "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10094 ["cron_job42_20260902_120000"],
10095 )
10096 .unwrap();
10097 connection
10098 .execute(
10099 "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10100 VALUES ('job42-after-compaction', 'cron', \
10101 'cron_job42_20260902_120000', 1788350000.0)",
10102 [],
10103 )
10104 .unwrap();
10105 }
10106 let response = runs_list(json!({
10107 "harness": "hermes",
10108 "job": "job42",
10109 "homes": {"hermes": scratch.join("state.db")},
10110 }));
10111 let result = &response["result"];
10112 assert_eq!(
10113 run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10114 "job42-after-compaction",
10115 "{result}"
10116 );
10117 std::fs::remove_dir_all(&scratch).ok();
10118 }
10119
10120 #[test]
10121 fn runs_list_filters_by_job_and_caps_by_limit() {
10122 let by_job = runs_list(json!({
10123 "harness": "openclaw",
10124 "job": OPENCLAW_HEALTH_JOB,
10125 "homes": jobs_fixture_homes(),
10126 }));
10127 let ids: Vec<&str> = by_job["result"]["runs"]
10128 .as_array()
10129 .unwrap()
10130 .iter()
10131 .map(|run| run["id"].as_str().unwrap())
10132 .collect();
10133 assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10134
10135 let capped = runs_list(json!({
10136 "harness": "openclaw",
10137 "limit": 1,
10138 "homes": jobs_fixture_homes(),
10139 }));
10140 let runs = capped["result"]["runs"].as_array().unwrap();
10141 assert_eq!(runs.len(), 1, "{capped}");
10142 assert_eq!(runs[0]["id"], "run_health_0002");
10144 }
10145
10146 #[test]
10147 fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10148 let mut service = HarnessSessionService::new();
10149 let hermes = service.handle(request(
10150 1,
10151 "harness.v1.runs.get",
10152 json!({
10153 "harness": "hermes",
10154 "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10155 "homes": jobs_fixture_homes(),
10156 }),
10157 ));
10158 assert_eq!(hermes["result"]["run"]["status"], "completed");
10159 assert_eq!(
10160 hermes["result"]["run"]["session_id"],
10161 "cron_job42_20260902_120000"
10162 );
10163 assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10165 assert_eq!(hermes["result"]["source"]["pid"], 4242);
10166 assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10167
10168 let openclaw = service.handle(request(
10169 2,
10170 "harness.v1.runs.get",
10171 json!({
10172 "harness": "openclaw",
10173 "id": "run_health_0002",
10174 "homes": jobs_fixture_homes(),
10175 }),
10176 ));
10177 assert_eq!(openclaw["result"]["run"]["status"], "error");
10178 assert_eq!(
10181 openclaw["result"]["source"]["delivery_status"],
10182 "not-delivered"
10183 );
10184 assert_eq!(
10185 openclaw["result"]["source"]["delivery_error"],
10186 "channel_not_found"
10187 );
10188 assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10189 assert_eq!(
10190 openclaw["result"]["run"]["delivery"]["state"],
10191 "not-delivered"
10192 );
10193 assert_eq!(
10194 openclaw["result"]["run"]["delivery"]["last_error"],
10195 "channel_not_found"
10196 );
10197
10198 let missing = service.handle(request(
10199 3,
10200 "harness.v1.runs.get",
10201 json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10202 ));
10203 assert!(missing["error"]["message"]
10204 .as_str()
10205 .is_some_and(|message| message.contains("no run `no-such-run`")));
10206 }
10207
10208 #[test]
10209 fn runs_refuse_a_harness_that_keeps_no_run_store() {
10210 let mut service = HarnessSessionService::new();
10211 for (id, method, params) in [
10212 (
10215 1,
10216 "harness.v1.runs.list",
10217 json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10218 ),
10219 (
10220 2,
10221 "harness.v1.runs.get",
10222 json!({"harness": "claude-code", "id": "anything"}),
10223 ),
10224 (
10225 3,
10226 "harness.v1.runs.list",
10227 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10228 ),
10229 ] {
10230 let response = service.handle(request(id, method, params));
10231 assert_eq!(response["error"]["code"], -32020, "{response}");
10232 assert!(response["error"]["message"]
10233 .as_str()
10234 .is_some_and(|message| message.contains("keeps no run store")));
10235 assert!(response.get("result").is_none());
10236 }
10237 }
10238
10239 #[test]
10240 fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10241 let scratch = std::env::temp_dir().join(format!(
10242 "supercode-runs-empty-{}-{}",
10243 std::process::id(),
10244 generated_session_id()
10245 ));
10246 std::fs::create_dir_all(&scratch).unwrap();
10247 let response = runs_list(json!({
10248 "harness": "openclaw",
10249 "homes": {"openclaw": scratch.clone()},
10250 }));
10251 let result = &response["result"];
10252 assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10253 assert_eq!(result["sources"][0]["state"], "absent_store");
10254 assert!(result["sources"][0]["path"]
10255 .as_str()
10256 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10257 std::fs::remove_dir_all(&scratch).ok();
10258 }
10259}