use std::fs::{self, OpenOptions};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Component, Path};
use serde::Deserialize;
use crate::{
Fidelity, HarnessHomes, HarnessId, Session, SessionFormat, SessionLocator, StorageLocator,
};
#[derive(Deserialize)]
pub(crate) struct MaterializeArtifact {
pub target_harness: HarnessId,
pub session_id: Option<String>,
pub content: String,
pub files: Vec<MaterializeFile>,
pub fidelity: Fidelity,
}
#[derive(Deserialize)]
pub(crate) struct MaterializeFile {
pub path: String,
pub content: String,
pub role: String,
}
pub(crate) fn materialize_native_artifact(
artifact: MaterializeArtifact,
cwd: &Path,
homes: &HarnessHomes,
) -> Result<SessionLocator, String> {
match artifact.target_harness.as_str() {
HarnessId::CLAUDE_CODE => materialize_at(artifact, cwd, &homes.claude_code),
HarnessId::CODEX => materialize_codex_at(artifact, cwd, &homes.codex),
HarnessId::PI => materialize_pi_at(artifact, cwd, &homes.pi),
HarnessId::GROK => materialize_grok_at(artifact, cwd, &homes.grok),
HarnessId::OPENCODE => materialize_opencode_at(artifact, cwd, &homes.opencode),
_ => Err("native materialization is not yet supported for this harness".into()),
}
}
fn validate(
artifact: &MaterializeArtifact,
cwd: &Path,
harness: &str,
companions: &[&str],
) -> Result<std::path::PathBuf, String> {
if artifact.fidelity == Fidelity::ByteLossless {
return Err(
"fresh native identity materialization cannot meet byte_lossless fidelity".into(),
);
}
let primary = artifact
.files
.iter()
.filter(|file| file.role == "primary")
.collect::<Vec<_>>();
if primary.len() != 1 || primary[0].content != artifact.content {
return Err("native artifact must contain one matching primary file".into());
}
for file in &artifact.files {
let path = Path::new(&file.path);
if path.as_os_str().is_empty()
|| path.is_absolute()
|| path
.components()
.any(|part| !matches!(part, Component::Normal(_)))
{
return Err("native artifact contains an unsafe relative filename".into());
}
}
if let Some(extra) = artifact.files.iter().find(|file| {
file.role != "primary"
&& file.role != "source_recovery"
&& !companions.contains(&file.path.as_str())
}) {
return Err(format!(
"{harness} continuation does not carry the companion artifact {}",
extra.path
));
}
cwd.canonicalize()
.map_err(|error| format!("continuation cwd is unavailable: {error}"))
}
fn write_exclusive(path: &Path, content: &str) -> Result<(), String> {
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
options.mode(0o600);
let mut file = options
.open(path)
.map_err(|error| format!("refusing to overwrite native continuation: {error}"))?;
if let Err(error) = file
.write_all(content.as_bytes())
.and_then(|_| file.sync_all())
{
let _ = fs::remove_file(path);
return Err(error.to_string());
}
Ok(())
}
fn contained_dir(root: &Path, parts: &[&str]) -> Result<std::path::PathBuf, String> {
fs::create_dir_all(root).map_err(|error| error.to_string())?;
let root = root
.canonicalize()
.map_err(|error| format!("native store root is unavailable: {error}"))?;
let mut directory = root.clone();
for part in parts {
directory = directory.join(part);
match fs::symlink_metadata(&directory) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
return Err("refusing unsafe native store directory".into());
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(&directory).map_err(|error| error.to_string())?;
}
Err(error) => return Err(error.to_string()),
}
}
let canonical = directory
.canonicalize()
.map_err(|error| format!("native store directory is unavailable: {error}"))?;
if !canonical.starts_with(&root) {
return Err("refusing native store directory outside its home".into());
}
Ok(canonical)
}
fn materialize_codex_at(
artifact: MaterializeArtifact,
cwd: &Path,
codex_sessions: &Path,
) -> Result<SessionLocator, String> {
let cwd = validate(&artifact, cwd, "Codex", &[])?;
let session = Session::load_str(&artifact.content, SessionFormat::Codex)
.map_err(|error| error.to_string())?;
if let Some(declared) = artifact.session_id.as_deref() {
if session.meta.session_id.as_deref() != Some(declared) {
return Err(
"native artifact declared session identity does not match its content".into(),
);
}
}
let session_id = crate::runtime::generated_session_id();
let rewritten_identity = session
.to_jsonl_spliced(SessionFormat::Codex, Some(&session_id))
.map_err(|error| error.to_string())?;
let destination_cwd = cwd.to_string_lossy().to_string();
let mut rewritten = String::new();
for line in rewritten_identity.lines() {
let mut record: serde_json::Value = serde_json::from_str(line)
.map_err(|error| format!("invalid rewritten Codex record: {error}"))?;
let kind = record
.get("type")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
if matches!(kind, "session_meta" | "turn_context") {
if let Some(payload) = record
.get_mut("payload")
.and_then(serde_json::Value::as_object_mut)
{
if payload.contains_key("cwd") {
payload.insert(
"cwd".into(),
serde_json::Value::String(destination_cwd.clone()),
);
}
}
}
rewritten.push_str(&serde_json::to_string(&record).map_err(|error| error.to_string())?);
rewritten.push('\n');
}
let now = supercode_interchange::sidecar::now_rfc3339();
let (year, month, day) = (&now[0..4], &now[5..7], &now[8..10]);
let directory = contained_dir(codex_sessions, &[year, month, day])?;
let stamp = now[0..19].replace(':', "-");
let path = directory.join(format!("rollout-{stamp}-{session_id}.jsonl"));
write_exclusive(&path, &rewritten)?;
Ok(SessionLocator {
harness: HarnessId::from(HarnessId::CODEX),
session_id,
storage: StorageLocator::File { path },
})
}
fn materialize_pi_at(
artifact: MaterializeArtifact,
cwd: &Path,
pi_sessions: &Path,
) -> Result<SessionLocator, String> {
let cwd = validate(&artifact, cwd, "Pi", &[])?;
let session = Session::load_str(&artifact.content, SessionFormat::Pi)
.map_err(|error| error.to_string())?;
if let Some(declared) = artifact.session_id.as_deref() {
if session.meta.session_id.as_deref() != Some(declared) {
return Err(
"native artifact declared session identity does not match its content".into(),
);
}
}
let session_id = crate::runtime::generated_session_id();
let rewritten_identity = session
.to_jsonl_spliced(SessionFormat::Pi, Some(&session_id))
.map_err(|error| error.to_string())?;
let destination_cwd = cwd.to_string_lossy().to_string();
let mut rewritten = String::new();
for line in rewritten_identity.lines() {
let mut record: serde_json::Value = serde_json::from_str(line)
.map_err(|error| format!("invalid rewritten Pi record: {error}"))?;
if record.get("type").and_then(serde_json::Value::as_str) == Some("session") {
if let Some(object) = record.as_object_mut() {
object.insert(
"cwd".into(),
serde_json::Value::String(destination_cwd.clone()),
);
}
}
rewritten.push_str(&serde_json::to_string(&record).map_err(|error| error.to_string())?);
rewritten.push('\n');
}
let trimmed = destination_cwd
.strip_prefix('/')
.or_else(|| destination_cwd.strip_prefix('\\'))
.unwrap_or(&destination_cwd);
let folder = format!("--{}--", trimmed.replace(['/', '\\', ':'], "-"));
let directory = contained_dir(pi_sessions, &[&folder])?;
let stamp = supercode_interchange::sidecar::now_rfc3339().replace([':', '.'], "-");
let path = directory.join(format!("{stamp}_{session_id}.jsonl"));
write_exclusive(&path, &rewritten)?;
Ok(SessionLocator {
harness: HarnessId::from(HarnessId::PI),
session_id,
storage: StorageLocator::File { path },
})
}
fn materialize_grok_at(
artifact: MaterializeArtifact,
cwd: &Path,
grok_sessions: &Path,
) -> Result<SessionLocator, String> {
let cwd = validate(&artifact, cwd, "Grok", &["summary.json", "updates.jsonl"])?;
let session = Session::load_str(&artifact.content, SessionFormat::Grok)
.map_err(|error| error.to_string())?;
let companion = |name: &str| {
artifact
.files
.iter()
.find(|file| file.role != "primary" && file.path == name)
.map(|file| file.content.as_str())
};
let source_summary = companion("summary.json")
.map(serde_json::from_str::<serde_json::Value>)
.transpose()
.map_err(|error| format!("invalid Grok summary.json: {error}"))?
.unwrap_or_else(|| serde_json::json!({}));
let source_id = source_summary
.pointer("/info/id")
.and_then(serde_json::Value::as_str)
.map(str::to_string);
if let (Some(declared), Some(source)) = (artifact.session_id.as_deref(), source_id.as_deref()) {
if source != declared {
return Err(
"native artifact declared session identity does not match its content".into(),
);
}
}
let model = source_summary
.get("current_model_id")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
.or_else(|| session.meta.model.clone())
.unwrap_or_default();
let session_id = crate::runtime::generated_session_id();
let transcript = session
.to_jsonl_spliced(SessionFormat::Grok, Some(&session_id))
.map_err(|error| error.to_string())?;
let destination_cwd = cwd.to_string_lossy().to_string();
let now = supercode_interchange::sidecar::now_rfc3339();
let mut summary = match source_summary {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
for key in [
"agent_id",
"attempt_id",
"request_id",
"grok_home",
"sandbox_profile",
] {
summary.remove(key);
}
summary.insert(
"info".into(),
serde_json::json!({"id": session_id, "cwd": destination_cwd}),
);
summary
.entry("session_summary")
.or_insert_with(|| serde_json::json!(""));
summary
.entry("created_at")
.or_insert_with(|| serde_json::json!(now));
summary.insert("updated_at".into(), serde_json::json!(now));
summary
.entry("num_messages")
.or_insert_with(|| serde_json::json!(session.messages.len()));
summary.insert("current_model_id".into(), serde_json::json!(model));
let directory = contained_dir(
grok_sessions,
&[&grok_workspace_dir(&destination_cwd), &session_id],
)?;
let path = directory.join("chat_history.jsonl");
write_exclusive(&path, &transcript)?;
write_exclusive(
&directory.join("summary.json"),
&serde_json::to_string_pretty(&summary).map_err(|error| error.to_string())?,
)?;
if let (Some(updates), Some(source_id)) = (companion("updates.jsonl"), source_id.as_deref()) {
let mut rewritten = String::new();
for line in updates.lines().filter(|line| !line.trim().is_empty()) {
let mut record: serde_json::Value = serde_json::from_str(line)
.map_err(|error| format!("invalid Grok updates record: {error}"))?;
if let Some(params) = record
.get_mut("params")
.and_then(serde_json::Value::as_object_mut)
{
if params.get("sessionId").and_then(serde_json::Value::as_str) == Some(source_id) {
params.insert("sessionId".into(), serde_json::json!(session_id));
}
if let Some(event) = params
.get_mut("_meta")
.and_then(|meta| meta.get_mut("eventId"))
{
if let Some(rest) = event.as_str().and_then(|id| id.strip_prefix(source_id)) {
*event = serde_json::json!(format!("{session_id}{rest}"));
}
}
}
rewritten.push_str(&serde_json::to_string(&record).map_err(|error| error.to_string())?);
rewritten.push('\n');
}
write_exclusive(&directory.join("updates.jsonl"), &rewritten)?;
}
Ok(SessionLocator {
harness: HarnessId::from(HarnessId::GROK),
session_id,
storage: StorageLocator::File { path },
})
}
fn materialize_opencode_at(
artifact: MaterializeArtifact,
cwd: &Path,
opencode_root: &Path,
) -> Result<SessionLocator, String> {
let cwd = validate(&artifact, cwd, "OpenCode", &[])?;
let mut document: serde_json::Value = serde_json::from_str(&artifact.content)
.map_err(|error| format!("invalid OpenCode export: {error}"))?;
if let Some(declared) = artifact.session_id.as_deref() {
if document
.pointer("/info/id")
.and_then(serde_json::Value::as_str)
!= Some(declared)
{
return Err(
"native artifact declared session identity does not match its content".into(),
);
}
}
let destination_cwd = cwd.to_string_lossy().to_string();
let mut ids = OpenCodeIds::default();
let session_id = ids.next("ses", true)?;
let info = document
.get_mut("info")
.and_then(serde_json::Value::as_object_mut)
.ok_or("OpenCode export has no session info")?;
info.insert("id".into(), serde_json::json!(session_id));
info.insert("directory".into(), serde_json::json!(destination_cwd));
info.insert(
"projectID".into(),
serde_json::json!(opencode_project_id(&cwd)),
);
info.remove("parentID");
let mut renamed = std::collections::HashMap::<String, String>::new();
let mut rename = |ids: &mut OpenCodeIds, old: &str| -> Result<String, String> {
if let Some(new) = renamed.get(old) {
return Ok(new.clone());
}
let prefix = old
.split_once('_')
.map(|(prefix, _)| prefix)
.unwrap_or("msg");
let new = ids.next(prefix, false)?;
renamed.insert(old.to_string(), new.clone());
Ok(new)
};
for message in document
.get_mut("messages")
.and_then(serde_json::Value::as_array_mut)
.ok_or("OpenCode export has no messages")?
{
let info = message
.get_mut("info")
.and_then(serde_json::Value::as_object_mut)
.ok_or("OpenCode message has no info")?;
info.insert("sessionID".into(), serde_json::json!(session_id));
let old = info
.get("id")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
let message_id = rename(&mut ids, &old)?;
info.insert("id".into(), serde_json::json!(message_id));
if let Some(parent) = info
.get("parentID")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
{
info.insert(
"parentID".into(),
serde_json::json!(rename(&mut ids, &parent)?),
);
}
if let Some(path) = info
.get_mut("path")
.and_then(serde_json::Value::as_object_mut)
{
for key in ["cwd", "root"] {
if path.contains_key(key) {
path.insert(key.into(), serde_json::json!(destination_cwd));
}
}
}
for part in message
.get_mut("parts")
.and_then(serde_json::Value::as_array_mut)
.into_iter()
.flatten()
{
if let Some(part) = part.as_object_mut() {
part.insert("sessionID".into(), serde_json::json!(session_id));
part.insert("messageID".into(), serde_json::json!(message_id));
let old = part
.get("id")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
part.insert("id".into(), serde_json::json!(rename(&mut ids, &old)?));
}
}
}
let staging = std::env::temp_dir().join(format!("supercode-opencode-{session_id}"));
fs::create_dir(&staging).map_err(|error| error.to_string())?;
let file = staging.join("session.json");
let imported = write_exclusive(
&file,
&serde_json::to_string(&document).map_err(|error| error.to_string())?,
)
.and_then(|_| {
std::process::Command::new("opencode")
.arg("import")
.arg(&file)
.current_dir(&cwd)
.stdin(std::process::Stdio::null())
.output()
.map_err(|error| format!("`opencode import` could not start: {error}"))
});
let _ = fs::remove_dir_all(&staging);
let output = imported?;
let stdout = String::from_utf8_lossy(&output.stdout);
let reported = stdout
.lines()
.find_map(|line| line.trim().strip_prefix("Imported session: "))
.map(str::trim);
if !output.status.success() || reported != Some(session_id.as_str()) {
return Err(format!(
"`opencode import` did not import {session_id} (exit {}): {}{}",
output.status,
stdout.trim(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
let database = opencode_database_holding(opencode_root, &session_id).ok_or_else(|| {
format!(
"`opencode import` reported {session_id}, but no OpenCode database under {} holds it",
opencode_root.display()
)
})?;
Ok(SessionLocator {
harness: HarnessId::from(HarnessId::OPENCODE),
session_id: session_id.clone(),
storage: StorageLocator::Sqlite {
path: database,
selector: session_id,
},
})
}
#[derive(Default)]
struct OpenCodeIds {
counter: u64,
}
impl OpenCodeIds {
fn next(&mut self, prefix: &str, descending: bool) -> Result<String, String> {
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
self.counter += 1;
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|error| error.to_string())?
.as_millis() as u64;
let mut value = millis.wrapping_mul(0x1000).wrapping_add(self.counter);
if descending {
value = !value;
}
let mut random = [0_u8; 14];
getrandom::getrandom(&mut random).map_err(|error| error.to_string())?;
let suffix: String = random
.iter()
.map(|byte| BASE62[usize::from(*byte) % BASE62.len()] as char)
.collect();
Ok(format!(
"{prefix}_{:012x}{suffix}",
value & 0xffff_ffff_ffff
))
}
}
fn opencode_project_id(cwd: &Path) -> String {
let git = |arguments: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(cwd)
.args(arguments)
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).to_string())
};
let Some(common) = git(&["rev-parse", "--path-format=absolute", "--git-common-dir"]) else {
return "global".into();
};
if let Ok(cached) = fs::read_to_string(Path::new(common.trim()).join("opencode")) {
if !cached.trim().is_empty() {
return cached.trim().to_string();
}
}
let mut roots = git(&["rev-list", "--max-parents=0", "--all"])
.map(|text| {
text.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(str::to_string)
.collect::<Vec<_>>()
})
.unwrap_or_default();
roots.sort();
roots.into_iter().next().unwrap_or_else(|| "global".into())
}
fn opencode_database_holding(root: &Path, session_id: &str) -> Option<std::path::PathBuf> {
let mut databases = if root.is_file() {
vec![root.to_path_buf()]
} else {
fs::read_dir(root)
.ok()?
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("opencode") && name.ends_with(".db"))
})
.collect::<Vec<_>>()
};
databases.sort();
databases.into_iter().find(|database| {
rusqlite::Connection::open_with_flags(database, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
.and_then(|connection| {
connection.query_row("SELECT 1 FROM session WHERE id = ?1", [session_id], |_| {
Ok(())
})
})
.is_ok()
})
}
fn grok_workspace_dir(cwd: &str) -> String {
let mut out = String::new();
for byte in cwd.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
out.push(byte as char);
} else {
out.push_str(&format!("%{byte:02X}"));
}
}
out
}
fn materialize_at(
artifact: MaterializeArtifact,
cwd: &Path,
claude_projects: &Path,
) -> Result<SessionLocator, String> {
let cwd = validate(&artifact, cwd, "Claude", &[])?;
let session = Session::load_str(&artifact.content, SessionFormat::ClaudeCode)
.map_err(|error| error.to_string())?;
if let Some(declared) = artifact.session_id.as_deref() {
if session.meta.session_id.as_deref() != Some(declared) {
return Err(
"native artifact declared session identity does not match its content".into(),
);
}
}
let session_id = crate::runtime::generated_session_id();
let rewritten_identity = session
.to_jsonl_spliced(SessionFormat::ClaudeCode, Some(&session_id))
.map_err(|error| error.to_string())?;
let destination_cwd = cwd.to_string_lossy();
let mut rewritten = String::new();
for line in rewritten_identity.lines() {
let mut record: serde_json::Value = serde_json::from_str(line)
.map_err(|error| format!("invalid rewritten Claude record: {error}"))?;
if let Some(object) = record.as_object_mut() {
object.insert(
"cwd".into(),
serde_json::Value::String(destination_cwd.to_string()),
);
}
rewritten.push_str(&serde_json::to_string(&record).map_err(|error| error.to_string())?);
rewritten.push('\n');
}
let project = cwd
.to_string_lossy()
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() {
character
} else {
'-'
}
})
.collect::<String>();
fs::create_dir_all(claude_projects).map_err(|error| error.to_string())?;
let projects_root = claude_projects
.canonicalize()
.map_err(|error| format!("Claude projects root is unavailable: {error}"))?;
let directory = projects_root.join(project);
match fs::symlink_metadata(&directory) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
return Err("refusing unsafe Claude project directory".into());
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(&directory).map_err(|error| error.to_string())?;
}
Err(error) => return Err(error.to_string()),
}
let canonical_directory = directory
.canonicalize()
.map_err(|error| format!("Claude project directory is unavailable: {error}"))?;
if !canonical_directory.starts_with(&projects_root) {
return Err("refusing Claude project directory outside its native home".into());
}
let path = canonical_directory.join(format!("{session_id}.jsonl"));
write_exclusive(&path, &rewritten)?;
Ok(SessionLocator {
harness: HarnessId::from(HarnessId::CLAUDE_CODE),
session_id,
storage: StorageLocator::File { path },
})
}
#[cfg(test)]
mod tests {
use super::*;
fn artifact(fidelity: Fidelity) -> MaterializeArtifact {
let content = include_str!("../tests/fixtures/claude_code_session.jsonl").to_string();
MaterializeArtifact {
target_harness: HarnessId::from(HarnessId::CLAUDE_CODE),
session_id: None,
files: vec![MaterializeFile {
path: "session.jsonl".into(),
content: content.clone(),
role: "primary".into(),
}],
content,
fidelity,
}
}
#[test]
fn claude_materialization_mints_fresh_exclusive_native_identity() {
let root = std::env::temp_dir().join(format!(
"supercode-materialize-{}",
crate::runtime::generated_session_id()
));
let cwd = root.join("workspace");
fs::create_dir_all(&cwd).unwrap();
let first = materialize_at(
artifact(Fidelity::ValueLossless),
&cwd,
&root.join("projects"),
)
.unwrap();
let second = materialize_at(
artifact(Fidelity::ValueLossless),
&cwd,
&root.join("projects"),
)
.unwrap();
assert_ne!(first.session_id, second.session_id);
assert!(first.storage.path().is_file());
let installed = fs::read_to_string(first.storage.path()).unwrap();
assert!(installed
.lines()
.all(|line| line.contains(&first.session_id)));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn byte_lossless_claim_is_refused_when_identity_must_change() {
let root = std::env::temp_dir().join(format!(
"supercode-materialize-{}",
crate::runtime::generated_session_id()
));
fs::create_dir_all(&root).unwrap();
let error = materialize_at(
artifact(Fidelity::ByteLossless),
&root,
&root.join("projects"),
)
.unwrap_err();
assert!(error.contains("byte_lossless"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn declared_identity_mismatch_and_companions_are_refused_before_writes() {
let root = std::env::temp_dir().join(format!(
"supercode-materialize-{}",
crate::runtime::generated_session_id()
));
let cwd = root.join("workspace");
fs::create_dir_all(&cwd).unwrap();
let mut mismatch = artifact(Fidelity::ValueLossless);
mismatch.session_id = Some("not-the-native-session".into());
let projects = root.join("identity-projects");
let error = materialize_at(mismatch, &cwd, &projects).unwrap_err();
assert!(error.contains("declared session identity"));
assert!(!projects.exists());
let mut companion = artifact(Fidelity::ValueLossless);
companion.files.push(MaterializeFile {
path: "sidecar.json".into(),
content: "{}".into(),
role: "sidecar".into(),
});
let projects = root.join("companion-projects");
let error = materialize_at(companion, &cwd, &projects).unwrap_err();
assert!(error.contains("companion artifact sidecar.json"));
assert!(!projects.exists());
fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn claude_materialization_refuses_symlinked_project_directory() {
use std::os::unix::fs::symlink;
let root = std::env::temp_dir().join(format!(
"supercode-materialize-{}",
crate::runtime::generated_session_id()
));
let cwd = root.join("workspace");
let projects = root.join("projects");
let outside = root.join("outside");
fs::create_dir_all(&cwd).unwrap();
fs::create_dir_all(&projects).unwrap();
fs::create_dir_all(&outside).unwrap();
let canonical_cwd = cwd.canonicalize().unwrap();
let project = canonical_cwd
.to_string_lossy()
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() {
character
} else {
'-'
}
})
.collect::<String>();
symlink(&outside, projects.join(project)).unwrap();
let error = materialize_at(artifact(Fidelity::ValueLossless), &cwd, &projects).unwrap_err();
assert!(error.contains("unsafe Claude project directory"));
assert_eq!(fs::read_dir(outside).unwrap().count(), 0);
fs::remove_dir_all(root).unwrap();
}
}