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