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