use std::fs::{self, OpenOptions};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(test)]
use std::sync::{Mutex, MutexGuard, OnceLock};
use serde::{Deserialize, Serialize};
const RECEIPT_SCHEMA: &str = "supercode.live-runtime.v1";
const ENDPOINT_PREFIX: &str = "supercode-live://";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LiveRuntimeSource {
pub harness: String,
pub session_id: String,
pub workspace: PathBuf,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct LiveRuntimeMetadata {
pub profile: Option<String>,
pub persistence_location: Option<PathBuf>,
pub endpoint_capabilities: Vec<String>,
pub supervisor: Option<LiveRuntimeSupervisor>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LiveRuntimeSupervisor {
Tmux {
session_name: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LiveRuntimeRecord {
pub endpoint: LiveRuntimeEndpoint,
pub runtime_session_id: String,
pub source: LiveRuntimeSource,
pub pid: u32,
pub created_at_ms: u128,
pub metadata: LiveRuntimeMetadata,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LiveRuntimeEndpoint(String);
impl LiveRuntimeEndpoint {
pub fn parse(value: &str) -> Result<Self, LiveRuntimeReceiptError> {
let id = value
.strip_prefix(ENDPOINT_PREFIX)
.filter(|id| !id.is_empty() && id.bytes().all(|byte| byte.is_ascii_hexdigit()))
.ok_or(LiveRuntimeReceiptError::InvalidEndpoint)?;
Ok(Self(format!("{ENDPOINT_PREFIX}{id}")))
}
pub fn as_str(&self) -> &str {
&self.0
}
fn receipt_id(&self) -> &str {
self.0
.strip_prefix(ENDPOINT_PREFIX)
.expect("LiveRuntimeEndpoint is validated at construction")
}
}
impl std::fmt::Display for LiveRuntimeEndpoint {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
pub struct ResolvedLiveRuntime {
pub endpoint: LiveRuntimeEndpoint,
pub runtime_session_id: String,
pub source: LiveRuntimeSource,
pub base_url: String,
pub token: String,
pub pid: u32,
}
pub struct LiveRuntimeRegistration {
endpoint: LiveRuntimeEndpoint,
path: PathBuf,
}
impl LiveRuntimeRegistration {
pub fn endpoint(&self) -> &LiveRuntimeEndpoint {
&self.endpoint
}
}
impl Drop for LiveRuntimeRegistration {
fn drop(&mut self) {
let Ok(bytes) = fs::read(&self.path) else {
return;
};
let Ok(receipt) = serde_json::from_slice::<Receipt>(&bytes) else {
return;
};
if receipt.receipt_id == self.endpoint.receipt_id() {
let _ = fs::remove_file(&self.path);
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum LiveRuntimeReceiptError {
#[error("invalid Supercode live-runtime endpoint")]
InvalidEndpoint,
#[error("Supercode live runtime is no longer available")]
NotLive,
#[error("Supercode live-runtime receipt does not match the requested session")]
IdentityMismatch,
#[error("Supercode live-runtime receipt I/O failed: {0}")]
Io(#[from] std::io::Error),
#[error("Supercode live-runtime receipt is invalid: {0}")]
InvalidReceipt(String),
#[error("multiple live runtimes share id `{0}`")]
AmbiguousRuntime(String),
}
#[derive(Serialize, Deserialize)]
struct Receipt {
schema: String,
receipt_id: String,
runtime_session_id: String,
source: LiveRuntimeSource,
base_url: String,
token: String,
pid: u32,
created_at_ms: u128,
#[serde(default)]
metadata: LiveRuntimeMetadata,
}
pub fn register_live_runtime(
runtime_session_id: impl Into<String>,
source: LiveRuntimeSource,
base_url: impl Into<String>,
token: impl Into<String>,
) -> Result<LiveRuntimeRegistration, LiveRuntimeReceiptError> {
register_live_runtime_with_metadata(
runtime_session_id,
source,
base_url,
token,
LiveRuntimeMetadata {
endpoint_capabilities: vec!["http".into(), "acp".into()],
..LiveRuntimeMetadata::default()
},
)
}
pub fn register_live_runtime_with_metadata(
runtime_session_id: impl Into<String>,
source: LiveRuntimeSource,
base_url: impl Into<String>,
token: impl Into<String>,
metadata: LiveRuntimeMetadata,
) -> Result<LiveRuntimeRegistration, LiveRuntimeReceiptError> {
let runtime_session_id = runtime_session_id.into();
let base_url = base_url.into();
let token = token.into();
if runtime_session_id.trim().is_empty()
|| source.harness.trim().is_empty()
|| source.session_id.trim().is_empty()
|| token.is_empty()
|| !is_loopback_http(&base_url)
{
return Err(LiveRuntimeReceiptError::InvalidReceipt(
"missing identity/token or non-loopback HTTP address".into(),
));
}
let mut random = [0_u8; 16];
getrandom::getrandom(&mut random).map_err(|error| {
LiveRuntimeReceiptError::InvalidReceipt(format!("OS randomness unavailable: {error}"))
})?;
let receipt_id = random.iter().map(|byte| format!("{byte:02x}")).collect();
let endpoint = LiveRuntimeEndpoint(format!("{ENDPOINT_PREFIX}{receipt_id}"));
let receipt = Receipt {
schema: RECEIPT_SCHEMA.into(),
receipt_id,
runtime_session_id,
source: LiveRuntimeSource {
workspace: normalized_path(&source.workspace),
..source
},
base_url,
token,
pid: std::process::id(),
created_at_ms: now_ms(),
metadata,
};
let directory = receipt_directory();
fs::create_dir_all(&directory)?;
#[cfg(unix)]
fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))?;
let path = directory.join(format!("{}.json", endpoint.receipt_id()));
let temporary = directory.join(format!(
".{}.{}.tmp",
endpoint.receipt_id(),
std::process::id()
));
let bytes = serde_json::to_vec(&receipt)
.map_err(|error| LiveRuntimeReceiptError::InvalidReceipt(error.to_string()))?;
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(&temporary)?;
file.write_all(&bytes)?;
file.sync_all()?;
fs::rename(&temporary, &path)?;
Ok(LiveRuntimeRegistration { endpoint, path })
}
pub fn list_live_runtimes() -> Result<Vec<LiveRuntimeRecord>, LiveRuntimeReceiptError> {
let mut records = read_receipts()?
.into_iter()
.map(|receipt| LiveRuntimeRecord {
endpoint: LiveRuntimeEndpoint(format!("{ENDPOINT_PREFIX}{}", receipt.receipt_id)),
runtime_session_id: receipt.runtime_session_id,
source: receipt.source,
pid: receipt.pid,
created_at_ms: receipt.created_at_ms,
metadata: receipt.metadata,
})
.collect::<Vec<_>>();
records.sort_by(|left, right| {
right
.created_at_ms
.cmp(&left.created_at_ms)
.then_with(|| left.runtime_session_id.cmp(&right.runtime_session_id))
});
Ok(records)
}
pub fn find_live_runtime(
runtime_session_id: &str,
) -> Result<Option<LiveRuntimeRecord>, LiveRuntimeReceiptError> {
let mut matches = list_live_runtimes()?
.into_iter()
.filter(|record| record.runtime_session_id == runtime_session_id)
.collect::<Vec<_>>();
match matches.len() {
0 => Ok(None),
1 => Ok(matches.pop()),
_ => Err(LiveRuntimeReceiptError::AmbiguousRuntime(
runtime_session_id.into(),
)),
}
}
pub fn forget_live_runtime(endpoint: &LiveRuntimeEndpoint) -> Result<(), LiveRuntimeReceiptError> {
let path = receipt_directory().join(format!("{}.json", endpoint.receipt_id()));
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.into()),
}
}
pub fn discover_live_runtime(
source: &LiveRuntimeSource,
) -> Result<Option<LiveRuntimeEndpoint>, LiveRuntimeReceiptError> {
let mut matches = read_receipts()?
.into_iter()
.filter(|receipt| source_matches(&receipt.source, source))
.collect::<Vec<_>>();
matches.sort_by_key(|receipt| std::cmp::Reverse(receipt.created_at_ms));
Ok(matches
.first()
.map(|receipt| LiveRuntimeEndpoint(format!("{ENDPOINT_PREFIX}{}", receipt.receipt_id))))
}
pub fn resolve_live_runtime(
endpoint: &LiveRuntimeEndpoint,
expected: &LiveRuntimeSource,
) -> Result<ResolvedLiveRuntime, LiveRuntimeReceiptError> {
let path = receipt_directory().join(format!("{}.json", endpoint.receipt_id()));
let receipt = read_receipt(&path)?.ok_or(LiveRuntimeReceiptError::NotLive)?;
if receipt.receipt_id != endpoint.receipt_id() || !source_matches(&receipt.source, expected) {
return Err(LiveRuntimeReceiptError::IdentityMismatch);
}
Ok(ResolvedLiveRuntime {
endpoint: endpoint.clone(),
runtime_session_id: receipt.runtime_session_id,
source: receipt.source,
base_url: receipt.base_url,
token: receipt.token,
pid: receipt.pid,
})
}
fn read_receipts() -> Result<Vec<Receipt>, LiveRuntimeReceiptError> {
let directory = receipt_directory();
let Ok(entries) = fs::read_dir(&directory) else {
return Ok(Vec::new());
};
let mut receipts = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|value| value.to_str()) != Some("json") {
continue;
}
if let Ok(Some(receipt)) = read_receipt(&path) {
receipts.push(receipt);
}
}
Ok(receipts)
}
fn read_receipt(path: &Path) -> Result<Option<Receipt>, LiveRuntimeReceiptError> {
let bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error.into()),
};
let receipt: Receipt = serde_json::from_slice(&bytes)
.map_err(|error| LiveRuntimeReceiptError::InvalidReceipt(error.to_string()))?;
if receipt.schema != RECEIPT_SCHEMA || !is_loopback_http(&receipt.base_url) {
return Err(LiveRuntimeReceiptError::InvalidReceipt(
"unsupported schema or non-loopback address".into(),
));
}
if !process_is_live(receipt.pid) {
let _ = fs::remove_file(path);
return Ok(None);
}
Ok(Some(receipt))
}
fn receipt_directory() -> PathBuf {
crate::agent::global_instructions_dir().join("live-runtimes")
}
#[cfg(test)]
pub(crate) fn test_environment_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn source_matches(left: &LiveRuntimeSource, right: &LiveRuntimeSource) -> bool {
left.harness == right.harness
&& left.session_id == right.session_id
&& normalized_path(&left.workspace) == normalized_path(&right.workspace)
}
fn normalized_path(path: &Path) -> PathBuf {
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn is_loopback_http(value: &str) -> bool {
let Some(authority) = value
.strip_prefix("http://")
.and_then(|rest| rest.split('/').next())
else {
return false;
};
let host = authority
.strip_prefix('[')
.and_then(|rest| rest.split(']').next())
.unwrap_or_else(|| authority.split(':').next().unwrap_or_default());
matches!(host, "127.0.0.1" | "localhost" | "::1")
}
fn now_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
}
#[cfg(unix)]
fn process_is_live(pid: u32) -> bool {
let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(not(unix))]
fn process_is_live(pid: u32) -> bool {
pid == std::process::id()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn receipt_is_opaque_private_and_identity_scoped() {
let _guard = test_environment_lock();
let root = std::env::temp_dir().join(format!(
"supercode-live-receipt-{}-{}",
std::process::id(),
now_ms()
));
let workspace = root.join("project");
fs::create_dir_all(&workspace).unwrap();
let workspace = fs::canonicalize(workspace).unwrap();
std::env::set_var("SUPERCODE_HOME", &root);
let source = LiveRuntimeSource {
harness: "grok".into(),
session_id: "source-1".into(),
workspace: workspace.clone(),
};
let registration = register_live_runtime(
"runtime-1",
source.clone(),
"http://127.0.0.1:43123",
"secret-token",
)
.unwrap();
assert!(registration
.endpoint()
.as_str()
.starts_with(ENDPOINT_PREFIX));
assert!(!registration.endpoint().as_str().contains("secret-token"));
assert_eq!(
discover_live_runtime(&source).unwrap().as_ref(),
Some(registration.endpoint())
);
let records = list_live_runtimes().unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].runtime_session_id, "runtime-1");
assert_eq!(records[0].source, source);
assert_eq!(records[0].metadata.endpoint_capabilities, ["http", "acp"]);
assert_eq!(
find_live_runtime("runtime-1").unwrap().as_ref(),
records.first()
);
let resolved = resolve_live_runtime(registration.endpoint(), &source).unwrap();
assert_eq!(resolved.runtime_session_id, "runtime-1");
assert_eq!(resolved.token, "secret-token");
let wrong = LiveRuntimeSource {
session_id: "other".into(),
..source.clone()
};
assert!(matches!(
resolve_live_runtime(registration.endpoint(), &wrong),
Err(LiveRuntimeReceiptError::IdentityMismatch)
));
let receipt_path =
receipt_directory().join(format!("{}.json", registration.endpoint().receipt_id()));
#[cfg(unix)]
{
assert_eq!(
fs::metadata(&receipt_path).unwrap().permissions().mode() & 0o777,
0o600
);
assert_eq!(
fs::metadata(receipt_directory())
.unwrap()
.permissions()
.mode()
& 0o777,
0o700
);
}
drop(registration);
assert!(!receipt_path.exists());
std::env::remove_var("SUPERCODE_HOME");
fs::remove_dir_all(root).ok();
}
}