use std::{
fs,
io::Write,
path::{Component, Path, PathBuf},
};
use shepherd::compiler::HarnessProfile;
use shepherd::digest::sha256_hex;
use crate::{ExecutionContext, interface::CliError};
const REQUEST_SCHEMA: &str = "shepherd.native-transport-bind/1";
const DESCRIPTOR_SCHEMA: &str = "shepherd.native-transport/2";
const BINDING_SCHEMA: &str = "shepherd.native-transport-binding/1";
const MANIFEST_NAME: &str = ".shepherd-generated.json";
const MAX_MANIFEST_BYTES: u64 = 4 * 1_048_576;
const MAX_BINARY_BYTES: u64 = 512 * 1_048_576;
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct BindRequest {
schema: String,
harness: String,
installed_package_root: PathBuf,
}
fn error(message: impl core::fmt::Display) -> CliError {
CliError::message(format!("native transport bind: {message}"))
}
fn profile_for(harness: &str) -> Result<HarnessProfile, CliError> {
match harness {
"claude-code" => Ok(HarnessProfile::claude()),
"codex" => Ok(HarnessProfile::codex()),
"pi" => Ok(HarnessProfile::pi()),
other => Err(error(format!("unknown harness `{other}`"))),
}
}
fn path_string(path: &Path) -> Result<String, CliError> {
path.to_str().map(str::to_owned).ok_or_else(|| {
error(format!(
"path is not valid UTF-8 and cannot be recorded: {}",
path.display()
))
})
}
fn reject_link_components(path: &Path) -> Result<(), CliError> {
let mut current = PathBuf::new();
for component in path.components() {
current.push(component.as_os_str());
if matches!(component, Component::Prefix(_) | Component::RootDir)
|| current.parent().is_none()
{
continue;
}
match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(error(format!(
"refusing a symlinked path component: {}",
current.display()
)));
}
Ok(_) => {}
Err(source) => {
return Err(error(format!("{}: {source}", current.display())));
}
}
}
Ok(())
}
fn directory_exists(path: &Path) -> Result<bool, CliError> {
match fs::symlink_metadata(path) {
Ok(metadata) => Ok(metadata.is_dir()),
Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(error(format!("{}: {source}", path.display()))),
}
}
fn measured_bytes(path: &Path, limit: u64) -> Result<Vec<u8>, CliError> {
reject_link_components(path)?;
let metadata = fs::symlink_metadata(path)
.map_err(|source| error(format!("{}: {source}", path.display())))?;
if !metadata.is_file() {
return Err(error(format!(
"custody input is not a regular file: {}",
path.display()
)));
}
if metadata.len() > limit {
return Err(error(format!(
"custody input exceeds {limit} bytes: {}",
path.display()
)));
}
fs::read(path).map_err(|source| error(format!("{}: {source}", path.display())))
}
pub(super) fn bind(
context: &ExecutionContext,
request: BindRequest,
) -> Result<serde_json::Value, CliError> {
if request.schema != REQUEST_SCHEMA {
return Err(error(format!("request schema must be `{REQUEST_SCHEMA}`")));
}
let profile = profile_for(&request.harness)?;
let installed = request.installed_package_root.as_path();
if !installed.is_absolute() {
return Err(error(format!(
"installed package root must be absolute: {}",
installed.display()
)));
}
reject_link_components(installed)?;
if !directory_exists(installed)? {
return Err(error(format!(
"installed package root is not a directory: {}",
installed.display()
)));
}
let manifest = installed.join(MANIFEST_NAME);
let manifest_bytes = measured_bytes(&manifest, MAX_MANIFEST_BYTES)?;
let declared: serde_json::Value = serde_json::from_slice(&manifest_bytes)
.map_err(|source| error(format!("{}: {source}", manifest.display())))?;
let declared_target = declared.get("target").and_then(serde_json::Value::as_str);
if declared_target != Some(profile.target.as_str()) {
return Err(error(format!(
"installed carrier declares target `{}`, not `{}`",
declared_target.unwrap_or("<absent>"),
profile.target.as_str()
)));
}
super::compile::verify_canonical_tree(installed, &profile)?;
let binary = std::env::current_exe()
.and_then(fs::canonicalize)
.map_err(|source| error(format!("cannot resolve the running binary: {source}")))?;
let binary_bytes = measured_bytes(&binary, MAX_BINARY_BYTES)?;
let directory = context.namespace.join("tmp").join("native-transports");
create_private_directory(&directory)?;
let descriptor_path = directory.join(format!("{}.json", uuid::Uuid::now_v7()));
let mut fields = serde_json::Map::new();
fields.insert("schema".into(), DESCRIPTOR_SCHEMA.into());
fields.insert("binary".into(), path_string(&binary)?.into());
fields.insert(
"project_root".into(),
path_string(&context.primary_root)?.into(),
);
fields.insert(
"installed_package_root".into(),
path_string(installed)?.into(),
);
fields.insert("installed_manifest".into(), path_string(&manifest)?.into());
fields.insert("candidate_sha256".into(), sha256_hex(&binary_bytes).into());
fields.insert(
"installed_manifest_sha256".into(),
sha256_hex(&manifest_bytes).into(),
);
fields.insert(
"env".into(),
serde_json::Value::Object(serde_json::Map::new()),
);
write_private_descriptor(&descriptor_path, &serde_json::Value::Object(fields))?;
Ok(serde_json::json!({
"schema": BINDING_SCHEMA,
"descriptor_path": path_string(&descriptor_path)?,
}))
}
fn create_private_directory(path: &Path) -> Result<(), CliError> {
let mut current = PathBuf::new();
for component in path.components() {
current.push(component.as_os_str());
if matches!(component, Component::Prefix(_) | Component::RootDir)
|| current.parent().is_none()
{
continue;
}
match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(error(format!(
"refusing a symlinked path component: {}",
current.display()
)));
}
Ok(metadata) if metadata.is_dir() => {}
Ok(_) => {
return Err(error(format!(
"path component is not a directory: {}",
current.display()
)));
}
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
create_owner_only(¤t)?;
}
Err(source) => {
return Err(error(format!("{}: {source}", current.display())));
}
}
}
Ok(())
}
#[cfg(unix)]
fn create_owner_only(path: &Path) -> Result<(), CliError> {
use std::os::unix::fs::DirBuilderExt;
fs::DirBuilder::new()
.mode(0o700)
.create(path)
.map_err(|source| error(format!("cannot create {}: {source}", path.display())))
}
#[cfg(not(unix))]
fn create_owner_only(path: &Path) -> Result<(), CliError> {
fs::DirBuilder::new()
.create(path)
.map_err(|source| error(format!("cannot create {}: {source}", path.display())))
}
fn write_private_descriptor(path: &Path, descriptor: &serde_json::Value) -> Result<(), CliError> {
let mut bytes = serde_json::to_vec(descriptor)
.map_err(|source| error(format!("cannot encode descriptor: {source}")))?;
bytes.push(b'\n');
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(path)
.map_err(|source| error(format!("cannot create {}: {source}", path.display())))?;
file.write_all(&bytes)
.map_err(|source| error(format!("cannot write {}: {source}", path.display())))?;
file.sync_all()
.map_err(|source| error(format!("cannot flush {}: {source}", path.display())))
}