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 if harness == HarnessId::SUPERCODE {
5643 let config_home = crate::agent::global_instructions_dir();
5644 for file in ["config.toml", "credentials.toml"] {
5645 copy_probe_path(
5646 &config_home.join(file),
5647 &root.join(".config/supercode").join(file),
5648 )?;
5649 }
5650 }
5651 configure_isolated_probe_auth(harness, &root)?;
5652
5653 let root_text = root.to_string_lossy().into_owned();
5654 for (key, value) in [
5655 ("HOME", root_text.clone()),
5656 (
5657 "XDG_CACHE_HOME",
5658 root.join(".cache").to_string_lossy().into_owned(),
5659 ),
5660 (
5661 "XDG_CONFIG_HOME",
5662 root.join(".config").to_string_lossy().into_owned(),
5663 ),
5664 (
5665 "XDG_DATA_HOME",
5666 root.join(".local/share").to_string_lossy().into_owned(),
5667 ),
5668 ] {
5669 launch.env.insert(key.into(), value);
5670 }
5671 let scoped = match harness {
5672 HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5673 HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5674 HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5675 HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5676 HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5677 HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5678 _ => None,
5679 };
5680 if let Some((key, value)) = scoped {
5681 launch
5682 .env
5683 .insert(key.into(), value.to_string_lossy().into_owned());
5684 }
5685 Ok(Self { launch, root })
5686 }
5687
5688 fn cleanup(&self) -> std::io::Result<()> {
5689 match std::fs::remove_dir_all(&self.root) {
5690 Ok(()) => Ok(()),
5691 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5692 Err(error) => Err(error),
5693 }
5694 }
5695}
5696
5697impl Drop for IsolatedProbeHome {
5698 fn drop(&mut self) {
5699 let _ = self.cleanup();
5700 }
5701}
5702
5703fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5704 match harness {
5705 HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5706 HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5710 HarnessId::CODEX => &[".codex/auth.json"],
5711 HarnessId::GEMINI => &[
5712 ".gemini/google_accounts.json",
5713 ".gemini/oauth_creds.json",
5714 ".gemini/settings.json",
5715 ],
5716 HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5717 HarnessId::OPENCODE => &[
5718 ".config/opencode/auth.json",
5719 ".local/share/opencode/auth.json",
5720 ],
5721 HarnessId::PI => &[".pi/agent/auth.json"],
5722 HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5727 _ => &[],
5728 }
5729}
5730
5731fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5732 copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5733}
5734
5735fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5736 if !source.is_file() {
5737 return Ok(());
5738 }
5739 if let Some(parent) = destination.parent() {
5740 std::fs::create_dir_all(parent)?;
5741 set_private_dir_permissions(parent)?;
5742 }
5743 std::fs::copy(source, destination)?;
5744 set_private_file_permissions(destination)
5745}
5746
5747fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5748 if harness != HarnessId::GEMINI {
5749 return Ok(());
5750 }
5751 let oauth = probe_home.join(".gemini/oauth_creds.json");
5752 if !oauth.is_file() {
5753 return Ok(());
5754 }
5755 let settings_path = probe_home.join(".gemini/settings.json");
5756 let mut settings = std::fs::read_to_string(&settings_path)
5757 .ok()
5758 .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5759 .unwrap_or_else(|| json!({}));
5760 settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5761 std::fs::write(
5762 &settings_path,
5763 serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5764 )?;
5765 set_private_file_permissions(&settings_path)
5766}
5767
5768#[cfg(unix)]
5769fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5770 use std::os::unix::fs::PermissionsExt;
5771 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5772}
5773
5774#[cfg(not(unix))]
5775fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5776 Ok(())
5777}
5778
5779#[cfg(unix)]
5780fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5781 use std::os::unix::fs::PermissionsExt;
5782 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5783}
5784
5785#[cfg(not(unix))]
5786fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5787 Ok(())
5788}
5789
5790fn find_executable(program: &str) -> Option<PathBuf> {
5791 let candidate = PathBuf::from(program);
5792 if candidate.components().count() > 1 {
5793 return candidate.is_file().then_some(candidate);
5794 }
5795 let path = std::env::var_os("PATH")?;
5796 for directory in std::env::split_paths(&path) {
5797 let candidate = directory.join(program);
5798 if candidate.is_file() {
5799 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5800 }
5801 #[cfg(windows)]
5802 {
5803 for extension in ["exe", "cmd", "bat"] {
5804 let candidate = directory.join(format!("{program}.{extension}"));
5805 if candidate.is_file() {
5806 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5807 }
5808 }
5809 }
5810 }
5811 None
5812}
5813
5814async fn executable_version(executable: &Path) -> Option<String> {
5815 let mut command = tokio::process::Command::new(executable);
5816 command
5817 .arg("--version")
5818 .stdin(std::process::Stdio::null())
5819 .stdout(std::process::Stdio::piped())
5820 .stderr(std::process::Stdio::piped())
5821 .kill_on_drop(true);
5822 let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5823 .await
5824 .ok()?
5825 .ok()?;
5826 let stdout = String::from_utf8_lossy(&output.stdout);
5827 let stderr = String::from_utf8_lossy(&output.stderr);
5828 stdout
5829 .lines()
5830 .chain(stderr.lines())
5831 .map(str::trim)
5832 .find(|line| !line.is_empty())
5833 .map(|line| truncate_text(line, 200))
5834}
5835
5836pub(crate) fn auth_evidence(harness: &str) -> bool {
5837 let env_names: &[&str] = match harness {
5838 HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5839 HarnessId::CODEX => &["OPENAI_API_KEY"],
5840 HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5841 HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5842 HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5843 HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5844 HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5845 _ => &[],
5846 };
5847 if env_names
5848 .iter()
5849 .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5850 {
5851 return true;
5852 }
5853 let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5854 return false;
5855 };
5856 let files: Vec<PathBuf> = match harness {
5857 HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5858 HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5859 HarnessId::OPENCODE => vec![
5860 home.join(".local/share/opencode/auth.json"),
5861 home.join(".config/opencode/auth.json"),
5862 ],
5863 HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5864 HarnessId::GROK => vec![home.join(".grok/auth.json")],
5865 HarnessId::GEMINI => vec![
5866 home.join(".gemini/oauth_creds.json"),
5867 home.join(".gemini/google_accounts.json"),
5868 ],
5869 HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5870 HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5871 _ => Vec::new(),
5872 };
5873 if files.into_iter().any(|path| {
5874 std::fs::metadata(path)
5875 .map(|metadata| metadata.is_file() && metadata.len() > 2)
5876 .unwrap_or(false)
5877 }) {
5878 return true;
5879 }
5880 if harness == HarnessId::CLAUDE_CODE {
5887 return std::fs::read_to_string(home.join(".claude.json"))
5888 .map(|text| text.contains("\"oauthAccount\""))
5889 .unwrap_or(false);
5890 }
5891 false
5892}
5893
5894fn looks_like_auth_error(message: &str) -> bool {
5895 let message = message.to_ascii_lowercase();
5896 [
5897 "auth",
5898 "login",
5899 "sign in",
5900 "sign-in",
5901 "credential",
5902 "unauthorized",
5903 "forbidden",
5904 "token",
5905 ]
5906 .iter()
5907 .any(|needle| message.contains(needle))
5908}
5909
5910fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5911 crate::RuntimeCapabilities {
5912 start_session: false,
5913 resume_session: false,
5914 attach_existing_process: false,
5915 send_input: false,
5916 stream_events: false,
5917 interrupt: false,
5918 steer: false,
5919 respond_to_requests: false,
5920 }
5921}
5922
5923fn truncate_text(text: &str, max_chars: usize) -> String {
5924 let mut chars = text.chars();
5925 let truncated = chars.by_ref().take(max_chars).collect::<String>();
5926 if chars.next().is_some() {
5927 format!("{truncated}…")
5928 } else {
5929 truncated
5930 }
5931}
5932
5933fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5940 match &handle.endpoint {
5941 crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5942 crate::RuntimeEndpoint::Http { .. } => None,
5943 }
5944}
5945
5946fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5952 match process_group {
5953 #[cfg(unix)]
5954 Some(pid) => {
5955 crate::lsp::kill_process_group(pid);
5956 true
5957 }
5958 #[cfg(not(unix))]
5959 Some(_) => false,
5960 None => false,
5961 }
5962}
5963
5964fn error_message(error: ServiceError) -> String {
5965 match error {
5966 ServiceError::InvalidParams(message)
5967 | ServiceError::Operation(message)
5968 | ServiceError::UnsupportedAction(message) => message,
5969 ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5970 ServiceError::Sdk(error) => error.to_string(),
5971 }
5972}
5973
5974#[derive(Debug)]
5975enum ServiceError {
5976 InvalidParams(String),
5977 MethodNotFound,
5978 UnsupportedAction(String),
5979 Operation(String),
5980 Sdk(SdkError),
5981}
5982
5983fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5984 match error {
5985 ServiceError::InvalidParams(message) => {
5986 SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5987 }
5988 ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5989 SdkError::unsupported(operation)
5990 }
5991 ServiceError::Operation(message) => {
5992 let code = if message.contains("already in progress") {
5993 SdkErrorCode::Busy
5994 } else if message.contains("not supported by this runtime") {
5995 SdkErrorCode::UnsupportedAction
5996 } else if message.contains("unknown runtime connection") {
5997 SdkErrorCode::NotFound
5998 } else {
5999 SdkErrorCode::Execution
6000 };
6001 SdkError::new(code, operation, message)
6002 }
6003 ServiceError::Sdk(error) => error,
6004 }
6005}
6006
6007fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
6008 let error_code = error.code();
6009 let code = match error_code {
6010 SdkErrorCode::Unauthenticated => -32030,
6011 SdkErrorCode::Unauthorized => -32031,
6012 SdkErrorCode::ControllerRequired => -32032,
6013 SdkErrorCode::LeaseExpired => -32033,
6014 SdkErrorCode::InvalidArgument => -32602,
6015 SdkErrorCode::NotFound => -32004,
6016 SdkErrorCode::Busy => -32000,
6017 SdkErrorCode::UnsupportedAction => -32020,
6018 SdkErrorCode::Execution => -32002,
6019 SdkErrorCode::Transport => -32003,
6020 };
6021 json!({
6022 "jsonrpc": "2.0",
6023 "id": id,
6024 "error": {
6025 "code": code,
6026 "name": error_code,
6027 "operation": error.operation(),
6028 "message": error.to_string(),
6029 },
6030 })
6031}
6032
6033fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
6034 serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
6035}
6036
6037fn operation(error: impl Into<crate::Error>) -> ServiceError {
6038 let error = error.into();
6039 match error {
6040 crate::Error::Sdk(error) => ServiceError::Sdk(error),
6041 error => ServiceError::Operation(error.to_string()),
6042 }
6043}
6044
6045#[derive(Debug, Clone, Deserialize, Default)]
6049#[serde(default)]
6050struct MemoryRequest {
6051 harness: Option<String>,
6053 query: Option<String>,
6055 profile: Option<String>,
6057 session: Option<String>,
6059 full: bool,
6061 regex: bool,
6063 cwd: Option<std::path::PathBuf>,
6065 homes: crate::HarnessHomes,
6067}
6068
6069fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6072 let request = decode::<MemoryRequest>(params)?;
6073 let harness = request
6074 .harness
6075 .clone()
6076 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6077 let to_service = |error: crate::memory::MemoryError| match error {
6078 crate::memory::MemoryError::UnsupportedHarness { .. }
6079 | crate::memory::MemoryError::SessionNotScoped { .. } => {
6080 ServiceError::UnsupportedAction(error.to_string())
6081 }
6082 other => ServiceError::InvalidParams(other.to_string()),
6083 };
6084 match method {
6085 "harness.v1.memory.show" => {
6086 let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6087 harness,
6088 profile: request.profile,
6089 session: request.session,
6090 full: request.full,
6091 cwd: request.cwd,
6092 homes: request.homes,
6093 })
6094 .map_err(to_service)?;
6095 Ok(json!({
6096 "schema": crate::memory::MEMORY_SCHEMA,
6097 "documents": documents,
6098 }))
6099 }
6100 "harness.v1.memory.search" => {
6101 let query = request
6102 .query
6103 .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6104 let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6105 harness,
6106 query,
6107 profile: request.profile,
6108 regex: request.regex,
6109 cwd: request.cwd,
6110 homes: request.homes,
6111 })
6112 .map_err(to_service)?;
6113 Ok(json!({
6114 "schema": crate::memory::MEMORY_SCHEMA,
6115 "matches": matches,
6116 }))
6117 }
6118 _ => Err(ServiceError::MethodNotFound),
6119 }
6120}
6121
6122#[derive(Debug, Clone, Deserialize)]
6126#[serde(default)]
6127struct ProfilesQuery {
6128 harness: Option<String>,
6130 name: Option<String>,
6132 homes: crate::HarnessHomes,
6134}
6135
6136impl Default for ProfilesQuery {
6137 fn default() -> Self {
6138 Self {
6139 harness: None,
6140 name: None,
6141 homes: crate::HarnessHomes::default(),
6142 }
6143 }
6144}
6145
6146fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6149 let query = decode::<ProfilesQuery>(params)?;
6150 let to_service = |error: crate::profiles::ProfileError| match error {
6151 crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6152 ServiceError::UnsupportedAction(error.to_string())
6153 }
6154 crate::profiles::ProfileError::NotFound { .. } => {
6155 ServiceError::InvalidParams(error.to_string())
6156 }
6157 };
6158 match method {
6159 "harness.v1.profiles.list" => {
6160 let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6161 .map_err(to_service)?;
6162 Ok(json!({
6163 "schema": crate::profiles::PROFILES_SCHEMA,
6164 "profiles": profiles,
6165 }))
6166 }
6167 "harness.v1.profiles.get" => {
6168 let harness = query
6169 .harness
6170 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6171 let name = query
6172 .name
6173 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6174 let profile =
6175 crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6176 Ok(json!({
6177 "schema": crate::profiles::PROFILES_SCHEMA,
6178 "profile": profile,
6179 }))
6180 }
6181 _ => Err(ServiceError::MethodNotFound),
6182 }
6183}
6184
6185#[derive(Debug, Clone, Deserialize)]
6189#[serde(default)]
6190struct ChannelsQuery {
6191 harness: Option<String>,
6193 name: Option<String>,
6195 homes: crate::HarnessHomes,
6197}
6198
6199impl Default for ChannelsQuery {
6200 fn default() -> Self {
6201 Self {
6202 harness: None,
6203 name: None,
6204 homes: crate::HarnessHomes::default(),
6205 }
6206 }
6207}
6208
6209#[derive(Debug, Clone, Deserialize)]
6213#[serde(default)]
6214struct RoutesQuery {
6215 harness: Option<String>,
6216 profile: Option<String>,
6218 homes: crate::HarnessHomes,
6219}
6220
6221impl Default for RoutesQuery {
6222 fn default() -> Self {
6223 Self {
6224 harness: None,
6225 profile: None,
6226 homes: crate::HarnessHomes::default(),
6227 }
6228 }
6229}
6230
6231#[derive(Debug, Clone, Deserialize)]
6232#[serde(default)]
6233struct TriggersQuery {
6234 harness: Option<String>,
6235 homes: crate::HarnessHomes,
6236}
6237
6238impl Default for TriggersQuery {
6239 fn default() -> Self {
6240 Self {
6241 harness: None,
6242 homes: crate::HarnessHomes::default(),
6243 }
6244 }
6245}
6246
6247fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6248 let query = decode::<TriggersQuery>(params)?;
6249 let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6250 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6251 Ok(json!({
6252 "schema": crate::triggers::TRIGGERS_SCHEMA,
6253 "triggers": triggers,
6254 }))
6255}
6256
6257fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6258 let query = decode::<RoutesQuery>(params)?;
6259 let routes = crate::routes::list_routes(
6260 &query.homes,
6261 query.harness.as_deref(),
6262 query.profile.as_deref(),
6263 )
6264 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6265 Ok(json!({
6266 "schema": crate::routes::ROUTES_SCHEMA,
6267 "routes": routes,
6268 }))
6269}
6270
6271fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6272 let query = decode::<ChannelsQuery>(params)?;
6273 let to_service = |error: crate::channels::ChannelError| match error {
6274 crate::channels::ChannelError::UnsupportedHarness { .. } => {
6275 ServiceError::UnsupportedAction(error.to_string())
6276 }
6277 crate::channels::ChannelError::NotFound { .. } => {
6278 ServiceError::InvalidParams(error.to_string())
6279 }
6280 };
6281 match method {
6282 "harness.v1.channels.list" => {
6283 let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6284 .map_err(to_service)?;
6285 Ok(json!({
6286 "schema": crate::channels::CHANNELS_SCHEMA,
6287 "channels": channels,
6288 }))
6289 }
6290 "harness.v1.channels.status" => {
6291 let harness = query
6292 .harness
6293 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6294 let name = query
6295 .name
6296 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6297 let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6298 .map_err(to_service)?;
6299 Ok(json!({
6300 "schema": crate::channels::CHANNELS_SCHEMA,
6301 "channel": channel,
6302 }))
6303 }
6304 _ => Err(ServiceError::MethodNotFound),
6305 }
6306}
6307
6308fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6309 json!({
6310 "jsonrpc": "2.0",
6311 "id": id,
6312 "error": {"code": code, "message": message},
6313 })
6314}
6315
6316#[cfg(test)]
6317mod tests {
6318 use super::*;
6319 use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6320 use async_trait::async_trait;
6321 use std::io::Write;
6322 use std::path::PathBuf;
6323 use std::time::Instant;
6324
6325 #[test]
6326 fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6327 let descriptor = SessionDescriptor {
6328 locator: SessionLocator {
6329 harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6330 session_id: "live-session".into(),
6331 storage: StorageLocator::File {
6332 path: PathBuf::from("/tmp/live-session.jsonl"),
6333 },
6334 },
6335 cwd: Some(PathBuf::from("/project")),
6336 title: None,
6337 preview_candidates: Vec::new(),
6338 latest_message_candidates: Vec::new(),
6339 updated_at_ms: Some(1),
6340 message_count: None,
6341 model: None,
6342 parent_session_id: None,
6343 child_session_count: 0,
6344 nouns: Default::default(),
6345 };
6346 let peer = crate::claude_peer::ClaudePeerSession {
6347 pid: 42,
6348 session_id: "live-session".into(),
6349 cwd: Some(PathBuf::from("/project")),
6350 name: "peer".into(),
6351 socket_path: PathBuf::from("/tmp/peer.sock"),
6352 status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6353 updated_at_ms: Some(1),
6354 version: Some("test".into()),
6355 };
6356
6357 let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6358 assert!(value["live_endpoint"]
6359 .as_str()
6360 .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6361 }
6362
6363 struct EndingRuntime {
6364 handle: RuntimeHandle,
6365 event: Option<HarnessEvent>,
6366 close_failures: usize,
6367 }
6368
6369 #[async_trait]
6370 impl RuntimeConnection for EndingRuntime {
6371 fn handle(&self) -> &RuntimeHandle {
6372 &self.handle
6373 }
6374
6375 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6376 unreachable!("ending runtime does not accept input")
6377 }
6378
6379 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6380 Ok(self.event.take())
6381 }
6382
6383 async fn interrupt(&mut self) -> crate::Result<()> {
6384 Ok(())
6385 }
6386
6387 async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6388 Ok(())
6389 }
6390
6391 async fn close(&mut self) -> crate::Result<()> {
6392 if self.close_failures > 0 {
6393 self.close_failures -= 1;
6394 return Err(crate::Error::Other(
6395 "cleanup temporarily unavailable".into(),
6396 ));
6397 }
6398 Ok(())
6399 }
6400 }
6401
6402 fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6403 Box::new(EndingRuntime {
6404 handle: RuntimeHandle {
6405 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6406 runtime_id: "ending-session".into(),
6407 endpoint: RuntimeEndpoint::LocalProcess {
6408 pid: None,
6409 command: vec!["ending-runtime".into()],
6410 protocol: "test".into(),
6411 },
6412 },
6413 event,
6414 close_failures: 0,
6415 })
6416 }
6417
6418 #[tokio::test]
6419 async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6420 let mut service = HarnessSessionService::new();
6421 let handle = ending_runtime(None).handle().clone();
6422 let runtime_id = handle.runtime_id.clone();
6423 let opened = service
6424 .insert_runtime(Box::new(EndingRuntime {
6425 handle,
6426 event: None,
6427 close_failures: 1,
6428 }))
6429 .unwrap();
6430 let connection = opened["connection"].as_str().unwrap().to_string();
6431 service.terminal_launches.insert(
6432 connection.clone(),
6433 StructuredLaunch {
6434 cwd: PathBuf::from("/fixture"),
6435 program: "fixture".into(),
6436 arguments: Vec::new(),
6437 env: BTreeMap::new(),
6438 },
6439 );
6440 let first = service
6441 .handle_async(request(
6442 1,
6443 "harness.v1.runtimes.close",
6444 json!({"connection": connection}),
6445 ))
6446 .await;
6447 assert!(first.get("error").is_some(), "{first}");
6449 assert!(!service.runtimes.contains_key(&connection));
6454 assert!(!service.terminal_launches.contains_key(&connection));
6455 assert!(!service.runtime_sequences.contains_key(&runtime_id));
6456 let again = service
6457 .handle_async(request(
6458 2,
6459 "harness.v1.runtimes.close",
6460 json!({"connection": connection}),
6461 ))
6462 .await;
6463 assert_eq!(again["error"]["code"], -32602, "{again}");
6464 }
6465
6466 fn request(id: u64, method: &str, params: Value) -> Value {
6467 json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6468 }
6469
6470 fn hermes_store() -> PathBuf {
6473 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6474 }
6475
6476 fn hermes_discovery(params: Value) -> Value {
6480 let mut response =
6481 HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6482 let store = hermes_store().display().to_string();
6483 for session in response["result"]["sessions"]
6484 .as_array_mut()
6485 .expect("sessions array")
6486 {
6487 if session["locator"]["storage"]["path"] == json!(store) {
6488 session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6489 }
6490 session.as_object_mut().unwrap().remove("activity");
6494 }
6495 response["result"].take()
6496 }
6497
6498 fn hermes_query() -> Value {
6499 json!({
6500 "harnesses": ["hermes"],
6501 "homes": {"hermes": hermes_store()},
6502 })
6503 }
6504
6505 fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6506 result["sessions"]
6507 .as_array()
6508 .expect("sessions array")
6509 .iter()
6510 .find(|session| session["locator"]["session_id"] == json!(id))
6511 .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6512 }
6513
6514 #[test]
6515 fn orch6_discover_rows_carry_the_conversation_nouns() {
6516 let result = hermes_discovery(hermes_query());
6517
6518 let dm = row(&result, "tg-dm-1");
6521 assert_eq!(dm["trigger"], json!("channel"));
6522 assert_eq!(dm["surface"]["platform"], json!("telegram"));
6523 assert_eq!(dm["surface"]["kind"], json!("dm"));
6524 assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6525 assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6526 assert_eq!(
6527 dm["workspace"],
6528 json!({"kind": "channel", "value": "telegram:123456"})
6529 );
6530 assert!(dm.get("profile").is_none(), "{dm:#}");
6531
6532 let fire = row(&result, "cron_job42_20260902_120000");
6534 assert_eq!(fire["trigger"], json!("cron"));
6535 assert_eq!(
6536 fire["recurrence"],
6537 json!({"job_id": "job42", "kind": "cron"})
6538 );
6539 assert_eq!(fire["workspace"]["kind"], json!("repo"));
6540
6541 let coder = row(&result, "tg-coder-1");
6544 assert_eq!(coder["trigger"], json!("channel"));
6545 assert_eq!(coder["profile"], json!("coder"));
6546 assert_eq!(coder["surface"]["thread_id"], json!("55"));
6547 assert_eq!(
6548 coder["surface"]["key"],
6549 json!("agent:coder:telegram:group:-100777:55")
6550 );
6551 assert_eq!(
6552 coder["workspace"],
6553 json!({"kind": "repo", "value": "/workspace/project"})
6554 );
6555 assert_eq!(
6556 coder["cross_surface"],
6557 json!({"state": "pending", "platform": "discord"})
6558 );
6559
6560 let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6562 assert_eq!(acp["trigger"], json!("human"));
6563 assert!(acp.get("surface").is_none(), "{acp:#}");
6564 assert_eq!(acp["workspace"], json!({"kind": "none"}));
6565 }
6566
6567 #[test]
6568 fn orch6_discover_filters_by_harness_and_profile() {
6569 let mut params = hermes_query();
6570 params["profile"] = json!("coder");
6571 let result = hermes_discovery(params);
6572 let ids: Vec<&str> = result["sessions"]
6573 .as_array()
6574 .expect("sessions array")
6575 .iter()
6576 .map(|session| session["locator"]["session_id"].as_str().unwrap())
6577 .collect();
6578 assert_eq!(ids, vec!["tg-coder-1"]);
6579
6580 let mut missing = hermes_query();
6583 missing["profile"] = json!("nobody");
6584 assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6585
6586 let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6589 assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6590 }
6591
6592 #[test]
6593 fn orch6_load_reports_the_same_nouns_as_discovery() {
6594 let mut service = HarnessSessionService::new();
6595 let loaded = service.handle(request(
6596 1,
6597 "harness.v1.sessions.load",
6598 json!({"locator": {
6599 "harness": "hermes",
6600 "session_id": "tg-coder-1",
6601 "storage": {"kind": "file", "path": hermes_store()},
6602 }}),
6603 ));
6604 let session = &loaded["result"]["session"];
6605 let discovered = hermes_discovery(hermes_query());
6606 let row = row(&discovered, "tg-coder-1");
6607 for noun in [
6608 "trigger",
6609 "surface",
6610 "profile",
6611 "recurrence",
6612 "cross_surface",
6613 "workspace",
6614 ] {
6615 assert_eq!(
6616 session[noun],
6617 row.get(noun).cloned().unwrap_or(Value::Null),
6618 "`{noun}` disagrees between sessions.load and sessions.discover"
6619 );
6620 }
6621 }
6622
6623 fn profile_fixture_homes() -> Value {
6626 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6627 json!({
6628 "hermes": fixtures.join("hermes_home/state.db"),
6629 "openclaw": fixtures.join("openclaw_home"),
6630 })
6631 }
6632
6633 fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6634 response["result"]["profiles"]
6635 .as_array()
6636 .unwrap_or_else(|| panic!("no profiles array in {response}"))
6637 .iter()
6638 .find(|row| row["harness"] == harness && row["name"] == name)
6639 .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6640 }
6641
6642 #[test]
6647 fn profiles_list_reads_every_source_uniformly() {
6648 let mut service = HarnessSessionService::new();
6649 let response = service.handle(request(
6650 1,
6651 "harness.v1.profiles.list",
6652 json!({"homes": profile_fixture_homes()}),
6653 ));
6654 assert_eq!(
6655 response["result"]["schema"],
6656 crate::profiles::PROFILES_SCHEMA
6657 );
6658
6659 let default = profile_row(&response, "hermes", "default");
6660 assert_eq!(default["kind"], "hermes_profile");
6661 assert_eq!(default["default"], true);
6662 assert_eq!(default["routes"], 0);
6663 assert_eq!(default["sessions"], 11);
6664 assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6665
6666 let coder = profile_row(&response, "hermes", "coder");
6667 assert_eq!(coder["kind"], "hermes_profile");
6668 assert_eq!(coder["default"], false);
6669 assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6670 assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6671 assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6672 assert!(coder["home"]
6673 .as_str()
6674 .unwrap()
6675 .ends_with("hermes_home/profiles/coder"));
6676
6677 let main = profile_row(&response, "openclaw", "main");
6678 assert_eq!(main["kind"], "openclaw_agent");
6679 assert_eq!(main["default"], true);
6682 assert_eq!(main["routes"], 0);
6683 assert_eq!(main["sessions"], 4);
6684 assert_eq!(
6685 main["model"],
6686 Value::Null,
6687 "`agents.defaults.model` is an install default, not this agent's pin"
6688 );
6689
6690 let design = profile_row(&response, "openclaw", "design");
6691 assert_eq!(design["default"], false);
6692 assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6693 assert_eq!(design["sessions"], 0);
6694 assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6695
6696 let preset = profile_row(&response, "supercode", "supercode-default");
6697 assert_eq!(preset["kind"], "preset");
6698 assert_eq!(preset["default"], true);
6699 assert_eq!(preset["home"], Value::Null);
6700 assert_eq!(preset["routes"], Value::Null);
6701 }
6702
6703 #[test]
6706 fn profiles_list_reads_codex_profile_tables() {
6707 let codex_home = std::env::temp_dir().join(format!(
6708 "supercode-orch10-codex-{}-{}",
6709 std::process::id(),
6710 std::time::SystemTime::now()
6711 .duration_since(std::time::UNIX_EPOCH)
6712 .unwrap()
6713 .as_nanos()
6714 ));
6715 std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6716 std::fs::write(
6717 codex_home.join("config.toml"),
6718 "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6719 )
6720 .unwrap();
6721
6722 let mut service = HarnessSessionService::new();
6723 let response = service.handle(request(
6724 1,
6725 "harness.v1.profiles.list",
6726 json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6727 ));
6728 let rows = response["result"]["profiles"].as_array().unwrap();
6729 assert_eq!(rows.len(), 2, "{response}");
6730 let review = profile_row(&response, "codex", "review");
6731 assert_eq!(review["kind"], "codex_profile");
6732 assert_eq!(review["default"], true);
6733 assert_eq!(review["model"], "gpt-5.1-codex");
6734 assert_eq!(review["home"], Value::Null);
6735 assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6736
6737 let got = service.handle(request(
6738 2,
6739 "harness.v1.profiles.get",
6740 json!({
6741 "harness": "codex",
6742 "name": "fast",
6743 "homes": {"codex": codex_home.join("sessions")},
6744 }),
6745 ));
6746 assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6747 std::fs::remove_dir_all(&codex_home).ok();
6748 }
6749
6750 #[test]
6753 fn profiles_refuse_harnesses_without_the_concept() {
6754 let mut service = HarnessSessionService::new();
6755 let response = service.handle(request(
6756 1,
6757 "harness.v1.profiles.list",
6758 json!({"harness": "claude-code"}),
6759 ));
6760 assert_eq!(response["error"]["code"], -32020, "{response}");
6761
6762 let missing = service.handle(request(
6763 2,
6764 "harness.v1.profiles.get",
6765 json!({
6766 "harness": "hermes",
6767 "name": "no-such-profile",
6768 "homes": profile_fixture_homes(),
6769 }),
6770 ));
6771 assert_eq!(missing["error"]["code"], -32602, "{missing}");
6772 }
6773
6774 #[test]
6777 fn profiles_methods_are_advertised() {
6778 let mut service = HarnessSessionService::new();
6779 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6780 let methods = response["result"]["methods"].as_array().unwrap();
6781 for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6782 assert!(
6783 methods.iter().any(|entry| entry == method),
6784 "{method} is not advertised"
6785 );
6786 }
6787 }
6788
6789 fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6794 response["result"]["channels"]
6795 .as_array()
6796 .unwrap_or_else(|| panic!("no channels array in {response}"))
6797 .iter()
6798 .find(|row| row["harness"] == harness && row["name"] == name)
6799 .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6800 }
6801
6802 fn channels_list(harness: Option<&str>) -> Value {
6803 let mut params = json!({"homes": profile_fixture_homes()});
6804 if let Some(harness) = harness {
6805 params["harness"] = json!(harness);
6806 }
6807 HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6808 }
6809
6810 #[test]
6814 fn channels_list_reads_both_gateway_harnesses_uniformly() {
6815 let response = channels_list(None);
6816 assert_eq!(
6817 response["result"]["schema"],
6818 crate::channels::CHANNELS_SCHEMA
6819 );
6820
6821 let telegram = channel_row(&response, "hermes", "telegram");
6824 assert_eq!(telegram["kind"], "telegram");
6825 assert_eq!(telegram["enabled"], true);
6826 assert_eq!(telegram["configured"], true);
6827 assert_eq!(telegram["sessions"], 2);
6831 let api = channel_row(&response, "hermes", "api_server");
6832 assert_eq!(api["configured"], true, "extra.key is a credential key");
6833 assert_eq!(api["sessions"], 0);
6834 let webhook = channel_row(&response, "hermes", "webhook");
6835 assert_eq!(webhook["enabled"], false);
6836 assert_eq!(webhook["configured"], true);
6839
6840 let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6842 assert_eq!(linked["kind"], "slack");
6843 assert_eq!(linked["account"], "T0FIXTURE");
6844 assert_eq!(linked["enabled"], true);
6845 assert_eq!(linked["configured"], true);
6846 let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6847 assert_eq!(unlinked["enabled"], false);
6848 assert_eq!(
6849 unlinked["configured"], false,
6850 "an account with no credential key is not configured"
6851 );
6852 let telegram = channel_row(&response, "openclaw", "telegram");
6855 assert_eq!(telegram["account"], "hermes-fixture-bot");
6856 assert_eq!(telegram["configured"], true);
6857
6858 for row in response["result"]["channels"].as_array().unwrap() {
6860 assert_eq!(row["status"], "unknown", "{row}");
6861 }
6862 }
6863
6864 #[test]
6868 fn channels_rows_never_carry_a_fixture_secret() {
6869 let secrets = [
6870 "FAKE-TOKEN-DO-NOT-EMIT",
6871 "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6872 "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6873 "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6874 "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6875 ];
6876 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6878 let raw = format!(
6879 "{}{}",
6880 std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6881 std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6882 );
6883 for secret in secrets {
6884 assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6885 }
6886
6887 let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6888 for secret in secrets {
6889 assert!(
6890 !emitted.contains(secret),
6891 "`{secret}` leaked into a channel row: {emitted}"
6892 );
6893 }
6894 for row in channels_list(None)["result"]["channels"]
6897 .as_array()
6898 .unwrap()
6899 {
6900 for key in row.as_object().unwrap().keys() {
6901 let key = key.to_ascii_lowercase();
6902 assert!(
6903 !["token", "key", "secret", "password", "credential"]
6904 .iter()
6905 .any(|marker| key.ends_with(marker)),
6906 "`{key}` is a credential-shaped field on a channel row"
6907 );
6908 }
6909 }
6910 }
6911
6912 #[test]
6914 fn channels_status_reads_one_row_by_name() {
6915 let mut service = HarnessSessionService::new();
6916 let got = service.handle(request(
6917 1,
6918 "harness.v1.channels.status",
6919 json!({
6920 "harness": "openclaw",
6921 "name": "slack/T0FIXTURE",
6922 "homes": profile_fixture_homes(),
6923 }),
6924 ));
6925 assert_eq!(got["result"]["channel"]["kind"], "slack");
6926 assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6927 assert_eq!(got["result"]["channel"]["status"], "unknown");
6928
6929 let missing = service.handle(request(
6930 2,
6931 "harness.v1.channels.status",
6932 json!({
6933 "harness": "openclaw",
6934 "name": "no-such-channel",
6935 "homes": profile_fixture_homes(),
6936 }),
6937 ));
6938 assert_eq!(missing["error"]["code"], -32602, "{missing}");
6939 }
6940
6941 #[test]
6945 fn channels_refuse_harnesses_without_the_concept() {
6946 let response = channels_list(Some("claude-code"));
6947 assert_eq!(response["error"]["code"], -32020, "{response}");
6948 let codex = channels_list(Some("codex"));
6949 assert_eq!(codex["error"]["code"], -32020, "{codex}");
6950 }
6951
6952 #[test]
6954 fn channels_list_filters_by_harness() {
6955 let response = channels_list(Some("openclaw"));
6956 let rows = response["result"]["channels"].as_array().unwrap();
6957 assert!(!rows.is_empty(), "{response}");
6958 assert!(
6959 rows.iter().all(|row| row["harness"] == "openclaw"),
6960 "harness filter leaked: {response}"
6961 );
6962 }
6963
6964 #[test]
6967 fn channels_methods_are_advertised() {
6968 let mut service = HarnessSessionService::new();
6969 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6970 let methods = response["result"]["methods"].as_array().unwrap();
6971 for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6972 assert!(
6973 methods.iter().any(|entry| entry == method),
6974 "{method} is not advertised"
6975 );
6976 }
6977 }
6978
6979 #[test]
6984 fn orch6_story_fixture_matches_the_live_discovery_response() {
6985 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6986 .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6987 let mut result = hermes_discovery(hermes_query());
6988 result.as_object_mut().unwrap().remove("next_cursor");
6992 let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6993 if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6994 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6995 std::fs::write(&path, &rendered).unwrap();
6996 }
6997 let committed = std::fs::read_to_string(&path).unwrap_or_default();
6998 assert_eq!(
6999 committed, rendered,
7000 "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
7001 re-run with SUPERCODE_UPDATE_FIXTURES=1"
7002 );
7003 }
7004
7005 fn pi_locator() -> SessionLocator {
7006 SessionLocator {
7007 harness: HarnessId::from(HarnessId::PI),
7008 session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
7009 storage: StorageLocator::File {
7010 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7011 .join("tests/fixtures/pi_session.jsonl"),
7012 },
7013 }
7014 }
7015
7016 fn opencode_locator() -> SessionLocator {
7017 let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
7018 SessionLocator {
7019 harness: HarnessId::from(HarnessId::OPENCODE),
7020 session_id: session_id.into(),
7021 storage: StorageLocator::Sqlite {
7022 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7023 .join("tests/fixtures/opencode_fixture/opencode.db"),
7024 selector: session_id.into(),
7025 },
7026 }
7027 }
7028
7029 fn grok_locator() -> SessionLocator {
7030 SessionLocator {
7031 harness: HarnessId::from(HarnessId::GROK),
7032 session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
7033 storage: StorageLocator::File {
7034 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7035 .join("tests/fixtures/grok_session/chat_history.jsonl"),
7036 },
7037 }
7038 }
7039
7040 fn fixture_homes() -> Value {
7043 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7044 json!({
7045 "claude_code": fixtures.join("__absent__"),
7046 "codex": fixtures.join("__absent__"),
7047 "opencode": fixtures.join("__absent__"),
7048 "pi": fixtures.join("__absent__"),
7049 "agents": fixtures.join("__absent__"),
7050 "hermes": fixtures.join("hermes_home"),
7051 "openclaw": fixtures.join("openclaw_home"),
7052 })
7053 }
7054
7055 #[test]
7056 fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
7057 let root = std::env::temp_dir().join(format!(
7058 "supercode-preview-rpc-{}-{}",
7059 std::process::id(),
7060 std::time::SystemTime::now()
7061 .duration_since(std::time::UNIX_EPOCH)
7062 .unwrap()
7063 .as_nanos()
7064 ));
7065 std::fs::create_dir_all(&root).unwrap();
7066 for id in ["first", "second"] {
7067 std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7068 json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7069 json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7070 )).unwrap();
7071 }
7072 let mut service = HarnessSessionService::new();
7073 let query = json!({
7074 "harnesses": ["codex"], "homes": {"codex": root},
7075 "query": "nebula", "search_previews": true, "limit": 1
7076 });
7077 let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7078 assert!(first.get("error").is_none(), "{first}");
7079 assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7080 assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7081 let mut next_query = query.clone();
7082 next_query["cursor"] = first["result"]["next_cursor"].clone();
7083 let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7084 assert_eq!(next["result"]["receipt"]["returned"], 1);
7085 assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7086 assert_eq!(next["result"]["receipt"]["truncated"], false);
7087 assert_ne!(
7088 first["result"]["sessions"][0]["locator"],
7089 next["result"]["sessions"][0]["locator"]
7090 );
7091 let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7092 assert!(
7093 refused["error"]["message"]
7094 .as_str()
7095 .unwrap()
7096 .contains("use sessions.discover"),
7097 "{refused}"
7098 );
7099 std::fs::remove_dir_all(root).unwrap();
7100 }
7101
7102 #[test]
7103 fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7104 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7105 let root = std::env::temp_dir().join(format!(
7106 "supercode-index-rpc-{}-{}",
7107 std::process::id(),
7108 std::time::SystemTime::now()
7109 .duration_since(std::time::UNIX_EPOCH)
7110 .unwrap()
7111 .as_nanos()
7112 ));
7113 std::fs::create_dir_all(&root).unwrap();
7114 for id in ["first", "second"] {
7115 std::fs::write(root.join(format!("{id}.jsonl")), format!(
7116 "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7117 )).unwrap();
7118 }
7119 let mut service = HarnessSessionService::new();
7120 let opened = service.handle(request(
7121 1,
7122 "harness.v1.sessions.index.subscribe",
7123 json!({
7124 "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7125 }),
7126 ));
7127 assert!(opened.get("error").is_none(), "{opened:#}");
7128 let subscription = opened["result"]["subscription"]
7129 .as_str()
7130 .unwrap()
7131 .to_owned();
7132 assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7133 for params in [
7134 json!({"subscription": subscription, "limit": 0}),
7135 json!({"subscription": subscription, "limit": 2049}),
7136 json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7137 json!({"subscription": "unknown", "limit": 2}),
7138 ] {
7139 let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7140 assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7141 }
7142 for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7143 let response = service.handle(request(
7144 3,
7145 "harness.v1.sessions.index.resize",
7146 json!({
7147 "subscription": subscription, "limit": limit
7148 }),
7149 ));
7150 assert!(response.get("error").is_none(), "{response:#}");
7151 assert_eq!(response["result"]["subscription"], subscription);
7152 assert_eq!(response["result"]["revision"], revision);
7153 assert_eq!(
7154 response["result"]["initial"].as_array().unwrap().len(),
7155 limit
7156 );
7157 assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7158 assert_eq!(service.index_subscriptions.len(), 1);
7159 }
7160 let removed = service.handle(request(
7161 4,
7162 "harness.v1.sessions.index.unsubscribe",
7163 json!({
7164 "subscription": subscription
7165 }),
7166 ));
7167 assert_eq!(removed["result"]["removed"], true);
7168 let stale = service.handle(request(
7169 5,
7170 "harness.v1.sessions.index.resize",
7171 json!({
7172 "subscription": subscription, "limit": 1
7173 }),
7174 ));
7175 assert_eq!(stale["error"]["code"], -32602);
7176 drop(service);
7177 std::fs::remove_dir_all(root).unwrap();
7178 }
7179
7180 fn skills_rows(params: Value) -> Vec<Value> {
7181 let response =
7182 HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7183 assert!(response.get("error").is_none(), "{response:#}");
7184 response["result"].as_array().cloned().unwrap_or_default()
7185 }
7186
7187 #[test]
7190 fn skills_list_reads_the_hermes_and_openclaw_roots() {
7191 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7192 let rows = skills_rows(json!({
7193 "homes": fixture_homes(),
7194 "cwd": fixtures.join("hermes_home"),
7195 }));
7196 let arxiv = rows
7197 .iter()
7198 .find(|row| row["name"] == json!("arxiv-search"))
7199 .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7200 assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7201 assert_eq!(arxiv["scope"], json!("user"));
7202 assert_eq!(arxiv["version"], json!("1.4.0"));
7203 assert!(arxiv["location"]
7204 .as_str()
7205 .unwrap()
7206 .ends_with("hermes_home/skills/research/arxiv"));
7207
7208 let bare = rows
7210 .iter()
7211 .find(|row| row["name"] == json!("bare-skill"))
7212 .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7213 assert_eq!(bare["enabled"], json!(null));
7214 assert!(bare.get("description").is_none());
7215
7216 let demo = rows
7217 .iter()
7218 .find(|row| row["name"] == json!("clawhub-demo"))
7219 .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7220 assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7221 assert_eq!(demo["scope"], json!("managed"));
7222 assert_eq!(demo["enabled"], json!(false));
7223 }
7224
7225 #[test]
7227 fn skills_list_filters_by_harness_and_scope() {
7228 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7229 let hermes = skills_rows(json!({
7230 "homes": fixture_homes(),
7231 "cwd": fixtures.join("hermes_home"),
7232 "harness": HarnessId::HERMES,
7233 }));
7234 assert!(!hermes.is_empty());
7235 assert!(hermes
7236 .iter()
7237 .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7238
7239 let managed = skills_rows(json!({
7240 "homes": fixture_homes(),
7241 "cwd": fixtures.join("openclaw_home"),
7242 "harness": HarnessId::OPENCLAW,
7243 "scope": "managed",
7244 }));
7245 assert_eq!(managed.len(), 1, "{managed:#?}");
7246 assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7247
7248 let bundled = skills_rows(json!({
7249 "homes": fixture_homes(),
7250 "cwd": fixtures.join("openclaw_home"),
7251 "harness": HarnessId::OPENCLAW,
7252 "scope": "bundled",
7253 }));
7254 assert!(bundled.is_empty(), "{bundled:#?}");
7255 }
7256
7257 #[test]
7260 fn skills_list_refuses_an_unknown_harness() {
7261 let response = HarnessSessionService::new().handle(request(
7262 1,
7263 "harness.v1.skills.list",
7264 json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7265 ));
7266 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7267 assert!(response["error"]["message"]
7268 .as_str()
7269 .unwrap()
7270 .contains("not-a-harness"));
7271 }
7272
7273 #[test]
7275 fn skills_list_is_an_advertised_method_and_sdk_operation() {
7276 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7277 assert_eq!(
7278 SdkOperation::from_method("harness.v1.skills.list"),
7279 Some(SdkOperation::SkillsList)
7280 );
7281 }
7282
7283 #[test]
7287 fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7288 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7289 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7290 assert_eq!(
7291 SdkOperation::from_method("harness.v1.skills.install"),
7292 Some(SdkOperation::SkillsInstall)
7293 );
7294 assert_eq!(
7295 SdkOperation::from_method("harness.v1.skills.remove"),
7296 Some(SdkOperation::SkillsRemove)
7297 );
7298 }
7299
7300 #[test]
7304 fn skills_install_and_remove_drive_the_directory_door() {
7305 let root = std::env::temp_dir().join(format!(
7306 "supercode-orch22-rpc-{}-{}",
7307 std::process::id(),
7308 std::time::SystemTime::now()
7309 .duration_since(std::time::UNIX_EPOCH)
7310 .unwrap()
7311 .as_nanos()
7312 ));
7313 let source = root.join("probe-src");
7314 std::fs::create_dir_all(&source).unwrap();
7315 std::fs::write(
7316 source.join("SKILL.md"),
7317 "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7318 )
7319 .unwrap();
7320 let homes = json!({
7321 "claude_code": root.join("claude_home"),
7322 "codex": root.join("__absent__"),
7323 "opencode": root.join("__absent__"),
7324 "pi": root.join("__absent__"),
7325 "hermes": root.join("__absent__"),
7326 "openclaw": root.join("__absent__"),
7327 "agents": root.join("__absent__"),
7328 });
7329
7330 let mut service = HarnessSessionService::new();
7331 let installed = service.handle(request(
7332 1,
7333 "harness.v1.skills.install",
7334 json!({
7335 "harness": HarnessId::CLAUDE_CODE,
7336 "source": source,
7337 "scope": "user",
7338 "cwd": root,
7339 "homes": homes,
7340 }),
7341 ));
7342 let result = &installed["result"];
7343 assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7344 assert_eq!(result["verb"], json!("install"));
7345 assert!(result["ran"]
7346 .as_str()
7347 .is_some_and(|ran| ran.starts_with("cp -R ")));
7348 assert_eq!(result["skill"]["scope"], json!("user"));
7349
7350 let removed = service.handle(request(
7351 2,
7352 "harness.v1.skills.remove",
7353 json!({
7354 "harness": HarnessId::CLAUDE_CODE,
7355 "name": "orch22-rpc",
7356 "scope": "user",
7357 "cwd": root,
7358 "homes": homes,
7359 }),
7360 ));
7361 assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7362 assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7363 std::fs::remove_dir_all(&root).ok();
7364 }
7365
7366 #[test]
7369 fn skills_remove_refuses_openclaw_at_the_pin() {
7370 let response = HarnessSessionService::new().handle(request(
7371 1,
7372 "harness.v1.skills.remove",
7373 json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7374 ));
7375 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7376 assert!(response["error"]["message"]
7377 .as_str()
7378 .unwrap()
7379 .contains("no `skills remove` verb"));
7380 }
7381
7382 #[test]
7385 fn skills_install_refuses_a_harness_without_a_skills_root() {
7386 let response = HarnessSessionService::new().handle(request(
7387 1,
7388 "harness.v1.skills.install",
7389 json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7390 ));
7391 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7392 assert!(response["error"]["message"]
7393 .as_str()
7394 .unwrap()
7395 .contains("not-a-harness"));
7396 }
7397
7398 fn memory_homes() -> Value {
7406 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7407 json!({
7408 "claude_code": fixtures.join("__absent__"),
7409 "codex": fixtures.join("__absent__"),
7410 "opencode": fixtures.join("__absent__"),
7411 "pi": fixtures.join("__absent__"),
7412 "grok": fixtures.join("__absent__"),
7413 "gemini": fixtures.join("__absent__"),
7414 "goose": fixtures.join("__absent__"),
7415 "supercode": fixtures.join("__absent__"),
7416 "hermes": fixtures.join("hermes_home/state.db"),
7417 "openclaw": fixtures.join("openclaw_home"),
7418 })
7419 }
7420
7421 fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7422 let response = HarnessSessionService::new().handle(request(1, method, params));
7423 assert!(response.get("error").is_none(), "{response:#}");
7424 assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7425 response["result"][key]
7426 .as_array()
7427 .cloned()
7428 .unwrap_or_default()
7429 }
7430
7431 fn memory_documents(params: Value) -> Vec<Value> {
7432 memory_call_ok("harness.v1.memory.show", params, "documents")
7433 }
7434
7435 fn memory_matches(params: Value) -> Vec<Value> {
7436 memory_call_ok("harness.v1.memory.search", params, "matches")
7437 }
7438
7439 fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7440 rows.iter()
7441 .find(|row| row["profile"] == profile && row["name"] == name)
7442 .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7443 }
7444
7445 #[test]
7448 fn memory_show_reads_the_hermes_profile_homes() {
7449 let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7450
7451 let notes = find_document(&rows, "default", "MEMORY.md");
7452 assert_eq!(notes["harness"], "hermes");
7453 assert_eq!(notes["scope"], "user");
7454 assert!(notes["size"].as_u64().unwrap() > 0);
7455 assert!(notes["updated_at"].is_string(), "{notes:#?}");
7456 assert!(notes.get("content").is_none(), "{notes:#?}");
7458 assert_eq!(notes["truncated"], true);
7459 assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7460
7461 let user = find_document(&rows, "default", "USER.md");
7462 assert_eq!(user["scope"], "user");
7463 assert!(user["preview"]
7464 .as_array()
7465 .unwrap()
7466 .iter()
7467 .any(|line| line.as_str().unwrap().contains("neovim")));
7468
7469 let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7470 assert!(topic["path"]
7471 .as_str()
7472 .unwrap()
7473 .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7474
7475 let coder = find_document(&rows, "coder", "MEMORY.md");
7477 assert_eq!(coder["scope"], "profile");
7478 assert!(coder["path"]
7479 .as_str()
7480 .unwrap()
7481 .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7482 }
7483
7484 #[test]
7487 fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7488 let rows = memory_documents(json!({
7489 "harness": "hermes",
7490 "profile": "coder",
7491 "full": true,
7492 "homes": memory_homes(),
7493 }));
7494 assert!(
7495 rows.iter().all(|row| row["profile"] == "coder"),
7496 "{rows:#?}"
7497 );
7498 let coder = find_document(&rows, "coder", "MEMORY.md");
7499 assert!(coder["content"]
7500 .as_str()
7501 .expect("full returns the body")
7502 .contains("anthropic/claude-opus-4-8"));
7503 }
7504
7505 #[test]
7509 fn memory_show_reads_the_openclaw_agent_workspaces() {
7510 let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7511
7512 let main = find_document(&rows, "main", "MEMORY.md");
7513 assert_eq!(main["scope"], "agent");
7514 assert!(main["path"]
7515 .as_str()
7516 .unwrap()
7517 .ends_with("openclaw_home/workspace/MEMORY.md"));
7518
7519 let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7520 assert!(topic["path"]
7521 .as_str()
7522 .unwrap()
7523 .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7524
7525 let design = find_document(&rows, "design", "MEMORY.md");
7526 assert!(design["path"]
7527 .as_str()
7528 .unwrap()
7529 .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7530 }
7531
7532 #[test]
7535 fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7536 let scratch = std::env::temp_dir().join(format!(
7537 "supercode-orch12-cc-{}-{}",
7538 std::process::id(),
7539 std::time::SystemTime::now()
7540 .duration_since(std::time::UNIX_EPOCH)
7541 .unwrap()
7542 .as_nanos()
7543 ));
7544 let project = scratch.join("repo");
7545 std::fs::create_dir_all(project.join(".git")).unwrap();
7546 let worktree = project.join("crates/harness");
7549 std::fs::create_dir_all(&worktree).unwrap();
7550 let slug: String = project
7551 .to_string_lossy()
7552 .chars()
7553 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7554 .collect();
7555 let projects = scratch.join("claude/projects");
7556 let memory = projects.join(&slug).join("memory");
7557 std::fs::create_dir_all(&memory).unwrap();
7558 std::fs::write(
7559 memory.join("MEMORY.md"),
7560 "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7561 )
7562 .unwrap();
7563 std::fs::write(
7564 memory.join("build-box.md"),
7565 "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7566 )
7567 .unwrap();
7568
7569 let mut homes = memory_homes();
7570 homes["claude_code"] = json!(projects);
7571 let rows = memory_documents(json!({
7572 "harness": "claude-code",
7573 "cwd": worktree,
7574 "homes": homes,
7575 }));
7576 let index = find_document(&rows, &slug, "MEMORY.md");
7577 assert_eq!(index["harness"], "claude-code");
7578 assert_eq!(index["scope"], "project");
7579 let topic = find_document(&rows, &slug, "build-box.md");
7580 assert!(topic["preview"]
7581 .as_array()
7582 .unwrap()
7583 .iter()
7584 .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7585
7586 let hits = memory_matches(json!({
7587 "harness": "claude-code",
7588 "query": "pinned harnesses",
7589 "cwd": worktree,
7590 "homes": homes,
7591 }));
7592 assert_eq!(hits.len(), 1, "{hits:#?}");
7593 assert_eq!(hits[0]["name"], "MEMORY.md");
7594 assert_eq!(hits[0]["line"], 2);
7595
7596 let _ = std::fs::remove_dir_all(&scratch);
7597 }
7598
7599 #[test]
7605 fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7606 let state = std::env::temp_dir().join(format!(
7607 "supercode-orch12-oc-{}-{}",
7608 std::process::id(),
7609 std::time::SystemTime::now()
7610 .duration_since(std::time::UNIX_EPOCH)
7611 .unwrap()
7612 .as_nanos()
7613 ));
7614 std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7616 std::fs::create_dir_all(state.join("workspace")).unwrap();
7617 std::fs::write(
7618 state.join("workspace/MEMORY.md"),
7619 "the gateway websocket needs credentials\n",
7620 )
7621 .unwrap();
7622
7623 let mut homes = memory_homes();
7624 homes["openclaw"] = json!(state);
7625 let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7626 assert_eq!(rows.len(), 1, "{rows:#?}");
7627 let row = find_document(&rows, "main", "MEMORY.md");
7628 assert_eq!(row["scope"], "agent");
7629 assert!(row["path"]
7630 .as_str()
7631 .unwrap()
7632 .ends_with("workspace/MEMORY.md"));
7633
7634 let _ = std::fs::remove_dir_all(&state);
7635 }
7636
7637 #[test]
7640 fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7641 let hit = memory_matches(json!({
7642 "harness": "hermes",
7643 "query": "NEOVIM",
7644 "homes": memory_homes(),
7645 }));
7646 assert_eq!(hit.len(), 1, "{hit:#?}");
7647 assert_eq!(hit[0]["harness"], "hermes");
7648 assert_eq!(hit[0]["name"], "USER.md");
7649 assert_eq!(hit[0]["scope"], "user");
7650 assert_eq!(hit[0]["line"], 5);
7651 assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7652
7653 let regex = memory_matches(json!({
7655 "harness": "hermes",
7656 "query": "neo(vim|vi)",
7657 "regex": true,
7658 "homes": memory_homes(),
7659 }));
7660 assert_eq!(regex.len(), 1, "{regex:#?}");
7661
7662 let miss = memory_matches(json!({
7663 "harness": "hermes",
7664 "query": "no-memory-line-says-this",
7665 "homes": memory_homes(),
7666 }));
7667 assert!(miss.is_empty(), "{miss:#?}");
7668 }
7669
7670 #[test]
7673 fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7674 for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7675 let response = HarnessSessionService::new().handle(request(
7676 1,
7677 method,
7678 json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7679 ));
7680 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7681 assert!(response["error"]["message"]
7682 .as_str()
7683 .unwrap()
7684 .contains("codex"));
7685 }
7686
7687 let response = HarnessSessionService::new().handle(request(
7688 1,
7689 "harness.v1.memory.show",
7690 json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7691 ));
7692 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7693
7694 let response = HarnessSessionService::new().handle(request(
7696 1,
7697 "harness.v1.memory.show",
7698 json!({"homes": memory_homes()}),
7699 ));
7700 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7701 }
7702
7703 #[test]
7705 fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7706 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7707 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7708 assert_eq!(
7709 SdkOperation::from_method("harness.v1.memory.show"),
7710 Some(SdkOperation::MemoryShow)
7711 );
7712 assert_eq!(
7713 SdkOperation::from_method("harness.v1.memory.search"),
7714 Some(SdkOperation::MemorySearch)
7715 );
7716 }
7717
7718 struct RequestingRuntime {
7723 handle: RuntimeHandle,
7724 events: std::collections::VecDeque<HarnessEvent>,
7725 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7726 }
7727
7728 #[async_trait]
7729 impl RuntimeConnection for RequestingRuntime {
7730 fn handle(&self) -> &RuntimeHandle {
7731 &self.handle
7732 }
7733
7734 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7735 unreachable!("this runtime only raises requests")
7736 }
7737
7738 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7739 match self.events.pop_front() {
7740 Some(event) => Ok(Some(event)),
7741 None => std::future::pending().await,
7745 }
7746 }
7747
7748 async fn interrupt(&mut self) -> crate::Result<()> {
7749 Ok(())
7750 }
7751
7752 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7753 self.answered
7757 .lock()
7758 .unwrap_or_else(std::sync::PoisonError::into_inner)
7759 .push(json!({"request_id": request_id, "response": response}));
7760 Ok(())
7761 }
7762
7763 async fn close(&mut self) -> crate::Result<()> {
7764 Ok(())
7765 }
7766 }
7767
7768 fn requesting_runtime(
7769 harness: &str,
7770 events: Vec<HarnessEvent>,
7771 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7772 ) -> Box<dyn RuntimeConnection> {
7773 requesting_runtime_named(harness, "hermes-live-session", events, answered)
7774 }
7775
7776 fn requesting_runtime_named(
7777 harness: &str,
7778 runtime_id: &str,
7779 events: Vec<HarnessEvent>,
7780 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7781 ) -> Box<dyn RuntimeConnection> {
7782 Box::new(RequestingRuntime {
7783 handle: RuntimeHandle {
7784 harness: HarnessId::from(harness),
7785 runtime_id: runtime_id.into(),
7786 endpoint: RuntimeEndpoint::LocalProcess {
7787 pid: None,
7788 command: vec!["hermes-acp".into()],
7789 protocol: "acp".into(),
7790 },
7791 },
7792 events: events.into(),
7793 answered,
7794 })
7795 }
7796
7797 fn permission_event(id: u64, title: &str) -> HarnessEvent {
7798 HarnessEvent {
7799 sequence: None,
7800 kind: "session/request_permission".into(),
7801 payload: json!({
7802 "jsonrpc": "2.0",
7803 "id": id,
7804 "method": "session/request_permission",
7805 "params": {
7806 "sessionId": "hermes-live-session",
7807 "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7808 "options": [
7809 {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7810 {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7811 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7812 ],
7813 },
7814 }),
7815 }
7816 }
7817
7818 fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7819 let response = service.handle(request(1, "harness.v1.approvals.list", params));
7820 assert!(response.get("error").is_none(), "{response:#}");
7821 response["result"].clone()
7822 }
7823
7824 #[tokio::test]
7832 async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7833 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7834 let mut service = HarnessSessionService::new();
7835 service.runtimes.insert(
7836 "runtime-cc".into(),
7837 requesting_runtime_named(
7838 HarnessId::CLAUDE_CODE,
7839 "claude-live-session",
7840 vec![HarnessEvent {
7841 sequence: None,
7842 kind: "control_request".into(),
7843 payload: json!({
7844 "type": "control_request",
7845 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7846 "request": {
7847 "subtype": "can_use_tool",
7848 "tool_name": "Bash",
7849 "display_name": "Bash",
7850 "input": {"command": "touch probe-artifact.txt"},
7851 "tool_use_id": "toolu_mock_1",
7852 },
7853 }),
7854 }],
7855 answered.clone(),
7856 ),
7857 );
7858
7859 let notifications = service.poll_runtimes().await;
7860 assert_eq!(notifications.len(), 1, "{notifications:#?}");
7861
7862 let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7863 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7864 let row = &rows[0];
7865 assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7866 assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7867 assert_eq!(row["status"], "pending");
7868 assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7869 assert_eq!(row["runtime_id"], "claude-live-session");
7870 assert_eq!(
7871 row["options"]
7872 .as_array()
7873 .unwrap()
7874 .iter()
7875 .map(|option| option["id"].as_str().unwrap())
7876 .collect::<Vec<_>>(),
7877 vec!["allow", "deny"],
7878 );
7879
7880 let response = resolve(
7881 &mut service,
7882 json!({"id": row["id"], "decision": "allow_once"}),
7883 )
7884 .await;
7885 assert!(response.get("error").is_none(), "{response:#}");
7886 assert_eq!(response["result"]["option_id"], "allow");
7887 assert_eq!(
7888 answered
7889 .lock()
7890 .unwrap_or_else(std::sync::PoisonError::into_inner)
7891 .as_slice(),
7892 &[json!({
7893 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7894 "response": {"behavior": "allow"},
7895 })],
7896 );
7897 assert_eq!(
7898 approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7899 .as_array()
7900 .map(Vec::len),
7901 Some(0),
7902 );
7903 }
7904
7905 #[tokio::test]
7909 async fn a_live_permission_request_lists_until_it_is_answered() {
7910 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7911 let mut service = HarnessSessionService::new();
7912 service.runtimes.insert(
7913 "runtime-1".into(),
7914 requesting_runtime(
7915 HarnessId::HERMES,
7916 vec![permission_event(7, "rm -rf build")],
7917 answered.clone(),
7918 ),
7919 );
7920
7921 let notifications = service.poll_runtimes().await;
7922 assert_eq!(notifications.len(), 1, "{notifications:#?}");
7923
7924 let rows = approvals(&mut service, json!({}));
7925 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7926 let row = &rows[0];
7927 assert_eq!(row["id"], "runtime-1/7");
7928 assert_eq!(row["harness"], HarnessId::HERMES);
7929 assert_eq!(row["kind"], "live");
7930 assert_eq!(row["status"], "pending");
7931 assert_eq!(row["subject"], "rm -rf build");
7932 assert_eq!(row["session_id"], "hermes-live-session");
7933 assert_eq!(row["runtime_id"], "hermes-live-session");
7934 assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7935 assert!(
7936 row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7937 "{row:#}"
7938 );
7939 assert_eq!(
7940 row["options"]
7941 .as_array()
7942 .unwrap()
7943 .iter()
7944 .map(|option| option["id"].as_str().unwrap())
7945 .collect::<Vec<_>>(),
7946 vec!["allow_once", "allow_for_session", "deny"],
7947 );
7948
7949 assert_eq!(
7951 approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7952 .as_array()
7953 .map(Vec::len),
7954 Some(1),
7955 );
7956 assert_eq!(
7957 approvals(&mut service, json!({"session": "some-other-session"}))
7958 .as_array()
7959 .map(Vec::len),
7960 Some(0),
7961 );
7962
7963 let response = service
7964 .handle_async(request(
7965 2,
7966 "harness.v1.runtimes.respond",
7967 json!({
7968 "connection": "runtime-1",
7969 "request_id": 7,
7970 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7971 }),
7972 ))
7973 .await;
7974 assert!(response.get("error").is_none(), "{response:#}");
7975 assert_eq!(
7976 answered
7977 .lock()
7978 .unwrap_or_else(std::sync::PoisonError::into_inner)
7979 .as_slice(),
7980 &[json!({
7981 "request_id": 7,
7982 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7983 })],
7984 );
7985
7986 let rows = approvals(&mut service, json!({}));
7987 assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7988 }
7989
7990 #[test]
7993 fn queued_subagent_approvals_list_through_the_same_door() {
7994 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7995 crate::subagents::QueuedApproval {
7996 child_agent_id: "child-7".into(),
7997 tool: "shell".into(),
7998 subject: Some("cargo publish --dry-run".into()),
7999 queued_at_ms: 1,
8000 outcome: None,
8001 },
8002 crate::subagents::QueuedApproval {
8003 child_agent_id: "child-8".into(),
8004 tool: "write_file".into(),
8005 subject: None,
8006 queued_at_ms: 2,
8007 outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
8008 },
8009 ]));
8010 let mut service = HarnessSessionService::new();
8011 service.observe_subagent_approvals(queue);
8012
8013 let rows = approvals(&mut service, json!({}));
8014 assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
8015 assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
8016 assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
8017 assert_eq!(rows[0]["status"], "pending");
8018 assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
8019 assert_eq!(rows[1]["status"], "denied");
8020 assert!(rows[1]["options"].as_array().unwrap().is_empty());
8021
8022 let only = approvals(&mut service, json!({"session": "child-8"}));
8024 assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
8025 assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
8026 }
8027
8028 #[test]
8034 fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
8035 let response = HarnessSessionService::new().handle(request(
8036 1,
8037 "harness.v1.approvals.list",
8038 json!({"harness": "not-a-harness"}),
8039 ));
8040 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
8041 assert!(response["error"]["message"]
8042 .as_str()
8043 .unwrap()
8044 .contains("not-a-harness"));
8045 for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
8046 let response = HarnessSessionService::new().handle(request(
8047 1,
8048 "harness.v1.approvals.list",
8049 json!({"harness": harness}),
8050 ));
8051 assert!(response.get("error").is_none(), "{harness}: {response:#}");
8052 }
8053 }
8054
8055 #[test]
8059 fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
8060 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8061 assert_eq!(
8062 SdkOperation::from_method("harness.v1.approvals.list"),
8063 Some(SdkOperation::ApprovalsList)
8064 );
8065 let registry = harness_support_registry();
8066 for id in [
8067 HarnessId::HERMES,
8068 HarnessId::OPENCLAW,
8069 HarnessId::CODEX,
8070 HarnessId::CLAUDE_CODE,
8073 ] {
8074 let concept = registry
8075 .harnesses
8076 .iter()
8077 .find(|harness| harness.id.as_str() == id)
8078 .unwrap()
8079 .orchestration
8080 .concepts
8081 .iter()
8082 .find(|concept| concept.concept == "pending_request")
8083 .unwrap();
8084 assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8085 assert!(concept
8086 .methods
8087 .iter()
8088 .any(|method| method == "harness.v1.approvals.list"));
8089 }
8090 }
8091
8092 async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8095 service
8096 .handle_async(request(3, "harness.v1.approvals.resolve", params))
8097 .await
8098 }
8099
8100 #[tokio::test]
8104 async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8105 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8106 let mut service = HarnessSessionService::new();
8107 service.runtimes.insert(
8108 "runtime-1".into(),
8109 requesting_runtime(
8110 HarnessId::HERMES,
8111 vec![permission_event(7, "rm -rf build")],
8112 answered.clone(),
8113 ),
8114 );
8115 service.poll_runtimes().await;
8116
8117 let rows = approvals(&mut service, json!({}));
8118 assert_eq!(rows[0]["id"], "runtime-1/7");
8119
8120 let response = resolve(
8121 &mut service,
8122 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8123 )
8124 .await;
8125 assert!(response.get("error").is_none(), "{response:#}");
8126 assert_eq!(
8127 response["result"],
8128 json!({
8129 "id": "runtime-1/7",
8130 "decision": "allow_once",
8131 "option_id": "allow_once",
8132 "resolved": true,
8133 }),
8134 );
8135 assert_eq!(
8137 answered
8138 .lock()
8139 .unwrap_or_else(std::sync::PoisonError::into_inner)
8140 .as_slice(),
8141 &[json!({
8142 "request_id": 7,
8143 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8144 })],
8145 );
8146 assert_eq!(
8148 approvals(&mut service, json!({})).as_array().map(Vec::len),
8149 Some(0),
8150 );
8151 let response = resolve(
8153 &mut service,
8154 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8155 )
8156 .await;
8157 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8158 }
8159
8160 #[tokio::test]
8163 async fn deny_selects_the_requests_own_reject_option() {
8164 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8165 let mut service = HarnessSessionService::new();
8166 service.runtimes.insert(
8167 "runtime-1".into(),
8168 requesting_runtime(
8169 HarnessId::HERMES,
8170 vec![permission_event(11, "git push --force")],
8171 answered.clone(),
8172 ),
8173 );
8174 service.poll_runtimes().await;
8175
8176 let response = resolve(
8177 &mut service,
8178 json!({"id": "runtime-1/11", "decision": "deny"}),
8179 )
8180 .await;
8181 assert!(response.get("error").is_none(), "{response:#}");
8182 assert_eq!(response["result"]["option_id"], "deny");
8184 assert_eq!(
8185 answered
8186 .lock()
8187 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8188 json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8189 );
8190 assert_eq!(
8191 approvals(&mut service, json!({})).as_array().map(Vec::len),
8192 Some(0),
8193 );
8194 }
8195
8196 #[tokio::test]
8199 async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8200 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8201 let mut service = HarnessSessionService::new();
8202 let mut event = permission_event(3, "rm -rf build");
8203 event.payload["params"]["options"] = json!([
8206 {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8207 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8208 ]);
8209 service.runtimes.insert(
8210 "runtime-1".into(),
8211 requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8212 );
8213 service.poll_runtimes().await;
8214
8215 let response = resolve(
8216 &mut service,
8217 json!({"id": "runtime-1/3", "decision": "allow_always"}),
8218 )
8219 .await;
8220 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8221 let message = response["error"]["message"].as_str().unwrap();
8222 assert!(message.contains("allow_always"), "{message}");
8223 assert!(message.contains("allow_once, deny"), "{message}");
8224 assert!(answered
8226 .lock()
8227 .unwrap_or_else(std::sync::PoisonError::into_inner)
8228 .is_empty());
8229 assert_eq!(
8230 approvals(&mut service, json!({})).as_array().map(Vec::len),
8231 Some(1),
8232 );
8233 }
8234
8235 #[tokio::test]
8239 async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8240 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8241 crate::subagents::QueuedApproval {
8242 child_agent_id: "child-7".into(),
8243 tool: "shell".into(),
8244 subject: Some("cargo publish --dry-run".into()),
8245 queued_at_ms: 1,
8246 outcome: None,
8247 },
8248 ]));
8249 let mut service = HarnessSessionService::new();
8250 service.observe_subagent_approvals(queue.clone());
8251 let row = approvals(&mut service, json!({}))[0]["id"]
8252 .as_str()
8253 .unwrap()
8254 .to_string();
8255 assert_eq!(row, "supercode/subagent/child-7/1/0");
8256
8257 let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8258 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8259 let message = response["error"]["message"].as_str().unwrap();
8260 assert!(message.contains("queued subagent record"), "{message}");
8261 assert!(message.contains("request"), "{message}");
8262 assert!(queue
8264 .lock()
8265 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8266 .outcome
8267 .is_none());
8268 }
8269
8270 #[tokio::test]
8273 async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8274 let mut service = HarnessSessionService::new();
8275 let response = resolve(
8276 &mut service,
8277 json!({"id": "runtime-9/4", "decision": "deny"}),
8278 )
8279 .await;
8280 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8281 assert!(response["error"]["message"]
8282 .as_str()
8283 .unwrap()
8284 .contains("runtime-9/4"));
8285
8286 let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8287 let message = response["error"]["message"].as_str().unwrap();
8288 assert!(
8289 message.contains("allow_once | allow_always | deny"),
8290 "{message}"
8291 );
8292
8293 let response = resolve(
8294 &mut service,
8295 json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8296 )
8297 .await;
8298 assert!(response["error"]["message"]
8299 .as_str()
8300 .unwrap()
8301 .contains("not both"));
8302 }
8303
8304 #[test]
8308 fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8309 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8310 assert_eq!(
8311 SdkOperation::from_method("harness.v1.approvals.resolve"),
8312 Some(SdkOperation::ApprovalsResolve)
8313 );
8314 assert_eq!(
8315 SdkOperation::ApprovalsResolve.action_name(),
8316 "approvals_resolve"
8317 );
8318 let registry = harness_support_registry();
8319 for id in [
8320 HarnessId::HERMES,
8321 HarnessId::OPENCLAW,
8322 HarnessId::CODEX,
8323 HarnessId::CLAUDE_CODE,
8326 ] {
8327 let concept = registry
8328 .harnesses
8329 .iter()
8330 .find(|harness| harness.id.as_str() == id)
8331 .unwrap()
8332 .orchestration
8333 .concepts
8334 .iter()
8335 .find(|concept| concept.concept == "pending_request")
8336 .unwrap();
8337 assert_eq!(
8338 concept.controlled,
8339 crate::ImplementationKind::BuiltIn,
8340 "{id}"
8341 );
8342 assert!(
8343 concept
8344 .methods
8345 .iter()
8346 .any(|method| method == "harness.v1.approvals.resolve"),
8347 "{id}"
8348 );
8349 }
8350 }
8351
8352 #[test]
8353 fn capabilities_are_explicit_and_versioned() {
8354 let mut service = HarnessSessionService::new();
8355 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8356 assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8357 assert_eq!(
8358 response["result"]["sdk"]["schema_version"],
8359 crate::SDK_SCHEMA_VERSION
8360 );
8361 assert_eq!(
8362 response["result"]["sdk"]["operations"]
8363 .as_array()
8364 .unwrap()
8365 .len(),
8366 SdkOperation::ALL.len()
8367 );
8368 assert_eq!(
8369 response["result"]["harnesses"].as_array().unwrap().len(),
8370 11
8371 );
8372 assert!(response["result"]["harnesses"]
8373 .as_array()
8374 .unwrap()
8375 .iter()
8376 .any(|harness| harness == HarnessId::GROK));
8377 assert!(response["result"]["harnesses"]
8378 .as_array()
8379 .unwrap()
8380 .iter()
8381 .any(|harness| harness == HarnessId::GOOSE));
8382 }
8383
8384 #[test]
8385 fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8386 let noisy_stderr = crate::HarnessEvent {
8387 sequence: None,
8388 kind: "transport_stderr".into(),
8389 payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8390 };
8391 assert_eq!(handshake_event_failure(&noisy_stderr), None);
8392
8393 let closed = crate::HarnessEvent {
8394 sequence: None,
8395 kind: "transport_closed".into(),
8396 payload: json!({}),
8397 };
8398 assert!(handshake_event_failure(&closed).is_some());
8399 }
8400
8401 #[tokio::test]
8402 async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8403 let mut service = HarnessSessionService::new();
8404 service
8405 .runtimes
8406 .insert("raw-eof".into(), ending_runtime(None));
8407 service.runtimes.insert(
8408 "explicit-close".into(),
8409 ending_runtime(Some(HarnessEvent {
8410 sequence: None,
8411 kind: "transport_closed".into(),
8412 payload: json!({"message": "native transport exited"}),
8413 })),
8414 );
8415
8416 let notifications = service.poll_runtimes().await;
8417
8418 assert_eq!(notifications.len(), 2);
8419 assert!(notifications
8420 .iter()
8421 .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8422 assert!(notifications.iter().all(|notification| {
8423 notification["params"]["session_id"] == "ending-session"
8424 && notification["params"]["connection"].is_string()
8425 }));
8426 let mut sequences = notifications
8427 .iter()
8428 .filter_map(|notification| notification["params"]["sequence"].as_u64())
8429 .collect::<Vec<_>>();
8430 sequences.sort_unstable();
8431 assert_eq!(sequences, vec![1, 2]);
8432 assert!(service.runtimes.is_empty());
8433 }
8434
8435 #[test]
8436 fn support_report_and_grok_default_binding_share_the_registry() {
8437 let mut service = HarnessSessionService::new();
8438 let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8439 assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8440 let params = RuntimeBackendParams {
8441 harness: HarnessId::from(HarnessId::GROK),
8442 protocol: None,
8443 launch: None,
8444 base_url: None,
8445 policy: RuntimePolicy::Default,
8446 };
8447 let backend = match runtime_backend(¶ms) {
8448 Ok(backend) => backend,
8449 Err(_) => panic!("Grok should bind through its registered ACP launch"),
8450 };
8451 assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8452 assert!(backend.capabilities().start_session);
8453 let registered = harness_support_registry()
8454 .harnesses
8455 .into_iter()
8456 .find(|harness| harness.id.as_str() == HarnessId::GROK)
8457 .and_then(|harness| harness.runtime.default_launch)
8458 .unwrap();
8459 assert!(!registered
8460 .arguments
8461 .iter()
8462 .any(|argument| argument == "--always-approve"));
8463 assert!(runtime_launch(¶ms).is_none());
8464
8465 let yolo = RuntimeBackendParams {
8466 policy: RuntimePolicy::Yolo,
8467 ..params
8468 };
8469 assert!(runtime_launch(&yolo)
8470 .unwrap()
8471 .arguments
8472 .iter()
8473 .any(|argument| argument == "--always-approve"));
8474
8475 let mismatched_protocol = RuntimeBackendParams {
8476 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8477 protocol: Some("acp".into()),
8478 launch: None,
8479 base_url: None,
8480 policy: RuntimePolicy::Default,
8481 };
8482 assert!(runtime_backend(&mismatched_protocol).is_err());
8483 }
8484
8485 #[test]
8486 fn load_follow_and_unfollow_share_the_same_locator() {
8487 let mut service = HarnessSessionService::new();
8488 let locator = pi_locator();
8489 let loaded = service.handle(request(
8490 1,
8491 "harness.v1.sessions.load",
8492 json!({"locator": locator}),
8493 ));
8494 assert_eq!(
8495 loaded["result"]["session"]["session_id"],
8496 locator.session_id
8497 );
8498
8499 let followed = service.handle(request(
8500 2,
8501 "harness.v1.sessions.follow",
8502 json!({"locator": locator}),
8503 ));
8504 assert_eq!(followed["result"]["subscription"], "sub-1");
8505 assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8506 assert!(service.poll().is_empty());
8507
8508 let unfollowed = service.handle(request(
8509 3,
8510 "harness.v1.sessions.unfollow",
8511 json!({"subscription": "sub-1"}),
8512 ));
8513 assert_eq!(unfollowed["result"]["removed"], true);
8514 }
8515
8516 #[test]
8517 fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8518 let temp = std::env::temp_dir().join(format!(
8519 "supercode-bounded-view-{}-{}",
8520 std::process::id(),
8521 generated_session_id()
8522 ));
8523 let path = temp.join("parent.jsonl");
8524 let subagents = temp.join("parent/subagents");
8525 std::fs::create_dir_all(&subagents).unwrap();
8526 let long_last = "x".repeat(300);
8527 let parent_records = [
8528 json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8529 json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8530 json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8531 ];
8532 std::fs::write(
8533 &path,
8534 format!(
8535 "{}\n",
8536 parent_records
8537 .iter()
8538 .map(Value::to_string)
8539 .collect::<Vec<_>>()
8540 .join("\n")
8541 ),
8542 )
8543 .unwrap();
8544 std::fs::write(
8545 subagents.join("agent-child.jsonl"),
8546 concat!(
8547 r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8548 "\n",
8549 ),
8550 )
8551 .unwrap();
8552 let locator = SessionLocator {
8553 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8554 session_id: "parent".into(),
8555 storage: StorageLocator::File { path },
8556 };
8557 let mut service = HarnessSessionService::new();
8558
8559 let complete = service.handle(request(
8560 1,
8561 "harness.v1.sessions.load",
8562 json!({"locator": locator}),
8563 ));
8564 assert_eq!(
8565 complete["result"]["session"]["subagents"]
8566 .as_array()
8567 .unwrap()
8568 .len(),
8569 1
8570 );
8571
8572 let bounded = service.handle(request(
8573 2,
8574 "harness.v1.sessions.load",
8575 json!({
8576 "locator": locator,
8577 "view": {
8578 "tail_messages": 1,
8579 "max_message_chars": 256,
8580 "include_subagents": false
8581 },
8582 }),
8583 ));
8584 let session = &bounded["result"]["session"];
8585 assert!(session["subagents"].as_array().unwrap().is_empty());
8586 assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8587 assert_eq!(
8588 session["messages"][0]["content"],
8589 format!("{}\n…", "x".repeat(256))
8590 );
8591
8592 let followed = service.handle(request(
8593 3,
8594 "harness.v1.sessions.follow",
8595 json!({
8596 "locator": locator,
8597 "view": {
8598 "tail_messages": 1,
8599 "max_message_chars": 256,
8600 "include_subagents": false
8601 },
8602 }),
8603 ));
8604 let initial = &followed["result"]["initial"]["session"];
8605 assert!(initial["subagents"].as_array().unwrap().is_empty());
8606 assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8607
8608 let _ = std::fs::remove_dir_all(&temp);
8609 }
8610
8611 #[test]
8612 fn forty_megabyte_display_load_is_bounded_and_prompt() {
8613 let temp = std::env::temp_dir().join(format!(
8614 "supercode-large-display-view-{}-{}",
8615 std::process::id(),
8616 generated_session_id()
8617 ));
8618 std::fs::create_dir_all(&temp).unwrap();
8619 let path = temp.join("rollout.jsonl");
8620 let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8621 writeln!(
8622 file,
8623 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8624 )
8625 .unwrap();
8626 let padding = "x".repeat(80 * 1024);
8627 for index in 0..512 {
8628 let marker = if index == 0 {
8629 "OLDEST-SHOULD-NOT-LOAD"
8630 } else if index == 511 {
8631 "LATEST-MUST-LOAD"
8632 } else {
8633 "bulk"
8634 };
8635 writeln!(
8636 file,
8637 "{}",
8638 json!({
8639 "timestamp": "2026-01-01T00:00:01Z",
8640 "type": "response_item",
8641 "payload": {
8642 "type": "message",
8643 "role": "assistant",
8644 "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8645 },
8646 })
8647 )
8648 .unwrap();
8649 }
8650 file.flush().unwrap();
8651 drop(file);
8652 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8653
8654 let locator = SessionLocator {
8655 harness: HarnessId::from(HarnessId::CODEX),
8656 session_id: "large-display".into(),
8657 storage: StorageLocator::File { path },
8658 };
8659 let started = Instant::now();
8660 let response = HarnessSessionService::new().handle(request(
8661 1,
8662 "harness.v1.sessions.load",
8663 json!({
8664 "locator": locator,
8665 "view": {
8666 "tail_messages": 500,
8667 "max_message_chars": 1024,
8668 "include_subagents": false,
8669 "display_history": true,
8670 },
8671 }),
8672 ));
8673 let elapsed = started.elapsed();
8674 let wire = response.to_string();
8675 eprintln!(
8676 "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8677 wire.len()
8678 );
8679 assert!(response.get("error").is_none(), "{response:#}");
8680 assert!(wire.contains("LATEST-MUST-LOAD"));
8681 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8682 assert!(
8683 wire.len() < 2 * 1024 * 1024,
8684 "bounded wire was {} bytes",
8685 wire.len()
8686 );
8687 assert!(
8688 elapsed.as_secs_f64() < 3.0,
8689 "bounded 40 MiB load took {elapsed:?}"
8690 );
8691
8692 let _ = std::fs::remove_dir_all(&temp);
8693 }
8694
8695 #[test]
8696 fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8697 let temp = std::env::temp_dir().join(format!(
8698 "supercode-large-goose-view-{}-{}",
8699 std::process::id(),
8700 generated_session_id()
8701 ));
8702 std::fs::create_dir_all(&temp).unwrap();
8703 let path = temp.join("sessions.db");
8704 let connection = rusqlite::Connection::open(&path).unwrap();
8705 connection
8706 .execute_batch(
8707 "CREATE TABLE sessions (
8708 id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8709 created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8710 session_type TEXT NOT NULL, extension_data TEXT,
8711 goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8712 archived_at TEXT
8713 );
8714 CREATE TABLE messages (
8715 id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8716 role TEXT NOT NULL, content_json TEXT NOT NULL,
8717 created_timestamp INTEGER NOT NULL, metadata_json TEXT
8718 );",
8719 )
8720 .unwrap();
8721 connection
8722 .execute(
8723 "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8724 rusqlite::params![
8725 "goose-large",
8726 "Large Goose session",
8727 "/tmp",
8728 "2026-01-01 00:00:00",
8729 "2026-01-01 00:00:02",
8730 "user",
8731 "{}",
8732 "auto",
8733 "anthropic",
8734 r#"{"model_name":"claude-sonnet"}"#,
8735 ],
8736 )
8737 .unwrap();
8738 let old_content = serde_json::to_string(&vec![json!({
8739 "type": "text",
8740 "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8741 })])
8742 .unwrap();
8743 connection
8744 .execute(
8745 "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8746 rusqlite::params!["goose-large", old_content],
8747 )
8748 .unwrap();
8749 connection
8750 .execute(
8751 "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8752 rusqlite::params![
8753 "goose-large",
8754 r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8755 ],
8756 )
8757 .unwrap();
8758 drop(connection);
8759 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8760
8761 let locator = SessionLocator {
8762 harness: HarnessId::from(HarnessId::GOOSE),
8763 session_id: "goose-large".into(),
8764 storage: StorageLocator::Sqlite {
8765 path,
8766 selector: "goose-large".into(),
8767 },
8768 };
8769 let started = Instant::now();
8770 let response = HarnessSessionService::new().handle(request(
8771 1,
8772 "harness.v1.sessions.load",
8773 json!({
8774 "locator": locator,
8775 "view": {
8776 "tail_messages": 1,
8777 "max_message_chars": 1024,
8778 "include_subagents": false,
8779 "display_history": true,
8780 },
8781 }),
8782 ));
8783 let elapsed = started.elapsed();
8784 let wire = response.to_string();
8785 eprintln!(
8786 "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8787 wire.len()
8788 );
8789 assert!(response.get("error").is_none(), "{response:#}");
8790 assert!(wire.contains("LATEST-MUST-LOAD"));
8791 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8792 assert!(
8793 wire.len() < 64 * 1024,
8794 "bounded wire was {} bytes",
8795 wire.len()
8796 );
8797 assert!(
8798 elapsed.as_secs_f64() < 1.0,
8799 "bounded Goose load took {elapsed:?}"
8800 );
8801
8802 let _ = std::fs::remove_dir_all(&temp);
8803 }
8804
8805 #[test]
8806 fn display_view_keeps_codex_assistant_history_across_compaction() {
8807 let temp = std::env::temp_dir().join(format!(
8808 "supercode-codex-display-view-{}-{}",
8809 std::process::id(),
8810 generated_session_id()
8811 ));
8812 std::fs::create_dir_all(&temp).unwrap();
8813 let path = temp.join("rollout.jsonl");
8814 std::fs::write(
8815 &path,
8816 concat!(
8817 r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8818 "\n",
8819 r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8820 "\n",
8821 r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8822 "\n",
8823 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"}]}}"#,
8824 "\n",
8825 r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8826 "\n",
8827 r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8828 "\n",
8829 ),
8830 )
8831 .unwrap();
8832 let locator = SessionLocator {
8833 harness: HarnessId::from(HarnessId::CODEX),
8834 session_id: "codex-display".into(),
8835 storage: StorageLocator::File { path },
8836 };
8837 let mut service = HarnessSessionService::new();
8838
8839 let continuation = service.handle(request(
8840 1,
8841 "harness.v1.sessions.load",
8842 json!({"locator": locator}),
8843 ));
8844 let continuation_text = continuation["result"]["session"]["messages"].to_string();
8845 assert!(!continuation_text.contains("old answer"));
8846
8847 let display = service.handle(request(
8848 2,
8849 "harness.v1.sessions.load",
8850 json!({
8851 "locator": locator,
8852 "view": {
8853 "tail_messages": 10,
8854 "include_subagents": false,
8855 "display_history": true,
8856 },
8857 }),
8858 ));
8859 let display_text = display["result"]["session"]["messages"].to_string();
8860 assert!(display_text.contains("old prompt"));
8861 assert!(display_text.contains("old answer"));
8862 assert!(display_text.contains("new prompt"));
8863 assert!(display_text.contains("new answer"));
8864
8865 let _ = std::fs::remove_dir_all(&temp);
8866 }
8867
8868 #[test]
8869 fn indexed_claude_windows_match_the_existing_wire_projection() {
8870 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8871 .join("tests/fixtures/claude_code_session.jsonl");
8872 let locator = SessionLocator {
8873 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8874 session_id: "fixture".into(),
8875 storage: StorageLocator::File { path },
8876 };
8877 let full = load_session(&locator).unwrap();
8878 for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8879 for offset in [0, 1, full.messages.len(), usize::MAX] {
8880 for limit in [0, 1, 3, usize::MAX] {
8881 let options = SessionLoadOptions {
8882 include_subagents: Some(false),
8883 inline_media,
8884 message_offset: Some(offset),
8885 message_limit: Some(limit),
8886 ..Default::default()
8887 };
8888 let expected = projected_session_result(&full, &options);
8889 assert_eq!(
8890 indexed_claude_window(&locator, &options).unwrap().unwrap(),
8891 expected
8892 );
8893 }
8894 }
8895 for tail in [0, 1, 3, usize::MAX] {
8896 let options = SessionLoadOptions {
8897 include_subagents: Some(false),
8898 inline_media,
8899 message_tail: Some(tail),
8900 ..Default::default()
8901 };
8902 assert_eq!(
8903 indexed_claude_window(&locator, &options).unwrap().unwrap(),
8904 projected_session_result(&full, &options)
8905 );
8906 }
8907 }
8908 }
8909
8910 #[test]
8911 fn load_supports_bounded_windows_and_media_metadata() {
8912 let mut service = HarnessSessionService::new();
8913 let locator = pi_locator();
8914 let bounded = service.handle(request(
8915 1,
8916 "harness.v1.sessions.load",
8917 json!({
8918 "locator": locator,
8919 "options": {
8920 "include_subagents": false,
8921 "message_limit": 2,
8922 "message_offset": 1
8923 }
8924 }),
8925 ));
8926 assert_eq!(bounded["result"]["window"]["offset"], 1);
8927 assert_eq!(bounded["result"]["window"]["returned"], 2);
8928 assert!(bounded["result"]["summary"]["first_message"].is_object());
8929 assert!(bounded["result"]["summary"]["last_message"].is_object());
8930 assert_eq!(
8931 bounded["result"]["session"]["messages"]
8932 .as_array()
8933 .unwrap()
8934 .len(),
8935 2
8936 );
8937 assert!(bounded["result"]["session"]["subagents"]
8938 .as_array()
8939 .unwrap()
8940 .is_empty());
8941
8942 let tail = service.handle(request(
8943 2,
8944 "harness.v1.sessions.load",
8945 json!({"locator": locator, "options": {"message_tail": 1}}),
8946 ));
8947 assert_eq!(tail["result"]["window"]["returned"], 1);
8948 assert_eq!(tail["result"]["window"]["has_more"], true);
8949 assert_eq!(tail["result"]["window"]["has_older"], true);
8950 assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8951 assert!(tail["result"]["summary"]["first_message"].is_object());
8952
8953 let metadata_only = service.handle(request(
8954 3,
8955 "harness.v1.sessions.load",
8956 json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8957 ));
8958 assert!(metadata_only["result"]["session"]
8959 .to_string()
8960 .contains("media_reference"));
8961 assert!(!metadata_only["result"]["session"]
8962 .to_string()
8963 .contains("data:image/"));
8964 }
8965
8966 #[test]
8967 fn import_translate_branch_and_handoff_use_typed_artifacts() {
8968 let mut service = HarnessSessionService::new();
8969 let locator = pi_locator();
8970 let translated = service.handle(request(
8971 1,
8972 "harness.v1.sessions.translate",
8973 json!({"locator": locator, "target_harness": "grok"}),
8974 ));
8975 assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8976 assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8977 assert!(translated["result"]["artifact"]["content"]
8978 .as_str()
8979 .is_some_and(|content| !content.is_empty()));
8980
8981 for target in ["opencode", "open-code"] {
8982 let opencode = service.handle(request(
8983 6,
8984 "harness.v1.sessions.translate",
8985 json!({"locator": locator, "target_harness": target}),
8986 ));
8987 assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8988 }
8989 let goose = service.handle(request(
8990 7,
8991 "harness.v1.sessions.translate",
8992 json!({"locator": locator, "target_harness": "goose"}),
8993 ));
8994 assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8995 assert!(serde_json::from_str::<Value>(
8996 goose["result"]["artifact"]["content"].as_str().unwrap()
8997 )
8998 .unwrap()["conversation"]
8999 .is_array());
9000
9001 let imported = service.handle(request(
9002 2,
9003 "harness.v1.sessions.import",
9004 json!({
9005 "source_harness": "grok",
9006 "content": translated["result"]["artifact"]["content"],
9007 }),
9008 ));
9009 assert_eq!(imported["result"]["session"]["source"], "grok");
9010
9011 let branched = service.handle(request(
9012 3,
9013 "harness.v1.sessions.branch",
9014 json!({"locator": locator, "target_harness": "codex"}),
9015 ));
9016 assert_eq!(branched["result"]["parent"]["harness"], "pi");
9017 assert!(branched["result"]["bootstrap_prompt"]
9018 .as_str()
9019 .unwrap()
9020 .contains("frozen parent transcript"));
9021 assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
9022
9023 let handoff = service.handle(request(
9024 4,
9025 "harness.v1.sessions.handoff",
9026 json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
9027 ));
9028 assert_eq!(handoff["result"]["launch"]["program"], "pi");
9029 assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
9030 assert_eq!(handoff["result"]["requires_materialization"], true);
9031
9032 let goose_handoff = service.handle(request(
9033 8,
9034 "harness.v1.sessions.handoff",
9035 json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
9036 ));
9037 assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
9038 assert_eq!(
9039 goose_handoff["result"]["materialize"]["arguments"],
9040 json!(["session", "import", "{artifact_path}"])
9041 );
9042
9043 let resumed = service.handle(request(
9044 5,
9045 "harness.v1.sessions.resume_instructions",
9046 json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
9047 ));
9048 assert_eq!(resumed["result"]["launch"]["program"], "pi");
9049 assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
9050 }
9051
9052 #[test]
9053 fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
9054 let temp = std::env::temp_dir().join(format!(
9055 "supercode-service-reduce-{}-{}",
9056 std::process::id(),
9057 generated_session_id()
9058 ));
9059 let source_path = temp.join("source.jsonl");
9060 let store_root = temp.join("store");
9061 std::fs::create_dir_all(&temp).unwrap();
9062
9063 let mut records = vec![json!({
9064 "timestamp": "2026-01-01T00:00:00Z",
9065 "type": "session_meta",
9066 "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9067 })];
9068 for turn in 0..16 {
9069 records.push(json!({
9070 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9071 "type": "response_item",
9072 "payload": {
9073 "type": "message",
9074 "role": "user",
9075 "content": [{
9076 "type": "input_text",
9077 "text": format!("request {turn}: {}", "context ".repeat(80)),
9078 }],
9079 },
9080 }));
9081 records.push(json!({
9082 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9083 "type": "response_item",
9084 "payload": {
9085 "type": "message",
9086 "role": "assistant",
9087 "content": [{
9088 "type": "output_text",
9089 "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9090 }],
9091 },
9092 }));
9093 }
9094 let source = format!(
9095 "{}\n",
9096 records
9097 .iter()
9098 .map(Value::to_string)
9099 .collect::<Vec<_>>()
9100 .join("\n")
9101 );
9102 std::fs::write(&source_path, &source).unwrap();
9103 let locator = SessionLocator {
9104 harness: HarnessId::from(HarnessId::CODEX),
9105 session_id: "codex-reduce".into(),
9106 storage: StorageLocator::File {
9107 path: source_path.clone(),
9108 },
9109 };
9110 let original = load_session(&locator).unwrap();
9111 let mut service =
9112 HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9113
9114 let response = service.handle(request(
9115 1,
9116 "harness.v1.sessions.reduce",
9117 json!({
9118 "locator": locator,
9119 "target_harness": "claude-code",
9120 "keep_last": 4,
9121 }),
9122 ));
9123 assert!(response.get("error").is_none(), "{response:#}");
9124 let receipt = &response["result"]["receipt"];
9125 assert_eq!(receipt["source_harness"], "codex");
9126 assert_eq!(receipt["target_harness"], "claude-code");
9127 assert_eq!(receipt["verified"], true);
9128 assert_eq!(receipt["reversible"], true);
9129 assert!(receipt["reductions"].as_u64().unwrap() > 0);
9130 assert!(
9131 receipt["source_tokens"].as_u64().unwrap()
9132 > receipt["reduced_tokens"].as_u64().unwrap()
9133 );
9134 assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9135 assert!(response["result"]["bootstrap_prompt"]
9136 .as_str()
9137 .unwrap()
9138 .contains("Do not guess hidden content"));
9139
9140 let rescue_id = receipt["id"].as_str().unwrap();
9141 let store = crate::SessionStore::open(&store_root).unwrap();
9142 let sidecar =
9143 Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9144 let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9145 let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9146 let policy = reduce::ReductionPolicy {
9147 clear_turns_older_than: Some(4),
9148 ..Default::default()
9149 };
9150 let (restamped_view, reapplied_log) =
9151 reduce::project_messages(&sidecar.messages, &policy, &log);
9152 assert_eq!(
9153 messages_jsonl(&persisted_view).unwrap(),
9154 messages_jsonl(&restamped_view).unwrap()
9155 );
9156 assert_eq!(reapplied_log, log);
9157 reduce::verify_log(&log, &sidecar).unwrap();
9158 assert_eq!(
9159 reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9160 original.messages
9161 );
9162 assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9163
9164 std::fs::remove_dir_all(temp).ok();
9165 }
9166
9167 #[test]
9168 fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9169 let temp = std::env::temp_dir().join(format!(
9170 "supercode-severed-view-{}-{}",
9171 std::process::id(),
9172 generated_session_id()
9173 ));
9174 std::fs::create_dir_all(&temp).unwrap();
9175 let path = temp.join("severed.jsonl");
9176 std::fs::write(
9179 &path,
9180 concat!(
9181 r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9182 "\n",
9183 r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9184 "\n",
9185 ),
9186 )
9187 .unwrap();
9188 let locator = SessionLocator {
9189 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9190 session_id: "severed".into(),
9191 storage: StorageLocator::File { path },
9192 };
9193 let mut service = HarnessSessionService::new();
9194
9195 let viewed = service.handle(request(
9196 1,
9197 "harness.v1.sessions.load",
9198 json!({"locator": locator}),
9199 ));
9200 let session = &viewed["result"]["session"];
9201 assert_eq!(session["fidelity"], "semantic");
9202 assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9203 assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9204 entry
9205 .as_str()
9206 .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9207 }));
9208
9209 let strict = service.handle(request(
9212 2,
9213 "harness.v1.sessions.load",
9214 json!({"locator": locator, "fidelity": "byte_lossless"}),
9215 ));
9216 assert!(strict["error"]["message"]
9217 .as_str()
9218 .unwrap()
9219 .contains("cannot reconstruct lossless Claude continuation"));
9220
9221 let translated = service.handle(request(
9223 3,
9224 "harness.v1.sessions.translate",
9225 json!({"locator": locator, "target_harness": "codex"}),
9226 ));
9227 assert!(translated["error"]["message"]
9228 .as_str()
9229 .unwrap()
9230 .contains("cannot reconstruct lossless Claude continuation"));
9231 let resumed = service.handle(request(
9232 4,
9233 "harness.v1.sessions.resume_instructions",
9234 json!({"locator": locator}),
9235 ));
9236 assert!(resumed["error"]["message"]
9237 .as_str()
9238 .unwrap()
9239 .contains("cannot reconstruct lossless Claude continuation"));
9240
9241 let _ = std::fs::remove_dir_all(&temp);
9242 }
9243
9244 #[test]
9245 fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9246 let codex = resume_launch(
9247 HarnessId::CODEX,
9248 "codex-session",
9249 Path::new("/tmp/project"),
9250 ResumePolicy::Yolo,
9251 )
9252 .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9253 assert_eq!(codex.program, "codex");
9254 assert_eq!(
9255 codex.arguments,
9256 [
9257 "-c",
9258 "check_for_update_on_startup=false",
9259 "-c",
9260 "projects.\"/tmp/project\".trust_level=\"trusted\"",
9261 "--dangerously-bypass-approvals-and-sandbox",
9262 "--dangerously-bypass-hook-trust",
9263 "resume",
9264 "codex-session",
9265 ]
9266 );
9267
9268 let gemini = resume_launch(
9269 HarnessId::GEMINI,
9270 "gemini-session",
9271 Path::new("/tmp/project"),
9272 ResumePolicy::Yolo,
9273 )
9274 .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9275 assert_eq!(gemini.program, "gemini");
9276 assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9277
9278 let goose = resume_launch(
9279 HarnessId::GOOSE,
9280 "goose-session",
9281 Path::new("/tmp/project"),
9282 ResumePolicy::Yolo,
9283 )
9284 .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9285 assert_eq!(goose.program, "goose");
9286 assert_eq!(
9287 goose.arguments,
9288 ["session", "--resume", "--session-id", "goose-session"]
9289 );
9290
9291 let supercode = resume_launch(
9292 HarnessId::SUPERCODE,
9293 "supercode-session",
9294 Path::new("/tmp/project"),
9295 ResumePolicy::Yolo,
9296 )
9297 .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9298 assert_eq!(supercode.program, "supercode");
9299 assert_eq!(
9300 supercode.arguments,
9301 ["--dangerous", "resume", "supercode-session"]
9302 );
9303 }
9304
9305 #[test]
9306 fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9307 let temp = std::env::temp_dir().join(format!(
9308 "supercode-harness-artifact-{}-{}",
9309 std::process::id(),
9310 generated_session_id()
9311 ));
9312 let main_path = temp.join("parent.jsonl");
9313 let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9314 std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9315 let fixture = std::fs::read_to_string(
9316 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9317 .join("tests/fixtures/claude_code_session.jsonl"),
9318 )
9319 .unwrap();
9320 let parent = fixture.trim_end_matches('\n');
9321 let child = fixture.trim_end_matches('\n');
9322 std::fs::write(&main_path, parent).unwrap();
9323 std::fs::write(&subagent_path, child).unwrap();
9324 let locator = SessionLocator {
9325 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9326 session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9327 storage: StorageLocator::File {
9328 path: main_path.clone(),
9329 },
9330 };
9331 let mut service = HarnessSessionService::new();
9332 let claude = service.handle(request(
9333 1,
9334 "harness.v1.sessions.translate",
9335 json!({"locator": locator, "target_harness": "claude-code"}),
9336 ));
9337 let artifact = &claude["result"]["artifact"];
9338 assert_eq!(artifact["fidelity"], "byte_lossless");
9339 assert_eq!(artifact["content"], parent);
9340 let files = artifact["files"].as_array().unwrap();
9341 assert!(files.iter().any(|file| {
9342 file["role"] == "subagent"
9343 && file["path"]
9344 .as_str()
9345 .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9346 && file["content"] == child
9347 }));
9348 assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9349
9350 let grok = service.handle(request(
9351 2,
9352 "harness.v1.sessions.translate",
9353 json!({"locator": grok_locator(), "target_harness": "grok"}),
9354 ));
9355 let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9356 for name in ["summary.json", "updates.jsonl"] {
9357 let expected = std::fs::read_to_string(
9358 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9359 .join("tests/fixtures/grok_session")
9360 .join(name),
9361 )
9362 .unwrap();
9363 assert!(files.iter().any(|file| {
9364 file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9365 }));
9366 }
9367 std::fs::remove_dir_all(temp).ok();
9368 }
9369
9370 #[test]
9371 fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9372 let mut service = HarnessSessionService::new();
9373 let source = pi_locator();
9374 for (target, format) in [
9375 ("claude-code", SessionFormat::ClaudeCode),
9376 ("codex", SessionFormat::Codex),
9377 ("opencode", SessionFormat::OpenCode),
9378 ("pi", SessionFormat::Pi),
9379 ] {
9380 let result = service.handle(request(
9381 1,
9382 "harness.v1.sessions.handoff",
9383 json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9384 ));
9385 let artifact = &result["result"]["artifact"];
9386 let target_id = artifact["session_id"].as_str().unwrap();
9387 assert_ne!(target_id, source.session_id, "{target}");
9388 let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9389 assert_eq!(
9390 parsed.meta.session_id.as_deref(),
9391 Some(target_id),
9392 "{target}"
9393 );
9394 if target != "pi" {
9395 assert!(result["result"]["launch"]["arguments"]
9396 .as_array()
9397 .unwrap()
9398 .iter()
9399 .any(|argument| argument == target_id));
9400 }
9401 if target == "opencode" {
9402 assert!(target_id.starts_with("ses_"));
9403 fn assert_session_ids(value: &Value, target_id: &str) {
9404 match value {
9405 Value::Object(fields) => {
9406 if let Some(session_id) = fields.get("sessionID") {
9407 assert_eq!(session_id, target_id);
9408 }
9409 for child in fields.values() {
9410 assert_session_ids(child, target_id);
9411 }
9412 }
9413 Value::Array(values) => {
9414 for child in values {
9415 assert_session_ids(child, target_id);
9416 }
9417 }
9418 _ => {}
9419 }
9420 }
9421 let document: Value =
9422 serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9423 assert_session_ids(&document, target_id);
9424 }
9425 }
9426
9427 let first = service.handle(request(
9428 2,
9429 "harness.v1.sessions.handoff",
9430 json!({"locator": source, "target_harness": "codex"}),
9431 ));
9432 let second = service.handle(request(
9433 3,
9434 "harness.v1.sessions.handoff",
9435 json!({"locator": source, "target_harness": "codex"}),
9436 ));
9437 assert_ne!(
9438 first["result"]["artifact"]["session_id"],
9439 second["result"]["artifact"]["session_id"]
9440 );
9441 }
9442
9443 #[test]
9444 fn grok_handoff_uses_the_official_importer_contract() {
9445 let mut service = HarnessSessionService::new();
9446 let source = opencode_locator();
9447 let response = service.handle(request(
9448 1,
9449 "harness.v1.sessions.handoff",
9450 json!({
9451 "locator": source,
9452 "target_harness": "grok",
9453 "cwd": "/tmp/grok-handoff-project",
9454 }),
9455 ));
9456 let result = &response["result"];
9457
9458 assert_eq!(result["artifact"]["target_harness"], "claude-code");
9462 assert!(result["artifact"]["suggested_filename"]
9463 .as_str()
9464 .unwrap()
9465 .ends_with(".grok-import.claude-code.jsonl"));
9466 let artifact = Session::load_str(
9467 result["artifact"]["content"].as_str().unwrap(),
9468 SessionFormat::ClaudeCode,
9469 )
9470 .unwrap();
9471 assert_eq!(
9472 artifact.meta.cwd.as_deref(),
9473 Some(Path::new("/tmp/grok-handoff-project"))
9474 );
9475 let target_session_id = artifact.meta.session_id.as_deref().unwrap();
9476 assert_eq!(target_session_id.len(), 36);
9477 assert_eq!(target_session_id.as_bytes()[14], b'4');
9478 assert_ne!(target_session_id, opencode_locator().session_id);
9479 assert_eq!(
9480 result["artifact"]["session_id"],
9481 artifact.meta.session_id.as_deref().unwrap()
9482 );
9483
9484 assert_eq!(
9485 result["materialize"]["arguments"],
9486 json!(["import", "--json", "{artifact_path}"])
9487 );
9488 assert_eq!(
9489 result["launch"]["arguments"],
9490 json!(["--resume", "{imported_session_id}", "--fork-session"])
9491 );
9492 assert!(result["note"]
9493 .as_str()
9494 .unwrap()
9495 .contains("outcome=imported"));
9496 assert!(!result["launch"]["arguments"]
9497 .as_array()
9498 .unwrap()
9499 .iter()
9500 .any(|argument| argument == &opencode_locator().session_id));
9501 }
9502
9503 #[tokio::test]
9504 async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9505 let mut service = HarnessSessionService::new();
9506 let inventory = service
9507 .handle_async(request(
9508 1,
9509 "harness.v1.harnesses.list",
9510 json!({"harnesses": ["missing"]}),
9511 ))
9512 .await;
9513 assert_eq!(inventory["error"]["code"], -32602);
9514
9515 let attached = service
9516 .handle_async(request(
9517 2,
9518 "harness.v1.runtimes.attach_existing",
9519 json!({"harness": "codex", "runtime_id": "thread-1"}),
9520 ))
9521 .await;
9522 assert_eq!(attached["error"]["code"], -32000);
9523 assert!(attached["error"]["message"]
9524 .as_str()
9525 .unwrap()
9526 .contains("runtimes.resume"));
9527 }
9528
9529 #[test]
9530 fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9531 let mut service = HarnessSessionService::new();
9532 let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9533 assert_eq!(invalid["error"]["code"], -32602);
9534 let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9535 assert_eq!(unknown["error"]["code"], -32601);
9536 }
9537
9538 #[cfg(unix)]
9539 #[tokio::test]
9540 #[allow(clippy::await_holding_lock)]
9543 async fn async_service_drives_a_generic_acp_runtime() {
9544 let _environment_guard = crate::live_runtime::test_environment_lock();
9545 let script = r#"
9546 i=0
9547 while IFS= read -r line; do
9548 i=$((i + 1))
9549 case "$i" in
9550 1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9551 2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9552 3)
9553 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9554 printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9555 ;;
9556 4)
9557 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9558 printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9559 ;;
9560 esac
9561 done
9562 "#;
9563 let mut service = HarnessSessionService::new();
9564 let started = service
9565 .handle_async(request(
9566 1,
9567 "harness.v1.runtimes.start",
9568 json!({
9569 "harness": "codex",
9570 "protocol": "acp",
9571 "cwd": std::env::current_dir().unwrap(),
9572 "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9573 }),
9574 ))
9575 .await;
9576 assert_eq!(started["result"]["connection"], "runtime-1");
9577 assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9578
9579 let terminal = service
9580 .handle_async(request(
9581 9,
9582 "harness.v1.runtimes.terminal_instructions",
9583 json!({"connection":"runtime-1"}),
9584 ))
9585 .await;
9586 let arguments = terminal["result"]["launch"]["arguments"]
9587 .as_array()
9588 .expect("hosted runtime should return terminal arguments");
9589 let endpoint_index = arguments
9590 .iter()
9591 .position(|value| value == "--endpoint")
9592 .expect("terminal command should use an opaque endpoint");
9593 let endpoint = LiveRuntimeEndpoint::parse(
9594 arguments[endpoint_index + 1]
9595 .as_str()
9596 .expect("endpoint argument should be text"),
9597 )
9598 .unwrap();
9599 assert!(!terminal.to_string().contains("Bearer"));
9600 let workspace = std::env::current_dir().unwrap();
9601 let receipt = resolve_live_runtime(
9602 &endpoint,
9603 &LiveRuntimeSource {
9604 harness: "codex".into(),
9605 session_id: "svc_acp".into(),
9606 workspace,
9607 },
9608 )
9609 .unwrap();
9610 let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9611 .await
9612 .unwrap();
9613 let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9614 .await
9615 .unwrap();
9616
9617 let sent = service
9618 .handle_async(request(
9619 2,
9620 "harness.v1.runtimes.send_input",
9621 json!({"connection": "runtime-1", "text": "hi"}),
9622 ))
9623 .await;
9624 assert_eq!(sent["result"]["turn_id"], "3");
9625
9626 let mut events = Vec::new();
9627 for _ in 0..20 {
9628 events.extend(service.poll_runtimes().await);
9629 if events.len() >= 2 {
9630 break;
9631 }
9632 tokio::time::sleep(Duration::from_millis(2)).await;
9633 }
9634 assert!(events
9635 .iter()
9636 .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9637 assert!(events.iter().any(|event| {
9638 event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9639 }));
9640
9641 let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9642 loop {
9643 let event = attachment.next_event().await.unwrap();
9644 if event.kind == "text_delta" && event.payload["text"] == "ok" {
9645 break;
9646 }
9647 }
9648 })
9649 .await;
9650 assert!(
9651 saw_editor_reply.is_ok(),
9652 "terminal should observe the editor-driven turn"
9653 );
9654
9655 crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9656 .await
9657 .unwrap();
9658 let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9659 loop {
9660 let event = attachment.next_event().await.unwrap();
9661 if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9662 break;
9663 }
9664 }
9665 })
9666 .await;
9667 assert!(
9668 saw_terminal_reply.is_ok(),
9669 "terminal should drive the same runtime"
9670 );
9671
9672 let closed = service
9673 .handle_async(request(
9674 3,
9675 "harness.v1.runtimes.close",
9676 json!({"connection": "runtime-1"}),
9677 ))
9678 .await;
9679 assert_eq!(closed["result"]["closed"], true);
9680 }
9681
9682 #[test]
9687 fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9688 let home = connect_scratch_home("uni7-running");
9689
9690 assert!(probe_hermes_running(&home, 300_000).is_none());
9697
9698 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9701 let port = listener.local_addr().unwrap().port();
9702 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9703 std::fs::write(
9704 home.join(".openclaw/openclaw.json"),
9705 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9706 )
9707 .unwrap();
9708 let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9709 assert!(matches!(
9710 running.method,
9711 RunningInstanceMethod::GatewayConnect
9712 ));
9713 assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9714 drop(listener);
9715 let mut closed_detected = probe_openclaw_running(&home).is_some();
9720 for _ in 0..3 {
9721 if !closed_detected {
9722 break;
9723 }
9724 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9725 let port = listener.local_addr().unwrap().port();
9726 drop(listener);
9727 std::fs::write(
9728 home.join(".openclaw/openclaw.json"),
9729 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9730 )
9731 .unwrap();
9732 closed_detected = probe_openclaw_running(&home).is_some();
9733 }
9734 assert!(
9735 !closed_detected,
9736 "a closed gateway must not read as running"
9737 );
9738
9739 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9741 let port = listener.local_addr().unwrap().port();
9742 std::fs::write(
9743 home.join(".openclaw/openclaw.json"),
9744 format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9745 )
9746 .unwrap();
9747 assert!(probe_openclaw_running(&home).is_some());
9748 drop(listener);
9749
9750 std::fs::create_dir_all(home.join(".hermes")).unwrap();
9752 let wal = home.join(".hermes/state.db-wal");
9753 std::fs::write(&wal, b"wal").unwrap();
9754 let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9755 assert!(matches!(
9756 running.method,
9757 RunningInstanceMethod::StoreWalActivity
9758 ));
9759 assert!(running.evidence.contains("state.db-wal"));
9760 let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9761 std::fs::File::options()
9762 .append(true)
9763 .open(&wal)
9764 .unwrap()
9765 .set_modified(stale)
9766 .unwrap();
9767 assert!(
9768 probe_hermes_running(&home, 300_000).is_none(),
9769 "a stale WAL (crash leftover) must not read as running"
9770 );
9771 }
9772
9773 fn connect_scratch_home(tag: &str) -> PathBuf {
9774 let dir = std::env::temp_dir().join(format!(
9775 "supercode-connect-service-{tag}-{}-{}",
9776 std::process::id(),
9777 std::time::SystemTime::now()
9778 .duration_since(std::time::UNIX_EPOCH)
9779 .unwrap()
9780 .as_nanos()
9781 ));
9782 std::fs::create_dir_all(&dir).unwrap();
9783 dir
9784 }
9785
9786 async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9790 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9791 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9792 let address = listener.local_addr().unwrap();
9793 let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9794 tokio::spawn(async move {
9795 loop {
9796 let Ok((mut stream, _)) = listener.accept().await else {
9797 break;
9798 };
9799 let request_sender = request_sender.clone();
9800 tokio::spawn(async move {
9801 let (reader, mut writer) = stream.split();
9802 let mut reader = BufReader::new(reader);
9803 let mut request_line = String::new();
9804 if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9805 return;
9806 }
9807 let request_line = request_line.trim_end().to_string();
9808 let mut authorization = String::new();
9809 let mut content_length = 0usize;
9810 loop {
9811 let mut line = String::new();
9812 if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9813 return;
9814 }
9815 let line = line.trim_end();
9816 if line.is_empty() {
9817 break;
9818 }
9819 let lower = line.to_ascii_lowercase();
9820 if let Some(value) = lower.strip_prefix("authorization:") {
9821 authorization = value.trim().to_string();
9822 }
9823 if let Some(value) = lower.strip_prefix("content-length:") {
9824 content_length = value.trim().parse().unwrap_or(0);
9825 }
9826 }
9827 if content_length > 0 {
9828 let mut body = vec![0u8; content_length];
9829 let _ = reader.read_exact(&mut body).await;
9830 }
9831 let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9832 if request_line.starts_with("GET /event") {
9833 let _ = writer
9834 .write_all(
9835 b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9836 )
9837 .await;
9838 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9839 return;
9840 }
9841 let body = if request_line.starts_with("POST /session") {
9842 r#"{"id":"mock-session"}"#
9843 } else {
9844 r#"{"status":"ok"}"#
9845 };
9846 let response = format!(
9847 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9848 body.len(),
9849 body
9850 );
9851 let _ = writer.write_all(response.as_bytes()).await;
9852 });
9853 }
9854 });
9855 (format!("http://{address}"), request_receiver)
9856 }
9857
9858 fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9859 crate::HarnessSupportDescriptor {
9860 orchestration: Default::default(),
9861 id: HarnessId::from(HarnessId::OPENCODE),
9862 display_name: "OpenCode".into(),
9863 native: crate::NativeSupport {
9864 discover: crate::ImplementationKind::Absent,
9865 load: crate::ImplementationKind::Absent,
9866 follow: crate::ImplementationKind::Absent,
9867 import: crate::ImplementationKind::Absent,
9868 export: crate::ImplementationKind::Absent,
9869 },
9870 runtime: crate::RuntimeSupport {
9871 implementation: crate::ImplementationKind::BuiltIn,
9872 protocol: protocol.into(),
9873 default_launch: None,
9874 connect_launch: Some(crate::RuntimeConnectLaunch {
9875 config_path: "~/opencode-tui.json".into(),
9876 address_pointer: "/server/url".into(),
9877 port_pointer: None,
9878 default_address: None,
9879 auth_pointer: Some("/server/token".into()),
9880 protocol: protocol.into(),
9881 }),
9882 capabilities: crate::RuntimeCapabilities {
9883 start_session: true,
9884 resume_session: true,
9885 attach_existing_process: true,
9886 send_input: true,
9887 stream_events: true,
9888 interrupt: true,
9889 steer: false,
9890 respond_to_requests: true,
9891 },
9892 },
9893 }
9894 }
9895
9896 #[tokio::test]
9897 async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9898 let (base_url, mut requests) = mock_opencode_endpoint().await;
9899 let home = connect_scratch_home("open");
9900 std::fs::write(
9901 home.join("opencode-tui.json"),
9902 format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9903 )
9904 .unwrap();
9905
9906 let descriptor = connect_descriptor("opencode-http-sse");
9907 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9908 assert!(backend.capabilities().attach_existing_process);
9909
9910 let connection = backend
9911 .start(crate::RuntimeStartRequest {
9912 cwd: home.clone(),
9913 launch: None,
9914 mcp_servers: Vec::new(),
9915 })
9916 .await
9917 .unwrap();
9918 let handle = connection.handle();
9919 assert_eq!(handle.runtime_id, "mock-session");
9920 match &handle.endpoint {
9921 crate::RuntimeEndpoint::Http {
9922 base_url: endpoint, ..
9923 } => assert_eq!(endpoint, &base_url),
9924 other => panic!("connect mode must join the running endpoint, got {other:?}"),
9925 }
9926
9927 let mut seen = Vec::new();
9928 while let Ok(line) = requests.try_recv() {
9929 seen.push(line);
9930 }
9931 assert!(seen
9932 .iter()
9933 .any(|line| line.starts_with("GET /global/health")
9934 && line.contains("bearer connect-secret")));
9935 assert!(seen.iter().any(
9936 |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9937 ));
9938 }
9939
9940 #[tokio::test]
9951 async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9952 let home = connect_scratch_home("openclaw");
9953 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9954 std::fs::write(
9955 home.join(".openclaw/openclaw.json"),
9956 r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9957 )
9958 .unwrap();
9959 let script = home.join("openclaw");
9960 std::fs::write(
9961 &script,
9962 r#"#!/bin/sh
9963# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9964[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9965[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9966[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9967[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9968while IFS= read -r line; do
9969 case "$line" in
9970 *'"initialize"'*)
9971 printf '%s
9972' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9973 *'"session/resume"'*)
9974 printf '%s
9975' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9976 *'"session/new"'*)
9977 printf '%s
9978' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9979 *'"session/prompt"'*)
9980 printf '%s
9981' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9982 printf '%s
9983' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9984 esac
9985done
9986"#,
9987 )
9988 .unwrap();
9989 use std::os::unix::fs::PermissionsExt;
9990 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9991
9992 let mut descriptor = crate::harness_support_registry()
9993 .harnesses
9994 .into_iter()
9995 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9996 .expect("openclaw must be registered");
9997 descriptor
9998 .runtime
9999 .connect_launch
10000 .as_mut()
10001 .unwrap()
10002 .config_path = "~/.openclaw/openclaw.json".into();
10003 descriptor.runtime.default_launch.as_mut().unwrap().program =
10004 script.to_string_lossy().into_owned();
10005 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10006 assert!(backend.capabilities().resume_session);
10007
10008 let joined = backend
10009 .attach(crate::RuntimeAttachRequest {
10010 runtime_id: "agent:main:main".into(),
10011 cwd: Some(home.clone()),
10012 launch: None,
10013 mcp_servers: Vec::new(),
10014 })
10015 .await;
10016 let mut connection = joined.expect("mock bridge attach must succeed");
10017 assert_eq!(connection.handle().runtime_id, "agent:main:main");
10018 let turn = connection
10019 .send_input(crate::RuntimeInput {
10020 text: "hello".into(),
10021 image_urls: Vec::new(),
10022 })
10023 .await;
10024 assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
10025 connection.close().await.unwrap();
10026 }
10027
10028 #[tokio::test]
10029 async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
10030 let home = connect_scratch_home("fail");
10031 std::fs::write(
10032 home.join("opencode-tui.json"),
10033 r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
10034 )
10035 .unwrap();
10036
10037 let gateway_only = connect_descriptor("acp-v1-jsonrpc");
10038 let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
10039 panic!("an ACP connect endpoint has no gateway client yet");
10040 };
10041 let message = format!("{error:?}");
10042 assert!(message.contains("acp-v1-jsonrpc"));
10043 assert!(!message.contains("connect-secret"));
10044
10045 let unreadable = connect_descriptor("opencode-http-sse");
10046 let missing_home = connect_scratch_home("missing");
10047 let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
10048 panic!("an unreadable connect config must fail closed");
10049 };
10050 let message = format!("{error:?}");
10051 assert!(message.contains("opencode-tui.json"));
10052 assert!(!message.contains("connect-secret"));
10053 }
10054
10055 fn jobs_fixture_root() -> PathBuf {
10060 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10061 }
10062
10063 fn jobs_fixture_homes() -> Value {
10066 let root = jobs_fixture_root();
10067 json!({
10068 "claude_code": root.join("claude_jobs_home/projects"),
10069 "hermes": root.join("hermes_home/state.db"),
10070 "openclaw": root.join("openclaw_home"),
10071 })
10072 }
10073
10074 fn jobs_list(params: Value) -> Value {
10075 let mut service = HarnessSessionService::new();
10076 service.handle(request(1, "harness.v1.jobs.list", params))
10077 }
10078
10079 fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10080 result["jobs"]
10081 .as_array()
10082 .expect("jobs is an array")
10083 .iter()
10084 .find(|job| job["id"] == id)
10085 .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10086 }
10087
10088 #[test]
10089 fn gateway_health_derives_from_running_probe_and_install_state() {
10090 let running = RunningInstance {
10091 method: RunningInstanceMethod::GatewayConnect,
10092 evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10093 checked_at_ms: 1,
10094 };
10095 let up = gateway_health(
10096 HarnessId::OPENCLAW,
10097 true,
10098 Some(&running),
10099 Some("2026.7.1-2"),
10100 );
10101 assert_eq!(up.state, GatewayState::Up);
10102 assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10103 assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10104 let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10108 std::fs::create_dir_all(&dir).unwrap();
10109 let fake = dir.join("hermes");
10110 let write_fake = |body: &str| {
10111 std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10112 #[cfg(unix)]
10113 {
10114 use std::os::unix::fs::PermissionsExt;
10115 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10116 }
10117 };
10118 write_fake("echo '✗ Gateway service is not installed'");
10119 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10120 *slot.borrow_mut() = Some((
10121 HarnessId::HERMES.to_string(),
10122 fake.to_string_lossy().into_owned(),
10123 ))
10124 });
10125 let down = gateway_health(HarnessId::HERMES, true, None, None);
10126 assert_eq!(down.state, GatewayState::Down, "{down:?}");
10127 assert!(down.endpoint.is_none());
10128 assert!(down.evidence.contains("not installed"));
10129 write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10130 let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10131 assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10132 assert!(idle_but_up.evidence.contains("PID 4242"));
10133 write_fake("echo 'something unparseable'");
10134 let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10135 assert_eq!(no_verdict.state, GatewayState::Down);
10136 assert!(no_verdict.evidence.contains("no verdict"));
10137 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10138 let absent = gateway_health(HarnessId::HERMES, false, None, None);
10139 assert_eq!(absent.state, GatewayState::Unknown);
10140 let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10141 assert_eq!(core.state, GatewayState::Unknown);
10142 assert!(core.evidence.contains("per session"));
10143 }
10144
10145 #[test]
10146 fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10147 let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10148 let rows = response["result"]["triggers"]
10149 .as_array()
10150 .expect("triggers")
10151 .clone();
10152 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10153 assert!(
10154 hermes.iter().any(|r| r["name"] == "deploys"
10155 && r["route"] == "/webhooks/deploys"
10156 && r["kind"] == "webhook"),
10157 "{rows:#?}"
10158 );
10159 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10160 assert!(openclaw
10161 .iter()
10162 .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10163 assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10164 && r["kind"] == "hook_mapping"
10165 && r["target"]["action"] == "agent"));
10166 let rendered = response.to_string();
10167 for secret in [
10168 "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10169 "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10170 ] {
10171 assert!(!rendered.contains(secret), "{rendered}");
10172 }
10173 let refused =
10174 triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10175 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10176 }
10177
10178 fn triggers_list(params: Value) -> Value {
10179 let mut service = HarnessSessionService::new();
10180 service.handle(request(1, "harness.v1.triggers.list", params))
10181 }
10182
10183 #[test]
10184 fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10185 let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10186 let rows = response["result"]["routes"]
10187 .as_array()
10188 .expect("routes")
10189 .clone();
10190 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10191 assert_eq!(hermes.len(), 2, "{rows:#?}");
10192 assert_eq!(hermes[0]["target"], "coder");
10193 assert_eq!(hermes[0]["match"]["platform"], "slack");
10194 assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10195 assert_eq!(hermes[0]["specificity"], 4);
10196 assert_eq!(hermes[1]["default"], true);
10197 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10198 assert!(
10199 openclaw.iter().any(|r| r["target"] == "design"
10200 && r["match"]["platform"] == "slack"
10201 && r["specificity"] == 1),
10202 "{openclaw:#?}"
10203 );
10204 assert!(openclaw.iter().any(|r| r["default"] == true));
10205 let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10207 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10208 }
10209
10210 fn routes_list(params: Value) -> Value {
10211 let mut service = HarnessSessionService::new();
10212 service.handle(request(1, "harness.v1.routes.list", params))
10213 }
10214
10215 #[test]
10216 fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10217 let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10218 let result = &response["result"];
10219 let ids: Vec<&str> = result["jobs"]
10220 .as_array()
10221 .unwrap()
10222 .iter()
10223 .map(|job| job["id"].as_str().unwrap())
10224 .collect();
10225 assert_eq!(
10226 ids,
10227 vec![
10228 "release-watch",
10229 "toolu_wake_recheck",
10230 "digest-15m",
10231 "nightly-audit",
10232 "coder-standup",
10233 "ops-once-boot",
10234 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10235 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10236 "cron_standup",
10237 "cron_reindex",
10238 ],
10239 "{result}"
10240 );
10241
10242 let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10246 assert_eq!(health["harness"], "openclaw");
10247 assert_eq!(health["schedule"]["kind"], "interval");
10248 assert_eq!(health["schedule"]["minutes"], 10.0);
10249 assert_eq!(health["session_target"], "isolated");
10250 assert_eq!(health["payload"]["kind"], "prompt");
10251 assert_eq!(health["payload"]["text"], "nightly health check");
10252 assert_eq!(health["deliver"]["mode"], "announce");
10256 assert_eq!(health["deliver"]["target"], "last");
10257 assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10258 let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10259 assert_eq!(digest["schedule"]["kind"], "cron");
10260 assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10261 assert_eq!(digest["session_target"], "main");
10262 assert_eq!(digest["payload"]["kind"], "system_event");
10263
10264 let cron = job_row(result, "release-watch");
10266 assert_eq!(cron["harness"], "claude-code");
10267 assert_eq!(cron["scope"], "session");
10268 assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10269 assert_eq!(cron["schedule"]["kind"], "cron");
10270 assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10271 assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10272 assert_eq!(cron["payload"]["kind"], "prompt");
10273 assert_eq!(cron["recurring"], true);
10274 assert_eq!(cron["deliver"]["target"], "session");
10275 let wakeup = job_row(result, "toolu_wake_recheck");
10276 assert_eq!(wakeup["payload"]["kind"], "wakeup");
10277 assert_eq!(wakeup["schedule"]["kind"], "once");
10278 assert_eq!(wakeup["recurring"], false);
10279 assert_eq!(wakeup["state"], "pending");
10280
10281 let interval = job_row(result, "digest-15m");
10283 assert_eq!(interval["harness"], "hermes");
10284 assert_eq!(interval["scope"], "install");
10285 assert_eq!(interval["profile"], Value::Null);
10286 assert_eq!(interval["schedule"]["kind"], "interval");
10287 assert_eq!(interval["schedule"]["minutes"], 15.0);
10288 assert_eq!(interval["schedule"]["display"], "every 15 min");
10289 assert_eq!(interval["deliver"]["target"], "origin");
10290 assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10291 assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10292 assert_eq!(interval["last_status"], "ok");
10293 let nightly = job_row(result, "nightly-audit");
10294 assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10295 assert_eq!(nightly["deliver"]["target"], "local");
10296 assert_eq!(nightly["enabled"], false);
10297 assert_eq!(nightly["state"], "paused");
10298 let profiled = job_row(result, "ops-once-boot");
10300 assert_eq!(profiled["profile"], "ops");
10301 assert_eq!(profiled["schedule"]["kind"], "once");
10302 assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10303 assert_eq!(profiled["payload"]["kind"], "script");
10304 assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10306 assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10307 assert_eq!(profiled["recurring"], false);
10308
10309 let standup_to_group = job_row(result, "coder-standup");
10312 assert_eq!(standup_to_group["deliver"]["target"], "origin");
10313 assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10314 assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10315 assert!(standup_to_group["deliver"]["mode"].is_null());
10317 assert!(standup_to_group["deliver"]["account"].is_null());
10318
10319 let standup = job_row(result, "cron_standup");
10322 assert_eq!(standup["harness"], "openclaw");
10323 assert_eq!(standup["session_target"], "isolated");
10324 assert_eq!(standup["deliver"]["mode"], "announce");
10325 assert_eq!(standup["deliver"]["target"], "slack");
10326 assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10327 assert_eq!(standup["payload"]["kind"], "prompt");
10328 assert_eq!(standup["profile"], "main");
10329 let reindex = job_row(result, "cron_reindex");
10330 assert_eq!(reindex["session_target"], "main");
10331 assert_eq!(reindex["payload"]["kind"], "system_event");
10332 assert_eq!(reindex["schedule"]["kind"], "interval");
10333 assert_eq!(reindex["schedule"]["display"], "every 240 min");
10334 assert_eq!(reindex["enabled"], false);
10335
10336 let states: Vec<(&str, &str)> = result["sources"]
10338 .as_array()
10339 .unwrap()
10340 .iter()
10341 .map(|source| {
10342 (
10343 source["harness"].as_str().unwrap(),
10344 source["state"].as_str().unwrap(),
10345 )
10346 })
10347 .collect();
10348 assert_eq!(
10352 states,
10353 vec![
10354 ("claude-code", "scanned"),
10355 ("hermes", "read"),
10356 ("hermes", "absent_store"),
10357 ("hermes", "read"),
10358 ("openclaw", "read"),
10359 ("openclaw", "read"),
10360 ],
10361 "{result}"
10362 );
10363 }
10364
10365 #[test]
10366 fn jobs_list_filters_by_harness_session_and_profile() {
10367 let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10368 let ids: Vec<&str> = by_harness["result"]["jobs"]
10369 .as_array()
10370 .unwrap()
10371 .iter()
10372 .map(|job| job["id"].as_str().unwrap())
10373 .collect();
10374 assert_eq!(
10375 ids,
10376 vec![
10377 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10378 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10379 "cron_standup",
10380 "cron_reindex",
10381 ]
10382 );
10383
10384 let by_session = jobs_list(json!({
10385 "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10386 "homes": jobs_fixture_homes(),
10387 }));
10388 let jobs = by_session["result"]["jobs"].as_array().unwrap();
10389 assert_eq!(jobs.len(), 2, "{by_session}");
10390 assert!(jobs
10391 .iter()
10392 .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10393
10394 let by_profile = jobs_list(json!({
10395 "harness": "hermes",
10396 "profile": "ops",
10397 "homes": jobs_fixture_homes(),
10398 }));
10399 let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10400 assert_eq!(jobs.len(), 1, "{by_profile}");
10401 assert_eq!(jobs[0]["id"], "ops-once-boot");
10402 }
10403
10404 #[test]
10405 fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10406 let mut service = HarnessSessionService::new();
10407 let hermes = service.handle(request(
10408 1,
10409 "harness.v1.jobs.get",
10410 json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10411 ));
10412 assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10413 assert_eq!(hermes["result"]["source"]["provider"], "nous");
10415 assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10416
10417 let claude = service.handle(request(
10418 2,
10419 "harness.v1.jobs.get",
10420 json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10421 ));
10422 assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10423 assert_eq!(
10424 claude["result"]["source"]["tool_use_id"],
10425 "toolu_cron_release_watch"
10426 );
10427
10428 let missing = service.handle(request(
10429 3,
10430 "harness.v1.jobs.get",
10431 json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10432 ));
10433 assert!(missing["error"]["message"]
10434 .as_str()
10435 .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10436 }
10437
10438 #[test]
10439 fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10440 let mut service = HarnessSessionService::new();
10441 for (id, method, params) in [
10442 (
10443 1,
10444 "harness.v1.jobs.list",
10445 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10446 ),
10447 (
10448 2,
10449 "harness.v1.jobs.get",
10450 json!({"harness": "codex", "id": "anything"}),
10451 ),
10452 ] {
10453 let response = service.handle(request(id, method, params));
10454 assert_eq!(response["error"]["code"], -32020, "{response}");
10455 assert!(response["error"]["message"]
10456 .as_str()
10457 .is_some_and(|message| message.contains("has no scheduled jobs")));
10458 assert!(response.get("result").is_none());
10459 }
10460 }
10461
10462 #[test]
10463 fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10464 let scratch = std::env::temp_dir().join(format!(
10465 "supercode-jobs-migrated-{}-{}",
10466 std::process::id(),
10467 generated_session_id()
10468 ));
10469 std::fs::create_dir_all(&scratch).unwrap();
10470 let response = jobs_list(json!({
10471 "harness": "openclaw",
10472 "homes": {"openclaw": scratch.clone()},
10473 }));
10474 let result = &response["result"];
10475 assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10476 assert_eq!(result["sources"][0]["state"], "absent_store");
10477 assert_eq!(result["sources"][0]["harness"], "openclaw");
10478 std::fs::remove_dir_all(&scratch).ok();
10479 }
10480
10481 const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10490 const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10492
10493 fn runs_list(params: Value) -> Value {
10494 let mut service = HarnessSessionService::new();
10495 service.handle(request(1, "harness.v1.runs.list", params))
10496 }
10497
10498 fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10499 result["runs"]
10500 .as_array()
10501 .expect("runs is an array")
10502 .iter()
10503 .find(|run| run["id"] == id)
10504 .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10505 }
10506
10507 #[test]
10508 fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10509 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10510 let result = &response["result"];
10511 let ids: Vec<&str> = result["runs"]
10512 .as_array()
10513 .expect("runs is an array")
10514 .iter()
10515 .map(|run| run["id"].as_str().unwrap())
10516 .collect();
10517 let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10518 assert_eq!(
10519 ids,
10520 vec![
10521 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10523 "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10524 "c3d4e5f60718293a4b5c6d7e8f901234",
10525 "f60718293a4b5c6d7e8f901234567890",
10526 "e5f60718293a4b5c6d7e8f9012345678",
10527 "d4e5f60718293a4b5c6d7e8f90123456",
10528 "run_health_0002",
10530 digest_fire.as_str(),
10531 "run_health_0001",
10532 ],
10533 "{result}"
10534 );
10535
10536 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10539 assert_eq!(failed["harness"], "hermes");
10540 assert_eq!(failed["job_id"], "job42");
10541 assert_eq!(failed["status"], "failed");
10542 assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10543 assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10544
10545 let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10548 assert_eq!(abandoned["status"], "unknown");
10549 assert_eq!(abandoned["job_id"], "ops-once-boot");
10550
10551 let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10553 assert_eq!(running["status"], "running");
10554 assert!(running["finished_at"].is_null(), "{running}");
10555 assert!(running["session_id"].is_null(), "{running}");
10556
10557 let ok = run_row(result, "run_health_0001");
10560 assert_eq!(ok["harness"], "openclaw");
10561 assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10562 assert_eq!(ok["status"], "ok");
10563 assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10564 assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10565 assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10566 assert!(ok["claimed_at"].is_null(), "{ok}");
10568
10569 assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10572
10573 for id in [
10578 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10579 "d4e5f60718293a4b5c6d7e8f90123456",
10580 ] {
10581 assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10582 }
10583
10584 let sources = result["sources"].as_array().unwrap();
10587 let states: Vec<(&str, &str)> = sources
10588 .iter()
10589 .map(|source| {
10590 (
10591 source["harness"].as_str().unwrap(),
10592 source["state"].as_str().unwrap(),
10593 )
10594 })
10595 .collect();
10596 assert_eq!(
10597 states,
10598 vec![
10599 ("hermes", "read"),
10600 ("hermes", "absent_store"),
10601 ("hermes", "read"),
10602 ("openclaw", "read"),
10603 ],
10604 "{result}"
10605 );
10606 assert_eq!(sources[2]["profile"], "ops");
10607 assert!(sources[3]["path"]
10608 .as_str()
10609 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10610 }
10611
10612 #[test]
10613 fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10614 let response = runs_list(json!({
10615 "harness": "hermes",
10616 "job": "job42",
10617 "homes": jobs_fixture_homes(),
10618 }));
10619 let result = &response["result"];
10620 assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10621
10622 let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10626 assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10627
10628 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10632 assert!(failed["session_id"].is_null(), "{failed}");
10633 }
10634
10635 #[test]
10639 fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10640 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10641 let result = &response["result"];
10642
10643 let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10647 assert_eq!(delivered["status"], "completed");
10648 assert_eq!(delivered["delivery"]["state"], "delivered");
10649 assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10650 assert_eq!(delivered["delivery"]["attempts"], 1);
10651 assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10652 assert_eq!(
10653 delivered["delivery"]["delivered_at"],
10654 "2026-09-02T09:00:30.400Z"
10655 );
10656
10657 let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10660 assert_eq!(undelivered["status"], "completed");
10661 assert_eq!(undelivered["delivery"]["state"], "failed");
10662 assert_eq!(undelivered["delivery"]["attempts"], 3);
10663 assert_eq!(
10664 undelivered["delivery"]["last_error"],
10665 "telegram send failed: Bad Request: chat not found"
10666 );
10667 assert!(
10670 undelivered["delivery"]["delivered_at"].is_null(),
10671 "{undelivered}"
10672 );
10673
10674 let announced = run_row(result, "run_health_0001");
10677 assert_eq!(announced["delivery"]["state"], "delivered");
10678 assert_eq!(announced["delivery"]["target"], "last");
10679 assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10681 assert!(
10682 announced["delivery"]["delivered_at"].is_null(),
10683 "{announced}"
10684 );
10685 let refused = run_row(result, "run_health_0002");
10686 assert_eq!(refused["delivery"]["state"], "not-delivered");
10687 assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10688
10689 let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10692 assert!(skipped["delivery"].is_null(), "{skipped}");
10693 }
10694
10695 #[test]
10700 fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10701 let scratch = std::env::temp_dir().join(format!(
10702 "supercode-runs-delivery-{}-{}",
10703 std::process::id(),
10704 generated_session_id()
10705 ));
10706 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10707 let fixture = jobs_fixture_root().join("hermes_home");
10708 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10709 for name in ["cron/executions.db", "cron/jobs.json"] {
10710 std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10711 }
10712 {
10713 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10714 connection
10717 .execute(
10718 "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10719 [],
10720 )
10721 .unwrap();
10722 connection
10725 .execute(
10726 "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10727 ('cron_coder-standup_20260902_090010', 'cron', \
10728 'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10729 [],
10730 )
10731 .unwrap();
10732 }
10733 let response = runs_list(json!({
10734 "harness": "hermes",
10735 "job": "coder-standup",
10736 "homes": {"hermes": scratch.join("state.db")},
10737 }));
10738 let result = &response["result"];
10739 let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10740 assert_eq!(
10741 matched["session_id"], "cron_coder-standup_20260902_090010",
10742 "{result}"
10743 );
10744 assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10745 assert_eq!(
10746 matched["delivery"]["target"], "slack:C0FALLBACK:55",
10747 "{result}"
10748 );
10749 std::fs::remove_dir_all(&scratch).ok();
10750 }
10751
10752 #[test]
10753 fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10754 let scratch = std::env::temp_dir().join(format!(
10759 "supercode-runs-compressed-{}-{}",
10760 std::process::id(),
10761 generated_session_id()
10762 ));
10763 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10764 let fixture = jobs_fixture_root().join("hermes_home");
10765 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10766 std::fs::copy(
10767 fixture.join("cron/executions.db"),
10768 scratch.join("cron/executions.db"),
10769 )
10770 .unwrap();
10771 {
10772 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10773 connection
10774 .execute(
10775 "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10776 ["cron_job42_20260902_120000"],
10777 )
10778 .unwrap();
10779 connection
10780 .execute(
10781 "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10782 VALUES ('job42-after-compaction', 'cron', \
10783 'cron_job42_20260902_120000', 1788350000.0)",
10784 [],
10785 )
10786 .unwrap();
10787 }
10788 let response = runs_list(json!({
10789 "harness": "hermes",
10790 "job": "job42",
10791 "homes": {"hermes": scratch.join("state.db")},
10792 }));
10793 let result = &response["result"];
10794 assert_eq!(
10795 run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10796 "job42-after-compaction",
10797 "{result}"
10798 );
10799 std::fs::remove_dir_all(&scratch).ok();
10800 }
10801
10802 #[test]
10803 fn runs_list_filters_by_job_and_caps_by_limit() {
10804 let by_job = runs_list(json!({
10805 "harness": "openclaw",
10806 "job": OPENCLAW_HEALTH_JOB,
10807 "homes": jobs_fixture_homes(),
10808 }));
10809 let ids: Vec<&str> = by_job["result"]["runs"]
10810 .as_array()
10811 .unwrap()
10812 .iter()
10813 .map(|run| run["id"].as_str().unwrap())
10814 .collect();
10815 assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10816
10817 let capped = runs_list(json!({
10818 "harness": "openclaw",
10819 "limit": 1,
10820 "homes": jobs_fixture_homes(),
10821 }));
10822 let runs = capped["result"]["runs"].as_array().unwrap();
10823 assert_eq!(runs.len(), 1, "{capped}");
10824 assert_eq!(runs[0]["id"], "run_health_0002");
10826 }
10827
10828 #[test]
10829 fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10830 let mut service = HarnessSessionService::new();
10831 let hermes = service.handle(request(
10832 1,
10833 "harness.v1.runs.get",
10834 json!({
10835 "harness": "hermes",
10836 "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10837 "homes": jobs_fixture_homes(),
10838 }),
10839 ));
10840 assert_eq!(hermes["result"]["run"]["status"], "completed");
10841 assert_eq!(
10842 hermes["result"]["run"]["session_id"],
10843 "cron_job42_20260902_120000"
10844 );
10845 assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10847 assert_eq!(hermes["result"]["source"]["pid"], 4242);
10848 assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10849
10850 let openclaw = service.handle(request(
10851 2,
10852 "harness.v1.runs.get",
10853 json!({
10854 "harness": "openclaw",
10855 "id": "run_health_0002",
10856 "homes": jobs_fixture_homes(),
10857 }),
10858 ));
10859 assert_eq!(openclaw["result"]["run"]["status"], "error");
10860 assert_eq!(
10863 openclaw["result"]["source"]["delivery_status"],
10864 "not-delivered"
10865 );
10866 assert_eq!(
10867 openclaw["result"]["source"]["delivery_error"],
10868 "channel_not_found"
10869 );
10870 assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10871 assert_eq!(
10872 openclaw["result"]["run"]["delivery"]["state"],
10873 "not-delivered"
10874 );
10875 assert_eq!(
10876 openclaw["result"]["run"]["delivery"]["last_error"],
10877 "channel_not_found"
10878 );
10879
10880 let missing = service.handle(request(
10881 3,
10882 "harness.v1.runs.get",
10883 json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10884 ));
10885 assert!(missing["error"]["message"]
10886 .as_str()
10887 .is_some_and(|message| message.contains("no run `no-such-run`")));
10888 }
10889
10890 #[test]
10891 fn runs_refuse_a_harness_that_keeps_no_run_store() {
10892 let mut service = HarnessSessionService::new();
10893 for (id, method, params) in [
10894 (
10897 1,
10898 "harness.v1.runs.list",
10899 json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10900 ),
10901 (
10902 2,
10903 "harness.v1.runs.get",
10904 json!({"harness": "claude-code", "id": "anything"}),
10905 ),
10906 (
10907 3,
10908 "harness.v1.runs.list",
10909 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10910 ),
10911 ] {
10912 let response = service.handle(request(id, method, params));
10913 assert_eq!(response["error"]["code"], -32020, "{response}");
10914 assert!(response["error"]["message"]
10915 .as_str()
10916 .is_some_and(|message| message.contains("keeps no run store")));
10917 assert!(response.get("result").is_none());
10918 }
10919 }
10920
10921 #[test]
10922 fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10923 let scratch = std::env::temp_dir().join(format!(
10924 "supercode-runs-empty-{}-{}",
10925 std::process::id(),
10926 generated_session_id()
10927 ));
10928 std::fs::create_dir_all(&scratch).unwrap();
10929 let response = runs_list(json!({
10930 "harness": "openclaw",
10931 "homes": {"openclaw": scratch.clone()},
10932 }));
10933 let result = &response["result"];
10934 assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10935 assert_eq!(result["sources"][0]["state"], "absent_store");
10936 assert!(result["sources"][0]["path"]
10937 .as_str()
10938 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10939 std::fs::remove_dir_all(&scratch).ok();
10940 }
10941}