1use std::collections::{BTreeMap, HashMap};
33use std::path::{Path, PathBuf};
34use std::time::{Duration, Instant};
35
36use serde::{Deserialize, Serialize};
37use serde_json::{json, Value};
38
39use crate::claude_peer::{read_registry, registry_dir, ClaudePeerSession};
40use crate::mailbox::{mail_root, Envelope, MailAddress, MailKind, ReplyVia};
41use crate::HarnessHomes;
42
43pub const RELAY_MODEL: &str = "haiku";
46
47pub const RELAY_NAME_PREFIX: &str = "sc-";
49
50pub const RELAY_STATUS_LINE: &str = "relay, not the session's status";
53
54pub const SEND_TIMEOUT: Duration = Duration::from_secs(90);
56
57const RELAY_TOOLS: &str = "ListAgents,SendMessage";
59
60const NATIVE_PEER_PREAMBLE: &str = "Another Claude session sent a message:";
64const NATIVE_PEER_OPENING: &str = "<cross-session-message ";
66const NATIVE_PEER_CLOSING: &str = "</cross-session-message>";
67const NATIVE_IDLE_NOTICE: &str = "[Cross-session idle notice]";
68const NATIVE_DELIVERY_NOTICE: &str = "[Cross-session delivery notice]";
69
70pub fn relay_name(name: &str, machine: &str) -> String {
75 let base = name.strip_prefix(RELAY_NAME_PREFIX).unwrap_or(name);
76 format!("{RELAY_NAME_PREFIX}{base}-on-{machine}")
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct QueuedSend {
82 pub to: String,
84 pub message: String,
86}
87
88pub fn gate_decision(hook_input: &Value, queued: Option<&QueuedSend>) -> Option<String> {
94 let tool = hook_input
95 .get("tool_name")
96 .and_then(Value::as_str)
97 .unwrap_or_default();
98 if tool == "ListAgents" {
99 return None;
100 }
101 if tool != "SendMessage" {
102 return Some(format!("a relay may not use {tool}"));
103 }
104 let Some(queued) = queued else {
105 return Some(
106 "nothing is queued to send; mail for the host is not answered by the relay".into(),
107 );
108 };
109 let input = hook_input.get("tool_input").cloned().unwrap_or(Value::Null);
110 let to = input.get("to").and_then(Value::as_str).unwrap_or_default();
111 let message = input
112 .get("message")
113 .and_then(Value::as_str)
114 .unwrap_or_default();
115 if to != queued.to {
116 return Some(format!(
117 "not the queued outbound message: `to` is {to:?}, the queue says {:?}",
118 queued.to
119 ));
120 }
121 if message != queued.message {
122 let at = message
123 .char_indices()
124 .zip(queued.message.chars())
125 .find(|((_, sent), queued)| sent != queued)
126 .map(|((index, _), _)| index)
127 .unwrap_or_else(|| message.len().min(queued.message.len()));
128 return Some(format!(
129 "not the queued outbound message: `message` ({} bytes) differs from the queue ({} \
130 bytes) at byte {at}: sent {:?}, queued {:?}",
131 message.len(),
132 queued.message.len(),
133 message
134 .get(at..)
135 .unwrap_or_default()
136 .chars()
137 .take(40)
138 .collect::<String>(),
139 queued
140 .message
141 .get(at..)
142 .unwrap_or_default()
143 .chars()
144 .take(40)
145 .collect::<String>(),
146 ));
147 }
148 None
149}
150
151pub fn gate_denial(reason: &str) -> Value {
153 json!({
154 "hookSpecificOutput": {
155 "hookEventName": "PreToolUse",
156 "permissionDecision": "deny",
157 "permissionDecisionReason": reason,
158 }
159 })
160}
161
162#[derive(Debug, Clone)]
165pub struct RelayPaths {
166 pub directory: PathBuf,
168 pub queue: PathBuf,
170 pub receipt: PathBuf,
172 pub settings: PathBuf,
174 pub record: PathBuf,
176 pub sent: PathBuf,
178 pub lock: PathBuf,
180}
181
182impl RelayPaths {
183 pub fn new(root: &Path, relay_name: &str) -> Self {
185 let hash = blake3::hash(relay_name.as_bytes()).to_hex();
186 Self::in_directory(root.join("relays").join(&hash[..24]))
187 }
188
189 pub fn in_directory(directory: PathBuf) -> Self {
191 Self {
192 queue: directory.join("queue.json"),
193 receipt: directory.join("receipt.json"),
194 settings: directory.join("settings.json"),
195 record: directory.join("relay.json"),
196 sent: directory.join("sent.json"),
197 lock: directory.join("send.lock"),
198 directory,
199 }
200 }
201
202 pub fn last_sent(&self) -> HashMap<String, String> {
204 std::fs::read(&self.sent)
205 .ok()
206 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
207 .unwrap_or_default()
208 }
209}
210
211#[derive(Debug, Clone)]
213pub struct RelaySpec {
214 pub represented: MailAddress,
216 pub represented_name: String,
218 pub name: String,
220 pub paths: RelayPaths,
222 pub program: PathBuf,
224}
225
226impl RelaySpec {
227 pub fn for_sender(represented: &MailAddress, represented_name: &str) -> std::io::Result<Self> {
229 let base = represented_name
230 .split('@')
231 .next()
232 .unwrap_or(represented_name);
233 let name = relay_name(base, &represented.machine);
234 Ok(Self {
235 represented: represented.clone(),
236 represented_name: represented_name.to_string(),
237 paths: RelayPaths::new(&mail_root(), &name),
238 name,
239 program: supercode_program()?,
240 })
241 }
242}
243
244pub fn relay_arguments(spec: &RelaySpec) -> Vec<String> {
250 vec![
251 "--print".into(),
252 "--input-format".into(),
253 "stream-json".into(),
254 "--output-format".into(),
255 "stream-json".into(),
256 "--verbose".into(),
257 "--permission-prompt-tool".into(),
258 "stdio".into(),
259 "--model".into(),
260 RELAY_MODEL.into(),
261 "--name".into(),
262 spec.name.clone(),
263 "--permission-mode".into(),
264 "bypassPermissions".into(),
265 "--setting-sources".into(),
266 "project".into(),
267 "--settings".into(),
268 spec.paths.settings.to_string_lossy().into_owned(),
269 "--tools".into(),
270 RELAY_TOOLS.into(),
271 "--no-session-persistence".into(),
272 ]
273}
274
275pub fn relay_environment(spec: &RelaySpec, endpoint: &str) -> BTreeMap<String, String> {
279 let key = spec
280 .paths
281 .directory
282 .file_name()
283 .map(|name| name.to_string_lossy().into_owned())
284 .unwrap_or_default();
285 BTreeMap::from([
286 ("ANTHROPIC_BASE_URL".to_string(), endpoint.to_string()),
287 ("ANTHROPIC_API_KEY".to_string(), key),
288 (
289 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC".to_string(),
290 "1".to_string(),
291 ),
292 ])
293}
294
295pub fn relay_settings(spec: &RelaySpec) -> Value {
297 let program = shell_quote(&spec.program.to_string_lossy());
298 let directory = shell_quote(&spec.paths.directory.to_string_lossy());
299 let represented = shell_quote(&spec.represented.to_string());
300 let command = |verb: &str| format!("{program} message {verb} {directory}");
301 json!({
302 "hooks": {
303 "PreToolUse": [{
305 "matcher": ".*",
306 "hooks": [{"type": "command", "command": command("gate")}],
307 }],
308 "PostToolUse": [{
309 "matcher": "SendMessage",
310 "hooks": [{"type": "command", "command": command("relay-receipt")}],
311 }],
312 "PostToolUseFailure": [{
314 "matcher": "SendMessage",
315 "hooks": [{"type": "command", "command": command("relay-receipt")}],
316 }],
317 "Stop": [{
318 "hooks": [{"type": "command", "command": command("relay-receipt")}],
319 }],
320 "StopFailure": [{
322 "hooks": [{"type": "command", "command": command("relay-receipt")}],
323 }],
324 "UserPromptSubmit": [{
325 "hooks": [{
326 "type": "command",
327 "command": format!("{program} message relay-inbound {represented} {directory}"),
328 }],
329 }],
330 }
331 })
332}
333
334fn shell_quote(value: &str) -> String {
335 format!("'{}'", value.replace('\'', "'\\''"))
336}
337
338pub fn is_inbound_prompt(prompt: &str) -> bool {
341 let trimmed = prompt.trim_start();
342 trimmed.starts_with(NATIVE_PEER_OPENING)
343 || trimmed.starts_with(NATIVE_IDLE_NOTICE)
344 || trimmed.starts_with(NATIVE_DELIVERY_NOTICE)
345}
346
347pub fn inbound_block() -> Value {
349 json!({
350 "decision": "block",
351 "reason": "filed in the mailbox of the session this relay speaks for",
352 })
353}
354
355pub fn send_turn(send: &QueuedSend) -> String {
357 format!(
358 "Send one message with SendMessage.\n\
359 to: {}\n\
360 message: the exact text between the markers, without the markers\n\
361 ---BEGIN MESSAGE---\n{}\n---END MESSAGE---",
362 send.to, send.message
363 )
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct NativeEnvelope {
369 pub from: String,
371 pub from_name: Option<String>,
373 pub body: String,
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
379pub enum RelayEvent {
380 Peer(NativeEnvelope),
382 IdleNotice(String),
384 DeliveryNotice(String),
386}
387
388pub fn resolve_native_sender(
390 registry: &[ClaudePeerSession],
391 native_from: &str,
392) -> Option<ClaudePeerSession> {
393 let socket = native_from.strip_prefix("uds:").unwrap_or(native_from);
394 registry
395 .iter()
396 .find(|session| session.socket_path.to_string_lossy() == socket)
397 .cloned()
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402#[serde(rename_all = "snake_case", tag = "outcome")]
403pub enum RelayReceipt {
404 Delivered {
406 detail: String,
408 native_msg_id: Option<String>,
410 },
411 Failed {
413 detail: String,
415 },
416}
417
418pub fn read_receipt(value: &Value) -> RelayReceipt {
421 if let Some(reason) = value.get("denied").and_then(Value::as_str) {
422 return RelayReceipt::Failed {
423 detail: format!("the relay's gate refused the send: {reason}"),
424 };
425 }
426 if let Some(error) = value.get("send_failed").and_then(Value::as_str) {
427 return RelayReceipt::Failed {
428 detail: format!("Claude Code refused the relay's send: {}", error.trim()),
429 };
430 }
431 if value.get("turn_ended").and_then(Value::as_bool) == Some(true) {
432 let said = value
433 .get("said")
434 .and_then(Value::as_str)
435 .filter(|said| !said.trim().is_empty())
436 .map(|said| format!("; it said: {}", said.trim()))
437 .unwrap_or_default();
438 return RelayReceipt::Failed {
439 detail: format!(
440 "the Claude relay ended its turn without sending; nothing was sent{said}"
441 ),
442 };
443 }
444 let response = value.get("tool_response").cloned().unwrap_or(Value::Null);
445 let parsed = match &response {
446 Value::String(text) => serde_json::from_str::<Value>(text).unwrap_or(response.clone()),
447 Value::Array(blocks) => blocks
448 .iter()
449 .find_map(|block| block.get("text").and_then(Value::as_str))
450 .and_then(|text| serde_json::from_str::<Value>(text).ok())
451 .unwrap_or(response.clone()),
452 _ => response.clone(),
453 };
454 if parsed.get("success").and_then(Value::as_bool) != Some(true) {
455 return RelayReceipt::Failed {
456 detail: format!("Claude did not report the send as successful: {parsed}"),
457 };
458 }
459 RelayReceipt::Delivered {
460 detail: parsed
461 .get("message")
462 .and_then(Value::as_str)
463 .unwrap_or_default()
464 .to_string(),
465 native_msg_id: parsed
466 .get("msg_id")
467 .and_then(Value::as_str)
468 .map(str::to_string),
469 }
470}
471
472pub async fn send_through_relay(
477 sender: &MailAddress,
478 sender_name: &str,
479 to: &str,
480 message: String,
481 message_id: &str,
482) -> RelayReceipt {
483 let failed = |detail: String| RelayReceipt::Failed { detail };
484 let spec = match RelaySpec::for_sender(sender, sender_name) {
485 Ok(spec) => spec,
486 Err(error) => return failed(error.to_string()),
487 };
488 if let Err(error) = std::fs::create_dir_all(&spec.paths.directory) {
489 return failed(error.to_string());
490 }
491 let lock = match tokio::task::spawn_blocking({
493 let path = spec.paths.lock.clone();
494 move || SendLock::acquire(&path)
495 })
496 .await
497 {
498 Ok(Ok(lock)) => lock,
499 Ok(Err(error)) => return failed(format!("could not take the relay's send lock: {error}")),
500 Err(error) => return failed(error.to_string()),
501 };
502 let runtime = match ensure_relay_runtime(&spec).await {
503 Ok(runtime) => runtime,
504 Err(detail) => return failed(detail),
505 };
506 let queued = QueuedSend {
507 to: to.to_string(),
508 message,
509 };
510 std::fs::remove_file(&spec.paths.receipt).ok();
511 if let Err(error) = std::fs::write(
512 &spec.paths.queue,
513 serde_json::to_vec(&queued).unwrap_or_default(),
514 ) {
515 return failed(error.to_string());
516 }
517 let delivered =
518 crate::runtime_mail::deliver_to_runtime(&runtime, send_turn(&queued), true).await;
519 let receipt = match delivered {
520 Err(detail) => failed(format!("could not reach the Claude relay: {detail}")),
521 Ok(_) => wait_for_receipt(&spec.paths.receipt).await,
522 };
523 std::fs::remove_file(&spec.paths.queue).ok();
524 if matches!(receipt, RelayReceipt::Delivered { .. }) {
525 let mut sent = spec.paths.last_sent();
526 sent.insert(to.to_string(), message_id.to_string());
527 std::fs::write(
528 &spec.paths.sent,
529 serde_json::to_vec(&sent).unwrap_or_default(),
530 )
531 .ok();
532 }
533 drop(lock);
534 receipt
535}
536
537async fn wait_for_receipt(path: &Path) -> RelayReceipt {
538 let started = Instant::now();
539 while started.elapsed() < SEND_TIMEOUT {
540 if let Some(value) = std::fs::read(path)
541 .ok()
542 .and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
543 {
544 return read_receipt(&value);
545 }
546 tokio::time::sleep(Duration::from_millis(200)).await;
547 }
548 RelayReceipt::Failed {
549 detail: format!(
550 "the Claude relay did not confirm the send within {} seconds; it may still arrive",
551 SEND_TIMEOUT.as_secs()
552 ),
553 }
554}
555
556async fn ensure_relay_runtime(
559 spec: &RelaySpec,
560) -> Result<crate::live_runtime::LiveRuntimeRecord, String> {
561 #[derive(Serialize, Deserialize)]
562 struct Record {
563 runtime_id: String,
564 #[serde(default)]
565 endpoint: String,
566 }
567 let endpoint = relay_endpoint(&spec.program).await?;
568 std::fs::write(
571 &spec.paths.settings,
572 serde_json::to_vec_pretty(&relay_settings(spec)).unwrap_or_default(),
573 )
574 .map_err(|error| error.to_string())?;
575 if let Some(record) = std::fs::read(&spec.paths.record)
576 .ok()
577 .and_then(|bytes| serde_json::from_slice::<Record>(&bytes).ok())
578 {
579 if let Some(runtime) =
580 crate::runtime_mail::controlled_runtime("claude-code", &record.runtime_id)
581 {
582 if record.endpoint == endpoint {
583 return Ok(runtime);
584 }
585 end_relay_process(&spec.name);
588 }
589 }
590 let params = json!({
591 "harness": "claude-code",
592 "launch": {
593 "program": "claude",
594 "arguments": relay_arguments(spec),
595 "env": relay_environment(spec, &endpoint),
596 },
597 "cwd": spec.paths.directory,
598 });
599 let result = machine_rpc(&spec.program, "runtimes.start", ¶ms).await?;
600 let runtime_id = result
601 .pointer("/handle/runtime_id")
602 .and_then(Value::as_str)
603 .ok_or_else(|| format!("the machine daemon did not start the relay: {result}"))?
604 .to_string();
605 std::fs::write(
606 &spec.paths.record,
607 serde_json::to_vec(&Record {
608 runtime_id: runtime_id.clone(),
609 endpoint,
610 })
611 .unwrap_or_default(),
612 )
613 .map_err(|error| error.to_string())?;
614 crate::runtime_mail::controlled_runtime("claude-code", &runtime_id)
615 .ok_or_else(|| "the relay started but registered no live runtime".to_string())
616}
617
618fn end_relay_process(name: &str) {
620 let registry = registry_dir(&HarnessHomes::default());
621 for session in read_registry(®istry) {
622 #[cfg(unix)]
623 if session.name == name {
624 unsafe {
626 libc::kill(session.pid as libc::pid_t, libc::SIGTERM);
627 }
628 }
629 }
630}
631
632async fn relay_endpoint(program: &Path) -> Result<String, String> {
635 if let Some(url) = crate::relay_endpoint::relay_endpoint_url() {
636 return Ok(url);
637 }
638 ensure_machine_daemon(program).await?;
639 let deadline = Instant::now() + Duration::from_secs(10);
640 while Instant::now() < deadline {
641 if let Some(url) = crate::relay_endpoint::relay_endpoint_url() {
642 return Ok(url);
643 }
644 tokio::time::sleep(Duration::from_millis(200)).await;
645 }
646 Err(
647 "the relay endpoint is not answering; the machine daemon's `supercode message watch` \
648 serves it (see mail/machine-daemon.log)"
649 .into(),
650 )
651}
652
653pub async fn machine_rpc(program: &Path, method: &str, params: &Value) -> Result<Value, String> {
656 let call = || {
657 let mut command = tokio::process::Command::new(program);
658 command
659 .args(["teams", "rpc", method, ¶ms.to_string()])
660 .stdin(std::process::Stdio::null())
661 .stdout(std::process::Stdio::piped())
662 .stderr(std::process::Stdio::piped());
663 command.output()
664 };
665 let output = call().await.map_err(|error| error.to_string())?;
666 if output.status.success() {
667 return serde_json::from_slice(&output.stdout)
668 .map_err(|error| format!("unreadable answer from the machine daemon: {error}"));
669 }
670 ensure_machine_daemon(program).await?;
673 let output = call().await.map_err(|error| error.to_string())?;
674 if output.status.success() {
675 return serde_json::from_slice(&output.stdout)
676 .map_err(|error| format!("unreadable answer from the machine daemon: {error}"));
677 }
678 Err(error_line(&String::from_utf8_lossy(&output.stderr)))
679}
680
681pub async fn ensure_machine_daemon(program: &Path) -> Result<(), String> {
684 let describe = || {
685 tokio::process::Command::new(program)
686 .args(["teams", "describe"])
687 .stdin(std::process::Stdio::null())
688 .stdout(std::process::Stdio::null())
689 .stderr(std::process::Stdio::null())
690 .status()
691 };
692 if describe().await.is_ok_and(|status| status.success()) {
693 return Ok(());
694 }
695 let root = mail_root();
696 std::fs::create_dir_all(&root).map_err(|error| error.to_string())?;
697 let log = std::fs::OpenOptions::new()
698 .create(true)
699 .append(true)
700 .open(root.join("machine-daemon.log"))
701 .map_err(|error| error.to_string())?;
702 let mut command = std::process::Command::new(program);
703 command
704 .args(["teams", "machine", "start", "--supercode"])
705 .arg(program)
706 .stdin(std::process::Stdio::null())
707 .stdout(log.try_clone().map_err(|error| error.to_string())?)
708 .stderr(log);
709 #[cfg(unix)]
710 {
711 use std::os::unix::process::CommandExt;
712 command.process_group(0);
715 }
716 command
717 .spawn()
718 .map_err(|error| format!("could not start the machine daemon: {error}"))?;
719 for _ in 0..50 {
720 tokio::time::sleep(Duration::from_millis(200)).await;
721 if describe().await.is_ok_and(|status| status.success()) {
722 return Ok(());
723 }
724 }
725 Err(format!(
726 "the machine daemon did not start within 10 seconds; see {}",
727 root.join("machine-daemon.log").display()
728 ))
729}
730
731fn error_line(stderr: &str) -> String {
733 let lines: Vec<&str> = stderr
734 .lines()
735 .map(str::trim)
736 .filter(|line| !line.is_empty())
737 .collect();
738 lines
739 .iter()
740 .find(|line| line.starts_with("Error") || line.starts_with("error"))
741 .or(lines.last())
742 .copied()
743 .unwrap_or("the machine daemon failed without saying why")
744 .chars()
745 .take(300)
746 .collect()
747}
748
749struct SendLock {
751 _file: std::fs::File,
752}
753
754impl SendLock {
755 fn acquire(path: &Path) -> std::io::Result<Self> {
756 let file = std::fs::OpenOptions::new()
757 .create(true)
758 .truncate(false)
759 .write(true)
760 .open(path)?;
761 #[cfg(unix)]
762 {
763 use std::os::unix::io::AsRawFd;
764 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } != 0 {
767 return Err(std::io::Error::last_os_error());
768 }
769 }
770 Ok(Self { _file: file })
771 }
772}
773
774pub fn file_inbound_prompt(
777 homes: &HarnessHomes,
778 represented: &MailAddress,
779 prompt: &str,
780 last_sent: Option<&HashMap<String, String>>,
781) {
782 match parse_inbound_text(prompt) {
783 Some(RelayEvent::Peer(native)) => {
784 let registry = read_registry(®istry_dir(homes));
785 let sender = resolve_native_sender(®istry, &native.from);
786 let in_reply_to = sender
787 .as_ref()
788 .and_then(|session| last_sent.and_then(|sent| sent.get(&session.name).cloned()));
789 file_inbound(represented, &native, sender.as_ref(), in_reply_to);
790 }
791 Some(RelayEvent::IdleNotice(text) | RelayEvent::DeliveryNotice(text)) => {
792 file_notice(represented, &text);
793 }
794 _ => {}
795 }
796}
797pub fn parse_inbound_text(text: &str) -> Option<RelayEvent> {
800 let trimmed = text.trim_start();
801 if trimmed.starts_with(NATIVE_IDLE_NOTICE) {
802 return Some(RelayEvent::IdleNotice(trimmed.to_string()));
803 }
804 if trimmed.starts_with(NATIVE_DELIVERY_NOTICE) {
805 return Some(RelayEvent::DeliveryNotice(trimmed.to_string()));
806 }
807 if !trimmed.starts_with(NATIVE_PEER_PREAMBLE) && !trimmed.starts_with(NATIVE_PEER_OPENING) {
808 return None;
809 }
810 let start = text.find(NATIVE_PEER_OPENING)?;
811 let header_end = start + text[start..].find(">\n")?;
812 let header = &text[start + NATIVE_PEER_OPENING.len()..header_end];
813 let body_start = header_end + 2;
814 let body_end = text.rfind(&format!("\n{NATIVE_PEER_CLOSING}"))?;
817 if body_end < body_start {
818 return None;
819 }
820 let attributes = parse_attributes(header);
821 Some(RelayEvent::Peer(NativeEnvelope {
822 from: attributes.get("from")?.clone(),
823 from_name: attributes.get("from-name").cloned(),
824 body: text[body_start..body_end].to_string(),
825 }))
826}
827fn parse_attributes(header: &str) -> BTreeMap<String, String> {
828 let mut attributes = BTreeMap::new();
829 let mut rest = header;
830 while let Some(equals) = rest.find("=\"") {
831 let key = rest[..equals].trim().to_string();
832 let value_start = equals + 2;
833 let Some(length) = rest[value_start..].find('"') else {
834 break;
835 };
836 attributes.insert(key, rest[value_start..value_start + length].to_string());
837 rest = &rest[value_start + length + 1..];
838 }
839 attributes
840}
841fn deliver_home(represented: &MailAddress, envelope: &Envelope) {
845 if let Err(error) = crate::mailbox::deliver_to(represented, envelope) {
846 eprintln!(
847 "could not deliver {} to {represented}: {error}; kept in this machine's mailbox for it",
848 envelope.id
849 );
850 if let Ok(mailbox) = crate::mailbox::Mailbox::open(&mail_root(), represented) {
851 mailbox.deliver(envelope).ok();
852 }
853 return;
854 }
855 if represented.machine == crate::mailbox::local_machine_name() {
860 if let Ok(program) = supercode_program() {
861 std::process::Command::new(program)
862 .args(["message", "push", &represented.to_string(), &envelope.id])
863 .stdin(std::process::Stdio::null())
864 .stdout(std::process::Stdio::null())
865 .stderr(std::process::Stdio::null())
866 .spawn()
867 .ok();
868 }
869 }
870}
871fn file_inbound(
872 represented: &MailAddress,
873 native: &NativeEnvelope,
874 sender: Option<&ClaudePeerSession>,
875 in_reply_to: Option<String>,
876) {
877 let machine = crate::mailbox::local_machine_name();
880 let (from, from_name) = match sender {
881 Some(session) => (
882 MailAddress::new(&machine, "claude-code", &session.session_id),
883 format!("{}@{machine}", session.name),
884 ),
885 None => (
886 MailAddress::new(&machine, "claude-code", "unknown"),
887 native
888 .from_name
889 .clone()
890 .unwrap_or_else(|| "an unknown Claude session".into()),
891 ),
892 };
893 let Ok(from) = from else { return };
894 let Ok(mut envelope) = Envelope::new(
895 from,
896 from_name,
897 MailKind::Peer,
898 ReplyVia::Command,
899 native.body.clone(),
900 ) else {
901 return;
902 };
903 envelope.native_from = Some(native.from.clone());
904 if let Some(in_reply_to) = in_reply_to {
905 envelope.in_reply_to = Some(in_reply_to);
906 envelope.in_reply_to_inferred = true;
907 }
908 deliver_home(represented, &envelope);
911}
912fn file_notice(represented: &MailAddress, text: &str) {
913 let Ok(from) = MailAddress::new(
914 crate::mailbox::local_machine_name(),
915 "claude-code",
916 "notice",
917 ) else {
918 return;
919 };
920 let Ok(envelope) = Envelope::new(
921 from,
922 "Claude Code",
923 MailKind::Notice,
924 ReplyVia::None,
925 text.to_string(),
926 ) else {
927 return;
928 };
929 deliver_home(represented, &envelope);
932}
933pub fn supercode_program() -> std::io::Result<PathBuf> {
936 let current = std::env::current_exe()?;
937 if current.file_stem().and_then(|stem| stem.to_str()) == Some("supercode") {
938 return Ok(current);
939 }
940 std::env::var_os("PATH")
941 .iter()
942 .flat_map(std::env::split_paths)
943 .map(|directory| directory.join("supercode"))
944 .find(|candidate| candidate.is_file())
945 .ok_or_else(|| {
946 std::io::Error::new(
947 std::io::ErrorKind::NotFound,
948 "the supercode program is not on PATH; Claude relays and the machine daemon need it",
949 )
950 })
951}