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