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