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