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 async fn runtime_call(
1471 &mut self,
1472 method: &str,
1473 params: Value,
1474 ) -> std::result::Result<Value, ServiceError> {
1475 match method {
1476 "harness.v1.runtimes.capabilities" => {
1477 let params = decode::<RuntimeBackendParams>(params)?;
1478 let backend = runtime_backend(¶ms)?;
1479 Ok(json!({
1480 "harness": backend.harness(),
1481 "capabilities": backend.capabilities(),
1482 }))
1483 }
1484 "harness.v1.runtimes.start" => {
1485 let params = decode::<RuntimeStartParams>(params)?;
1486 let backend = runtime_backend(¶ms.backend)?;
1487 let capabilities = backend.capabilities();
1488 let workspace = params.cwd.clone();
1489 let runtime = backend
1490 .start(RuntimeStartRequest {
1491 cwd: params.cwd,
1492 launch: runtime_launch(¶ms.backend),
1493 mcp_servers: params.mcp_servers,
1494 })
1495 .await
1496 .map_err(operation)?;
1497 self.insert_hosted_runtime(runtime, capabilities, workspace)
1498 .await
1499 }
1500 "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
1501 let params = decode::<RuntimeAttachParams>(params)?;
1502 let backend = runtime_backend(¶ms.backend)?;
1503 let capabilities = backend.capabilities();
1504 let workspace = params.cwd.clone().unwrap_or_else(|| {
1505 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
1506 });
1507 let runtime = backend
1508 .attach(RuntimeAttachRequest {
1509 runtime_id: params.runtime_id,
1510 cwd: params.cwd,
1511 launch: runtime_launch(¶ms.backend),
1512 })
1513 .await
1514 .map_err(operation)?;
1515 self.insert_hosted_runtime(runtime, capabilities, workspace)
1516 .await
1517 }
1518 "harness.v1.runtimes.attach_existing" => {
1519 let params = decode::<RuntimeAttachParams>(params)?;
1520 let backend: Box<dyn RuntimeBackend> = match params
1521 .backend
1522 .base_url
1523 .as_deref()
1524 .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
1525 {
1526 Some(endpoint) => {
1527 #[cfg(not(feature = "adapter-api"))]
1528 {
1529 let _ = endpoint;
1530 return Err(ServiceError::UnsupportedAction(
1531 "live HTTP attachment adapter is not compiled".into(),
1532 ));
1533 }
1534 #[cfg(feature = "adapter-api")]
1535 {
1536 let workspace = params.cwd.clone().ok_or_else(|| {
1537 ServiceError::InvalidParams(
1538 "Supercode live attach requires the project cwd".into(),
1539 )
1540 })?;
1541 let source = LiveRuntimeSource {
1542 harness: params.backend.harness.as_str().to_string(),
1543 session_id: params.runtime_id.clone(),
1544 workspace,
1545 };
1546 let receipt = resolve_live_runtime(&endpoint, &source)
1547 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1548 Box::new(SupercodeHttpRuntimeBackend::new(receipt))
1549 }
1550 }
1551 None => runtime_backend(¶ms.backend)?,
1552 };
1553 if !backend.capabilities().attach_existing_process {
1554 return Err(ServiceError::Operation(format!(
1555 "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
1556 backend.harness().as_str()
1557 )));
1558 }
1559 let runtime = backend
1560 .attach_existing(RuntimeAttachRequest {
1561 runtime_id: params.runtime_id,
1562 cwd: params.cwd,
1563 launch: runtime_launch(¶ms.backend),
1564 })
1565 .await
1566 .map_err(operation)?;
1567 self.insert_runtime(runtime)
1568 }
1569 "harness.v1.runtimes.send_input" => {
1570 let params = decode::<RuntimeInputParams>(params)?;
1571 let image_urls = validate_runtime_image_urls(params.image_urls)?;
1572 let runtime = self.runtime_mut(¶ms.connection)?;
1573 let turn_id = runtime
1574 .send_input(RuntimeInput {
1575 text: params.text,
1576 image_urls,
1577 })
1578 .await
1579 .map_err(operation)?;
1580 Ok(json!({"turn_id": turn_id}))
1581 }
1582 "harness.v1.runtimes.interrupt" => {
1583 let params = decode::<RuntimeConnectionParams>(params)?;
1584 self.runtime_mut(¶ms.connection)?
1585 .interrupt()
1586 .await
1587 .map_err(operation)?;
1588 Ok(json!({}))
1589 }
1590 "harness.v1.runtimes.steer" => {
1591 let params = decode::<RuntimeInputParams>(params)?;
1592 if !params.image_urls.is_empty() {
1593 return Err(ServiceError::InvalidParams(
1594 "runtime steering accepts text only".into(),
1595 ));
1596 }
1597 let text = params.text.trim();
1598 if text.is_empty() || text.chars().count() > 50_000 {
1599 return Err(ServiceError::InvalidParams(
1600 "runtime steering requires 1 to 50,000 text characters".into(),
1601 ));
1602 }
1603 self.runtime_mut(¶ms.connection)?
1604 .steer(text.to_string())
1605 .await
1606 .map_err(operation)?;
1607 Ok(json!({}))
1608 }
1609 "harness.v1.runtimes.respond" => {
1610 let params = decode::<RuntimeRespondParams>(params)?;
1611 let request_id = params.request_id.clone();
1612 self.runtime_mut(¶ms.connection)?
1613 .respond(params.request_id, params.response)
1614 .await
1615 .map_err(operation)?;
1616 self.approvals.answered(¶ms.connection, &request_id);
1618 Ok(json!({}))
1619 }
1620 "harness.v1.runtimes.terminal_instructions" => {
1621 let params = decode::<RuntimeConnectionParams>(params)?;
1622 let launch = self
1623 .terminal_launches
1624 .get(¶ms.connection)
1625 .ok_or_else(|| {
1626 ServiceError::Operation(
1627 "this runtime is not hosted for terminal attachment".into(),
1628 )
1629 })?;
1630 Ok(json!({"launch":launch}))
1631 }
1632 "harness.v1.runtimes.close" => {
1633 let params = decode::<RuntimeConnectionParams>(params)?;
1634 let Some(mut runtime) = self.runtimes.remove(¶ms.connection) else {
1635 return Err(ServiceError::InvalidParams(format!(
1636 "unknown runtime connection `{}`",
1637 params.connection
1638 )));
1639 };
1640 self.terminal_launches.remove(¶ms.connection);
1641 self.runtime_sequences.remove(&runtime.handle().runtime_id);
1642 self.approvals.forget(¶ms.connection);
1643 runtime.close().await.map_err(operation)?;
1644 Ok(json!({"closed": true}))
1645 }
1646 _ => Err(ServiceError::MethodNotFound),
1647 }
1648 }
1649
1650 #[cfg(feature = "adapter-api")]
1652 async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1653 let params = decode::<MessageSessionParams>(params)?;
1654 Ok(message_live_session(¶ms, &crate::claude_peer::ProcessCourierRunner).await)
1655 }
1656
1657 #[cfg(feature = "adapter-api")]
1658 fn harness_settings_call(
1659 &self,
1660 method: &str,
1661 params: Value,
1662 ) -> std::result::Result<Value, ServiceError> {
1663 let homes = crate::HarnessHomes::default();
1664 match method {
1665 "harness.v1.harnesses.settings" => {
1666 let params = decode::<HarnessSettingsParams>(params)?;
1667 let report = crate::inspect_harness_interop_settings(&homes, ¶ms.harness)
1668 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1669 serde_json::to_value(report)
1670 .map_err(|error| ServiceError::Operation(error.to_string()))
1671 }
1672 "harness.v1.harnesses.configure" => {
1673 let params = decode::<ConfigureHarnessParams>(params)?;
1674 let report = crate::configure_harness_interop_settings(
1675 &homes,
1676 ¶ms.harness,
1677 ¶ms.changes,
1678 params.expected_revision.as_deref(),
1679 )
1680 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1681 serde_json::to_value(report)
1682 .map_err(|error| ServiceError::Operation(error.to_string()))
1683 }
1684 _ => Err(ServiceError::MethodNotFound),
1685 }
1686 }
1687
1688 fn insert_runtime(
1689 &mut self,
1690 runtime: Box<dyn RuntimeConnection>,
1691 ) -> std::result::Result<Value, ServiceError> {
1692 let connection = format!("runtime-{}", self.next_runtime);
1693 self.next_runtime += 1;
1694 let handle = runtime.handle().clone();
1695 self.runtime_sequences
1696 .entry(handle.runtime_id.clone())
1697 .or_insert(0);
1698 self.runtimes.insert(connection.clone(), runtime);
1699 Ok(json!({"connection": connection, "handle": handle}))
1700 }
1701
1702 #[cfg(feature = "adapter-api")]
1703 async fn insert_hosted_runtime(
1704 &mut self,
1705 runtime: Box<dyn RuntimeConnection>,
1706 capabilities: crate::RuntimeCapabilities,
1707 workspace: PathBuf,
1708 ) -> std::result::Result<Value, ServiceError> {
1709 let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1710 let token: std::sync::Arc<str> = crate::server::generate_token().into();
1711 let server = crate::server::run_frontend_http(
1712 host.clone(),
1713 host.frontend_sender(),
1714 "127.0.0.1:0",
1715 token.clone(),
1716 )
1717 .await
1718 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1719 let source = LiveRuntimeSource {
1720 harness: connection.handle().harness.as_str().to_string(),
1721 session_id: connection.handle().runtime_id.clone(),
1722 workspace: workspace.clone(),
1723 };
1724 let registration = register_live_runtime(
1725 connection.handle().runtime_id.clone(),
1726 source.clone(),
1727 format!("http://{}", server.address()),
1728 token.to_string(),
1729 )
1730 .map_err(|error| ServiceError::Operation(error.to_string()))?;
1731 let endpoint = registration.endpoint().to_string();
1732 let launch = StructuredLaunch {
1733 cwd: workspace,
1734 program: std::env::current_exe()
1738 .ok()
1739 .map(|path| path.to_string_lossy().into_owned())
1740 .unwrap_or_else(|| "supercode".into()),
1741 arguments: vec![
1742 "harness".into(),
1743 "attach".into(),
1744 "--endpoint".into(),
1745 endpoint,
1746 "--harness".into(),
1747 source.harness,
1748 "--session".into(),
1749 source.session_id,
1750 ],
1751 env: BTreeMap::new(),
1752 };
1753 let lease = HostedRuntimeLease {
1754 connection,
1755 _host: host,
1756 _registration: registration,
1757 _server: server,
1758 };
1759 let opened = self.insert_runtime(Box::new(lease))?;
1760 let connection_id = opened["connection"]
1761 .as_str()
1762 .expect("insert_runtime returns a connection id")
1763 .to_string();
1764 self.terminal_launches.insert(connection_id, launch);
1765 Ok(opened)
1766 }
1767
1768 #[cfg(not(feature = "adapter-api"))]
1769 async fn insert_hosted_runtime(
1770 &mut self,
1771 runtime: Box<dyn RuntimeConnection>,
1772 _capabilities: crate::RuntimeCapabilities,
1773 _workspace: PathBuf,
1774 ) -> std::result::Result<Value, ServiceError> {
1775 self.insert_runtime(runtime)
1776 }
1777
1778 fn runtime_mut(
1779 &mut self,
1780 connection: &str,
1781 ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1782 self.runtimes.get_mut(connection).ok_or_else(|| {
1783 ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1784 })
1785 }
1786
1787 async fn mutate_session(
1798 &mut self,
1799 verb: crate::SessionVerb,
1800 params: Value,
1801 ) -> std::result::Result<Value, ServiceError> {
1802 let mutation = decode::<crate::SessionMutation>(params)?;
1803 let door = crate::sessions_control::door(&mutation.harness, verb)
1804 .map_err(session_control_error)?;
1805 let outcome = match door {
1806 #[cfg(not(feature = "adapter-api"))]
1810 crate::SessionDoor::Live(command) => {
1811 return Err(ServiceError::Operation(format!(
1812 "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
1813 session, which needs this build's `adapter-api` feature",
1814 mutation.harness,
1815 verb.as_str()
1816 )));
1817 }
1818 #[cfg(feature = "adapter-api")]
1819 crate::SessionDoor::Live(command) => {
1820 let connection = mutation
1821 .connection
1822 .clone()
1823 .filter(|value| !value.trim().is_empty())
1824 .ok_or_else(|| {
1825 ServiceError::InvalidParams(format!(
1826 "`{}` performs `sessions.{}` by typing `{command}` into a live \
1827 driven session: pass the `connection` of an open runtime \
1828 (`harness.v1.runtimes.start`)",
1829 mutation.harness,
1830 verb.as_str()
1831 ))
1832 })?;
1833 let runtime = self.runtime_mut(&connection)?;
1834 let session = mutation
1835 .session
1836 .clone()
1837 .filter(|value| !value.trim().is_empty())
1838 .unwrap_or_else(|| runtime.handle().runtime_id.clone());
1839 runtime
1840 .send_input(RuntimeInput {
1841 text: command.to_string(),
1842 image_urls: Vec::new(),
1843 })
1844 .await
1845 .map_err(operation)?;
1846 crate::sessions_control::live_outcome(verb, &mutation, command, session)
1847 .map_err(session_control_error)?
1848 }
1849 _ => crate::sessions_control::mutate(verb, &mutation)
1850 .await
1851 .map_err(session_control_error)?,
1852 };
1853 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
1854 }
1855
1856 async fn inventory_call(
1857 &self,
1858 method: &str,
1859 params: Value,
1860 ) -> std::result::Result<Value, ServiceError> {
1861 let mut params = decode::<HarnessInventoryParams>(params)?;
1862 if method == "harness.v1.harnesses.probe" {
1863 let harness = params.harness.take().ok_or_else(|| {
1864 ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
1865 })?;
1866 params.harnesses = vec![harness];
1867 }
1868 let selected = params
1869 .harnesses
1870 .iter()
1871 .map(HarnessId::as_str)
1872 .collect::<std::collections::BTreeSet<_>>();
1873 let supported = harness_support_registry()
1874 .harnesses
1875 .into_iter()
1876 .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
1877 .collect::<Vec<_>>();
1878 if !params.harnesses.is_empty() && supported.len() != selected.len() {
1879 let known = supported
1880 .iter()
1881 .map(|harness| harness.id.as_str())
1882 .collect::<std::collections::BTreeSet<_>>();
1883 let missing = params
1884 .harnesses
1885 .iter()
1886 .filter(|id| !known.contains(id.as_str()))
1887 .map(HarnessId::as_str)
1888 .collect::<Vec<_>>();
1889 return Err(ServiceError::InvalidParams(format!(
1890 "unknown harness(es): {}",
1891 missing.join(", ")
1892 )));
1893 }
1894 let global_counts = params
1895 .include_sessions
1896 .then(|| self.session_counts(None, ¶ms.harnesses));
1897 let workspace_counts = params.include_sessions.then(|| {
1898 params
1899 .workspace
1900 .as_deref()
1901 .map(|workspace| self.session_counts(Some(workspace), ¶ms.harnesses))
1902 });
1903 let probes = supported.into_iter().map(|descriptor| {
1904 let global = global_counts
1905 .as_ref()
1906 .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
1907 let workspace = workspace_counts
1908 .as_ref()
1909 .and_then(Option::as_ref)
1910 .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
1911 self.probe_harness(descriptor, ¶ms, global, workspace)
1912 });
1913 let harnesses = futures::future::join_all(probes).await;
1914 serde_json::to_value(HarnessInventoryReport {
1915 probe: params.probe,
1916 workspace: params.workspace,
1917 harnesses,
1918 })
1919 .map_err(|error| ServiceError::Operation(error.to_string()))
1920 }
1921
1922 #[cfg(feature = "adapter-api")]
1923 async fn harness_authentication_call(
1924 &self,
1925 method: &str,
1926 params: Value,
1927 ) -> std::result::Result<Value, ServiceError> {
1928 match method {
1929 "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
1930 let params = decode::<HarnessAuthenticationParams>(params)?;
1931 serde_json::to_value(crate::inspect_harness_authentication(¶ms.harness).await)
1932 .map_err(|error| ServiceError::Operation(error.to_string()))
1933 }
1934 "harness.v1.harnesses.auth.begin" => {
1935 let params = decode::<BeginHarnessAuthenticationParams>(params)?;
1936 let cwd = params
1937 .cwd
1938 .or_else(|| std::env::current_dir().ok())
1939 .unwrap_or_else(|| PathBuf::from("."));
1940 let plan = crate::harness_authentication_plan(
1941 ¶ms.harness,
1942 params.environment,
1943 params.method,
1944 &cwd,
1945 )
1946 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
1947 serde_json::to_value(plan)
1948 .map_err(|error| ServiceError::Operation(error.to_string()))
1949 }
1950 _ => Err(ServiceError::MethodNotFound),
1951 }
1952 }
1953
1954 async fn probe_harness(
1955 &self,
1956 descriptor: crate::HarnessSupportDescriptor,
1957 params: &HarnessInventoryParams,
1958 global: Option<usize>,
1959 workspace: Option<usize>,
1960 ) -> LocalHarness {
1961 let launch = descriptor.runtime.default_launch.as_ref();
1962 let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
1967 .then(crate::orchestrator::daemon_entry)
1968 .and_then(Result::ok);
1969 let executable = match &orchestrator_entry {
1970 Some(entry) => Some(entry.clone()),
1971 None => launch.and_then(|launch| find_executable(&launch.program)),
1972 };
1973 let installed = executable.is_some();
1974 let version = if params.skip_versions || orchestrator_entry.is_some() {
1975 None
1978 } else {
1979 match executable.as_deref() {
1980 Some(path) => executable_version(path).await,
1981 None => None,
1982 }
1983 };
1984 let configured = auth_evidence(descriptor.id.as_str());
1985 let mut auth = if configured {
1986 HarnessAuthState::Configured
1987 } else if matches!(
1988 descriptor.id.as_str(),
1989 HarnessId::CLAUDE_CODE | HarnessId::CODEX
1990 ) {
1991 HarnessAuthState::Required
1996 } else {
1997 HarnessAuthState::Unknown
1998 };
1999 let mut runtime = if installed {
2000 HarnessRuntimeState::Degraded
2001 } else {
2002 HarnessRuntimeState::Unavailable
2003 };
2004 let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2005 let mut reason = (!installed).then(|| {
2006 if is_orchestrator {
2007 format!(
2008 "{} is supported but its daemon entry `{}` was not found",
2009 descriptor.display_name,
2010 crate::orchestrator::DAEMON_ENTRY
2011 )
2012 } else {
2013 format!(
2014 "{} is supported but `{}` was not found on PATH",
2015 descriptor.display_name,
2016 launch
2017 .map(|launch| launch.program.as_str())
2018 .unwrap_or("executable")
2019 )
2020 }
2021 });
2022 let mut repair = (!installed).then(|| {
2023 if is_orchestrator {
2024 format!(
2025 "Install the `supercode-orchestrator` package so `{}` resolves.",
2026 crate::orchestrator::DAEMON_ENTRY
2027 )
2028 } else {
2029 format!(
2030 "Install {} and ensure `{}` is on PATH.",
2031 descriptor.display_name,
2032 launch
2033 .map(|launch| launch.program.as_str())
2034 .unwrap_or("its executable")
2035 )
2036 }
2037 });
2038
2039 if installed && params.probe == HarnessProbeLevel::Handshake {
2040 let backend_params = RuntimeBackendParams {
2041 harness: descriptor.id.clone(),
2042 protocol: None,
2043 launch: None,
2044 base_url: None,
2045 policy: RuntimePolicy::Default,
2046 };
2047 match runtime_backend(&backend_params) {
2048 Ok(backend) => {
2049 let cwd = params
2050 .workspace
2051 .clone()
2052 .or_else(|| std::env::current_dir().ok())
2053 .unwrap_or_else(|| PathBuf::from("."));
2054 let isolated = descriptor
2055 .runtime
2056 .default_launch
2057 .clone()
2058 .and_then(|launch| {
2059 IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok()
2060 });
2061 let Some(isolated) = isolated else {
2062 reason = Some(
2063 "No-prompt runtime handshake could not create its isolated harness home."
2064 .into(),
2065 );
2066 repair = Some(
2067 "Check temporary-directory permissions, then run the handshake probe again."
2068 .into(),
2069 );
2070 let running = probe_running_instance(descriptor.id.as_str());
2071 return LocalHarness {
2072 gateway: gateway_health(
2073 descriptor.id.as_str(),
2074 installed,
2075 running.as_ref(),
2076 version.as_deref(),
2077 ),
2078 id: descriptor.id,
2079 display_name: descriptor.display_name,
2080 supported: true,
2081 installed,
2082 executable: executable.map(|path| path.to_string_lossy().into_owned()),
2083 version,
2084 auth,
2085 runtime,
2086 protocol: descriptor.runtime.protocol,
2087 capabilities: descriptor.runtime.capabilities.clone(),
2088 effective_capabilities: descriptor.runtime.capabilities,
2089 sessions: HarnessSessionCounts { global, workspace },
2090 running,
2091 reason,
2092 repair,
2093 };
2094 };
2095 match tokio::time::timeout(
2096 Duration::from_secs(30),
2097 backend.start(RuntimeStartRequest {
2098 cwd,
2099 launch: Some(isolated.launch.clone()),
2100 mcp_servers: Vec::new(),
2101 }),
2102 )
2103 .await
2104 {
2105 Ok(Ok(mut connection)) => {
2106 match stabilize_handshake(connection.as_mut()).await {
2107 Ok(()) => {
2108 auth = HarnessAuthState::Ready;
2109 runtime = HarnessRuntimeState::Ready;
2110 reason = Some(
2111 "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2112 .into(),
2113 );
2114 repair = None;
2115 }
2116 Err(message) => {
2117 auth = if looks_like_auth_error(&message) {
2118 HarnessAuthState::Required
2119 } else if configured {
2120 HarnessAuthState::Configured
2121 } else {
2122 HarnessAuthState::Unknown
2123 };
2124 reason = Some(format!(
2125 "No-prompt runtime handshake became unhealthy during startup: {message}"
2126 ));
2127 repair = Some(if auth == HarnessAuthState::Required {
2128 format!(
2129 "Run `{}` interactively once and complete sign-in, then probe again.",
2130 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2131 )
2132 } else {
2133 "Run the harness directly to inspect its startup failure, then probe again."
2134 .into()
2135 });
2136 }
2137 }
2138 let _ =
2139 tokio::time::timeout(Duration::from_secs(3), connection.close())
2140 .await;
2141 }
2142 Ok(Err(error)) => {
2143 let message = truncate_text(&error.to_string(), 500);
2144 auth = if looks_like_auth_error(&message) {
2145 HarnessAuthState::Required
2146 } else if configured {
2147 HarnessAuthState::Configured
2148 } else {
2149 HarnessAuthState::Unknown
2150 };
2151 reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2152 repair = Some(if auth == HarnessAuthState::Required {
2153 format!(
2154 "Run `{}` interactively once and complete sign-in, then probe again.",
2155 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2156 )
2157 } else {
2158 "Check the harness installation and run the handshake probe again."
2159 .into()
2160 });
2161 }
2162 Err(_) => {
2163 reason = Some(
2164 "No-prompt runtime handshake timed out after 30 seconds.".into(),
2165 );
2166 repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2167 }
2168 }
2169 let _ = isolated.cleanup();
2178 tokio::time::sleep(Duration::from_millis(250)).await;
2179 if let Err(error) = isolated.cleanup() {
2180 auth = if configured {
2181 HarnessAuthState::Configured
2182 } else {
2183 HarnessAuthState::Unknown
2184 };
2185 runtime = HarnessRuntimeState::Degraded;
2186 reason = Some(format!(
2187 "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2188 ));
2189 repair = Some(
2190 "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2191 .into(),
2192 );
2193 }
2194 }
2195 Err(error) => {
2196 reason = Some(error_message(error));
2197 }
2198 }
2199 } else if installed && configured {
2200 reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2201 } else if installed && auth == HarnessAuthState::Required {
2202 reason =
2203 Some("Executable found, but no native authentication evidence is present.".into());
2204 repair = Some(format!(
2205 "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2206 descriptor.id.as_str()
2207 ));
2208 } else if installed {
2209 reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2210 repair =
2211 Some(format!(
2212 "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2213 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2214 ));
2215 }
2216
2217 let effective_capabilities = if installed {
2218 descriptor.runtime.capabilities.clone()
2219 } else {
2220 unavailable_capabilities()
2221 };
2222 let running = probe_running_instance(descriptor.id.as_str());
2223 LocalHarness {
2224 gateway: gateway_health(
2225 descriptor.id.as_str(),
2226 installed,
2227 running.as_ref(),
2228 version.as_deref(),
2229 ),
2230 id: descriptor.id,
2231 display_name: descriptor.display_name,
2232 supported: true,
2233 installed,
2234 executable: executable.map(|path| path.to_string_lossy().into_owned()),
2235 version,
2236 auth,
2237 runtime,
2238 protocol: descriptor.runtime.protocol,
2239 capabilities: descriptor.runtime.capabilities,
2240 effective_capabilities,
2241 sessions: HarnessSessionCounts { global, workspace },
2242 running,
2243 reason,
2244 repair,
2245 }
2246 }
2247
2248 fn session_counts(
2249 &self,
2250 workspace: Option<&Path>,
2251 harnesses: &[HarnessId],
2252 ) -> BTreeMap<String, usize> {
2253 let mut counts = BTreeMap::new();
2254 for session in self
2255 .catalog
2256 .discover(&DiscoveryQuery {
2257 workspace: workspace.map(Path::to_path_buf),
2258 harnesses: harnesses.to_vec(),
2259 ..DiscoveryQuery::default()
2260 })
2261 .unwrap_or_default()
2262 {
2263 *counts
2264 .entry(session.locator.harness.as_str().to_string())
2265 .or_insert(0) += 1;
2266 }
2267 counts
2268 }
2269}
2270
2271#[async_trait::async_trait]
2272impl SdkService for HarnessSessionService {
2273 fn capabilities(&self) -> SdkCapabilities {
2274 SdkCapabilities::default()
2275 }
2276
2277 async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2278 if request.operation == SdkOperation::Events {
2279 let events = self
2280 .poll_sdk_events()
2281 .await
2282 .into_iter()
2283 .map(|(_, event)| event)
2284 .collect::<Vec<_>>();
2285 return serde_json::to_value(events).map_err(|error| {
2286 SdkError::new(
2287 SdkErrorCode::Execution,
2288 request.operation,
2289 error.to_string(),
2290 )
2291 });
2292 }
2293 if self.runtimes.is_empty()
2294 && matches!(
2295 request.operation,
2296 SdkOperation::Input
2297 | SdkOperation::Interrupt
2298 | SdkOperation::Steer
2299 | SdkOperation::Respond
2300 | SdkOperation::Close
2301 )
2302 {
2303 return Err(SdkError::unsupported(request.operation));
2304 }
2305 let method = request
2306 .operation
2307 .method()
2308 .ok_or_else(|| SdkError::unsupported(request.operation))?;
2309 let result = match request.operation {
2310 SdkOperation::Discover
2311 | SdkOperation::Load
2312 | SdkOperation::Export
2313 | SdkOperation::ProfilesList
2314 | SdkOperation::ProfilesGet
2315 | SdkOperation::ProfilesCreate
2316 | SdkOperation::ProfilesDelete
2317 | SdkOperation::SkillsList
2318 | SdkOperation::SkillsInstall
2319 | SdkOperation::SkillsRemove
2320 | SdkOperation::ChannelsList
2321 | SdkOperation::RoutesList
2322 | SdkOperation::TriggersList
2323 | SdkOperation::ChannelsStatus
2324 | SdkOperation::MemoryShow
2325 | SdkOperation::MemorySearch
2326 | SdkOperation::JobsList
2327 | SdkOperation::JobsGet
2328 | SdkOperation::JobsCreate
2329 | SdkOperation::JobsUpdate
2330 | SdkOperation::JobsPause
2331 | SdkOperation::JobsResume
2332 | SdkOperation::JobsRun
2333 | SdkOperation::JobsDelete
2334 | SdkOperation::RunsList
2335 | SdkOperation::RunsGet
2336 | SdkOperation::ApprovalsList
2337 | SdkOperation::OrchestrationLoad
2338 | SdkOperation::OrchestrationSave
2339 | SdkOperation::OrchestrationCompile
2340 | SdkOperation::OrchestrationDecompile
2341 | SdkOperation::OrchestrationImport
2342 | SdkOperation::OrchestrationExport
2343 | SdkOperation::WorkflowLoad => self.call(method, request.params),
2344 SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2347 SdkOperation::Start
2348 | SdkOperation::Resume
2349 | SdkOperation::Input
2350 | SdkOperation::Interrupt
2351 | SdkOperation::Steer
2352 | SdkOperation::Respond
2353 | SdkOperation::Close => self.runtime_call(method, request.params).await,
2354 SdkOperation::SessionsNew => {
2359 self.mutate_session(crate::SessionVerb::New, request.params)
2360 .await
2361 }
2362 SdkOperation::SessionsReset => {
2363 self.mutate_session(crate::SessionVerb::Reset, request.params)
2364 .await
2365 }
2366 SdkOperation::SessionsArchive => {
2367 self.mutate_session(crate::SessionVerb::Archive, request.params)
2368 .await
2369 }
2370 SdkOperation::SessionsDelete => {
2371 self.mutate_session(crate::SessionVerb::Delete, request.params)
2372 .await
2373 }
2374 SdkOperation::Events => unreachable!("handled before method dispatch"),
2375 };
2376 result.map_err(|error| sdk_error(request.operation, error))
2377 }
2378
2379 async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2380 Ok(self
2381 .poll_sdk_events()
2382 .await
2383 .into_iter()
2384 .map(|(_, event)| event)
2385 .collect())
2386 }
2387}
2388
2389#[cfg(feature = "adapter-api")]
2390struct HostedRuntimeLease {
2391 connection: HostedHarnessConnection,
2392 _host: std::sync::Arc<HostedHarnessRuntime>,
2393 _registration: LiveRuntimeRegistration,
2394 _server: crate::server::FrontendHttpServer,
2395}
2396
2397#[async_trait::async_trait]
2398#[cfg(feature = "adapter-api")]
2399impl RuntimeConnection for HostedRuntimeLease {
2400 fn handle(&self) -> &crate::RuntimeHandle {
2401 self.connection.handle()
2402 }
2403
2404 async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2405 self.connection.send_input(input).await
2406 }
2407
2408 async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2409 self.connection.next_event().await
2410 }
2411
2412 async fn interrupt(&mut self) -> crate::Result<()> {
2413 self.connection.interrupt().await
2414 }
2415
2416 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2417 self.connection.respond(request_id, response).await
2418 }
2419
2420 async fn close(&mut self) -> crate::Result<()> {
2421 self.connection.close().await
2422 }
2423}
2424
2425async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2426 let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2427 loop {
2428 let now = tokio::time::Instant::now();
2429 if now >= deadline {
2430 return Ok(());
2431 }
2432 match tokio::time::timeout(deadline - now, connection.next_event()).await {
2433 Err(_) => return Ok(()),
2434 Ok(Ok(Some(event))) => {
2435 if let Some(message) = handshake_event_failure(&event) {
2436 return Err(truncate_text(&message, 500));
2437 }
2438 }
2439 Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2440 Ok(Err(error)) => return Err(error.to_string()),
2441 }
2442 }
2443}
2444
2445fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2446 let detail = event
2447 .payload
2448 .get("message")
2449 .or_else(|| event.payload.get("line"))
2450 .and_then(Value::as_str)
2451 .unwrap_or(event.kind.as_str());
2452 match event.kind.as_str() {
2453 "transport_closed" => Some("runtime transport closed during startup".into()),
2454 "transport_error" => Some(format!("runtime transport error: {detail}")),
2455 "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2456 _ => None,
2461 }
2462}
2463
2464fn indexed_claude_window(
2465 locator: &SessionLocator,
2466 options: &SessionLoadOptions,
2467) -> std::result::Result<Option<Value>, ServiceError> {
2468 use supercode_interchange::session::ClaudeReadIndex;
2469 if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2472 || options.include_subagents != Some(false)
2473 {
2474 return Ok(None);
2475 }
2476 let crate::StorageLocator::File { path } = &locator.storage else {
2477 return Ok(None);
2478 };
2479 if !ClaudeReadIndex::supports(path)
2480 .map_err(|error| ServiceError::Operation(error.to_string()))?
2481 {
2482 return Ok(None);
2483 }
2484 let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2485 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2486 let total = index.len();
2487 let (offset, end) = projected_message_window(total, options);
2488 let session = index
2489 .read_messages(offset..end)
2490 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2491 let summary = index
2492 .read_summary()
2493 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2494 let selected_options = SessionLoadOptions {
2495 message_offset: None,
2496 message_limit: None,
2497 message_tail: None,
2498 ..options.clone()
2499 };
2500 let mut selected = projected_session_json(&session, &selected_options);
2501 selected["raw_record_count"] = json!(index.raw_record_count());
2502 Ok(Some(json!({
2503 "session": selected,
2504 "summary": projected_session_summary(&summary, options),
2505 "window": {
2506 "has_more": offset > 0 || end < total, "has_newer": end < total,
2507 "has_older": offset > 0, "newer_items": index.item_count(end..total),
2508 "offset": offset, "older_items": index.item_count(0..offset),
2509 "returned": end - offset, "total_messages": total,
2510 }
2511 })))
2512}
2513
2514fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2515 let total_messages = session.messages.len();
2516 let (offset, end) = projected_message_window(total_messages, options);
2517 json!({
2518 "session": projected_session_json(session, options),
2519 "summary": projected_session_summary(session, options),
2520 "window": {
2521 "has_more": offset > 0 || end < total_messages,
2522 "has_newer": end < total_messages,
2523 "has_older": offset > 0,
2524 "newer_items": normalized_item_count(&session.messages[end..]),
2525 "offset": offset,
2526 "older_items": normalized_item_count(&session.messages[..offset]),
2527 "returned": end.saturating_sub(offset),
2528 "total_messages": total_messages,
2529 }
2530 })
2531}
2532
2533fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2534 messages
2535 .iter()
2536 .map(|message| {
2537 let conversation = usize::from(
2538 matches!(message.role, Role::Assistant | Role::User)
2539 && message_has_content(message),
2540 );
2541 let tool_result =
2542 usize::from(message.role == Role::Tool && message_has_content(message));
2543 conversation + tool_result + message.tool_calls().len()
2544 })
2545 .sum()
2546}
2547
2548fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2549 let mut conversational = session.messages.iter().filter(|message| {
2550 matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2551 });
2552 let first_message = conversational.clone().next();
2553 let last_message = conversational.next_back();
2554 let mut assistant = session
2555 .messages
2556 .iter()
2557 .filter(|message| message.role == Role::Assistant && message_has_content(message));
2558 let first_assistant_message = assistant.clone().next();
2559 let last_assistant_message = assistant.next_back();
2560 let end_of_turn = session
2561 .messages
2562 .iter()
2563 .rev()
2564 .find(|message| message.role != Role::System)
2565 .is_some_and(|message| {
2566 message.role == Role::Assistant
2567 && message_has_content(message)
2568 && message.tool_calls().is_empty()
2569 });
2570 let project = |message: Option<&crate::ChatMessage>| {
2571 message.map(|message| project_inline_media(message_json(message), options))
2572 };
2573 json!({
2574 "end_of_turn": end_of_turn,
2575 "first_assistant_message": project(first_assistant_message),
2576 "first_message": project(first_message),
2577 "last_assistant_message": project(last_assistant_message),
2578 "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2579 "last_message": project(last_message),
2580 })
2581}
2582
2583fn message_has_content(message: &crate::ChatMessage) -> bool {
2584 message
2585 .content
2586 .as_deref()
2587 .is_some_and(|content| !content.trim().is_empty())
2588 || message
2589 .content_parts
2590 .as_ref()
2591 .is_some_and(|parts| !parts.is_empty())
2592}
2593
2594fn message_text(message: &crate::ChatMessage) -> String {
2595 if let Some(content) = &message.content {
2596 return content.clone();
2597 }
2598 message
2599 .content_parts
2600 .as_ref()
2601 .into_iter()
2602 .flatten()
2603 .filter_map(|part| part.get("text").and_then(Value::as_str))
2604 .collect::<Vec<_>>()
2605 .join("\n")
2606}
2607
2608fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2609 let (offset, end) = projected_message_window(session.messages.len(), options);
2610 let messages = session.messages[offset..end]
2611 .iter()
2612 .map(|message| project_inline_media(message_json(message), options))
2613 .collect::<Vec<_>>();
2614 let subagents = if options.include_subagents.unwrap_or(true) {
2615 let subagent_options = SessionLoadOptions {
2620 message_limit: None,
2621 message_offset: None,
2622 message_tail: None,
2623 ..options.clone()
2624 };
2625 session
2626 .subagents
2627 .iter()
2628 .map(|subagent| projected_session_json(subagent, &subagent_options))
2629 .collect::<Vec<_>>()
2630 } else {
2631 Vec::new()
2632 };
2633 json!({
2634 "source": match session.meta.source {
2635 SessionSource::ClaudeCode => "claude_code",
2636 SessionSource::Codex => "codex",
2637 SessionSource::Gemini => "gemini",
2638 SessionSource::Goose => "goose",
2639 SessionSource::Grok => "grok",
2640 SessionSource::Native => "native",
2641 SessionSource::OpenClaw => "openclaw",
2642 SessionSource::Hermes => "hermes",
2643 SessionSource::OpenCode => "opencode",
2644 SessionSource::Pi => "pi",
2645 },
2646 "session_id": session.meta.session_id,
2647 "model": session.meta.model,
2648 "cwd": session.meta.cwd,
2649 "system_prompt": session.meta.system_prompt,
2650 "agent_id": session.meta.agent_id,
2651 "parent_tool_use_id": session.meta.parent_tool_use_id,
2652 "lineage": session.meta.lineage,
2653 "messages": messages,
2654 "subagents": subagents,
2655 "raw_record_count": session.raw.len(),
2656 "parse_error_lines": session.parse_error_lines,
2657 })
2658}
2659
2660fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
2661 if let Some(tail) = options.message_tail {
2662 return (total.saturating_sub(tail), total);
2663 }
2664 let offset = options.message_offset.unwrap_or(0).min(total);
2665 let end = options
2666 .message_limit
2667 .map(|limit| offset.saturating_add(limit).min(total))
2668 .unwrap_or(total);
2669 (offset, end)
2670}
2671
2672fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
2673 let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
2674 return message;
2675 };
2676 for part in parts {
2677 let Some(url) = part
2678 .get("image_url")
2679 .and_then(|image| image.get("url"))
2680 .and_then(Value::as_str)
2681 else {
2682 continue;
2683 };
2684 let Some(rest) = url.strip_prefix("data:") else {
2685 continue;
2686 };
2687 let Some((media_type, encoded)) = rest.split_once(";base64,") else {
2688 continue;
2689 };
2690 let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
2691 let decoded_bytes = encoded.len().saturating_mul(3) / 4;
2692 let decoded_bytes = decoded_bytes.saturating_sub(padding);
2693 let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
2694 || options
2695 .max_inline_media_bytes
2696 .is_some_and(|limit| decoded_bytes > limit);
2697 if should_elide {
2698 *part = json!({
2699 "type": "media_reference",
2700 "media_type": media_type,
2701 "encoding": "base64",
2702 "encoded_bytes": encoded.len(),
2703 "decoded_bytes": decoded_bytes,
2704 "omitted": true,
2705 });
2706 }
2707 }
2708 message
2709}
2710
2711#[derive(Deserialize)]
2712struct LocatorParams {
2713 locator: SessionLocator,
2714 #[serde(default)]
2727 fidelity: Option<Fidelity>,
2728 #[serde(default)]
2731 view: Option<SessionReadView>,
2732}
2733
2734#[derive(Deserialize)]
2735struct SessionReadView {
2736 #[serde(default)]
2739 tail_messages: Option<usize>,
2740 #[serde(default)]
2743 include_subagents: bool,
2744 #[serde(default)]
2746 display_history: bool,
2747 #[serde(default)]
2750 max_message_chars: Option<usize>,
2751}
2752
2753impl LocatorParams {
2754 fn read_fidelity(&self) -> Fidelity {
2755 self.fidelity.unwrap_or(Fidelity::Semantic)
2756 }
2757
2758 fn include_subagents(&self) -> bool {
2759 self.view
2760 .as_ref()
2761 .map(|view| view.include_subagents)
2762 .unwrap_or(true)
2763 }
2764
2765 fn tail_messages(&self) -> Option<usize> {
2766 self.view
2767 .as_ref()
2768 .and_then(|view| view.tail_messages)
2769 .map(|limit| limit.clamp(1, 5_000))
2770 }
2771
2772 fn display_history(&self) -> bool {
2773 self.view.as_ref().is_some_and(|view| view.display_history)
2774 }
2775
2776 fn max_message_chars(&self) -> Option<usize> {
2777 self.view
2778 .as_ref()
2779 .and_then(|view| view.max_message_chars)
2780 .map(|limit| limit.clamp(256, 64_000))
2781 }
2782
2783 fn bound_session(&self, session: &mut Session) {
2784 bound_session_view(session, self.tail_messages(), self.max_message_chars());
2785 }
2786}
2787
2788#[derive(Debug, Clone, Copy, Default, Deserialize)]
2789#[serde(rename_all = "snake_case")]
2790enum InlineMediaMode {
2791 #[default]
2792 Full,
2793 Metadata,
2794}
2795
2796#[derive(Debug, Clone, Default, Deserialize)]
2797#[serde(default)]
2798struct SessionLoadOptions {
2799 include_subagents: Option<bool>,
2800 inline_media: InlineMediaMode,
2801 max_inline_media_bytes: Option<usize>,
2802 message_limit: Option<usize>,
2803 message_offset: Option<usize>,
2804 message_tail: Option<usize>,
2805}
2806
2807impl SessionLoadOptions {
2808 fn validate(&self) -> std::result::Result<(), ServiceError> {
2809 if self.message_tail.is_some()
2810 && (self.message_limit.is_some() || self.message_offset.is_some())
2811 {
2812 return Err(ServiceError::InvalidParams(
2813 "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
2814 .into(),
2815 ));
2816 }
2817 Ok(())
2818 }
2819}
2820
2821#[derive(Deserialize)]
2822struct LoadSessionParams {
2823 #[serde(flatten)]
2824 read: LocatorParams,
2825 #[serde(default)]
2826 options: Option<SessionLoadOptions>,
2827}
2828
2829#[derive(Deserialize)]
2830struct UnfollowParams {
2831 subscription: String,
2832}
2833
2834#[derive(Debug, Deserialize)]
2835#[serde(deny_unknown_fields)]
2836struct IndexResizeParams {
2837 subscription: String,
2838 limit: usize,
2839}
2840
2841#[derive(Deserialize)]
2842struct ActivitySubscribeParams {
2843 locators: Vec<SessionLocator>,
2844 #[serde(default)]
2845 homes: crate::HarnessHomes,
2846}
2847
2848#[derive(Deserialize)]
2849struct MessageSessionParams {
2850 locator: SessionLocator,
2851 text: String,
2852 #[serde(default)]
2855 homes: crate::HarnessHomes,
2856}
2857
2858#[derive(Deserialize)]
2859#[serde(deny_unknown_fields)]
2860struct HarnessSettingsParams {
2861 harness: String,
2862}
2863
2864#[derive(Deserialize)]
2865#[serde(deny_unknown_fields)]
2866struct ConfigureHarnessParams {
2867 harness: String,
2868 #[serde(default)]
2869 changes: Vec<crate::HarnessSettingChange>,
2870 #[serde(default)]
2871 expected_revision: Option<String>,
2872}
2873
2874fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
2875 match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
2876 Ok(report) => (
2877 serde_json::to_value(report).unwrap_or(Value::Null),
2878 Value::Null,
2879 ),
2880 Err(error) => (
2881 Value::Null,
2882 Value::String(format!(
2883 "Supercode could not inspect Claude Code inbound controls: {error}"
2884 )),
2885 ),
2886 }
2887}
2888
2889#[cfg(feature = "adapter-api")]
2901async fn message_live_session(
2902 params: &MessageSessionParams,
2903 runner: &dyn crate::claude_peer::CourierRunner,
2904) -> Value {
2905 if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
2906 return json!({
2907 "delivered_to_bus": false,
2908 "refusal": {
2909 "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
2910 "message": format!(
2911 "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
2912 params.locator.harness.as_str()
2913 ),
2914 },
2915 });
2916 }
2917 let (inbound_controls, inbound_controls_error) =
2918 claude_inbound_controls_or_error(¶ms.homes);
2919 match crate::claude_peer::message_claude_peer(
2920 ¶ms.homes,
2921 ¶ms.locator.session_id,
2922 ¶ms.text,
2923 runner,
2924 )
2925 .await
2926 {
2927 Ok(delivery) => json!({
2928 "delivered_to_bus": true,
2929 "target": {
2930 "session_id": delivery.target.session_id,
2931 "name": delivery.target.name,
2932 "pid": delivery.target.pid,
2933 "cwd": delivery.target.cwd,
2934 "status": delivery.target.status.map(|status| status.as_str()),
2935 },
2936 "courier": {
2937 "model": crate::claude_peer::COURIER_MODEL,
2938 "report": delivery.courier_report,
2939 },
2940 "inbound_controls": inbound_controls,
2941 "inbound_controls_error": inbound_controls_error,
2942 }),
2943 Err(refusal) => json!({
2944 "delivered_to_bus": false,
2945 "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
2946 "inbound_controls": inbound_controls,
2947 "inbound_controls_error": inbound_controls_error,
2948 }),
2949 }
2950}
2951
2952#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
2957struct FollowedSource {
2958 harness: String,
2959 session_id: String,
2960 reported: Option<String>,
2961}
2962
2963#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
2964struct ActivitySubscription {
2965 locators: Vec<SessionLocator>,
2966 homes: crate::HarnessHomes,
2967 reported: BTreeMap<(String, String), crate::SessionActivity>,
2968}
2969
2970fn peers_for_descriptors(
2971 descriptors: &[SessionDescriptor],
2972 homes: &HarnessHomes,
2973) -> Vec<crate::claude_peer::ClaudePeerSession> {
2974 if descriptors
2975 .iter()
2976 .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
2977 {
2978 crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
2979 } else {
2980 Vec::new()
2981 }
2982}
2983
2984fn live_descriptor_value(
2990 session: &SessionDescriptor,
2991 peers: &[crate::claude_peer::ClaudePeerSession],
2992) -> std::result::Result<Value, ServiceError> {
2993 let mut value = serde_json::to_value(session)
2994 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2995 if let Some(workspace) = &session.cwd {
2996 let source = LiveRuntimeSource {
2997 harness: session.locator.harness.as_str().to_string(),
2998 session_id: session.locator.session_id.clone(),
2999 workspace: workspace.clone(),
3000 };
3001 if let Some(endpoint) = discover_live_runtime(&source)
3002 .map_err(|error| ServiceError::Operation(error.to_string()))?
3003 {
3004 value["live_endpoint"] = json!(endpoint.as_str());
3005 }
3006 }
3007 if value.get("live_endpoint").is_none() {
3008 if let Some(peer) = peers.iter().find(|peer| {
3009 session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3010 && peer.session_id == session.locator.session_id
3011 }) {
3012 value["live_endpoint"] = json!(peer.endpoint().as_str());
3013 }
3014 }
3015 Ok(value)
3016}
3017
3018fn live_index_changes(
3019 changes: Vec<crate::session_index::SessionIndexChange>,
3020 homes: &HarnessHomes,
3021) -> std::result::Result<Vec<Value>, ServiceError> {
3022 use crate::session_index::SessionIndexChange;
3023 let has_claude = changes.iter().any(|change| match change {
3024 SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3025 descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3026 }
3027 SessionIndexChange::Removed { .. } => false,
3028 });
3029 let peers = if has_claude {
3030 crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3031 } else {
3032 Vec::new()
3033 };
3034 changes
3035 .into_iter()
3036 .map(|change| match change {
3037 SessionIndexChange::Added { descriptor } => Ok(json!({
3038 "kind": "added",
3039 "descriptor": live_descriptor_value(&descriptor, &peers)?,
3040 })),
3041 SessionIndexChange::Updated { descriptor } => Ok(json!({
3042 "kind": "updated",
3043 "descriptor": live_descriptor_value(&descriptor, &peers)?,
3044 })),
3045 SessionIndexChange::Removed { key } => Ok(json!({
3046 "kind": "removed",
3047 "key": key,
3048 })),
3049 })
3050 .collect()
3051}
3052
3053fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3054 use crate::{SessionPresence, SessionTurnState};
3055 match (activity.presence, activity.turn) {
3056 (SessionPresence::Persisted, _) => None,
3057 (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3058 (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3059 (SessionPresence::Running, SessionTurnState::Unknown)
3063 if activity.evidence.native_state.is_none() =>
3064 {
3065 None
3066 }
3067 (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3068 }
3069}
3070
3071#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3072#[serde(rename_all = "kebab-case")]
3073enum TransferFormat {
3074 ClaudeCode,
3075 Codex,
3076 #[serde(rename = "opencode", alias = "open-code")]
3077 OpenCode,
3078 Pi,
3079 Grok,
3080 Gemini,
3081 Goose,
3082 Hermes,
3086}
3087
3088impl TransferFormat {
3089 fn id(self) -> &'static str {
3090 match self {
3091 Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3092 Self::Codex => HarnessId::CODEX,
3093 Self::OpenCode => HarnessId::OPENCODE,
3094 Self::Pi => HarnessId::PI,
3095 Self::Grok => HarnessId::GROK,
3096 Self::Gemini => HarnessId::GEMINI,
3097 Self::Goose => HarnessId::GOOSE,
3098 Self::Hermes => HarnessId::HERMES,
3099 }
3100 }
3101}
3102
3103impl From<TransferFormat> for SessionFormat {
3104 fn from(value: TransferFormat) -> Self {
3105 match value {
3106 TransferFormat::ClaudeCode => Self::ClaudeCode,
3107 TransferFormat::Codex => Self::Codex,
3108 TransferFormat::OpenCode => Self::OpenCode,
3109 TransferFormat::Pi => Self::Pi,
3110 TransferFormat::Grok => Self::Grok,
3111 TransferFormat::Gemini => Self::Gemini,
3112 TransferFormat::Goose => Self::Goose,
3113 TransferFormat::Hermes => Self::Codex,
3115 }
3116 }
3117}
3118
3119#[derive(Deserialize)]
3120struct ImportSessionParams {
3121 source_harness: TransferFormat,
3122 content: String,
3123}
3124
3125#[derive(Deserialize)]
3126struct ExportSessionParams {
3127 locator: SessionLocator,
3128 target_harness: TransferFormat,
3129}
3130
3131#[derive(Deserialize)]
3132struct ReduceSessionParams {
3133 locator: SessionLocator,
3134 target_harness: TransferFormat,
3135 #[serde(default = "default_keep_last")]
3136 keep_last: usize,
3137}
3138
3139fn default_keep_last() -> usize {
3140 6
3141}
3142
3143#[derive(Deserialize)]
3144struct BranchSessionParams {
3145 locator: SessionLocator,
3146 #[serde(default)]
3147 target_harness: Option<TransferFormat>,
3148}
3149
3150#[derive(Deserialize)]
3151struct HandoffSessionParams {
3152 locator: SessionLocator,
3153 target_harness: TransferFormat,
3154 #[serde(default)]
3155 cwd: Option<PathBuf>,
3156}
3157
3158#[derive(Debug, Clone, Copy, Default, Deserialize)]
3159#[serde(rename_all = "snake_case")]
3160enum ResumePolicy {
3161 #[default]
3162 Default,
3163 Yolo,
3164}
3165
3166#[derive(Deserialize)]
3167struct ResumeInstructionsParams {
3168 locator: SessionLocator,
3169 #[serde(default)]
3170 cwd: Option<PathBuf>,
3171 #[serde(default)]
3172 policy: ResumePolicy,
3173}
3174
3175#[derive(Deserialize)]
3177struct WorkflowLoadParams {
3178 from: crate::workflow_doors::WorkflowHarness,
3179 home: PathBuf,
3180}
3181
3182#[derive(Deserialize)]
3185struct OrchestrationLoadParams {
3186 root: PathBuf,
3187 #[serde(default)]
3188 flavor: crate::orchestration_doors::HomeFlavor,
3189}
3190
3191#[derive(Deserialize)]
3194struct OrchestrationSaveParams {
3195 root: PathBuf,
3196 orchestration: crate::orchestration::Orchestration,
3197 #[serde(default)]
3198 vault: BTreeMap<String, String>,
3199}
3200
3201#[derive(Deserialize)]
3203struct OrchestrationCompileParams {
3204 from: crate::orchestration_doors::OrchestrationHarness,
3205 home: PathBuf,
3206}
3207
3208#[derive(Deserialize)]
3212struct OrchestrationDecompileParams {
3213 to: crate::orchestration_doors::OrchestrationHarness,
3214 orchestration: crate::orchestration::Orchestration,
3215 source: PathBuf,
3216 #[serde(default)]
3217 source_flavor: crate::orchestration_doors::SourceFlavor,
3218 dest: PathBuf,
3219 #[serde(default)]
3220 vault: BTreeMap<String, String>,
3221}
3222
3223#[derive(Deserialize)]
3226struct OrchestrationImportParams {
3227 from: crate::orchestration_doors::OrchestrationHarness,
3228 home: PathBuf,
3229 into: PathBuf,
3230}
3231
3232#[derive(Deserialize)]
3235struct OrchestrationExportParams {
3236 to: crate::orchestration_doors::OrchestrationHarness,
3237 root: PathBuf,
3238 dest: PathBuf,
3239}
3240
3241#[derive(Deserialize)]
3243struct JobsGetParams {
3244 harness: String,
3245 id: String,
3246 #[serde(default)]
3247 homes: crate::HarnessHomes,
3248}
3249
3250fn mutate_job(
3258 verb: crate::jobs_control::JobVerb,
3259 params: Value,
3260) -> std::result::Result<Value, ServiceError> {
3261 let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3262 refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3263 let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3264 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3265}
3266
3267fn mutate_skill(
3274 verb: crate::skills_control::SkillVerb,
3275 params: Value,
3276) -> std::result::Result<Value, ServiceError> {
3277 let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3278 if !crate::skills_control::supports_skill_control(&mutation.harness) {
3279 return Err(ServiceError::UnsupportedAction(format!(
3280 "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3281 mutation.harness,
3282 verb.as_str(),
3283 crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3284 )));
3285 }
3286 let outcome =
3287 crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3288 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3289}
3290
3291fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3293 match error {
3294 crate::skills_control::SkillControlError::Unsupported(message) => {
3295 ServiceError::UnsupportedAction(message)
3296 }
3297 crate::skills_control::SkillControlError::Invalid(message) => {
3298 ServiceError::InvalidParams(message)
3299 }
3300 crate::skills_control::SkillControlError::Failed(message) => {
3301 ServiceError::Operation(message)
3302 }
3303 }
3304}
3305
3306fn mutate_profile(
3314 verb: crate::profiles_control::ProfileVerb,
3315 params: Value,
3316) -> std::result::Result<Value, ServiceError> {
3317 let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3318 let outcome =
3319 crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3320 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3321}
3322
3323fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3325 match error {
3326 crate::profiles_control::ProfileControlError::Unsupported(message) => {
3327 ServiceError::UnsupportedAction(message)
3328 }
3329 crate::profiles_control::ProfileControlError::Invalid(message) => {
3330 ServiceError::InvalidParams(message)
3331 }
3332 crate::profiles_control::ProfileControlError::Failed(message) => {
3333 ServiceError::Operation(message)
3334 }
3335 }
3336}
3337
3338fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3342 match error {
3343 crate::jobs_control::JobControlError::Unsupported(message) => {
3344 ServiceError::UnsupportedAction(message)
3345 }
3346 crate::jobs_control::JobControlError::Invalid(message) => {
3347 ServiceError::InvalidParams(message)
3348 }
3349 crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3350 }
3351}
3352
3353fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3358 match error {
3359 crate::SessionControlError::Unsupported(message) => {
3360 ServiceError::UnsupportedAction(message)
3361 }
3362 crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3363 crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3364 }
3365}
3366
3367fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3372 if crate::jobs::supports_jobs(harness) {
3373 return Ok(());
3374 }
3375 Err(ServiceError::UnsupportedAction(format!(
3376 "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3377 crate::jobs::JOB_HARNESSES.join(", ")
3378 )))
3379}
3380
3381#[derive(Deserialize)]
3383struct RunsGetParams {
3384 harness: String,
3385 id: String,
3386 #[serde(default)]
3387 homes: crate::HarnessHomes,
3388}
3389
3390fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3395 if crate::runs::supports_runs(harness) {
3396 return Ok(());
3397 }
3398 Err(ServiceError::UnsupportedAction(format!(
3399 "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3400 crate::runs::RUN_HARNESSES.join(", ")
3401 )))
3402}
3403
3404#[derive(Serialize)]
3405struct SessionArtifact {
3406 source_harness: HarnessId,
3407 target_harness: &'static str,
3408 session_id: Option<String>,
3409 content: String,
3410 suggested_filename: String,
3411 files: Vec<SessionArtifactFile>,
3412 fidelity: Fidelity,
3413 residue: Vec<String>,
3414}
3415
3416#[derive(Serialize)]
3417struct SessionArtifactFile {
3418 path: String,
3419 content: String,
3420 role: ArtifactFileRole,
3421}
3422
3423#[derive(Serialize)]
3424#[serde(rename_all = "snake_case")]
3425enum ArtifactFileRole {
3426 Primary,
3427 Subagent,
3428 Bundle,
3429 SourceRecovery,
3430}
3431
3432#[derive(Serialize)]
3433struct StructuredLaunch {
3434 cwd: PathBuf,
3435 program: String,
3436 arguments: Vec<String>,
3437 env: BTreeMap<String, String>,
3438}
3439
3440struct HandoffInstructions {
3441 launch: StructuredLaunch,
3442 materialize: Option<StructuredLaunch>,
3443 requires_materialization: bool,
3444 note: String,
3445}
3446
3447#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3448#[serde(rename_all = "snake_case")]
3449enum HarnessProbeLevel {
3450 #[default]
3451 Passive,
3452 Handshake,
3453}
3454
3455#[derive(Default, Deserialize)]
3456#[serde(default)]
3457struct HarnessInventoryParams {
3458 harness: Option<HarnessId>,
3459 harnesses: Vec<HarnessId>,
3460 workspace: Option<PathBuf>,
3461 probe: HarnessProbeLevel,
3462 include_sessions: bool,
3463 skip_versions: bool,
3465}
3466
3467#[derive(Deserialize)]
3468struct HarnessAuthenticationParams {
3469 harness: HarnessId,
3470}
3471
3472#[derive(Deserialize)]
3473struct BeginHarnessAuthenticationParams {
3474 harness: HarnessId,
3475 #[serde(default = "local_browser_authentication_environment")]
3476 environment: crate::HarnessAuthenticationEnvironment,
3477 #[serde(default)]
3478 method: Option<crate::HarnessAuthenticationMethodId>,
3479 #[serde(default)]
3480 cwd: Option<PathBuf>,
3481}
3482
3483fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3484 crate::HarnessAuthenticationEnvironment::LocalBrowser
3485}
3486
3487#[derive(Serialize)]
3488struct HarnessInventoryReport {
3489 probe: HarnessProbeLevel,
3490 workspace: Option<PathBuf>,
3491 harnesses: Vec<LocalHarness>,
3492}
3493
3494#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3495#[serde(rename_all = "snake_case")]
3496enum HarnessAuthState {
3497 Ready,
3498 Configured,
3499 Required,
3500 Unknown,
3501}
3502
3503#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3504#[serde(rename_all = "snake_case")]
3505enum HarnessRuntimeState {
3506 Ready,
3507 Degraded,
3508 Unavailable,
3509}
3510
3511#[derive(Serialize)]
3512struct HarnessSessionCounts {
3513 global: Option<usize>,
3514 workspace: Option<usize>,
3515}
3516
3517#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3528#[serde(rename_all = "snake_case")]
3529pub enum GatewayState {
3530 Up,
3531 Down,
3532 Unknown,
3533}
3534
3535#[derive(Debug, Clone, Serialize)]
3537pub struct GatewayHealth {
3538 pub state: GatewayState,
3539 #[serde(skip_serializing_if = "Option::is_none")]
3544 pub endpoint: Option<String>,
3545 #[serde(skip_serializing_if = "Option::is_none")]
3546 pub version: Option<String>,
3547 pub evidence: String,
3549 pub checked_at_ms: u64,
3550}
3551
3552fn openclaw_gateway_endpoint(home: &Path) -> String {
3556 let config_path = home.join(".openclaw/openclaw.json");
3557 let gateway = std::fs::read_to_string(&config_path)
3558 .ok()
3559 .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3560 .and_then(|config| config.get("gateway").cloned());
3561 if let Some(url) = gateway
3562 .as_ref()
3563 .and_then(|gateway| gateway.get("url"))
3564 .and_then(serde_json::Value::as_str)
3565 {
3566 return url.to_string();
3567 }
3568 let port = gateway
3569 .as_ref()
3570 .and_then(|gateway| gateway.get("port"))
3571 .and_then(serde_json::Value::as_u64)
3572 .unwrap_or(18789);
3573 format!("ws://127.0.0.1:{port}")
3574}
3575
3576fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3583 let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3584 let output = std::process::Command::new(&program)
3585 .args(["gateway", "status"])
3586 .stdin(std::process::Stdio::null())
3587 .output()
3588 .ok()?;
3589 let text = format!(
3590 "{}{}",
3591 String::from_utf8_lossy(&output.stdout),
3592 String::from_utf8_lossy(&output.stderr)
3593 );
3594 let verdict = text.lines().find_map(|line| {
3595 let l = line.trim();
3596 if l.contains("supervised by launchd (PID")
3597 || l.contains("supervised by systemd (PID")
3598 || l.contains("Gateway is running")
3599 || l.contains("process is running")
3600 {
3601 Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3602 } else if l.contains("not running") || l.contains("not installed") {
3603 Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3604 } else {
3605 None
3606 }
3607 });
3608 verdict
3609}
3610
3611fn gateway_health(
3612 id: &str,
3613 installed: bool,
3614 running: Option<&RunningInstance>,
3615 version: Option<&str>,
3616) -> GatewayHealth {
3617 let checked_at_ms = now_epoch_ms();
3618 let home = std::env::var_os("HOME").map(PathBuf::from);
3619 match id {
3620 HarnessId::HERMES | HarnessId::OPENCLAW => {
3621 let endpoint = (id == HarnessId::OPENCLAW)
3622 .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3623 .flatten();
3624 let (state, evidence) = match running {
3625 Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3626 None if !installed => (
3627 GatewayState::Unknown,
3628 format!("`{id}` is not installed; no gateway to probe"),
3629 ),
3630 None if id == HarnessId::HERMES => match hermes_gateway_status() {
3631 Some((state, evidence)) => (state, evidence),
3634 None => (
3635 GatewayState::Down,
3636 "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
3637 ),
3638 },
3639 None => (
3640 GatewayState::Down,
3641 format!(
3642 "no TCP listener at {}",
3643 endpoint.as_deref().unwrap_or("the gateway endpoint")
3644 ),
3645 ),
3646 };
3647 GatewayHealth {
3648 state,
3649 endpoint,
3650 version: version.map(str::to_string),
3651 evidence,
3652 checked_at_ms,
3653 }
3654 }
3655 HarnessId::ORCHESTRATOR => {
3662 let root = crate::HarnessHomes::default().orchestrator;
3663 let (state, evidence) = match crate::orchestrator::read_lease(&root) {
3664 Some(lease) if crate::orchestrator::pid_is_live(lease.pid) => (
3665 GatewayState::Up,
3666 format!(
3667 "`{}` names pid {} (started {}), which is live",
3668 crate::orchestrator::lock_path(&root).display(),
3669 lease.pid,
3670 lease.started_at
3671 ),
3672 ),
3673 Some(lease) => (
3674 GatewayState::Down,
3675 format!(
3676 "stale lease `{}`: pid {} is gone",
3677 crate::orchestrator::lock_path(&root).display(),
3678 lease.pid
3679 ),
3680 ),
3681 None => (
3682 GatewayState::Down,
3683 format!(
3684 "no lease at `{}`; `supercode orchestrator start` writes one",
3685 crate::orchestrator::lock_path(&root).display()
3686 ),
3687 ),
3688 };
3689 GatewayHealth {
3690 state,
3691 endpoint: None,
3692 version: version.map(str::to_string),
3693 evidence,
3694 checked_at_ms,
3695 }
3696 }
3697 _ => GatewayHealth {
3698 state: GatewayState::Unknown,
3699 endpoint: None,
3700 version: version.map(str::to_string),
3701 evidence: format!("`{id}` runs per session, not as a gateway"),
3702 checked_at_ms,
3703 },
3704 }
3705}
3706
3707#[derive(Debug, Clone, Serialize)]
3708struct RunningInstance {
3709 method: RunningInstanceMethod,
3711 evidence: String,
3713 checked_at_ms: u64,
3715}
3716
3717#[derive(Debug, Clone, Copy, Serialize)]
3718#[serde(rename_all = "snake_case")]
3719enum RunningInstanceMethod {
3720 GatewayConnect,
3723 StoreWalActivity,
3726}
3727
3728fn now_epoch_ms() -> u64 {
3729 std::time::SystemTime::now()
3730 .duration_since(std::time::UNIX_EPOCH)
3731 .map(|elapsed| elapsed.as_millis() as u64)
3732 .unwrap_or(0)
3733}
3734
3735fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
3739 let config_path = home.join(".openclaw/openclaw.json");
3740 let text = std::fs::read_to_string(&config_path).ok();
3741 let gateway = text
3742 .as_deref()
3743 .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
3744 .and_then(|config| config.get("gateway").cloned());
3745 let address = gateway
3746 .as_ref()
3747 .and_then(|gateway| gateway.get("url"))
3748 .and_then(serde_json::Value::as_str)
3749 .and_then(|url| {
3750 url.split("://").nth(1).map(|rest| {
3751 rest.trim_end_matches('/')
3752 .split('/')
3753 .next()
3754 .unwrap_or(rest)
3755 .to_string()
3756 })
3757 })
3758 .unwrap_or_else(|| {
3759 let port = gateway
3760 .as_ref()
3761 .and_then(|gateway| gateway.get("port"))
3762 .and_then(serde_json::Value::as_u64)
3763 .unwrap_or(18789);
3764 format!("127.0.0.1:{port}")
3765 });
3766 let reachable = std::net::TcpStream::connect_timeout(
3767 &address.parse().ok()?,
3768 std::time::Duration::from_millis(400),
3769 )
3770 .is_ok();
3771 reachable.then(|| RunningInstance {
3772 method: RunningInstanceMethod::GatewayConnect,
3773 evidence: format!(
3774 "gateway endpoint {address} accepted a TCP connect (from {})",
3775 config_path.display()
3776 ),
3777 checked_at_ms: now_epoch_ms(),
3778 })
3779}
3780
3781fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
3786 let wal = home.join(".hermes/state.db-wal");
3787 let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
3788 let age_ms = std::time::SystemTime::now()
3789 .duration_since(modified)
3790 .map(|age| age.as_millis() as u64)
3791 .unwrap_or(u64::MAX);
3792 (age_ms <= max_wal_age_ms).then(|| RunningInstance {
3793 method: RunningInstanceMethod::StoreWalActivity,
3794 evidence: format!(
3795 "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
3796 wal.display()
3797 ),
3798 checked_at_ms: now_epoch_ms(),
3799 })
3800}
3801
3802fn probe_running_instance(id: &str) -> Option<RunningInstance> {
3804 let home = std::env::var_os("HOME").map(PathBuf::from)?;
3805 match id {
3806 HarnessId::OPENCLAW => probe_openclaw_running(&home),
3807 HarnessId::HERMES => probe_hermes_running(&home, 300_000),
3808 _ => None,
3809 }
3810}
3811
3812#[derive(Serialize)]
3813struct LocalHarness {
3814 id: HarnessId,
3815 display_name: String,
3816 supported: bool,
3817 installed: bool,
3818 executable: Option<String>,
3819 version: Option<String>,
3820 auth: HarnessAuthState,
3821 runtime: HarnessRuntimeState,
3822 protocol: String,
3823 capabilities: crate::RuntimeCapabilities,
3824 effective_capabilities: crate::RuntimeCapabilities,
3825 sessions: HarnessSessionCounts,
3826 #[serde(skip_serializing_if = "Option::is_none")]
3829 running: Option<RunningInstance>,
3830 gateway: GatewayHealth,
3832 reason: Option<String>,
3833 repair: Option<String>,
3834}
3835
3836#[derive(Clone, Deserialize)]
3837struct RuntimeBackendParams {
3838 harness: HarnessId,
3839 #[serde(default)]
3840 protocol: Option<String>,
3841 #[serde(default)]
3842 launch: Option<RuntimeLaunch>,
3843 #[serde(default)]
3844 base_url: Option<String>,
3845 #[serde(default)]
3846 policy: RuntimePolicy,
3847}
3848
3849#[derive(Debug, Clone, Copy, Default, Deserialize)]
3850#[serde(rename_all = "snake_case")]
3851enum RuntimePolicy {
3852 #[default]
3853 Default,
3854 Yolo,
3855}
3856
3857#[derive(Deserialize)]
3858struct RuntimeStartParams {
3859 #[serde(flatten)]
3860 backend: RuntimeBackendParams,
3861 cwd: PathBuf,
3862 #[serde(default)]
3865 mcp_servers: Vec<crate::McpServerLaunch>,
3866}
3867
3868#[derive(Deserialize)]
3869struct RuntimeAttachParams {
3870 #[serde(flatten)]
3871 backend: RuntimeBackendParams,
3872 runtime_id: String,
3873 #[serde(default)]
3874 cwd: Option<PathBuf>,
3875}
3876
3877#[derive(Deserialize)]
3878struct RuntimeConnectionParams {
3879 connection: String,
3880}
3881
3882#[derive(Deserialize)]
3883struct RuntimeInputParams {
3884 connection: String,
3885 text: String,
3886 #[serde(default)]
3887 image_urls: Vec<String>,
3888}
3889
3890const MAX_RUNTIME_IMAGES: usize = 4;
3891const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
3892const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
3893
3894fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
3895 if image_urls.len() > MAX_RUNTIME_IMAGES {
3896 return Err(ServiceError::InvalidParams(format!(
3897 "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
3898 )));
3899 }
3900 let mut total = 0usize;
3901 for url in &image_urls {
3902 if !(url.starts_with("data:image/")
3903 || url.starts_with("https://")
3904 || url.starts_with("http://"))
3905 {
3906 return Err(ServiceError::InvalidParams(
3907 "runtime images must be image data URLs or HTTP(S) URLs".into(),
3908 ));
3909 }
3910 if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
3911 return Err(ServiceError::InvalidParams(format!(
3912 "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
3913 )));
3914 }
3915 total = total.saturating_add(url.len());
3916 }
3917 if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
3918 return Err(ServiceError::InvalidParams(format!(
3919 "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
3920 )));
3921 }
3922 Ok(image_urls)
3923}
3924
3925#[derive(Deserialize)]
3926struct RuntimeRespondParams {
3927 connection: String,
3928 request_id: Value,
3929 response: Value,
3930}
3931
3932fn default_reduction_store_root() -> PathBuf {
3933 if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
3934 return PathBuf::from(root).join("sessions");
3935 }
3936 if let Some(home) = std::env::var_os("HOME") {
3937 return PathBuf::from(home).join(".supercode").join("sessions");
3938 }
3939 PathBuf::from(".supercode").join("sessions")
3940}
3941
3942fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
3943 let mut output = String::new();
3944 for message in messages {
3945 output.push_str(
3946 &serde_json::to_string(message)
3947 .map_err(|error| ServiceError::Operation(error.to_string()))?,
3948 );
3949 output.push('\n');
3950 }
3951 Ok(output)
3952}
3953
3954fn parse_messages_jsonl(
3955 content: &str,
3956) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
3957 content
3958 .lines()
3959 .enumerate()
3960 .filter(|(_, line)| !line.trim().is_empty())
3961 .map(|(index, line)| {
3962 serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
3963 ServiceError::Operation(format!(
3964 "reduced transcript line {} is invalid: {error}",
3965 index + 1
3966 ))
3967 })
3968 })
3969 .collect()
3970}
3971
3972fn reduced_bootstrap_prompt(
3973 source: &SessionLocator,
3974 target: TransferFormat,
3975 view_jsonl: &str,
3976 sidecar_path: &Path,
3977 reduction_log_path: &Path,
3978) -> String {
3979 format!(
3980 "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
3981 \n\
3982 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\
3983 \n\
3984 <supercode-reduced-session source-session=\"{source_id}\">\n\
3985 {view_jsonl}\
3986 </supercode-reduced-session>\n\
3987 \n\
3988 Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
3989 source_harness = source.harness.as_str(),
3990 target_harness = target.id(),
3991 sidecar = sidecar_path.display(),
3992 log = reduction_log_path.display(),
3993 source_id = source.session_id,
3994 )
3995}
3996
3997fn session_artifact(
3998 locator: &SessionLocator,
3999 session: &Session,
4000 target: TransferFormat,
4001) -> std::result::Result<SessionArtifact, ServiceError> {
4002 session_artifact_with_id(locator, session, target, None)
4003}
4004
4005fn session_artifact_with_id(
4006 locator: &SessionLocator,
4007 session: &Session,
4008 target: TransferFormat,
4009 target_session_id: Option<&str>,
4010) -> std::result::Result<SessionArtifact, ServiceError> {
4011 let format: SessionFormat = target.into();
4012 let diagonal = format.source() == session.meta.source;
4013 let has_appended_turns = session
4014 .imported_message_count
4015 .is_some_and(|imported| imported < session.messages.len());
4016 let content = if let Some(id) = target_session_id {
4017 if diagonal && format != SessionFormat::OpenCode {
4018 session
4019 .to_jsonl_spliced(format, Some(id))
4020 .map_err(operation)?
4021 } else {
4022 let mut rewritten = session.clone();
4023 rewritten.meta.session_id = Some(id.to_string());
4024 rewritten.to_jsonl(format).map_err(operation)?
4025 }
4026 } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4027 session.raw_verbatim()
4028 } else if diagonal {
4029 session.to_jsonl_spliced(format, None).map_err(operation)?
4030 } else {
4031 session.to_jsonl(format).map_err(operation)?
4032 };
4033 let stem = sanitize_filename(
4034 target_session_id
4035 .or(session.meta.session_id.as_deref())
4036 .unwrap_or(&locator.session_id),
4037 );
4038 let suggested_filename = if diagonal && target == TransferFormat::Grok {
4039 "chat_history.jsonl".to_string()
4040 } else if target == TransferFormat::Goose {
4041 format!("{stem}.goose.json")
4042 } else {
4043 format!("{stem}.{}.jsonl", target.id())
4044 };
4045 let mut files = vec![SessionArtifactFile {
4046 path: suggested_filename.clone(),
4047 content: content.clone(),
4048 role: ArtifactFileRole::Primary,
4049 }];
4050 if target == TransferFormat::ClaudeCode {
4051 let bundle_stem = Path::new(&suggested_filename)
4052 .file_stem()
4053 .and_then(|stem| stem.to_str())
4054 .unwrap_or(&stem);
4055 let mut child_paths = BTreeSet::new();
4056 for (index, subagent) in session.subagents.iter().enumerate() {
4057 let agent_id = subagent
4058 .meta
4059 .agent_id
4060 .as_deref()
4061 .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4062 .map(sanitize_filename)
4063 .filter(|id| !id.is_empty())
4064 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4065 let child_has_appended_turns = subagent
4066 .imported_message_count
4067 .is_some_and(|imported| imported < subagent.messages.len());
4068 let child_content = if target_session_id.is_none()
4069 && subagent.meta.source == SessionSource::ClaudeCode
4070 && subagent.raw_is_verbatim
4071 && !child_has_appended_turns
4072 {
4073 subagent.raw_verbatim()
4074 } else if subagent.meta.source == SessionSource::ClaudeCode {
4075 subagent
4076 .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4077 .map_err(operation)?
4078 } else {
4079 let mut child = subagent.clone();
4080 if let Some(id) = target_session_id {
4081 child.meta.session_id = Some(id.to_string());
4082 }
4083 child
4084 .to_jsonl(SessionFormat::ClaudeCode)
4085 .map_err(operation)?
4086 };
4087 let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4088 if !child_paths.insert(path.clone()) {
4089 return Err(ServiceError::Operation(format!(
4090 "Claude subagent ids collide at artifact path `{path}`"
4091 )));
4092 }
4093 files.push(SessionArtifactFile {
4094 path,
4095 content: child_content,
4096 role: ArtifactFileRole::Subagent,
4097 });
4098 }
4099 }
4100 if diagonal && target == TransferFormat::Grok {
4101 append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4102 }
4103 if !diagonal || !session.raw_is_verbatim {
4104 files.push(SessionArtifactFile {
4105 path: "recovery/source.supercode.jsonl".into(),
4106 content: session.to_native_jsonl(),
4107 role: ArtifactFileRole::SourceRecovery,
4108 });
4109 for (index, subagent) in session.subagents.iter().enumerate() {
4110 let id = subagent
4111 .meta
4112 .agent_id
4113 .as_deref()
4114 .map(sanitize_filename)
4115 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4116 files.push(SessionArtifactFile {
4117 path: format!("recovery/subagents/{id}.supercode.jsonl"),
4118 content: subagent.to_native_jsonl(),
4119 role: ArtifactFileRole::SourceRecovery,
4120 });
4121 }
4122 }
4123 if !diagonal && session.meta.source == SessionSource::Grok {
4124 append_grok_bundle_files(
4125 locator,
4126 "recovery/grok/",
4127 ArtifactFileRole::SourceRecovery,
4128 &mut files,
4129 )?;
4130 }
4131 let (fidelity, residue) = if diagonal
4132 && target_session_id.is_none()
4133 && session.raw_is_verbatim
4134 && !has_appended_turns
4135 {
4136 (Fidelity::ByteLossless, Vec::new())
4137 } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4138 (
4139 Fidelity::ValueLossless,
4140 vec![if target_session_id.is_some() {
4141 "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4142 } else {
4143 "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4144 }],
4145 )
4146 } else {
4147 (
4148 Fidelity::Semantic,
4149 vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4150 )
4151 };
4152 Ok(SessionArtifact {
4153 source_harness: locator.harness.clone(),
4154 target_harness: target.id(),
4155 session_id: target_session_id
4156 .map(str::to_string)
4157 .or_else(|| session.meta.session_id.clone()),
4158 content,
4159 suggested_filename,
4160 files,
4161 fidelity,
4162 residue,
4163 })
4164}
4165
4166fn append_grok_bundle_files(
4167 locator: &SessionLocator,
4168 prefix: &str,
4169 role: ArtifactFileRole,
4170 files: &mut Vec<SessionArtifactFile>,
4171) -> std::result::Result<(), ServiceError> {
4172 let primary = locator.storage.path();
4173 if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4174 return Err(ServiceError::Operation(format!(
4175 "Grok bundle locator must name chat_history.jsonl, got {}",
4176 primary.display()
4177 )));
4178 }
4179 let parent = primary.parent().ok_or_else(|| {
4180 ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4181 })?;
4182 for name in ["summary.json", "updates.jsonl"] {
4183 let path = parent.join(name);
4184 let metadata = match std::fs::symlink_metadata(&path) {
4185 Ok(metadata) => metadata,
4186 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4187 Err(error) => return Err(ServiceError::Operation(error.to_string())),
4188 };
4189 if metadata.file_type().is_symlink() || !metadata.is_file() {
4190 return Err(ServiceError::Operation(format!(
4191 "refusing non-regular Grok bundle member {}",
4192 path.display()
4193 )));
4194 }
4195 let content = std::fs::read_to_string(&path).map_err(|error| {
4196 ServiceError::Operation(format!(
4197 "Grok bundle member {} is not representable as UTF-8: {error}",
4198 path.display()
4199 ))
4200 })?;
4201 files.push(SessionArtifactFile {
4202 path: format!("{prefix}{name}"),
4203 content,
4204 role: match role {
4205 ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4206 _ => ArtifactFileRole::SourceRecovery,
4207 },
4208 });
4209 }
4210 Ok(())
4211}
4212
4213fn handoff_artifact(
4214 locator: &SessionLocator,
4215 session: &Session,
4216 target: TransferFormat,
4217 cwd: &Path,
4218) -> std::result::Result<SessionArtifact, ServiceError> {
4219 if target != TransferFormat::Grok {
4220 let target_session_id = target_session_id(target);
4221 return session_artifact_with_id(locator, session, target, Some(&target_session_id));
4222 }
4223
4224 let mut importable = session.clone();
4228 importable.meta.session_id = Some(target_session_id(TransferFormat::ClaudeCode));
4234 importable.meta.cwd = Some(if cwd.is_absolute() {
4235 cwd.to_path_buf()
4236 } else {
4237 std::env::current_dir()
4238 .map_err(|error| ServiceError::Operation(error.to_string()))?
4239 .join(cwd)
4240 });
4241 let content = importable
4242 .to_jsonl(SessionFormat::ClaudeCode)
4243 .map_err(operation)?;
4244 let stem = sanitize_filename(
4245 importable
4246 .meta
4247 .session_id
4248 .as_deref()
4249 .unwrap_or(&locator.session_id),
4250 );
4251 let suggested_filename = format!("{stem}.grok-import.claude-code.jsonl");
4252 Ok(SessionArtifact {
4253 source_harness: locator.harness.clone(),
4254 target_harness: TransferFormat::ClaudeCode.id(),
4257 session_id: importable.meta.session_id.clone(),
4258 content: content.clone(),
4259 suggested_filename: suggested_filename.clone(),
4260 files: vec![SessionArtifactFile {
4261 path: suggested_filename,
4262 content,
4263 role: ArtifactFileRole::Primary,
4264 }],
4265 fidelity: Fidelity::Semantic,
4266 residue: vec!["Grok's stock importer accepts a Claude Code transcript, not a complete Grok updates/session bundle".into()],
4267 })
4268}
4269
4270fn target_session_id(target: TransferFormat) -> String {
4271 let uuid = generated_session_id();
4272 match target {
4273 TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4274 TransferFormat::ClaudeCode
4275 | TransferFormat::Codex
4276 | TransferFormat::Pi
4277 | TransferFormat::Grok
4278 | TransferFormat::Gemini
4279 | TransferFormat::Goose
4280 | TransferFormat::Hermes => uuid,
4281 }
4282}
4283
4284fn sanitize_filename(value: &str) -> String {
4285 let value = value
4286 .chars()
4287 .map(|character| {
4288 if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4289 character
4290 } else {
4291 '-'
4292 }
4293 })
4294 .collect::<String>();
4295 let value = value.trim_matches('-');
4296 if value.is_empty() {
4297 "session".into()
4298 } else {
4299 value.chars().take(100).collect()
4300 }
4301}
4302
4303fn handoff_instructions(
4304 target: TransferFormat,
4305 session_id: &str,
4306 cwd: &Path,
4307) -> HandoffInstructions {
4308 let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4309 cwd: cwd.to_path_buf(),
4310 program: program.into(),
4311 arguments,
4312 env: BTreeMap::new(),
4313 };
4314 match target {
4315 TransferFormat::ClaudeCode => HandoffInstructions {
4316 launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4317 materialize: None,
4318 requires_materialization: true,
4319 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(),
4320 },
4321 TransferFormat::Hermes => HandoffInstructions {
4322 launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4323 materialize: None,
4324 requires_materialization: true,
4325 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(),
4326 },
4327 TransferFormat::Codex => HandoffInstructions {
4328 launch: launch("codex", vec!["resume".into(), session_id.into()]),
4329 materialize: None,
4330 requires_materialization: true,
4331 note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4332 },
4333 TransferFormat::OpenCode => HandoffInstructions {
4334 launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4335 materialize: Some(launch(
4336 "opencode",
4337 vec!["import".into(), "{artifact_path}".into()],
4338 )),
4339 requires_materialization: true,
4340 note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4341 },
4342 TransferFormat::Pi => HandoffInstructions {
4343 launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4344 materialize: None,
4345 requires_materialization: true,
4346 note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4347 },
4348 TransferFormat::Grok => HandoffInstructions {
4349 launch: launch(
4350 "grok",
4351 vec![
4352 "--resume".into(),
4353 "{imported_session_id}".into(),
4354 "--fork-session".into(),
4355 ],
4356 ),
4357 materialize: Some(launch(
4358 "grok",
4359 vec!["import".into(), "--json".into(), "{artifact_path}".into()],
4360 )),
4361 requires_materialization: true,
4362 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(),
4363 },
4364 TransferFormat::Gemini => HandoffInstructions {
4365 launch: launch(
4366 "gemini",
4367 vec!["--session-file".into(), "{artifact_path}".into()],
4368 ),
4369 materialize: None,
4370 requires_materialization: true,
4371 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(),
4372 },
4373 TransferFormat::Goose => HandoffInstructions {
4374 launch: launch(
4375 "goose",
4376 vec![
4377 "session".into(),
4378 "--resume".into(),
4379 "--session-id".into(),
4380 "{imported_session_id}".into(),
4381 ],
4382 ),
4383 materialize: Some(launch(
4384 "goose",
4385 vec!["session".into(), "import".into(), "{artifact_path}".into()],
4386 )),
4387 requires_materialization: true,
4388 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(),
4389 },
4390 }
4391}
4392
4393fn resume_launch(
4394 harness: &str,
4395 session_id: &str,
4396 cwd: &Path,
4397 policy: ResumePolicy,
4398) -> std::result::Result<StructuredLaunch, ServiceError> {
4399 let mut arguments = Vec::new();
4400 let program = match harness {
4401 HarnessId::GROK => {
4402 if matches!(policy, ResumePolicy::Yolo) {
4403 if crate::support::self_sandbox_supported() {
4404 arguments.extend(["--sandbox".into(), "workspace".into()]);
4405 }
4406 arguments.push("--always-approve".into());
4407 }
4408 arguments.extend(["--resume".into(), session_id.into()]);
4409 "grok"
4410 }
4411 HarnessId::CODEX => {
4412 let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4413 .expect("a filesystem path always serializes as JSON text");
4414 arguments.extend([
4415 "-c".into(),
4416 "check_for_update_on_startup=false".into(),
4417 "-c".into(),
4418 format!("projects.{cwd_key}.trust_level=\"trusted\""),
4419 ]);
4420 if matches!(policy, ResumePolicy::Yolo) {
4421 arguments.extend([
4422 "--dangerously-bypass-approvals-and-sandbox".into(),
4423 "--dangerously-bypass-hook-trust".into(),
4424 ]);
4425 }
4426 arguments.extend(["resume".into(), session_id.into()]);
4427 "codex"
4428 }
4429 HarnessId::CLAUDE_CODE => {
4430 if matches!(policy, ResumePolicy::Yolo) {
4431 arguments.push("--dangerously-skip-permissions".into());
4432 }
4433 arguments.extend(["--resume".into(), session_id.into()]);
4434 "claude"
4435 }
4436 HarnessId::GEMINI => {
4437 if matches!(policy, ResumePolicy::Yolo) {
4438 arguments.push("--yolo".into());
4439 }
4440 arguments.extend(["--resume".into(), session_id.into()]);
4441 "gemini"
4442 }
4443 HarnessId::GOOSE => {
4444 arguments.extend([
4445 "session".into(),
4446 "--resume".into(),
4447 "--session-id".into(),
4448 session_id.into(),
4449 ]);
4450 "goose"
4451 }
4452 HarnessId::PI => {
4453 if matches!(policy, ResumePolicy::Yolo) {
4454 arguments.push("--approve".into());
4455 }
4456 arguments.extend(["--session".into(), session_id.into()]);
4457 "pi"
4458 }
4459 HarnessId::OPENCODE => {
4460 arguments.extend(["--session".into(), session_id.into()]);
4461 "opencode"
4462 }
4463 HarnessId::SUPERCODE => {
4464 if matches!(policy, ResumePolicy::Yolo) {
4465 arguments.push("--dangerous".into());
4466 }
4467 arguments.extend(["resume".into(), session_id.into()]);
4468 "supercode"
4469 }
4470 other => {
4471 return Err(ServiceError::InvalidParams(format!(
4472 "no structured resume launch is registered for harness `{other}`"
4473 )))
4474 }
4475 };
4476 Ok(StructuredLaunch {
4477 cwd: cwd.to_path_buf(),
4478 program: program.into(),
4479 arguments,
4480 env: BTreeMap::new(),
4481 })
4482}
4483
4484fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4490 let digest = blake3::hash(address.as_bytes()).to_hex();
4491 let path = std::env::temp_dir().join(format!(
4492 "supercode-openclaw-gateway-token-{}",
4493 &digest.as_str()[..16]
4494 ));
4495 #[cfg(unix)]
4496 {
4497 use std::io::Write;
4498 use std::os::unix::fs::OpenOptionsExt;
4499 let mut file = std::fs::OpenOptions::new()
4500 .write(true)
4501 .create(true)
4502 .truncate(true)
4503 .mode(0o600)
4504 .open(&path)?;
4505 file.write_all(secret.as_bytes())?;
4506 }
4507 #[cfg(not(unix))]
4508 std::fs::write(&path, secret)?;
4509 Ok(path)
4510}
4511
4512fn open_connect_descriptor(
4518 descriptor: &crate::HarnessSupportDescriptor,
4519 home: &Path,
4520) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4521 let Some(connect) = &descriptor.runtime.connect_launch else {
4522 return Err(ServiceError::InvalidParams(format!(
4523 "harness `{}` has no registered connect-mode launch",
4524 descriptor.id.as_str()
4525 )));
4526 };
4527 let resolved = connect
4528 .resolve(home)
4529 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4530 match (descriptor.id.as_str(), connect.protocol.as_str()) {
4531 (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4532 let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4533 if let Some(token) = resolved.auth {
4534 backend = backend.with_bearer(token);
4535 }
4536 Ok(Box::new(backend))
4537 }
4538 (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4539 let mut env = BTreeMap::new();
4550 let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4551 if let Some(token) = resolved.auth {
4552 let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4553 .map_err(|error| {
4554 ServiceError::UnsupportedAction(format!(
4555 "could not stage the gateway credential for the bridge: {error}"
4556 ))
4557 })?;
4558 arguments.push("--token-file".into());
4559 arguments.push(token_path.to_string_lossy().into_owned());
4560 env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4561 }
4562 let program = descriptor
4567 .runtime
4568 .default_launch
4569 .as_ref()
4570 .map(|launch| launch.program.clone())
4571 .unwrap_or_else(|| "openclaw".into());
4572 let launch = RuntimeLaunch {
4573 program,
4574 arguments,
4575 env,
4576 };
4577 Ok(Box::new(
4578 crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4579 .with_resume_support(descriptor.runtime.capabilities.resume_session),
4580 ))
4581 }
4582 _ => Err(ServiceError::UnsupportedAction(format!(
4583 "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4584 descriptor.id.as_str(),
4585 connect.protocol
4586 ))),
4587 }
4588}
4589
4590fn registry_connect_descriptor(
4593 params: &RuntimeBackendParams,
4594) -> Option<crate::HarnessSupportDescriptor> {
4595 if params.launch.is_some() || params.base_url.is_some() {
4596 return None;
4597 }
4598 harness_support_registry()
4599 .harnesses
4600 .into_iter()
4601 .find(|descriptor| descriptor.id == params.harness)
4602 .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4603}
4604
4605fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4606 std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4607 ServiceError::UnsupportedAction(
4608 "connect-mode launches need HOME to locate the harness config".into(),
4609 )
4610 })
4611}
4612
4613fn runtime_backend(
4614 params: &RuntimeBackendParams,
4615) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4616 if let Some(descriptor) = registry_connect_descriptor(params) {
4617 return open_connect_descriptor(&descriptor, &service_home()?);
4618 }
4619 if params.protocol.as_deref() == Some("acp") {
4620 let launch = params
4621 .launch
4622 .clone()
4623 .or_else(|| {
4624 harness_support_registry()
4625 .harnesses
4626 .into_iter()
4627 .find(|harness| harness.id == params.harness)
4628 .filter(|harness| {
4629 harness.runtime.implementation == ImplementationKind::GenericProtocol
4630 && harness.runtime.protocol.starts_with("acp")
4631 })
4632 .and_then(|harness| harness.runtime.default_launch)
4633 })
4634 .ok_or_else(|| {
4635 ServiceError::InvalidParams(
4636 "an ACP runtime requires `launch` unless the harness has a registered default"
4637 .into(),
4638 )
4639 })?;
4640 let resume_session = harness_support_registry()
4641 .harnesses
4642 .into_iter()
4643 .find(|harness| harness.id == params.harness)
4644 .is_some_and(|harness| harness.runtime.capabilities.resume_session);
4645 return Ok(Box::new(
4646 AcpRuntimeBackend::new(params.harness.clone(), launch)
4647 .with_resume_support(resume_session),
4648 ));
4649 }
4650 let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
4651 HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
4652 HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
4653 HarnessId::PI => Box::new(PiRuntimeBackend::new()),
4654 HarnessId::OPENCODE => match ¶ms.base_url {
4655 Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
4656 None => Box::new(OpenCodeRuntimeBackend::new()),
4657 },
4658 harness => {
4659 let descriptor = harness_support_registry()
4660 .harnesses
4661 .into_iter()
4662 .find(|descriptor| descriptor.id.as_str() == harness)
4663 .filter(|descriptor| {
4664 descriptor.runtime.implementation == ImplementationKind::GenericProtocol
4665 && descriptor.runtime.protocol.starts_with("acp")
4666 });
4667 let Some(descriptor) = descriptor else {
4668 return Err(ServiceError::InvalidParams(format!(
4669 "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
4670 )));
4671 };
4672 let resume = descriptor.runtime.capabilities.resume_session;
4673 Box::new(
4674 AcpRuntimeBackend::new(
4675 descriptor.id,
4676 descriptor
4677 .runtime
4678 .default_launch
4679 .expect("generic ACP registry entry includes its launch"),
4680 )
4681 .with_resume_support(resume),
4682 )
4683 }
4684 };
4685 Ok(backend)
4686}
4687
4688fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
4689 if let Some(launch) = ¶ms.launch {
4690 return Some(launch.clone());
4691 }
4692 if !matches!(params.policy, RuntimePolicy::Yolo) {
4693 return None;
4694 }
4695 let launch = match params.harness.as_str() {
4696 HarnessId::GROK => RuntimeLaunch {
4697 program: "grok".into(),
4698 arguments: {
4699 let mut arguments: Vec<String> = Vec::new();
4700 if crate::support::self_sandbox_supported() {
4701 arguments.extend(["--sandbox".into(), "workspace".into()]);
4702 }
4703 arguments.extend([
4704 "--always-approve".into(),
4705 "agent".into(),
4706 "--no-leader".into(),
4707 "stdio".into(),
4708 ]);
4709 arguments
4710 },
4711 env: BTreeMap::from([("GROK_AGENT_DASHBOARD".into(), "0".into())]),
4712 },
4713 HarnessId::CODEX => RuntimeLaunch {
4714 program: "codex".into(),
4715 arguments: vec![
4716 "--dangerously-bypass-approvals-and-sandbox".into(),
4717 "--dangerously-bypass-hook-trust".into(),
4718 "app-server".into(),
4719 ],
4720 env: BTreeMap::new(),
4721 },
4722 HarnessId::CLAUDE_CODE => RuntimeLaunch {
4723 program: "claude".into(),
4724 arguments: vec![
4725 "--dangerously-skip-permissions".into(),
4726 "--print".into(),
4727 "--input-format".into(),
4728 "stream-json".into(),
4729 "--output-format".into(),
4730 "stream-json".into(),
4731 "--verbose".into(),
4732 ],
4733 env: BTreeMap::new(),
4734 },
4735 HarnessId::PI => RuntimeLaunch {
4736 program: "pi".into(),
4737 arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
4738 env: BTreeMap::new(),
4739 },
4740 HarnessId::OPENCODE => RuntimeLaunch {
4741 program: "opencode".into(),
4742 arguments: vec!["serve".into()],
4743 env: BTreeMap::new(),
4744 },
4745 HarnessId::GEMINI => RuntimeLaunch {
4746 program: "gemini".into(),
4747 arguments: vec!["--acp".into(), "--yolo".into()],
4748 env: BTreeMap::new(),
4749 },
4750 HarnessId::GOOSE => RuntimeLaunch {
4751 program: "goose".into(),
4752 arguments: vec!["acp".into()],
4753 env: BTreeMap::new(),
4754 },
4755 HarnessId::SUPERCODE => RuntimeLaunch {
4756 program: "supercode".into(),
4757 arguments: vec!["acp".into(), "--dangerous".into()],
4758 env: BTreeMap::new(),
4759 },
4760 _ => return None,
4761 };
4762 Some(launch)
4763}
4764
4765struct IsolatedProbeHome {
4771 launch: RuntimeLaunch,
4772 root: PathBuf,
4773}
4774
4775impl IsolatedProbeHome {
4776 fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
4777 let root = std::env::temp_dir().join(format!(
4778 "supercode-harness-probe-{harness}-{}",
4779 generated_session_id()
4780 ));
4781 std::fs::create_dir_all(&root)?;
4782 set_private_dir_permissions(&root)?;
4783
4784 if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
4785 for relative in probe_auth_files(harness) {
4786 copy_probe_file(&source_home, &root, relative)?;
4787 }
4788 }
4789 configure_isolated_probe_auth(harness, &root)?;
4790
4791 let root_text = root.to_string_lossy().into_owned();
4792 for (key, value) in [
4793 ("HOME", root_text.clone()),
4794 (
4795 "XDG_CACHE_HOME",
4796 root.join(".cache").to_string_lossy().into_owned(),
4797 ),
4798 (
4799 "XDG_CONFIG_HOME",
4800 root.join(".config").to_string_lossy().into_owned(),
4801 ),
4802 (
4803 "XDG_DATA_HOME",
4804 root.join(".local/share").to_string_lossy().into_owned(),
4805 ),
4806 ] {
4807 launch.env.insert(key.into(), value);
4808 }
4809 let scoped = match harness {
4810 HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
4811 HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
4812 HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
4813 HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
4814 HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
4815 HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
4816 _ => None,
4817 };
4818 if let Some((key, value)) = scoped {
4819 launch
4820 .env
4821 .insert(key.into(), value.to_string_lossy().into_owned());
4822 }
4823 Ok(Self { launch, root })
4824 }
4825
4826 fn cleanup(&self) -> std::io::Result<()> {
4827 match std::fs::remove_dir_all(&self.root) {
4828 Ok(()) => Ok(()),
4829 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
4830 Err(error) => Err(error),
4831 }
4832 }
4833}
4834
4835impl Drop for IsolatedProbeHome {
4836 fn drop(&mut self) {
4837 let _ = self.cleanup();
4838 }
4839}
4840
4841fn probe_auth_files(harness: &str) -> &'static [&'static str] {
4842 match harness {
4843 HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
4844 HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
4848 HarnessId::CODEX => &[".codex/auth.json"],
4849 HarnessId::GEMINI => &[
4850 ".gemini/google_accounts.json",
4851 ".gemini/oauth_creds.json",
4852 ".gemini/settings.json",
4853 ],
4854 HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
4855 HarnessId::OPENCODE => &[
4856 ".config/opencode/auth.json",
4857 ".local/share/opencode/auth.json",
4858 ],
4859 HarnessId::PI => &[".pi/agent/auth.json"],
4860 HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
4865 HarnessId::SUPERCODE => &[
4866 ".config/supercode/config.toml",
4867 ".config/supercode/credentials.toml",
4868 ],
4869 _ => &[],
4870 }
4871}
4872
4873fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
4874 let source = source_home.join(relative);
4875 if !source.is_file() {
4876 return Ok(());
4877 }
4878 let destination = probe_home.join(relative);
4879 if let Some(parent) = destination.parent() {
4880 std::fs::create_dir_all(parent)?;
4881 set_private_dir_permissions(parent)?;
4882 }
4883 std::fs::copy(source, &destination)?;
4884 set_private_file_permissions(&destination)
4885}
4886
4887fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
4888 if harness != HarnessId::GEMINI {
4889 return Ok(());
4890 }
4891 let oauth = probe_home.join(".gemini/oauth_creds.json");
4892 if !oauth.is_file() {
4893 return Ok(());
4894 }
4895 let settings_path = probe_home.join(".gemini/settings.json");
4896 let mut settings = std::fs::read_to_string(&settings_path)
4897 .ok()
4898 .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
4899 .unwrap_or_else(|| json!({}));
4900 settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
4901 std::fs::write(
4902 &settings_path,
4903 serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
4904 )?;
4905 set_private_file_permissions(&settings_path)
4906}
4907
4908#[cfg(unix)]
4909fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
4910 use std::os::unix::fs::PermissionsExt;
4911 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
4912}
4913
4914#[cfg(not(unix))]
4915fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
4916 Ok(())
4917}
4918
4919#[cfg(unix)]
4920fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
4921 use std::os::unix::fs::PermissionsExt;
4922 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
4923}
4924
4925#[cfg(not(unix))]
4926fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
4927 Ok(())
4928}
4929
4930fn find_executable(program: &str) -> Option<PathBuf> {
4931 let candidate = PathBuf::from(program);
4932 if candidate.components().count() > 1 {
4933 return candidate.is_file().then_some(candidate);
4934 }
4935 let path = std::env::var_os("PATH")?;
4936 for directory in std::env::split_paths(&path) {
4937 let candidate = directory.join(program);
4938 if candidate.is_file() {
4939 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
4940 }
4941 #[cfg(windows)]
4942 {
4943 for extension in ["exe", "cmd", "bat"] {
4944 let candidate = directory.join(format!("{program}.{extension}"));
4945 if candidate.is_file() {
4946 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
4947 }
4948 }
4949 }
4950 }
4951 None
4952}
4953
4954async fn executable_version(executable: &Path) -> Option<String> {
4955 let mut command = tokio::process::Command::new(executable);
4956 command
4957 .arg("--version")
4958 .stdin(std::process::Stdio::null())
4959 .stdout(std::process::Stdio::piped())
4960 .stderr(std::process::Stdio::piped())
4961 .kill_on_drop(true);
4962 let output = tokio::time::timeout(Duration::from_secs(3), command.output())
4963 .await
4964 .ok()?
4965 .ok()?;
4966 let stdout = String::from_utf8_lossy(&output.stdout);
4967 let stderr = String::from_utf8_lossy(&output.stderr);
4968 stdout
4969 .lines()
4970 .chain(stderr.lines())
4971 .map(str::trim)
4972 .find(|line| !line.is_empty())
4973 .map(|line| truncate_text(line, 200))
4974}
4975
4976pub(crate) fn auth_evidence(harness: &str) -> bool {
4977 let env_names: &[&str] = match harness {
4978 HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
4979 HarnessId::CODEX => &["OPENAI_API_KEY"],
4980 HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
4981 HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
4982 HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
4983 HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
4984 HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
4985 _ => &[],
4986 };
4987 if env_names
4988 .iter()
4989 .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
4990 {
4991 return true;
4992 }
4993 let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
4994 return false;
4995 };
4996 let files: Vec<PathBuf> = match harness {
4997 HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
4998 HarnessId::CODEX => vec![home.join(".codex/auth.json")],
4999 HarnessId::OPENCODE => vec![
5000 home.join(".local/share/opencode/auth.json"),
5001 home.join(".config/opencode/auth.json"),
5002 ],
5003 HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5004 HarnessId::GROK => vec![home.join(".grok/auth.json")],
5005 HarnessId::GEMINI => vec![
5006 home.join(".gemini/oauth_creds.json"),
5007 home.join(".gemini/google_accounts.json"),
5008 ],
5009 HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5010 HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5011 _ => Vec::new(),
5012 };
5013 if files.into_iter().any(|path| {
5014 std::fs::metadata(path)
5015 .map(|metadata| metadata.is_file() && metadata.len() > 2)
5016 .unwrap_or(false)
5017 }) {
5018 return true;
5019 }
5020 if harness == HarnessId::CLAUDE_CODE {
5027 return std::fs::read_to_string(home.join(".claude.json"))
5028 .map(|text| text.contains("\"oauthAccount\""))
5029 .unwrap_or(false);
5030 }
5031 false
5032}
5033
5034fn looks_like_auth_error(message: &str) -> bool {
5035 let message = message.to_ascii_lowercase();
5036 [
5037 "auth",
5038 "login",
5039 "sign in",
5040 "sign-in",
5041 "credential",
5042 "unauthorized",
5043 "forbidden",
5044 "token",
5045 ]
5046 .iter()
5047 .any(|needle| message.contains(needle))
5048}
5049
5050fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5051 crate::RuntimeCapabilities {
5052 start_session: false,
5053 resume_session: false,
5054 attach_existing_process: false,
5055 send_input: false,
5056 stream_events: false,
5057 interrupt: false,
5058 steer: false,
5059 respond_to_requests: false,
5060 }
5061}
5062
5063fn truncate_text(text: &str, max_chars: usize) -> String {
5064 let mut chars = text.chars();
5065 let truncated = chars.by_ref().take(max_chars).collect::<String>();
5066 if chars.next().is_some() {
5067 format!("{truncated}…")
5068 } else {
5069 truncated
5070 }
5071}
5072
5073fn error_message(error: ServiceError) -> String {
5074 match error {
5075 ServiceError::InvalidParams(message)
5076 | ServiceError::Operation(message)
5077 | ServiceError::UnsupportedAction(message) => message,
5078 ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5079 ServiceError::Sdk(error) => error.to_string(),
5080 }
5081}
5082
5083#[derive(Debug)]
5084enum ServiceError {
5085 InvalidParams(String),
5086 MethodNotFound,
5087 UnsupportedAction(String),
5088 Operation(String),
5089 Sdk(SdkError),
5090}
5091
5092fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5093 match error {
5094 ServiceError::InvalidParams(message) => {
5095 SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5096 }
5097 ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5098 SdkError::unsupported(operation)
5099 }
5100 ServiceError::Operation(message) => {
5101 let code = if message.contains("already in progress") {
5102 SdkErrorCode::Busy
5103 } else if message.contains("not supported by this runtime") {
5104 SdkErrorCode::UnsupportedAction
5105 } else if message.contains("unknown runtime connection") {
5106 SdkErrorCode::NotFound
5107 } else {
5108 SdkErrorCode::Execution
5109 };
5110 SdkError::new(code, operation, message)
5111 }
5112 ServiceError::Sdk(error) => error,
5113 }
5114}
5115
5116fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5117 let error_code = error.code();
5118 let code = match error_code {
5119 SdkErrorCode::Unauthenticated => -32030,
5120 SdkErrorCode::Unauthorized => -32031,
5121 SdkErrorCode::ControllerRequired => -32032,
5122 SdkErrorCode::LeaseExpired => -32033,
5123 SdkErrorCode::InvalidArgument => -32602,
5124 SdkErrorCode::NotFound => -32004,
5125 SdkErrorCode::Busy => -32000,
5126 SdkErrorCode::UnsupportedAction => -32020,
5127 SdkErrorCode::Execution => -32002,
5128 SdkErrorCode::Transport => -32003,
5129 };
5130 json!({
5131 "jsonrpc": "2.0",
5132 "id": id,
5133 "error": {
5134 "code": code,
5135 "name": error_code,
5136 "operation": error.operation(),
5137 "message": error.to_string(),
5138 },
5139 })
5140}
5141
5142fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
5143 serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
5144}
5145
5146fn operation(error: impl Into<crate::Error>) -> ServiceError {
5147 let error = error.into();
5148 match error {
5149 crate::Error::Sdk(error) => ServiceError::Sdk(error),
5150 error => ServiceError::Operation(error.to_string()),
5151 }
5152}
5153
5154#[derive(Debug, Clone, Deserialize, Default)]
5158#[serde(default)]
5159struct MemoryRequest {
5160 harness: Option<String>,
5162 query: Option<String>,
5164 profile: Option<String>,
5166 session: Option<String>,
5168 full: bool,
5170 regex: bool,
5172 cwd: Option<std::path::PathBuf>,
5174 homes: crate::HarnessHomes,
5176}
5177
5178fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
5181 let request = decode::<MemoryRequest>(params)?;
5182 let harness = request
5183 .harness
5184 .clone()
5185 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
5186 let to_service = |error: crate::memory::MemoryError| match error {
5187 crate::memory::MemoryError::UnsupportedHarness { .. }
5188 | crate::memory::MemoryError::SessionNotScoped { .. } => {
5189 ServiceError::UnsupportedAction(error.to_string())
5190 }
5191 other => ServiceError::InvalidParams(other.to_string()),
5192 };
5193 match method {
5194 "harness.v1.memory.show" => {
5195 let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
5196 harness,
5197 profile: request.profile,
5198 session: request.session,
5199 full: request.full,
5200 cwd: request.cwd,
5201 homes: request.homes,
5202 })
5203 .map_err(to_service)?;
5204 Ok(json!({
5205 "schema": crate::memory::MEMORY_SCHEMA,
5206 "documents": documents,
5207 }))
5208 }
5209 "harness.v1.memory.search" => {
5210 let query = request
5211 .query
5212 .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
5213 let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
5214 harness,
5215 query,
5216 profile: request.profile,
5217 regex: request.regex,
5218 cwd: request.cwd,
5219 homes: request.homes,
5220 })
5221 .map_err(to_service)?;
5222 Ok(json!({
5223 "schema": crate::memory::MEMORY_SCHEMA,
5224 "matches": matches,
5225 }))
5226 }
5227 _ => Err(ServiceError::MethodNotFound),
5228 }
5229}
5230
5231#[derive(Debug, Clone, Deserialize)]
5235#[serde(default)]
5236struct ProfilesQuery {
5237 harness: Option<String>,
5239 name: Option<String>,
5241 homes: crate::HarnessHomes,
5243}
5244
5245impl Default for ProfilesQuery {
5246 fn default() -> Self {
5247 Self {
5248 harness: None,
5249 name: None,
5250 homes: crate::HarnessHomes::default(),
5251 }
5252 }
5253}
5254
5255fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
5258 let query = decode::<ProfilesQuery>(params)?;
5259 let to_service = |error: crate::profiles::ProfileError| match error {
5260 crate::profiles::ProfileError::UnsupportedHarness { .. } => {
5261 ServiceError::UnsupportedAction(error.to_string())
5262 }
5263 crate::profiles::ProfileError::NotFound { .. } => {
5264 ServiceError::InvalidParams(error.to_string())
5265 }
5266 };
5267 match method {
5268 "harness.v1.profiles.list" => {
5269 let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
5270 .map_err(to_service)?;
5271 Ok(json!({
5272 "schema": crate::profiles::PROFILES_SCHEMA,
5273 "profiles": profiles,
5274 }))
5275 }
5276 "harness.v1.profiles.get" => {
5277 let harness = query
5278 .harness
5279 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
5280 let name = query
5281 .name
5282 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
5283 let profile =
5284 crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
5285 Ok(json!({
5286 "schema": crate::profiles::PROFILES_SCHEMA,
5287 "profile": profile,
5288 }))
5289 }
5290 _ => Err(ServiceError::MethodNotFound),
5291 }
5292}
5293
5294#[derive(Debug, Clone, Deserialize)]
5298#[serde(default)]
5299struct ChannelsQuery {
5300 harness: Option<String>,
5302 name: Option<String>,
5304 homes: crate::HarnessHomes,
5306}
5307
5308impl Default for ChannelsQuery {
5309 fn default() -> Self {
5310 Self {
5311 harness: None,
5312 name: None,
5313 homes: crate::HarnessHomes::default(),
5314 }
5315 }
5316}
5317
5318#[derive(Debug, Clone, Deserialize)]
5322#[serde(default)]
5323struct RoutesQuery {
5324 harness: Option<String>,
5325 profile: Option<String>,
5327 homes: crate::HarnessHomes,
5328}
5329
5330impl Default for RoutesQuery {
5331 fn default() -> Self {
5332 Self {
5333 harness: None,
5334 profile: None,
5335 homes: crate::HarnessHomes::default(),
5336 }
5337 }
5338}
5339
5340#[derive(Debug, Clone, Deserialize)]
5341#[serde(default)]
5342struct TriggersQuery {
5343 harness: Option<String>,
5344 homes: crate::HarnessHomes,
5345}
5346
5347impl Default for TriggersQuery {
5348 fn default() -> Self {
5349 Self {
5350 harness: None,
5351 homes: crate::HarnessHomes::default(),
5352 }
5353 }
5354}
5355
5356fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
5357 let query = decode::<TriggersQuery>(params)?;
5358 let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
5359 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
5360 Ok(json!({
5361 "schema": crate::triggers::TRIGGERS_SCHEMA,
5362 "triggers": triggers,
5363 }))
5364}
5365
5366fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
5367 let query = decode::<RoutesQuery>(params)?;
5368 let routes = crate::routes::list_routes(
5369 &query.homes,
5370 query.harness.as_deref(),
5371 query.profile.as_deref(),
5372 )
5373 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
5374 Ok(json!({
5375 "schema": crate::routes::ROUTES_SCHEMA,
5376 "routes": routes,
5377 }))
5378}
5379
5380fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
5381 let query = decode::<ChannelsQuery>(params)?;
5382 let to_service = |error: crate::channels::ChannelError| match error {
5383 crate::channels::ChannelError::UnsupportedHarness { .. } => {
5384 ServiceError::UnsupportedAction(error.to_string())
5385 }
5386 crate::channels::ChannelError::NotFound { .. } => {
5387 ServiceError::InvalidParams(error.to_string())
5388 }
5389 };
5390 match method {
5391 "harness.v1.channels.list" => {
5392 let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
5393 .map_err(to_service)?;
5394 Ok(json!({
5395 "schema": crate::channels::CHANNELS_SCHEMA,
5396 "channels": channels,
5397 }))
5398 }
5399 "harness.v1.channels.status" => {
5400 let harness = query
5401 .harness
5402 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
5403 let name = query
5404 .name
5405 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
5406 let channel = crate::channels::channel_status(&query.homes, &harness, &name)
5407 .map_err(to_service)?;
5408 Ok(json!({
5409 "schema": crate::channels::CHANNELS_SCHEMA,
5410 "channel": channel,
5411 }))
5412 }
5413 _ => Err(ServiceError::MethodNotFound),
5414 }
5415}
5416
5417fn rpc_error(id: Value, code: i64, message: &str) -> Value {
5418 json!({
5419 "jsonrpc": "2.0",
5420 "id": id,
5421 "error": {"code": code, "message": message},
5422 })
5423}
5424
5425#[cfg(test)]
5426mod tests {
5427 use super::*;
5428 use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
5429 use async_trait::async_trait;
5430 use std::io::Write;
5431 use std::path::PathBuf;
5432 use std::time::Instant;
5433
5434 #[test]
5435 fn indexed_claude_descriptor_keeps_the_live_peer_address() {
5436 let descriptor = SessionDescriptor {
5437 locator: SessionLocator {
5438 harness: HarnessId::new(HarnessId::CLAUDE_CODE),
5439 session_id: "live-session".into(),
5440 storage: StorageLocator::File {
5441 path: PathBuf::from("/tmp/live-session.jsonl"),
5442 },
5443 },
5444 cwd: Some(PathBuf::from("/project")),
5445 title: None,
5446 preview_candidates: Vec::new(),
5447 latest_message_candidates: Vec::new(),
5448 updated_at_ms: Some(1),
5449 message_count: None,
5450 model: None,
5451 parent_session_id: None,
5452 child_session_count: 0,
5453 nouns: Default::default(),
5454 };
5455 let peer = crate::claude_peer::ClaudePeerSession {
5456 pid: 42,
5457 session_id: "live-session".into(),
5458 cwd: Some(PathBuf::from("/project")),
5459 name: "peer".into(),
5460 socket_path: PathBuf::from("/tmp/peer.sock"),
5461 status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
5462 updated_at_ms: Some(1),
5463 version: Some("test".into()),
5464 };
5465
5466 let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
5467 assert!(value["live_endpoint"]
5468 .as_str()
5469 .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
5470 }
5471
5472 struct EndingRuntime {
5473 handle: RuntimeHandle,
5474 event: Option<HarnessEvent>,
5475 }
5476
5477 #[async_trait]
5478 impl RuntimeConnection for EndingRuntime {
5479 fn handle(&self) -> &RuntimeHandle {
5480 &self.handle
5481 }
5482
5483 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
5484 unreachable!("ending runtime does not accept input")
5485 }
5486
5487 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
5488 Ok(self.event.take())
5489 }
5490
5491 async fn interrupt(&mut self) -> crate::Result<()> {
5492 Ok(())
5493 }
5494
5495 async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
5496 Ok(())
5497 }
5498
5499 async fn close(&mut self) -> crate::Result<()> {
5500 Ok(())
5501 }
5502 }
5503
5504 fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
5505 Box::new(EndingRuntime {
5506 handle: RuntimeHandle {
5507 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
5508 runtime_id: "ending-session".into(),
5509 endpoint: RuntimeEndpoint::LocalProcess {
5510 pid: None,
5511 command: vec!["ending-runtime".into()],
5512 protocol: "test".into(),
5513 },
5514 },
5515 event,
5516 })
5517 }
5518
5519 fn request(id: u64, method: &str, params: Value) -> Value {
5520 json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
5521 }
5522
5523 fn hermes_store() -> PathBuf {
5526 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
5527 }
5528
5529 fn hermes_discovery(params: Value) -> Value {
5533 let mut response =
5534 HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
5535 let store = hermes_store().display().to_string();
5536 for session in response["result"]["sessions"]
5537 .as_array_mut()
5538 .expect("sessions array")
5539 {
5540 if session["locator"]["storage"]["path"] == json!(store) {
5541 session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
5542 }
5543 session.as_object_mut().unwrap().remove("activity");
5547 }
5548 response["result"].take()
5549 }
5550
5551 fn hermes_query() -> Value {
5552 json!({
5553 "harnesses": ["hermes"],
5554 "homes": {"hermes": hermes_store()},
5555 })
5556 }
5557
5558 fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
5559 result["sessions"]
5560 .as_array()
5561 .expect("sessions array")
5562 .iter()
5563 .find(|session| session["locator"]["session_id"] == json!(id))
5564 .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
5565 }
5566
5567 #[test]
5568 fn orch6_discover_rows_carry_the_conversation_nouns() {
5569 let result = hermes_discovery(hermes_query());
5570
5571 let dm = row(&result, "tg-dm-1");
5574 assert_eq!(dm["trigger"], json!("channel"));
5575 assert_eq!(dm["surface"]["platform"], json!("telegram"));
5576 assert_eq!(dm["surface"]["kind"], json!("dm"));
5577 assert_eq!(dm["surface"]["chat_id"], json!("123456"));
5578 assert_eq!(dm["surface"]["participant_id"], json!("u1"));
5579 assert_eq!(
5580 dm["workspace"],
5581 json!({"kind": "channel", "value": "telegram:123456"})
5582 );
5583 assert!(dm.get("profile").is_none(), "{dm:#}");
5584
5585 let fire = row(&result, "cron_job42_20260902_120000");
5587 assert_eq!(fire["trigger"], json!("cron"));
5588 assert_eq!(
5589 fire["recurrence"],
5590 json!({"job_id": "job42", "kind": "cron"})
5591 );
5592 assert_eq!(fire["workspace"]["kind"], json!("repo"));
5593
5594 let coder = row(&result, "tg-coder-1");
5597 assert_eq!(coder["trigger"], json!("channel"));
5598 assert_eq!(coder["profile"], json!("coder"));
5599 assert_eq!(coder["surface"]["thread_id"], json!("55"));
5600 assert_eq!(
5601 coder["surface"]["key"],
5602 json!("agent:coder:telegram:group:-100777:55")
5603 );
5604 assert_eq!(
5605 coder["workspace"],
5606 json!({"kind": "repo", "value": "/workspace/project"})
5607 );
5608 assert_eq!(
5609 coder["cross_surface"],
5610 json!({"state": "pending", "platform": "discord"})
5611 );
5612
5613 let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
5615 assert_eq!(acp["trigger"], json!("human"));
5616 assert!(acp.get("surface").is_none(), "{acp:#}");
5617 assert_eq!(acp["workspace"], json!({"kind": "none"}));
5618 }
5619
5620 #[test]
5621 fn orch6_discover_filters_by_harness_and_profile() {
5622 let mut params = hermes_query();
5623 params["profile"] = json!("coder");
5624 let result = hermes_discovery(params);
5625 let ids: Vec<&str> = result["sessions"]
5626 .as_array()
5627 .expect("sessions array")
5628 .iter()
5629 .map(|session| session["locator"]["session_id"].as_str().unwrap())
5630 .collect();
5631 assert_eq!(ids, vec!["tg-coder-1"]);
5632
5633 let mut missing = hermes_query();
5636 missing["profile"] = json!("nobody");
5637 assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
5638
5639 let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
5642 assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
5643 }
5644
5645 #[test]
5646 fn orch6_load_reports_the_same_nouns_as_discovery() {
5647 let mut service = HarnessSessionService::new();
5648 let loaded = service.handle(request(
5649 1,
5650 "harness.v1.sessions.load",
5651 json!({"locator": {
5652 "harness": "hermes",
5653 "session_id": "tg-coder-1",
5654 "storage": {"kind": "file", "path": hermes_store()},
5655 }}),
5656 ));
5657 let session = &loaded["result"]["session"];
5658 let discovered = hermes_discovery(hermes_query());
5659 let row = row(&discovered, "tg-coder-1");
5660 for noun in [
5661 "trigger",
5662 "surface",
5663 "profile",
5664 "recurrence",
5665 "cross_surface",
5666 "workspace",
5667 ] {
5668 assert_eq!(
5669 session[noun],
5670 row.get(noun).cloned().unwrap_or(Value::Null),
5671 "`{noun}` disagrees between sessions.load and sessions.discover"
5672 );
5673 }
5674 }
5675
5676 fn profile_fixture_homes() -> Value {
5679 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
5680 json!({
5681 "hermes": fixtures.join("hermes_home/state.db"),
5682 "openclaw": fixtures.join("openclaw_home"),
5683 })
5684 }
5685
5686 fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
5687 response["result"]["profiles"]
5688 .as_array()
5689 .unwrap_or_else(|| panic!("no profiles array in {response}"))
5690 .iter()
5691 .find(|row| row["harness"] == harness && row["name"] == name)
5692 .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
5693 }
5694
5695 #[test]
5700 fn profiles_list_reads_every_source_uniformly() {
5701 let mut service = HarnessSessionService::new();
5702 let response = service.handle(request(
5703 1,
5704 "harness.v1.profiles.list",
5705 json!({"homes": profile_fixture_homes()}),
5706 ));
5707 assert_eq!(
5708 response["result"]["schema"],
5709 crate::profiles::PROFILES_SCHEMA
5710 );
5711
5712 let default = profile_row(&response, "hermes", "default");
5713 assert_eq!(default["kind"], "hermes_profile");
5714 assert_eq!(default["default"], true);
5715 assert_eq!(default["routes"], 0);
5716 assert_eq!(default["sessions"], 11);
5717 assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
5718
5719 let coder = profile_row(&response, "hermes", "coder");
5720 assert_eq!(coder["kind"], "hermes_profile");
5721 assert_eq!(coder["default"], false);
5722 assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
5723 assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
5724 assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
5725 assert!(coder["home"]
5726 .as_str()
5727 .unwrap()
5728 .ends_with("hermes_home/profiles/coder"));
5729
5730 let main = profile_row(&response, "openclaw", "main");
5731 assert_eq!(main["kind"], "openclaw_agent");
5732 assert_eq!(main["default"], true);
5735 assert_eq!(main["routes"], 0);
5736 assert_eq!(main["sessions"], 4);
5737 assert_eq!(
5738 main["model"],
5739 Value::Null,
5740 "`agents.defaults.model` is an install default, not this agent's pin"
5741 );
5742
5743 let design = profile_row(&response, "openclaw", "design");
5744 assert_eq!(design["default"], false);
5745 assert_eq!(design["routes"], 1, "one binding names agentId `design`");
5746 assert_eq!(design["sessions"], 0);
5747 assert_eq!(design["model"], "anthropic/claude-opus-4-8");
5748
5749 let preset = profile_row(&response, "supercode", "supercode-default");
5750 assert_eq!(preset["kind"], "preset");
5751 assert_eq!(preset["default"], true);
5752 assert_eq!(preset["home"], Value::Null);
5753 assert_eq!(preset["routes"], Value::Null);
5754 }
5755
5756 #[test]
5759 fn profiles_list_reads_codex_profile_tables() {
5760 let codex_home = std::env::temp_dir().join(format!(
5761 "supercode-orch10-codex-{}-{}",
5762 std::process::id(),
5763 std::time::SystemTime::now()
5764 .duration_since(std::time::UNIX_EPOCH)
5765 .unwrap()
5766 .as_nanos()
5767 ));
5768 std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
5769 std::fs::write(
5770 codex_home.join("config.toml"),
5771 "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
5772 )
5773 .unwrap();
5774
5775 let mut service = HarnessSessionService::new();
5776 let response = service.handle(request(
5777 1,
5778 "harness.v1.profiles.list",
5779 json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
5780 ));
5781 let rows = response["result"]["profiles"].as_array().unwrap();
5782 assert_eq!(rows.len(), 2, "{response}");
5783 let review = profile_row(&response, "codex", "review");
5784 assert_eq!(review["kind"], "codex_profile");
5785 assert_eq!(review["default"], true);
5786 assert_eq!(review["model"], "gpt-5.1-codex");
5787 assert_eq!(review["home"], Value::Null);
5788 assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
5789
5790 let got = service.handle(request(
5791 2,
5792 "harness.v1.profiles.get",
5793 json!({
5794 "harness": "codex",
5795 "name": "fast",
5796 "homes": {"codex": codex_home.join("sessions")},
5797 }),
5798 ));
5799 assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
5800 std::fs::remove_dir_all(&codex_home).ok();
5801 }
5802
5803 #[test]
5806 fn profiles_refuse_harnesses_without_the_concept() {
5807 let mut service = HarnessSessionService::new();
5808 let response = service.handle(request(
5809 1,
5810 "harness.v1.profiles.list",
5811 json!({"harness": "claude-code"}),
5812 ));
5813 assert_eq!(response["error"]["code"], -32020, "{response}");
5814
5815 let missing = service.handle(request(
5816 2,
5817 "harness.v1.profiles.get",
5818 json!({
5819 "harness": "hermes",
5820 "name": "no-such-profile",
5821 "homes": profile_fixture_homes(),
5822 }),
5823 ));
5824 assert_eq!(missing["error"]["code"], -32602, "{missing}");
5825 }
5826
5827 #[test]
5830 fn profiles_methods_are_advertised() {
5831 let mut service = HarnessSessionService::new();
5832 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
5833 let methods = response["result"]["methods"].as_array().unwrap();
5834 for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
5835 assert!(
5836 methods.iter().any(|entry| entry == method),
5837 "{method} is not advertised"
5838 );
5839 }
5840 }
5841
5842 fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
5847 response["result"]["channels"]
5848 .as_array()
5849 .unwrap_or_else(|| panic!("no channels array in {response}"))
5850 .iter()
5851 .find(|row| row["harness"] == harness && row["name"] == name)
5852 .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
5853 }
5854
5855 fn channels_list(harness: Option<&str>) -> Value {
5856 let mut params = json!({"homes": profile_fixture_homes()});
5857 if let Some(harness) = harness {
5858 params["harness"] = json!(harness);
5859 }
5860 HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
5861 }
5862
5863 #[test]
5867 fn channels_list_reads_both_gateway_harnesses_uniformly() {
5868 let response = channels_list(None);
5869 assert_eq!(
5870 response["result"]["schema"],
5871 crate::channels::CHANNELS_SCHEMA
5872 );
5873
5874 let telegram = channel_row(&response, "hermes", "telegram");
5877 assert_eq!(telegram["kind"], "telegram");
5878 assert_eq!(telegram["enabled"], true);
5879 assert_eq!(telegram["configured"], true);
5880 assert_eq!(telegram["sessions"], 2);
5884 let api = channel_row(&response, "hermes", "api_server");
5885 assert_eq!(api["configured"], true, "extra.key is a credential key");
5886 assert_eq!(api["sessions"], 0);
5887 let webhook = channel_row(&response, "hermes", "webhook");
5888 assert_eq!(webhook["enabled"], false);
5889 assert_eq!(webhook["configured"], true);
5892
5893 let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
5895 assert_eq!(linked["kind"], "slack");
5896 assert_eq!(linked["account"], "T0FIXTURE");
5897 assert_eq!(linked["enabled"], true);
5898 assert_eq!(linked["configured"], true);
5899 let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
5900 assert_eq!(unlinked["enabled"], false);
5901 assert_eq!(
5902 unlinked["configured"], false,
5903 "an account with no credential key is not configured"
5904 );
5905 let telegram = channel_row(&response, "openclaw", "telegram");
5908 assert_eq!(telegram["account"], "hermes-fixture-bot");
5909 assert_eq!(telegram["configured"], true);
5910
5911 for row in response["result"]["channels"].as_array().unwrap() {
5913 assert_eq!(row["status"], "unknown", "{row}");
5914 }
5915 }
5916
5917 #[test]
5921 fn channels_rows_never_carry_a_fixture_secret() {
5922 let secrets = [
5923 "FAKE-TOKEN-DO-NOT-EMIT",
5924 "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
5925 "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
5926 "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
5927 "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
5928 ];
5929 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
5931 let raw = format!(
5932 "{}{}",
5933 std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
5934 std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
5935 );
5936 for secret in secrets {
5937 assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
5938 }
5939
5940 let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
5941 for secret in secrets {
5942 assert!(
5943 !emitted.contains(secret),
5944 "`{secret}` leaked into a channel row: {emitted}"
5945 );
5946 }
5947 for row in channels_list(None)["result"]["channels"]
5950 .as_array()
5951 .unwrap()
5952 {
5953 for key in row.as_object().unwrap().keys() {
5954 let key = key.to_ascii_lowercase();
5955 assert!(
5956 !["token", "key", "secret", "password", "credential"]
5957 .iter()
5958 .any(|marker| key.ends_with(marker)),
5959 "`{key}` is a credential-shaped field on a channel row"
5960 );
5961 }
5962 }
5963 }
5964
5965 #[test]
5967 fn channels_status_reads_one_row_by_name() {
5968 let mut service = HarnessSessionService::new();
5969 let got = service.handle(request(
5970 1,
5971 "harness.v1.channels.status",
5972 json!({
5973 "harness": "openclaw",
5974 "name": "slack/T0FIXTURE",
5975 "homes": profile_fixture_homes(),
5976 }),
5977 ));
5978 assert_eq!(got["result"]["channel"]["kind"], "slack");
5979 assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
5980 assert_eq!(got["result"]["channel"]["status"], "unknown");
5981
5982 let missing = service.handle(request(
5983 2,
5984 "harness.v1.channels.status",
5985 json!({
5986 "harness": "openclaw",
5987 "name": "no-such-channel",
5988 "homes": profile_fixture_homes(),
5989 }),
5990 ));
5991 assert_eq!(missing["error"]["code"], -32602, "{missing}");
5992 }
5993
5994 #[test]
5998 fn channels_refuse_harnesses_without_the_concept() {
5999 let response = channels_list(Some("claude-code"));
6000 assert_eq!(response["error"]["code"], -32020, "{response}");
6001 let codex = channels_list(Some("codex"));
6002 assert_eq!(codex["error"]["code"], -32020, "{codex}");
6003 }
6004
6005 #[test]
6007 fn channels_list_filters_by_harness() {
6008 let response = channels_list(Some("openclaw"));
6009 let rows = response["result"]["channels"].as_array().unwrap();
6010 assert!(!rows.is_empty(), "{response}");
6011 assert!(
6012 rows.iter().all(|row| row["harness"] == "openclaw"),
6013 "harness filter leaked: {response}"
6014 );
6015 }
6016
6017 #[test]
6020 fn channels_methods_are_advertised() {
6021 let mut service = HarnessSessionService::new();
6022 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6023 let methods = response["result"]["methods"].as_array().unwrap();
6024 for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6025 assert!(
6026 methods.iter().any(|entry| entry == method),
6027 "{method} is not advertised"
6028 );
6029 }
6030 }
6031
6032 #[test]
6037 fn orch6_story_fixture_matches_the_live_discovery_response() {
6038 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6039 .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6040 let mut result = hermes_discovery(hermes_query());
6041 result.as_object_mut().unwrap().remove("next_cursor");
6045 let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6046 if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6047 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6048 std::fs::write(&path, &rendered).unwrap();
6049 }
6050 let committed = std::fs::read_to_string(&path).unwrap_or_default();
6051 assert_eq!(
6052 committed, rendered,
6053 "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6054 re-run with SUPERCODE_UPDATE_FIXTURES=1"
6055 );
6056 }
6057
6058 fn pi_locator() -> SessionLocator {
6059 SessionLocator {
6060 harness: HarnessId::from(HarnessId::PI),
6061 session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6062 storage: StorageLocator::File {
6063 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6064 .join("tests/fixtures/pi_session.jsonl"),
6065 },
6066 }
6067 }
6068
6069 fn opencode_locator() -> SessionLocator {
6070 let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
6071 SessionLocator {
6072 harness: HarnessId::from(HarnessId::OPENCODE),
6073 session_id: session_id.into(),
6074 storage: StorageLocator::Sqlite {
6075 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6076 .join("tests/fixtures/opencode_fixture/opencode.db"),
6077 selector: session_id.into(),
6078 },
6079 }
6080 }
6081
6082 fn grok_locator() -> SessionLocator {
6083 SessionLocator {
6084 harness: HarnessId::from(HarnessId::GROK),
6085 session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
6086 storage: StorageLocator::File {
6087 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6088 .join("tests/fixtures/grok_session/chat_history.jsonl"),
6089 },
6090 }
6091 }
6092
6093 fn fixture_homes() -> Value {
6096 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6097 json!({
6098 "claude_code": fixtures.join("__absent__"),
6099 "codex": fixtures.join("__absent__"),
6100 "opencode": fixtures.join("__absent__"),
6101 "pi": fixtures.join("__absent__"),
6102 "agents": fixtures.join("__absent__"),
6103 "hermes": fixtures.join("hermes_home"),
6104 "openclaw": fixtures.join("openclaw_home"),
6105 })
6106 }
6107
6108 #[test]
6109 fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
6110 let root = std::env::temp_dir().join(format!(
6111 "supercode-preview-rpc-{}-{}",
6112 std::process::id(),
6113 std::time::SystemTime::now()
6114 .duration_since(std::time::UNIX_EPOCH)
6115 .unwrap()
6116 .as_nanos()
6117 ));
6118 std::fs::create_dir_all(&root).unwrap();
6119 for id in ["first", "second"] {
6120 std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
6121 json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
6122 json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
6123 )).unwrap();
6124 }
6125 let mut service = HarnessSessionService::new();
6126 let query = json!({
6127 "harnesses": ["codex"], "homes": {"codex": root},
6128 "query": "nebula", "search_previews": true, "limit": 1
6129 });
6130 let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
6131 assert!(first.get("error").is_none(), "{first}");
6132 assert_eq!(first["result"]["receipt"]["searched_previews"], true);
6133 assert_eq!(first["result"]["receipt"]["total_matched"], 2);
6134 let mut next_query = query.clone();
6135 next_query["cursor"] = first["result"]["next_cursor"].clone();
6136 let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
6137 assert_eq!(next["result"]["receipt"]["returned"], 1);
6138 assert_eq!(next["result"]["receipt"]["total_matched"], 2);
6139 assert_eq!(next["result"]["receipt"]["truncated"], false);
6140 assert_ne!(
6141 first["result"]["sessions"][0]["locator"],
6142 next["result"]["sessions"][0]["locator"]
6143 );
6144 let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
6145 assert!(
6146 refused["error"]["message"]
6147 .as_str()
6148 .unwrap()
6149 .contains("use sessions.discover"),
6150 "{refused}"
6151 );
6152 std::fs::remove_dir_all(root).unwrap();
6153 }
6154
6155 #[test]
6156 fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
6157 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
6158 let root = std::env::temp_dir().join(format!(
6159 "supercode-index-rpc-{}-{}",
6160 std::process::id(),
6161 std::time::SystemTime::now()
6162 .duration_since(std::time::UNIX_EPOCH)
6163 .unwrap()
6164 .as_nanos()
6165 ));
6166 std::fs::create_dir_all(&root).unwrap();
6167 for id in ["first", "second"] {
6168 std::fs::write(root.join(format!("{id}.jsonl")), format!(
6169 "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
6170 )).unwrap();
6171 }
6172 let mut service = HarnessSessionService::new();
6173 let opened = service.handle(request(
6174 1,
6175 "harness.v1.sessions.index.subscribe",
6176 json!({
6177 "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
6178 }),
6179 ));
6180 assert!(opened.get("error").is_none(), "{opened:#}");
6181 let subscription = opened["result"]["subscription"]
6182 .as_str()
6183 .unwrap()
6184 .to_owned();
6185 assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
6186 for params in [
6187 json!({"subscription": subscription, "limit": 0}),
6188 json!({"subscription": subscription, "limit": 2049}),
6189 json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
6190 json!({"subscription": "unknown", "limit": 2}),
6191 ] {
6192 let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
6193 assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
6194 }
6195 for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
6196 let response = service.handle(request(
6197 3,
6198 "harness.v1.sessions.index.resize",
6199 json!({
6200 "subscription": subscription, "limit": limit
6201 }),
6202 ));
6203 assert!(response.get("error").is_none(), "{response:#}");
6204 assert_eq!(response["result"]["subscription"], subscription);
6205 assert_eq!(response["result"]["revision"], revision);
6206 assert_eq!(
6207 response["result"]["initial"].as_array().unwrap().len(),
6208 limit
6209 );
6210 assert_eq!(response["result"]["receipt"]["total_matched"], 2);
6211 assert_eq!(service.index_subscriptions.len(), 1);
6212 }
6213 let removed = service.handle(request(
6214 4,
6215 "harness.v1.sessions.index.unsubscribe",
6216 json!({
6217 "subscription": subscription
6218 }),
6219 ));
6220 assert_eq!(removed["result"]["removed"], true);
6221 let stale = service.handle(request(
6222 5,
6223 "harness.v1.sessions.index.resize",
6224 json!({
6225 "subscription": subscription, "limit": 1
6226 }),
6227 ));
6228 assert_eq!(stale["error"]["code"], -32602);
6229 drop(service);
6230 std::fs::remove_dir_all(root).unwrap();
6231 }
6232
6233 fn skills_rows(params: Value) -> Vec<Value> {
6234 let response =
6235 HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
6236 assert!(response.get("error").is_none(), "{response:#}");
6237 response["result"].as_array().cloned().unwrap_or_default()
6238 }
6239
6240 #[test]
6243 fn skills_list_reads_the_hermes_and_openclaw_roots() {
6244 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6245 let rows = skills_rows(json!({
6246 "homes": fixture_homes(),
6247 "cwd": fixtures.join("hermes_home"),
6248 }));
6249 let arxiv = rows
6250 .iter()
6251 .find(|row| row["name"] == json!("arxiv-search"))
6252 .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
6253 assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
6254 assert_eq!(arxiv["scope"], json!("user"));
6255 assert_eq!(arxiv["version"], json!("1.4.0"));
6256 assert!(arxiv["location"]
6257 .as_str()
6258 .unwrap()
6259 .ends_with("hermes_home/skills/research/arxiv"));
6260
6261 let bare = rows
6263 .iter()
6264 .find(|row| row["name"] == json!("bare-skill"))
6265 .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
6266 assert_eq!(bare["enabled"], json!(null));
6267 assert!(bare.get("description").is_none());
6268
6269 let demo = rows
6270 .iter()
6271 .find(|row| row["name"] == json!("clawhub-demo"))
6272 .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
6273 assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
6274 assert_eq!(demo["scope"], json!("managed"));
6275 assert_eq!(demo["enabled"], json!(false));
6276 }
6277
6278 #[test]
6280 fn skills_list_filters_by_harness_and_scope() {
6281 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6282 let hermes = skills_rows(json!({
6283 "homes": fixture_homes(),
6284 "cwd": fixtures.join("hermes_home"),
6285 "harness": HarnessId::HERMES,
6286 }));
6287 assert!(!hermes.is_empty());
6288 assert!(hermes
6289 .iter()
6290 .all(|row| row["harness"] == json!(HarnessId::HERMES)));
6291
6292 let managed = skills_rows(json!({
6293 "homes": fixture_homes(),
6294 "cwd": fixtures.join("openclaw_home"),
6295 "harness": HarnessId::OPENCLAW,
6296 "scope": "managed",
6297 }));
6298 assert_eq!(managed.len(), 1, "{managed:#?}");
6299 assert_eq!(managed[0]["name"], json!("clawhub-demo"));
6300
6301 let bundled = skills_rows(json!({
6302 "homes": fixture_homes(),
6303 "cwd": fixtures.join("openclaw_home"),
6304 "harness": HarnessId::OPENCLAW,
6305 "scope": "bundled",
6306 }));
6307 assert!(bundled.is_empty(), "{bundled:#?}");
6308 }
6309
6310 #[test]
6313 fn skills_list_refuses_an_unknown_harness() {
6314 let response = HarnessSessionService::new().handle(request(
6315 1,
6316 "harness.v1.skills.list",
6317 json!({"harness": "not-a-harness", "homes": fixture_homes()}),
6318 ));
6319 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6320 assert!(response["error"]["message"]
6321 .as_str()
6322 .unwrap()
6323 .contains("not-a-harness"));
6324 }
6325
6326 #[test]
6328 fn skills_list_is_an_advertised_method_and_sdk_operation() {
6329 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
6330 assert_eq!(
6331 SdkOperation::from_method("harness.v1.skills.list"),
6332 Some(SdkOperation::SkillsList)
6333 );
6334 }
6335
6336 #[test]
6340 fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
6341 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
6342 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
6343 assert_eq!(
6344 SdkOperation::from_method("harness.v1.skills.install"),
6345 Some(SdkOperation::SkillsInstall)
6346 );
6347 assert_eq!(
6348 SdkOperation::from_method("harness.v1.skills.remove"),
6349 Some(SdkOperation::SkillsRemove)
6350 );
6351 }
6352
6353 #[test]
6357 fn skills_install_and_remove_drive_the_directory_door() {
6358 let root = std::env::temp_dir().join(format!(
6359 "supercode-orch22-rpc-{}-{}",
6360 std::process::id(),
6361 std::time::SystemTime::now()
6362 .duration_since(std::time::UNIX_EPOCH)
6363 .unwrap()
6364 .as_nanos()
6365 ));
6366 let source = root.join("probe-src");
6367 std::fs::create_dir_all(&source).unwrap();
6368 std::fs::write(
6369 source.join("SKILL.md"),
6370 "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
6371 )
6372 .unwrap();
6373 let homes = json!({
6374 "claude_code": root.join("claude_home"),
6375 "codex": root.join("__absent__"),
6376 "opencode": root.join("__absent__"),
6377 "pi": root.join("__absent__"),
6378 "hermes": root.join("__absent__"),
6379 "openclaw": root.join("__absent__"),
6380 "agents": root.join("__absent__"),
6381 });
6382
6383 let mut service = HarnessSessionService::new();
6384 let installed = service.handle(request(
6385 1,
6386 "harness.v1.skills.install",
6387 json!({
6388 "harness": HarnessId::CLAUDE_CODE,
6389 "source": source,
6390 "scope": "user",
6391 "cwd": root,
6392 "homes": homes,
6393 }),
6394 ));
6395 let result = &installed["result"];
6396 assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
6397 assert_eq!(result["verb"], json!("install"));
6398 assert!(result["ran"]
6399 .as_str()
6400 .is_some_and(|ran| ran.starts_with("cp -R ")));
6401 assert_eq!(result["skill"]["scope"], json!("user"));
6402
6403 let removed = service.handle(request(
6404 2,
6405 "harness.v1.skills.remove",
6406 json!({
6407 "harness": HarnessId::CLAUDE_CODE,
6408 "name": "orch22-rpc",
6409 "scope": "user",
6410 "cwd": root,
6411 "homes": homes,
6412 }),
6413 ));
6414 assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
6415 assert!(!root.join("claude_home/skills/orch22-rpc").exists());
6416 std::fs::remove_dir_all(&root).ok();
6417 }
6418
6419 #[test]
6422 fn skills_remove_refuses_openclaw_at_the_pin() {
6423 let response = HarnessSessionService::new().handle(request(
6424 1,
6425 "harness.v1.skills.remove",
6426 json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
6427 ));
6428 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6429 assert!(response["error"]["message"]
6430 .as_str()
6431 .unwrap()
6432 .contains("no `skills remove` verb"));
6433 }
6434
6435 #[test]
6438 fn skills_install_refuses_a_harness_without_a_skills_root() {
6439 let response = HarnessSessionService::new().handle(request(
6440 1,
6441 "harness.v1.skills.install",
6442 json!({"harness": "not-a-harness", "source": "/tmp/x"}),
6443 ));
6444 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6445 assert!(response["error"]["message"]
6446 .as_str()
6447 .unwrap()
6448 .contains("not-a-harness"));
6449 }
6450
6451 fn memory_homes() -> Value {
6459 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6460 json!({
6461 "claude_code": fixtures.join("__absent__"),
6462 "codex": fixtures.join("__absent__"),
6463 "opencode": fixtures.join("__absent__"),
6464 "pi": fixtures.join("__absent__"),
6465 "grok": fixtures.join("__absent__"),
6466 "gemini": fixtures.join("__absent__"),
6467 "goose": fixtures.join("__absent__"),
6468 "supercode": fixtures.join("__absent__"),
6469 "hermes": fixtures.join("hermes_home/state.db"),
6470 "openclaw": fixtures.join("openclaw_home"),
6471 })
6472 }
6473
6474 fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
6475 let response = HarnessSessionService::new().handle(request(1, method, params));
6476 assert!(response.get("error").is_none(), "{response:#}");
6477 assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
6478 response["result"][key]
6479 .as_array()
6480 .cloned()
6481 .unwrap_or_default()
6482 }
6483
6484 fn memory_documents(params: Value) -> Vec<Value> {
6485 memory_call_ok("harness.v1.memory.show", params, "documents")
6486 }
6487
6488 fn memory_matches(params: Value) -> Vec<Value> {
6489 memory_call_ok("harness.v1.memory.search", params, "matches")
6490 }
6491
6492 fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
6493 rows.iter()
6494 .find(|row| row["profile"] == profile && row["name"] == name)
6495 .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
6496 }
6497
6498 #[test]
6501 fn memory_show_reads_the_hermes_profile_homes() {
6502 let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
6503
6504 let notes = find_document(&rows, "default", "MEMORY.md");
6505 assert_eq!(notes["harness"], "hermes");
6506 assert_eq!(notes["scope"], "user");
6507 assert!(notes["size"].as_u64().unwrap() > 0);
6508 assert!(notes["updated_at"].is_string(), "{notes:#?}");
6509 assert!(notes.get("content").is_none(), "{notes:#?}");
6511 assert_eq!(notes["truncated"], true);
6512 assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
6513
6514 let user = find_document(&rows, "default", "USER.md");
6515 assert_eq!(user["scope"], "user");
6516 assert!(user["preview"]
6517 .as_array()
6518 .unwrap()
6519 .iter()
6520 .any(|line| line.as_str().unwrap().contains("neovim")));
6521
6522 let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
6523 assert!(topic["path"]
6524 .as_str()
6525 .unwrap()
6526 .ends_with("hermes_home/memories/2026-09-01-notes.md"));
6527
6528 let coder = find_document(&rows, "coder", "MEMORY.md");
6530 assert_eq!(coder["scope"], "profile");
6531 assert!(coder["path"]
6532 .as_str()
6533 .unwrap()
6534 .ends_with("hermes_home/profiles/coder/MEMORY.md"));
6535 }
6536
6537 #[test]
6540 fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
6541 let rows = memory_documents(json!({
6542 "harness": "hermes",
6543 "profile": "coder",
6544 "full": true,
6545 "homes": memory_homes(),
6546 }));
6547 assert!(
6548 rows.iter().all(|row| row["profile"] == "coder"),
6549 "{rows:#?}"
6550 );
6551 let coder = find_document(&rows, "coder", "MEMORY.md");
6552 assert!(coder["content"]
6553 .as_str()
6554 .expect("full returns the body")
6555 .contains("anthropic/claude-opus-4-8"));
6556 }
6557
6558 #[test]
6562 fn memory_show_reads_the_openclaw_agent_workspaces() {
6563 let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
6564
6565 let main = find_document(&rows, "main", "MEMORY.md");
6566 assert_eq!(main["scope"], "agent");
6567 assert!(main["path"]
6568 .as_str()
6569 .unwrap()
6570 .ends_with("openclaw_home/workspace/MEMORY.md"));
6571
6572 let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
6573 assert!(topic["path"]
6574 .as_str()
6575 .unwrap()
6576 .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
6577
6578 let design = find_document(&rows, "design", "MEMORY.md");
6579 assert!(design["path"]
6580 .as_str()
6581 .unwrap()
6582 .ends_with("openclaw_home/workspace-design/MEMORY.md"));
6583 }
6584
6585 #[test]
6588 fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
6589 let scratch = std::env::temp_dir().join(format!(
6590 "supercode-orch12-cc-{}-{}",
6591 std::process::id(),
6592 std::time::SystemTime::now()
6593 .duration_since(std::time::UNIX_EPOCH)
6594 .unwrap()
6595 .as_nanos()
6596 ));
6597 let project = scratch.join("repo");
6598 std::fs::create_dir_all(project.join(".git")).unwrap();
6599 let worktree = project.join("crates/harness");
6602 std::fs::create_dir_all(&worktree).unwrap();
6603 let slug: String = project
6604 .to_string_lossy()
6605 .chars()
6606 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
6607 .collect();
6608 let projects = scratch.join("claude/projects");
6609 let memory = projects.join(&slug).join("memory");
6610 std::fs::create_dir_all(&memory).unwrap();
6611 std::fs::write(
6612 memory.join("MEMORY.md"),
6613 "# index\n- [build box](build-box.md) — the pinned harnesses\n",
6614 )
6615 .unwrap();
6616 std::fs::write(
6617 memory.join("build-box.md"),
6618 "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
6619 )
6620 .unwrap();
6621
6622 let mut homes = memory_homes();
6623 homes["claude_code"] = json!(projects);
6624 let rows = memory_documents(json!({
6625 "harness": "claude-code",
6626 "cwd": worktree,
6627 "homes": homes,
6628 }));
6629 let index = find_document(&rows, &slug, "MEMORY.md");
6630 assert_eq!(index["harness"], "claude-code");
6631 assert_eq!(index["scope"], "project");
6632 let topic = find_document(&rows, &slug, "build-box.md");
6633 assert!(topic["preview"]
6634 .as_array()
6635 .unwrap()
6636 .iter()
6637 .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
6638
6639 let hits = memory_matches(json!({
6640 "harness": "claude-code",
6641 "query": "pinned harnesses",
6642 "cwd": worktree,
6643 "homes": homes,
6644 }));
6645 assert_eq!(hits.len(), 1, "{hits:#?}");
6646 assert_eq!(hits[0]["name"], "MEMORY.md");
6647 assert_eq!(hits[0]["line"], 2);
6648
6649 let _ = std::fs::remove_dir_all(&scratch);
6650 }
6651
6652 #[test]
6658 fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
6659 let state = std::env::temp_dir().join(format!(
6660 "supercode-orch12-oc-{}-{}",
6661 std::process::id(),
6662 std::time::SystemTime::now()
6663 .duration_since(std::time::UNIX_EPOCH)
6664 .unwrap()
6665 .as_nanos()
6666 ));
6667 std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
6669 std::fs::create_dir_all(state.join("workspace")).unwrap();
6670 std::fs::write(
6671 state.join("workspace/MEMORY.md"),
6672 "the gateway websocket needs credentials\n",
6673 )
6674 .unwrap();
6675
6676 let mut homes = memory_homes();
6677 homes["openclaw"] = json!(state);
6678 let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
6679 assert_eq!(rows.len(), 1, "{rows:#?}");
6680 let row = find_document(&rows, "main", "MEMORY.md");
6681 assert_eq!(row["scope"], "agent");
6682 assert!(row["path"]
6683 .as_str()
6684 .unwrap()
6685 .ends_with("workspace/MEMORY.md"));
6686
6687 let _ = std::fs::remove_dir_all(&state);
6688 }
6689
6690 #[test]
6693 fn memory_search_reports_hits_by_line_and_misses_as_empty() {
6694 let hit = memory_matches(json!({
6695 "harness": "hermes",
6696 "query": "NEOVIM",
6697 "homes": memory_homes(),
6698 }));
6699 assert_eq!(hit.len(), 1, "{hit:#?}");
6700 assert_eq!(hit[0]["harness"], "hermes");
6701 assert_eq!(hit[0]["name"], "USER.md");
6702 assert_eq!(hit[0]["scope"], "user");
6703 assert_eq!(hit[0]["line"], 5);
6704 assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
6705
6706 let regex = memory_matches(json!({
6708 "harness": "hermes",
6709 "query": "neo(vim|vi)",
6710 "regex": true,
6711 "homes": memory_homes(),
6712 }));
6713 assert_eq!(regex.len(), 1, "{regex:#?}");
6714
6715 let miss = memory_matches(json!({
6716 "harness": "hermes",
6717 "query": "no-memory-line-says-this",
6718 "homes": memory_homes(),
6719 }));
6720 assert!(miss.is_empty(), "{miss:#?}");
6721 }
6722
6723 #[test]
6726 fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
6727 for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
6728 let response = HarnessSessionService::new().handle(request(
6729 1,
6730 method,
6731 json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
6732 ));
6733 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6734 assert!(response["error"]["message"]
6735 .as_str()
6736 .unwrap()
6737 .contains("codex"));
6738 }
6739
6740 let response = HarnessSessionService::new().handle(request(
6741 1,
6742 "harness.v1.memory.show",
6743 json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
6744 ));
6745 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
6746
6747 let response = HarnessSessionService::new().handle(request(
6749 1,
6750 "harness.v1.memory.show",
6751 json!({"homes": memory_homes()}),
6752 ));
6753 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
6754 }
6755
6756 #[test]
6758 fn memory_methods_are_advertised_and_map_to_sdk_operations() {
6759 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
6760 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
6761 assert_eq!(
6762 SdkOperation::from_method("harness.v1.memory.show"),
6763 Some(SdkOperation::MemoryShow)
6764 );
6765 assert_eq!(
6766 SdkOperation::from_method("harness.v1.memory.search"),
6767 Some(SdkOperation::MemorySearch)
6768 );
6769 }
6770
6771 struct RequestingRuntime {
6776 handle: RuntimeHandle,
6777 events: std::collections::VecDeque<HarnessEvent>,
6778 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
6779 }
6780
6781 #[async_trait]
6782 impl RuntimeConnection for RequestingRuntime {
6783 fn handle(&self) -> &RuntimeHandle {
6784 &self.handle
6785 }
6786
6787 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6788 unreachable!("this runtime only raises requests")
6789 }
6790
6791 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6792 match self.events.pop_front() {
6793 Some(event) => Ok(Some(event)),
6794 None => std::future::pending().await,
6798 }
6799 }
6800
6801 async fn interrupt(&mut self) -> crate::Result<()> {
6802 Ok(())
6803 }
6804
6805 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
6806 self.answered
6810 .lock()
6811 .unwrap_or_else(std::sync::PoisonError::into_inner)
6812 .push(json!({"request_id": request_id, "response": response}));
6813 Ok(())
6814 }
6815
6816 async fn close(&mut self) -> crate::Result<()> {
6817 Ok(())
6818 }
6819 }
6820
6821 fn requesting_runtime(
6822 harness: &str,
6823 events: Vec<HarnessEvent>,
6824 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
6825 ) -> Box<dyn RuntimeConnection> {
6826 requesting_runtime_named(harness, "hermes-live-session", events, answered)
6827 }
6828
6829 fn requesting_runtime_named(
6830 harness: &str,
6831 runtime_id: &str,
6832 events: Vec<HarnessEvent>,
6833 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
6834 ) -> Box<dyn RuntimeConnection> {
6835 Box::new(RequestingRuntime {
6836 handle: RuntimeHandle {
6837 harness: HarnessId::from(harness),
6838 runtime_id: runtime_id.into(),
6839 endpoint: RuntimeEndpoint::LocalProcess {
6840 pid: None,
6841 command: vec!["hermes-acp".into()],
6842 protocol: "acp".into(),
6843 },
6844 },
6845 events: events.into(),
6846 answered,
6847 })
6848 }
6849
6850 fn permission_event(id: u64, title: &str) -> HarnessEvent {
6851 HarnessEvent {
6852 sequence: None,
6853 kind: "session/request_permission".into(),
6854 payload: json!({
6855 "jsonrpc": "2.0",
6856 "id": id,
6857 "method": "session/request_permission",
6858 "params": {
6859 "sessionId": "hermes-live-session",
6860 "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
6861 "options": [
6862 {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
6863 {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
6864 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
6865 ],
6866 },
6867 }),
6868 }
6869 }
6870
6871 fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
6872 let response = service.handle(request(1, "harness.v1.approvals.list", params));
6873 assert!(response.get("error").is_none(), "{response:#}");
6874 response["result"].clone()
6875 }
6876
6877 #[tokio::test]
6885 async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
6886 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
6887 let mut service = HarnessSessionService::new();
6888 service.runtimes.insert(
6889 "runtime-cc".into(),
6890 requesting_runtime_named(
6891 HarnessId::CLAUDE_CODE,
6892 "claude-live-session",
6893 vec![HarnessEvent {
6894 sequence: None,
6895 kind: "control_request".into(),
6896 payload: json!({
6897 "type": "control_request",
6898 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
6899 "request": {
6900 "subtype": "can_use_tool",
6901 "tool_name": "Bash",
6902 "display_name": "Bash",
6903 "input": {"command": "touch probe-artifact.txt"},
6904 "tool_use_id": "toolu_mock_1",
6905 },
6906 }),
6907 }],
6908 answered.clone(),
6909 ),
6910 );
6911
6912 let notifications = service.poll_runtimes().await;
6913 assert_eq!(notifications.len(), 1, "{notifications:#?}");
6914
6915 let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
6916 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
6917 let row = &rows[0];
6918 assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
6919 assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
6920 assert_eq!(row["status"], "pending");
6921 assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
6922 assert_eq!(row["runtime_id"], "claude-live-session");
6923 assert_eq!(
6924 row["options"]
6925 .as_array()
6926 .unwrap()
6927 .iter()
6928 .map(|option| option["id"].as_str().unwrap())
6929 .collect::<Vec<_>>(),
6930 vec!["allow", "deny"],
6931 );
6932
6933 let response = resolve(
6934 &mut service,
6935 json!({"id": row["id"], "decision": "allow_once"}),
6936 )
6937 .await;
6938 assert!(response.get("error").is_none(), "{response:#}");
6939 assert_eq!(response["result"]["option_id"], "allow");
6940 assert_eq!(
6941 answered
6942 .lock()
6943 .unwrap_or_else(std::sync::PoisonError::into_inner)
6944 .as_slice(),
6945 &[json!({
6946 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
6947 "response": {"behavior": "allow"},
6948 })],
6949 );
6950 assert_eq!(
6951 approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
6952 .as_array()
6953 .map(Vec::len),
6954 Some(0),
6955 );
6956 }
6957
6958 #[tokio::test]
6962 async fn a_live_permission_request_lists_until_it_is_answered() {
6963 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
6964 let mut service = HarnessSessionService::new();
6965 service.runtimes.insert(
6966 "runtime-1".into(),
6967 requesting_runtime(
6968 HarnessId::HERMES,
6969 vec![permission_event(7, "rm -rf build")],
6970 answered.clone(),
6971 ),
6972 );
6973
6974 let notifications = service.poll_runtimes().await;
6975 assert_eq!(notifications.len(), 1, "{notifications:#?}");
6976
6977 let rows = approvals(&mut service, json!({}));
6978 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
6979 let row = &rows[0];
6980 assert_eq!(row["id"], "runtime-1/7");
6981 assert_eq!(row["harness"], HarnessId::HERMES);
6982 assert_eq!(row["kind"], "live");
6983 assert_eq!(row["status"], "pending");
6984 assert_eq!(row["subject"], "rm -rf build");
6985 assert_eq!(row["session_id"], "hermes-live-session");
6986 assert_eq!(row["runtime_id"], "hermes-live-session");
6987 assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
6988 assert!(
6989 row["age_ms"].as_i64().is_some_and(|age| age >= 0),
6990 "{row:#}"
6991 );
6992 assert_eq!(
6993 row["options"]
6994 .as_array()
6995 .unwrap()
6996 .iter()
6997 .map(|option| option["id"].as_str().unwrap())
6998 .collect::<Vec<_>>(),
6999 vec!["allow_once", "allow_for_session", "deny"],
7000 );
7001
7002 assert_eq!(
7004 approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7005 .as_array()
7006 .map(Vec::len),
7007 Some(1),
7008 );
7009 assert_eq!(
7010 approvals(&mut service, json!({"session": "some-other-session"}))
7011 .as_array()
7012 .map(Vec::len),
7013 Some(0),
7014 );
7015
7016 let response = service
7017 .handle_async(request(
7018 2,
7019 "harness.v1.runtimes.respond",
7020 json!({
7021 "connection": "runtime-1",
7022 "request_id": 7,
7023 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7024 }),
7025 ))
7026 .await;
7027 assert!(response.get("error").is_none(), "{response:#}");
7028 assert_eq!(
7029 answered
7030 .lock()
7031 .unwrap_or_else(std::sync::PoisonError::into_inner)
7032 .as_slice(),
7033 &[json!({
7034 "request_id": 7,
7035 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7036 })],
7037 );
7038
7039 let rows = approvals(&mut service, json!({}));
7040 assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7041 }
7042
7043 #[test]
7046 fn queued_subagent_approvals_list_through_the_same_door() {
7047 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7048 crate::subagents::QueuedApproval {
7049 child_agent_id: "child-7".into(),
7050 tool: "shell".into(),
7051 subject: Some("cargo publish --dry-run".into()),
7052 queued_at_ms: 1,
7053 outcome: None,
7054 },
7055 crate::subagents::QueuedApproval {
7056 child_agent_id: "child-8".into(),
7057 tool: "write_file".into(),
7058 subject: None,
7059 queued_at_ms: 2,
7060 outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7061 },
7062 ]));
7063 let mut service = HarnessSessionService::new();
7064 service.observe_subagent_approvals(queue);
7065
7066 let rows = approvals(&mut service, json!({}));
7067 assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
7068 assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
7069 assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
7070 assert_eq!(rows[0]["status"], "pending");
7071 assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
7072 assert_eq!(rows[1]["status"], "denied");
7073 assert!(rows[1]["options"].as_array().unwrap().is_empty());
7074
7075 let only = approvals(&mut service, json!({"session": "child-8"}));
7077 assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
7078 assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
7079 }
7080
7081 #[test]
7087 fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
7088 let response = HarnessSessionService::new().handle(request(
7089 1,
7090 "harness.v1.approvals.list",
7091 json!({"harness": "not-a-harness"}),
7092 ));
7093 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7094 assert!(response["error"]["message"]
7095 .as_str()
7096 .unwrap()
7097 .contains("not-a-harness"));
7098 for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
7099 let response = HarnessSessionService::new().handle(request(
7100 1,
7101 "harness.v1.approvals.list",
7102 json!({"harness": harness}),
7103 ));
7104 assert!(response.get("error").is_none(), "{harness}: {response:#}");
7105 }
7106 }
7107
7108 #[test]
7112 fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
7113 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
7114 assert_eq!(
7115 SdkOperation::from_method("harness.v1.approvals.list"),
7116 Some(SdkOperation::ApprovalsList)
7117 );
7118 let registry = harness_support_registry();
7119 for id in [
7120 HarnessId::HERMES,
7121 HarnessId::OPENCLAW,
7122 HarnessId::CODEX,
7123 HarnessId::CLAUDE_CODE,
7126 ] {
7127 let concept = registry
7128 .harnesses
7129 .iter()
7130 .find(|harness| harness.id.as_str() == id)
7131 .unwrap()
7132 .orchestration
7133 .concepts
7134 .iter()
7135 .find(|concept| concept.concept == "pending_request")
7136 .unwrap();
7137 assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
7138 assert!(concept
7139 .methods
7140 .iter()
7141 .any(|method| method == "harness.v1.approvals.list"));
7142 }
7143 }
7144
7145 async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
7148 service
7149 .handle_async(request(3, "harness.v1.approvals.resolve", params))
7150 .await
7151 }
7152
7153 #[tokio::test]
7157 async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
7158 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7159 let mut service = HarnessSessionService::new();
7160 service.runtimes.insert(
7161 "runtime-1".into(),
7162 requesting_runtime(
7163 HarnessId::HERMES,
7164 vec![permission_event(7, "rm -rf build")],
7165 answered.clone(),
7166 ),
7167 );
7168 service.poll_runtimes().await;
7169
7170 let rows = approvals(&mut service, json!({}));
7171 assert_eq!(rows[0]["id"], "runtime-1/7");
7172
7173 let response = resolve(
7174 &mut service,
7175 json!({"id": "runtime-1/7", "decision": "allow_once"}),
7176 )
7177 .await;
7178 assert!(response.get("error").is_none(), "{response:#}");
7179 assert_eq!(
7180 response["result"],
7181 json!({
7182 "id": "runtime-1/7",
7183 "decision": "allow_once",
7184 "option_id": "allow_once",
7185 "resolved": true,
7186 }),
7187 );
7188 assert_eq!(
7190 answered
7191 .lock()
7192 .unwrap_or_else(std::sync::PoisonError::into_inner)
7193 .as_slice(),
7194 &[json!({
7195 "request_id": 7,
7196 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7197 })],
7198 );
7199 assert_eq!(
7201 approvals(&mut service, json!({})).as_array().map(Vec::len),
7202 Some(0),
7203 );
7204 let response = resolve(
7206 &mut service,
7207 json!({"id": "runtime-1/7", "decision": "allow_once"}),
7208 )
7209 .await;
7210 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7211 }
7212
7213 #[tokio::test]
7216 async fn deny_selects_the_requests_own_reject_option() {
7217 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7218 let mut service = HarnessSessionService::new();
7219 service.runtimes.insert(
7220 "runtime-1".into(),
7221 requesting_runtime(
7222 HarnessId::HERMES,
7223 vec![permission_event(11, "git push --force")],
7224 answered.clone(),
7225 ),
7226 );
7227 service.poll_runtimes().await;
7228
7229 let response = resolve(
7230 &mut service,
7231 json!({"id": "runtime-1/11", "decision": "deny"}),
7232 )
7233 .await;
7234 assert!(response.get("error").is_none(), "{response:#}");
7235 assert_eq!(response["result"]["option_id"], "deny");
7237 assert_eq!(
7238 answered
7239 .lock()
7240 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
7241 json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
7242 );
7243 assert_eq!(
7244 approvals(&mut service, json!({})).as_array().map(Vec::len),
7245 Some(0),
7246 );
7247 }
7248
7249 #[tokio::test]
7252 async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
7253 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7254 let mut service = HarnessSessionService::new();
7255 let mut event = permission_event(3, "rm -rf build");
7256 event.payload["params"]["options"] = json!([
7259 {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
7260 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7261 ]);
7262 service.runtimes.insert(
7263 "runtime-1".into(),
7264 requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
7265 );
7266 service.poll_runtimes().await;
7267
7268 let response = resolve(
7269 &mut service,
7270 json!({"id": "runtime-1/3", "decision": "allow_always"}),
7271 )
7272 .await;
7273 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7274 let message = response["error"]["message"].as_str().unwrap();
7275 assert!(message.contains("allow_always"), "{message}");
7276 assert!(message.contains("allow_once, deny"), "{message}");
7277 assert!(answered
7279 .lock()
7280 .unwrap_or_else(std::sync::PoisonError::into_inner)
7281 .is_empty());
7282 assert_eq!(
7283 approvals(&mut service, json!({})).as_array().map(Vec::len),
7284 Some(1),
7285 );
7286 }
7287
7288 #[tokio::test]
7292 async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
7293 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7294 crate::subagents::QueuedApproval {
7295 child_agent_id: "child-7".into(),
7296 tool: "shell".into(),
7297 subject: Some("cargo publish --dry-run".into()),
7298 queued_at_ms: 1,
7299 outcome: None,
7300 },
7301 ]));
7302 let mut service = HarnessSessionService::new();
7303 service.observe_subagent_approvals(queue.clone());
7304 let row = approvals(&mut service, json!({}))[0]["id"]
7305 .as_str()
7306 .unwrap()
7307 .to_string();
7308 assert_eq!(row, "supercode/subagent/child-7/1/0");
7309
7310 let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
7311 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7312 let message = response["error"]["message"].as_str().unwrap();
7313 assert!(message.contains("queued subagent record"), "{message}");
7314 assert!(message.contains("request"), "{message}");
7315 assert!(queue
7317 .lock()
7318 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
7319 .outcome
7320 .is_none());
7321 }
7322
7323 #[tokio::test]
7326 async fn an_unknown_row_and_a_missing_decision_are_both_named() {
7327 let mut service = HarnessSessionService::new();
7328 let response = resolve(
7329 &mut service,
7330 json!({"id": "runtime-9/4", "decision": "deny"}),
7331 )
7332 .await;
7333 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7334 assert!(response["error"]["message"]
7335 .as_str()
7336 .unwrap()
7337 .contains("runtime-9/4"));
7338
7339 let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
7340 let message = response["error"]["message"].as_str().unwrap();
7341 assert!(
7342 message.contains("allow_once | allow_always | deny"),
7343 "{message}"
7344 );
7345
7346 let response = resolve(
7347 &mut service,
7348 json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
7349 )
7350 .await;
7351 assert!(response["error"]["message"]
7352 .as_str()
7353 .unwrap()
7354 .contains("not both"));
7355 }
7356
7357 #[test]
7361 fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
7362 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
7363 assert_eq!(
7364 SdkOperation::from_method("harness.v1.approvals.resolve"),
7365 Some(SdkOperation::ApprovalsResolve)
7366 );
7367 assert_eq!(
7368 SdkOperation::ApprovalsResolve.action_name(),
7369 "approvals_resolve"
7370 );
7371 let registry = harness_support_registry();
7372 for id in [
7373 HarnessId::HERMES,
7374 HarnessId::OPENCLAW,
7375 HarnessId::CODEX,
7376 HarnessId::CLAUDE_CODE,
7379 ] {
7380 let concept = registry
7381 .harnesses
7382 .iter()
7383 .find(|harness| harness.id.as_str() == id)
7384 .unwrap()
7385 .orchestration
7386 .concepts
7387 .iter()
7388 .find(|concept| concept.concept == "pending_request")
7389 .unwrap();
7390 assert_eq!(
7391 concept.controlled,
7392 crate::ImplementationKind::BuiltIn,
7393 "{id}"
7394 );
7395 assert!(
7396 concept
7397 .methods
7398 .iter()
7399 .any(|method| method == "harness.v1.approvals.resolve"),
7400 "{id}"
7401 );
7402 }
7403 }
7404
7405 #[test]
7406 fn capabilities_are_explicit_and_versioned() {
7407 let mut service = HarnessSessionService::new();
7408 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
7409 assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
7410 assert_eq!(
7411 response["result"]["sdk"]["schema_version"],
7412 crate::SDK_SCHEMA_VERSION
7413 );
7414 assert_eq!(
7415 response["result"]["sdk"]["operations"]
7416 .as_array()
7417 .unwrap()
7418 .len(),
7419 SdkOperation::ALL.len()
7420 );
7421 assert_eq!(
7422 response["result"]["harnesses"].as_array().unwrap().len(),
7423 11
7424 );
7425 assert!(response["result"]["harnesses"]
7426 .as_array()
7427 .unwrap()
7428 .iter()
7429 .any(|harness| harness == HarnessId::GROK));
7430 assert!(response["result"]["harnesses"]
7431 .as_array()
7432 .unwrap()
7433 .iter()
7434 .any(|harness| harness == HarnessId::GOOSE));
7435 }
7436
7437 #[test]
7438 fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
7439 let noisy_stderr = crate::HarnessEvent {
7440 sequence: None,
7441 kind: "transport_stderr".into(),
7442 payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
7443 };
7444 assert_eq!(handshake_event_failure(&noisy_stderr), None);
7445
7446 let closed = crate::HarnessEvent {
7447 sequence: None,
7448 kind: "transport_closed".into(),
7449 payload: json!({}),
7450 };
7451 assert!(handshake_event_failure(&closed).is_some());
7452 }
7453
7454 #[tokio::test]
7455 async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
7456 let mut service = HarnessSessionService::new();
7457 service
7458 .runtimes
7459 .insert("raw-eof".into(), ending_runtime(None));
7460 service.runtimes.insert(
7461 "explicit-close".into(),
7462 ending_runtime(Some(HarnessEvent {
7463 sequence: None,
7464 kind: "transport_closed".into(),
7465 payload: json!({"message": "native transport exited"}),
7466 })),
7467 );
7468
7469 let notifications = service.poll_runtimes().await;
7470
7471 assert_eq!(notifications.len(), 2);
7472 assert!(notifications
7473 .iter()
7474 .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
7475 assert!(notifications.iter().all(|notification| {
7476 notification["params"]["session_id"] == "ending-session"
7477 && notification["params"]["connection"].is_string()
7478 }));
7479 let mut sequences = notifications
7480 .iter()
7481 .filter_map(|notification| notification["params"]["sequence"].as_u64())
7482 .collect::<Vec<_>>();
7483 sequences.sort_unstable();
7484 assert_eq!(sequences, vec![1, 2]);
7485 assert!(service.runtimes.is_empty());
7486 }
7487
7488 #[test]
7489 fn support_report_and_grok_default_binding_share_the_registry() {
7490 let mut service = HarnessSessionService::new();
7491 let response = service.handle(request(1, "harness.v1.support.report", json!({})));
7492 assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
7493 let params = RuntimeBackendParams {
7494 harness: HarnessId::from(HarnessId::GROK),
7495 protocol: None,
7496 launch: None,
7497 base_url: None,
7498 policy: RuntimePolicy::Default,
7499 };
7500 let backend = match runtime_backend(¶ms) {
7501 Ok(backend) => backend,
7502 Err(_) => panic!("Grok should bind through its registered ACP launch"),
7503 };
7504 assert_eq!(backend.harness().as_str(), HarnessId::GROK);
7505 assert!(backend.capabilities().start_session);
7506 let registered = harness_support_registry()
7507 .harnesses
7508 .into_iter()
7509 .find(|harness| harness.id.as_str() == HarnessId::GROK)
7510 .and_then(|harness| harness.runtime.default_launch)
7511 .unwrap();
7512 assert!(!registered
7513 .arguments
7514 .iter()
7515 .any(|argument| argument == "--always-approve"));
7516 assert!(runtime_launch(¶ms).is_none());
7517
7518 let yolo = RuntimeBackendParams {
7519 policy: RuntimePolicy::Yolo,
7520 ..params
7521 };
7522 assert!(runtime_launch(&yolo)
7523 .unwrap()
7524 .arguments
7525 .iter()
7526 .any(|argument| argument == "--always-approve"));
7527
7528 let mismatched_protocol = RuntimeBackendParams {
7529 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
7530 protocol: Some("acp".into()),
7531 launch: None,
7532 base_url: None,
7533 policy: RuntimePolicy::Default,
7534 };
7535 assert!(runtime_backend(&mismatched_protocol).is_err());
7536 }
7537
7538 #[test]
7539 fn load_follow_and_unfollow_share_the_same_locator() {
7540 let mut service = HarnessSessionService::new();
7541 let locator = pi_locator();
7542 let loaded = service.handle(request(
7543 1,
7544 "harness.v1.sessions.load",
7545 json!({"locator": locator}),
7546 ));
7547 assert_eq!(
7548 loaded["result"]["session"]["session_id"],
7549 locator.session_id
7550 );
7551
7552 let followed = service.handle(request(
7553 2,
7554 "harness.v1.sessions.follow",
7555 json!({"locator": locator}),
7556 ));
7557 assert_eq!(followed["result"]["subscription"], "sub-1");
7558 assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
7559 assert!(service.poll().is_empty());
7560
7561 let unfollowed = service.handle(request(
7562 3,
7563 "harness.v1.sessions.unfollow",
7564 json!({"subscription": "sub-1"}),
7565 ));
7566 assert_eq!(unfollowed["result"]["removed"], true);
7567 }
7568
7569 #[test]
7570 fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
7571 let temp = std::env::temp_dir().join(format!(
7572 "supercode-bounded-view-{}-{}",
7573 std::process::id(),
7574 generated_session_id()
7575 ));
7576 let path = temp.join("parent.jsonl");
7577 let subagents = temp.join("parent/subagents");
7578 std::fs::create_dir_all(&subagents).unwrap();
7579 let long_last = "x".repeat(300);
7580 let parent_records = [
7581 json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
7582 json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
7583 json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
7584 ];
7585 std::fs::write(
7586 &path,
7587 format!(
7588 "{}\n",
7589 parent_records
7590 .iter()
7591 .map(Value::to_string)
7592 .collect::<Vec<_>>()
7593 .join("\n")
7594 ),
7595 )
7596 .unwrap();
7597 std::fs::write(
7598 subagents.join("agent-child.jsonl"),
7599 concat!(
7600 r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
7601 "\n",
7602 ),
7603 )
7604 .unwrap();
7605 let locator = SessionLocator {
7606 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
7607 session_id: "parent".into(),
7608 storage: StorageLocator::File { path },
7609 };
7610 let mut service = HarnessSessionService::new();
7611
7612 let complete = service.handle(request(
7613 1,
7614 "harness.v1.sessions.load",
7615 json!({"locator": locator}),
7616 ));
7617 assert_eq!(
7618 complete["result"]["session"]["subagents"]
7619 .as_array()
7620 .unwrap()
7621 .len(),
7622 1
7623 );
7624
7625 let bounded = service.handle(request(
7626 2,
7627 "harness.v1.sessions.load",
7628 json!({
7629 "locator": locator,
7630 "view": {
7631 "tail_messages": 1,
7632 "max_message_chars": 256,
7633 "include_subagents": false
7634 },
7635 }),
7636 ));
7637 let session = &bounded["result"]["session"];
7638 assert!(session["subagents"].as_array().unwrap().is_empty());
7639 assert_eq!(session["messages"].as_array().unwrap().len(), 1);
7640 assert_eq!(
7641 session["messages"][0]["content"],
7642 format!("{}\n…", "x".repeat(256))
7643 );
7644
7645 let followed = service.handle(request(
7646 3,
7647 "harness.v1.sessions.follow",
7648 json!({
7649 "locator": locator,
7650 "view": {
7651 "tail_messages": 1,
7652 "max_message_chars": 256,
7653 "include_subagents": false
7654 },
7655 }),
7656 ));
7657 let initial = &followed["result"]["initial"]["session"];
7658 assert!(initial["subagents"].as_array().unwrap().is_empty());
7659 assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
7660
7661 let _ = std::fs::remove_dir_all(&temp);
7662 }
7663
7664 #[test]
7665 fn forty_megabyte_display_load_is_bounded_and_prompt() {
7666 let temp = std::env::temp_dir().join(format!(
7667 "supercode-large-display-view-{}-{}",
7668 std::process::id(),
7669 generated_session_id()
7670 ));
7671 std::fs::create_dir_all(&temp).unwrap();
7672 let path = temp.join("rollout.jsonl");
7673 let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
7674 writeln!(
7675 file,
7676 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
7677 )
7678 .unwrap();
7679 let padding = "x".repeat(80 * 1024);
7680 for index in 0..512 {
7681 let marker = if index == 0 {
7682 "OLDEST-SHOULD-NOT-LOAD"
7683 } else if index == 511 {
7684 "LATEST-MUST-LOAD"
7685 } else {
7686 "bulk"
7687 };
7688 writeln!(
7689 file,
7690 "{}",
7691 json!({
7692 "timestamp": "2026-01-01T00:00:01Z",
7693 "type": "response_item",
7694 "payload": {
7695 "type": "message",
7696 "role": "assistant",
7697 "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
7698 },
7699 })
7700 )
7701 .unwrap();
7702 }
7703 file.flush().unwrap();
7704 drop(file);
7705 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
7706
7707 let locator = SessionLocator {
7708 harness: HarnessId::from(HarnessId::CODEX),
7709 session_id: "large-display".into(),
7710 storage: StorageLocator::File { path },
7711 };
7712 let started = Instant::now();
7713 let response = HarnessSessionService::new().handle(request(
7714 1,
7715 "harness.v1.sessions.load",
7716 json!({
7717 "locator": locator,
7718 "view": {
7719 "tail_messages": 500,
7720 "max_message_chars": 1024,
7721 "include_subagents": false,
7722 "display_history": true,
7723 },
7724 }),
7725 ));
7726 let elapsed = started.elapsed();
7727 let wire = response.to_string();
7728 eprintln!(
7729 "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
7730 wire.len()
7731 );
7732 assert!(response.get("error").is_none(), "{response:#}");
7733 assert!(wire.contains("LATEST-MUST-LOAD"));
7734 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
7735 assert!(
7736 wire.len() < 2 * 1024 * 1024,
7737 "bounded wire was {} bytes",
7738 wire.len()
7739 );
7740 assert!(
7741 elapsed.as_secs_f64() < 3.0,
7742 "bounded 40 MiB load took {elapsed:?}"
7743 );
7744
7745 let _ = std::fs::remove_dir_all(&temp);
7746 }
7747
7748 #[test]
7749 fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
7750 let temp = std::env::temp_dir().join(format!(
7751 "supercode-large-goose-view-{}-{}",
7752 std::process::id(),
7753 generated_session_id()
7754 ));
7755 std::fs::create_dir_all(&temp).unwrap();
7756 let path = temp.join("sessions.db");
7757 let connection = rusqlite::Connection::open(&path).unwrap();
7758 connection
7759 .execute_batch(
7760 "CREATE TABLE sessions (
7761 id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
7762 created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
7763 session_type TEXT NOT NULL, extension_data TEXT,
7764 goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
7765 archived_at TEXT
7766 );
7767 CREATE TABLE messages (
7768 id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
7769 role TEXT NOT NULL, content_json TEXT NOT NULL,
7770 created_timestamp INTEGER NOT NULL, metadata_json TEXT
7771 );",
7772 )
7773 .unwrap();
7774 connection
7775 .execute(
7776 "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
7777 rusqlite::params![
7778 "goose-large",
7779 "Large Goose session",
7780 "/tmp",
7781 "2026-01-01 00:00:00",
7782 "2026-01-01 00:00:02",
7783 "user",
7784 "{}",
7785 "auto",
7786 "anthropic",
7787 r#"{"model_name":"claude-sonnet"}"#,
7788 ],
7789 )
7790 .unwrap();
7791 let old_content = serde_json::to_string(&vec![json!({
7792 "type": "text",
7793 "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
7794 })])
7795 .unwrap();
7796 connection
7797 .execute(
7798 "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
7799 rusqlite::params!["goose-large", old_content],
7800 )
7801 .unwrap();
7802 connection
7803 .execute(
7804 "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
7805 rusqlite::params![
7806 "goose-large",
7807 r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
7808 ],
7809 )
7810 .unwrap();
7811 drop(connection);
7812 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
7813
7814 let locator = SessionLocator {
7815 harness: HarnessId::from(HarnessId::GOOSE),
7816 session_id: "goose-large".into(),
7817 storage: StorageLocator::Sqlite {
7818 path,
7819 selector: "goose-large".into(),
7820 },
7821 };
7822 let started = Instant::now();
7823 let response = HarnessSessionService::new().handle(request(
7824 1,
7825 "harness.v1.sessions.load",
7826 json!({
7827 "locator": locator,
7828 "view": {
7829 "tail_messages": 1,
7830 "max_message_chars": 1024,
7831 "include_subagents": false,
7832 "display_history": true,
7833 },
7834 }),
7835 ));
7836 let elapsed = started.elapsed();
7837 let wire = response.to_string();
7838 eprintln!(
7839 "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
7840 wire.len()
7841 );
7842 assert!(response.get("error").is_none(), "{response:#}");
7843 assert!(wire.contains("LATEST-MUST-LOAD"));
7844 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
7845 assert!(
7846 wire.len() < 64 * 1024,
7847 "bounded wire was {} bytes",
7848 wire.len()
7849 );
7850 assert!(
7851 elapsed.as_secs_f64() < 1.0,
7852 "bounded Goose load took {elapsed:?}"
7853 );
7854
7855 let _ = std::fs::remove_dir_all(&temp);
7856 }
7857
7858 #[test]
7859 fn display_view_keeps_codex_assistant_history_across_compaction() {
7860 let temp = std::env::temp_dir().join(format!(
7861 "supercode-codex-display-view-{}-{}",
7862 std::process::id(),
7863 generated_session_id()
7864 ));
7865 std::fs::create_dir_all(&temp).unwrap();
7866 let path = temp.join("rollout.jsonl");
7867 std::fs::write(
7868 &path,
7869 concat!(
7870 r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
7871 "\n",
7872 r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
7873 "\n",
7874 r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
7875 "\n",
7876 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"}]}}"#,
7877 "\n",
7878 r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
7879 "\n",
7880 r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
7881 "\n",
7882 ),
7883 )
7884 .unwrap();
7885 let locator = SessionLocator {
7886 harness: HarnessId::from(HarnessId::CODEX),
7887 session_id: "codex-display".into(),
7888 storage: StorageLocator::File { path },
7889 };
7890 let mut service = HarnessSessionService::new();
7891
7892 let continuation = service.handle(request(
7893 1,
7894 "harness.v1.sessions.load",
7895 json!({"locator": locator}),
7896 ));
7897 let continuation_text = continuation["result"]["session"]["messages"].to_string();
7898 assert!(!continuation_text.contains("old answer"));
7899
7900 let display = service.handle(request(
7901 2,
7902 "harness.v1.sessions.load",
7903 json!({
7904 "locator": locator,
7905 "view": {
7906 "tail_messages": 10,
7907 "include_subagents": false,
7908 "display_history": true,
7909 },
7910 }),
7911 ));
7912 let display_text = display["result"]["session"]["messages"].to_string();
7913 assert!(display_text.contains("old prompt"));
7914 assert!(display_text.contains("old answer"));
7915 assert!(display_text.contains("new prompt"));
7916 assert!(display_text.contains("new answer"));
7917
7918 let _ = std::fs::remove_dir_all(&temp);
7919 }
7920
7921 #[test]
7922 fn indexed_claude_windows_match_the_existing_wire_projection() {
7923 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7924 .join("tests/fixtures/claude_code_session.jsonl");
7925 let locator = SessionLocator {
7926 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
7927 session_id: "fixture".into(),
7928 storage: StorageLocator::File { path },
7929 };
7930 let full = load_session(&locator).unwrap();
7931 for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
7932 for offset in [0, 1, full.messages.len(), usize::MAX] {
7933 for limit in [0, 1, 3, usize::MAX] {
7934 let options = SessionLoadOptions {
7935 include_subagents: Some(false),
7936 inline_media,
7937 message_offset: Some(offset),
7938 message_limit: Some(limit),
7939 ..Default::default()
7940 };
7941 let expected = projected_session_result(&full, &options);
7942 assert_eq!(
7943 indexed_claude_window(&locator, &options).unwrap().unwrap(),
7944 expected
7945 );
7946 }
7947 }
7948 for tail in [0, 1, 3, usize::MAX] {
7949 let options = SessionLoadOptions {
7950 include_subagents: Some(false),
7951 inline_media,
7952 message_tail: Some(tail),
7953 ..Default::default()
7954 };
7955 assert_eq!(
7956 indexed_claude_window(&locator, &options).unwrap().unwrap(),
7957 projected_session_result(&full, &options)
7958 );
7959 }
7960 }
7961 }
7962
7963 #[test]
7964 fn load_supports_bounded_windows_and_media_metadata() {
7965 let mut service = HarnessSessionService::new();
7966 let locator = pi_locator();
7967 let bounded = service.handle(request(
7968 1,
7969 "harness.v1.sessions.load",
7970 json!({
7971 "locator": locator,
7972 "options": {
7973 "include_subagents": false,
7974 "message_limit": 2,
7975 "message_offset": 1
7976 }
7977 }),
7978 ));
7979 assert_eq!(bounded["result"]["window"]["offset"], 1);
7980 assert_eq!(bounded["result"]["window"]["returned"], 2);
7981 assert!(bounded["result"]["summary"]["first_message"].is_object());
7982 assert!(bounded["result"]["summary"]["last_message"].is_object());
7983 assert_eq!(
7984 bounded["result"]["session"]["messages"]
7985 .as_array()
7986 .unwrap()
7987 .len(),
7988 2
7989 );
7990 assert!(bounded["result"]["session"]["subagents"]
7991 .as_array()
7992 .unwrap()
7993 .is_empty());
7994
7995 let tail = service.handle(request(
7996 2,
7997 "harness.v1.sessions.load",
7998 json!({"locator": locator, "options": {"message_tail": 1}}),
7999 ));
8000 assert_eq!(tail["result"]["window"]["returned"], 1);
8001 assert_eq!(tail["result"]["window"]["has_more"], true);
8002 assert_eq!(tail["result"]["window"]["has_older"], true);
8003 assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8004 assert!(tail["result"]["summary"]["first_message"].is_object());
8005
8006 let metadata_only = service.handle(request(
8007 3,
8008 "harness.v1.sessions.load",
8009 json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8010 ));
8011 assert!(metadata_only["result"]["session"]
8012 .to_string()
8013 .contains("media_reference"));
8014 assert!(!metadata_only["result"]["session"]
8015 .to_string()
8016 .contains("data:image/"));
8017 }
8018
8019 #[test]
8020 fn import_translate_branch_and_handoff_use_typed_artifacts() {
8021 let mut service = HarnessSessionService::new();
8022 let locator = pi_locator();
8023 let translated = service.handle(request(
8024 1,
8025 "harness.v1.sessions.translate",
8026 json!({"locator": locator, "target_harness": "grok"}),
8027 ));
8028 assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8029 assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8030 assert!(translated["result"]["artifact"]["content"]
8031 .as_str()
8032 .is_some_and(|content| !content.is_empty()));
8033
8034 for target in ["opencode", "open-code"] {
8035 let opencode = service.handle(request(
8036 6,
8037 "harness.v1.sessions.translate",
8038 json!({"locator": locator, "target_harness": target}),
8039 ));
8040 assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8041 }
8042 let goose = service.handle(request(
8043 7,
8044 "harness.v1.sessions.translate",
8045 json!({"locator": locator, "target_harness": "goose"}),
8046 ));
8047 assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8048 assert!(serde_json::from_str::<Value>(
8049 goose["result"]["artifact"]["content"].as_str().unwrap()
8050 )
8051 .unwrap()["conversation"]
8052 .is_array());
8053
8054 let imported = service.handle(request(
8055 2,
8056 "harness.v1.sessions.import",
8057 json!({
8058 "source_harness": "grok",
8059 "content": translated["result"]["artifact"]["content"],
8060 }),
8061 ));
8062 assert_eq!(imported["result"]["session"]["source"], "grok");
8063
8064 let branched = service.handle(request(
8065 3,
8066 "harness.v1.sessions.branch",
8067 json!({"locator": locator, "target_harness": "codex"}),
8068 ));
8069 assert_eq!(branched["result"]["parent"]["harness"], "pi");
8070 assert!(branched["result"]["bootstrap_prompt"]
8071 .as_str()
8072 .unwrap()
8073 .contains("frozen parent transcript"));
8074 assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
8075
8076 let handoff = service.handle(request(
8077 4,
8078 "harness.v1.sessions.handoff",
8079 json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
8080 ));
8081 assert_eq!(handoff["result"]["launch"]["program"], "pi");
8082 assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
8083 assert_eq!(handoff["result"]["requires_materialization"], true);
8084
8085 let goose_handoff = service.handle(request(
8086 8,
8087 "harness.v1.sessions.handoff",
8088 json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
8089 ));
8090 assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
8091 assert_eq!(
8092 goose_handoff["result"]["materialize"]["arguments"],
8093 json!(["session", "import", "{artifact_path}"])
8094 );
8095
8096 let resumed = service.handle(request(
8097 5,
8098 "harness.v1.sessions.resume_instructions",
8099 json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
8100 ));
8101 assert_eq!(resumed["result"]["launch"]["program"], "pi");
8102 assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
8103 }
8104
8105 #[test]
8106 fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
8107 let temp = std::env::temp_dir().join(format!(
8108 "supercode-service-reduce-{}-{}",
8109 std::process::id(),
8110 generated_session_id()
8111 ));
8112 let source_path = temp.join("source.jsonl");
8113 let store_root = temp.join("store");
8114 std::fs::create_dir_all(&temp).unwrap();
8115
8116 let mut records = vec![json!({
8117 "timestamp": "2026-01-01T00:00:00Z",
8118 "type": "session_meta",
8119 "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
8120 })];
8121 for turn in 0..16 {
8122 records.push(json!({
8123 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
8124 "type": "response_item",
8125 "payload": {
8126 "type": "message",
8127 "role": "user",
8128 "content": [{
8129 "type": "input_text",
8130 "text": format!("request {turn}: {}", "context ".repeat(80)),
8131 }],
8132 },
8133 }));
8134 records.push(json!({
8135 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
8136 "type": "response_item",
8137 "payload": {
8138 "type": "message",
8139 "role": "assistant",
8140 "content": [{
8141 "type": "output_text",
8142 "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
8143 }],
8144 },
8145 }));
8146 }
8147 let source = format!(
8148 "{}\n",
8149 records
8150 .iter()
8151 .map(Value::to_string)
8152 .collect::<Vec<_>>()
8153 .join("\n")
8154 );
8155 std::fs::write(&source_path, &source).unwrap();
8156 let locator = SessionLocator {
8157 harness: HarnessId::from(HarnessId::CODEX),
8158 session_id: "codex-reduce".into(),
8159 storage: StorageLocator::File {
8160 path: source_path.clone(),
8161 },
8162 };
8163 let original = load_session(&locator).unwrap();
8164 let mut service =
8165 HarnessSessionService::new().with_reduction_store_root(store_root.clone());
8166
8167 let response = service.handle(request(
8168 1,
8169 "harness.v1.sessions.reduce",
8170 json!({
8171 "locator": locator,
8172 "target_harness": "claude-code",
8173 "keep_last": 4,
8174 }),
8175 ));
8176 assert!(response.get("error").is_none(), "{response:#}");
8177 let receipt = &response["result"]["receipt"];
8178 assert_eq!(receipt["source_harness"], "codex");
8179 assert_eq!(receipt["target_harness"], "claude-code");
8180 assert_eq!(receipt["verified"], true);
8181 assert_eq!(receipt["reversible"], true);
8182 assert!(receipt["reductions"].as_u64().unwrap() > 0);
8183 assert!(
8184 receipt["source_tokens"].as_u64().unwrap()
8185 > receipt["reduced_tokens"].as_u64().unwrap()
8186 );
8187 assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
8188 assert!(response["result"]["bootstrap_prompt"]
8189 .as_str()
8190 .unwrap()
8191 .contains("Do not guess hidden content"));
8192
8193 let rescue_id = receipt["id"].as_str().unwrap();
8194 let store = crate::SessionStore::open(&store_root).unwrap();
8195 let sidecar =
8196 Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
8197 let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
8198 let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
8199 let policy = reduce::ReductionPolicy {
8200 clear_turns_older_than: Some(4),
8201 ..Default::default()
8202 };
8203 let (restamped_view, reapplied_log) =
8204 reduce::project_messages(&sidecar.messages, &policy, &log);
8205 assert_eq!(
8206 messages_jsonl(&persisted_view).unwrap(),
8207 messages_jsonl(&restamped_view).unwrap()
8208 );
8209 assert_eq!(reapplied_log, log);
8210 reduce::verify_log(&log, &sidecar).unwrap();
8211 assert_eq!(
8212 reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
8213 original.messages
8214 );
8215 assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
8216
8217 std::fs::remove_dir_all(temp).ok();
8218 }
8219
8220 #[test]
8221 fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
8222 let temp = std::env::temp_dir().join(format!(
8223 "supercode-severed-view-{}-{}",
8224 std::process::id(),
8225 generated_session_id()
8226 ));
8227 std::fs::create_dir_all(&temp).unwrap();
8228 let path = temp.join("severed.jsonl");
8229 std::fs::write(
8232 &path,
8233 concat!(
8234 r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
8235 "\n",
8236 r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
8237 "\n",
8238 ),
8239 )
8240 .unwrap();
8241 let locator = SessionLocator {
8242 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8243 session_id: "severed".into(),
8244 storage: StorageLocator::File { path },
8245 };
8246 let mut service = HarnessSessionService::new();
8247
8248 let viewed = service.handle(request(
8249 1,
8250 "harness.v1.sessions.load",
8251 json!({"locator": locator}),
8252 ));
8253 let session = &viewed["result"]["session"];
8254 assert_eq!(session["fidelity"], "semantic");
8255 assert_eq!(session["messages"].as_array().unwrap().len(), 2);
8256 assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
8257 entry
8258 .as_str()
8259 .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
8260 }));
8261
8262 let strict = service.handle(request(
8265 2,
8266 "harness.v1.sessions.load",
8267 json!({"locator": locator, "fidelity": "byte_lossless"}),
8268 ));
8269 assert!(strict["error"]["message"]
8270 .as_str()
8271 .unwrap()
8272 .contains("cannot reconstruct lossless Claude continuation"));
8273
8274 let translated = service.handle(request(
8276 3,
8277 "harness.v1.sessions.translate",
8278 json!({"locator": locator, "target_harness": "codex"}),
8279 ));
8280 assert!(translated["error"]["message"]
8281 .as_str()
8282 .unwrap()
8283 .contains("cannot reconstruct lossless Claude continuation"));
8284 let resumed = service.handle(request(
8285 4,
8286 "harness.v1.sessions.resume_instructions",
8287 json!({"locator": locator}),
8288 ));
8289 assert!(resumed["error"]["message"]
8290 .as_str()
8291 .unwrap()
8292 .contains("cannot reconstruct lossless Claude continuation"));
8293
8294 let _ = std::fs::remove_dir_all(&temp);
8295 }
8296
8297 #[test]
8298 fn structured_resume_launches_cover_gemini_goose_and_supercode() {
8299 let codex = resume_launch(
8300 HarnessId::CODEX,
8301 "codex-session",
8302 Path::new("/tmp/project"),
8303 ResumePolicy::Yolo,
8304 )
8305 .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
8306 assert_eq!(codex.program, "codex");
8307 assert_eq!(
8308 codex.arguments,
8309 [
8310 "-c",
8311 "check_for_update_on_startup=false",
8312 "-c",
8313 "projects.\"/tmp/project\".trust_level=\"trusted\"",
8314 "--dangerously-bypass-approvals-and-sandbox",
8315 "--dangerously-bypass-hook-trust",
8316 "resume",
8317 "codex-session",
8318 ]
8319 );
8320
8321 let gemini = resume_launch(
8322 HarnessId::GEMINI,
8323 "gemini-session",
8324 Path::new("/tmp/project"),
8325 ResumePolicy::Yolo,
8326 )
8327 .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
8328 assert_eq!(gemini.program, "gemini");
8329 assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
8330
8331 let goose = resume_launch(
8332 HarnessId::GOOSE,
8333 "goose-session",
8334 Path::new("/tmp/project"),
8335 ResumePolicy::Yolo,
8336 )
8337 .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
8338 assert_eq!(goose.program, "goose");
8339 assert_eq!(
8340 goose.arguments,
8341 ["session", "--resume", "--session-id", "goose-session"]
8342 );
8343
8344 let supercode = resume_launch(
8345 HarnessId::SUPERCODE,
8346 "supercode-session",
8347 Path::new("/tmp/project"),
8348 ResumePolicy::Yolo,
8349 )
8350 .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
8351 assert_eq!(supercode.program, "supercode");
8352 assert_eq!(
8353 supercode.arguments,
8354 ["--dangerous", "resume", "supercode-session"]
8355 );
8356 }
8357
8358 #[test]
8359 fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
8360 let temp = std::env::temp_dir().join(format!(
8361 "supercode-harness-artifact-{}-{}",
8362 std::process::id(),
8363 generated_session_id()
8364 ));
8365 let main_path = temp.join("parent.jsonl");
8366 let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
8367 std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
8368 let fixture = std::fs::read_to_string(
8369 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8370 .join("tests/fixtures/claude_code_session.jsonl"),
8371 )
8372 .unwrap();
8373 let parent = fixture.trim_end_matches('\n');
8374 let child = fixture.trim_end_matches('\n');
8375 std::fs::write(&main_path, parent).unwrap();
8376 std::fs::write(&subagent_path, child).unwrap();
8377 let locator = SessionLocator {
8378 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8379 session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
8380 storage: StorageLocator::File {
8381 path: main_path.clone(),
8382 },
8383 };
8384 let mut service = HarnessSessionService::new();
8385 let claude = service.handle(request(
8386 1,
8387 "harness.v1.sessions.translate",
8388 json!({"locator": locator, "target_harness": "claude-code"}),
8389 ));
8390 let artifact = &claude["result"]["artifact"];
8391 assert_eq!(artifact["fidelity"], "byte_lossless");
8392 assert_eq!(artifact["content"], parent);
8393 let files = artifact["files"].as_array().unwrap();
8394 assert!(files.iter().any(|file| {
8395 file["role"] == "subagent"
8396 && file["path"]
8397 .as_str()
8398 .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
8399 && file["content"] == child
8400 }));
8401 assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
8402
8403 let grok = service.handle(request(
8404 2,
8405 "harness.v1.sessions.translate",
8406 json!({"locator": grok_locator(), "target_harness": "grok"}),
8407 ));
8408 let files = grok["result"]["artifact"]["files"].as_array().unwrap();
8409 for name in ["summary.json", "updates.jsonl"] {
8410 let expected = std::fs::read_to_string(
8411 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8412 .join("tests/fixtures/grok_session")
8413 .join(name),
8414 )
8415 .unwrap();
8416 assert!(files.iter().any(|file| {
8417 file["path"] == name && file["role"] == "bundle" && file["content"] == expected
8418 }));
8419 }
8420 std::fs::remove_dir_all(temp).ok();
8421 }
8422
8423 #[test]
8424 fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
8425 let mut service = HarnessSessionService::new();
8426 let source = pi_locator();
8427 for (target, format) in [
8428 ("claude-code", SessionFormat::ClaudeCode),
8429 ("codex", SessionFormat::Codex),
8430 ("opencode", SessionFormat::OpenCode),
8431 ("pi", SessionFormat::Pi),
8432 ] {
8433 let result = service.handle(request(
8434 1,
8435 "harness.v1.sessions.handoff",
8436 json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
8437 ));
8438 let artifact = &result["result"]["artifact"];
8439 let target_id = artifact["session_id"].as_str().unwrap();
8440 assert_ne!(target_id, source.session_id, "{target}");
8441 let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
8442 assert_eq!(
8443 parsed.meta.session_id.as_deref(),
8444 Some(target_id),
8445 "{target}"
8446 );
8447 if target != "pi" {
8448 assert!(result["result"]["launch"]["arguments"]
8449 .as_array()
8450 .unwrap()
8451 .iter()
8452 .any(|argument| argument == target_id));
8453 }
8454 if target == "opencode" {
8455 assert!(target_id.starts_with("ses_"));
8456 fn assert_session_ids(value: &Value, target_id: &str) {
8457 match value {
8458 Value::Object(fields) => {
8459 if let Some(session_id) = fields.get("sessionID") {
8460 assert_eq!(session_id, target_id);
8461 }
8462 for child in fields.values() {
8463 assert_session_ids(child, target_id);
8464 }
8465 }
8466 Value::Array(values) => {
8467 for child in values {
8468 assert_session_ids(child, target_id);
8469 }
8470 }
8471 _ => {}
8472 }
8473 }
8474 let document: Value =
8475 serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
8476 assert_session_ids(&document, target_id);
8477 }
8478 }
8479
8480 let first = service.handle(request(
8481 2,
8482 "harness.v1.sessions.handoff",
8483 json!({"locator": source, "target_harness": "codex"}),
8484 ));
8485 let second = service.handle(request(
8486 3,
8487 "harness.v1.sessions.handoff",
8488 json!({"locator": source, "target_harness": "codex"}),
8489 ));
8490 assert_ne!(
8491 first["result"]["artifact"]["session_id"],
8492 second["result"]["artifact"]["session_id"]
8493 );
8494 }
8495
8496 #[test]
8497 fn grok_handoff_uses_the_official_importer_contract() {
8498 let mut service = HarnessSessionService::new();
8499 let source = opencode_locator();
8500 let response = service.handle(request(
8501 1,
8502 "harness.v1.sessions.handoff",
8503 json!({
8504 "locator": source,
8505 "target_harness": "grok",
8506 "cwd": "/tmp/grok-handoff-project",
8507 }),
8508 ));
8509 let result = &response["result"];
8510
8511 assert_eq!(result["artifact"]["target_harness"], "claude-code");
8515 assert!(result["artifact"]["suggested_filename"]
8516 .as_str()
8517 .unwrap()
8518 .ends_with(".grok-import.claude-code.jsonl"));
8519 let artifact = Session::load_str(
8520 result["artifact"]["content"].as_str().unwrap(),
8521 SessionFormat::ClaudeCode,
8522 )
8523 .unwrap();
8524 assert_eq!(
8525 artifact.meta.cwd.as_deref(),
8526 Some(Path::new("/tmp/grok-handoff-project"))
8527 );
8528 let target_session_id = artifact.meta.session_id.as_deref().unwrap();
8529 assert_eq!(target_session_id.len(), 36);
8530 assert_eq!(target_session_id.as_bytes()[14], b'4');
8531 assert_ne!(target_session_id, opencode_locator().session_id);
8532 assert_eq!(
8533 result["artifact"]["session_id"],
8534 artifact.meta.session_id.as_deref().unwrap()
8535 );
8536
8537 assert_eq!(
8538 result["materialize"]["arguments"],
8539 json!(["import", "--json", "{artifact_path}"])
8540 );
8541 assert_eq!(
8542 result["launch"]["arguments"],
8543 json!(["--resume", "{imported_session_id}", "--fork-session"])
8544 );
8545 assert!(result["note"]
8546 .as_str()
8547 .unwrap()
8548 .contains("outcome=imported"));
8549 assert!(!result["launch"]["arguments"]
8550 .as_array()
8551 .unwrap()
8552 .iter()
8553 .any(|argument| argument == &opencode_locator().session_id));
8554 }
8555
8556 #[tokio::test]
8557 async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
8558 let mut service = HarnessSessionService::new();
8559 let inventory = service
8560 .handle_async(request(
8561 1,
8562 "harness.v1.harnesses.list",
8563 json!({"harnesses": ["missing"]}),
8564 ))
8565 .await;
8566 assert_eq!(inventory["error"]["code"], -32602);
8567
8568 let attached = service
8569 .handle_async(request(
8570 2,
8571 "harness.v1.runtimes.attach_existing",
8572 json!({"harness": "codex", "runtime_id": "thread-1"}),
8573 ))
8574 .await;
8575 assert_eq!(attached["error"]["code"], -32000);
8576 assert!(attached["error"]["message"]
8577 .as_str()
8578 .unwrap()
8579 .contains("runtimes.resume"));
8580 }
8581
8582 #[test]
8583 fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
8584 let mut service = HarnessSessionService::new();
8585 let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
8586 assert_eq!(invalid["error"]["code"], -32602);
8587 let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
8588 assert_eq!(unknown["error"]["code"], -32601);
8589 }
8590
8591 #[cfg(unix)]
8592 #[tokio::test]
8593 #[allow(clippy::await_holding_lock)]
8596 async fn async_service_drives_a_generic_acp_runtime() {
8597 let _environment_guard = crate::live_runtime::test_environment_lock();
8598 let script = r#"
8599 i=0
8600 while IFS= read -r line; do
8601 i=$((i + 1))
8602 case "$i" in
8603 1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
8604 2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
8605 3)
8606 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
8607 printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
8608 ;;
8609 4)
8610 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
8611 printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
8612 ;;
8613 esac
8614 done
8615 "#;
8616 let mut service = HarnessSessionService::new();
8617 let started = service
8618 .handle_async(request(
8619 1,
8620 "harness.v1.runtimes.start",
8621 json!({
8622 "harness": "codex",
8623 "protocol": "acp",
8624 "cwd": std::env::current_dir().unwrap(),
8625 "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
8626 }),
8627 ))
8628 .await;
8629 assert_eq!(started["result"]["connection"], "runtime-1");
8630 assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
8631
8632 let terminal = service
8633 .handle_async(request(
8634 9,
8635 "harness.v1.runtimes.terminal_instructions",
8636 json!({"connection":"runtime-1"}),
8637 ))
8638 .await;
8639 let arguments = terminal["result"]["launch"]["arguments"]
8640 .as_array()
8641 .expect("hosted runtime should return terminal arguments");
8642 let endpoint_index = arguments
8643 .iter()
8644 .position(|value| value == "--endpoint")
8645 .expect("terminal command should use an opaque endpoint");
8646 let endpoint = LiveRuntimeEndpoint::parse(
8647 arguments[endpoint_index + 1]
8648 .as_str()
8649 .expect("endpoint argument should be text"),
8650 )
8651 .unwrap();
8652 assert!(!terminal.to_string().contains("Bearer"));
8653 let workspace = std::env::current_dir().unwrap();
8654 let receipt = resolve_live_runtime(
8655 &endpoint,
8656 &LiveRuntimeSource {
8657 harness: "codex".into(),
8658 session_id: "svc_acp".into(),
8659 workspace,
8660 },
8661 )
8662 .unwrap();
8663 let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
8664 .await
8665 .unwrap();
8666 let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
8667 .await
8668 .unwrap();
8669
8670 let sent = service
8671 .handle_async(request(
8672 2,
8673 "harness.v1.runtimes.send_input",
8674 json!({"connection": "runtime-1", "text": "hi"}),
8675 ))
8676 .await;
8677 assert_eq!(sent["result"]["turn_id"], "3");
8678
8679 let mut events = Vec::new();
8680 for _ in 0..20 {
8681 events.extend(service.poll_runtimes().await);
8682 if events.len() >= 2 {
8683 break;
8684 }
8685 tokio::time::sleep(Duration::from_millis(2)).await;
8686 }
8687 assert!(events
8688 .iter()
8689 .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
8690 assert!(events.iter().any(|event| {
8691 event["params"]["event"]["kind"] == "supercode/acp_request_completed"
8692 }));
8693
8694 let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
8695 loop {
8696 let event = attachment.next_event().await.unwrap();
8697 if event.kind == "text_delta" && event.payload["text"] == "ok" {
8698 break;
8699 }
8700 }
8701 })
8702 .await;
8703 assert!(
8704 saw_editor_reply.is_ok(),
8705 "terminal should observe the editor-driven turn"
8706 );
8707
8708 crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
8709 .await
8710 .unwrap();
8711 let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
8712 loop {
8713 let event = attachment.next_event().await.unwrap();
8714 if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
8715 break;
8716 }
8717 }
8718 })
8719 .await;
8720 assert!(
8721 saw_terminal_reply.is_ok(),
8722 "terminal should drive the same runtime"
8723 );
8724
8725 let closed = service
8726 .handle_async(request(
8727 3,
8728 "harness.v1.runtimes.close",
8729 json!({"connection": "runtime-1"}),
8730 ))
8731 .await;
8732 assert_eq!(closed["result"]["closed"], true);
8733 }
8734
8735 #[test]
8740 fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
8741 let home = connect_scratch_home("uni7-running");
8742
8743 assert!(probe_hermes_running(&home, 300_000).is_none());
8750
8751 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
8754 let port = listener.local_addr().unwrap().port();
8755 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
8756 std::fs::write(
8757 home.join(".openclaw/openclaw.json"),
8758 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
8759 )
8760 .unwrap();
8761 let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
8762 assert!(matches!(
8763 running.method,
8764 RunningInstanceMethod::GatewayConnect
8765 ));
8766 assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
8767 drop(listener);
8768 let mut closed_detected = probe_openclaw_running(&home).is_some();
8773 for _ in 0..3 {
8774 if !closed_detected {
8775 break;
8776 }
8777 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
8778 let port = listener.local_addr().unwrap().port();
8779 drop(listener);
8780 std::fs::write(
8781 home.join(".openclaw/openclaw.json"),
8782 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
8783 )
8784 .unwrap();
8785 closed_detected = probe_openclaw_running(&home).is_some();
8786 }
8787 assert!(
8788 !closed_detected,
8789 "a closed gateway must not read as running"
8790 );
8791
8792 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
8794 let port = listener.local_addr().unwrap().port();
8795 std::fs::write(
8796 home.join(".openclaw/openclaw.json"),
8797 format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
8798 )
8799 .unwrap();
8800 assert!(probe_openclaw_running(&home).is_some());
8801 drop(listener);
8802
8803 std::fs::create_dir_all(home.join(".hermes")).unwrap();
8805 let wal = home.join(".hermes/state.db-wal");
8806 std::fs::write(&wal, b"wal").unwrap();
8807 let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
8808 assert!(matches!(
8809 running.method,
8810 RunningInstanceMethod::StoreWalActivity
8811 ));
8812 assert!(running.evidence.contains("state.db-wal"));
8813 let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
8814 std::fs::File::options()
8815 .append(true)
8816 .open(&wal)
8817 .unwrap()
8818 .set_modified(stale)
8819 .unwrap();
8820 assert!(
8821 probe_hermes_running(&home, 300_000).is_none(),
8822 "a stale WAL (crash leftover) must not read as running"
8823 );
8824 }
8825
8826 fn connect_scratch_home(tag: &str) -> PathBuf {
8827 let dir = std::env::temp_dir().join(format!(
8828 "supercode-connect-service-{tag}-{}-{}",
8829 std::process::id(),
8830 std::time::SystemTime::now()
8831 .duration_since(std::time::UNIX_EPOCH)
8832 .unwrap()
8833 .as_nanos()
8834 ));
8835 std::fs::create_dir_all(&dir).unwrap();
8836 dir
8837 }
8838
8839 async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
8843 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
8844 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
8845 let address = listener.local_addr().unwrap();
8846 let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
8847 tokio::spawn(async move {
8848 loop {
8849 let Ok((mut stream, _)) = listener.accept().await else {
8850 break;
8851 };
8852 let request_sender = request_sender.clone();
8853 tokio::spawn(async move {
8854 let (reader, mut writer) = stream.split();
8855 let mut reader = BufReader::new(reader);
8856 let mut request_line = String::new();
8857 if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
8858 return;
8859 }
8860 let request_line = request_line.trim_end().to_string();
8861 let mut authorization = String::new();
8862 let mut content_length = 0usize;
8863 loop {
8864 let mut line = String::new();
8865 if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
8866 return;
8867 }
8868 let line = line.trim_end();
8869 if line.is_empty() {
8870 break;
8871 }
8872 let lower = line.to_ascii_lowercase();
8873 if let Some(value) = lower.strip_prefix("authorization:") {
8874 authorization = value.trim().to_string();
8875 }
8876 if let Some(value) = lower.strip_prefix("content-length:") {
8877 content_length = value.trim().parse().unwrap_or(0);
8878 }
8879 }
8880 if content_length > 0 {
8881 let mut body = vec![0u8; content_length];
8882 let _ = reader.read_exact(&mut body).await;
8883 }
8884 let _ = request_sender.send(format!("{request_line} :: {authorization}"));
8885 if request_line.starts_with("GET /event") {
8886 let _ = writer
8887 .write_all(
8888 b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
8889 )
8890 .await;
8891 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
8892 return;
8893 }
8894 let body = if request_line.starts_with("POST /session") {
8895 r#"{"id":"mock-session"}"#
8896 } else {
8897 r#"{"status":"ok"}"#
8898 };
8899 let response = format!(
8900 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
8901 body.len(),
8902 body
8903 );
8904 let _ = writer.write_all(response.as_bytes()).await;
8905 });
8906 }
8907 });
8908 (format!("http://{address}"), request_receiver)
8909 }
8910
8911 fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
8912 crate::HarnessSupportDescriptor {
8913 orchestration: Default::default(),
8914 id: HarnessId::from(HarnessId::OPENCODE),
8915 display_name: "OpenCode".into(),
8916 native: crate::NativeSupport {
8917 discover: crate::ImplementationKind::Absent,
8918 load: crate::ImplementationKind::Absent,
8919 follow: crate::ImplementationKind::Absent,
8920 import: crate::ImplementationKind::Absent,
8921 export: crate::ImplementationKind::Absent,
8922 },
8923 runtime: crate::RuntimeSupport {
8924 implementation: crate::ImplementationKind::BuiltIn,
8925 protocol: protocol.into(),
8926 default_launch: None,
8927 connect_launch: Some(crate::RuntimeConnectLaunch {
8928 config_path: "~/opencode-tui.json".into(),
8929 address_pointer: "/server/url".into(),
8930 port_pointer: None,
8931 default_address: None,
8932 auth_pointer: Some("/server/token".into()),
8933 protocol: protocol.into(),
8934 }),
8935 capabilities: crate::RuntimeCapabilities {
8936 start_session: true,
8937 resume_session: true,
8938 attach_existing_process: true,
8939 send_input: true,
8940 stream_events: true,
8941 interrupt: true,
8942 steer: false,
8943 respond_to_requests: true,
8944 },
8945 },
8946 }
8947 }
8948
8949 #[tokio::test]
8950 async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
8951 let (base_url, mut requests) = mock_opencode_endpoint().await;
8952 let home = connect_scratch_home("open");
8953 std::fs::write(
8954 home.join("opencode-tui.json"),
8955 format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
8956 )
8957 .unwrap();
8958
8959 let descriptor = connect_descriptor("opencode-http-sse");
8960 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
8961 assert!(backend.capabilities().attach_existing_process);
8962
8963 let connection = backend
8964 .start(crate::RuntimeStartRequest {
8965 cwd: home.clone(),
8966 launch: None,
8967 mcp_servers: Vec::new(),
8968 })
8969 .await
8970 .unwrap();
8971 let handle = connection.handle();
8972 assert_eq!(handle.runtime_id, "mock-session");
8973 match &handle.endpoint {
8974 crate::RuntimeEndpoint::Http {
8975 base_url: endpoint, ..
8976 } => assert_eq!(endpoint, &base_url),
8977 other => panic!("connect mode must join the running endpoint, got {other:?}"),
8978 }
8979
8980 let mut seen = Vec::new();
8981 while let Ok(line) = requests.try_recv() {
8982 seen.push(line);
8983 }
8984 assert!(seen
8985 .iter()
8986 .any(|line| line.starts_with("GET /global/health")
8987 && line.contains("bearer connect-secret")));
8988 assert!(seen.iter().any(
8989 |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
8990 ));
8991 }
8992
8993 #[tokio::test]
9004 async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9005 let home = connect_scratch_home("openclaw");
9006 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9007 std::fs::write(
9008 home.join(".openclaw/openclaw.json"),
9009 r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9010 )
9011 .unwrap();
9012 let script = home.join("openclaw");
9013 std::fs::write(
9014 &script,
9015 r#"#!/bin/sh
9016# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9017[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9018[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9019[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9020[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9021while IFS= read -r line; do
9022 case "$line" in
9023 *'"initialize"'*)
9024 printf '%s
9025' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9026 *'"session/resume"'*)
9027 printf '%s
9028' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9029 *'"session/new"'*)
9030 printf '%s
9031' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9032 *'"session/prompt"'*)
9033 printf '%s
9034' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9035 printf '%s
9036' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9037 esac
9038done
9039"#,
9040 )
9041 .unwrap();
9042 use std::os::unix::fs::PermissionsExt;
9043 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9044
9045 let mut descriptor = crate::harness_support_registry()
9046 .harnesses
9047 .into_iter()
9048 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9049 .expect("openclaw must be registered");
9050 descriptor
9051 .runtime
9052 .connect_launch
9053 .as_mut()
9054 .unwrap()
9055 .config_path = "~/.openclaw/openclaw.json".into();
9056 descriptor.runtime.default_launch.as_mut().unwrap().program =
9057 script.to_string_lossy().into_owned();
9058 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9059 assert!(backend.capabilities().resume_session);
9060
9061 let joined = backend
9062 .attach(crate::RuntimeAttachRequest {
9063 runtime_id: "agent:main:main".into(),
9064 cwd: Some(home.clone()),
9065 launch: None,
9066 })
9067 .await;
9068 let mut connection = joined.expect("mock bridge attach must succeed");
9069 assert_eq!(connection.handle().runtime_id, "agent:main:main");
9070 let turn = connection
9071 .send_input(crate::RuntimeInput {
9072 text: "hello".into(),
9073 image_urls: Vec::new(),
9074 })
9075 .await;
9076 assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
9077 connection.close().await.unwrap();
9078 }
9079
9080 #[tokio::test]
9081 async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
9082 let home = connect_scratch_home("fail");
9083 std::fs::write(
9084 home.join("opencode-tui.json"),
9085 r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
9086 )
9087 .unwrap();
9088
9089 let gateway_only = connect_descriptor("acp-v1-jsonrpc");
9090 let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
9091 panic!("an ACP connect endpoint has no gateway client yet");
9092 };
9093 let message = format!("{error:?}");
9094 assert!(message.contains("acp-v1-jsonrpc"));
9095 assert!(!message.contains("connect-secret"));
9096
9097 let unreadable = connect_descriptor("opencode-http-sse");
9098 let missing_home = connect_scratch_home("missing");
9099 let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
9100 panic!("an unreadable connect config must fail closed");
9101 };
9102 let message = format!("{error:?}");
9103 assert!(message.contains("opencode-tui.json"));
9104 assert!(!message.contains("connect-secret"));
9105 }
9106
9107 fn jobs_fixture_root() -> PathBuf {
9112 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
9113 }
9114
9115 fn jobs_fixture_homes() -> Value {
9118 let root = jobs_fixture_root();
9119 json!({
9120 "claude_code": root.join("claude_jobs_home/projects"),
9121 "hermes": root.join("hermes_home/state.db"),
9122 "openclaw": root.join("openclaw_home"),
9123 })
9124 }
9125
9126 fn jobs_list(params: Value) -> Value {
9127 let mut service = HarnessSessionService::new();
9128 service.handle(request(1, "harness.v1.jobs.list", params))
9129 }
9130
9131 fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
9132 result["jobs"]
9133 .as_array()
9134 .expect("jobs is an array")
9135 .iter()
9136 .find(|job| job["id"] == id)
9137 .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
9138 }
9139
9140 #[test]
9141 fn gateway_health_derives_from_running_probe_and_install_state() {
9142 let running = RunningInstance {
9143 method: RunningInstanceMethod::GatewayConnect,
9144 evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
9145 checked_at_ms: 1,
9146 };
9147 let up = gateway_health(
9148 HarnessId::OPENCLAW,
9149 true,
9150 Some(&running),
9151 Some("2026.7.1-2"),
9152 );
9153 assert_eq!(up.state, GatewayState::Up);
9154 assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
9155 assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
9156 let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
9160 std::fs::create_dir_all(&dir).unwrap();
9161 let fake = dir.join("hermes");
9162 let write_fake = |body: &str| {
9163 std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
9164 #[cfg(unix)]
9165 {
9166 use std::os::unix::fs::PermissionsExt;
9167 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
9168 }
9169 };
9170 write_fake("echo '✗ Gateway service is not installed'");
9171 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
9172 *slot.borrow_mut() = Some((
9173 HarnessId::HERMES.to_string(),
9174 fake.to_string_lossy().into_owned(),
9175 ))
9176 });
9177 let down = gateway_health(HarnessId::HERMES, true, None, None);
9178 assert_eq!(down.state, GatewayState::Down, "{down:?}");
9179 assert!(down.endpoint.is_none());
9180 assert!(down.evidence.contains("not installed"));
9181 write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
9182 let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
9183 assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
9184 assert!(idle_but_up.evidence.contains("PID 4242"));
9185 write_fake("echo 'something unparseable'");
9186 let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
9187 assert_eq!(no_verdict.state, GatewayState::Down);
9188 assert!(no_verdict.evidence.contains("no verdict"));
9189 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
9190 let absent = gateway_health(HarnessId::HERMES, false, None, None);
9191 assert_eq!(absent.state, GatewayState::Unknown);
9192 let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
9193 assert_eq!(core.state, GatewayState::Unknown);
9194 assert!(core.evidence.contains("per session"));
9195 }
9196
9197 #[test]
9198 fn triggers_list_reads_both_stores_and_never_emits_secrets() {
9199 let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
9200 let rows = response["result"]["triggers"]
9201 .as_array()
9202 .expect("triggers")
9203 .clone();
9204 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
9205 assert!(
9206 hermes.iter().any(|r| r["name"] == "deploys"
9207 && r["route"] == "/webhooks/deploys"
9208 && r["kind"] == "webhook"),
9209 "{rows:#?}"
9210 );
9211 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
9212 assert!(openclaw
9213 .iter()
9214 .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
9215 assert!(openclaw.iter().any(|r| r["name"] == "gmail"
9216 && r["kind"] == "hook_mapping"
9217 && r["target"]["action"] == "agent"));
9218 let rendered = response.to_string();
9219 for secret in [
9220 "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
9221 "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
9222 ] {
9223 assert!(!rendered.contains(secret), "{rendered}");
9224 }
9225 let refused =
9226 triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
9227 assert_eq!(refused["error"]["code"], -32020, "{refused}");
9228 }
9229
9230 fn triggers_list(params: Value) -> Value {
9231 let mut service = HarnessSessionService::new();
9232 service.handle(request(1, "harness.v1.triggers.list", params))
9233 }
9234
9235 #[test]
9236 fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
9237 let response = routes_list(json!({"homes": jobs_fixture_homes()}));
9238 let rows = response["result"]["routes"]
9239 .as_array()
9240 .expect("routes")
9241 .clone();
9242 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
9243 assert_eq!(hermes.len(), 2, "{rows:#?}");
9244 assert_eq!(hermes[0]["target"], "coder");
9245 assert_eq!(hermes[0]["match"]["platform"], "slack");
9246 assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
9247 assert_eq!(hermes[0]["specificity"], 4);
9248 assert_eq!(hermes[1]["default"], true);
9249 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
9250 assert!(
9251 openclaw.iter().any(|r| r["target"] == "design"
9252 && r["match"]["platform"] == "slack"
9253 && r["specificity"] == 1),
9254 "{openclaw:#?}"
9255 );
9256 assert!(openclaw.iter().any(|r| r["default"] == true));
9257 let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
9259 assert_eq!(refused["error"]["code"], -32020, "{refused}");
9260 }
9261
9262 fn routes_list(params: Value) -> Value {
9263 let mut service = HarnessSessionService::new();
9264 service.handle(request(1, "harness.v1.routes.list", params))
9265 }
9266
9267 #[test]
9268 fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
9269 let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
9270 let result = &response["result"];
9271 let ids: Vec<&str> = result["jobs"]
9272 .as_array()
9273 .unwrap()
9274 .iter()
9275 .map(|job| job["id"].as_str().unwrap())
9276 .collect();
9277 assert_eq!(
9278 ids,
9279 vec![
9280 "release-watch",
9281 "toolu_wake_recheck",
9282 "digest-15m",
9283 "nightly-audit",
9284 "coder-standup",
9285 "ops-once-boot",
9286 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
9287 "8bb7d938-ca46-4a6d-90eb-c92331155566",
9288 "cron_standup",
9289 "cron_reindex",
9290 ],
9291 "{result}"
9292 );
9293
9294 let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
9298 assert_eq!(health["harness"], "openclaw");
9299 assert_eq!(health["schedule"]["kind"], "interval");
9300 assert_eq!(health["schedule"]["minutes"], 10.0);
9301 assert_eq!(health["session_target"], "isolated");
9302 assert_eq!(health["payload"]["kind"], "prompt");
9303 assert_eq!(health["payload"]["text"], "nightly health check");
9304 assert_eq!(health["deliver"]["mode"], "announce");
9308 assert_eq!(health["deliver"]["target"], "last");
9309 assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
9310 let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
9311 assert_eq!(digest["schedule"]["kind"], "cron");
9312 assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
9313 assert_eq!(digest["session_target"], "main");
9314 assert_eq!(digest["payload"]["kind"], "system_event");
9315
9316 let cron = job_row(result, "release-watch");
9318 assert_eq!(cron["harness"], "claude-code");
9319 assert_eq!(cron["scope"], "session");
9320 assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
9321 assert_eq!(cron["schedule"]["kind"], "cron");
9322 assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
9323 assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
9324 assert_eq!(cron["payload"]["kind"], "prompt");
9325 assert_eq!(cron["recurring"], true);
9326 assert_eq!(cron["deliver"]["target"], "session");
9327 let wakeup = job_row(result, "toolu_wake_recheck");
9328 assert_eq!(wakeup["payload"]["kind"], "wakeup");
9329 assert_eq!(wakeup["schedule"]["kind"], "once");
9330 assert_eq!(wakeup["recurring"], false);
9331 assert_eq!(wakeup["state"], "pending");
9332
9333 let interval = job_row(result, "digest-15m");
9335 assert_eq!(interval["harness"], "hermes");
9336 assert_eq!(interval["scope"], "install");
9337 assert_eq!(interval["profile"], Value::Null);
9338 assert_eq!(interval["schedule"]["kind"], "interval");
9339 assert_eq!(interval["schedule"]["minutes"], 15.0);
9340 assert_eq!(interval["schedule"]["display"], "every 15 min");
9341 assert_eq!(interval["deliver"]["target"], "origin");
9342 assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
9343 assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
9344 assert_eq!(interval["last_status"], "ok");
9345 let nightly = job_row(result, "nightly-audit");
9346 assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
9347 assert_eq!(nightly["deliver"]["target"], "local");
9348 assert_eq!(nightly["enabled"], false);
9349 assert_eq!(nightly["state"], "paused");
9350 let profiled = job_row(result, "ops-once-boot");
9352 assert_eq!(profiled["profile"], "ops");
9353 assert_eq!(profiled["schedule"]["kind"], "once");
9354 assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
9355 assert_eq!(profiled["payload"]["kind"], "script");
9356 assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
9358 assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
9359 assert_eq!(profiled["recurring"], false);
9360
9361 let standup_to_group = job_row(result, "coder-standup");
9364 assert_eq!(standup_to_group["deliver"]["target"], "origin");
9365 assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
9366 assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
9367 assert!(standup_to_group["deliver"]["mode"].is_null());
9369 assert!(standup_to_group["deliver"]["account"].is_null());
9370
9371 let standup = job_row(result, "cron_standup");
9374 assert_eq!(standup["harness"], "openclaw");
9375 assert_eq!(standup["session_target"], "isolated");
9376 assert_eq!(standup["deliver"]["mode"], "announce");
9377 assert_eq!(standup["deliver"]["target"], "slack");
9378 assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
9379 assert_eq!(standup["payload"]["kind"], "prompt");
9380 assert_eq!(standup["profile"], "main");
9381 let reindex = job_row(result, "cron_reindex");
9382 assert_eq!(reindex["session_target"], "main");
9383 assert_eq!(reindex["payload"]["kind"], "system_event");
9384 assert_eq!(reindex["schedule"]["kind"], "interval");
9385 assert_eq!(reindex["schedule"]["display"], "every 240 min");
9386 assert_eq!(reindex["enabled"], false);
9387
9388 let states: Vec<(&str, &str)> = result["sources"]
9390 .as_array()
9391 .unwrap()
9392 .iter()
9393 .map(|source| {
9394 (
9395 source["harness"].as_str().unwrap(),
9396 source["state"].as_str().unwrap(),
9397 )
9398 })
9399 .collect();
9400 assert_eq!(
9404 states,
9405 vec![
9406 ("claude-code", "scanned"),
9407 ("hermes", "read"),
9408 ("hermes", "absent_store"),
9409 ("hermes", "read"),
9410 ("openclaw", "read"),
9411 ("openclaw", "read"),
9412 ],
9413 "{result}"
9414 );
9415 }
9416
9417 #[test]
9418 fn jobs_list_filters_by_harness_session_and_profile() {
9419 let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
9420 let ids: Vec<&str> = by_harness["result"]["jobs"]
9421 .as_array()
9422 .unwrap()
9423 .iter()
9424 .map(|job| job["id"].as_str().unwrap())
9425 .collect();
9426 assert_eq!(
9427 ids,
9428 vec![
9429 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
9430 "8bb7d938-ca46-4a6d-90eb-c92331155566",
9431 "cron_standup",
9432 "cron_reindex",
9433 ]
9434 );
9435
9436 let by_session = jobs_list(json!({
9437 "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
9438 "homes": jobs_fixture_homes(),
9439 }));
9440 let jobs = by_session["result"]["jobs"].as_array().unwrap();
9441 assert_eq!(jobs.len(), 2, "{by_session}");
9442 assert!(jobs
9443 .iter()
9444 .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
9445
9446 let by_profile = jobs_list(json!({
9447 "harness": "hermes",
9448 "profile": "ops",
9449 "homes": jobs_fixture_homes(),
9450 }));
9451 let jobs = by_profile["result"]["jobs"].as_array().unwrap();
9452 assert_eq!(jobs.len(), 1, "{by_profile}");
9453 assert_eq!(jobs[0]["id"], "ops-once-boot");
9454 }
9455
9456 #[test]
9457 fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
9458 let mut service = HarnessSessionService::new();
9459 let hermes = service.handle(request(
9460 1,
9461 "harness.v1.jobs.get",
9462 json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
9463 ));
9464 assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
9465 assert_eq!(hermes["result"]["source"]["provider"], "nous");
9467 assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
9468
9469 let claude = service.handle(request(
9470 2,
9471 "harness.v1.jobs.get",
9472 json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
9473 ));
9474 assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
9475 assert_eq!(
9476 claude["result"]["source"]["tool_use_id"],
9477 "toolu_cron_release_watch"
9478 );
9479
9480 let missing = service.handle(request(
9481 3,
9482 "harness.v1.jobs.get",
9483 json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
9484 ));
9485 assert!(missing["error"]["message"]
9486 .as_str()
9487 .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
9488 }
9489
9490 #[test]
9491 fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
9492 let mut service = HarnessSessionService::new();
9493 for (id, method, params) in [
9494 (
9495 1,
9496 "harness.v1.jobs.list",
9497 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
9498 ),
9499 (
9500 2,
9501 "harness.v1.jobs.get",
9502 json!({"harness": "codex", "id": "anything"}),
9503 ),
9504 ] {
9505 let response = service.handle(request(id, method, params));
9506 assert_eq!(response["error"]["code"], -32020, "{response}");
9507 assert!(response["error"]["message"]
9508 .as_str()
9509 .is_some_and(|message| message.contains("has no scheduled jobs")));
9510 assert!(response.get("result").is_none());
9511 }
9512 }
9513
9514 #[test]
9515 fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
9516 let scratch = std::env::temp_dir().join(format!(
9517 "supercode-jobs-migrated-{}-{}",
9518 std::process::id(),
9519 generated_session_id()
9520 ));
9521 std::fs::create_dir_all(&scratch).unwrap();
9522 let response = jobs_list(json!({
9523 "harness": "openclaw",
9524 "homes": {"openclaw": scratch.clone()},
9525 }));
9526 let result = &response["result"];
9527 assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
9528 assert_eq!(result["sources"][0]["state"], "absent_store");
9529 assert_eq!(result["sources"][0]["harness"], "openclaw");
9530 std::fs::remove_dir_all(&scratch).ok();
9531 }
9532
9533 const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
9542 const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
9544
9545 fn runs_list(params: Value) -> Value {
9546 let mut service = HarnessSessionService::new();
9547 service.handle(request(1, "harness.v1.runs.list", params))
9548 }
9549
9550 fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
9551 result["runs"]
9552 .as_array()
9553 .expect("runs is an array")
9554 .iter()
9555 .find(|run| run["id"] == id)
9556 .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
9557 }
9558
9559 #[test]
9560 fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
9561 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
9562 let result = &response["result"];
9563 let ids: Vec<&str> = result["runs"]
9564 .as_array()
9565 .expect("runs is an array")
9566 .iter()
9567 .map(|run| run["id"].as_str().unwrap())
9568 .collect();
9569 let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
9570 assert_eq!(
9571 ids,
9572 vec![
9573 "b2c3d4e5f60718293a4b5c6d7e8f9012",
9575 "a1b2c3d4e5f60718293a4b5c6d7e8f90",
9576 "c3d4e5f60718293a4b5c6d7e8f901234",
9577 "f60718293a4b5c6d7e8f901234567890",
9578 "e5f60718293a4b5c6d7e8f9012345678",
9579 "d4e5f60718293a4b5c6d7e8f90123456",
9580 "run_health_0002",
9582 digest_fire.as_str(),
9583 "run_health_0001",
9584 ],
9585 "{result}"
9586 );
9587
9588 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
9591 assert_eq!(failed["harness"], "hermes");
9592 assert_eq!(failed["job_id"], "job42");
9593 assert_eq!(failed["status"], "failed");
9594 assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
9595 assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
9596
9597 let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
9600 assert_eq!(abandoned["status"], "unknown");
9601 assert_eq!(abandoned["job_id"], "ops-once-boot");
9602
9603 let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
9605 assert_eq!(running["status"], "running");
9606 assert!(running["finished_at"].is_null(), "{running}");
9607 assert!(running["session_id"].is_null(), "{running}");
9608
9609 let ok = run_row(result, "run_health_0001");
9612 assert_eq!(ok["harness"], "openclaw");
9613 assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
9614 assert_eq!(ok["status"], "ok");
9615 assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
9616 assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
9617 assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
9618 assert!(ok["claimed_at"].is_null(), "{ok}");
9620
9621 assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
9624
9625 for id in [
9630 "b2c3d4e5f60718293a4b5c6d7e8f9012",
9631 "d4e5f60718293a4b5c6d7e8f90123456",
9632 ] {
9633 assert!(run_row(result, id)["delivery"].is_null(), "{id}");
9634 }
9635
9636 let sources = result["sources"].as_array().unwrap();
9639 let states: Vec<(&str, &str)> = sources
9640 .iter()
9641 .map(|source| {
9642 (
9643 source["harness"].as_str().unwrap(),
9644 source["state"].as_str().unwrap(),
9645 )
9646 })
9647 .collect();
9648 assert_eq!(
9649 states,
9650 vec![
9651 ("hermes", "read"),
9652 ("hermes", "absent_store"),
9653 ("hermes", "read"),
9654 ("openclaw", "read"),
9655 ],
9656 "{result}"
9657 );
9658 assert_eq!(sources[2]["profile"], "ops");
9659 assert!(sources[3]["path"]
9660 .as_str()
9661 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
9662 }
9663
9664 #[test]
9665 fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
9666 let response = runs_list(json!({
9667 "harness": "hermes",
9668 "job": "job42",
9669 "homes": jobs_fixture_homes(),
9670 }));
9671 let result = &response["result"];
9672 assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
9673
9674 let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
9678 assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
9679
9680 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
9684 assert!(failed["session_id"].is_null(), "{failed}");
9685 }
9686
9687 #[test]
9691 fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
9692 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
9693 let result = &response["result"];
9694
9695 let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
9699 assert_eq!(delivered["status"], "completed");
9700 assert_eq!(delivered["delivery"]["state"], "delivered");
9701 assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
9702 assert_eq!(delivered["delivery"]["attempts"], 1);
9703 assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
9704 assert_eq!(
9705 delivered["delivery"]["delivered_at"],
9706 "2026-09-02T09:00:30.400Z"
9707 );
9708
9709 let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
9712 assert_eq!(undelivered["status"], "completed");
9713 assert_eq!(undelivered["delivery"]["state"], "failed");
9714 assert_eq!(undelivered["delivery"]["attempts"], 3);
9715 assert_eq!(
9716 undelivered["delivery"]["last_error"],
9717 "telegram send failed: Bad Request: chat not found"
9718 );
9719 assert!(
9722 undelivered["delivery"]["delivered_at"].is_null(),
9723 "{undelivered}"
9724 );
9725
9726 let announced = run_row(result, "run_health_0001");
9729 assert_eq!(announced["delivery"]["state"], "delivered");
9730 assert_eq!(announced["delivery"]["target"], "last");
9731 assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
9733 assert!(
9734 announced["delivery"]["delivered_at"].is_null(),
9735 "{announced}"
9736 );
9737 let refused = run_row(result, "run_health_0002");
9738 assert_eq!(refused["delivery"]["state"], "not-delivered");
9739 assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
9740
9741 let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
9744 assert!(skipped["delivery"].is_null(), "{skipped}");
9745 }
9746
9747 #[test]
9752 fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
9753 let scratch = std::env::temp_dir().join(format!(
9754 "supercode-runs-delivery-{}-{}",
9755 std::process::id(),
9756 generated_session_id()
9757 ));
9758 std::fs::create_dir_all(scratch.join("cron")).unwrap();
9759 let fixture = jobs_fixture_root().join("hermes_home");
9760 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
9761 for name in ["cron/executions.db", "cron/jobs.json"] {
9762 std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
9763 }
9764 {
9765 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
9766 connection
9769 .execute(
9770 "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
9771 [],
9772 )
9773 .unwrap();
9774 connection
9777 .execute(
9778 "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
9779 ('cron_coder-standup_20260902_090010', 'cron', \
9780 'agent:coder:telegram:group:-100777:55', 1788339610.0)",
9781 [],
9782 )
9783 .unwrap();
9784 }
9785 let response = runs_list(json!({
9786 "harness": "hermes",
9787 "job": "coder-standup",
9788 "homes": {"hermes": scratch.join("state.db")},
9789 }));
9790 let result = &response["result"];
9791 let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
9792 assert_eq!(
9793 matched["session_id"], "cron_coder-standup_20260902_090010",
9794 "{result}"
9795 );
9796 assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
9797 assert_eq!(
9798 matched["delivery"]["target"], "slack:C0FALLBACK:55",
9799 "{result}"
9800 );
9801 std::fs::remove_dir_all(&scratch).ok();
9802 }
9803
9804 #[test]
9805 fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
9806 let scratch = std::env::temp_dir().join(format!(
9811 "supercode-runs-compressed-{}-{}",
9812 std::process::id(),
9813 generated_session_id()
9814 ));
9815 std::fs::create_dir_all(scratch.join("cron")).unwrap();
9816 let fixture = jobs_fixture_root().join("hermes_home");
9817 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
9818 std::fs::copy(
9819 fixture.join("cron/executions.db"),
9820 scratch.join("cron/executions.db"),
9821 )
9822 .unwrap();
9823 {
9824 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
9825 connection
9826 .execute(
9827 "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
9828 ["cron_job42_20260902_120000"],
9829 )
9830 .unwrap();
9831 connection
9832 .execute(
9833 "INSERT INTO sessions (id, source, parent_session_id, started_at) \
9834 VALUES ('job42-after-compaction', 'cron', \
9835 'cron_job42_20260902_120000', 1788350000.0)",
9836 [],
9837 )
9838 .unwrap();
9839 }
9840 let response = runs_list(json!({
9841 "harness": "hermes",
9842 "job": "job42",
9843 "homes": {"hermes": scratch.join("state.db")},
9844 }));
9845 let result = &response["result"];
9846 assert_eq!(
9847 run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
9848 "job42-after-compaction",
9849 "{result}"
9850 );
9851 std::fs::remove_dir_all(&scratch).ok();
9852 }
9853
9854 #[test]
9855 fn runs_list_filters_by_job_and_caps_by_limit() {
9856 let by_job = runs_list(json!({
9857 "harness": "openclaw",
9858 "job": OPENCLAW_HEALTH_JOB,
9859 "homes": jobs_fixture_homes(),
9860 }));
9861 let ids: Vec<&str> = by_job["result"]["runs"]
9862 .as_array()
9863 .unwrap()
9864 .iter()
9865 .map(|run| run["id"].as_str().unwrap())
9866 .collect();
9867 assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
9868
9869 let capped = runs_list(json!({
9870 "harness": "openclaw",
9871 "limit": 1,
9872 "homes": jobs_fixture_homes(),
9873 }));
9874 let runs = capped["result"]["runs"].as_array().unwrap();
9875 assert_eq!(runs.len(), 1, "{capped}");
9876 assert_eq!(runs[0]["id"], "run_health_0002");
9878 }
9879
9880 #[test]
9881 fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
9882 let mut service = HarnessSessionService::new();
9883 let hermes = service.handle(request(
9884 1,
9885 "harness.v1.runs.get",
9886 json!({
9887 "harness": "hermes",
9888 "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
9889 "homes": jobs_fixture_homes(),
9890 }),
9891 ));
9892 assert_eq!(hermes["result"]["run"]["status"], "completed");
9893 assert_eq!(
9894 hermes["result"]["run"]["session_id"],
9895 "cron_job42_20260902_120000"
9896 );
9897 assert_eq!(hermes["result"]["source"]["source"], "scheduler");
9899 assert_eq!(hermes["result"]["source"]["pid"], 4242);
9900 assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
9901
9902 let openclaw = service.handle(request(
9903 2,
9904 "harness.v1.runs.get",
9905 json!({
9906 "harness": "openclaw",
9907 "id": "run_health_0002",
9908 "homes": jobs_fixture_homes(),
9909 }),
9910 ));
9911 assert_eq!(openclaw["result"]["run"]["status"], "error");
9912 assert_eq!(
9915 openclaw["result"]["source"]["delivery_status"],
9916 "not-delivered"
9917 );
9918 assert_eq!(
9919 openclaw["result"]["source"]["delivery_error"],
9920 "channel_not_found"
9921 );
9922 assert_eq!(openclaw["result"]["source"]["delivered"], 0);
9923 assert_eq!(
9924 openclaw["result"]["run"]["delivery"]["state"],
9925 "not-delivered"
9926 );
9927 assert_eq!(
9928 openclaw["result"]["run"]["delivery"]["last_error"],
9929 "channel_not_found"
9930 );
9931
9932 let missing = service.handle(request(
9933 3,
9934 "harness.v1.runs.get",
9935 json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
9936 ));
9937 assert!(missing["error"]["message"]
9938 .as_str()
9939 .is_some_and(|message| message.contains("no run `no-such-run`")));
9940 }
9941
9942 #[test]
9943 fn runs_refuse_a_harness_that_keeps_no_run_store() {
9944 let mut service = HarnessSessionService::new();
9945 for (id, method, params) in [
9946 (
9949 1,
9950 "harness.v1.runs.list",
9951 json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
9952 ),
9953 (
9954 2,
9955 "harness.v1.runs.get",
9956 json!({"harness": "claude-code", "id": "anything"}),
9957 ),
9958 (
9959 3,
9960 "harness.v1.runs.list",
9961 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
9962 ),
9963 ] {
9964 let response = service.handle(request(id, method, params));
9965 assert_eq!(response["error"]["code"], -32020, "{response}");
9966 assert!(response["error"]["message"]
9967 .as_str()
9968 .is_some_and(|message| message.contains("keeps no run store")));
9969 assert!(response.get("result").is_none());
9970 }
9971 }
9972
9973 #[test]
9974 fn runs_list_reports_an_install_with_no_run_store_as_absent() {
9975 let scratch = std::env::temp_dir().join(format!(
9976 "supercode-runs-empty-{}-{}",
9977 std::process::id(),
9978 generated_session_id()
9979 ));
9980 std::fs::create_dir_all(&scratch).unwrap();
9981 let response = runs_list(json!({
9982 "harness": "openclaw",
9983 "homes": {"openclaw": scratch.clone()},
9984 }));
9985 let result = &response["result"];
9986 assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
9987 assert_eq!(result["sources"][0]["state"], "absent_store");
9988 assert!(result["sources"][0]["path"]
9989 .as_str()
9990 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
9991 std::fs::remove_dir_all(&scratch).ok();
9992 }
9993}