shepherd-cli 6.6.1

The canonical shepherd command-line interface over the per-project registry, run artifacts, and sprint pipeline.
//! Native transport binding for an installed harness carrier.
//!
//! A packaged carrier must learn the exact native binary and its measured
//! bytes before any run, lane, or dispatch record exists, so this command runs
//! ahead of project scaffolding and mints no authority of its own.
//!
//! Every field a caller could use to redirect execution -- the binary, the
//! project root, the environment, the digests, the destination -- is derived
//! here and never read from the request. The one caller-named input, the
//! installed package root, is therefore proven rather than trusted: it must
//! carry no symlink component and must be byte-identical to what this compiler
//! emits for the requested harness. Nothing touches the filesystem until every
//! one of those checks has passed, so a refused request leaves no transport
//! directory behind for a later caller to inherit.

use std::{
    fs,
    io::Write,
    path::{Component, Path, PathBuf},
};

use shepherd::compiler::HarnessProfile;
// Digests come from the engine. This file used to carry its own copy of the
// sha256/hex helper; `scripts/check-rust-duplicates.py` counted fourteen of
// them across cli, render and registry, and asserts one home in shepherd-core.
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;

/// One caller request.
///
/// `deny_unknown_fields` is the authority boundary rather than a tidiness
/// preference: `binary`, `project_root`, `session_id`, `role`, `env`,
/// `auth_snapshot`, `candidate_sha256`, and `destination` are all derived
/// facts, so naming any of them is refused instead of silently ignored.
#[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}"))
}

/// Map the harness a carrier calls itself to the compiler profile that owns
/// its tree. The names differ -- `claude-code` compiles from the `claude`
/// target -- so this is a real translation, not a passthrough.
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()
        ))
    })
}

/// Refuse a path any of whose components is a symlink.
///
/// The consumer re-checks this at use time, so this is the producing half of
/// the same rule rather than the only enforcement: a descriptor must never
/// record a path that already resolves through a link.
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()
        {
            // A volume prefix or a bare root is not a link and cannot be
            // inspected on its own: on Windows `\\?\C:` names a device, and
            // stat'ing it fails with "Incorrect function" (os error 1).
            continue;
        }
        match fs::symlink_metadata(&current) {
            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()))),
    }
}

/// Read one bounded regular file whose every path component is a real
/// directory. The size is checked before the read so an oversized input is
/// refused rather than buffered.
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)?;

    // Name a wrong-harness carrier before verifying its bytes. Byte
    // verification would reject it too, but only as opaque manifest drift,
    // which tells an operator nothing about what they actually installed.
    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()
        )));
    }

    // Proves that not one owned byte has drifted since the tree was emitted.
    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)?;

    // First write of the whole command: every refusal above returns before a
    // transport directory can exist.
    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(),
    );
    // An empty object, not an absent key: the consumer requires `env`, and this
    // command grants no environment. `auth_snapshot` is omitted entirely,
    // because an absent key and a null one are different facts downstream.
    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)?,
    }))
}

/// Create every missing component of an owner-only directory without ever
/// following a link.
///
/// A symlinked `.shepherd/tmp` must be refused rather than silently redirect
/// the descriptor into whatever it points at, so each component is inspected
/// with `symlink_metadata` before it is entered or created.
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()
        {
            // The volume prefix and the root already exist and are not links;
            // only the components below them are created or inspected.
            continue;
        }
        match fs::symlink_metadata(&current) {
            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(&current)?;
            }
            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())))
}

/// Publish the descriptor as a fresh owner-only regular file.
///
/// `create_new` refuses to clobber an existing name, and the mode is set at
/// creation so the bytes are never briefly readable by anyone else.
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())))
}