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