supercode_harness/
launch_agent.rs1use std::collections::BTreeMap;
11use std::path::{Path, PathBuf};
12
13use serde_json::{json, Value};
14
15use crate::SessionLocator;
16
17const NAMES: &[&str] = &[
20 "SUPERCODE_AGENT_ID",
21 "SUPERCODE_AGENT_ORG",
22 "SUPERCODE_AGENT_CONTEXT",
23 "SUPERCODE_AGENT_MAIN",
24 "SUPERCODE_AGENT_ASSIGNMENTS",
25];
26
27fn record_path(home: &Path, session_id: &str) -> Option<PathBuf> {
29 (!session_id.is_empty()
30 && session_id
31 .chars()
32 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'))
33 .then(|| {
34 home.join("supercode")
35 .join("launches")
36 .join(format!("{session_id}.json"))
37 })
38}
39
40fn home_of(locator: &SessionLocator) -> Option<PathBuf> {
43 let crate::StorageLocator::File { path } = &locator.storage else {
44 return None;
45 };
46 let store = match locator.harness.as_str() {
47 crate::HarnessId::CODEX => "sessions",
48 crate::HarnessId::CLAUDE_CODE => "projects",
49 _ => return None,
50 };
51 path.ancestors()
52 .find(|dir| dir.file_name().and_then(|name| name.to_str()) == Some(store))
53 .and_then(Path::parent)
54 .map(Path::to_path_buf)
55}
56
57pub fn agent_environment(env: &BTreeMap<String, String>) -> Option<BTreeMap<String, String>> {
59 env.get("SUPERCODE_AGENT_ID").filter(|id| !id.is_empty())?;
60 Some(
61 NAMES
62 .iter()
63 .filter_map(|name| {
64 env.get(*name)
65 .map(|value| (name.to_string(), value.clone()))
66 })
67 .collect(),
68 )
69}
70
71pub fn record(
74 home: &Path,
75 session_id: &str,
76 env: &BTreeMap<String, String>,
77) -> std::io::Result<()> {
78 let Some(agent) = agent_environment(env) else {
79 return Ok(());
80 };
81 let Some(path) = record_path(home, session_id) else {
82 return Ok(());
83 };
84 if let Some(parent) = path.parent() {
85 std::fs::create_dir_all(parent)?;
86 }
87 let temporary = path.with_extension(format!("{}.tmp", std::process::id()));
88 let body = serde_json::to_vec(&json!({"version": 1, "environment": agent}))?;
89 {
90 use std::io::Write;
91 let mut options = std::fs::OpenOptions::new();
92 options.write(true).create(true).truncate(true);
93 #[cfg(unix)]
94 {
95 use std::os::unix::fs::OpenOptionsExt;
96 options.mode(0o600);
97 }
98 let mut file = options.open(&temporary)?;
99 file.write_all(&body)?;
100 file.sync_all()?;
101 }
102 std::fs::rename(&temporary, &path)
103}
104
105pub fn read(locator: &SessionLocator) -> Value {
107 home_of(locator)
108 .and_then(|home| record_path(&home, &locator.session_id))
109 .and_then(|path| std::fs::read(path).ok())
110 .and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
111 .and_then(|record| record.get("environment").cloned())
112 .filter(Value::is_object)
113 .unwrap_or(Value::Null)
114}