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