Skip to main content

supercode_harness/
launch_agent.rs

1//! The agent a launch named in a session's environment, kept in the harness home the session is stored in.
2//!
3//! A launcher that starts a session for an agent puts the agent's id and reader context in the
4//! session's environment (`SUPERCODE_AGENT_*`). A managed runtime records those variables in the
5//! session's harness home (`<home>/supercode/launches/<session id>.json`) before the session's
6//! transcript is visible, so whoever discovers the session reads its agent from the session's own
7//! home ([`read`], the `harness.v1.sessions.launch_agent` door), however short its process lived.
8//! The machine daemon binds the session to that agent when its connector discovers it.
9
10use std::collections::BTreeMap;
11use std::path::{Path, PathBuf};
12
13use serde_json::{json, Value};
14
15use crate::SessionLocator;
16
17/// The launch variables recorded: who the session is, never a credential (a context is a name in
18/// the machine's own contexts file).
19const NAMES: &[&str] = &[
20    "SUPERCODE_AGENT_ID",
21    "SUPERCODE_AGENT_ORG",
22    "SUPERCODE_AGENT_CONTEXT",
23    "SUPERCODE_AGENT_MAIN",
24    "SUPERCODE_AGENT_ASSIGNMENTS",
25];
26
27/// The record of `session_id` in the harness home `home`; none for an id that is not a plain name.
28fn 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
40/// The harness home a session's transcript is stored in: a Codex rollout lies under
41/// `<CODEX_HOME>/sessions/`, a Claude Code transcript under `<CLAUDE_CONFIG_DIR>/projects/`.
42fn 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
57/// The agent variables of a launch environment, or `None` when it names no agent.
58pub 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
71/// Record `env`'s agent variables for `session_id` in the harness home `home`, written whole and
72/// renamed into place. A launch that names no agent records nothing.
73pub 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
105/// The agent variables recorded for the session at `locator`, or null when its launch named none.
106pub 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}