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 approval_policy: None,
2638 }),
2639 )
2640 .await
2641 {
2642 Ok(Ok(mut connection)) => {
2643 match stabilize_handshake(connection.as_mut()).await {
2644 Ok(()) => {
2645 auth = HarnessAuthState::Ready;
2646 runtime = HarnessRuntimeState::Ready;
2647 reason = Some(
2648 "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2649 .into(),
2650 );
2651 repair = None;
2652 }
2653 Err(message) => {
2654 auth = if looks_like_auth_error(&message) {
2655 HarnessAuthState::Required
2656 } else if configured {
2657 HarnessAuthState::Configured
2658 } else {
2659 HarnessAuthState::Unknown
2660 };
2661 reason = Some(format!(
2662 "No-prompt runtime handshake became unhealthy during startup: {message}"
2663 ));
2664 repair = Some(if auth == HarnessAuthState::Required {
2665 format!(
2666 "Run `{}` interactively once and complete sign-in, then probe again.",
2667 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2668 )
2669 } else {
2670 "Run the harness directly to inspect its startup failure, then probe again."
2671 .into()
2672 });
2673 }
2674 }
2675 let _ =
2676 tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2677 }
2678 Ok(Err(error)) => {
2679 let message = truncate_text(&error.to_string(), 500);
2680 auth = if looks_like_auth_error(&message) {
2681 HarnessAuthState::Required
2682 } else if configured {
2683 HarnessAuthState::Configured
2684 } else {
2685 HarnessAuthState::Unknown
2686 };
2687 reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2688 repair = Some(if auth == HarnessAuthState::Required {
2689 format!(
2690 "Run `{}` interactively once and complete sign-in, then probe again.",
2691 launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2692 )
2693 } else {
2694 "Check the harness installation and run the handshake probe again."
2695 .into()
2696 });
2697 }
2698 Err(_) => {
2699 reason =
2700 Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2701 repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2702 }
2703 }
2704 let _ = isolated.cleanup();
2713 tokio::time::sleep(Duration::from_millis(250)).await;
2714 if let Err(error) = isolated.cleanup() {
2715 auth = if configured {
2716 HarnessAuthState::Configured
2717 } else {
2718 HarnessAuthState::Unknown
2719 };
2720 runtime = HarnessRuntimeState::Degraded;
2721 reason = Some(format!(
2722 "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2723 ));
2724 repair = Some(
2725 "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2726 .into(),
2727 );
2728 }
2729 }
2730 Err(error) => {
2731 reason = Some(error_message(error));
2732 }
2733 }
2734 } else if installed && configured {
2735 reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2736 } else if installed && auth == HarnessAuthState::Required {
2737 reason = Some("Executable found, but no native authentication evidence is present.".into());
2738 repair = Some(format!(
2739 "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2740 descriptor.id.as_str()
2741 ));
2742 } else if installed {
2743 reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2744 repair = Some(format!(
2745 "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2746 launch
2747 .map(|launch| launch.program.as_str())
2748 .unwrap_or("the harness")
2749 ));
2750 }
2751
2752 let effective_capabilities = if installed {
2753 descriptor.runtime.capabilities.clone()
2754 } else {
2755 unavailable_capabilities()
2756 };
2757 let running = probe_running_instance(descriptor.id.as_str());
2758 LocalHarness {
2759 gateway: gateway_health(
2760 descriptor.id.as_str(),
2761 installed,
2762 running.as_ref(),
2763 version.as_deref(),
2764 ),
2765 id: descriptor.id,
2766 display_name: descriptor.display_name,
2767 supported: true,
2768 installed,
2769 executable: executable.map(|path| path.to_string_lossy().into_owned()),
2770 version,
2771 auth,
2772 runtime,
2773 protocol: descriptor.runtime.protocol,
2774 capabilities: descriptor.runtime.capabilities,
2775 effective_capabilities,
2776 sessions: HarnessSessionCounts { global, workspace },
2777 running,
2778 reason,
2779 repair,
2780 }
2781}
2782
2783async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2784 let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2785 loop {
2786 let now = tokio::time::Instant::now();
2787 if now >= deadline {
2788 return Ok(());
2789 }
2790 match tokio::time::timeout(deadline - now, connection.next_event()).await {
2791 Err(_) => return Ok(()),
2792 Ok(Ok(Some(event))) => {
2793 if let Some(message) = handshake_event_failure(&event) {
2794 return Err(truncate_text(&message, 500));
2795 }
2796 }
2797 Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2798 Ok(Err(error)) => return Err(error.to_string()),
2799 }
2800 }
2801}
2802
2803fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2804 let detail = event
2805 .payload
2806 .get("message")
2807 .or_else(|| event.payload.get("line"))
2808 .and_then(Value::as_str)
2809 .unwrap_or(event.kind.as_str());
2810 match event.kind.as_str() {
2811 "transport_closed" => Some("runtime transport closed during startup".into()),
2812 "transport_error" => Some(format!("runtime transport error: {detail}")),
2813 "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2814 _ => None,
2819 }
2820}
2821
2822fn indexed_claude_window(
2823 locator: &SessionLocator,
2824 options: &SessionLoadOptions,
2825) -> std::result::Result<Option<Value>, ServiceError> {
2826 use supercode_interchange::session::ClaudeReadIndex;
2827 if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2830 || options.include_subagents != Some(false)
2831 {
2832 return Ok(None);
2833 }
2834 let crate::StorageLocator::File { path } = &locator.storage else {
2835 return Ok(None);
2836 };
2837 if !ClaudeReadIndex::supports(path)
2838 .map_err(|error| ServiceError::Operation(error.to_string()))?
2839 {
2840 return Ok(None);
2841 }
2842 let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2843 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2844 let total = index.len();
2845 let (offset, end) = projected_message_window(total, options);
2846 let session = index
2847 .read_messages(offset..end)
2848 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2849 let summary = index
2850 .read_summary()
2851 .map_err(|error| ServiceError::Operation(error.to_string()))?;
2852 let selected_options = SessionLoadOptions {
2853 message_offset: None,
2854 message_limit: None,
2855 message_tail: None,
2856 ..options.clone()
2857 };
2858 let mut selected = projected_session_json(&session, &selected_options);
2859 selected["raw_record_count"] = json!(index.raw_record_count());
2860 Ok(Some(json!({
2861 "session": selected,
2862 "summary": projected_session_summary(&summary, options),
2863 "window": {
2864 "has_more": offset > 0 || end < total, "has_newer": end < total,
2865 "has_older": offset > 0, "newer_items": index.item_count(end..total),
2866 "offset": offset, "older_items": index.item_count(0..offset),
2867 "returned": end - offset, "total_messages": total,
2868 }
2869 })))
2870}
2871
2872fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2873 let total_messages = session.messages.len();
2874 let (offset, end) = projected_message_window(total_messages, options);
2875 json!({
2876 "session": projected_session_json(session, options),
2877 "summary": projected_session_summary(session, options),
2878 "window": {
2879 "has_more": offset > 0 || end < total_messages,
2880 "has_newer": end < total_messages,
2881 "has_older": offset > 0,
2882 "newer_items": normalized_item_count(&session.messages[end..]),
2883 "offset": offset,
2884 "older_items": normalized_item_count(&session.messages[..offset]),
2885 "returned": end.saturating_sub(offset),
2886 "total_messages": total_messages,
2887 }
2888 })
2889}
2890
2891fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2892 messages
2893 .iter()
2894 .map(|message| {
2895 let conversation = usize::from(
2896 matches!(message.role, Role::Assistant | Role::User)
2897 && message_has_content(message),
2898 );
2899 let tool_result =
2900 usize::from(message.role == Role::Tool && message_has_content(message));
2901 conversation + tool_result + message.tool_calls().len()
2902 })
2903 .sum()
2904}
2905
2906fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2907 let mut conversational = session.messages.iter().filter(|message| {
2908 matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2909 });
2910 let first_message = conversational.clone().next();
2911 let last_message = conversational.next_back();
2912 let mut assistant = session
2913 .messages
2914 .iter()
2915 .filter(|message| message.role == Role::Assistant && message_has_content(message));
2916 let first_assistant_message = assistant.clone().next();
2917 let last_assistant_message = assistant.next_back();
2918 let end_of_turn = session
2919 .messages
2920 .iter()
2921 .rev()
2922 .find(|message| message.role != Role::System)
2923 .is_some_and(|message| {
2924 message.role == Role::Assistant
2925 && message_has_content(message)
2926 && message.tool_calls().is_empty()
2927 && message.metadata.get("phase").map(String::as_str) != Some("commentary")
2929 });
2930 let project = |message: Option<&crate::ChatMessage>| {
2931 message.map(|message| project_inline_media(message_json(message), options))
2932 };
2933 json!({
2934 "end_of_turn": end_of_turn,
2935 "first_assistant_message": project(first_assistant_message),
2936 "first_message": project(first_message),
2937 "last_assistant_message": project(last_assistant_message),
2938 "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2939 "last_message": project(last_message),
2940 })
2941}
2942
2943fn message_has_content(message: &crate::ChatMessage) -> bool {
2944 message
2945 .content
2946 .as_deref()
2947 .is_some_and(|content| !content.trim().is_empty())
2948 || message
2949 .content_parts
2950 .as_ref()
2951 .is_some_and(|parts| !parts.is_empty())
2952}
2953
2954fn message_text(message: &crate::ChatMessage) -> String {
2955 if let Some(content) = &message.content {
2956 return content.clone();
2957 }
2958 message
2959 .content_parts
2960 .as_ref()
2961 .into_iter()
2962 .flatten()
2963 .filter_map(|part| part.get("text").and_then(Value::as_str))
2964 .collect::<Vec<_>>()
2965 .join("\n")
2966}
2967
2968fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2969 let (offset, end) = projected_message_window(session.messages.len(), options);
2970 let messages = session.messages[offset..end]
2971 .iter()
2972 .map(|message| project_inline_media(message_json(message), options))
2973 .collect::<Vec<_>>();
2974 let subagents = if options.include_subagents.unwrap_or(true) {
2975 let subagent_options = SessionLoadOptions {
2980 message_limit: None,
2981 message_offset: None,
2982 message_tail: None,
2983 ..options.clone()
2984 };
2985 session
2986 .subagents
2987 .iter()
2988 .map(|subagent| projected_session_json(subagent, &subagent_options))
2989 .collect::<Vec<_>>()
2990 } else {
2991 Vec::new()
2992 };
2993 json!({
2994 "source": match session.meta.source {
2995 SessionSource::ClaudeCode => "claude_code",
2996 SessionSource::Codex => "codex",
2997 SessionSource::Gemini => "gemini",
2998 SessionSource::Goose => "goose",
2999 SessionSource::Grok => "grok",
3000 SessionSource::Native => "native",
3001 SessionSource::OpenClaw => "openclaw",
3002 SessionSource::Hermes => "hermes",
3003 SessionSource::OpenCode => "opencode",
3004 SessionSource::Pi => "pi",
3005 },
3006 "session_id": session.meta.session_id,
3007 "ended_at": session.meta.ended_at,
3008 "end_reason": session.meta.end_reason,
3009 "model": session.meta.model,
3010 "cwd": session.meta.cwd,
3011 "system_prompt": session.meta.system_prompt,
3012 "agent_id": session.meta.agent_id,
3013 "parent_tool_use_id": session.meta.parent_tool_use_id,
3014 "lineage": session.meta.lineage,
3015 "messages": messages,
3016 "subagents": subagents,
3017 "raw_record_count": session.raw.len(),
3018 "parse_error_lines": session.parse_error_lines,
3019 })
3020}
3021
3022fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
3023 if let Some(tail) = options.message_tail {
3024 return (total.saturating_sub(tail), total);
3025 }
3026 let offset = options.message_offset.unwrap_or(0).min(total);
3027 let end = options
3028 .message_limit
3029 .map(|limit| offset.saturating_add(limit).min(total))
3030 .unwrap_or(total);
3031 (offset, end)
3032}
3033
3034fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
3035 let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
3036 return message;
3037 };
3038 for part in parts {
3039 let Some(url) = part
3040 .get("image_url")
3041 .and_then(|image| image.get("url"))
3042 .and_then(Value::as_str)
3043 else {
3044 continue;
3045 };
3046 let Some(rest) = url.strip_prefix("data:") else {
3047 continue;
3048 };
3049 let Some((media_type, encoded)) = rest.split_once(";base64,") else {
3050 continue;
3051 };
3052 let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
3053 let decoded_bytes = encoded.len().saturating_mul(3) / 4;
3054 let decoded_bytes = decoded_bytes.saturating_sub(padding);
3055 let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
3056 || options
3057 .max_inline_media_bytes
3058 .is_some_and(|limit| decoded_bytes > limit);
3059 if should_elide {
3060 *part = json!({
3061 "type": "media_reference",
3062 "media_type": media_type,
3063 "encoding": "base64",
3064 "encoded_bytes": encoded.len(),
3065 "decoded_bytes": decoded_bytes,
3066 "omitted": true,
3067 });
3068 }
3069 }
3070 message
3071}
3072
3073#[derive(Deserialize)]
3074struct LocatorParams {
3075 locator: SessionLocator,
3076 #[serde(default)]
3089 fidelity: Option<Fidelity>,
3090 #[serde(default)]
3093 view: Option<SessionReadView>,
3094}
3095
3096#[derive(Deserialize)]
3097struct SessionReadView {
3098 #[serde(default)]
3101 tail_messages: Option<usize>,
3102 #[serde(default)]
3105 include_subagents: bool,
3106 #[serde(default)]
3108 display_history: bool,
3109 #[serde(default)]
3112 max_message_chars: Option<usize>,
3113}
3114
3115impl LocatorParams {
3116 fn read_fidelity(&self) -> Fidelity {
3117 self.fidelity.unwrap_or(Fidelity::Semantic)
3118 }
3119
3120 fn include_subagents(&self) -> bool {
3121 self.view
3122 .as_ref()
3123 .map(|view| view.include_subagents)
3124 .unwrap_or(true)
3125 }
3126
3127 fn tail_messages(&self) -> Option<usize> {
3128 self.view
3129 .as_ref()
3130 .and_then(|view| view.tail_messages)
3131 .map(|limit| limit.clamp(1, 5_000))
3132 }
3133
3134 fn display_history(&self) -> bool {
3135 self.view.as_ref().is_some_and(|view| view.display_history)
3136 }
3137
3138 fn max_message_chars(&self) -> Option<usize> {
3139 self.view
3140 .as_ref()
3141 .and_then(|view| view.max_message_chars)
3142 .map(|limit| limit.clamp(256, 64_000))
3143 }
3144
3145 fn bound_session(&self, session: &mut Session) {
3146 bound_session_view(session, self.tail_messages(), self.max_message_chars());
3147 }
3148}
3149
3150#[derive(Debug, Clone, Copy, Default, Deserialize)]
3151#[serde(rename_all = "snake_case")]
3152enum InlineMediaMode {
3153 #[default]
3154 Full,
3155 Metadata,
3156}
3157
3158#[derive(Debug, Clone, Default, Deserialize)]
3159#[serde(default)]
3160struct SessionLoadOptions {
3161 include_subagents: Option<bool>,
3162 inline_media: InlineMediaMode,
3163 max_inline_media_bytes: Option<usize>,
3164 message_limit: Option<usize>,
3165 message_offset: Option<usize>,
3166 message_tail: Option<usize>,
3167}
3168
3169impl SessionLoadOptions {
3170 fn validate(&self) -> std::result::Result<(), ServiceError> {
3171 if self.message_tail.is_some()
3172 && (self.message_limit.is_some() || self.message_offset.is_some())
3173 {
3174 return Err(ServiceError::InvalidParams(
3175 "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
3176 .into(),
3177 ));
3178 }
3179 Ok(())
3180 }
3181}
3182
3183#[derive(Deserialize)]
3184struct LoadSessionParams {
3185 #[serde(flatten)]
3186 read: LocatorParams,
3187 #[serde(default)]
3188 options: Option<SessionLoadOptions>,
3189}
3190
3191#[derive(Deserialize)]
3192struct UnfollowParams {
3193 subscription: String,
3194}
3195
3196#[derive(Debug, Deserialize)]
3197#[serde(deny_unknown_fields)]
3198struct IndexResizeParams {
3199 subscription: String,
3200 limit: usize,
3201}
3202
3203#[derive(Deserialize)]
3204struct ActivitySubscribeParams {
3205 locators: Vec<SessionLocator>,
3206 #[serde(default)]
3207 homes: crate::HarnessHomes,
3208}
3209
3210#[derive(Deserialize)]
3211struct MessageSessionParams {
3212 locator: SessionLocator,
3213 text: String,
3214 #[serde(default)]
3217 homes: crate::HarnessHomes,
3218}
3219
3220#[derive(Deserialize)]
3221#[serde(deny_unknown_fields)]
3222struct HarnessSettingsParams {
3223 harness: String,
3224}
3225
3226#[derive(Deserialize)]
3227#[serde(deny_unknown_fields)]
3228struct ConfigureHarnessParams {
3229 harness: String,
3230 #[serde(default)]
3231 changes: Vec<crate::HarnessSettingChange>,
3232 #[serde(default)]
3233 expected_revision: Option<String>,
3234}
3235
3236fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3237 match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3238 Ok(report) => (
3239 serde_json::to_value(report).unwrap_or(Value::Null),
3240 Value::Null,
3241 ),
3242 Err(error) => (
3243 Value::Null,
3244 Value::String(format!(
3245 "Supercode could not inspect Claude Code inbound controls: {error}"
3246 )),
3247 ),
3248 }
3249}
3250
3251async fn message_live_session(
3263 params: &MessageSessionParams,
3264 runner: &dyn crate::claude_peer::CourierRunner,
3265) -> Value {
3266 if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
3267 return json!({
3268 "delivered_to_bus": false,
3269 "refusal": {
3270 "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
3271 "message": format!(
3272 "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
3273 params.locator.harness.as_str()
3274 ),
3275 },
3276 });
3277 }
3278 let (inbound_controls, inbound_controls_error) =
3279 claude_inbound_controls_or_error(¶ms.homes);
3280 match crate::claude_peer::message_claude_peer(
3281 ¶ms.homes,
3282 ¶ms.locator.session_id,
3283 ¶ms.text,
3284 runner,
3285 )
3286 .await
3287 {
3288 Ok(delivery) => json!({
3289 "delivered_to_bus": true,
3290 "target": {
3291 "session_id": delivery.target.session_id,
3292 "name": delivery.target.name,
3293 "pid": delivery.target.pid,
3294 "cwd": delivery.target.cwd,
3295 "status": delivery.target.status.map(|status| status.as_str()),
3296 },
3297 "courier": {
3298 "model": crate::claude_peer::COURIER_MODEL,
3299 "report": delivery.courier_report,
3300 },
3301 "inbound_controls": inbound_controls,
3302 "inbound_controls_error": inbound_controls_error,
3303 }),
3304 Err(refusal) => json!({
3305 "delivered_to_bus": false,
3306 "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
3307 "inbound_controls": inbound_controls,
3308 "inbound_controls_error": inbound_controls_error,
3309 }),
3310 }
3311}
3312
3313#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3318struct FollowedSource {
3319 harness: String,
3320 session_id: String,
3321 reported: Option<String>,
3322}
3323
3324#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3325struct ActivitySubscription {
3326 locators: Vec<SessionLocator>,
3327 homes: crate::HarnessHomes,
3328 reported: BTreeMap<(String, String), crate::SessionActivity>,
3329}
3330
3331fn peers_for_descriptors(
3332 descriptors: &[SessionDescriptor],
3333 homes: &HarnessHomes,
3334) -> Vec<crate::claude_peer::ClaudePeerSession> {
3335 if descriptors
3336 .iter()
3337 .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
3338 {
3339 crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3340 } else {
3341 Vec::new()
3342 }
3343}
3344
3345fn live_descriptor_value(
3351 session: &SessionDescriptor,
3352 peers: &[crate::claude_peer::ClaudePeerSession],
3353) -> std::result::Result<Value, ServiceError> {
3354 let mut value = serde_json::to_value(session)
3355 .map_err(|error| ServiceError::Operation(error.to_string()))?;
3356 if let Some(workspace) = &session.cwd {
3357 let source = LiveRuntimeSource {
3358 harness: session.locator.harness.as_str().to_string(),
3359 session_id: session.locator.session_id.clone(),
3360 workspace: workspace.clone(),
3361 };
3362 if let Some(endpoint) = discover_live_runtime(&source)
3363 .map_err(|error| ServiceError::Operation(error.to_string()))?
3364 {
3365 value["live_endpoint"] = json!(endpoint.as_str());
3366 }
3367 }
3368 if value.get("live_endpoint").is_none() {
3369 if let Some(peer) = peers.iter().find(|peer| {
3370 session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3371 && peer.session_id == session.locator.session_id
3372 }) {
3373 value["live_endpoint"] = json!(peer.endpoint().as_str());
3374 }
3375 }
3376 Ok(value)
3377}
3378
3379fn live_index_changes(
3380 changes: Vec<crate::session_index::SessionIndexChange>,
3381 homes: &HarnessHomes,
3382) -> std::result::Result<Vec<Value>, ServiceError> {
3383 use crate::session_index::SessionIndexChange;
3384 let has_claude = changes.iter().any(|change| match change {
3385 SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3386 descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3387 }
3388 SessionIndexChange::Removed { .. } => false,
3389 });
3390 let peers = if has_claude {
3391 crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3392 } else {
3393 Vec::new()
3394 };
3395 changes
3396 .into_iter()
3397 .map(|change| match change {
3398 SessionIndexChange::Added { descriptor } => Ok(json!({
3399 "kind": "added",
3400 "descriptor": live_descriptor_value(&descriptor, &peers)?,
3401 })),
3402 SessionIndexChange::Updated { descriptor } => Ok(json!({
3403 "kind": "updated",
3404 "descriptor": live_descriptor_value(&descriptor, &peers)?,
3405 })),
3406 SessionIndexChange::Removed { key } => Ok(json!({
3407 "kind": "removed",
3408 "key": key,
3409 })),
3410 })
3411 .collect()
3412}
3413
3414fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3415 use crate::{SessionPresence, SessionTurnState};
3416 match (activity.presence, activity.turn) {
3417 (SessionPresence::Persisted, _) => None,
3418 (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3419 (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3420 (SessionPresence::Running, SessionTurnState::Unknown)
3424 if activity.evidence.native_state.is_none() =>
3425 {
3426 None
3427 }
3428 (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3429 }
3430}
3431
3432#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3433#[serde(rename_all = "kebab-case")]
3434enum TransferFormat {
3435 ClaudeCode,
3436 Codex,
3437 #[serde(rename = "opencode", alias = "open-code")]
3438 OpenCode,
3439 Pi,
3440 Grok,
3441 Gemini,
3442 Goose,
3443 Hermes,
3447}
3448
3449impl TransferFormat {
3450 fn id(self) -> &'static str {
3451 match self {
3452 Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3453 Self::Codex => HarnessId::CODEX,
3454 Self::OpenCode => HarnessId::OPENCODE,
3455 Self::Pi => HarnessId::PI,
3456 Self::Grok => HarnessId::GROK,
3457 Self::Gemini => HarnessId::GEMINI,
3458 Self::Goose => HarnessId::GOOSE,
3459 Self::Hermes => HarnessId::HERMES,
3460 }
3461 }
3462}
3463
3464impl From<TransferFormat> for SessionFormat {
3465 fn from(value: TransferFormat) -> Self {
3466 match value {
3467 TransferFormat::ClaudeCode => Self::ClaudeCode,
3468 TransferFormat::Codex => Self::Codex,
3469 TransferFormat::OpenCode => Self::OpenCode,
3470 TransferFormat::Pi => Self::Pi,
3471 TransferFormat::Grok => Self::Grok,
3472 TransferFormat::Gemini => Self::Gemini,
3473 TransferFormat::Goose => Self::Goose,
3474 TransferFormat::Hermes => Self::Codex,
3476 }
3477 }
3478}
3479
3480#[derive(Deserialize)]
3481struct ImportSessionParams {
3482 source_harness: TransferFormat,
3483 content: String,
3484}
3485
3486#[derive(Deserialize)]
3487struct ExportSessionParams {
3488 locator: SessionLocator,
3489 target_harness: TransferFormat,
3490}
3491
3492#[derive(Deserialize)]
3493struct ReduceSessionParams {
3494 locator: SessionLocator,
3495 target_harness: TransferFormat,
3496 #[serde(default = "default_keep_last")]
3497 keep_last: usize,
3498}
3499
3500fn default_keep_last() -> usize {
3501 6
3502}
3503
3504#[derive(Deserialize)]
3505struct BranchSessionParams {
3506 locator: SessionLocator,
3507 #[serde(default)]
3508 target_harness: Option<TransferFormat>,
3509}
3510
3511#[derive(Deserialize)]
3512struct HandoffSessionParams {
3513 locator: SessionLocator,
3514 target_harness: TransferFormat,
3515 #[serde(default)]
3516 cwd: Option<PathBuf>,
3517}
3518
3519#[derive(Deserialize)]
3520struct MaterializeSessionParams {
3521 artifact: crate::native_materialize::MaterializeArtifact,
3522 cwd: PathBuf,
3523 #[serde(default)]
3525 homes: HarnessHomes,
3526}
3527
3528#[derive(Debug, Clone, Copy, Default, Deserialize)]
3529#[serde(rename_all = "snake_case")]
3530enum ResumePolicy {
3531 #[default]
3532 Default,
3533 Yolo,
3534}
3535
3536#[derive(Deserialize)]
3537struct ResumeInstructionsParams {
3538 locator: SessionLocator,
3539 #[serde(default)]
3540 cwd: Option<PathBuf>,
3541 #[serde(default)]
3542 policy: ResumePolicy,
3543}
3544
3545#[derive(Deserialize)]
3547struct WorkflowLoadParams {
3548 from: crate::workflow_doors::WorkflowHarness,
3549 home: PathBuf,
3550}
3551
3552#[derive(Deserialize)]
3555struct OrchestrationLoadParams {
3556 root: PathBuf,
3557 #[serde(default)]
3558 flavor: crate::orchestration_doors::HomeFlavor,
3559}
3560
3561#[derive(Deserialize)]
3564struct OrchestrationSaveParams {
3565 root: PathBuf,
3566 orchestration: crate::orchestration::Orchestration,
3567 #[serde(default)]
3568 vault: BTreeMap<String, String>,
3569}
3570
3571#[derive(Deserialize)]
3573struct OrchestrationCompileParams {
3574 from: crate::orchestration_doors::OrchestrationHarness,
3575 home: PathBuf,
3576}
3577
3578#[derive(Deserialize)]
3582struct OrchestrationDecompileParams {
3583 to: crate::orchestration_doors::OrchestrationHarness,
3584 orchestration: crate::orchestration::Orchestration,
3585 source: PathBuf,
3586 #[serde(default)]
3587 source_flavor: crate::orchestration_doors::SourceFlavor,
3588 dest: PathBuf,
3589 #[serde(default)]
3590 vault: BTreeMap<String, String>,
3591}
3592
3593#[derive(Deserialize)]
3596struct OrchestrationImportParams {
3597 from: crate::orchestration_doors::OrchestrationHarness,
3598 home: PathBuf,
3599 into: PathBuf,
3600}
3601
3602#[derive(Deserialize)]
3605struct OrchestrationExportParams {
3606 to: crate::orchestration_doors::OrchestrationHarness,
3607 root: PathBuf,
3608 dest: PathBuf,
3609}
3610
3611#[derive(Deserialize)]
3613struct JobsGetParams {
3614 harness: String,
3615 id: String,
3616 #[serde(default)]
3617 homes: crate::HarnessHomes,
3618}
3619
3620fn mutate_job(
3628 verb: crate::jobs_control::JobVerb,
3629 params: Value,
3630) -> std::result::Result<Value, ServiceError> {
3631 let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3632 refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3633 let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3634 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3635}
3636
3637fn mutate_skill(
3644 verb: crate::skills_control::SkillVerb,
3645 params: Value,
3646) -> std::result::Result<Value, ServiceError> {
3647 let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3648 if !crate::skills_control::supports_skill_control(&mutation.harness) {
3649 return Err(ServiceError::UnsupportedAction(format!(
3650 "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3651 mutation.harness,
3652 verb.as_str(),
3653 crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3654 )));
3655 }
3656 let outcome =
3657 crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3658 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3659}
3660
3661fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3663 match error {
3664 crate::skills_control::SkillControlError::Unsupported(message) => {
3665 ServiceError::UnsupportedAction(message)
3666 }
3667 crate::skills_control::SkillControlError::Invalid(message) => {
3668 ServiceError::InvalidParams(message)
3669 }
3670 crate::skills_control::SkillControlError::Failed(message) => {
3671 ServiceError::Operation(message)
3672 }
3673 }
3674}
3675
3676fn mutate_profile(
3684 verb: crate::profiles_control::ProfileVerb,
3685 params: Value,
3686) -> std::result::Result<Value, ServiceError> {
3687 let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3688 let outcome =
3689 crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3690 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3691}
3692
3693fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3695 match error {
3696 crate::profiles_control::ProfileControlError::Unsupported(message) => {
3697 ServiceError::UnsupportedAction(message)
3698 }
3699 crate::profiles_control::ProfileControlError::Invalid(message) => {
3700 ServiceError::InvalidParams(message)
3701 }
3702 crate::profiles_control::ProfileControlError::Failed(message) => {
3703 ServiceError::Operation(message)
3704 }
3705 }
3706}
3707
3708fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3712 match error {
3713 crate::jobs_control::JobControlError::Unsupported(message) => {
3714 ServiceError::UnsupportedAction(message)
3715 }
3716 crate::jobs_control::JobControlError::Invalid(message) => {
3717 ServiceError::InvalidParams(message)
3718 }
3719 crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3720 }
3721}
3722
3723fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3728 match error {
3729 crate::SessionControlError::Unsupported(message) => {
3730 ServiceError::UnsupportedAction(message)
3731 }
3732 crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3733 crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3734 }
3735}
3736
3737fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3742 if crate::jobs::supports_jobs(harness) {
3743 return Ok(());
3744 }
3745 Err(ServiceError::UnsupportedAction(format!(
3746 "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3747 crate::jobs::JOB_HARNESSES.join(", ")
3748 )))
3749}
3750
3751#[derive(Deserialize)]
3753struct RunsGetParams {
3754 harness: String,
3755 id: String,
3756 #[serde(default)]
3757 homes: crate::HarnessHomes,
3758}
3759
3760fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3765 if crate::runs::supports_runs(harness) {
3766 return Ok(());
3767 }
3768 Err(ServiceError::UnsupportedAction(format!(
3769 "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3770 crate::runs::RUN_HARNESSES.join(", ")
3771 )))
3772}
3773
3774#[derive(Serialize)]
3775struct SessionArtifact {
3776 source_harness: HarnessId,
3777 target_harness: &'static str,
3778 session_id: Option<String>,
3779 content: String,
3780 suggested_filename: String,
3781 files: Vec<SessionArtifactFile>,
3782 fidelity: Fidelity,
3783 residue: Vec<String>,
3784}
3785
3786#[derive(Serialize)]
3787struct SessionArtifactFile {
3788 path: String,
3789 content: String,
3790 role: ArtifactFileRole,
3791}
3792
3793#[derive(Serialize)]
3794#[serde(rename_all = "snake_case")]
3795enum ArtifactFileRole {
3796 Primary,
3797 Subagent,
3798 Bundle,
3799 SourceRecovery,
3800}
3801
3802#[derive(Serialize)]
3803struct StructuredLaunch {
3804 cwd: PathBuf,
3805 program: String,
3806 arguments: Vec<String>,
3807 env: BTreeMap<String, String>,
3808}
3809
3810struct HandoffInstructions {
3811 launch: StructuredLaunch,
3812 materialize: Option<StructuredLaunch>,
3813 requires_materialization: bool,
3814 note: String,
3815}
3816
3817#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3818#[serde(rename_all = "snake_case")]
3819enum HarnessProbeLevel {
3820 #[default]
3821 Passive,
3822 Handshake,
3823}
3824
3825#[derive(Default, Deserialize)]
3826#[serde(default)]
3827struct HarnessInventoryParams {
3828 harness: Option<HarnessId>,
3829 harnesses: Vec<HarnessId>,
3830 workspace: Option<PathBuf>,
3831 probe: HarnessProbeLevel,
3832 include_sessions: bool,
3833 skip_versions: bool,
3835}
3836
3837#[derive(Deserialize)]
3838struct HarnessAuthenticationParams {
3839 harness: HarnessId,
3840}
3841
3842#[derive(Deserialize)]
3843struct BeginHarnessAuthenticationParams {
3844 harness: HarnessId,
3845 #[serde(default = "local_browser_authentication_environment")]
3846 environment: crate::HarnessAuthenticationEnvironment,
3847 #[serde(default)]
3848 method: Option<crate::HarnessAuthenticationMethodId>,
3849 #[serde(default)]
3850 cwd: Option<PathBuf>,
3851}
3852
3853fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3854 crate::HarnessAuthenticationEnvironment::LocalBrowser
3855}
3856
3857#[derive(Serialize)]
3858struct HarnessInventoryReport {
3859 probe: HarnessProbeLevel,
3860 workspace: Option<PathBuf>,
3861 harnesses: Vec<LocalHarness>,
3862}
3863
3864#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3865#[serde(rename_all = "snake_case")]
3866enum HarnessAuthState {
3867 Ready,
3868 Configured,
3869 Required,
3870 Unknown,
3871}
3872
3873#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3874#[serde(rename_all = "snake_case")]
3875enum HarnessRuntimeState {
3876 Ready,
3877 Degraded,
3878 Unavailable,
3879}
3880
3881#[derive(Serialize)]
3882struct HarnessSessionCounts {
3883 global: Option<usize>,
3884 workspace: Option<usize>,
3885}
3886
3887#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3898#[serde(rename_all = "snake_case")]
3899pub enum GatewayState {
3900 Up,
3901 Down,
3902 Unknown,
3903}
3904
3905#[derive(Debug, Clone, Serialize)]
3907pub struct GatewayHealth {
3908 pub state: GatewayState,
3909 #[serde(skip_serializing_if = "Option::is_none")]
3914 pub endpoint: Option<String>,
3915 #[serde(skip_serializing_if = "Option::is_none")]
3916 pub version: Option<String>,
3917 pub evidence: String,
3919 pub checked_at_ms: u64,
3920}
3921
3922fn openclaw_gateway_endpoint(home: &Path) -> String {
3926 let config_path = home.join(".openclaw/openclaw.json");
3927 let gateway = std::fs::read_to_string(&config_path)
3928 .ok()
3929 .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3930 .and_then(|config| config.get("gateway").cloned());
3931 if let Some(url) = gateway
3932 .as_ref()
3933 .and_then(|gateway| gateway.get("url"))
3934 .and_then(serde_json::Value::as_str)
3935 {
3936 return url.to_string();
3937 }
3938 let port = gateway
3939 .as_ref()
3940 .and_then(|gateway| gateway.get("port"))
3941 .and_then(serde_json::Value::as_u64)
3942 .unwrap_or(18789);
3943 format!("ws://127.0.0.1:{port}")
3944}
3945
3946fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3953 let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3954 let output = std::process::Command::new(&program)
3955 .args(["gateway", "status"])
3956 .stdin(std::process::Stdio::null())
3957 .output()
3958 .ok()?;
3959 let text = format!(
3960 "{}{}",
3961 String::from_utf8_lossy(&output.stdout),
3962 String::from_utf8_lossy(&output.stderr)
3963 );
3964 let verdict = text.lines().find_map(|line| {
3965 let l = line.trim();
3966 if l.contains("supervised by launchd (PID")
3967 || l.contains("supervised by systemd (PID")
3968 || l.contains("Gateway is running")
3969 || l.contains("process is running")
3970 {
3971 Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3972 } else if l.contains("not running") || l.contains("not installed") {
3973 Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3974 } else {
3975 None
3976 }
3977 });
3978 verdict
3979}
3980
3981fn gateway_health(
3982 id: &str,
3983 installed: bool,
3984 running: Option<&RunningInstance>,
3985 version: Option<&str>,
3986) -> GatewayHealth {
3987 let checked_at_ms = now_epoch_ms();
3988 let home = std::env::var_os("HOME").map(PathBuf::from);
3989 match id {
3990 HarnessId::HERMES | HarnessId::OPENCLAW => {
3991 let endpoint = (id == HarnessId::OPENCLAW)
3992 .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3993 .flatten();
3994 let (state, evidence) = match running {
3995 Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3996 None if !installed => (
3997 GatewayState::Unknown,
3998 format!("`{id}` is not installed; no gateway to probe"),
3999 ),
4000 None if id == HarnessId::HERMES => match hermes_gateway_status() {
4001 Some((state, evidence)) => (state, evidence),
4004 None => (
4005 GatewayState::Down,
4006 "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
4007 ),
4008 },
4009 None => (
4010 GatewayState::Down,
4011 format!(
4012 "no TCP listener at {}",
4013 endpoint.as_deref().unwrap_or("the gateway endpoint")
4014 ),
4015 ),
4016 };
4017 GatewayHealth {
4018 state,
4019 endpoint,
4020 version: version.map(str::to_string),
4021 evidence,
4022 checked_at_ms,
4023 }
4024 }
4025 HarnessId::ORCHESTRATOR => {
4032 let root = crate::HarnessHomes::default().orchestrator;
4033 let (state, evidence) = match crate::orchestrator::read_lease(&root) {
4034 Some(lease) if lease.is_live() => (
4035 GatewayState::Up,
4036 format!(
4037 "`{}` names pid {} (started {}), which is live",
4038 crate::orchestrator::lock_path(&root).display(),
4039 lease.pid,
4040 lease.started_at
4041 ),
4042 ),
4043 Some(lease) => (
4044 GatewayState::Down,
4045 format!(
4046 "stale lease `{}`: pid {} is gone",
4047 crate::orchestrator::lock_path(&root).display(),
4048 lease.pid
4049 ),
4050 ),
4051 None => (
4052 GatewayState::Down,
4053 format!(
4054 "no lease at `{}`; `supercode orchestrator start` writes one",
4055 crate::orchestrator::lock_path(&root).display()
4056 ),
4057 ),
4058 };
4059 GatewayHealth {
4060 state,
4061 endpoint: None,
4062 version: version.map(str::to_string),
4063 evidence,
4064 checked_at_ms,
4065 }
4066 }
4067 _ => GatewayHealth {
4068 state: GatewayState::Unknown,
4069 endpoint: None,
4070 version: version.map(str::to_string),
4071 evidence: format!("`{id}` runs per session, not as a gateway"),
4072 checked_at_ms,
4073 },
4074 }
4075}
4076
4077#[derive(Debug, Clone, Serialize)]
4078struct RunningInstance {
4079 method: RunningInstanceMethod,
4081 evidence: String,
4083 checked_at_ms: u64,
4085}
4086
4087#[derive(Debug, Clone, Copy, Serialize)]
4088#[serde(rename_all = "snake_case")]
4089enum RunningInstanceMethod {
4090 GatewayConnect,
4093 StoreWalActivity,
4096}
4097
4098fn now_epoch_ms() -> u64 {
4099 std::time::SystemTime::now()
4100 .duration_since(std::time::UNIX_EPOCH)
4101 .map(|elapsed| elapsed.as_millis() as u64)
4102 .unwrap_or(0)
4103}
4104
4105fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4109 let config_path = home.join(".openclaw/openclaw.json");
4110 let text = std::fs::read_to_string(&config_path).ok();
4111 let gateway = text
4112 .as_deref()
4113 .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4114 .and_then(|config| config.get("gateway").cloned());
4115 let address = gateway
4116 .as_ref()
4117 .and_then(|gateway| gateway.get("url"))
4118 .and_then(serde_json::Value::as_str)
4119 .and_then(|url| {
4120 url.split("://").nth(1).map(|rest| {
4121 rest.trim_end_matches('/')
4122 .split('/')
4123 .next()
4124 .unwrap_or(rest)
4125 .to_string()
4126 })
4127 })
4128 .unwrap_or_else(|| {
4129 let port = gateway
4130 .as_ref()
4131 .and_then(|gateway| gateway.get("port"))
4132 .and_then(serde_json::Value::as_u64)
4133 .unwrap_or(18789);
4134 format!("127.0.0.1:{port}")
4135 });
4136 let reachable = std::net::TcpStream::connect_timeout(
4137 &address.parse().ok()?,
4138 std::time::Duration::from_millis(400),
4139 )
4140 .is_ok();
4141 reachable.then(|| RunningInstance {
4142 method: RunningInstanceMethod::GatewayConnect,
4143 evidence: format!(
4144 "gateway endpoint {address} accepted a TCP connect (from {})",
4145 config_path.display()
4146 ),
4147 checked_at_ms: now_epoch_ms(),
4148 })
4149}
4150
4151fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4156 let wal = home.join(".hermes/state.db-wal");
4157 let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4158 let age_ms = std::time::SystemTime::now()
4159 .duration_since(modified)
4160 .map(|age| age.as_millis() as u64)
4161 .unwrap_or(u64::MAX);
4162 (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4163 method: RunningInstanceMethod::StoreWalActivity,
4164 evidence: format!(
4165 "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4166 wal.display()
4167 ),
4168 checked_at_ms: now_epoch_ms(),
4169 })
4170}
4171
4172fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4174 let home = std::env::var_os("HOME").map(PathBuf::from)?;
4175 match id {
4176 HarnessId::OPENCLAW => probe_openclaw_running(&home),
4177 HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4178 _ => None,
4179 }
4180}
4181
4182#[derive(Serialize)]
4183struct LocalHarness {
4184 id: HarnessId,
4185 display_name: String,
4186 supported: bool,
4187 installed: bool,
4188 executable: Option<String>,
4189 version: Option<String>,
4190 auth: HarnessAuthState,
4191 runtime: HarnessRuntimeState,
4192 protocol: String,
4193 capabilities: crate::RuntimeCapabilities,
4194 effective_capabilities: crate::RuntimeCapabilities,
4195 sessions: HarnessSessionCounts,
4196 #[serde(skip_serializing_if = "Option::is_none")]
4199 running: Option<RunningInstance>,
4200 gateway: GatewayHealth,
4202 reason: Option<String>,
4203 repair: Option<String>,
4204}
4205
4206#[derive(Clone, Deserialize)]
4207struct RuntimeBackendParams {
4208 harness: HarnessId,
4209 #[serde(default)]
4210 protocol: Option<String>,
4211 #[serde(default)]
4212 launch: Option<RuntimeLaunch>,
4213 #[serde(default)]
4214 base_url: Option<String>,
4215 #[serde(default)]
4216 policy: RuntimePolicy,
4217}
4218
4219#[derive(Debug, Clone, Copy, Default, Deserialize)]
4220#[serde(rename_all = "snake_case")]
4221enum RuntimePolicy {
4222 #[default]
4223 Default,
4224 Yolo,
4225}
4226
4227#[derive(Deserialize)]
4228struct RuntimeStartParams {
4229 #[serde(flatten)]
4230 backend: RuntimeBackendParams,
4231 cwd: PathBuf,
4232 #[serde(default)]
4235 mcp_servers: Vec<crate::McpServerLaunch>,
4236 #[serde(default)]
4238 approval_policy: Option<String>,
4239}
4240
4241#[derive(Deserialize)]
4242struct RuntimeAttachParams {
4243 #[serde(flatten)]
4244 backend: RuntimeBackendParams,
4245 runtime_id: String,
4246 #[serde(default)]
4247 cwd: Option<PathBuf>,
4248 #[serde(default)]
4251 mcp_servers: Vec<crate::McpServerLaunch>,
4252 #[serde(default)]
4254 approval_policy: Option<String>,
4255}
4256
4257#[derive(Deserialize)]
4258struct RuntimeConnectionParams {
4259 connection: String,
4260}
4261
4262#[derive(Deserialize)]
4263struct RuntimeInputParams {
4264 connection: String,
4265 text: String,
4266 #[serde(default)]
4267 image_urls: Vec<String>,
4268}
4269
4270const MAX_RUNTIME_IMAGES: usize = 4;
4271const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4272const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4273
4274fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4275 if image_urls.len() > MAX_RUNTIME_IMAGES {
4276 return Err(ServiceError::InvalidParams(format!(
4277 "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4278 )));
4279 }
4280 let mut total = 0usize;
4281 for url in &image_urls {
4282 if !(url.starts_with("data:image/")
4283 || url.starts_with("https://")
4284 || url.starts_with("http://"))
4285 {
4286 return Err(ServiceError::InvalidParams(
4287 "runtime images must be image data URLs or HTTP(S) URLs".into(),
4288 ));
4289 }
4290 if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4291 return Err(ServiceError::InvalidParams(format!(
4292 "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4293 )));
4294 }
4295 total = total.saturating_add(url.len());
4296 }
4297 if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4298 return Err(ServiceError::InvalidParams(format!(
4299 "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4300 )));
4301 }
4302 Ok(image_urls)
4303}
4304
4305#[derive(Deserialize)]
4306struct RuntimeRespondParams {
4307 connection: String,
4308 request_id: Value,
4309 response: Value,
4310}
4311
4312fn default_reduction_store_root() -> PathBuf {
4313 if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4314 return PathBuf::from(root).join("sessions");
4315 }
4316 if let Some(home) = std::env::var_os("HOME") {
4317 return PathBuf::from(home).join(".supercode").join("sessions");
4318 }
4319 PathBuf::from(".supercode").join("sessions")
4320}
4321
4322fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4323 let mut output = String::new();
4324 for message in messages {
4325 output.push_str(
4326 &serde_json::to_string(message)
4327 .map_err(|error| ServiceError::Operation(error.to_string()))?,
4328 );
4329 output.push('\n');
4330 }
4331 Ok(output)
4332}
4333
4334fn parse_messages_jsonl(
4335 content: &str,
4336) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4337 content
4338 .lines()
4339 .enumerate()
4340 .filter(|(_, line)| !line.trim().is_empty())
4341 .map(|(index, line)| {
4342 serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4343 ServiceError::Operation(format!(
4344 "reduced transcript line {} is invalid: {error}",
4345 index + 1
4346 ))
4347 })
4348 })
4349 .collect()
4350}
4351
4352fn reduced_bootstrap_prompt(
4353 source: &SessionLocator,
4354 target: TransferFormat,
4355 view_jsonl: &str,
4356 sidecar_path: &Path,
4357 reduction_log_path: &Path,
4358) -> String {
4359 format!(
4360 "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4361 \n\
4362 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\
4363 \n\
4364 <supercode-reduced-session source-session=\"{source_id}\">\n\
4365 {view_jsonl}\
4366 </supercode-reduced-session>\n\
4367 \n\
4368 Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4369 source_harness = source.harness.as_str(),
4370 target_harness = target.id(),
4371 sidecar = sidecar_path.display(),
4372 log = reduction_log_path.display(),
4373 source_id = source.session_id,
4374 )
4375}
4376
4377fn session_artifact(
4378 locator: &SessionLocator,
4379 session: &Session,
4380 target: TransferFormat,
4381) -> std::result::Result<SessionArtifact, ServiceError> {
4382 session_artifact_with_id(locator, session, target, None)
4383}
4384
4385fn session_artifact_with_id(
4386 locator: &SessionLocator,
4387 session: &Session,
4388 target: TransferFormat,
4389 target_session_id: Option<&str>,
4390) -> std::result::Result<SessionArtifact, ServiceError> {
4391 let format: SessionFormat = target.into();
4392 let diagonal = format.source() == session.meta.source;
4393 crate::residue_store::store_segments(session);
4394 let has_appended_turns = session
4395 .imported_message_count
4396 .is_some_and(|imported| imported < session.messages.len());
4397 let mut restoration = None;
4398 let content = if let Some(id) = target_session_id {
4399 if diagonal && format != SessionFormat::OpenCode {
4400 session
4401 .to_jsonl_spliced(format, Some(id))
4402 .map_err(operation)?
4403 } else {
4404 let mut rewritten = session.clone();
4405 rewritten.meta.session_id = Some(id.to_string());
4406 rewritten.to_jsonl(format).map_err(operation)?
4407 }
4408 } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4409 session.raw_verbatim()
4410 } else if diagonal {
4411 session.to_jsonl_spliced(format, None).map_err(operation)?
4412 } else {
4413 match session
4416 .restore_residue(format, crate::residue_store::lookup)
4417 .map_err(operation)?
4418 {
4419 Some((content, report)) => {
4420 restoration = Some(report);
4421 content
4422 }
4423 None => session.to_jsonl(format).map_err(operation)?,
4424 }
4425 };
4426 let stem = sanitize_filename(
4427 target_session_id
4428 .or(session.meta.session_id.as_deref())
4429 .unwrap_or(&locator.session_id),
4430 );
4431 let suggested_filename = if diagonal && target == TransferFormat::Grok {
4432 "chat_history.jsonl".to_string()
4433 } else if target == TransferFormat::Goose {
4434 format!("{stem}.goose.json")
4435 } else {
4436 format!("{stem}.{}.jsonl", target.id())
4437 };
4438 let mut files = vec![SessionArtifactFile {
4439 path: suggested_filename.clone(),
4440 content: content.clone(),
4441 role: ArtifactFileRole::Primary,
4442 }];
4443 if target == TransferFormat::ClaudeCode {
4444 let bundle_stem = Path::new(&suggested_filename)
4445 .file_stem()
4446 .and_then(|stem| stem.to_str())
4447 .unwrap_or(&stem);
4448 let mut child_paths = BTreeSet::new();
4449 for (index, subagent) in session.subagents.iter().enumerate() {
4450 let agent_id = subagent
4451 .meta
4452 .agent_id
4453 .as_deref()
4454 .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4455 .map(sanitize_filename)
4456 .filter(|id| !id.is_empty())
4457 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4458 let child_has_appended_turns = subagent
4459 .imported_message_count
4460 .is_some_and(|imported| imported < subagent.messages.len());
4461 let child_content = if target_session_id.is_none()
4462 && subagent.meta.source == SessionSource::ClaudeCode
4463 && subagent.raw_is_verbatim
4464 && !child_has_appended_turns
4465 {
4466 subagent.raw_verbatim()
4467 } else if subagent.meta.source == SessionSource::ClaudeCode {
4468 subagent
4469 .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4470 .map_err(operation)?
4471 } else {
4472 let mut child = subagent.clone();
4473 if let Some(id) = target_session_id {
4474 child.meta.session_id = Some(id.to_string());
4475 }
4476 child
4477 .to_jsonl(SessionFormat::ClaudeCode)
4478 .map_err(operation)?
4479 };
4480 let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4481 if !child_paths.insert(path.clone()) {
4482 return Err(ServiceError::Operation(format!(
4483 "Claude subagent ids collide at artifact path `{path}`"
4484 )));
4485 }
4486 files.push(SessionArtifactFile {
4487 path,
4488 content: child_content,
4489 role: ArtifactFileRole::Subagent,
4490 });
4491 }
4492 }
4493 if diagonal && target == TransferFormat::Grok {
4494 append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4495 }
4496 if !diagonal || !session.raw_is_verbatim {
4497 files.push(SessionArtifactFile {
4498 path: "recovery/source.supercode.jsonl".into(),
4499 content: session.to_native_jsonl(),
4500 role: ArtifactFileRole::SourceRecovery,
4501 });
4502 for (index, subagent) in session.subagents.iter().enumerate() {
4503 let id = subagent
4504 .meta
4505 .agent_id
4506 .as_deref()
4507 .map(sanitize_filename)
4508 .unwrap_or_else(|| format!("subagent-{}", index + 1));
4509 files.push(SessionArtifactFile {
4510 path: format!("recovery/subagents/{id}.supercode.jsonl"),
4511 content: subagent.to_native_jsonl(),
4512 role: ArtifactFileRole::SourceRecovery,
4513 });
4514 }
4515 }
4516 if !diagonal && session.meta.source == SessionSource::Grok {
4517 append_grok_bundle_files(
4518 locator,
4519 "recovery/grok/",
4520 ArtifactFileRole::SourceRecovery,
4521 &mut files,
4522 )?;
4523 }
4524 let (fidelity, residue) = if diagonal
4525 && target_session_id.is_none()
4526 && session.raw_is_verbatim
4527 && !has_appended_turns
4528 {
4529 (Fidelity::ByteLossless, Vec::new())
4530 } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4531 (
4532 Fidelity::ValueLossless,
4533 vec![if target_session_id.is_some() {
4534 "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4535 } else {
4536 "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4537 }],
4538 )
4539 } else {
4540 match restoration {
4541 Some(report) if report.rendered_messages == 0 => (
4542 Fidelity::ByteLossless,
4543 vec![format!(
4544 "restored verbatim from this conversation's {} source records in the residue store",
4545 target.id()
4546 )],
4547 ),
4548 Some(report) => (
4549 Fidelity::Semantic,
4550 vec![format!(
4551 "{} of {} messages restored verbatim from the residue store; the other {} written by the {} writer",
4552 report.restored_messages,
4553 report.restored_messages + report.rendered_messages,
4554 report.rendered_messages,
4555 target.id()
4556 )],
4557 ),
4558 None => (
4559 Fidelity::Semantic,
4560 vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4561 ),
4562 }
4563 };
4564 Ok(SessionArtifact {
4565 source_harness: locator.harness.clone(),
4566 target_harness: target.id(),
4567 session_id: target_session_id
4568 .map(str::to_string)
4569 .or_else(|| session.meta.session_id.clone()),
4570 content,
4571 suggested_filename,
4572 files,
4573 fidelity,
4574 residue,
4575 })
4576}
4577
4578fn append_grok_bundle_files(
4579 locator: &SessionLocator,
4580 prefix: &str,
4581 role: ArtifactFileRole,
4582 files: &mut Vec<SessionArtifactFile>,
4583) -> std::result::Result<(), ServiceError> {
4584 let primary = locator.storage.path();
4585 if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4586 return Err(ServiceError::Operation(format!(
4587 "Grok bundle locator must name chat_history.jsonl, got {}",
4588 primary.display()
4589 )));
4590 }
4591 let parent = primary.parent().ok_or_else(|| {
4592 ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4593 })?;
4594 for name in ["summary.json", "updates.jsonl"] {
4595 let path = parent.join(name);
4596 let metadata = match std::fs::symlink_metadata(&path) {
4597 Ok(metadata) => metadata,
4598 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4599 Err(error) => return Err(ServiceError::Operation(error.to_string())),
4600 };
4601 if metadata.file_type().is_symlink() || !metadata.is_file() {
4602 return Err(ServiceError::Operation(format!(
4603 "refusing non-regular Grok bundle member {}",
4604 path.display()
4605 )));
4606 }
4607 let content = std::fs::read_to_string(&path).map_err(|error| {
4608 ServiceError::Operation(format!(
4609 "Grok bundle member {} is not representable as UTF-8: {error}",
4610 path.display()
4611 ))
4612 })?;
4613 files.push(SessionArtifactFile {
4614 path: format!("{prefix}{name}"),
4615 content,
4616 role: match role {
4617 ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4618 _ => ArtifactFileRole::SourceRecovery,
4619 },
4620 });
4621 }
4622 Ok(())
4623}
4624
4625fn handoff_artifact(
4626 locator: &SessionLocator,
4627 session: &Session,
4628 target: TransferFormat,
4629) -> std::result::Result<SessionArtifact, ServiceError> {
4630 let target_session_id = target_session_id(target);
4631 session_artifact_with_id(locator, session, target, Some(&target_session_id))
4632}
4633
4634fn target_session_id(target: TransferFormat) -> String {
4635 let uuid = generated_session_id();
4636 match target {
4637 TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4638 TransferFormat::ClaudeCode
4639 | TransferFormat::Codex
4640 | TransferFormat::Pi
4641 | TransferFormat::Grok
4642 | TransferFormat::Gemini
4643 | TransferFormat::Goose
4644 | TransferFormat::Hermes => uuid,
4645 }
4646}
4647
4648fn sanitize_filename(value: &str) -> String {
4649 let value = value
4650 .chars()
4651 .map(|character| {
4652 if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4653 character
4654 } else {
4655 '-'
4656 }
4657 })
4658 .collect::<String>();
4659 let value = value.trim_matches('-');
4660 if value.is_empty() {
4661 "session".into()
4662 } else {
4663 value.chars().take(100).collect()
4664 }
4665}
4666
4667fn handoff_instructions(
4668 target: TransferFormat,
4669 session_id: &str,
4670 cwd: &Path,
4671) -> HandoffInstructions {
4672 let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4673 cwd: cwd.to_path_buf(),
4674 program: program.into(),
4675 arguments,
4676 env: BTreeMap::new(),
4677 };
4678 match target {
4679 TransferFormat::ClaudeCode => HandoffInstructions {
4680 launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4681 materialize: None,
4682 requires_materialization: true,
4683 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(),
4684 },
4685 TransferFormat::Hermes => HandoffInstructions {
4686 launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4687 materialize: None,
4688 requires_materialization: true,
4689 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(),
4690 },
4691 TransferFormat::Codex => HandoffInstructions {
4692 launch: launch("codex", vec!["resume".into(), session_id.into()]),
4693 materialize: None,
4694 requires_materialization: true,
4695 note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4696 },
4697 TransferFormat::OpenCode => HandoffInstructions {
4698 launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4699 materialize: Some(launch(
4700 "opencode",
4701 vec!["import".into(), "{artifact_path}".into()],
4702 )),
4703 requires_materialization: true,
4704 note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4705 },
4706 TransferFormat::Pi => HandoffInstructions {
4707 launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4708 materialize: None,
4709 requires_materialization: true,
4710 note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4711 },
4712 TransferFormat::Grok => HandoffInstructions {
4713 launch: launch(
4714 "grok",
4715 vec!["--resume".into(), "{materialized_session_id}".into()],
4716 ),
4717 materialize: None,
4718 requires_materialization: true,
4719 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(),
4720 },
4721 TransferFormat::Gemini => HandoffInstructions {
4722 launch: launch(
4723 "gemini",
4724 vec!["--session-file".into(), "{artifact_path}".into()],
4725 ),
4726 materialize: None,
4727 requires_materialization: true,
4728 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(),
4729 },
4730 TransferFormat::Goose => HandoffInstructions {
4731 launch: launch(
4732 "goose",
4733 vec![
4734 "session".into(),
4735 "--resume".into(),
4736 "--session-id".into(),
4737 "{imported_session_id}".into(),
4738 ],
4739 ),
4740 materialize: Some(launch(
4741 "goose",
4742 vec!["session".into(), "import".into(), "{artifact_path}".into()],
4743 )),
4744 requires_materialization: true,
4745 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(),
4746 },
4747 }
4748}
4749
4750fn resume_launch(
4751 harness: &str,
4752 session_id: &str,
4753 cwd: &Path,
4754 policy: ResumePolicy,
4755) -> std::result::Result<StructuredLaunch, ServiceError> {
4756 let mut arguments = Vec::new();
4757 let program = match harness {
4758 HarnessId::GROK => {
4759 if matches!(policy, ResumePolicy::Yolo) {
4760 if crate::support::self_sandbox_supported() {
4761 arguments.extend(["--sandbox".into(), "workspace".into()]);
4762 }
4763 arguments.push("--always-approve".into());
4764 }
4765 arguments.extend(["--resume".into(), session_id.into()]);
4766 "grok"
4767 }
4768 HarnessId::CODEX => {
4769 let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4770 .expect("a filesystem path always serializes as JSON text");
4771 arguments.extend([
4772 "-c".into(),
4773 "check_for_update_on_startup=false".into(),
4774 "-c".into(),
4775 format!("projects.{cwd_key}.trust_level=\"trusted\""),
4776 ]);
4777 if matches!(policy, ResumePolicy::Yolo) {
4778 arguments.extend([
4779 "--dangerously-bypass-approvals-and-sandbox".into(),
4780 "--dangerously-bypass-hook-trust".into(),
4781 ]);
4782 }
4783 arguments.extend(["resume".into(), session_id.into()]);
4784 "codex"
4785 }
4786 HarnessId::CLAUDE_CODE => {
4787 if matches!(policy, ResumePolicy::Yolo) {
4788 arguments.push("--dangerously-skip-permissions".into());
4789 }
4790 arguments.extend(["--resume".into(), session_id.into()]);
4791 "claude"
4792 }
4793 HarnessId::GEMINI => {
4794 if matches!(policy, ResumePolicy::Yolo) {
4795 arguments.push("--yolo".into());
4796 }
4797 arguments.extend(["--resume".into(), session_id.into()]);
4798 "gemini"
4799 }
4800 HarnessId::GOOSE => {
4801 arguments.extend([
4802 "session".into(),
4803 "--resume".into(),
4804 "--session-id".into(),
4805 session_id.into(),
4806 ]);
4807 "goose"
4808 }
4809 HarnessId::PI => {
4810 if matches!(policy, ResumePolicy::Yolo) {
4811 arguments.push("--approve".into());
4812 }
4813 arguments.extend(["--session".into(), session_id.into()]);
4814 "pi"
4815 }
4816 HarnessId::OPENCODE => {
4817 arguments.extend(["--session".into(), session_id.into()]);
4818 "opencode"
4819 }
4820 HarnessId::SUPERCODE => {
4821 if matches!(policy, ResumePolicy::Yolo) {
4822 arguments.push("--dangerous".into());
4823 }
4824 arguments.extend(["resume".into(), session_id.into()]);
4825 "supercode"
4826 }
4827 other => {
4828 return Err(ServiceError::InvalidParams(format!(
4829 "no structured resume launch is registered for harness `{other}`"
4830 )))
4831 }
4832 };
4833 Ok(StructuredLaunch {
4834 cwd: cwd.to_path_buf(),
4835 env: if program == "grok" {
4836 crate::support::grok_home_env()
4837 } else {
4838 BTreeMap::new()
4839 },
4840 program: program.into(),
4841 arguments,
4842 })
4843}
4844
4845fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4851 let digest = blake3::hash(address.as_bytes()).to_hex();
4852 let path = std::env::temp_dir().join(format!(
4853 "supercode-openclaw-gateway-token-{}",
4854 &digest.as_str()[..16]
4855 ));
4856 #[cfg(unix)]
4857 {
4858 use std::io::Write;
4859 use std::os::unix::fs::OpenOptionsExt;
4860 let mut file = std::fs::OpenOptions::new()
4861 .write(true)
4862 .create(true)
4863 .truncate(true)
4864 .mode(0o600)
4865 .open(&path)?;
4866 file.write_all(secret.as_bytes())?;
4867 }
4868 #[cfg(not(unix))]
4869 std::fs::write(&path, secret)?;
4870 Ok(path)
4871}
4872
4873fn open_connect_descriptor(
4879 descriptor: &crate::HarnessSupportDescriptor,
4880 home: &Path,
4881) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4882 let Some(connect) = &descriptor.runtime.connect_launch else {
4883 return Err(ServiceError::InvalidParams(format!(
4884 "harness `{}` has no registered connect-mode launch",
4885 descriptor.id.as_str()
4886 )));
4887 };
4888 let resolved = connect
4889 .resolve(home)
4890 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4891 match (descriptor.id.as_str(), connect.protocol.as_str()) {
4892 (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4893 let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4894 if let Some(token) = resolved.auth {
4895 backend = backend.with_bearer(token);
4896 }
4897 Ok(Box::new(backend))
4898 }
4899 (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4900 let mut env = BTreeMap::new();
4911 let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4912 if let Some(token) = resolved.auth {
4913 let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4914 .map_err(|error| {
4915 ServiceError::UnsupportedAction(format!(
4916 "could not stage the gateway credential for the bridge: {error}"
4917 ))
4918 })?;
4919 arguments.push("--token-file".into());
4920 arguments.push(token_path.to_string_lossy().into_owned());
4921 env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4922 }
4923 let program = descriptor
4928 .runtime
4929 .default_launch
4930 .as_ref()
4931 .map(|launch| launch.program.clone())
4932 .unwrap_or_else(|| "openclaw".into());
4933 let launch = RuntimeLaunch {
4934 program,
4935 arguments,
4936 env,
4937 };
4938 Ok(Box::new(
4939 crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4940 .with_resume_support(descriptor.runtime.capabilities.resume_session),
4941 ))
4942 }
4943 _ => Err(ServiceError::UnsupportedAction(format!(
4944 "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4945 descriptor.id.as_str(),
4946 connect.protocol
4947 ))),
4948 }
4949}
4950
4951fn registry_connect_descriptor(
4954 params: &RuntimeBackendParams,
4955) -> Option<crate::HarnessSupportDescriptor> {
4956 if params.launch.is_some() || params.base_url.is_some() {
4957 return None;
4958 }
4959 harness_support_registry()
4960 .harnesses
4961 .into_iter()
4962 .find(|descriptor| descriptor.id == params.harness)
4963 .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4964}
4965
4966fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4967 std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4968 ServiceError::UnsupportedAction(
4969 "connect-mode launches need HOME to locate the harness config".into(),
4970 )
4971 })
4972}
4973
4974pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4977 "harness.v1.runtimes.start",
4978 "harness.v1.runtimes.resume",
4979 "harness.v1.runtimes.attach",
4980 "harness.v1.runtimes.attach_existing",
4981];
4982
4983pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
4988
4989pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
4995
4996pub const DETACHED_METHODS: &[&str] = &[
5002 "harness.v1.harnesses.list",
5003 "harness.v1.harnesses.probe",
5004 "harness.v1.sessions.message",
5005 "harness.v1.sessions.new",
5006 "harness.v1.sessions.reset",
5007 "harness.v1.sessions.archive",
5008 "harness.v1.sessions.delete",
5009];
5010
5011pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
5017
5018pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
5026
5027async fn within_control_deadline<F: std::future::Future>(
5030 method: &str,
5031 call: F,
5032) -> std::result::Result<F::Output, ServiceError> {
5033 tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
5034 .await
5035 .map_err(|_| {
5036 ServiceError::Operation(format!(
5037 "`{method}` gave up after {}s: the runtime did not answer",
5038 RUNTIME_CONTROL_DEADLINE.as_secs()
5039 ))
5040 })
5041}
5042
5043pub struct RuntimeOpen {
5047 id: Value,
5048 method: String,
5049 params: Value,
5050}
5051
5052impl RuntimeOpen {
5053 pub async fn open(self) -> OpenedRuntime {
5057 let Self { id, method, params } = self;
5058 let outcome = open_runtime(&method, params).await;
5059 OpenedRuntime { id, outcome }
5060 }
5061}
5062
5063pub struct OpenedRuntime {
5066 id: Value,
5067 outcome: std::result::Result<OpenRuntime, ServiceError>,
5068}
5069
5070pub struct DetachedCall {
5075 id: Value,
5076 method: String,
5077 work: std::result::Result<Work, ServiceError>,
5078}
5079
5080impl DetachedCall {
5081 pub async fn run(self) -> DetachedAnswer {
5084 let Self { id, method, work } = self;
5085 match work {
5086 Ok(Work::Runtime(work)) => {
5091 let (result, returned) = work.run().await;
5092 DetachedAnswer {
5093 response: service_response(id, result),
5094 returned,
5095 }
5096 }
5097 Ok(Work::Free(work)) => {
5098 let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5099 Ok(result) => result,
5100 Err(_) => Err(ServiceError::Operation(format!(
5101 "`{method}` gave up after {}s: the harness it waits on did not answer",
5102 DETACHED_CALL_DEADLINE.as_secs()
5103 ))),
5104 };
5105 DetachedAnswer {
5106 response: service_response(id, result),
5107 returned: None,
5108 }
5109 }
5110 Err(error) => DetachedAnswer {
5111 response: service_response(id, Err(error)),
5112 returned: None,
5113 },
5114 }
5115 }
5116}
5117
5118pub struct DetachedAnswer {
5122 response: Value,
5123 returned: Option<ReturnedRuntime>,
5124}
5125
5126impl DetachedAnswer {
5127 pub fn into_response(self) -> Value {
5130 self.response
5131 }
5132}
5133
5134pub struct ReturnedRuntime {
5137 connection: String,
5138 runtime: Box<dyn RuntimeConnection>,
5139}
5140
5141enum Work {
5144 Free(DetachedWork),
5145 Runtime(RuntimeWork),
5146}
5147
5148enum DetachedWork {
5151 Inventory(InventoryWork),
5155 Message(MessageSessionParams),
5157 SessionMutation {
5160 verb: crate::SessionVerb,
5161 mutation: crate::SessionMutation,
5162 },
5163}
5164
5165impl DetachedWork {
5166 async fn run(self) -> std::result::Result<Value, ServiceError> {
5167 match self {
5168 Self::Inventory(work) => run_inventory(work).await,
5169 Self::Message(params) => {
5170 Ok(message_live_session(¶ms, &crate::claude_peer::ProcessCourierRunner).await)
5171 }
5172 Self::SessionMutation { verb, mutation } => {
5173 let outcome = run_session_mutation(verb, &mutation).await?;
5174 serde_json::to_value(outcome)
5175 .map_err(|error| ServiceError::Operation(error.to_string()))
5176 }
5177 }
5178 }
5179}
5180
5181enum RuntimeWork {
5183 Close {
5185 runtime: Box<dyn RuntimeConnection>,
5186 process_group: Option<u32>,
5187 },
5188 LiveCommand {
5191 connection: String,
5192 runtime: Box<dyn RuntimeConnection>,
5193 verb: crate::SessionVerb,
5194 mutation: crate::SessionMutation,
5195 command: &'static str,
5196 session: String,
5197 },
5198}
5199
5200type RuntimeWorkAnswer = (
5203 std::result::Result<Value, ServiceError>,
5204 Option<ReturnedRuntime>,
5205);
5206
5207impl RuntimeWork {
5208 async fn run(self) -> RuntimeWorkAnswer {
5209 match self {
5210 Self::Close {
5211 runtime,
5212 process_group,
5213 } => (close_runtime(runtime, process_group).await, None),
5214 Self::LiveCommand {
5215 connection,
5216 mut runtime,
5217 verb,
5218 mutation,
5219 command,
5220 session,
5221 } => {
5222 let result =
5223 type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5224 (
5225 result,
5226 Some(ReturnedRuntime {
5227 connection,
5228 runtime,
5229 }),
5230 )
5231 }
5232 }
5233 }
5234}
5235
5236async fn close_runtime(
5239 mut runtime: Box<dyn RuntimeConnection>,
5240 process_group: Option<u32>,
5241) -> std::result::Result<Value, ServiceError> {
5242 match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5243 Ok(result) => {
5244 result.map_err(operation)?;
5245 Ok(json!({"closed": true}))
5246 }
5247 Err(deadline) => {
5248 let killed = kill_runtime_process_group(process_group);
5253 drop(runtime);
5254 Ok(json!({
5255 "closed": true,
5256 "killed": killed,
5257 "detail": error_message(deadline),
5258 }))
5259 }
5260 }
5261}
5262
5263fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5266 mutation
5267 .session
5268 .clone()
5269 .filter(|value| !value.trim().is_empty())
5270 .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5271}
5272
5273async fn type_live_command(
5277 runtime: &mut dyn RuntimeConnection,
5278 verb: crate::SessionVerb,
5279 mutation: &crate::SessionMutation,
5280 command: &str,
5281 session: String,
5282) -> std::result::Result<Value, ServiceError> {
5283 within_control_deadline(
5284 &format!("sessions.{}", verb.as_str()),
5285 runtime.send_input(RuntimeInput {
5286 text: command.to_string(),
5287 image_urls: Vec::new(),
5288 }),
5289 )
5290 .await?
5291 .map_err(operation)?;
5292 let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5293 .map_err(session_control_error)?;
5294 serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5295}
5296
5297enum OpenRuntime {
5300 Hosted {
5303 runtime: Box<dyn RuntimeConnection>,
5304 capabilities: crate::RuntimeCapabilities,
5305 workspace: PathBuf,
5306 },
5307 Joined { runtime: Box<dyn RuntimeConnection> },
5310}
5311
5312async fn open_runtime(
5317 method: &str,
5318 params: Value,
5319) -> std::result::Result<OpenRuntime, ServiceError> {
5320 match tokio::time::timeout(
5321 RUNTIME_OPEN_DEADLINE,
5322 open_runtime_unbounded(method, params),
5323 )
5324 .await
5325 {
5326 Ok(result) => result,
5327 Err(_) => Err(ServiceError::Operation(format!(
5328 "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5329 RUNTIME_OPEN_DEADLINE.as_secs()
5330 ))),
5331 }
5332}
5333
5334async fn open_runtime_unbounded(
5335 method: &str,
5336 params: Value,
5337) -> std::result::Result<OpenRuntime, ServiceError> {
5338 match method {
5339 "harness.v1.runtimes.start" => {
5340 let params = decode::<RuntimeStartParams>(params)?;
5341 let backend = runtime_backend(¶ms.backend)?;
5342 let capabilities = backend.capabilities();
5343 let workspace = params.cwd.clone();
5344 let runtime = backend
5345 .start(RuntimeStartRequest {
5346 cwd: params.cwd,
5347 launch: runtime_launch(¶ms.backend),
5348 mcp_servers: params.mcp_servers,
5349 approval_policy: params.approval_policy,
5350 })
5351 .await
5352 .map_err(operation)?;
5353 Ok(OpenRuntime::Hosted {
5354 runtime,
5355 capabilities,
5356 workspace,
5357 })
5358 }
5359 "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5360 let params = decode::<RuntimeAttachParams>(params)?;
5361 let backend = runtime_backend(¶ms.backend)?;
5362 let capabilities = backend.capabilities();
5363 let workspace = params
5364 .cwd
5365 .clone()
5366 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5367 let runtime = backend
5368 .attach(RuntimeAttachRequest {
5369 runtime_id: params.runtime_id,
5370 cwd: params.cwd,
5371 launch: runtime_launch(¶ms.backend),
5372 mcp_servers: params.mcp_servers,
5373 approval_policy: params.approval_policy,
5374 })
5375 .await
5376 .map_err(operation)?;
5377 Ok(OpenRuntime::Hosted {
5378 runtime,
5379 capabilities,
5380 workspace,
5381 })
5382 }
5383 "harness.v1.runtimes.attach_existing" => {
5384 let params = decode::<RuntimeAttachParams>(params)?;
5385 let backend: Box<dyn RuntimeBackend> = match params
5386 .backend
5387 .base_url
5388 .as_deref()
5389 .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5390 {
5391 Some(endpoint) => {
5392 #[cfg(not(feature = "adapter-api"))]
5393 {
5394 let _ = endpoint;
5395 return Err(ServiceError::UnsupportedAction(
5396 "live HTTP attachment adapter is not compiled".into(),
5397 ));
5398 }
5399 #[cfg(feature = "adapter-api")]
5400 {
5401 let workspace = params.cwd.clone().ok_or_else(|| {
5402 ServiceError::InvalidParams(
5403 "Supercode live attach requires the project cwd".into(),
5404 )
5405 })?;
5406 let source = LiveRuntimeSource {
5407 harness: params.backend.harness.as_str().to_string(),
5408 session_id: params.runtime_id.clone(),
5409 workspace,
5410 };
5411 let receipt = resolve_live_runtime(&endpoint, &source)
5412 .map_err(|error| ServiceError::Operation(error.to_string()))?;
5413 Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5414 }
5415 }
5416 None => runtime_backend(¶ms.backend)?,
5417 };
5418 let capabilities = backend.capabilities();
5419 if !capabilities.attach_existing_process {
5420 return Err(ServiceError::Operation(format!(
5421 "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5422 backend.harness().as_str()
5423 )));
5424 }
5425 let runtime = backend
5426 .attach_existing(RuntimeAttachRequest {
5427 runtime_id: params.runtime_id,
5428 cwd: params.cwd,
5429 launch: runtime_launch(¶ms.backend),
5430 mcp_servers: params.mcp_servers,
5431 approval_policy: params.approval_policy,
5432 })
5433 .await
5434 .map_err(operation)?;
5435 Ok(OpenRuntime::Joined { runtime })
5436 }
5437 _ => Err(ServiceError::MethodNotFound),
5438 }
5439}
5440
5441fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5443 match result {
5444 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5445 Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5446 Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5447 Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5448 Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5449 Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5450 }
5451}
5452
5453fn runtime_backend(
5454 params: &RuntimeBackendParams,
5455) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5456 if let Some(descriptor) = registry_connect_descriptor(params) {
5457 return open_connect_descriptor(&descriptor, &service_home()?);
5458 }
5459 if params.protocol.as_deref() == Some("acp") {
5460 let launch = params
5461 .launch
5462 .clone()
5463 .or_else(|| {
5464 harness_support_registry()
5465 .harnesses
5466 .into_iter()
5467 .find(|harness| harness.id == params.harness)
5468 .filter(|harness| {
5469 harness.runtime.implementation == ImplementationKind::GenericProtocol
5470 && harness.runtime.protocol.starts_with("acp")
5471 })
5472 .and_then(|harness| harness.runtime.default_launch)
5473 })
5474 .ok_or_else(|| {
5475 ServiceError::InvalidParams(
5476 "an ACP runtime requires `launch` unless the harness has a registered default"
5477 .into(),
5478 )
5479 })?;
5480 let resume_session = harness_support_registry()
5481 .harnesses
5482 .into_iter()
5483 .find(|harness| harness.id == params.harness)
5484 .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5485 return Ok(Box::new(
5486 AcpRuntimeBackend::new(params.harness.clone(), launch)
5487 .with_resume_support(resume_session),
5488 ));
5489 }
5490 let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5491 HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5492 HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5493 HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5494 HarnessId::OPENCODE => match ¶ms.base_url {
5495 Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5496 None => Box::new(OpenCodeRuntimeBackend::new()),
5497 },
5498 harness => {
5499 let descriptor = harness_support_registry()
5500 .harnesses
5501 .into_iter()
5502 .find(|descriptor| descriptor.id.as_str() == harness)
5503 .filter(|descriptor| {
5504 descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5505 && descriptor.runtime.protocol.starts_with("acp")
5506 });
5507 let Some(descriptor) = descriptor else {
5508 return Err(ServiceError::InvalidParams(format!(
5509 "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5510 )));
5511 };
5512 let resume = descriptor.runtime.capabilities.resume_session;
5513 Box::new(
5514 AcpRuntimeBackend::new(
5515 descriptor.id,
5516 descriptor
5517 .runtime
5518 .default_launch
5519 .expect("generic ACP registry entry includes its launch"),
5520 )
5521 .with_resume_support(resume),
5522 )
5523 }
5524 };
5525 Ok(backend)
5526}
5527
5528fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5529 if let Some(launch) = ¶ms.launch {
5530 return Some(launch.clone());
5531 }
5532 if !matches!(params.policy, RuntimePolicy::Yolo) {
5533 return None;
5534 }
5535 let launch = match params.harness.as_str() {
5536 HarnessId::GROK => RuntimeLaunch {
5537 program: "grok".into(),
5538 arguments: {
5539 let mut arguments: Vec<String> = Vec::new();
5540 if crate::support::self_sandbox_supported() {
5541 arguments.extend(["--sandbox".into(), "workspace".into()]);
5542 }
5543 arguments.extend([
5544 "--always-approve".into(),
5545 "agent".into(),
5546 "--no-leader".into(),
5547 "stdio".into(),
5548 ]);
5549 arguments
5550 },
5551 env: crate::support::grok_env(),
5552 },
5553 HarnessId::CODEX => RuntimeLaunch {
5554 program: "codex".into(),
5555 arguments: vec![
5556 "--dangerously-bypass-approvals-and-sandbox".into(),
5557 "--dangerously-bypass-hook-trust".into(),
5558 "app-server".into(),
5559 ],
5560 env: BTreeMap::new(),
5561 },
5562 HarnessId::CLAUDE_CODE => RuntimeLaunch {
5563 program: "claude".into(),
5564 arguments: vec![
5565 "--dangerously-skip-permissions".into(),
5566 "--print".into(),
5567 "--input-format".into(),
5568 "stream-json".into(),
5569 "--output-format".into(),
5570 "stream-json".into(),
5571 "--verbose".into(),
5572 ],
5573 env: BTreeMap::new(),
5574 },
5575 HarnessId::PI => RuntimeLaunch {
5576 program: "pi".into(),
5577 arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5578 env: BTreeMap::new(),
5579 },
5580 HarnessId::OPENCODE => RuntimeLaunch {
5581 program: "opencode".into(),
5582 arguments: vec!["serve".into()],
5583 env: BTreeMap::new(),
5584 },
5585 HarnessId::GEMINI => RuntimeLaunch {
5586 program: "gemini".into(),
5587 arguments: vec!["--acp".into(), "--yolo".into()],
5588 env: BTreeMap::new(),
5589 },
5590 HarnessId::GOOSE => RuntimeLaunch {
5591 program: "goose".into(),
5592 arguments: vec!["acp".into()],
5593 env: BTreeMap::new(),
5594 },
5595 HarnessId::SUPERCODE => RuntimeLaunch {
5596 program: "supercode".into(),
5597 arguments: vec!["acp".into(), "--dangerous".into()],
5598 env: BTreeMap::new(),
5599 },
5600 _ => return None,
5601 };
5602 Some(launch)
5603}
5604
5605struct IsolatedProbeHome {
5611 launch: RuntimeLaunch,
5612 root: PathBuf,
5613}
5614
5615impl IsolatedProbeHome {
5616 fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5617 let root = std::env::temp_dir().join(format!(
5618 "supercode-harness-probe-{harness}-{}",
5619 generated_session_id()
5620 ));
5621 std::fs::create_dir_all(&root)?;
5622 set_private_dir_permissions(&root)?;
5623
5624 if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
5625 for relative in probe_auth_files(harness) {
5626 copy_probe_file(&source_home, &root, relative)?;
5627 }
5628 }
5629 if harness == HarnessId::SUPERCODE {
5634 let config_home = crate::agent::global_instructions_dir();
5635 for file in ["config.toml", "credentials.toml"] {
5636 copy_probe_path(
5637 &config_home.join(file),
5638 &root.join(".config/supercode").join(file),
5639 )?;
5640 }
5641 }
5642 configure_isolated_probe_auth(harness, &root)?;
5643
5644 let root_text = root.to_string_lossy().into_owned();
5645 for (key, value) in [
5646 ("HOME", root_text.clone()),
5647 (
5648 "XDG_CACHE_HOME",
5649 root.join(".cache").to_string_lossy().into_owned(),
5650 ),
5651 (
5652 "XDG_CONFIG_HOME",
5653 root.join(".config").to_string_lossy().into_owned(),
5654 ),
5655 (
5656 "XDG_DATA_HOME",
5657 root.join(".local/share").to_string_lossy().into_owned(),
5658 ),
5659 ] {
5660 launch.env.insert(key.into(), value);
5661 }
5662 let scoped = match harness {
5663 HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5664 HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5665 HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5666 HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5667 HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5668 HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5669 _ => None,
5670 };
5671 if let Some((key, value)) = scoped {
5672 launch
5673 .env
5674 .insert(key.into(), value.to_string_lossy().into_owned());
5675 }
5676 Ok(Self { launch, root })
5677 }
5678
5679 fn cleanup(&self) -> std::io::Result<()> {
5680 match std::fs::remove_dir_all(&self.root) {
5681 Ok(()) => Ok(()),
5682 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5683 Err(error) => Err(error),
5684 }
5685 }
5686}
5687
5688impl Drop for IsolatedProbeHome {
5689 fn drop(&mut self) {
5690 let _ = self.cleanup();
5691 }
5692}
5693
5694fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5695 match harness {
5696 HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5697 HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5701 HarnessId::CODEX => &[".codex/auth.json"],
5702 HarnessId::GEMINI => &[
5703 ".gemini/google_accounts.json",
5704 ".gemini/oauth_creds.json",
5705 ".gemini/settings.json",
5706 ],
5707 HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5708 HarnessId::OPENCODE => &[
5709 ".config/opencode/auth.json",
5710 ".local/share/opencode/auth.json",
5711 ],
5712 HarnessId::PI => &[".pi/agent/auth.json"],
5713 HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5718 _ => &[],
5719 }
5720}
5721
5722fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5723 copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5724}
5725
5726fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5727 if !source.is_file() {
5728 return Ok(());
5729 }
5730 if let Some(parent) = destination.parent() {
5731 std::fs::create_dir_all(parent)?;
5732 set_private_dir_permissions(parent)?;
5733 }
5734 std::fs::copy(source, destination)?;
5735 set_private_file_permissions(destination)
5736}
5737
5738fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5739 if harness != HarnessId::GEMINI {
5740 return Ok(());
5741 }
5742 let oauth = probe_home.join(".gemini/oauth_creds.json");
5743 if !oauth.is_file() {
5744 return Ok(());
5745 }
5746 let settings_path = probe_home.join(".gemini/settings.json");
5747 let mut settings = std::fs::read_to_string(&settings_path)
5748 .ok()
5749 .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5750 .unwrap_or_else(|| json!({}));
5751 settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5752 std::fs::write(
5753 &settings_path,
5754 serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5755 )?;
5756 set_private_file_permissions(&settings_path)
5757}
5758
5759#[cfg(unix)]
5760fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5761 use std::os::unix::fs::PermissionsExt;
5762 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5763}
5764
5765#[cfg(not(unix))]
5766fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5767 Ok(())
5768}
5769
5770#[cfg(unix)]
5771fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5772 use std::os::unix::fs::PermissionsExt;
5773 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5774}
5775
5776#[cfg(not(unix))]
5777fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5778 Ok(())
5779}
5780
5781fn find_executable(program: &str) -> Option<PathBuf> {
5782 let candidate = PathBuf::from(program);
5783 if candidate.components().count() > 1 {
5784 return candidate.is_file().then_some(candidate);
5785 }
5786 let path = std::env::var_os("PATH")?;
5787 for directory in std::env::split_paths(&path) {
5788 let candidate = directory.join(program);
5789 if candidate.is_file() {
5790 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5791 }
5792 #[cfg(windows)]
5793 {
5794 for extension in ["exe", "cmd", "bat"] {
5795 let candidate = directory.join(format!("{program}.{extension}"));
5796 if candidate.is_file() {
5797 return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5798 }
5799 }
5800 }
5801 }
5802 None
5803}
5804
5805async fn executable_version(executable: &Path) -> Option<String> {
5806 let mut command = tokio::process::Command::new(executable);
5807 command
5808 .arg("--version")
5809 .stdin(std::process::Stdio::null())
5810 .stdout(std::process::Stdio::piped())
5811 .stderr(std::process::Stdio::piped())
5812 .kill_on_drop(true);
5813 let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5814 .await
5815 .ok()?
5816 .ok()?;
5817 let stdout = String::from_utf8_lossy(&output.stdout);
5818 let stderr = String::from_utf8_lossy(&output.stderr);
5819 stdout
5820 .lines()
5821 .chain(stderr.lines())
5822 .map(str::trim)
5823 .find(|line| !line.is_empty())
5824 .map(|line| truncate_text(line, 200))
5825}
5826
5827pub(crate) fn auth_evidence(harness: &str) -> bool {
5828 let env_names: &[&str] = match harness {
5829 HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5830 HarnessId::CODEX => &["OPENAI_API_KEY"],
5831 HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5832 HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5833 HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5834 HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5835 HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5836 _ => &[],
5837 };
5838 if env_names
5839 .iter()
5840 .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5841 {
5842 return true;
5843 }
5844 let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5845 return false;
5846 };
5847 let files: Vec<PathBuf> = match harness {
5848 HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5849 HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5850 HarnessId::OPENCODE => vec![
5851 home.join(".local/share/opencode/auth.json"),
5852 home.join(".config/opencode/auth.json"),
5853 ],
5854 HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5855 HarnessId::GROK => vec![home.join(".grok/auth.json")],
5856 HarnessId::GEMINI => vec![
5857 home.join(".gemini/oauth_creds.json"),
5858 home.join(".gemini/google_accounts.json"),
5859 ],
5860 HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5861 HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5862 _ => Vec::new(),
5863 };
5864 if files.into_iter().any(|path| {
5865 std::fs::metadata(path)
5866 .map(|metadata| metadata.is_file() && metadata.len() > 2)
5867 .unwrap_or(false)
5868 }) {
5869 return true;
5870 }
5871 if harness == HarnessId::CLAUDE_CODE {
5878 return std::fs::read_to_string(home.join(".claude.json"))
5879 .map(|text| text.contains("\"oauthAccount\""))
5880 .unwrap_or(false);
5881 }
5882 false
5883}
5884
5885fn looks_like_auth_error(message: &str) -> bool {
5886 let message = message.to_ascii_lowercase();
5887 [
5888 "auth",
5889 "login",
5890 "sign in",
5891 "sign-in",
5892 "credential",
5893 "unauthorized",
5894 "forbidden",
5895 "token",
5896 ]
5897 .iter()
5898 .any(|needle| message.contains(needle))
5899}
5900
5901fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5902 crate::RuntimeCapabilities {
5903 start_session: false,
5904 resume_session: false,
5905 attach_existing_process: false,
5906 send_input: false,
5907 stream_events: false,
5908 interrupt: false,
5909 steer: false,
5910 respond_to_requests: false,
5911 }
5912}
5913
5914fn truncate_text(text: &str, max_chars: usize) -> String {
5915 let mut chars = text.chars();
5916 let truncated = chars.by_ref().take(max_chars).collect::<String>();
5917 if chars.next().is_some() {
5918 format!("{truncated}…")
5919 } else {
5920 truncated
5921 }
5922}
5923
5924fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5931 match &handle.endpoint {
5932 crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5933 crate::RuntimeEndpoint::Http { .. } => None,
5934 }
5935}
5936
5937fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5943 match process_group {
5944 #[cfg(unix)]
5945 Some(pid) => {
5946 crate::lsp::kill_process_group(pid);
5947 true
5948 }
5949 #[cfg(not(unix))]
5950 Some(_) => false,
5951 None => false,
5952 }
5953}
5954
5955fn error_message(error: ServiceError) -> String {
5956 match error {
5957 ServiceError::InvalidParams(message)
5958 | ServiceError::Operation(message)
5959 | ServiceError::UnsupportedAction(message) => message,
5960 ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5961 ServiceError::Sdk(error) => error.to_string(),
5962 }
5963}
5964
5965#[derive(Debug)]
5966enum ServiceError {
5967 InvalidParams(String),
5968 MethodNotFound,
5969 UnsupportedAction(String),
5970 Operation(String),
5971 Sdk(SdkError),
5972}
5973
5974fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5975 match error {
5976 ServiceError::InvalidParams(message) => {
5977 SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5978 }
5979 ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5980 SdkError::unsupported(operation)
5981 }
5982 ServiceError::Operation(message) => {
5983 let code = if message.contains("already in progress") {
5984 SdkErrorCode::Busy
5985 } else if message.contains("not supported by this runtime") {
5986 SdkErrorCode::UnsupportedAction
5987 } else if message.contains("unknown runtime connection") {
5988 SdkErrorCode::NotFound
5989 } else {
5990 SdkErrorCode::Execution
5991 };
5992 SdkError::new(code, operation, message)
5993 }
5994 ServiceError::Sdk(error) => error,
5995 }
5996}
5997
5998fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5999 let error_code = error.code();
6000 let code = match error_code {
6001 SdkErrorCode::Unauthenticated => -32030,
6002 SdkErrorCode::Unauthorized => -32031,
6003 SdkErrorCode::ControllerRequired => -32032,
6004 SdkErrorCode::LeaseExpired => -32033,
6005 SdkErrorCode::InvalidArgument => -32602,
6006 SdkErrorCode::NotFound => -32004,
6007 SdkErrorCode::Busy => -32000,
6008 SdkErrorCode::UnsupportedAction => -32020,
6009 SdkErrorCode::Execution => -32002,
6010 SdkErrorCode::Transport => -32003,
6011 };
6012 json!({
6013 "jsonrpc": "2.0",
6014 "id": id,
6015 "error": {
6016 "code": code,
6017 "name": error_code,
6018 "operation": error.operation(),
6019 "message": error.to_string(),
6020 },
6021 })
6022}
6023
6024fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
6025 serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
6026}
6027
6028fn operation(error: impl Into<crate::Error>) -> ServiceError {
6029 let error = error.into();
6030 match error {
6031 crate::Error::Sdk(error) => ServiceError::Sdk(error),
6032 error => ServiceError::Operation(error.to_string()),
6033 }
6034}
6035
6036#[derive(Debug, Clone, Deserialize, Default)]
6040#[serde(default)]
6041struct MemoryRequest {
6042 harness: Option<String>,
6044 query: Option<String>,
6046 profile: Option<String>,
6048 session: Option<String>,
6050 full: bool,
6052 regex: bool,
6054 cwd: Option<std::path::PathBuf>,
6056 homes: crate::HarnessHomes,
6058}
6059
6060fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6063 let request = decode::<MemoryRequest>(params)?;
6064 let harness = request
6065 .harness
6066 .clone()
6067 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6068 let to_service = |error: crate::memory::MemoryError| match error {
6069 crate::memory::MemoryError::UnsupportedHarness { .. }
6070 | crate::memory::MemoryError::SessionNotScoped { .. } => {
6071 ServiceError::UnsupportedAction(error.to_string())
6072 }
6073 other => ServiceError::InvalidParams(other.to_string()),
6074 };
6075 match method {
6076 "harness.v1.memory.show" => {
6077 let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6078 harness,
6079 profile: request.profile,
6080 session: request.session,
6081 full: request.full,
6082 cwd: request.cwd,
6083 homes: request.homes,
6084 })
6085 .map_err(to_service)?;
6086 Ok(json!({
6087 "schema": crate::memory::MEMORY_SCHEMA,
6088 "documents": documents,
6089 }))
6090 }
6091 "harness.v1.memory.search" => {
6092 let query = request
6093 .query
6094 .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6095 let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6096 harness,
6097 query,
6098 profile: request.profile,
6099 regex: request.regex,
6100 cwd: request.cwd,
6101 homes: request.homes,
6102 })
6103 .map_err(to_service)?;
6104 Ok(json!({
6105 "schema": crate::memory::MEMORY_SCHEMA,
6106 "matches": matches,
6107 }))
6108 }
6109 _ => Err(ServiceError::MethodNotFound),
6110 }
6111}
6112
6113#[derive(Debug, Clone, Deserialize)]
6117#[serde(default)]
6118struct ProfilesQuery {
6119 harness: Option<String>,
6121 name: Option<String>,
6123 homes: crate::HarnessHomes,
6125}
6126
6127impl Default for ProfilesQuery {
6128 fn default() -> Self {
6129 Self {
6130 harness: None,
6131 name: None,
6132 homes: crate::HarnessHomes::default(),
6133 }
6134 }
6135}
6136
6137fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6140 let query = decode::<ProfilesQuery>(params)?;
6141 let to_service = |error: crate::profiles::ProfileError| match error {
6142 crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6143 ServiceError::UnsupportedAction(error.to_string())
6144 }
6145 crate::profiles::ProfileError::NotFound { .. } => {
6146 ServiceError::InvalidParams(error.to_string())
6147 }
6148 };
6149 match method {
6150 "harness.v1.profiles.list" => {
6151 let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6152 .map_err(to_service)?;
6153 Ok(json!({
6154 "schema": crate::profiles::PROFILES_SCHEMA,
6155 "profiles": profiles,
6156 }))
6157 }
6158 "harness.v1.profiles.get" => {
6159 let harness = query
6160 .harness
6161 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6162 let name = query
6163 .name
6164 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6165 let profile =
6166 crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6167 Ok(json!({
6168 "schema": crate::profiles::PROFILES_SCHEMA,
6169 "profile": profile,
6170 }))
6171 }
6172 _ => Err(ServiceError::MethodNotFound),
6173 }
6174}
6175
6176#[derive(Debug, Clone, Deserialize)]
6180#[serde(default)]
6181struct ChannelsQuery {
6182 harness: Option<String>,
6184 name: Option<String>,
6186 homes: crate::HarnessHomes,
6188}
6189
6190impl Default for ChannelsQuery {
6191 fn default() -> Self {
6192 Self {
6193 harness: None,
6194 name: None,
6195 homes: crate::HarnessHomes::default(),
6196 }
6197 }
6198}
6199
6200#[derive(Debug, Clone, Deserialize)]
6204#[serde(default)]
6205struct RoutesQuery {
6206 harness: Option<String>,
6207 profile: Option<String>,
6209 homes: crate::HarnessHomes,
6210}
6211
6212impl Default for RoutesQuery {
6213 fn default() -> Self {
6214 Self {
6215 harness: None,
6216 profile: None,
6217 homes: crate::HarnessHomes::default(),
6218 }
6219 }
6220}
6221
6222#[derive(Debug, Clone, Deserialize)]
6223#[serde(default)]
6224struct TriggersQuery {
6225 harness: Option<String>,
6226 homes: crate::HarnessHomes,
6227}
6228
6229impl Default for TriggersQuery {
6230 fn default() -> Self {
6231 Self {
6232 harness: None,
6233 homes: crate::HarnessHomes::default(),
6234 }
6235 }
6236}
6237
6238fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6239 let query = decode::<TriggersQuery>(params)?;
6240 let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6241 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6242 Ok(json!({
6243 "schema": crate::triggers::TRIGGERS_SCHEMA,
6244 "triggers": triggers,
6245 }))
6246}
6247
6248fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6249 let query = decode::<RoutesQuery>(params)?;
6250 let routes = crate::routes::list_routes(
6251 &query.homes,
6252 query.harness.as_deref(),
6253 query.profile.as_deref(),
6254 )
6255 .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6256 Ok(json!({
6257 "schema": crate::routes::ROUTES_SCHEMA,
6258 "routes": routes,
6259 }))
6260}
6261
6262fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6263 let query = decode::<ChannelsQuery>(params)?;
6264 let to_service = |error: crate::channels::ChannelError| match error {
6265 crate::channels::ChannelError::UnsupportedHarness { .. } => {
6266 ServiceError::UnsupportedAction(error.to_string())
6267 }
6268 crate::channels::ChannelError::NotFound { .. } => {
6269 ServiceError::InvalidParams(error.to_string())
6270 }
6271 };
6272 match method {
6273 "harness.v1.channels.list" => {
6274 let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6275 .map_err(to_service)?;
6276 Ok(json!({
6277 "schema": crate::channels::CHANNELS_SCHEMA,
6278 "channels": channels,
6279 }))
6280 }
6281 "harness.v1.channels.status" => {
6282 let harness = query
6283 .harness
6284 .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6285 let name = query
6286 .name
6287 .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6288 let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6289 .map_err(to_service)?;
6290 Ok(json!({
6291 "schema": crate::channels::CHANNELS_SCHEMA,
6292 "channel": channel,
6293 }))
6294 }
6295 _ => Err(ServiceError::MethodNotFound),
6296 }
6297}
6298
6299fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6300 json!({
6301 "jsonrpc": "2.0",
6302 "id": id,
6303 "error": {"code": code, "message": message},
6304 })
6305}
6306
6307#[cfg(test)]
6308mod tests {
6309 use super::*;
6310 use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6311 use async_trait::async_trait;
6312 use std::io::Write;
6313 use std::path::PathBuf;
6314 use std::time::Instant;
6315
6316 #[test]
6317 fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6318 let descriptor = SessionDescriptor {
6319 locator: SessionLocator {
6320 harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6321 session_id: "live-session".into(),
6322 storage: StorageLocator::File {
6323 path: PathBuf::from("/tmp/live-session.jsonl"),
6324 },
6325 },
6326 cwd: Some(PathBuf::from("/project")),
6327 title: None,
6328 preview_candidates: Vec::new(),
6329 latest_message_candidates: Vec::new(),
6330 updated_at_ms: Some(1),
6331 message_count: None,
6332 model: None,
6333 parent_session_id: None,
6334 child_session_count: 0,
6335 nouns: Default::default(),
6336 };
6337 let peer = crate::claude_peer::ClaudePeerSession {
6338 pid: 42,
6339 session_id: "live-session".into(),
6340 cwd: Some(PathBuf::from("/project")),
6341 name: "peer".into(),
6342 socket_path: PathBuf::from("/tmp/peer.sock"),
6343 status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6344 updated_at_ms: Some(1),
6345 version: Some("test".into()),
6346 };
6347
6348 let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6349 assert!(value["live_endpoint"]
6350 .as_str()
6351 .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6352 }
6353
6354 struct EndingRuntime {
6355 handle: RuntimeHandle,
6356 event: Option<HarnessEvent>,
6357 close_failures: usize,
6358 }
6359
6360 #[async_trait]
6361 impl RuntimeConnection for EndingRuntime {
6362 fn handle(&self) -> &RuntimeHandle {
6363 &self.handle
6364 }
6365
6366 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6367 unreachable!("ending runtime does not accept input")
6368 }
6369
6370 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6371 Ok(self.event.take())
6372 }
6373
6374 async fn interrupt(&mut self) -> crate::Result<()> {
6375 Ok(())
6376 }
6377
6378 async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6379 Ok(())
6380 }
6381
6382 async fn close(&mut self) -> crate::Result<()> {
6383 if self.close_failures > 0 {
6384 self.close_failures -= 1;
6385 return Err(crate::Error::Other(
6386 "cleanup temporarily unavailable".into(),
6387 ));
6388 }
6389 Ok(())
6390 }
6391 }
6392
6393 fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6394 Box::new(EndingRuntime {
6395 handle: RuntimeHandle {
6396 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6397 runtime_id: "ending-session".into(),
6398 endpoint: RuntimeEndpoint::LocalProcess {
6399 pid: None,
6400 command: vec!["ending-runtime".into()],
6401 protocol: "test".into(),
6402 },
6403 },
6404 event,
6405 close_failures: 0,
6406 })
6407 }
6408
6409 #[tokio::test]
6410 async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6411 let mut service = HarnessSessionService::new();
6412 let handle = ending_runtime(None).handle().clone();
6413 let runtime_id = handle.runtime_id.clone();
6414 let opened = service
6415 .insert_runtime(Box::new(EndingRuntime {
6416 handle,
6417 event: None,
6418 close_failures: 1,
6419 }))
6420 .unwrap();
6421 let connection = opened["connection"].as_str().unwrap().to_string();
6422 service.terminal_launches.insert(
6423 connection.clone(),
6424 StructuredLaunch {
6425 cwd: PathBuf::from("/fixture"),
6426 program: "fixture".into(),
6427 arguments: Vec::new(),
6428 env: BTreeMap::new(),
6429 },
6430 );
6431 let first = service
6432 .handle_async(request(
6433 1,
6434 "harness.v1.runtimes.close",
6435 json!({"connection": connection}),
6436 ))
6437 .await;
6438 assert!(first.get("error").is_some(), "{first}");
6440 assert!(!service.runtimes.contains_key(&connection));
6445 assert!(!service.terminal_launches.contains_key(&connection));
6446 assert!(!service.runtime_sequences.contains_key(&runtime_id));
6447 let again = service
6448 .handle_async(request(
6449 2,
6450 "harness.v1.runtimes.close",
6451 json!({"connection": connection}),
6452 ))
6453 .await;
6454 assert_eq!(again["error"]["code"], -32602, "{again}");
6455 }
6456
6457 fn request(id: u64, method: &str, params: Value) -> Value {
6458 json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6459 }
6460
6461 fn hermes_store() -> PathBuf {
6464 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6465 }
6466
6467 fn hermes_discovery(params: Value) -> Value {
6471 let mut response =
6472 HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6473 let store = hermes_store().display().to_string();
6474 for session in response["result"]["sessions"]
6475 .as_array_mut()
6476 .expect("sessions array")
6477 {
6478 if session["locator"]["storage"]["path"] == json!(store) {
6479 session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6480 }
6481 session.as_object_mut().unwrap().remove("activity");
6485 }
6486 response["result"].take()
6487 }
6488
6489 fn hermes_query() -> Value {
6490 json!({
6491 "harnesses": ["hermes"],
6492 "homes": {"hermes": hermes_store()},
6493 })
6494 }
6495
6496 fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6497 result["sessions"]
6498 .as_array()
6499 .expect("sessions array")
6500 .iter()
6501 .find(|session| session["locator"]["session_id"] == json!(id))
6502 .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6503 }
6504
6505 #[test]
6506 fn orch6_discover_rows_carry_the_conversation_nouns() {
6507 let result = hermes_discovery(hermes_query());
6508
6509 let dm = row(&result, "tg-dm-1");
6512 assert_eq!(dm["trigger"], json!("channel"));
6513 assert_eq!(dm["surface"]["platform"], json!("telegram"));
6514 assert_eq!(dm["surface"]["kind"], json!("dm"));
6515 assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6516 assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6517 assert_eq!(
6518 dm["workspace"],
6519 json!({"kind": "channel", "value": "telegram:123456"})
6520 );
6521 assert!(dm.get("profile").is_none(), "{dm:#}");
6522
6523 let fire = row(&result, "cron_job42_20260902_120000");
6525 assert_eq!(fire["trigger"], json!("cron"));
6526 assert_eq!(
6527 fire["recurrence"],
6528 json!({"job_id": "job42", "kind": "cron"})
6529 );
6530 assert_eq!(fire["workspace"]["kind"], json!("repo"));
6531
6532 let coder = row(&result, "tg-coder-1");
6535 assert_eq!(coder["trigger"], json!("channel"));
6536 assert_eq!(coder["profile"], json!("coder"));
6537 assert_eq!(coder["surface"]["thread_id"], json!("55"));
6538 assert_eq!(
6539 coder["surface"]["key"],
6540 json!("agent:coder:telegram:group:-100777:55")
6541 );
6542 assert_eq!(
6543 coder["workspace"],
6544 json!({"kind": "repo", "value": "/workspace/project"})
6545 );
6546 assert_eq!(
6547 coder["cross_surface"],
6548 json!({"state": "pending", "platform": "discord"})
6549 );
6550
6551 let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6553 assert_eq!(acp["trigger"], json!("human"));
6554 assert!(acp.get("surface").is_none(), "{acp:#}");
6555 assert_eq!(acp["workspace"], json!({"kind": "none"}));
6556 }
6557
6558 #[test]
6559 fn orch6_discover_filters_by_harness_and_profile() {
6560 let mut params = hermes_query();
6561 params["profile"] = json!("coder");
6562 let result = hermes_discovery(params);
6563 let ids: Vec<&str> = result["sessions"]
6564 .as_array()
6565 .expect("sessions array")
6566 .iter()
6567 .map(|session| session["locator"]["session_id"].as_str().unwrap())
6568 .collect();
6569 assert_eq!(ids, vec!["tg-coder-1"]);
6570
6571 let mut missing = hermes_query();
6574 missing["profile"] = json!("nobody");
6575 assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6576
6577 let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6580 assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6581 }
6582
6583 #[test]
6584 fn orch6_load_reports_the_same_nouns_as_discovery() {
6585 let mut service = HarnessSessionService::new();
6586 let loaded = service.handle(request(
6587 1,
6588 "harness.v1.sessions.load",
6589 json!({"locator": {
6590 "harness": "hermes",
6591 "session_id": "tg-coder-1",
6592 "storage": {"kind": "file", "path": hermes_store()},
6593 }}),
6594 ));
6595 let session = &loaded["result"]["session"];
6596 let discovered = hermes_discovery(hermes_query());
6597 let row = row(&discovered, "tg-coder-1");
6598 for noun in [
6599 "trigger",
6600 "surface",
6601 "profile",
6602 "recurrence",
6603 "cross_surface",
6604 "workspace",
6605 ] {
6606 assert_eq!(
6607 session[noun],
6608 row.get(noun).cloned().unwrap_or(Value::Null),
6609 "`{noun}` disagrees between sessions.load and sessions.discover"
6610 );
6611 }
6612 }
6613
6614 fn profile_fixture_homes() -> Value {
6617 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6618 json!({
6619 "hermes": fixtures.join("hermes_home/state.db"),
6620 "openclaw": fixtures.join("openclaw_home"),
6621 })
6622 }
6623
6624 fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6625 response["result"]["profiles"]
6626 .as_array()
6627 .unwrap_or_else(|| panic!("no profiles array in {response}"))
6628 .iter()
6629 .find(|row| row["harness"] == harness && row["name"] == name)
6630 .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6631 }
6632
6633 #[test]
6638 fn profiles_list_reads_every_source_uniformly() {
6639 let mut service = HarnessSessionService::new();
6640 let response = service.handle(request(
6641 1,
6642 "harness.v1.profiles.list",
6643 json!({"homes": profile_fixture_homes()}),
6644 ));
6645 assert_eq!(
6646 response["result"]["schema"],
6647 crate::profiles::PROFILES_SCHEMA
6648 );
6649
6650 let default = profile_row(&response, "hermes", "default");
6651 assert_eq!(default["kind"], "hermes_profile");
6652 assert_eq!(default["default"], true);
6653 assert_eq!(default["routes"], 0);
6654 assert_eq!(default["sessions"], 11);
6655 assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6656
6657 let coder = profile_row(&response, "hermes", "coder");
6658 assert_eq!(coder["kind"], "hermes_profile");
6659 assert_eq!(coder["default"], false);
6660 assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6661 assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6662 assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6663 assert!(coder["home"]
6664 .as_str()
6665 .unwrap()
6666 .ends_with("hermes_home/profiles/coder"));
6667
6668 let main = profile_row(&response, "openclaw", "main");
6669 assert_eq!(main["kind"], "openclaw_agent");
6670 assert_eq!(main["default"], true);
6673 assert_eq!(main["routes"], 0);
6674 assert_eq!(main["sessions"], 4);
6675 assert_eq!(
6676 main["model"],
6677 Value::Null,
6678 "`agents.defaults.model` is an install default, not this agent's pin"
6679 );
6680
6681 let design = profile_row(&response, "openclaw", "design");
6682 assert_eq!(design["default"], false);
6683 assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6684 assert_eq!(design["sessions"], 0);
6685 assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6686
6687 let preset = profile_row(&response, "supercode", "supercode-default");
6688 assert_eq!(preset["kind"], "preset");
6689 assert_eq!(preset["default"], true);
6690 assert_eq!(preset["home"], Value::Null);
6691 assert_eq!(preset["routes"], Value::Null);
6692 }
6693
6694 #[test]
6697 fn profiles_list_reads_codex_profile_tables() {
6698 let codex_home = std::env::temp_dir().join(format!(
6699 "supercode-orch10-codex-{}-{}",
6700 std::process::id(),
6701 std::time::SystemTime::now()
6702 .duration_since(std::time::UNIX_EPOCH)
6703 .unwrap()
6704 .as_nanos()
6705 ));
6706 std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6707 std::fs::write(
6708 codex_home.join("config.toml"),
6709 "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6710 )
6711 .unwrap();
6712
6713 let mut service = HarnessSessionService::new();
6714 let response = service.handle(request(
6715 1,
6716 "harness.v1.profiles.list",
6717 json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6718 ));
6719 let rows = response["result"]["profiles"].as_array().unwrap();
6720 assert_eq!(rows.len(), 2, "{response}");
6721 let review = profile_row(&response, "codex", "review");
6722 assert_eq!(review["kind"], "codex_profile");
6723 assert_eq!(review["default"], true);
6724 assert_eq!(review["model"], "gpt-5.1-codex");
6725 assert_eq!(review["home"], Value::Null);
6726 assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6727
6728 let got = service.handle(request(
6729 2,
6730 "harness.v1.profiles.get",
6731 json!({
6732 "harness": "codex",
6733 "name": "fast",
6734 "homes": {"codex": codex_home.join("sessions")},
6735 }),
6736 ));
6737 assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6738 std::fs::remove_dir_all(&codex_home).ok();
6739 }
6740
6741 #[test]
6744 fn profiles_refuse_harnesses_without_the_concept() {
6745 let mut service = HarnessSessionService::new();
6746 let response = service.handle(request(
6747 1,
6748 "harness.v1.profiles.list",
6749 json!({"harness": "claude-code"}),
6750 ));
6751 assert_eq!(response["error"]["code"], -32020, "{response}");
6752
6753 let missing = service.handle(request(
6754 2,
6755 "harness.v1.profiles.get",
6756 json!({
6757 "harness": "hermes",
6758 "name": "no-such-profile",
6759 "homes": profile_fixture_homes(),
6760 }),
6761 ));
6762 assert_eq!(missing["error"]["code"], -32602, "{missing}");
6763 }
6764
6765 #[test]
6768 fn profiles_methods_are_advertised() {
6769 let mut service = HarnessSessionService::new();
6770 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6771 let methods = response["result"]["methods"].as_array().unwrap();
6772 for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6773 assert!(
6774 methods.iter().any(|entry| entry == method),
6775 "{method} is not advertised"
6776 );
6777 }
6778 }
6779
6780 fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6785 response["result"]["channels"]
6786 .as_array()
6787 .unwrap_or_else(|| panic!("no channels array in {response}"))
6788 .iter()
6789 .find(|row| row["harness"] == harness && row["name"] == name)
6790 .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6791 }
6792
6793 fn channels_list(harness: Option<&str>) -> Value {
6794 let mut params = json!({"homes": profile_fixture_homes()});
6795 if let Some(harness) = harness {
6796 params["harness"] = json!(harness);
6797 }
6798 HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6799 }
6800
6801 #[test]
6805 fn channels_list_reads_both_gateway_harnesses_uniformly() {
6806 let response = channels_list(None);
6807 assert_eq!(
6808 response["result"]["schema"],
6809 crate::channels::CHANNELS_SCHEMA
6810 );
6811
6812 let telegram = channel_row(&response, "hermes", "telegram");
6815 assert_eq!(telegram["kind"], "telegram");
6816 assert_eq!(telegram["enabled"], true);
6817 assert_eq!(telegram["configured"], true);
6818 assert_eq!(telegram["sessions"], 2);
6822 let api = channel_row(&response, "hermes", "api_server");
6823 assert_eq!(api["configured"], true, "extra.key is a credential key");
6824 assert_eq!(api["sessions"], 0);
6825 let webhook = channel_row(&response, "hermes", "webhook");
6826 assert_eq!(webhook["enabled"], false);
6827 assert_eq!(webhook["configured"], true);
6830
6831 let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6833 assert_eq!(linked["kind"], "slack");
6834 assert_eq!(linked["account"], "T0FIXTURE");
6835 assert_eq!(linked["enabled"], true);
6836 assert_eq!(linked["configured"], true);
6837 let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6838 assert_eq!(unlinked["enabled"], false);
6839 assert_eq!(
6840 unlinked["configured"], false,
6841 "an account with no credential key is not configured"
6842 );
6843 let telegram = channel_row(&response, "openclaw", "telegram");
6846 assert_eq!(telegram["account"], "hermes-fixture-bot");
6847 assert_eq!(telegram["configured"], true);
6848
6849 for row in response["result"]["channels"].as_array().unwrap() {
6851 assert_eq!(row["status"], "unknown", "{row}");
6852 }
6853 }
6854
6855 #[test]
6859 fn channels_rows_never_carry_a_fixture_secret() {
6860 let secrets = [
6861 "FAKE-TOKEN-DO-NOT-EMIT",
6862 "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6863 "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6864 "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6865 "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6866 ];
6867 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6869 let raw = format!(
6870 "{}{}",
6871 std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6872 std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6873 );
6874 for secret in secrets {
6875 assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6876 }
6877
6878 let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6879 for secret in secrets {
6880 assert!(
6881 !emitted.contains(secret),
6882 "`{secret}` leaked into a channel row: {emitted}"
6883 );
6884 }
6885 for row in channels_list(None)["result"]["channels"]
6888 .as_array()
6889 .unwrap()
6890 {
6891 for key in row.as_object().unwrap().keys() {
6892 let key = key.to_ascii_lowercase();
6893 assert!(
6894 !["token", "key", "secret", "password", "credential"]
6895 .iter()
6896 .any(|marker| key.ends_with(marker)),
6897 "`{key}` is a credential-shaped field on a channel row"
6898 );
6899 }
6900 }
6901 }
6902
6903 #[test]
6905 fn channels_status_reads_one_row_by_name() {
6906 let mut service = HarnessSessionService::new();
6907 let got = service.handle(request(
6908 1,
6909 "harness.v1.channels.status",
6910 json!({
6911 "harness": "openclaw",
6912 "name": "slack/T0FIXTURE",
6913 "homes": profile_fixture_homes(),
6914 }),
6915 ));
6916 assert_eq!(got["result"]["channel"]["kind"], "slack");
6917 assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6918 assert_eq!(got["result"]["channel"]["status"], "unknown");
6919
6920 let missing = service.handle(request(
6921 2,
6922 "harness.v1.channels.status",
6923 json!({
6924 "harness": "openclaw",
6925 "name": "no-such-channel",
6926 "homes": profile_fixture_homes(),
6927 }),
6928 ));
6929 assert_eq!(missing["error"]["code"], -32602, "{missing}");
6930 }
6931
6932 #[test]
6936 fn channels_refuse_harnesses_without_the_concept() {
6937 let response = channels_list(Some("claude-code"));
6938 assert_eq!(response["error"]["code"], -32020, "{response}");
6939 let codex = channels_list(Some("codex"));
6940 assert_eq!(codex["error"]["code"], -32020, "{codex}");
6941 }
6942
6943 #[test]
6945 fn channels_list_filters_by_harness() {
6946 let response = channels_list(Some("openclaw"));
6947 let rows = response["result"]["channels"].as_array().unwrap();
6948 assert!(!rows.is_empty(), "{response}");
6949 assert!(
6950 rows.iter().all(|row| row["harness"] == "openclaw"),
6951 "harness filter leaked: {response}"
6952 );
6953 }
6954
6955 #[test]
6958 fn channels_methods_are_advertised() {
6959 let mut service = HarnessSessionService::new();
6960 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6961 let methods = response["result"]["methods"].as_array().unwrap();
6962 for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6963 assert!(
6964 methods.iter().any(|entry| entry == method),
6965 "{method} is not advertised"
6966 );
6967 }
6968 }
6969
6970 #[test]
6975 fn orch6_story_fixture_matches_the_live_discovery_response() {
6976 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6977 .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6978 let mut result = hermes_discovery(hermes_query());
6979 result.as_object_mut().unwrap().remove("next_cursor");
6983 let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6984 if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6985 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6986 std::fs::write(&path, &rendered).unwrap();
6987 }
6988 let committed = std::fs::read_to_string(&path).unwrap_or_default();
6989 assert_eq!(
6990 committed, rendered,
6991 "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6992 re-run with SUPERCODE_UPDATE_FIXTURES=1"
6993 );
6994 }
6995
6996 fn pi_locator() -> SessionLocator {
6997 SessionLocator {
6998 harness: HarnessId::from(HarnessId::PI),
6999 session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
7000 storage: StorageLocator::File {
7001 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7002 .join("tests/fixtures/pi_session.jsonl"),
7003 },
7004 }
7005 }
7006
7007 fn opencode_locator() -> SessionLocator {
7008 let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
7009 SessionLocator {
7010 harness: HarnessId::from(HarnessId::OPENCODE),
7011 session_id: session_id.into(),
7012 storage: StorageLocator::Sqlite {
7013 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7014 .join("tests/fixtures/opencode_fixture/opencode.db"),
7015 selector: session_id.into(),
7016 },
7017 }
7018 }
7019
7020 fn grok_locator() -> SessionLocator {
7021 SessionLocator {
7022 harness: HarnessId::from(HarnessId::GROK),
7023 session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
7024 storage: StorageLocator::File {
7025 path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7026 .join("tests/fixtures/grok_session/chat_history.jsonl"),
7027 },
7028 }
7029 }
7030
7031 fn fixture_homes() -> Value {
7034 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7035 json!({
7036 "claude_code": fixtures.join("__absent__"),
7037 "codex": fixtures.join("__absent__"),
7038 "opencode": fixtures.join("__absent__"),
7039 "pi": fixtures.join("__absent__"),
7040 "agents": fixtures.join("__absent__"),
7041 "hermes": fixtures.join("hermes_home"),
7042 "openclaw": fixtures.join("openclaw_home"),
7043 })
7044 }
7045
7046 #[test]
7047 fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
7048 let root = std::env::temp_dir().join(format!(
7049 "supercode-preview-rpc-{}-{}",
7050 std::process::id(),
7051 std::time::SystemTime::now()
7052 .duration_since(std::time::UNIX_EPOCH)
7053 .unwrap()
7054 .as_nanos()
7055 ));
7056 std::fs::create_dir_all(&root).unwrap();
7057 for id in ["first", "second"] {
7058 std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7059 json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7060 json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7061 )).unwrap();
7062 }
7063 let mut service = HarnessSessionService::new();
7064 let query = json!({
7065 "harnesses": ["codex"], "homes": {"codex": root},
7066 "query": "nebula", "search_previews": true, "limit": 1
7067 });
7068 let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7069 assert!(first.get("error").is_none(), "{first}");
7070 assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7071 assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7072 let mut next_query = query.clone();
7073 next_query["cursor"] = first["result"]["next_cursor"].clone();
7074 let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7075 assert_eq!(next["result"]["receipt"]["returned"], 1);
7076 assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7077 assert_eq!(next["result"]["receipt"]["truncated"], false);
7078 assert_ne!(
7079 first["result"]["sessions"][0]["locator"],
7080 next["result"]["sessions"][0]["locator"]
7081 );
7082 let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7083 assert!(
7084 refused["error"]["message"]
7085 .as_str()
7086 .unwrap()
7087 .contains("use sessions.discover"),
7088 "{refused}"
7089 );
7090 std::fs::remove_dir_all(root).unwrap();
7091 }
7092
7093 #[test]
7094 fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7095 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7096 let root = std::env::temp_dir().join(format!(
7097 "supercode-index-rpc-{}-{}",
7098 std::process::id(),
7099 std::time::SystemTime::now()
7100 .duration_since(std::time::UNIX_EPOCH)
7101 .unwrap()
7102 .as_nanos()
7103 ));
7104 std::fs::create_dir_all(&root).unwrap();
7105 for id in ["first", "second"] {
7106 std::fs::write(root.join(format!("{id}.jsonl")), format!(
7107 "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7108 )).unwrap();
7109 }
7110 let mut service = HarnessSessionService::new();
7111 let opened = service.handle(request(
7112 1,
7113 "harness.v1.sessions.index.subscribe",
7114 json!({
7115 "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7116 }),
7117 ));
7118 assert!(opened.get("error").is_none(), "{opened:#}");
7119 let subscription = opened["result"]["subscription"]
7120 .as_str()
7121 .unwrap()
7122 .to_owned();
7123 assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7124 for params in [
7125 json!({"subscription": subscription, "limit": 0}),
7126 json!({"subscription": subscription, "limit": 2049}),
7127 json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7128 json!({"subscription": "unknown", "limit": 2}),
7129 ] {
7130 let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7131 assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7132 }
7133 for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7134 let response = service.handle(request(
7135 3,
7136 "harness.v1.sessions.index.resize",
7137 json!({
7138 "subscription": subscription, "limit": limit
7139 }),
7140 ));
7141 assert!(response.get("error").is_none(), "{response:#}");
7142 assert_eq!(response["result"]["subscription"], subscription);
7143 assert_eq!(response["result"]["revision"], revision);
7144 assert_eq!(
7145 response["result"]["initial"].as_array().unwrap().len(),
7146 limit
7147 );
7148 assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7149 assert_eq!(service.index_subscriptions.len(), 1);
7150 }
7151 let removed = service.handle(request(
7152 4,
7153 "harness.v1.sessions.index.unsubscribe",
7154 json!({
7155 "subscription": subscription
7156 }),
7157 ));
7158 assert_eq!(removed["result"]["removed"], true);
7159 let stale = service.handle(request(
7160 5,
7161 "harness.v1.sessions.index.resize",
7162 json!({
7163 "subscription": subscription, "limit": 1
7164 }),
7165 ));
7166 assert_eq!(stale["error"]["code"], -32602);
7167 drop(service);
7168 std::fs::remove_dir_all(root).unwrap();
7169 }
7170
7171 fn skills_rows(params: Value) -> Vec<Value> {
7172 let response =
7173 HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7174 assert!(response.get("error").is_none(), "{response:#}");
7175 response["result"].as_array().cloned().unwrap_or_default()
7176 }
7177
7178 #[test]
7181 fn skills_list_reads_the_hermes_and_openclaw_roots() {
7182 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7183 let rows = skills_rows(json!({
7184 "homes": fixture_homes(),
7185 "cwd": fixtures.join("hermes_home"),
7186 }));
7187 let arxiv = rows
7188 .iter()
7189 .find(|row| row["name"] == json!("arxiv-search"))
7190 .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7191 assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7192 assert_eq!(arxiv["scope"], json!("user"));
7193 assert_eq!(arxiv["version"], json!("1.4.0"));
7194 assert!(arxiv["location"]
7195 .as_str()
7196 .unwrap()
7197 .ends_with("hermes_home/skills/research/arxiv"));
7198
7199 let bare = rows
7201 .iter()
7202 .find(|row| row["name"] == json!("bare-skill"))
7203 .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7204 assert_eq!(bare["enabled"], json!(null));
7205 assert!(bare.get("description").is_none());
7206
7207 let demo = rows
7208 .iter()
7209 .find(|row| row["name"] == json!("clawhub-demo"))
7210 .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7211 assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7212 assert_eq!(demo["scope"], json!("managed"));
7213 assert_eq!(demo["enabled"], json!(false));
7214 }
7215
7216 #[test]
7218 fn skills_list_filters_by_harness_and_scope() {
7219 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7220 let hermes = skills_rows(json!({
7221 "homes": fixture_homes(),
7222 "cwd": fixtures.join("hermes_home"),
7223 "harness": HarnessId::HERMES,
7224 }));
7225 assert!(!hermes.is_empty());
7226 assert!(hermes
7227 .iter()
7228 .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7229
7230 let managed = skills_rows(json!({
7231 "homes": fixture_homes(),
7232 "cwd": fixtures.join("openclaw_home"),
7233 "harness": HarnessId::OPENCLAW,
7234 "scope": "managed",
7235 }));
7236 assert_eq!(managed.len(), 1, "{managed:#?}");
7237 assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7238
7239 let bundled = skills_rows(json!({
7240 "homes": fixture_homes(),
7241 "cwd": fixtures.join("openclaw_home"),
7242 "harness": HarnessId::OPENCLAW,
7243 "scope": "bundled",
7244 }));
7245 assert!(bundled.is_empty(), "{bundled:#?}");
7246 }
7247
7248 #[test]
7251 fn skills_list_refuses_an_unknown_harness() {
7252 let response = HarnessSessionService::new().handle(request(
7253 1,
7254 "harness.v1.skills.list",
7255 json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7256 ));
7257 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7258 assert!(response["error"]["message"]
7259 .as_str()
7260 .unwrap()
7261 .contains("not-a-harness"));
7262 }
7263
7264 #[test]
7266 fn skills_list_is_an_advertised_method_and_sdk_operation() {
7267 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7268 assert_eq!(
7269 SdkOperation::from_method("harness.v1.skills.list"),
7270 Some(SdkOperation::SkillsList)
7271 );
7272 }
7273
7274 #[test]
7278 fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7279 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7280 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7281 assert_eq!(
7282 SdkOperation::from_method("harness.v1.skills.install"),
7283 Some(SdkOperation::SkillsInstall)
7284 );
7285 assert_eq!(
7286 SdkOperation::from_method("harness.v1.skills.remove"),
7287 Some(SdkOperation::SkillsRemove)
7288 );
7289 }
7290
7291 #[test]
7295 fn skills_install_and_remove_drive_the_directory_door() {
7296 let root = std::env::temp_dir().join(format!(
7297 "supercode-orch22-rpc-{}-{}",
7298 std::process::id(),
7299 std::time::SystemTime::now()
7300 .duration_since(std::time::UNIX_EPOCH)
7301 .unwrap()
7302 .as_nanos()
7303 ));
7304 let source = root.join("probe-src");
7305 std::fs::create_dir_all(&source).unwrap();
7306 std::fs::write(
7307 source.join("SKILL.md"),
7308 "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7309 )
7310 .unwrap();
7311 let homes = json!({
7312 "claude_code": root.join("claude_home"),
7313 "codex": root.join("__absent__"),
7314 "opencode": root.join("__absent__"),
7315 "pi": root.join("__absent__"),
7316 "hermes": root.join("__absent__"),
7317 "openclaw": root.join("__absent__"),
7318 "agents": root.join("__absent__"),
7319 });
7320
7321 let mut service = HarnessSessionService::new();
7322 let installed = service.handle(request(
7323 1,
7324 "harness.v1.skills.install",
7325 json!({
7326 "harness": HarnessId::CLAUDE_CODE,
7327 "source": source,
7328 "scope": "user",
7329 "cwd": root,
7330 "homes": homes,
7331 }),
7332 ));
7333 let result = &installed["result"];
7334 assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7335 assert_eq!(result["verb"], json!("install"));
7336 assert!(result["ran"]
7337 .as_str()
7338 .is_some_and(|ran| ran.starts_with("cp -R ")));
7339 assert_eq!(result["skill"]["scope"], json!("user"));
7340
7341 let removed = service.handle(request(
7342 2,
7343 "harness.v1.skills.remove",
7344 json!({
7345 "harness": HarnessId::CLAUDE_CODE,
7346 "name": "orch22-rpc",
7347 "scope": "user",
7348 "cwd": root,
7349 "homes": homes,
7350 }),
7351 ));
7352 assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7353 assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7354 std::fs::remove_dir_all(&root).ok();
7355 }
7356
7357 #[test]
7360 fn skills_remove_refuses_openclaw_at_the_pin() {
7361 let response = HarnessSessionService::new().handle(request(
7362 1,
7363 "harness.v1.skills.remove",
7364 json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7365 ));
7366 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7367 assert!(response["error"]["message"]
7368 .as_str()
7369 .unwrap()
7370 .contains("no `skills remove` verb"));
7371 }
7372
7373 #[test]
7376 fn skills_install_refuses_a_harness_without_a_skills_root() {
7377 let response = HarnessSessionService::new().handle(request(
7378 1,
7379 "harness.v1.skills.install",
7380 json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7381 ));
7382 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7383 assert!(response["error"]["message"]
7384 .as_str()
7385 .unwrap()
7386 .contains("not-a-harness"));
7387 }
7388
7389 fn memory_homes() -> Value {
7397 let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7398 json!({
7399 "claude_code": fixtures.join("__absent__"),
7400 "codex": fixtures.join("__absent__"),
7401 "opencode": fixtures.join("__absent__"),
7402 "pi": fixtures.join("__absent__"),
7403 "grok": fixtures.join("__absent__"),
7404 "gemini": fixtures.join("__absent__"),
7405 "goose": fixtures.join("__absent__"),
7406 "supercode": fixtures.join("__absent__"),
7407 "hermes": fixtures.join("hermes_home/state.db"),
7408 "openclaw": fixtures.join("openclaw_home"),
7409 })
7410 }
7411
7412 fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7413 let response = HarnessSessionService::new().handle(request(1, method, params));
7414 assert!(response.get("error").is_none(), "{response:#}");
7415 assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7416 response["result"][key]
7417 .as_array()
7418 .cloned()
7419 .unwrap_or_default()
7420 }
7421
7422 fn memory_documents(params: Value) -> Vec<Value> {
7423 memory_call_ok("harness.v1.memory.show", params, "documents")
7424 }
7425
7426 fn memory_matches(params: Value) -> Vec<Value> {
7427 memory_call_ok("harness.v1.memory.search", params, "matches")
7428 }
7429
7430 fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7431 rows.iter()
7432 .find(|row| row["profile"] == profile && row["name"] == name)
7433 .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7434 }
7435
7436 #[test]
7439 fn memory_show_reads_the_hermes_profile_homes() {
7440 let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7441
7442 let notes = find_document(&rows, "default", "MEMORY.md");
7443 assert_eq!(notes["harness"], "hermes");
7444 assert_eq!(notes["scope"], "user");
7445 assert!(notes["size"].as_u64().unwrap() > 0);
7446 assert!(notes["updated_at"].is_string(), "{notes:#?}");
7447 assert!(notes.get("content").is_none(), "{notes:#?}");
7449 assert_eq!(notes["truncated"], true);
7450 assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7451
7452 let user = find_document(&rows, "default", "USER.md");
7453 assert_eq!(user["scope"], "user");
7454 assert!(user["preview"]
7455 .as_array()
7456 .unwrap()
7457 .iter()
7458 .any(|line| line.as_str().unwrap().contains("neovim")));
7459
7460 let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7461 assert!(topic["path"]
7462 .as_str()
7463 .unwrap()
7464 .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7465
7466 let coder = find_document(&rows, "coder", "MEMORY.md");
7468 assert_eq!(coder["scope"], "profile");
7469 assert!(coder["path"]
7470 .as_str()
7471 .unwrap()
7472 .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7473 }
7474
7475 #[test]
7478 fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7479 let rows = memory_documents(json!({
7480 "harness": "hermes",
7481 "profile": "coder",
7482 "full": true,
7483 "homes": memory_homes(),
7484 }));
7485 assert!(
7486 rows.iter().all(|row| row["profile"] == "coder"),
7487 "{rows:#?}"
7488 );
7489 let coder = find_document(&rows, "coder", "MEMORY.md");
7490 assert!(coder["content"]
7491 .as_str()
7492 .expect("full returns the body")
7493 .contains("anthropic/claude-opus-4-8"));
7494 }
7495
7496 #[test]
7500 fn memory_show_reads_the_openclaw_agent_workspaces() {
7501 let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7502
7503 let main = find_document(&rows, "main", "MEMORY.md");
7504 assert_eq!(main["scope"], "agent");
7505 assert!(main["path"]
7506 .as_str()
7507 .unwrap()
7508 .ends_with("openclaw_home/workspace/MEMORY.md"));
7509
7510 let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7511 assert!(topic["path"]
7512 .as_str()
7513 .unwrap()
7514 .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7515
7516 let design = find_document(&rows, "design", "MEMORY.md");
7517 assert!(design["path"]
7518 .as_str()
7519 .unwrap()
7520 .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7521 }
7522
7523 #[test]
7526 fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7527 let scratch = std::env::temp_dir().join(format!(
7528 "supercode-orch12-cc-{}-{}",
7529 std::process::id(),
7530 std::time::SystemTime::now()
7531 .duration_since(std::time::UNIX_EPOCH)
7532 .unwrap()
7533 .as_nanos()
7534 ));
7535 let project = scratch.join("repo");
7536 std::fs::create_dir_all(project.join(".git")).unwrap();
7537 let worktree = project.join("crates/harness");
7540 std::fs::create_dir_all(&worktree).unwrap();
7541 let slug: String = project
7542 .to_string_lossy()
7543 .chars()
7544 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7545 .collect();
7546 let projects = scratch.join("claude/projects");
7547 let memory = projects.join(&slug).join("memory");
7548 std::fs::create_dir_all(&memory).unwrap();
7549 std::fs::write(
7550 memory.join("MEMORY.md"),
7551 "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7552 )
7553 .unwrap();
7554 std::fs::write(
7555 memory.join("build-box.md"),
7556 "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7557 )
7558 .unwrap();
7559
7560 let mut homes = memory_homes();
7561 homes["claude_code"] = json!(projects);
7562 let rows = memory_documents(json!({
7563 "harness": "claude-code",
7564 "cwd": worktree,
7565 "homes": homes,
7566 }));
7567 let index = find_document(&rows, &slug, "MEMORY.md");
7568 assert_eq!(index["harness"], "claude-code");
7569 assert_eq!(index["scope"], "project");
7570 let topic = find_document(&rows, &slug, "build-box.md");
7571 assert!(topic["preview"]
7572 .as_array()
7573 .unwrap()
7574 .iter()
7575 .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7576
7577 let hits = memory_matches(json!({
7578 "harness": "claude-code",
7579 "query": "pinned harnesses",
7580 "cwd": worktree,
7581 "homes": homes,
7582 }));
7583 assert_eq!(hits.len(), 1, "{hits:#?}");
7584 assert_eq!(hits[0]["name"], "MEMORY.md");
7585 assert_eq!(hits[0]["line"], 2);
7586
7587 let _ = std::fs::remove_dir_all(&scratch);
7588 }
7589
7590 #[test]
7596 fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7597 let state = std::env::temp_dir().join(format!(
7598 "supercode-orch12-oc-{}-{}",
7599 std::process::id(),
7600 std::time::SystemTime::now()
7601 .duration_since(std::time::UNIX_EPOCH)
7602 .unwrap()
7603 .as_nanos()
7604 ));
7605 std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7607 std::fs::create_dir_all(state.join("workspace")).unwrap();
7608 std::fs::write(
7609 state.join("workspace/MEMORY.md"),
7610 "the gateway websocket needs credentials\n",
7611 )
7612 .unwrap();
7613
7614 let mut homes = memory_homes();
7615 homes["openclaw"] = json!(state);
7616 let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7617 assert_eq!(rows.len(), 1, "{rows:#?}");
7618 let row = find_document(&rows, "main", "MEMORY.md");
7619 assert_eq!(row["scope"], "agent");
7620 assert!(row["path"]
7621 .as_str()
7622 .unwrap()
7623 .ends_with("workspace/MEMORY.md"));
7624
7625 let _ = std::fs::remove_dir_all(&state);
7626 }
7627
7628 #[test]
7631 fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7632 let hit = memory_matches(json!({
7633 "harness": "hermes",
7634 "query": "NEOVIM",
7635 "homes": memory_homes(),
7636 }));
7637 assert_eq!(hit.len(), 1, "{hit:#?}");
7638 assert_eq!(hit[0]["harness"], "hermes");
7639 assert_eq!(hit[0]["name"], "USER.md");
7640 assert_eq!(hit[0]["scope"], "user");
7641 assert_eq!(hit[0]["line"], 5);
7642 assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7643
7644 let regex = memory_matches(json!({
7646 "harness": "hermes",
7647 "query": "neo(vim|vi)",
7648 "regex": true,
7649 "homes": memory_homes(),
7650 }));
7651 assert_eq!(regex.len(), 1, "{regex:#?}");
7652
7653 let miss = memory_matches(json!({
7654 "harness": "hermes",
7655 "query": "no-memory-line-says-this",
7656 "homes": memory_homes(),
7657 }));
7658 assert!(miss.is_empty(), "{miss:#?}");
7659 }
7660
7661 #[test]
7664 fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7665 for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7666 let response = HarnessSessionService::new().handle(request(
7667 1,
7668 method,
7669 json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7670 ));
7671 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7672 assert!(response["error"]["message"]
7673 .as_str()
7674 .unwrap()
7675 .contains("codex"));
7676 }
7677
7678 let response = HarnessSessionService::new().handle(request(
7679 1,
7680 "harness.v1.memory.show",
7681 json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7682 ));
7683 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7684
7685 let response = HarnessSessionService::new().handle(request(
7687 1,
7688 "harness.v1.memory.show",
7689 json!({"homes": memory_homes()}),
7690 ));
7691 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7692 }
7693
7694 #[test]
7696 fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7697 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7698 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7699 assert_eq!(
7700 SdkOperation::from_method("harness.v1.memory.show"),
7701 Some(SdkOperation::MemoryShow)
7702 );
7703 assert_eq!(
7704 SdkOperation::from_method("harness.v1.memory.search"),
7705 Some(SdkOperation::MemorySearch)
7706 );
7707 }
7708
7709 struct RequestingRuntime {
7714 handle: RuntimeHandle,
7715 events: std::collections::VecDeque<HarnessEvent>,
7716 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7717 }
7718
7719 #[async_trait]
7720 impl RuntimeConnection for RequestingRuntime {
7721 fn handle(&self) -> &RuntimeHandle {
7722 &self.handle
7723 }
7724
7725 async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7726 unreachable!("this runtime only raises requests")
7727 }
7728
7729 async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7730 match self.events.pop_front() {
7731 Some(event) => Ok(Some(event)),
7732 None => std::future::pending().await,
7736 }
7737 }
7738
7739 async fn interrupt(&mut self) -> crate::Result<()> {
7740 Ok(())
7741 }
7742
7743 async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7744 self.answered
7748 .lock()
7749 .unwrap_or_else(std::sync::PoisonError::into_inner)
7750 .push(json!({"request_id": request_id, "response": response}));
7751 Ok(())
7752 }
7753
7754 async fn close(&mut self) -> crate::Result<()> {
7755 Ok(())
7756 }
7757 }
7758
7759 fn requesting_runtime(
7760 harness: &str,
7761 events: Vec<HarnessEvent>,
7762 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7763 ) -> Box<dyn RuntimeConnection> {
7764 requesting_runtime_named(harness, "hermes-live-session", events, answered)
7765 }
7766
7767 fn requesting_runtime_named(
7768 harness: &str,
7769 runtime_id: &str,
7770 events: Vec<HarnessEvent>,
7771 answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7772 ) -> Box<dyn RuntimeConnection> {
7773 Box::new(RequestingRuntime {
7774 handle: RuntimeHandle {
7775 harness: HarnessId::from(harness),
7776 runtime_id: runtime_id.into(),
7777 endpoint: RuntimeEndpoint::LocalProcess {
7778 pid: None,
7779 command: vec!["hermes-acp".into()],
7780 protocol: "acp".into(),
7781 },
7782 },
7783 events: events.into(),
7784 answered,
7785 })
7786 }
7787
7788 fn permission_event(id: u64, title: &str) -> HarnessEvent {
7789 HarnessEvent {
7790 sequence: None,
7791 kind: "session/request_permission".into(),
7792 payload: json!({
7793 "jsonrpc": "2.0",
7794 "id": id,
7795 "method": "session/request_permission",
7796 "params": {
7797 "sessionId": "hermes-live-session",
7798 "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7799 "options": [
7800 {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7801 {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7802 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7803 ],
7804 },
7805 }),
7806 }
7807 }
7808
7809 fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7810 let response = service.handle(request(1, "harness.v1.approvals.list", params));
7811 assert!(response.get("error").is_none(), "{response:#}");
7812 response["result"].clone()
7813 }
7814
7815 #[tokio::test]
7823 async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7824 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7825 let mut service = HarnessSessionService::new();
7826 service.runtimes.insert(
7827 "runtime-cc".into(),
7828 requesting_runtime_named(
7829 HarnessId::CLAUDE_CODE,
7830 "claude-live-session",
7831 vec![HarnessEvent {
7832 sequence: None,
7833 kind: "control_request".into(),
7834 payload: json!({
7835 "type": "control_request",
7836 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7837 "request": {
7838 "subtype": "can_use_tool",
7839 "tool_name": "Bash",
7840 "display_name": "Bash",
7841 "input": {"command": "touch probe-artifact.txt"},
7842 "tool_use_id": "toolu_mock_1",
7843 },
7844 }),
7845 }],
7846 answered.clone(),
7847 ),
7848 );
7849
7850 let notifications = service.poll_runtimes().await;
7851 assert_eq!(notifications.len(), 1, "{notifications:#?}");
7852
7853 let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7854 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7855 let row = &rows[0];
7856 assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7857 assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7858 assert_eq!(row["status"], "pending");
7859 assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7860 assert_eq!(row["runtime_id"], "claude-live-session");
7861 assert_eq!(
7862 row["options"]
7863 .as_array()
7864 .unwrap()
7865 .iter()
7866 .map(|option| option["id"].as_str().unwrap())
7867 .collect::<Vec<_>>(),
7868 vec!["allow", "deny"],
7869 );
7870
7871 let response = resolve(
7872 &mut service,
7873 json!({"id": row["id"], "decision": "allow_once"}),
7874 )
7875 .await;
7876 assert!(response.get("error").is_none(), "{response:#}");
7877 assert_eq!(response["result"]["option_id"], "allow");
7878 assert_eq!(
7879 answered
7880 .lock()
7881 .unwrap_or_else(std::sync::PoisonError::into_inner)
7882 .as_slice(),
7883 &[json!({
7884 "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7885 "response": {"behavior": "allow"},
7886 })],
7887 );
7888 assert_eq!(
7889 approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7890 .as_array()
7891 .map(Vec::len),
7892 Some(0),
7893 );
7894 }
7895
7896 #[tokio::test]
7900 async fn a_live_permission_request_lists_until_it_is_answered() {
7901 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7902 let mut service = HarnessSessionService::new();
7903 service.runtimes.insert(
7904 "runtime-1".into(),
7905 requesting_runtime(
7906 HarnessId::HERMES,
7907 vec![permission_event(7, "rm -rf build")],
7908 answered.clone(),
7909 ),
7910 );
7911
7912 let notifications = service.poll_runtimes().await;
7913 assert_eq!(notifications.len(), 1, "{notifications:#?}");
7914
7915 let rows = approvals(&mut service, json!({}));
7916 assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7917 let row = &rows[0];
7918 assert_eq!(row["id"], "runtime-1/7");
7919 assert_eq!(row["harness"], HarnessId::HERMES);
7920 assert_eq!(row["kind"], "live");
7921 assert_eq!(row["status"], "pending");
7922 assert_eq!(row["subject"], "rm -rf build");
7923 assert_eq!(row["session_id"], "hermes-live-session");
7924 assert_eq!(row["runtime_id"], "hermes-live-session");
7925 assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7926 assert!(
7927 row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7928 "{row:#}"
7929 );
7930 assert_eq!(
7931 row["options"]
7932 .as_array()
7933 .unwrap()
7934 .iter()
7935 .map(|option| option["id"].as_str().unwrap())
7936 .collect::<Vec<_>>(),
7937 vec!["allow_once", "allow_for_session", "deny"],
7938 );
7939
7940 assert_eq!(
7942 approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7943 .as_array()
7944 .map(Vec::len),
7945 Some(1),
7946 );
7947 assert_eq!(
7948 approvals(&mut service, json!({"session": "some-other-session"}))
7949 .as_array()
7950 .map(Vec::len),
7951 Some(0),
7952 );
7953
7954 let response = service
7955 .handle_async(request(
7956 2,
7957 "harness.v1.runtimes.respond",
7958 json!({
7959 "connection": "runtime-1",
7960 "request_id": 7,
7961 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7962 }),
7963 ))
7964 .await;
7965 assert!(response.get("error").is_none(), "{response:#}");
7966 assert_eq!(
7967 answered
7968 .lock()
7969 .unwrap_or_else(std::sync::PoisonError::into_inner)
7970 .as_slice(),
7971 &[json!({
7972 "request_id": 7,
7973 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7974 })],
7975 );
7976
7977 let rows = approvals(&mut service, json!({}));
7978 assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7979 }
7980
7981 #[test]
7984 fn queued_subagent_approvals_list_through_the_same_door() {
7985 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7986 crate::subagents::QueuedApproval {
7987 child_agent_id: "child-7".into(),
7988 tool: "shell".into(),
7989 subject: Some("cargo publish --dry-run".into()),
7990 queued_at_ms: 1,
7991 outcome: None,
7992 },
7993 crate::subagents::QueuedApproval {
7994 child_agent_id: "child-8".into(),
7995 tool: "write_file".into(),
7996 subject: None,
7997 queued_at_ms: 2,
7998 outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7999 },
8000 ]));
8001 let mut service = HarnessSessionService::new();
8002 service.observe_subagent_approvals(queue);
8003
8004 let rows = approvals(&mut service, json!({}));
8005 assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
8006 assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
8007 assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
8008 assert_eq!(rows[0]["status"], "pending");
8009 assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
8010 assert_eq!(rows[1]["status"], "denied");
8011 assert!(rows[1]["options"].as_array().unwrap().is_empty());
8012
8013 let only = approvals(&mut service, json!({"session": "child-8"}));
8015 assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
8016 assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
8017 }
8018
8019 #[test]
8025 fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
8026 let response = HarnessSessionService::new().handle(request(
8027 1,
8028 "harness.v1.approvals.list",
8029 json!({"harness": "not-a-harness"}),
8030 ));
8031 assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
8032 assert!(response["error"]["message"]
8033 .as_str()
8034 .unwrap()
8035 .contains("not-a-harness"));
8036 for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
8037 let response = HarnessSessionService::new().handle(request(
8038 1,
8039 "harness.v1.approvals.list",
8040 json!({"harness": harness}),
8041 ));
8042 assert!(response.get("error").is_none(), "{harness}: {response:#}");
8043 }
8044 }
8045
8046 #[test]
8050 fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
8051 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8052 assert_eq!(
8053 SdkOperation::from_method("harness.v1.approvals.list"),
8054 Some(SdkOperation::ApprovalsList)
8055 );
8056 let registry = harness_support_registry();
8057 for id in [
8058 HarnessId::HERMES,
8059 HarnessId::OPENCLAW,
8060 HarnessId::CODEX,
8061 HarnessId::CLAUDE_CODE,
8064 ] {
8065 let concept = registry
8066 .harnesses
8067 .iter()
8068 .find(|harness| harness.id.as_str() == id)
8069 .unwrap()
8070 .orchestration
8071 .concepts
8072 .iter()
8073 .find(|concept| concept.concept == "pending_request")
8074 .unwrap();
8075 assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8076 assert!(concept
8077 .methods
8078 .iter()
8079 .any(|method| method == "harness.v1.approvals.list"));
8080 }
8081 }
8082
8083 async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8086 service
8087 .handle_async(request(3, "harness.v1.approvals.resolve", params))
8088 .await
8089 }
8090
8091 #[tokio::test]
8095 async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8096 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8097 let mut service = HarnessSessionService::new();
8098 service.runtimes.insert(
8099 "runtime-1".into(),
8100 requesting_runtime(
8101 HarnessId::HERMES,
8102 vec![permission_event(7, "rm -rf build")],
8103 answered.clone(),
8104 ),
8105 );
8106 service.poll_runtimes().await;
8107
8108 let rows = approvals(&mut service, json!({}));
8109 assert_eq!(rows[0]["id"], "runtime-1/7");
8110
8111 let response = resolve(
8112 &mut service,
8113 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8114 )
8115 .await;
8116 assert!(response.get("error").is_none(), "{response:#}");
8117 assert_eq!(
8118 response["result"],
8119 json!({
8120 "id": "runtime-1/7",
8121 "decision": "allow_once",
8122 "option_id": "allow_once",
8123 "resolved": true,
8124 }),
8125 );
8126 assert_eq!(
8128 answered
8129 .lock()
8130 .unwrap_or_else(std::sync::PoisonError::into_inner)
8131 .as_slice(),
8132 &[json!({
8133 "request_id": 7,
8134 "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8135 })],
8136 );
8137 assert_eq!(
8139 approvals(&mut service, json!({})).as_array().map(Vec::len),
8140 Some(0),
8141 );
8142 let response = resolve(
8144 &mut service,
8145 json!({"id": "runtime-1/7", "decision": "allow_once"}),
8146 )
8147 .await;
8148 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8149 }
8150
8151 #[tokio::test]
8154 async fn deny_selects_the_requests_own_reject_option() {
8155 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8156 let mut service = HarnessSessionService::new();
8157 service.runtimes.insert(
8158 "runtime-1".into(),
8159 requesting_runtime(
8160 HarnessId::HERMES,
8161 vec![permission_event(11, "git push --force")],
8162 answered.clone(),
8163 ),
8164 );
8165 service.poll_runtimes().await;
8166
8167 let response = resolve(
8168 &mut service,
8169 json!({"id": "runtime-1/11", "decision": "deny"}),
8170 )
8171 .await;
8172 assert!(response.get("error").is_none(), "{response:#}");
8173 assert_eq!(response["result"]["option_id"], "deny");
8175 assert_eq!(
8176 answered
8177 .lock()
8178 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8179 json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8180 );
8181 assert_eq!(
8182 approvals(&mut service, json!({})).as_array().map(Vec::len),
8183 Some(0),
8184 );
8185 }
8186
8187 #[tokio::test]
8190 async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8191 let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8192 let mut service = HarnessSessionService::new();
8193 let mut event = permission_event(3, "rm -rf build");
8194 event.payload["params"]["options"] = json!([
8197 {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8198 {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8199 ]);
8200 service.runtimes.insert(
8201 "runtime-1".into(),
8202 requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8203 );
8204 service.poll_runtimes().await;
8205
8206 let response = resolve(
8207 &mut service,
8208 json!({"id": "runtime-1/3", "decision": "allow_always"}),
8209 )
8210 .await;
8211 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8212 let message = response["error"]["message"].as_str().unwrap();
8213 assert!(message.contains("allow_always"), "{message}");
8214 assert!(message.contains("allow_once, deny"), "{message}");
8215 assert!(answered
8217 .lock()
8218 .unwrap_or_else(std::sync::PoisonError::into_inner)
8219 .is_empty());
8220 assert_eq!(
8221 approvals(&mut service, json!({})).as_array().map(Vec::len),
8222 Some(1),
8223 );
8224 }
8225
8226 #[tokio::test]
8230 async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8231 let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8232 crate::subagents::QueuedApproval {
8233 child_agent_id: "child-7".into(),
8234 tool: "shell".into(),
8235 subject: Some("cargo publish --dry-run".into()),
8236 queued_at_ms: 1,
8237 outcome: None,
8238 },
8239 ]));
8240 let mut service = HarnessSessionService::new();
8241 service.observe_subagent_approvals(queue.clone());
8242 let row = approvals(&mut service, json!({}))[0]["id"]
8243 .as_str()
8244 .unwrap()
8245 .to_string();
8246 assert_eq!(row, "supercode/subagent/child-7/1/0");
8247
8248 let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8249 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8250 let message = response["error"]["message"].as_str().unwrap();
8251 assert!(message.contains("queued subagent record"), "{message}");
8252 assert!(message.contains("request"), "{message}");
8253 assert!(queue
8255 .lock()
8256 .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8257 .outcome
8258 .is_none());
8259 }
8260
8261 #[tokio::test]
8264 async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8265 let mut service = HarnessSessionService::new();
8266 let response = resolve(
8267 &mut service,
8268 json!({"id": "runtime-9/4", "decision": "deny"}),
8269 )
8270 .await;
8271 assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8272 assert!(response["error"]["message"]
8273 .as_str()
8274 .unwrap()
8275 .contains("runtime-9/4"));
8276
8277 let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8278 let message = response["error"]["message"].as_str().unwrap();
8279 assert!(
8280 message.contains("allow_once | allow_always | deny"),
8281 "{message}"
8282 );
8283
8284 let response = resolve(
8285 &mut service,
8286 json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8287 )
8288 .await;
8289 assert!(response["error"]["message"]
8290 .as_str()
8291 .unwrap()
8292 .contains("not both"));
8293 }
8294
8295 #[test]
8299 fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8300 assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8301 assert_eq!(
8302 SdkOperation::from_method("harness.v1.approvals.resolve"),
8303 Some(SdkOperation::ApprovalsResolve)
8304 );
8305 assert_eq!(
8306 SdkOperation::ApprovalsResolve.action_name(),
8307 "approvals_resolve"
8308 );
8309 let registry = harness_support_registry();
8310 for id in [
8311 HarnessId::HERMES,
8312 HarnessId::OPENCLAW,
8313 HarnessId::CODEX,
8314 HarnessId::CLAUDE_CODE,
8317 ] {
8318 let concept = registry
8319 .harnesses
8320 .iter()
8321 .find(|harness| harness.id.as_str() == id)
8322 .unwrap()
8323 .orchestration
8324 .concepts
8325 .iter()
8326 .find(|concept| concept.concept == "pending_request")
8327 .unwrap();
8328 assert_eq!(
8329 concept.controlled,
8330 crate::ImplementationKind::BuiltIn,
8331 "{id}"
8332 );
8333 assert!(
8334 concept
8335 .methods
8336 .iter()
8337 .any(|method| method == "harness.v1.approvals.resolve"),
8338 "{id}"
8339 );
8340 }
8341 }
8342
8343 #[test]
8344 fn capabilities_are_explicit_and_versioned() {
8345 let mut service = HarnessSessionService::new();
8346 let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8347 assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8348 assert_eq!(
8349 response["result"]["sdk"]["schema_version"],
8350 crate::SDK_SCHEMA_VERSION
8351 );
8352 assert_eq!(
8353 response["result"]["sdk"]["operations"]
8354 .as_array()
8355 .unwrap()
8356 .len(),
8357 SdkOperation::ALL.len()
8358 );
8359 assert_eq!(
8360 response["result"]["harnesses"].as_array().unwrap().len(),
8361 11
8362 );
8363 assert!(response["result"]["harnesses"]
8364 .as_array()
8365 .unwrap()
8366 .iter()
8367 .any(|harness| harness == HarnessId::GROK));
8368 assert!(response["result"]["harnesses"]
8369 .as_array()
8370 .unwrap()
8371 .iter()
8372 .any(|harness| harness == HarnessId::GOOSE));
8373 }
8374
8375 #[test]
8376 fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8377 let noisy_stderr = crate::HarnessEvent {
8378 sequence: None,
8379 kind: "transport_stderr".into(),
8380 payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8381 };
8382 assert_eq!(handshake_event_failure(&noisy_stderr), None);
8383
8384 let closed = crate::HarnessEvent {
8385 sequence: None,
8386 kind: "transport_closed".into(),
8387 payload: json!({}),
8388 };
8389 assert!(handshake_event_failure(&closed).is_some());
8390 }
8391
8392 #[tokio::test]
8393 async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8394 let mut service = HarnessSessionService::new();
8395 service
8396 .runtimes
8397 .insert("raw-eof".into(), ending_runtime(None));
8398 service.runtimes.insert(
8399 "explicit-close".into(),
8400 ending_runtime(Some(HarnessEvent {
8401 sequence: None,
8402 kind: "transport_closed".into(),
8403 payload: json!({"message": "native transport exited"}),
8404 })),
8405 );
8406
8407 let notifications = service.poll_runtimes().await;
8408
8409 assert_eq!(notifications.len(), 2);
8410 assert!(notifications
8411 .iter()
8412 .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8413 assert!(notifications.iter().all(|notification| {
8414 notification["params"]["session_id"] == "ending-session"
8415 && notification["params"]["connection"].is_string()
8416 }));
8417 let mut sequences = notifications
8418 .iter()
8419 .filter_map(|notification| notification["params"]["sequence"].as_u64())
8420 .collect::<Vec<_>>();
8421 sequences.sort_unstable();
8422 assert_eq!(sequences, vec![1, 2]);
8423 assert!(service.runtimes.is_empty());
8424 }
8425
8426 #[test]
8427 fn support_report_and_grok_default_binding_share_the_registry() {
8428 let mut service = HarnessSessionService::new();
8429 let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8430 assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8431 let params = RuntimeBackendParams {
8432 harness: HarnessId::from(HarnessId::GROK),
8433 protocol: None,
8434 launch: None,
8435 base_url: None,
8436 policy: RuntimePolicy::Default,
8437 };
8438 let backend = match runtime_backend(¶ms) {
8439 Ok(backend) => backend,
8440 Err(_) => panic!("Grok should bind through its registered ACP launch"),
8441 };
8442 assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8443 assert!(backend.capabilities().start_session);
8444 let registered = harness_support_registry()
8445 .harnesses
8446 .into_iter()
8447 .find(|harness| harness.id.as_str() == HarnessId::GROK)
8448 .and_then(|harness| harness.runtime.default_launch)
8449 .unwrap();
8450 assert!(!registered
8451 .arguments
8452 .iter()
8453 .any(|argument| argument == "--always-approve"));
8454 assert!(runtime_launch(¶ms).is_none());
8455
8456 let yolo = RuntimeBackendParams {
8457 policy: RuntimePolicy::Yolo,
8458 ..params
8459 };
8460 assert!(runtime_launch(&yolo)
8461 .unwrap()
8462 .arguments
8463 .iter()
8464 .any(|argument| argument == "--always-approve"));
8465
8466 let mismatched_protocol = RuntimeBackendParams {
8467 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8468 protocol: Some("acp".into()),
8469 launch: None,
8470 base_url: None,
8471 policy: RuntimePolicy::Default,
8472 };
8473 assert!(runtime_backend(&mismatched_protocol).is_err());
8474 }
8475
8476 #[test]
8477 fn load_follow_and_unfollow_share_the_same_locator() {
8478 let mut service = HarnessSessionService::new();
8479 let locator = pi_locator();
8480 let loaded = service.handle(request(
8481 1,
8482 "harness.v1.sessions.load",
8483 json!({"locator": locator}),
8484 ));
8485 assert_eq!(
8486 loaded["result"]["session"]["session_id"],
8487 locator.session_id
8488 );
8489
8490 let followed = service.handle(request(
8491 2,
8492 "harness.v1.sessions.follow",
8493 json!({"locator": locator}),
8494 ));
8495 assert_eq!(followed["result"]["subscription"], "sub-1");
8496 assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8497 assert!(service.poll().is_empty());
8498
8499 let unfollowed = service.handle(request(
8500 3,
8501 "harness.v1.sessions.unfollow",
8502 json!({"subscription": "sub-1"}),
8503 ));
8504 assert_eq!(unfollowed["result"]["removed"], true);
8505 }
8506
8507 #[test]
8508 fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8509 let temp = std::env::temp_dir().join(format!(
8510 "supercode-bounded-view-{}-{}",
8511 std::process::id(),
8512 generated_session_id()
8513 ));
8514 let path = temp.join("parent.jsonl");
8515 let subagents = temp.join("parent/subagents");
8516 std::fs::create_dir_all(&subagents).unwrap();
8517 let long_last = "x".repeat(300);
8518 let parent_records = [
8519 json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8520 json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8521 json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8522 ];
8523 std::fs::write(
8524 &path,
8525 format!(
8526 "{}\n",
8527 parent_records
8528 .iter()
8529 .map(Value::to_string)
8530 .collect::<Vec<_>>()
8531 .join("\n")
8532 ),
8533 )
8534 .unwrap();
8535 std::fs::write(
8536 subagents.join("agent-child.jsonl"),
8537 concat!(
8538 r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8539 "\n",
8540 ),
8541 )
8542 .unwrap();
8543 let locator = SessionLocator {
8544 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8545 session_id: "parent".into(),
8546 storage: StorageLocator::File { path },
8547 };
8548 let mut service = HarnessSessionService::new();
8549
8550 let complete = service.handle(request(
8551 1,
8552 "harness.v1.sessions.load",
8553 json!({"locator": locator}),
8554 ));
8555 assert_eq!(
8556 complete["result"]["session"]["subagents"]
8557 .as_array()
8558 .unwrap()
8559 .len(),
8560 1
8561 );
8562
8563 let bounded = service.handle(request(
8564 2,
8565 "harness.v1.sessions.load",
8566 json!({
8567 "locator": locator,
8568 "view": {
8569 "tail_messages": 1,
8570 "max_message_chars": 256,
8571 "include_subagents": false
8572 },
8573 }),
8574 ));
8575 let session = &bounded["result"]["session"];
8576 assert!(session["subagents"].as_array().unwrap().is_empty());
8577 assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8578 assert_eq!(
8579 session["messages"][0]["content"],
8580 format!("{}\n…", "x".repeat(256))
8581 );
8582
8583 let followed = service.handle(request(
8584 3,
8585 "harness.v1.sessions.follow",
8586 json!({
8587 "locator": locator,
8588 "view": {
8589 "tail_messages": 1,
8590 "max_message_chars": 256,
8591 "include_subagents": false
8592 },
8593 }),
8594 ));
8595 let initial = &followed["result"]["initial"]["session"];
8596 assert!(initial["subagents"].as_array().unwrap().is_empty());
8597 assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8598
8599 let _ = std::fs::remove_dir_all(&temp);
8600 }
8601
8602 #[test]
8603 fn forty_megabyte_display_load_is_bounded_and_prompt() {
8604 let temp = std::env::temp_dir().join(format!(
8605 "supercode-large-display-view-{}-{}",
8606 std::process::id(),
8607 generated_session_id()
8608 ));
8609 std::fs::create_dir_all(&temp).unwrap();
8610 let path = temp.join("rollout.jsonl");
8611 let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8612 writeln!(
8613 file,
8614 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8615 )
8616 .unwrap();
8617 let padding = "x".repeat(80 * 1024);
8618 for index in 0..512 {
8619 let marker = if index == 0 {
8620 "OLDEST-SHOULD-NOT-LOAD"
8621 } else if index == 511 {
8622 "LATEST-MUST-LOAD"
8623 } else {
8624 "bulk"
8625 };
8626 writeln!(
8627 file,
8628 "{}",
8629 json!({
8630 "timestamp": "2026-01-01T00:00:01Z",
8631 "type": "response_item",
8632 "payload": {
8633 "type": "message",
8634 "role": "assistant",
8635 "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8636 },
8637 })
8638 )
8639 .unwrap();
8640 }
8641 file.flush().unwrap();
8642 drop(file);
8643 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8644
8645 let locator = SessionLocator {
8646 harness: HarnessId::from(HarnessId::CODEX),
8647 session_id: "large-display".into(),
8648 storage: StorageLocator::File { path },
8649 };
8650 let started = Instant::now();
8651 let response = HarnessSessionService::new().handle(request(
8652 1,
8653 "harness.v1.sessions.load",
8654 json!({
8655 "locator": locator,
8656 "view": {
8657 "tail_messages": 500,
8658 "max_message_chars": 1024,
8659 "include_subagents": false,
8660 "display_history": true,
8661 },
8662 }),
8663 ));
8664 let elapsed = started.elapsed();
8665 let wire = response.to_string();
8666 eprintln!(
8667 "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8668 wire.len()
8669 );
8670 assert!(response.get("error").is_none(), "{response:#}");
8671 assert!(wire.contains("LATEST-MUST-LOAD"));
8672 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8673 assert!(
8674 wire.len() < 2 * 1024 * 1024,
8675 "bounded wire was {} bytes",
8676 wire.len()
8677 );
8678 assert!(
8679 elapsed.as_secs_f64() < 3.0,
8680 "bounded 40 MiB load took {elapsed:?}"
8681 );
8682
8683 let anchored = temp.join("anchored.jsonl");
8687 let mut file = std::io::BufWriter::new(std::fs::File::create(&anchored).unwrap());
8688 writeln!(
8689 file,
8690 r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8691 )
8692 .unwrap();
8693 for index in 0..512 {
8694 let marker = if index == 0 {
8695 "OLDEST-SHOULD-NOT-LOAD"
8696 } else if index == 511 {
8697 "LATEST-MUST-LOAD"
8698 } else {
8699 "bulk"
8700 };
8701 let (role, kind) = if index % 8 == 0 {
8702 ("user", "input_text")
8703 } else {
8704 ("assistant", "output_text")
8705 };
8706 writeln!(
8707 file,
8708 "{}",
8709 json!({
8710 "timestamp": "2026-01-01T00:00:01Z",
8711 "type": "response_item",
8712 "payload": {
8713 "type": "message",
8714 "role": role,
8715 "content": [{"type": kind, "text": format!("{marker}:{padding}")}],
8716 },
8717 })
8718 )
8719 .unwrap();
8720 }
8721 file.flush().unwrap();
8722 drop(file);
8723 let short = HarnessSessionService::new().handle(request(
8724 2,
8725 "harness.v1.sessions.load",
8726 json!({
8727 "locator": SessionLocator {
8728 harness: HarnessId::from(HarnessId::CODEX),
8729 session_id: "large-display".into(),
8730 storage: StorageLocator::File { path: anchored },
8731 },
8732 "view": {
8733 "tail_messages": 20,
8734 "max_message_chars": 1024,
8735 "include_subagents": false,
8736 "display_history": true,
8737 },
8738 }),
8739 ));
8740 let records = short["result"]["session"]["raw_record_count"].as_u64();
8741 assert!(
8742 records.is_some_and(|records| records < 128),
8743 "{records:?} records read"
8744 );
8745 let short = short.to_string();
8746 assert!(short.contains("LATEST-MUST-LOAD"));
8747 assert!(short.contains("older native records remain outside this bounded display window"));
8748
8749 let _ = std::fs::remove_dir_all(&temp);
8750 }
8751
8752 #[test]
8753 fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8754 let temp = std::env::temp_dir().join(format!(
8755 "supercode-large-goose-view-{}-{}",
8756 std::process::id(),
8757 generated_session_id()
8758 ));
8759 std::fs::create_dir_all(&temp).unwrap();
8760 let path = temp.join("sessions.db");
8761 let connection = rusqlite::Connection::open(&path).unwrap();
8762 connection
8763 .execute_batch(
8764 "CREATE TABLE sessions (
8765 id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8766 created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8767 session_type TEXT NOT NULL, extension_data TEXT,
8768 goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8769 archived_at TEXT
8770 );
8771 CREATE TABLE messages (
8772 id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8773 role TEXT NOT NULL, content_json TEXT NOT NULL,
8774 created_timestamp INTEGER NOT NULL, metadata_json TEXT
8775 );",
8776 )
8777 .unwrap();
8778 connection
8779 .execute(
8780 "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8781 rusqlite::params![
8782 "goose-large",
8783 "Large Goose session",
8784 "/tmp",
8785 "2026-01-01 00:00:00",
8786 "2026-01-01 00:00:02",
8787 "user",
8788 "{}",
8789 "auto",
8790 "anthropic",
8791 r#"{"model_name":"claude-sonnet"}"#,
8792 ],
8793 )
8794 .unwrap();
8795 let old_content = serde_json::to_string(&vec![json!({
8796 "type": "text",
8797 "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8798 })])
8799 .unwrap();
8800 connection
8801 .execute(
8802 "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8803 rusqlite::params!["goose-large", old_content],
8804 )
8805 .unwrap();
8806 connection
8807 .execute(
8808 "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8809 rusqlite::params![
8810 "goose-large",
8811 r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8812 ],
8813 )
8814 .unwrap();
8815 drop(connection);
8816 assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8817
8818 let locator = SessionLocator {
8819 harness: HarnessId::from(HarnessId::GOOSE),
8820 session_id: "goose-large".into(),
8821 storage: StorageLocator::Sqlite {
8822 path,
8823 selector: "goose-large".into(),
8824 },
8825 };
8826 let started = Instant::now();
8827 let response = HarnessSessionService::new().handle(request(
8828 1,
8829 "harness.v1.sessions.load",
8830 json!({
8831 "locator": locator,
8832 "view": {
8833 "tail_messages": 1,
8834 "max_message_chars": 1024,
8835 "include_subagents": false,
8836 "display_history": true,
8837 },
8838 }),
8839 ));
8840 let elapsed = started.elapsed();
8841 let wire = response.to_string();
8842 eprintln!(
8843 "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8844 wire.len()
8845 );
8846 assert!(response.get("error").is_none(), "{response:#}");
8847 assert!(wire.contains("LATEST-MUST-LOAD"));
8848 assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8849 assert!(
8850 wire.len() < 64 * 1024,
8851 "bounded wire was {} bytes",
8852 wire.len()
8853 );
8854 assert!(
8855 elapsed.as_secs_f64() < 1.0,
8856 "bounded Goose load took {elapsed:?}"
8857 );
8858
8859 let _ = std::fs::remove_dir_all(&temp);
8860 }
8861
8862 #[test]
8863 fn display_view_keeps_codex_assistant_history_across_compaction() {
8864 let temp = std::env::temp_dir().join(format!(
8865 "supercode-codex-display-view-{}-{}",
8866 std::process::id(),
8867 generated_session_id()
8868 ));
8869 std::fs::create_dir_all(&temp).unwrap();
8870 let path = temp.join("rollout.jsonl");
8871 std::fs::write(
8872 &path,
8873 concat!(
8874 r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8875 "\n",
8876 r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8877 "\n",
8878 r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8879 "\n",
8880 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"}]}}"#,
8881 "\n",
8882 r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8883 "\n",
8884 r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8885 "\n",
8886 ),
8887 )
8888 .unwrap();
8889 let locator = SessionLocator {
8890 harness: HarnessId::from(HarnessId::CODEX),
8891 session_id: "codex-display".into(),
8892 storage: StorageLocator::File { path },
8893 };
8894 let mut service = HarnessSessionService::new();
8895
8896 let continuation = service.handle(request(
8897 1,
8898 "harness.v1.sessions.load",
8899 json!({"locator": locator}),
8900 ));
8901 let continuation_text = continuation["result"]["session"]["messages"].to_string();
8902 assert!(!continuation_text.contains("old answer"));
8903
8904 let display = service.handle(request(
8905 2,
8906 "harness.v1.sessions.load",
8907 json!({
8908 "locator": locator,
8909 "view": {
8910 "tail_messages": 10,
8911 "include_subagents": false,
8912 "display_history": true,
8913 },
8914 }),
8915 ));
8916 let display_text = display["result"]["session"]["messages"].to_string();
8917 assert!(display_text.contains("old prompt"));
8918 assert!(display_text.contains("old answer"));
8919 assert!(display_text.contains("new prompt"));
8920 assert!(display_text.contains("new answer"));
8921
8922 let _ = std::fs::remove_dir_all(&temp);
8923 }
8924
8925 #[test]
8926 fn indexed_claude_windows_match_the_existing_wire_projection() {
8927 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8928 .join("tests/fixtures/claude_code_session.jsonl");
8929 let locator = SessionLocator {
8930 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8931 session_id: "fixture".into(),
8932 storage: StorageLocator::File { path },
8933 };
8934 let full = load_session(&locator).unwrap();
8935 for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8936 for offset in [0, 1, full.messages.len(), usize::MAX] {
8937 for limit in [0, 1, 3, usize::MAX] {
8938 let options = SessionLoadOptions {
8939 include_subagents: Some(false),
8940 inline_media,
8941 message_offset: Some(offset),
8942 message_limit: Some(limit),
8943 ..Default::default()
8944 };
8945 let expected = projected_session_result(&full, &options);
8946 assert_eq!(
8947 indexed_claude_window(&locator, &options).unwrap().unwrap(),
8948 expected
8949 );
8950 }
8951 }
8952 for tail in [0, 1, 3, usize::MAX] {
8953 let options = SessionLoadOptions {
8954 include_subagents: Some(false),
8955 inline_media,
8956 message_tail: Some(tail),
8957 ..Default::default()
8958 };
8959 assert_eq!(
8960 indexed_claude_window(&locator, &options).unwrap().unwrap(),
8961 projected_session_result(&full, &options)
8962 );
8963 }
8964 }
8965 }
8966
8967 #[test]
8968 fn load_supports_bounded_windows_and_media_metadata() {
8969 let mut service = HarnessSessionService::new();
8970 let locator = pi_locator();
8971 let bounded = service.handle(request(
8972 1,
8973 "harness.v1.sessions.load",
8974 json!({
8975 "locator": locator,
8976 "options": {
8977 "include_subagents": false,
8978 "message_limit": 2,
8979 "message_offset": 1
8980 }
8981 }),
8982 ));
8983 assert_eq!(bounded["result"]["window"]["offset"], 1);
8984 assert_eq!(bounded["result"]["window"]["returned"], 2);
8985 assert!(bounded["result"]["summary"]["first_message"].is_object());
8986 assert!(bounded["result"]["summary"]["last_message"].is_object());
8987 assert_eq!(
8988 bounded["result"]["session"]["messages"]
8989 .as_array()
8990 .unwrap()
8991 .len(),
8992 2
8993 );
8994 assert!(bounded["result"]["session"]["subagents"]
8995 .as_array()
8996 .unwrap()
8997 .is_empty());
8998
8999 let tail = service.handle(request(
9000 2,
9001 "harness.v1.sessions.load",
9002 json!({"locator": locator, "options": {"message_tail": 1}}),
9003 ));
9004 assert_eq!(tail["result"]["window"]["returned"], 1);
9005 assert_eq!(tail["result"]["window"]["has_more"], true);
9006 assert_eq!(tail["result"]["window"]["has_older"], true);
9007 assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
9008 assert!(tail["result"]["summary"]["first_message"].is_object());
9009
9010 let metadata_only = service.handle(request(
9011 3,
9012 "harness.v1.sessions.load",
9013 json!({"locator": locator, "options": {"inline_media": "metadata"}}),
9014 ));
9015 assert!(metadata_only["result"]["session"]
9016 .to_string()
9017 .contains("media_reference"));
9018 assert!(!metadata_only["result"]["session"]
9019 .to_string()
9020 .contains("data:image/"));
9021 }
9022
9023 #[test]
9024 fn import_translate_branch_and_handoff_use_typed_artifacts() {
9025 let mut service = HarnessSessionService::new();
9026 let locator = pi_locator();
9027 let translated = service.handle(request(
9028 1,
9029 "harness.v1.sessions.translate",
9030 json!({"locator": locator, "target_harness": "grok"}),
9031 ));
9032 assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
9033 assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
9034 assert!(translated["result"]["artifact"]["content"]
9035 .as_str()
9036 .is_some_and(|content| !content.is_empty()));
9037
9038 for target in ["opencode", "open-code"] {
9039 let opencode = service.handle(request(
9040 6,
9041 "harness.v1.sessions.translate",
9042 json!({"locator": locator, "target_harness": target}),
9043 ));
9044 assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
9045 }
9046 let goose = service.handle(request(
9047 7,
9048 "harness.v1.sessions.translate",
9049 json!({"locator": locator, "target_harness": "goose"}),
9050 ));
9051 assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
9052 assert!(serde_json::from_str::<Value>(
9053 goose["result"]["artifact"]["content"].as_str().unwrap()
9054 )
9055 .unwrap()["conversation"]
9056 .is_array());
9057
9058 let imported = service.handle(request(
9059 2,
9060 "harness.v1.sessions.import",
9061 json!({
9062 "source_harness": "grok",
9063 "content": translated["result"]["artifact"]["content"],
9064 }),
9065 ));
9066 assert_eq!(imported["result"]["session"]["source"], "grok");
9067
9068 let branched = service.handle(request(
9069 3,
9070 "harness.v1.sessions.branch",
9071 json!({"locator": locator, "target_harness": "codex"}),
9072 ));
9073 assert_eq!(branched["result"]["parent"]["harness"], "pi");
9074 assert!(branched["result"]["bootstrap_prompt"]
9075 .as_str()
9076 .unwrap()
9077 .contains("frozen parent transcript"));
9078 assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
9079
9080 let handoff = service.handle(request(
9081 4,
9082 "harness.v1.sessions.handoff",
9083 json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
9084 ));
9085 assert_eq!(handoff["result"]["launch"]["program"], "pi");
9086 assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
9087 assert_eq!(handoff["result"]["requires_materialization"], true);
9088
9089 let goose_handoff = service.handle(request(
9090 8,
9091 "harness.v1.sessions.handoff",
9092 json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
9093 ));
9094 assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
9095 assert_eq!(
9096 goose_handoff["result"]["materialize"]["arguments"],
9097 json!(["session", "import", "{artifact_path}"])
9098 );
9099
9100 let resumed = service.handle(request(
9101 5,
9102 "harness.v1.sessions.resume_instructions",
9103 json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
9104 ));
9105 assert_eq!(resumed["result"]["launch"]["program"], "pi");
9106 assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
9107 }
9108
9109 #[test]
9110 fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
9111 let temp = std::env::temp_dir().join(format!(
9112 "supercode-service-reduce-{}-{}",
9113 std::process::id(),
9114 generated_session_id()
9115 ));
9116 let source_path = temp.join("source.jsonl");
9117 let store_root = temp.join("store");
9118 std::fs::create_dir_all(&temp).unwrap();
9119
9120 let mut records = vec![json!({
9121 "timestamp": "2026-01-01T00:00:00Z",
9122 "type": "session_meta",
9123 "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9124 })];
9125 for turn in 0..16 {
9126 records.push(json!({
9127 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9128 "type": "response_item",
9129 "payload": {
9130 "type": "message",
9131 "role": "user",
9132 "content": [{
9133 "type": "input_text",
9134 "text": format!("request {turn}: {}", "context ".repeat(80)),
9135 }],
9136 },
9137 }));
9138 records.push(json!({
9139 "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9140 "type": "response_item",
9141 "payload": {
9142 "type": "message",
9143 "role": "assistant",
9144 "content": [{
9145 "type": "output_text",
9146 "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9147 }],
9148 },
9149 }));
9150 }
9151 let source = format!(
9152 "{}\n",
9153 records
9154 .iter()
9155 .map(Value::to_string)
9156 .collect::<Vec<_>>()
9157 .join("\n")
9158 );
9159 std::fs::write(&source_path, &source).unwrap();
9160 let locator = SessionLocator {
9161 harness: HarnessId::from(HarnessId::CODEX),
9162 session_id: "codex-reduce".into(),
9163 storage: StorageLocator::File {
9164 path: source_path.clone(),
9165 },
9166 };
9167 let original = load_session(&locator).unwrap();
9168 let mut service =
9169 HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9170
9171 let response = service.handle(request(
9172 1,
9173 "harness.v1.sessions.reduce",
9174 json!({
9175 "locator": locator,
9176 "target_harness": "claude-code",
9177 "keep_last": 4,
9178 }),
9179 ));
9180 assert!(response.get("error").is_none(), "{response:#}");
9181 let receipt = &response["result"]["receipt"];
9182 assert_eq!(receipt["source_harness"], "codex");
9183 assert_eq!(receipt["target_harness"], "claude-code");
9184 assert_eq!(receipt["verified"], true);
9185 assert_eq!(receipt["reversible"], true);
9186 assert!(receipt["reductions"].as_u64().unwrap() > 0);
9187 assert!(
9188 receipt["source_tokens"].as_u64().unwrap()
9189 > receipt["reduced_tokens"].as_u64().unwrap()
9190 );
9191 assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9192 assert!(response["result"]["bootstrap_prompt"]
9193 .as_str()
9194 .unwrap()
9195 .contains("Do not guess hidden content"));
9196
9197 let rescue_id = receipt["id"].as_str().unwrap();
9198 let store = crate::SessionStore::open(&store_root).unwrap();
9199 let sidecar =
9200 Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9201 let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9202 let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9203 let policy = reduce::ReductionPolicy {
9204 clear_turns_older_than: Some(4),
9205 ..Default::default()
9206 };
9207 let (restamped_view, reapplied_log) =
9208 reduce::project_messages(&sidecar.messages, &policy, &log);
9209 assert_eq!(
9210 messages_jsonl(&persisted_view).unwrap(),
9211 messages_jsonl(&restamped_view).unwrap()
9212 );
9213 assert_eq!(reapplied_log, log);
9214 reduce::verify_log(&log, &sidecar).unwrap();
9215 assert_eq!(
9216 reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9217 original.messages
9218 );
9219 assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9220
9221 std::fs::remove_dir_all(temp).ok();
9222 }
9223
9224 #[test]
9225 fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9226 let temp = std::env::temp_dir().join(format!(
9227 "supercode-severed-view-{}-{}",
9228 std::process::id(),
9229 generated_session_id()
9230 ));
9231 std::fs::create_dir_all(&temp).unwrap();
9232 let path = temp.join("severed.jsonl");
9233 std::fs::write(
9236 &path,
9237 concat!(
9238 r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9239 "\n",
9240 r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9241 "\n",
9242 ),
9243 )
9244 .unwrap();
9245 let locator = SessionLocator {
9246 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9247 session_id: "severed".into(),
9248 storage: StorageLocator::File { path },
9249 };
9250 let mut service = HarnessSessionService::new();
9251
9252 let viewed = service.handle(request(
9253 1,
9254 "harness.v1.sessions.load",
9255 json!({"locator": locator}),
9256 ));
9257 let session = &viewed["result"]["session"];
9258 assert_eq!(session["fidelity"], "semantic");
9259 assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9260 assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9261 entry
9262 .as_str()
9263 .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9264 }));
9265
9266 let strict = service.handle(request(
9269 2,
9270 "harness.v1.sessions.load",
9271 json!({"locator": locator, "fidelity": "byte_lossless"}),
9272 ));
9273 assert!(strict["error"]["message"]
9274 .as_str()
9275 .unwrap()
9276 .contains("cannot reconstruct lossless Claude continuation"));
9277
9278 let translated = service.handle(request(
9280 3,
9281 "harness.v1.sessions.translate",
9282 json!({"locator": locator, "target_harness": "codex"}),
9283 ));
9284 assert!(translated["error"]["message"]
9285 .as_str()
9286 .unwrap()
9287 .contains("cannot reconstruct lossless Claude continuation"));
9288 let resumed = service.handle(request(
9289 4,
9290 "harness.v1.sessions.resume_instructions",
9291 json!({"locator": locator}),
9292 ));
9293 assert!(resumed["error"]["message"]
9294 .as_str()
9295 .unwrap()
9296 .contains("cannot reconstruct lossless Claude continuation"));
9297
9298 let _ = std::fs::remove_dir_all(&temp);
9299 }
9300
9301 #[test]
9302 fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9303 let codex = resume_launch(
9304 HarnessId::CODEX,
9305 "codex-session",
9306 Path::new("/tmp/project"),
9307 ResumePolicy::Yolo,
9308 )
9309 .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9310 assert_eq!(codex.program, "codex");
9311 assert_eq!(
9312 codex.arguments,
9313 [
9314 "-c",
9315 "check_for_update_on_startup=false",
9316 "-c",
9317 "projects.\"/tmp/project\".trust_level=\"trusted\"",
9318 "--dangerously-bypass-approvals-and-sandbox",
9319 "--dangerously-bypass-hook-trust",
9320 "resume",
9321 "codex-session",
9322 ]
9323 );
9324
9325 let gemini = resume_launch(
9326 HarnessId::GEMINI,
9327 "gemini-session",
9328 Path::new("/tmp/project"),
9329 ResumePolicy::Yolo,
9330 )
9331 .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9332 assert_eq!(gemini.program, "gemini");
9333 assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9334
9335 let goose = resume_launch(
9336 HarnessId::GOOSE,
9337 "goose-session",
9338 Path::new("/tmp/project"),
9339 ResumePolicy::Yolo,
9340 )
9341 .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9342 assert_eq!(goose.program, "goose");
9343 assert_eq!(
9344 goose.arguments,
9345 ["session", "--resume", "--session-id", "goose-session"]
9346 );
9347
9348 let supercode = resume_launch(
9349 HarnessId::SUPERCODE,
9350 "supercode-session",
9351 Path::new("/tmp/project"),
9352 ResumePolicy::Yolo,
9353 )
9354 .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9355 assert_eq!(supercode.program, "supercode");
9356 assert_eq!(
9357 supercode.arguments,
9358 ["--dangerous", "resume", "supercode-session"]
9359 );
9360 }
9361
9362 #[test]
9363 fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9364 let temp = std::env::temp_dir().join(format!(
9365 "supercode-harness-artifact-{}-{}",
9366 std::process::id(),
9367 generated_session_id()
9368 ));
9369 let main_path = temp.join("parent.jsonl");
9370 let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9371 std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9372 let fixture = std::fs::read_to_string(
9373 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9374 .join("tests/fixtures/claude_code_session.jsonl"),
9375 )
9376 .unwrap();
9377 let parent = fixture.trim_end_matches('\n');
9378 let child = fixture.trim_end_matches('\n');
9379 std::fs::write(&main_path, parent).unwrap();
9380 std::fs::write(&subagent_path, child).unwrap();
9381 let locator = SessionLocator {
9382 harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9383 session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9384 storage: StorageLocator::File {
9385 path: main_path.clone(),
9386 },
9387 };
9388 let mut service = HarnessSessionService::new();
9389 let claude = service.handle(request(
9390 1,
9391 "harness.v1.sessions.translate",
9392 json!({"locator": locator, "target_harness": "claude-code"}),
9393 ));
9394 let artifact = &claude["result"]["artifact"];
9395 assert_eq!(artifact["fidelity"], "byte_lossless");
9396 assert_eq!(artifact["content"], parent);
9397 let files = artifact["files"].as_array().unwrap();
9398 assert!(files.iter().any(|file| {
9399 file["role"] == "subagent"
9400 && file["path"]
9401 .as_str()
9402 .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9403 && file["content"] == child
9404 }));
9405 assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9406
9407 let grok = service.handle(request(
9408 2,
9409 "harness.v1.sessions.translate",
9410 json!({"locator": grok_locator(), "target_harness": "grok"}),
9411 ));
9412 let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9413 for name in ["summary.json", "updates.jsonl"] {
9414 let expected = std::fs::read_to_string(
9415 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9416 .join("tests/fixtures/grok_session")
9417 .join(name),
9418 )
9419 .unwrap();
9420 assert!(files.iter().any(|file| {
9421 file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9422 }));
9423 }
9424 std::fs::remove_dir_all(temp).ok();
9425 }
9426
9427 #[test]
9428 fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9429 let mut service = HarnessSessionService::new();
9430 let source = pi_locator();
9431 for (target, format) in [
9432 ("claude-code", SessionFormat::ClaudeCode),
9433 ("codex", SessionFormat::Codex),
9434 ("opencode", SessionFormat::OpenCode),
9435 ("pi", SessionFormat::Pi),
9436 ] {
9437 let result = service.handle(request(
9438 1,
9439 "harness.v1.sessions.handoff",
9440 json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9441 ));
9442 let artifact = &result["result"]["artifact"];
9443 let target_id = artifact["session_id"].as_str().unwrap();
9444 assert_ne!(target_id, source.session_id, "{target}");
9445 let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9446 assert_eq!(
9447 parsed.meta.session_id.as_deref(),
9448 Some(target_id),
9449 "{target}"
9450 );
9451 if target != "pi" {
9452 assert!(result["result"]["launch"]["arguments"]
9453 .as_array()
9454 .unwrap()
9455 .iter()
9456 .any(|argument| argument == target_id));
9457 }
9458 if target == "opencode" {
9459 assert!(target_id.starts_with("ses_"));
9460 fn assert_session_ids(value: &Value, target_id: &str) {
9461 match value {
9462 Value::Object(fields) => {
9463 if let Some(session_id) = fields.get("sessionID") {
9464 assert_eq!(session_id, target_id);
9465 }
9466 for child in fields.values() {
9467 assert_session_ids(child, target_id);
9468 }
9469 }
9470 Value::Array(values) => {
9471 for child in values {
9472 assert_session_ids(child, target_id);
9473 }
9474 }
9475 _ => {}
9476 }
9477 }
9478 let document: Value =
9479 serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9480 assert_session_ids(&document, target_id);
9481 }
9482 }
9483
9484 let first = service.handle(request(
9485 2,
9486 "harness.v1.sessions.handoff",
9487 json!({"locator": source, "target_harness": "codex"}),
9488 ));
9489 let second = service.handle(request(
9490 3,
9491 "harness.v1.sessions.handoff",
9492 json!({"locator": source, "target_harness": "codex"}),
9493 ));
9494 assert_ne!(
9495 first["result"]["artifact"]["session_id"],
9496 second["result"]["artifact"]["session_id"]
9497 );
9498 }
9499
9500 #[test]
9501 fn grok_handoff_materializes_through_the_core_door() {
9502 let mut service = HarnessSessionService::new();
9503 let source = opencode_locator();
9504 let response = service.handle(request(
9505 1,
9506 "harness.v1.sessions.handoff",
9507 json!({
9508 "locator": source,
9509 "target_harness": "grok",
9510 "cwd": "/tmp/grok-handoff-project",
9511 }),
9512 ));
9513 let result = &response["result"];
9514
9515 assert_eq!(result["artifact"]["target_harness"], "grok");
9518 let artifact = Session::load_str(
9519 result["artifact"]["content"].as_str().unwrap(),
9520 SessionFormat::Grok,
9521 )
9522 .unwrap();
9523 assert!(!artifact.messages.is_empty());
9524 let target_session_id = result["artifact"]["session_id"].as_str().unwrap();
9525 assert_eq!(target_session_id.len(), 36);
9526 assert_ne!(target_session_id, opencode_locator().session_id);
9527 assert!(result["materialize"].is_null());
9528 assert_eq!(
9529 result["launch"]["arguments"],
9530 json!(["--resume", "{materialized_session_id}"])
9531 );
9532 assert!(result["note"]
9533 .as_str()
9534 .unwrap()
9535 .contains("harness.v1.sessions.materialize"));
9536 }
9537
9538 #[tokio::test]
9539 async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9540 let mut service = HarnessSessionService::new();
9541 let inventory = service
9542 .handle_async(request(
9543 1,
9544 "harness.v1.harnesses.list",
9545 json!({"harnesses": ["missing"]}),
9546 ))
9547 .await;
9548 assert_eq!(inventory["error"]["code"], -32602);
9549
9550 let attached = service
9551 .handle_async(request(
9552 2,
9553 "harness.v1.runtimes.attach_existing",
9554 json!({"harness": "codex", "runtime_id": "thread-1"}),
9555 ))
9556 .await;
9557 assert_eq!(attached["error"]["code"], -32000);
9558 assert!(attached["error"]["message"]
9559 .as_str()
9560 .unwrap()
9561 .contains("runtimes.resume"));
9562 }
9563
9564 #[test]
9565 fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9566 let mut service = HarnessSessionService::new();
9567 let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9568 assert_eq!(invalid["error"]["code"], -32602);
9569 let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9570 assert_eq!(unknown["error"]["code"], -32601);
9571 }
9572
9573 #[cfg(unix)]
9574 #[tokio::test]
9575 #[allow(clippy::await_holding_lock)]
9578 async fn async_service_drives_a_generic_acp_runtime() {
9579 let _environment_guard = crate::live_runtime::test_environment_lock();
9580 let script = r#"
9581 i=0
9582 while IFS= read -r line; do
9583 i=$((i + 1))
9584 case "$i" in
9585 1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9586 2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9587 3)
9588 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9589 printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9590 ;;
9591 4)
9592 printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9593 printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9594 ;;
9595 esac
9596 done
9597 "#;
9598 let mut service = HarnessSessionService::new();
9599 let started = service
9600 .handle_async(request(
9601 1,
9602 "harness.v1.runtimes.start",
9603 json!({
9604 "harness": "codex",
9605 "protocol": "acp",
9606 "cwd": std::env::current_dir().unwrap(),
9607 "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9608 }),
9609 ))
9610 .await;
9611 assert_eq!(started["result"]["connection"], "runtime-1");
9612 assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9613
9614 let terminal = service
9615 .handle_async(request(
9616 9,
9617 "harness.v1.runtimes.terminal_instructions",
9618 json!({"connection":"runtime-1"}),
9619 ))
9620 .await;
9621 let arguments = terminal["result"]["launch"]["arguments"]
9622 .as_array()
9623 .expect("hosted runtime should return terminal arguments");
9624 let endpoint_index = arguments
9625 .iter()
9626 .position(|value| value == "--endpoint")
9627 .expect("terminal command should use an opaque endpoint");
9628 let endpoint = LiveRuntimeEndpoint::parse(
9629 arguments[endpoint_index + 1]
9630 .as_str()
9631 .expect("endpoint argument should be text"),
9632 )
9633 .unwrap();
9634 assert!(!terminal.to_string().contains("Bearer"));
9635 let workspace = std::env::current_dir().unwrap();
9636 let receipt = resolve_live_runtime(
9637 &endpoint,
9638 &LiveRuntimeSource {
9639 harness: "codex".into(),
9640 session_id: "svc_acp".into(),
9641 workspace,
9642 },
9643 )
9644 .unwrap();
9645 let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9646 .await
9647 .unwrap();
9648 let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9649 .await
9650 .unwrap();
9651
9652 let sent = service
9653 .handle_async(request(
9654 2,
9655 "harness.v1.runtimes.send_input",
9656 json!({"connection": "runtime-1", "text": "hi"}),
9657 ))
9658 .await;
9659 assert_eq!(sent["result"]["turn_id"], "3");
9660
9661 let mut events = Vec::new();
9662 for _ in 0..20 {
9663 events.extend(service.poll_runtimes().await);
9664 if events.len() >= 2 {
9665 break;
9666 }
9667 tokio::time::sleep(Duration::from_millis(2)).await;
9668 }
9669 assert!(events
9670 .iter()
9671 .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9672 assert!(events.iter().any(|event| {
9673 event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9674 }));
9675
9676 let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9677 loop {
9678 let event = attachment.next_event().await.unwrap();
9679 if event.kind == "text_delta" && event.payload["text"] == "ok" {
9680 break;
9681 }
9682 }
9683 })
9684 .await;
9685 assert!(
9686 saw_editor_reply.is_ok(),
9687 "terminal should observe the editor-driven turn"
9688 );
9689
9690 crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9691 .await
9692 .unwrap();
9693 let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9694 loop {
9695 let event = attachment.next_event().await.unwrap();
9696 if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9697 break;
9698 }
9699 }
9700 })
9701 .await;
9702 assert!(
9703 saw_terminal_reply.is_ok(),
9704 "terminal should drive the same runtime"
9705 );
9706
9707 let closed = service
9708 .handle_async(request(
9709 3,
9710 "harness.v1.runtimes.close",
9711 json!({"connection": "runtime-1"}),
9712 ))
9713 .await;
9714 assert_eq!(closed["result"]["closed"], true);
9715 }
9716
9717 #[test]
9722 fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9723 let home = connect_scratch_home("uni7-running");
9724
9725 assert!(probe_hermes_running(&home, 300_000).is_none());
9732
9733 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9736 let port = listener.local_addr().unwrap().port();
9737 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9738 std::fs::write(
9739 home.join(".openclaw/openclaw.json"),
9740 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9741 )
9742 .unwrap();
9743 let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9744 assert!(matches!(
9745 running.method,
9746 RunningInstanceMethod::GatewayConnect
9747 ));
9748 assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9749 drop(listener);
9750 let mut closed_detected = probe_openclaw_running(&home).is_some();
9755 for _ in 0..3 {
9756 if !closed_detected {
9757 break;
9758 }
9759 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9760 let port = listener.local_addr().unwrap().port();
9761 drop(listener);
9762 std::fs::write(
9763 home.join(".openclaw/openclaw.json"),
9764 format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9765 )
9766 .unwrap();
9767 closed_detected = probe_openclaw_running(&home).is_some();
9768 }
9769 assert!(
9770 !closed_detected,
9771 "a closed gateway must not read as running"
9772 );
9773
9774 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9776 let port = listener.local_addr().unwrap().port();
9777 std::fs::write(
9778 home.join(".openclaw/openclaw.json"),
9779 format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9780 )
9781 .unwrap();
9782 assert!(probe_openclaw_running(&home).is_some());
9783 drop(listener);
9784
9785 std::fs::create_dir_all(home.join(".hermes")).unwrap();
9787 let wal = home.join(".hermes/state.db-wal");
9788 std::fs::write(&wal, b"wal").unwrap();
9789 let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9790 assert!(matches!(
9791 running.method,
9792 RunningInstanceMethod::StoreWalActivity
9793 ));
9794 assert!(running.evidence.contains("state.db-wal"));
9795 let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9796 std::fs::File::options()
9797 .append(true)
9798 .open(&wal)
9799 .unwrap()
9800 .set_modified(stale)
9801 .unwrap();
9802 assert!(
9803 probe_hermes_running(&home, 300_000).is_none(),
9804 "a stale WAL (crash leftover) must not read as running"
9805 );
9806 }
9807
9808 fn connect_scratch_home(tag: &str) -> PathBuf {
9809 let dir = std::env::temp_dir().join(format!(
9810 "supercode-connect-service-{tag}-{}-{}",
9811 std::process::id(),
9812 std::time::SystemTime::now()
9813 .duration_since(std::time::UNIX_EPOCH)
9814 .unwrap()
9815 .as_nanos()
9816 ));
9817 std::fs::create_dir_all(&dir).unwrap();
9818 dir
9819 }
9820
9821 async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9825 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9826 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9827 let address = listener.local_addr().unwrap();
9828 let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9829 tokio::spawn(async move {
9830 loop {
9831 let Ok((mut stream, _)) = listener.accept().await else {
9832 break;
9833 };
9834 let request_sender = request_sender.clone();
9835 tokio::spawn(async move {
9836 let (reader, mut writer) = stream.split();
9837 let mut reader = BufReader::new(reader);
9838 let mut request_line = String::new();
9839 if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9840 return;
9841 }
9842 let request_line = request_line.trim_end().to_string();
9843 let mut authorization = String::new();
9844 let mut content_length = 0usize;
9845 loop {
9846 let mut line = String::new();
9847 if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9848 return;
9849 }
9850 let line = line.trim_end();
9851 if line.is_empty() {
9852 break;
9853 }
9854 let lower = line.to_ascii_lowercase();
9855 if let Some(value) = lower.strip_prefix("authorization:") {
9856 authorization = value.trim().to_string();
9857 }
9858 if let Some(value) = lower.strip_prefix("content-length:") {
9859 content_length = value.trim().parse().unwrap_or(0);
9860 }
9861 }
9862 if content_length > 0 {
9863 let mut body = vec![0u8; content_length];
9864 let _ = reader.read_exact(&mut body).await;
9865 }
9866 let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9867 if request_line.starts_with("GET /event") {
9868 let _ = writer
9869 .write_all(
9870 b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9871 )
9872 .await;
9873 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9874 return;
9875 }
9876 let body = if request_line.starts_with("POST /session") {
9877 r#"{"id":"mock-session"}"#
9878 } else {
9879 r#"{"status":"ok"}"#
9880 };
9881 let response = format!(
9882 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9883 body.len(),
9884 body
9885 );
9886 let _ = writer.write_all(response.as_bytes()).await;
9887 });
9888 }
9889 });
9890 (format!("http://{address}"), request_receiver)
9891 }
9892
9893 fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9894 crate::HarnessSupportDescriptor {
9895 orchestration: Default::default(),
9896 id: HarnessId::from(HarnessId::OPENCODE),
9897 display_name: "OpenCode".into(),
9898 native: crate::NativeSupport {
9899 discover: crate::ImplementationKind::Absent,
9900 load: crate::ImplementationKind::Absent,
9901 follow: crate::ImplementationKind::Absent,
9902 import: crate::ImplementationKind::Absent,
9903 export: crate::ImplementationKind::Absent,
9904 },
9905 runtime: crate::RuntimeSupport {
9906 implementation: crate::ImplementationKind::BuiltIn,
9907 protocol: protocol.into(),
9908 default_launch: None,
9909 connect_launch: Some(crate::RuntimeConnectLaunch {
9910 config_path: "~/opencode-tui.json".into(),
9911 address_pointer: "/server/url".into(),
9912 port_pointer: None,
9913 default_address: None,
9914 auth_pointer: Some("/server/token".into()),
9915 protocol: protocol.into(),
9916 }),
9917 capabilities: crate::RuntimeCapabilities {
9918 start_session: true,
9919 resume_session: true,
9920 attach_existing_process: true,
9921 send_input: true,
9922 stream_events: true,
9923 interrupt: true,
9924 steer: false,
9925 respond_to_requests: true,
9926 },
9927 },
9928 }
9929 }
9930
9931 #[tokio::test]
9932 async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9933 let (base_url, mut requests) = mock_opencode_endpoint().await;
9934 let home = connect_scratch_home("open");
9935 std::fs::write(
9936 home.join("opencode-tui.json"),
9937 format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9938 )
9939 .unwrap();
9940
9941 let descriptor = connect_descriptor("opencode-http-sse");
9942 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9943 assert!(backend.capabilities().attach_existing_process);
9944
9945 let connection = backend
9946 .start(crate::RuntimeStartRequest {
9947 cwd: home.clone(),
9948 launch: None,
9949 mcp_servers: Vec::new(),
9950 approval_policy: None,
9951 })
9952 .await
9953 .unwrap();
9954 let handle = connection.handle();
9955 assert_eq!(handle.runtime_id, "mock-session");
9956 match &handle.endpoint {
9957 crate::RuntimeEndpoint::Http {
9958 base_url: endpoint, ..
9959 } => assert_eq!(endpoint, &base_url),
9960 other => panic!("connect mode must join the running endpoint, got {other:?}"),
9961 }
9962
9963 let mut seen = Vec::new();
9964 while let Ok(line) = requests.try_recv() {
9965 seen.push(line);
9966 }
9967 assert!(seen
9968 .iter()
9969 .any(|line| line.starts_with("GET /global/health")
9970 && line.contains("bearer connect-secret")));
9971 assert!(seen.iter().any(
9972 |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9973 ));
9974 }
9975
9976 #[tokio::test]
9987 async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9988 let home = connect_scratch_home("openclaw");
9989 std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9990 std::fs::write(
9991 home.join(".openclaw/openclaw.json"),
9992 r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9993 )
9994 .unwrap();
9995 let script = home.join("openclaw");
9996 std::fs::write(
9997 &script,
9998 r#"#!/bin/sh
9999# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
10000[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
10001[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
10002[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
10003[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
10004while IFS= read -r line; do
10005 case "$line" in
10006 *'"initialize"'*)
10007 printf '%s
10008' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
10009 *'"session/resume"'*)
10010 printf '%s
10011' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
10012 *'"session/new"'*)
10013 printf '%s
10014' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
10015 *'"session/prompt"'*)
10016 printf '%s
10017' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
10018 printf '%s
10019' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
10020 esac
10021done
10022"#,
10023 )
10024 .unwrap();
10025 use std::os::unix::fs::PermissionsExt;
10026 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
10027
10028 let mut descriptor = crate::harness_support_registry()
10029 .harnesses
10030 .into_iter()
10031 .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
10032 .expect("openclaw must be registered");
10033 descriptor
10034 .runtime
10035 .connect_launch
10036 .as_mut()
10037 .unwrap()
10038 .config_path = "~/.openclaw/openclaw.json".into();
10039 descriptor.runtime.default_launch.as_mut().unwrap().program =
10040 script.to_string_lossy().into_owned();
10041 let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10042 assert!(backend.capabilities().resume_session);
10043
10044 let joined = backend
10045 .attach(crate::RuntimeAttachRequest {
10046 runtime_id: "agent:main:main".into(),
10047 cwd: Some(home.clone()),
10048 launch: None,
10049 mcp_servers: Vec::new(),
10050 approval_policy: None,
10051 })
10052 .await;
10053 let mut connection = joined.expect("mock bridge attach must succeed");
10054 assert_eq!(connection.handle().runtime_id, "agent:main:main");
10055 let turn = connection
10056 .send_input(crate::RuntimeInput {
10057 text: "hello".into(),
10058 image_urls: Vec::new(),
10059 })
10060 .await;
10061 assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
10062 connection.close().await.unwrap();
10063 }
10064
10065 #[tokio::test]
10066 async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
10067 let home = connect_scratch_home("fail");
10068 std::fs::write(
10069 home.join("opencode-tui.json"),
10070 r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
10071 )
10072 .unwrap();
10073
10074 let gateway_only = connect_descriptor("acp-v1-jsonrpc");
10075 let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
10076 panic!("an ACP connect endpoint has no gateway client yet");
10077 };
10078 let message = format!("{error:?}");
10079 assert!(message.contains("acp-v1-jsonrpc"));
10080 assert!(!message.contains("connect-secret"));
10081
10082 let unreadable = connect_descriptor("opencode-http-sse");
10083 let missing_home = connect_scratch_home("missing");
10084 let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
10085 panic!("an unreadable connect config must fail closed");
10086 };
10087 let message = format!("{error:?}");
10088 assert!(message.contains("opencode-tui.json"));
10089 assert!(!message.contains("connect-secret"));
10090 }
10091
10092 fn jobs_fixture_root() -> PathBuf {
10097 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10098 }
10099
10100 fn jobs_fixture_homes() -> Value {
10103 let root = jobs_fixture_root();
10104 json!({
10105 "claude_code": root.join("claude_jobs_home/projects"),
10106 "hermes": root.join("hermes_home/state.db"),
10107 "openclaw": root.join("openclaw_home"),
10108 })
10109 }
10110
10111 fn jobs_list(params: Value) -> Value {
10112 let mut service = HarnessSessionService::new();
10113 service.handle(request(1, "harness.v1.jobs.list", params))
10114 }
10115
10116 fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10117 result["jobs"]
10118 .as_array()
10119 .expect("jobs is an array")
10120 .iter()
10121 .find(|job| job["id"] == id)
10122 .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10123 }
10124
10125 #[test]
10126 fn gateway_health_derives_from_running_probe_and_install_state() {
10127 let running = RunningInstance {
10128 method: RunningInstanceMethod::GatewayConnect,
10129 evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10130 checked_at_ms: 1,
10131 };
10132 let up = gateway_health(
10133 HarnessId::OPENCLAW,
10134 true,
10135 Some(&running),
10136 Some("2026.7.1-2"),
10137 );
10138 assert_eq!(up.state, GatewayState::Up);
10139 assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10140 assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10141 let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10145 std::fs::create_dir_all(&dir).unwrap();
10146 let fake = dir.join("hermes");
10147 let write_fake = |body: &str| {
10148 std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10149 #[cfg(unix)]
10150 {
10151 use std::os::unix::fs::PermissionsExt;
10152 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10153 }
10154 };
10155 write_fake("echo '✗ Gateway service is not installed'");
10156 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10157 *slot.borrow_mut() = Some((
10158 HarnessId::HERMES.to_string(),
10159 fake.to_string_lossy().into_owned(),
10160 ))
10161 });
10162 let down = gateway_health(HarnessId::HERMES, true, None, None);
10163 assert_eq!(down.state, GatewayState::Down, "{down:?}");
10164 assert!(down.endpoint.is_none());
10165 assert!(down.evidence.contains("not installed"));
10166 write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10167 let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10168 assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10169 assert!(idle_but_up.evidence.contains("PID 4242"));
10170 write_fake("echo 'something unparseable'");
10171 let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10172 assert_eq!(no_verdict.state, GatewayState::Down);
10173 assert!(no_verdict.evidence.contains("no verdict"));
10174 crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10175 let absent = gateway_health(HarnessId::HERMES, false, None, None);
10176 assert_eq!(absent.state, GatewayState::Unknown);
10177 let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10178 assert_eq!(core.state, GatewayState::Unknown);
10179 assert!(core.evidence.contains("per session"));
10180 }
10181
10182 #[test]
10183 fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10184 let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10185 let rows = response["result"]["triggers"]
10186 .as_array()
10187 .expect("triggers")
10188 .clone();
10189 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10190 assert!(
10191 hermes.iter().any(|r| r["name"] == "deploys"
10192 && r["route"] == "/webhooks/deploys"
10193 && r["kind"] == "webhook"),
10194 "{rows:#?}"
10195 );
10196 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10197 assert!(openclaw
10198 .iter()
10199 .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10200 assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10201 && r["kind"] == "hook_mapping"
10202 && r["target"]["action"] == "agent"));
10203 let rendered = response.to_string();
10204 for secret in [
10205 "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10206 "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10207 ] {
10208 assert!(!rendered.contains(secret), "{rendered}");
10209 }
10210 let refused =
10211 triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10212 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10213 }
10214
10215 fn triggers_list(params: Value) -> Value {
10216 let mut service = HarnessSessionService::new();
10217 service.handle(request(1, "harness.v1.triggers.list", params))
10218 }
10219
10220 #[test]
10221 fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10222 let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10223 let rows = response["result"]["routes"]
10224 .as_array()
10225 .expect("routes")
10226 .clone();
10227 let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10228 assert_eq!(hermes.len(), 2, "{rows:#?}");
10229 assert_eq!(hermes[0]["target"], "coder");
10230 assert_eq!(hermes[0]["match"]["platform"], "slack");
10231 assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10232 assert_eq!(hermes[0]["specificity"], 4);
10233 assert_eq!(hermes[1]["default"], true);
10234 let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10235 assert!(
10236 openclaw.iter().any(|r| r["target"] == "design"
10237 && r["match"]["platform"] == "slack"
10238 && r["specificity"] == 1),
10239 "{openclaw:#?}"
10240 );
10241 assert!(openclaw.iter().any(|r| r["default"] == true));
10242 let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10244 assert_eq!(refused["error"]["code"], -32020, "{refused}");
10245 }
10246
10247 fn routes_list(params: Value) -> Value {
10248 let mut service = HarnessSessionService::new();
10249 service.handle(request(1, "harness.v1.routes.list", params))
10250 }
10251
10252 #[test]
10253 fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10254 let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10255 let result = &response["result"];
10256 let ids: Vec<&str> = result["jobs"]
10257 .as_array()
10258 .unwrap()
10259 .iter()
10260 .map(|job| job["id"].as_str().unwrap())
10261 .collect();
10262 assert_eq!(
10263 ids,
10264 vec![
10265 "release-watch",
10266 "toolu_wake_recheck",
10267 "digest-15m",
10268 "nightly-audit",
10269 "coder-standup",
10270 "ops-once-boot",
10271 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10272 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10273 "cron_standup",
10274 "cron_reindex",
10275 ],
10276 "{result}"
10277 );
10278
10279 let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10283 assert_eq!(health["harness"], "openclaw");
10284 assert_eq!(health["schedule"]["kind"], "interval");
10285 assert_eq!(health["schedule"]["minutes"], 10.0);
10286 assert_eq!(health["session_target"], "isolated");
10287 assert_eq!(health["payload"]["kind"], "prompt");
10288 assert_eq!(health["payload"]["text"], "nightly health check");
10289 assert_eq!(health["deliver"]["mode"], "announce");
10293 assert_eq!(health["deliver"]["target"], "last");
10294 assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10295 let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10296 assert_eq!(digest["schedule"]["kind"], "cron");
10297 assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10298 assert_eq!(digest["session_target"], "main");
10299 assert_eq!(digest["payload"]["kind"], "system_event");
10300
10301 let cron = job_row(result, "release-watch");
10303 assert_eq!(cron["harness"], "claude-code");
10304 assert_eq!(cron["scope"], "session");
10305 assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10306 assert_eq!(cron["schedule"]["kind"], "cron");
10307 assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10308 assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10309 assert_eq!(cron["payload"]["kind"], "prompt");
10310 assert_eq!(cron["recurring"], true);
10311 assert_eq!(cron["deliver"]["target"], "session");
10312 let wakeup = job_row(result, "toolu_wake_recheck");
10313 assert_eq!(wakeup["payload"]["kind"], "wakeup");
10314 assert_eq!(wakeup["schedule"]["kind"], "once");
10315 assert_eq!(wakeup["recurring"], false);
10316 assert_eq!(wakeup["state"], "pending");
10317
10318 let interval = job_row(result, "digest-15m");
10320 assert_eq!(interval["harness"], "hermes");
10321 assert_eq!(interval["scope"], "install");
10322 assert_eq!(interval["profile"], Value::Null);
10323 assert_eq!(interval["schedule"]["kind"], "interval");
10324 assert_eq!(interval["schedule"]["minutes"], 15.0);
10325 assert_eq!(interval["schedule"]["display"], "every 15 min");
10326 assert_eq!(interval["deliver"]["target"], "origin");
10327 assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10328 assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10329 assert_eq!(interval["last_status"], "ok");
10330 let nightly = job_row(result, "nightly-audit");
10331 assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10332 assert_eq!(nightly["deliver"]["target"], "local");
10333 assert_eq!(nightly["enabled"], false);
10334 assert_eq!(nightly["state"], "paused");
10335 let profiled = job_row(result, "ops-once-boot");
10337 assert_eq!(profiled["profile"], "ops");
10338 assert_eq!(profiled["schedule"]["kind"], "once");
10339 assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10340 assert_eq!(profiled["payload"]["kind"], "script");
10341 assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10343 assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10344 assert_eq!(profiled["recurring"], false);
10345
10346 let standup_to_group = job_row(result, "coder-standup");
10349 assert_eq!(standup_to_group["deliver"]["target"], "origin");
10350 assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10351 assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10352 assert!(standup_to_group["deliver"]["mode"].is_null());
10354 assert!(standup_to_group["deliver"]["account"].is_null());
10355
10356 let standup = job_row(result, "cron_standup");
10359 assert_eq!(standup["harness"], "openclaw");
10360 assert_eq!(standup["session_target"], "isolated");
10361 assert_eq!(standup["deliver"]["mode"], "announce");
10362 assert_eq!(standup["deliver"]["target"], "slack");
10363 assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10364 assert_eq!(standup["payload"]["kind"], "prompt");
10365 assert_eq!(standup["profile"], "main");
10366 let reindex = job_row(result, "cron_reindex");
10367 assert_eq!(reindex["session_target"], "main");
10368 assert_eq!(reindex["payload"]["kind"], "system_event");
10369 assert_eq!(reindex["schedule"]["kind"], "interval");
10370 assert_eq!(reindex["schedule"]["display"], "every 240 min");
10371 assert_eq!(reindex["enabled"], false);
10372
10373 let states: Vec<(&str, &str)> = result["sources"]
10375 .as_array()
10376 .unwrap()
10377 .iter()
10378 .map(|source| {
10379 (
10380 source["harness"].as_str().unwrap(),
10381 source["state"].as_str().unwrap(),
10382 )
10383 })
10384 .collect();
10385 assert_eq!(
10389 states,
10390 vec![
10391 ("claude-code", "scanned"),
10392 ("hermes", "read"),
10393 ("hermes", "absent_store"),
10394 ("hermes", "read"),
10395 ("openclaw", "read"),
10396 ("openclaw", "read"),
10397 ],
10398 "{result}"
10399 );
10400 }
10401
10402 #[test]
10403 fn jobs_list_filters_by_harness_session_and_profile() {
10404 let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10405 let ids: Vec<&str> = by_harness["result"]["jobs"]
10406 .as_array()
10407 .unwrap()
10408 .iter()
10409 .map(|job| job["id"].as_str().unwrap())
10410 .collect();
10411 assert_eq!(
10412 ids,
10413 vec![
10414 "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10415 "8bb7d938-ca46-4a6d-90eb-c92331155566",
10416 "cron_standup",
10417 "cron_reindex",
10418 ]
10419 );
10420
10421 let by_session = jobs_list(json!({
10422 "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10423 "homes": jobs_fixture_homes(),
10424 }));
10425 let jobs = by_session["result"]["jobs"].as_array().unwrap();
10426 assert_eq!(jobs.len(), 2, "{by_session}");
10427 assert!(jobs
10428 .iter()
10429 .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10430
10431 let by_profile = jobs_list(json!({
10432 "harness": "hermes",
10433 "profile": "ops",
10434 "homes": jobs_fixture_homes(),
10435 }));
10436 let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10437 assert_eq!(jobs.len(), 1, "{by_profile}");
10438 assert_eq!(jobs[0]["id"], "ops-once-boot");
10439 }
10440
10441 #[test]
10442 fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10443 let mut service = HarnessSessionService::new();
10444 let hermes = service.handle(request(
10445 1,
10446 "harness.v1.jobs.get",
10447 json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10448 ));
10449 assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10450 assert_eq!(hermes["result"]["source"]["provider"], "nous");
10452 assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10453
10454 let claude = service.handle(request(
10455 2,
10456 "harness.v1.jobs.get",
10457 json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10458 ));
10459 assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10460 assert_eq!(
10461 claude["result"]["source"]["tool_use_id"],
10462 "toolu_cron_release_watch"
10463 );
10464
10465 let missing = service.handle(request(
10466 3,
10467 "harness.v1.jobs.get",
10468 json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10469 ));
10470 assert!(missing["error"]["message"]
10471 .as_str()
10472 .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10473 }
10474
10475 #[test]
10476 fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10477 let mut service = HarnessSessionService::new();
10478 for (id, method, params) in [
10479 (
10480 1,
10481 "harness.v1.jobs.list",
10482 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10483 ),
10484 (
10485 2,
10486 "harness.v1.jobs.get",
10487 json!({"harness": "codex", "id": "anything"}),
10488 ),
10489 ] {
10490 let response = service.handle(request(id, method, params));
10491 assert_eq!(response["error"]["code"], -32020, "{response}");
10492 assert!(response["error"]["message"]
10493 .as_str()
10494 .is_some_and(|message| message.contains("has no scheduled jobs")));
10495 assert!(response.get("result").is_none());
10496 }
10497 }
10498
10499 #[test]
10500 fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10501 let scratch = std::env::temp_dir().join(format!(
10502 "supercode-jobs-migrated-{}-{}",
10503 std::process::id(),
10504 generated_session_id()
10505 ));
10506 std::fs::create_dir_all(&scratch).unwrap();
10507 let response = jobs_list(json!({
10508 "harness": "openclaw",
10509 "homes": {"openclaw": scratch.clone()},
10510 }));
10511 let result = &response["result"];
10512 assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10513 assert_eq!(result["sources"][0]["state"], "absent_store");
10514 assert_eq!(result["sources"][0]["harness"], "openclaw");
10515 std::fs::remove_dir_all(&scratch).ok();
10516 }
10517
10518 const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10527 const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10529
10530 fn runs_list(params: Value) -> Value {
10531 let mut service = HarnessSessionService::new();
10532 service.handle(request(1, "harness.v1.runs.list", params))
10533 }
10534
10535 fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10536 result["runs"]
10537 .as_array()
10538 .expect("runs is an array")
10539 .iter()
10540 .find(|run| run["id"] == id)
10541 .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10542 }
10543
10544 #[test]
10545 fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10546 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10547 let result = &response["result"];
10548 let ids: Vec<&str> = result["runs"]
10549 .as_array()
10550 .expect("runs is an array")
10551 .iter()
10552 .map(|run| run["id"].as_str().unwrap())
10553 .collect();
10554 let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10555 assert_eq!(
10556 ids,
10557 vec![
10558 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10560 "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10561 "c3d4e5f60718293a4b5c6d7e8f901234",
10562 "f60718293a4b5c6d7e8f901234567890",
10563 "e5f60718293a4b5c6d7e8f9012345678",
10564 "d4e5f60718293a4b5c6d7e8f90123456",
10565 "run_health_0002",
10567 digest_fire.as_str(),
10568 "run_health_0001",
10569 ],
10570 "{result}"
10571 );
10572
10573 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10576 assert_eq!(failed["harness"], "hermes");
10577 assert_eq!(failed["job_id"], "job42");
10578 assert_eq!(failed["status"], "failed");
10579 assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10580 assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10581
10582 let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10585 assert_eq!(abandoned["status"], "unknown");
10586 assert_eq!(abandoned["job_id"], "ops-once-boot");
10587
10588 let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10590 assert_eq!(running["status"], "running");
10591 assert!(running["finished_at"].is_null(), "{running}");
10592 assert!(running["session_id"].is_null(), "{running}");
10593
10594 let ok = run_row(result, "run_health_0001");
10597 assert_eq!(ok["harness"], "openclaw");
10598 assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10599 assert_eq!(ok["status"], "ok");
10600 assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10601 assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10602 assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10603 assert!(ok["claimed_at"].is_null(), "{ok}");
10605
10606 assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10609
10610 for id in [
10615 "b2c3d4e5f60718293a4b5c6d7e8f9012",
10616 "d4e5f60718293a4b5c6d7e8f90123456",
10617 ] {
10618 assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10619 }
10620
10621 let sources = result["sources"].as_array().unwrap();
10624 let states: Vec<(&str, &str)> = sources
10625 .iter()
10626 .map(|source| {
10627 (
10628 source["harness"].as_str().unwrap(),
10629 source["state"].as_str().unwrap(),
10630 )
10631 })
10632 .collect();
10633 assert_eq!(
10634 states,
10635 vec![
10636 ("hermes", "read"),
10637 ("hermes", "absent_store"),
10638 ("hermes", "read"),
10639 ("openclaw", "read"),
10640 ],
10641 "{result}"
10642 );
10643 assert_eq!(sources[2]["profile"], "ops");
10644 assert!(sources[3]["path"]
10645 .as_str()
10646 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10647 }
10648
10649 #[test]
10650 fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10651 let response = runs_list(json!({
10652 "harness": "hermes",
10653 "job": "job42",
10654 "homes": jobs_fixture_homes(),
10655 }));
10656 let result = &response["result"];
10657 assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10658
10659 let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10663 assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10664
10665 let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10669 assert!(failed["session_id"].is_null(), "{failed}");
10670 }
10671
10672 #[test]
10676 fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10677 let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10678 let result = &response["result"];
10679
10680 let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10684 assert_eq!(delivered["status"], "completed");
10685 assert_eq!(delivered["delivery"]["state"], "delivered");
10686 assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10687 assert_eq!(delivered["delivery"]["attempts"], 1);
10688 assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10689 assert_eq!(
10690 delivered["delivery"]["delivered_at"],
10691 "2026-09-02T09:00:30.400Z"
10692 );
10693
10694 let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10697 assert_eq!(undelivered["status"], "completed");
10698 assert_eq!(undelivered["delivery"]["state"], "failed");
10699 assert_eq!(undelivered["delivery"]["attempts"], 3);
10700 assert_eq!(
10701 undelivered["delivery"]["last_error"],
10702 "telegram send failed: Bad Request: chat not found"
10703 );
10704 assert!(
10707 undelivered["delivery"]["delivered_at"].is_null(),
10708 "{undelivered}"
10709 );
10710
10711 let announced = run_row(result, "run_health_0001");
10714 assert_eq!(announced["delivery"]["state"], "delivered");
10715 assert_eq!(announced["delivery"]["target"], "last");
10716 assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10718 assert!(
10719 announced["delivery"]["delivered_at"].is_null(),
10720 "{announced}"
10721 );
10722 let refused = run_row(result, "run_health_0002");
10723 assert_eq!(refused["delivery"]["state"], "not-delivered");
10724 assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10725
10726 let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10729 assert!(skipped["delivery"].is_null(), "{skipped}");
10730 }
10731
10732 #[test]
10737 fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10738 let scratch = std::env::temp_dir().join(format!(
10739 "supercode-runs-delivery-{}-{}",
10740 std::process::id(),
10741 generated_session_id()
10742 ));
10743 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10744 let fixture = jobs_fixture_root().join("hermes_home");
10745 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10746 for name in ["cron/executions.db", "cron/jobs.json"] {
10747 std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10748 }
10749 {
10750 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10751 connection
10754 .execute(
10755 "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10756 [],
10757 )
10758 .unwrap();
10759 connection
10762 .execute(
10763 "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10764 ('cron_coder-standup_20260902_090010', 'cron', \
10765 'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10766 [],
10767 )
10768 .unwrap();
10769 }
10770 let response = runs_list(json!({
10771 "harness": "hermes",
10772 "job": "coder-standup",
10773 "homes": {"hermes": scratch.join("state.db")},
10774 }));
10775 let result = &response["result"];
10776 let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10777 assert_eq!(
10778 matched["session_id"], "cron_coder-standup_20260902_090010",
10779 "{result}"
10780 );
10781 assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10782 assert_eq!(
10783 matched["delivery"]["target"], "slack:C0FALLBACK:55",
10784 "{result}"
10785 );
10786 std::fs::remove_dir_all(&scratch).ok();
10787 }
10788
10789 #[test]
10790 fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10791 let scratch = std::env::temp_dir().join(format!(
10796 "supercode-runs-compressed-{}-{}",
10797 std::process::id(),
10798 generated_session_id()
10799 ));
10800 std::fs::create_dir_all(scratch.join("cron")).unwrap();
10801 let fixture = jobs_fixture_root().join("hermes_home");
10802 std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10803 std::fs::copy(
10804 fixture.join("cron/executions.db"),
10805 scratch.join("cron/executions.db"),
10806 )
10807 .unwrap();
10808 {
10809 let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10810 connection
10811 .execute(
10812 "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10813 ["cron_job42_20260902_120000"],
10814 )
10815 .unwrap();
10816 connection
10817 .execute(
10818 "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10819 VALUES ('job42-after-compaction', 'cron', \
10820 'cron_job42_20260902_120000', 1788350000.0)",
10821 [],
10822 )
10823 .unwrap();
10824 }
10825 let response = runs_list(json!({
10826 "harness": "hermes",
10827 "job": "job42",
10828 "homes": {"hermes": scratch.join("state.db")},
10829 }));
10830 let result = &response["result"];
10831 assert_eq!(
10832 run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10833 "job42-after-compaction",
10834 "{result}"
10835 );
10836 std::fs::remove_dir_all(&scratch).ok();
10837 }
10838
10839 #[test]
10840 fn runs_list_filters_by_job_and_caps_by_limit() {
10841 let by_job = runs_list(json!({
10842 "harness": "openclaw",
10843 "job": OPENCLAW_HEALTH_JOB,
10844 "homes": jobs_fixture_homes(),
10845 }));
10846 let ids: Vec<&str> = by_job["result"]["runs"]
10847 .as_array()
10848 .unwrap()
10849 .iter()
10850 .map(|run| run["id"].as_str().unwrap())
10851 .collect();
10852 assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10853
10854 let capped = runs_list(json!({
10855 "harness": "openclaw",
10856 "limit": 1,
10857 "homes": jobs_fixture_homes(),
10858 }));
10859 let runs = capped["result"]["runs"].as_array().unwrap();
10860 assert_eq!(runs.len(), 1, "{capped}");
10861 assert_eq!(runs[0]["id"], "run_health_0002");
10863 }
10864
10865 #[test]
10866 fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10867 let mut service = HarnessSessionService::new();
10868 let hermes = service.handle(request(
10869 1,
10870 "harness.v1.runs.get",
10871 json!({
10872 "harness": "hermes",
10873 "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10874 "homes": jobs_fixture_homes(),
10875 }),
10876 ));
10877 assert_eq!(hermes["result"]["run"]["status"], "completed");
10878 assert_eq!(
10879 hermes["result"]["run"]["session_id"],
10880 "cron_job42_20260902_120000"
10881 );
10882 assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10884 assert_eq!(hermes["result"]["source"]["pid"], 4242);
10885 assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10886
10887 let openclaw = service.handle(request(
10888 2,
10889 "harness.v1.runs.get",
10890 json!({
10891 "harness": "openclaw",
10892 "id": "run_health_0002",
10893 "homes": jobs_fixture_homes(),
10894 }),
10895 ));
10896 assert_eq!(openclaw["result"]["run"]["status"], "error");
10897 assert_eq!(
10900 openclaw["result"]["source"]["delivery_status"],
10901 "not-delivered"
10902 );
10903 assert_eq!(
10904 openclaw["result"]["source"]["delivery_error"],
10905 "channel_not_found"
10906 );
10907 assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10908 assert_eq!(
10909 openclaw["result"]["run"]["delivery"]["state"],
10910 "not-delivered"
10911 );
10912 assert_eq!(
10913 openclaw["result"]["run"]["delivery"]["last_error"],
10914 "channel_not_found"
10915 );
10916
10917 let missing = service.handle(request(
10918 3,
10919 "harness.v1.runs.get",
10920 json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10921 ));
10922 assert!(missing["error"]["message"]
10923 .as_str()
10924 .is_some_and(|message| message.contains("no run `no-such-run`")));
10925 }
10926
10927 #[test]
10928 fn runs_refuse_a_harness_that_keeps_no_run_store() {
10929 let mut service = HarnessSessionService::new();
10930 for (id, method, params) in [
10931 (
10934 1,
10935 "harness.v1.runs.list",
10936 json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10937 ),
10938 (
10939 2,
10940 "harness.v1.runs.get",
10941 json!({"harness": "claude-code", "id": "anything"}),
10942 ),
10943 (
10944 3,
10945 "harness.v1.runs.list",
10946 json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10947 ),
10948 ] {
10949 let response = service.handle(request(id, method, params));
10950 assert_eq!(response["error"]["code"], -32020, "{response}");
10951 assert!(response["error"]["message"]
10952 .as_str()
10953 .is_some_and(|message| message.contains("keeps no run store")));
10954 assert!(response.get("result").is_none());
10955 }
10956 }
10957
10958 #[test]
10959 fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10960 let scratch = std::env::temp_dir().join(format!(
10961 "supercode-runs-empty-{}-{}",
10962 std::process::id(),
10963 generated_session_id()
10964 ));
10965 std::fs::create_dir_all(&scratch).unwrap();
10966 let response = runs_list(json!({
10967 "harness": "openclaw",
10968 "homes": {"openclaw": scratch.clone()},
10969 }));
10970 let result = &response["result"];
10971 assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10972 assert_eq!(result["sources"][0]["state"], "absent_store");
10973 assert!(result["sources"][0]["path"]
10974 .as_str()
10975 .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10976 std::fs::remove_dir_all(&scratch).ok();
10977 }
10978}