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