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