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