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