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