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