use std::{
collections::{BTreeMap, BTreeSet},
fs::{self, Metadata},
io::Read,
path::{Path, PathBuf},
process::Command,
};
#[cfg(unix)]
use std::{fs::File, path::Component};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use shepherd::{
RunState,
candidate::{
CandidateCustody, CandidateRecord, CandidateState, CompilerDigests, FileReference,
ProductArtifacts, ProductFile, ProductManifest, ProductSource, canonical_relative,
evidence_only_path, exact_hex, excluded_product_path, product_symlink_target,
},
compiler::{HarnessProfile, compile},
dispatch::RunId,
};
use crate::{
ContextInputs, ExecutionContext, RunStore, RunStoreError,
content_compiler::{embedded_compile_input, load_compile_input},
interface::{CliError, CliGlobals},
};
const STATE_KEY: &str = "candidate_custody";
const MAX_DOCUMENT: u64 = 16 * 1024 * 1024;
const MAX_ARTIFACT: u64 = 256 * 1024 * 1024;
const PACKAGE_NAMES: [&str; 4] = [
"@pzzld/component-runtime",
"@pzzld/claude-shepherd",
"@pzzld/codex-shepherd",
"@pzzld/pi-shepherd",
];
#[derive(
Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, clap::Args, Deserialize, Serialize,
)]
pub struct CandidateCmd {
#[command(subcommand)]
action: CandidateAction,
}
#[derive(
Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, clap::Subcommand, Deserialize, Serialize,
)]
enum CandidateAction {
Manifest {
#[arg(long)]
run: String,
#[arg(long)]
component: PathBuf,
#[arg(long)]
json: bool,
},
Freeze {
#[arg(long)]
run: String,
#[arg(long)]
source: String,
#[arg(long)]
manifest: PathBuf,
#[arg(long)]
json: bool,
},
Verify {
#[arg(long)]
run: String,
#[arg(long, value_enum)]
stage: CandidateStage,
#[arg(long)]
json: bool,
},
Pack {
#[arg(long)]
run: String,
#[arg(long)]
json: bool,
},
Attest {
#[arg(long)]
run: String,
#[arg(long, value_enum)]
kind: AttestationKind,
#[arg(long)]
manifest: PathBuf,
#[arg(long)]
json: bool,
},
Revoke {
#[arg(long)]
run: String,
#[arg(long)]
reason: String,
#[arg(long)]
json: bool,
},
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
clap::ValueEnum,
Deserialize,
Serialize,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
enum CandidateStage {
Source,
Packages,
Attested,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
clap::ValueEnum,
Deserialize,
Serialize,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
enum AttestationKind {
Packages,
Lifecycle,
}
impl CandidateAction {
fn run_id(&self) -> &str {
match self {
Self::Manifest { run, .. }
| Self::Freeze { run, .. }
| Self::Verify { run, .. }
| Self::Pack { run, .. }
| Self::Attest { run, .. }
| Self::Revoke { run, .. } => run,
}
}
}
impl CandidateCmd {
pub(crate) fn run(self, globals: CliGlobals) -> Result<(), CliError> {
RunId::new(self.action.run_id()).map_err(error)?;
let cwd = std::env::current_dir().map_err(error)?;
let mut inputs = ContextInputs::from_environment(cwd).map_err(error)?;
inputs.explicit_config = globals.config;
inputs.verbosity = globals.verbosity;
let mut context = ExecutionContext::discover(inputs).map_err(error)?;
let run = self.action.run_id();
let store = RunStore::new(context.runs_root.join(run).join("run.json"));
if let CandidateAction::Manifest { component, .. } = &self.action {
let state = store.load().map_err(error)?;
ensure_run(&state, run)?;
let manifest = collect_manifest(&context.workspace_root, run, component)?;
return write_json(&mut context, &manifest);
}
let outcome = store
.update(|state| {
ensure_run(state, run).map_err(|cause| {
RunStoreError::mutation(
cause.message_text().unwrap_or("run identity is invalid"),
)
})?;
let before = state.clone();
let operation = self.apply(&context.workspace_root, state);
if let Err(cause) = &operation
&& *state == before
{
return Err(RunStoreError::mutation(
cause.message_text().unwrap_or("candidate operation failed"),
));
}
Ok(operation)
})
.map_err(error)??;
write_json(&mut context, &outcome)
}
fn apply(&self, root: &Path, state: &mut RunState) -> Result<CandidateRecord, CliError> {
if let CandidateAction::Freeze {
source, manifest, ..
} = &self.action
{
if !exact_hex(source, 40) {
return Err(error("source must be an exact lowercase 40-hex commit"));
}
let manifest_ref = external_reference(root, manifest, MAX_DOCUMENT)?;
let requested: ProductManifest = read_json_reference(&manifest_ref)?;
requested.validate().map_err(error)?;
if source != &requested.source.commit {
return Err(error("source and product manifest commit disagree"));
}
let measured = collect_manifest(
root,
&state.run,
Path::new(&requested.artifacts.component_wasm.path),
)?;
if measured != requested {
return Err(error(
"product manifest differs from independently measured product bytes, modes, executable, or compiler",
));
}
let identity = digest(uuid::Uuid::now_v7().as_bytes());
let candidate = CandidateRecord::freeze(
identity,
state.run_incarnation.clone(),
measured,
manifest_ref.sha256,
)
.map_err(error)?;
let custody = match state.extra.get(STATE_KEY) {
None => CandidateCustody::new(candidate).map_err(error)?,
Some(value) => {
let mut custody: CandidateCustody =
serde_json::from_value(value.clone()).map_err(error)?;
custody.refreeze(candidate).map_err(error)?;
custody
}
};
let result = custody.current.clone();
persist(state, &custody)?;
return Ok(result);
}
let value = state
.extra
.get(STATE_KEY)
.ok_or_else(|| error("no native candidate is frozen for this run"))?;
let mut custody: CandidateCustody = serde_json::from_value(value.clone()).map_err(error)?;
custody.validate().map_err(error)?;
if custody.current.run != state.run
|| custody.current.run_incarnation != state.run_incarnation
{
return Err(error("candidate is not bound to this run incarnation"));
}
if let CandidateAction::Revoke { reason, .. } = &self.action {
custody.current.revoke(reason.clone()).map_err(error)?;
} else {
if custody.current.state == CandidateState::Revoked {
return Err(error(
"candidate is revoked; freeze a new candidate after repairing its cause",
));
}
let verification = verify_current(root, &custody.current);
if let Err(cause) = verification {
let cause = cause
.message_text()
.unwrap_or("candidate verification failed");
let mut reason = format!("verification failed: {cause}");
if reason.len() > 4096 {
let mut end = 4093;
while !reason.is_char_boundary(end) {
end -= 1;
}
reason.truncate(end);
reason.push_str("...");
}
custody.current.revoke(reason).map_err(error)?;
persist(state, &custody)?;
return Err(error(format!("candidate revoked: {cause}")));
}
match &self.action {
CandidateAction::Verify { stage, .. } => {
custody.current.verify_source().map_err(error)?;
if matches!(stage, CandidateStage::Packages | CandidateStage::Attested)
&& custody.current.packages_manifest.is_none()
{
return Err(error("candidate package stage is not attested"));
}
if *stage == CandidateStage::Attested
&& custody.current.lifecycle_manifest.is_none()
{
return Err(error("candidate lifecycle stage is not attested"));
}
}
CandidateAction::Pack { .. } => custody.current.reserve_pack().map_err(error)?,
CandidateAction::Attest { kind, manifest, .. } => {
let reference = match kind {
AttestationKind::Packages => {
external_reference(root, manifest, MAX_DOCUMENT)?
}
AttestationKind::Lifecycle => {
lifecycle_reference(root, &state.run, manifest)?
}
};
match kind {
AttestationKind::Packages => {
validate_packages(root, &custody.current, &reference)?;
custody.current.attest_packages(reference).map_err(error)?;
}
AttestationKind::Lifecycle => {
validate_lifecycle(root, &custody.current, &reference)?;
custody.current.attest_lifecycle(reference).map_err(error)?;
}
}
}
_ => return Err(error("invalid candidate operation")),
}
}
let result = custody.current.clone();
persist(state, &custody)?;
Ok(result)
}
}
fn ensure_run(state: &RunState, run: &str) -> Result<(), CliError> {
if state.run != run || state.run_incarnation.is_empty() {
return Err(error(
"candidate requires the exact initialized run incarnation",
));
}
Ok(())
}
fn persist(state: &mut RunState, custody: &CandidateCustody) -> Result<(), CliError> {
custody.validate().map_err(error)?;
state.extra.insert(
STATE_KEY.into(),
serde_json::to_value(custody).map_err(error)?,
);
Ok(())
}
fn write_json(context: &mut ExecutionContext, value: &impl Serialize) -> Result<(), CliError> {
let mut bytes = serde_json::to_vec_pretty(value).map_err(error)?;
bytes.push(b'\n');
context.write_stdout(&bytes).map_err(error)
}
fn error(message: impl std::fmt::Display) -> CliError {
CliError::message(format!("candidate custody: {message}"))
}
fn digest(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn git(root: &Path, args: &[&str]) -> Result<Vec<u8>, CliError> {
let executable = crate::dispatch_service::trusted_git_executable().map_err(error)?;
let output = Command::new(executable)
.env_clear()
.env("GIT_OPTIONAL_LOCKS", "0")
.args([
"--no-replace-objects",
"--no-lazy-fetch",
"-c",
"core.fsmonitor=false",
])
.args(args)
.current_dir(root)
.output()
.map_err(error)?;
if !output.status.success() {
return Err(error(format!(
"Git {} failed: {}",
args.first().unwrap_or(&"probe"),
String::from_utf8_lossy(&output.stderr).trim()
)));
}
if output.stdout.len() as u64 > MAX_ARTIFACT {
return Err(error("Git output exceeds the custody byte budget"));
}
Ok(output.stdout)
}
fn git_text(root: &Path, args: &[&str]) -> Result<String, CliError> {
String::from_utf8(git(root, args)?)
.map(|text| text.trim().to_owned())
.map_err(error)
}
fn exact_commit(root: &Path, value: &str) -> Result<(), CliError> {
if !exact_hex(value, 40)
|| git_text(root, &["cat-file", "-t", value])? != "commit"
|| git_text(
root,
&[
"rev-parse",
"--verify",
"--end-of-options",
&format!("{value}^{{object}}"),
],
)? != value
{
return Err(error("source does not identify the exact commit object"));
}
Ok(())
}
fn clean_source(root: &Path) -> Result<(), CliError> {
if !git(root, &["status", "--porcelain=v1", "--untracked-files=all"])?.is_empty() {
return Err(error("candidate freeze requires a clean source checkout"));
}
Ok(())
}
fn compiler_digests(root: &Path) -> Result<CompilerDigests, CliError> {
let authored = load_compile_input(&root.join("content"))?;
let embedded = embedded_compile_input()?;
if authored != embedded {
return Err(error(
"current executable embeds different compiler-owned content",
));
}
let mut digests = Vec::new();
for profile in HarnessProfile::canonical() {
let tree = compile(&authored, &profile).map_err(error)?;
digests.push((profile.target.as_str(), tree.digest));
}
let measured = |name| {
digests
.iter()
.find(|(target, _)| *target == name)
.map(|(_, value)| value.clone())
.ok_or_else(|| error("canonical compiler target is missing"))
};
Ok(CompilerDigests {
claude: measured("claude")?,
codex: measured("codex")?,
pi: measured("pi")?,
})
}
fn collect_manifest(root: &Path, run: &str, component: &Path) -> Result<ProductManifest, CliError> {
clean_source(root)?;
let root = fs::canonicalize(root).map_err(error)?;
let commit = git_text(&root, &["rev-parse", "--verify", "HEAD^{commit}"])?;
exact_commit(&root, &commit)?;
let tree = git_text(&root, &["rev-parse", "--verify", "HEAD^{tree}"])?;
let executable = fs::canonicalize(std::env::current_exe().map_err(error)?).map_err(error)?;
let native_cli = file_reference(&executable, MAX_ARTIFACT, LinkPolicy::SelfMeasured)?;
let component_wasm = file_reference(component, MAX_ARTIFACT, LinkPolicy::Unique)?;
let component_bytes = read_regular(component, MAX_ARTIFACT, LinkPolicy::Unique)?;
if !component_bytes.starts_with(b"\0asm\x0d\0\x01\0")
|| digest(&component_bytes) != component_wasm.sha256
{
return Err(error(
"supplied Component artifact does not have a stable Component Model header/hash",
));
}
let manifest = ProductManifest {
schema: "shepherd.product-manifest/1".into(),
run: run.into(),
source: ProductSource {
root: path_text(&root)?,
commit: commit.clone(),
tree,
},
files: product_files(&root, &commit)?,
compiler: compiler_digests(&root)?,
artifacts: ProductArtifacts {
native_cli,
component_wasm,
},
};
manifest.validate().map_err(error)?;
clean_source(&root)?;
if git_text(&root, &["rev-parse", "HEAD"])? != commit {
return Err(error("HEAD changed during candidate measurement"));
}
Ok(manifest)
}
fn product_files(root: &Path, commit: &str) -> Result<Vec<ProductFile>, CliError> {
let listing = git(root, &["ls-tree", "-rz", "--full-tree", commit])?;
let mut files = Vec::new();
for entry in listing
.split(|byte| *byte == 0)
.filter(|entry| !entry.is_empty())
{
let entry = std::str::from_utf8(entry).map_err(error)?;
let (header, path) = entry
.split_once('\t')
.ok_or_else(|| error("invalid Git tree entry"))?;
let fields: Vec<_> = header.split(' ').collect();
if fields.len() != 3 || fields[1] != "blob" || !canonical_relative(path) {
return Err(error("candidate tree contains unsupported path or object"));
}
if excluded_product_path(path) {
continue;
}
let absolute = root.join(path);
let bytes = if fields[0] == "120000" {
let expected = product_symlink_target(path)
.ok_or_else(|| error(format!("unapproved product symlink: {path}")))?;
let before = fs::symlink_metadata(&absolute).map_err(error)?;
if !before.file_type().is_symlink() {
return Err(error("tracked product symlink is no longer a symlink"));
}
#[cfg(unix)]
let link = rustix::fs::readlinkat(
&open_directory_nofollow(
absolute
.parent()
.ok_or_else(|| error("symlink parent missing"))?,
)?,
absolute
.file_name()
.ok_or_else(|| error("symlink leaf missing"))?,
Vec::new(),
)
.map_err(error)?
.into_bytes();
#[cfg(not(unix))]
let link = {
crate::safe_fs::reject_link_components(
absolute
.parent()
.ok_or_else(|| error("symlink parent missing"))?,
)
.map_err(error)?;
if fs::read_link(&absolute).map_err(error)? != Path::new(expected) {
return Err(error(
"product symlink target differs from its canonical pair",
));
}
expected.as_bytes().to_vec()
};
if link != expected.as_bytes()
|| !same_metadata(
&before,
&fs::symlink_metadata(&absolute).map_err(error)?,
LinkPolicy::Unique,
)
{
return Err(error(format!("product symlink target changed: {path}")));
}
let resolved = fs::canonicalize(&absolute).map_err(error)?;
if !resolved.starts_with(root) {
return Err(error("product symlink escapes the source root"));
}
expected.as_bytes().to_vec()
} else {
if !matches!(fields[0], "100644" | "100755") {
return Err(error("unsupported tracked product mode"));
}
let bytes = read_regular(&absolute, MAX_DOCUMENT, LinkPolicy::Unique)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let executable = fs::symlink_metadata(&absolute)
.map_err(error)?
.permissions()
.mode()
& 0o111
!= 0;
if executable != (fields[0] == "100755") {
return Err(error(format!("tracked product mode changed: {path}")));
}
}
bytes
};
files.push(ProductFile {
path: path.into(),
mode: fields[0].into(),
sha256: digest(&bytes),
});
if files.len() > 65_536 {
return Err(error("product path count exceeds custody budget"));
}
}
files.sort_by(|left, right| left.path.cmp(&right.path));
for file in files.iter().filter(|file| file.mode == "120000") {
let target = fs::canonicalize(root.join(&file.path)).map_err(error)?;
let target = target
.strip_prefix(root)
.map_err(error)?
.to_str()
.ok_or_else(|| error("symlink target is not UTF-8"))?
.replace('\\', "/");
if !files.iter().any(|entry| {
entry.mode != "120000"
&& (entry.path == target || entry.path.starts_with(&format!("{target}/")))
}) {
return Err(error(
"canonical symlink target is absent from the product inventory",
));
}
}
Ok(files)
}
fn verify_current(root: &Path, record: &CandidateRecord) -> Result<(), CliError> {
if path_text(&fs::canonicalize(root).map_err(error)?)? != record.product_manifest.source.root {
return Err(error("candidate belongs to another source worktree"));
}
exact_commit(root, &record.source_commit)?;
let head = git_text(root, &["rev-parse", "--verify", "HEAD^{commit}"])?;
exact_commit(root, &head)?;
git(
root,
&["merge-base", "--is-ancestor", &record.source_commit, &head],
)?;
let mut changes = git(
root,
&[
"diff",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--name-only",
"-z",
&record.source_commit,
&head,
],
)?;
changes.extend(git(
root,
&[
"diff",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--name-only",
"-z",
"HEAD",
],
)?);
changes.extend(git(
root,
&["ls-files", "--others", "--exclude-standard", "-z"],
)?);
for path in changes
.split(|byte| *byte == 0)
.filter(|path| !path.is_empty())
{
let path = std::str::from_utf8(path).map_err(error)?;
if !evidence_only_path(&record.run, path) {
return Err(error(format!(
"post-freeze delta is outside the evidence allowlist: {path}"
)));
}
if fs::symlink_metadata(root.join(path)).is_ok() {
read_regular(&root.join(path), MAX_DOCUMENT, LinkPolicy::Unique)?;
}
}
if product_files(root, &head)? != record.product_manifest.files {
return Err(error(
"product inventory bytes or modes changed after freeze",
));
}
if compiler_digests(root)? != record.product_manifest.compiler {
return Err(error("compiler emissions changed after freeze"));
}
let current = file_reference(
&fs::canonicalize(std::env::current_exe().map_err(error)?).map_err(error)?,
MAX_ARTIFACT,
LinkPolicy::SelfMeasured,
)?;
if current != record.product_manifest.artifacts.native_cli {
return Err(error(
"current native executable is not the frozen candidate",
));
}
check_reference(
&record.product_manifest.artifacts.component_wasm,
MAX_ARTIFACT,
)?;
if let Some(packages) = &record.packages_manifest {
validate_packages(root, record, packages)?;
}
if let Some(lifecycle) = &record.lifecycle_manifest {
validate_lifecycle(root, record, lifecycle)?;
}
if git_text(root, &["rev-parse", "HEAD"])? != head {
return Err(error("HEAD changed during candidate verification"));
}
Ok(())
}
fn path_text(path: &Path) -> Result<String, CliError> {
path.to_str()
.map(str::to_owned)
.ok_or_else(|| error("custody path is not UTF-8"))
}
fn file_reference(path: &Path, limit: u64, policy: LinkPolicy) -> Result<FileReference, CliError> {
let bytes = read_regular(path, limit, policy)?;
let reference = FileReference {
path: path_text(&canonical_location(path)?)?,
sha256: digest(&bytes),
bytes: bytes.len() as u64,
};
reference.validate().map_err(error)?;
Ok(reference)
}
fn external_reference(root: &Path, path: &Path, limit: u64) -> Result<FileReference, CliError> {
if canonical_location(path)?.starts_with(root) {
return Err(error(
"candidate manifest must be external to the source; self-inclusion is forbidden",
));
}
file_reference(path, limit, LinkPolicy::Unique)
}
fn lifecycle_reference(root: &Path, run: &str, path: &Path) -> Result<FileReference, CliError> {
let canonical = canonical_location(path)?;
if let Ok(relative) = canonical.strip_prefix(root)
&& !evidence_only_path(run, &path_text(relative)?.replace('\\', "/"))
{
return Err(error(
"lifecycle manifest inside the source must be current-run evidence",
));
}
file_reference(path, MAX_DOCUMENT, LinkPolicy::Unique)
}
fn check_reference(reference: &FileReference, limit: u64) -> Result<Vec<u8>, CliError> {
reference.validate().map_err(error)?;
let bytes = read_regular(Path::new(&reference.path), limit, LinkPolicy::Unique)?;
if digest(&bytes) != reference.sha256 || bytes.len() as u64 != reference.bytes {
return Err(error(format!("artifact bytes changed: {}", reference.path)));
}
Ok(bytes)
}
fn read_json_reference<T: serde::de::DeserializeOwned>(
reference: &FileReference,
) -> Result<T, CliError> {
serde_json::from_slice(&check_reference(reference, MAX_DOCUMENT)?).map_err(error)
}
fn read_regular(path: &Path, limit: u64, policy: LinkPolicy) -> Result<Vec<u8>, CliError> {
let canonical = canonical_location(path)?;
let path = canonical.as_path();
#[cfg(unix)]
let mut file = {
use rustix::fs::{Mode, OFlags, openat};
let directory = open_directory_nofollow(
path.parent()
.ok_or_else(|| error("custody path has no parent"))?,
)?;
let leaf = path
.file_name()
.ok_or_else(|| error("custody path has no filename"))?;
File::from(
openat(
&directory,
leaf,
OFlags::RDONLY | OFlags::NONBLOCK | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(error)?,
)
};
#[cfg(not(unix))]
let mut file = crate::safe_fs::open_regular_nofollow(path).map_err(error)?;
#[cfg(windows)]
let identity = crate::safe_fs::windows_file_id(&file).map_err(error)?;
let before = file.metadata().map_err(error)?;
let listed = fs::symlink_metadata(path).map_err(error)?;
if !before.is_file() {
return Err(error(format!(
"custody input is not a regular file: {}",
path.display()
)));
}
if before.len() > limit {
return Err(error(format!(
"custody input exceeds {limit} bytes: {}",
path.display()
)));
}
#[cfg(windows)]
if policy == LinkPolicy::Unique
&& crate::safe_fs::windows_file_links(&file).map_err(error)? != 1
{
return Err(error(format!(
"custody input is hardlinked: {}",
path.display()
)));
}
if let Some(field) = metadata_difference(&before, &listed, policy) {
return Err(error(format!(
"custody input is hardlinked or changed while opening ({field}): {}",
path.display()
)));
}
let mut bytes = Vec::new();
(&mut file)
.take(limit + 1)
.read_to_end(&mut bytes)
.map_err(error)?;
let after = file.metadata().map_err(error)?;
if bytes.len() as u64 > limit
|| !same_metadata(&before, &after, policy)
|| !same_metadata(&after, &fs::symlink_metadata(path).map_err(error)?, policy)
|| fs::canonicalize(path).map_err(error)? != path
{
return Err(error("custody input changed while reading"));
}
#[cfg(windows)]
if crate::safe_fs::windows_file_id(&file).map_err(error)? != identity
|| crate::safe_fs::windows_file_id(
&crate::safe_fs::open_regular_nofollow(path).map_err(error)?,
)
.map_err(error)?
!= identity
{
return Err(error("custody file identity changed while reading"));
}
Ok(bytes)
}
fn canonical_location(path: &Path) -> Result<PathBuf, CliError> {
if !path.is_absolute() {
return Err(error("custody path must be absolute"));
}
let canonical = fs::canonicalize(path).map_err(error)?;
#[cfg(windows)]
let equivalent = {
let spelling = |path: &Path| -> Result<String, CliError> {
let value = path_text(path)?;
Ok(if let Some(rest) = value.strip_prefix(r"\\?\UNC\") {
format!(r"\\{rest}")
} else {
value.strip_prefix(r"\\?\").unwrap_or(&value).into()
})
};
spelling(path)? == spelling(&canonical)?
};
#[cfg(not(windows))]
let equivalent = canonical == path;
if !equivalent {
return Err(error(format!(
"path is not canonical and link-free: {}",
path.display()
)));
}
Ok(canonical)
}
#[cfg(unix)]
fn open_directory_nofollow(path: &Path) -> Result<rustix::fd::OwnedFd, CliError> {
use rustix::fs::{Mode, OFlags, open, openat};
if !path.is_absolute() {
return Err(error("custody directory is not absolute"));
}
let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
let mut directory = open("/", flags, Mode::empty()).map_err(error)?;
for part in path.components().skip(1) {
let Component::Normal(name) = part else {
return Err(error("noncanonical custody path component"));
};
directory = openat(&directory, name, flags, Mode::empty()).map_err(error)?;
}
Ok(directory)
}
#[derive(
Clone,
Copy,
Eq,
PartialEq,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
enum LinkPolicy {
Unique,
SelfMeasured,
}
fn same_metadata(left: &Metadata, right: &Metadata, policy: LinkPolicy) -> bool {
metadata_difference(left, right, policy).is_none()
}
fn metadata_difference(
left: &Metadata,
right: &Metadata,
policy: LinkPolicy,
) -> Option<&'static str> {
if left.file_type() != right.file_type() {
return Some("file type");
}
if left.len() != right.len() {
return Some("size");
}
if left.modified().ok() != right.modified().ok() {
return Some("modification time");
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if left.dev() != right.dev() {
return Some("device");
}
if left.ino() != right.ino() {
return Some("inode");
}
if left.mode() != right.mode() {
return Some("mode");
}
if left.nlink() != right.nlink() {
return Some("link count");
}
if policy == LinkPolicy::Unique && (left.nlink() != 1 || right.nlink() != 1) {
return Some("link count");
}
if left.ctime() != right.ctime() || left.ctime_nsec() != right.ctime_nsec() {
return Some("status-change time");
}
}
#[cfg(not(unix))]
{
let _ = policy;
if left.created().ok() != right.created().ok() {
return Some("creation time");
}
if left.permissions() != right.permissions() {
return Some("permissions");
}
}
None
}
fn object<'a>(value: &'a Value, key: &str) -> Result<&'a serde_json::Map<String, Value>, CliError> {
value
.get(key)
.and_then(Value::as_object)
.ok_or_else(|| error(format!("missing {key} object")))
}
fn string<'a>(value: &'a Value, pointer: &str) -> Result<&'a str, CliError> {
value
.pointer(pointer)
.and_then(Value::as_str)
.ok_or_else(|| error(format!("missing {pointer} string")))
}
fn validate_packages(
root: &Path,
record: &CandidateRecord,
reference: &FileReference,
) -> Result<(), CliError> {
let value: Value = read_json_reference(reference)?;
if string(&value, "/schema")? != "shepherd.packed-dogfood-input/1"
|| string(&value, "/candidate_id")? != record.candidate_id
|| string(&value, "/source/commit")? != record.source_commit
|| string(&value, "/source/tree")? != record.source_tree
|| canonical_location(Path::new(string(&value, "/source/root")?))?
!= Path::new(&record.product_manifest.source.root)
|| string(&value, "/version")? != env!("CARGO_PKG_VERSION")
{
return Err(error(
"package manifest does not bind the reserved candidate/source/version",
));
}
let accepted = string(&value, "/source/accepted_base")?;
exact_commit(root, accepted)?;
git(
root,
&[
"merge-base",
"--is-ancestor",
accepted,
&record.source_commit,
],
)?;
let candidates = object(&value, "candidate")?;
let expected = [
"native_cli",
"component_js",
"component_wasm",
"adapter_test",
"adapter_pi_test",
"dogfood_prompt",
];
if candidates.len() != expected.len()
|| expected.iter().any(|key| !candidates.contains_key(*key))
{
return Err(error("packed candidate artifact set is not exact"));
}
let mut paths = BTreeSet::new();
for (name, entry) in candidates {
let artifact: FileReference = serde_json::from_value(entry.clone()).map_err(error)?;
validate_external_artifact(root, &artifact, &mut paths)?;
let frozen = match name.as_str() {
"native_cli" => Some(&record.product_manifest.artifacts.native_cli),
"component_wasm" => Some(&record.product_manifest.artifacts.component_wasm),
_ => None,
};
if frozen.is_some_and(|frozen| {
frozen.sha256 != artifact.sha256 || frozen.bytes != artifact.bytes
}) {
return Err(error(
"packed executable/Component differs from the native frozen artifact",
));
}
let source = match name.as_str() {
"adapter_test" => Some("packages/scripts/test-active-adapters.mjs"),
"adapter_pi_test" => Some("packages/scripts/test-active-pi.mjs"),
"dogfood_prompt" => Some("scripts/prompts/packed-dogfood.md"),
_ => None,
};
if let Some(source) = source
&& !record
.product_manifest
.files
.iter()
.any(|file| file.path == source && file.sha256 == artifact.sha256)
{
return Err(error(
"packed adapter or prompt is not the frozen product input",
));
}
#[cfg(unix)]
if name == "native_cli" {
use std::os::unix::fs::PermissionsExt;
if fs::symlink_metadata(&artifact.path)
.map_err(error)?
.permissions()
.mode()
& 0o111
== 0
{
return Err(error("packed native executable has lost executable mode"));
}
}
}
let packages = object(&value, "packages")?;
if packages.len() != PACKAGE_NAMES.len()
|| PACKAGE_NAMES
.iter()
.any(|name| !packages.contains_key(*name))
{
return Err(error(
"packed package inventory must contain exactly four canonical packages",
));
}
let installed = canonical_location(Path::new(string(&value, "/installed/root")?))?;
for (name, package) in packages {
if string(package, "/version")? != env!("CARGO_PKG_VERSION") {
return Err(error("packed package version differs from candidate"));
}
let artifact: FileReference = serde_json::from_value(
package
.get("archive")
.cloned()
.ok_or_else(|| error("archive is missing"))?,
)
.map_err(error)?;
validate_external_artifact(root, &artifact, &mut paths)?;
let metadata: Value = serde_json::from_slice(&read_regular(
&installed.join(name).join("package.json"),
MAX_DOCUMENT,
LinkPolicy::Unique,
)?)
.map_err(error)?;
if string(&metadata, "/name")? != name
|| string(&metadata, "/version")? != env!("CARGO_PKG_VERSION")
{
return Err(error(
"installed package metadata differs from its canonical package/version",
));
}
}
let carriers = object(&value, "carriers")?;
if carriers.len() != 3
|| ["claude", "codex", "pi"]
.iter()
.any(|name| !carriers.contains_key(*name))
{
return Err(error(
"carrier inventory must be exactly Claude, Codex and Pi",
));
}
for pointer in ["/installed", "/carriers/claude", "/carriers/codex"] {
let tree = value
.pointer(pointer)
.ok_or_else(|| error("packed tree reference missing"))?;
let path = canonical_location(Path::new(string(tree, "/root")?))?;
if path.starts_with(root) {
return Err(error("packed tree imports the source checkout"));
}
if tree_digest(&path)? != string(tree, "/sha256")? {
return Err(error(
"packed tree digest differs from measured bytes and modes",
));
}
}
if string(&value, "/carriers/pi/package")? != "@pzzld/pi-shepherd"
|| string(&value, "/carriers/pi/sha256")?
!= string(&packages["@pzzld/pi-shepherd"], "/archive/sha256")?
{
return Err(error("Pi carrier is not the exact packed archive"));
}
for (relative, key) in [
("runtime/shepherd-component.js", "component_js"),
("runtime/shepherd-component.wasm", "component_wasm"),
] {
let bytes = read_regular(
&installed.join("@pzzld/component-runtime").join(relative),
MAX_ARTIFACT,
LinkPolicy::Unique,
)?;
if digest(&bytes) != string(&candidates[key], "/sha256")? {
return Err(error(
"installed Component pair differs from the measured packed candidate",
));
}
}
for profile in HarnessProfile::canonical() {
super::compile::verify_canonical_tree(
&installed.join(format!("@pzzld/{}-shepherd", profile.target.as_str())),
&profile,
)?;
}
let claude = canonical_location(Path::new(string(&value, "/carriers/claude/root")?))?;
let marketplace = canonical_location(Path::new(string(&value, "/carriers/codex/root")?))?;
if claude != marketplace.join("plugins/shepherd") {
return Err(error(
"Claude carrier is not the exact plugin in the Codex marketplace",
));
}
let input = embedded_compile_input()?;
for profile in [HarnessProfile::claude(), HarnessProfile::codex()] {
let tree = compile(&input, &profile).map_err(error)?;
for file in tree.files {
let destination = if profile.target.as_str() == "claude" {
claude.join(&file.path)
} else if let Some(relative) = file.path.strip_prefix(".agents/skills/") {
claude.join("codex/skills").join(relative)
} else {
continue;
};
if read_regular(&destination, MAX_DOCUMENT, LinkPolicy::Unique)?
!= file.content.as_bytes()
{
return Err(error(
"marketplace carrier differs from the frozen compiler output",
));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if fs::symlink_metadata(&destination)
.map_err(error)?
.permissions()
.mode()
& 0o777
!= file.mode
{
return Err(error("marketplace compiler output mode differs"));
}
}
}
}
Ok(())
}
fn validate_external_artifact(
root: &Path,
artifact: &FileReference,
paths: &mut BTreeSet<String>,
) -> Result<(), CliError> {
let canonical = canonical_location(Path::new(&artifact.path))?;
if canonical.starts_with(root) || !paths.insert(path_text(&canonical)?) {
return Err(error(
"packed artifact imports checkout bytes or aliases another artifact",
));
}
check_reference(artifact, MAX_ARTIFACT)?;
Ok(())
}
fn tree_digest(root: &Path) -> Result<String, CliError> {
let canonical = canonical_location(root)?;
let root = canonical.as_path();
let mut pending = vec![(root.to_path_buf(), 0)];
let mut files = BTreeMap::new();
let mut total = 0_u64;
let mut directories = 0;
let root_metadata = fs::symlink_metadata(root).map_err(error)?;
while let Some((directory, depth)) = pending.pop() {
let before = fs::symlink_metadata(&directory).map_err(error)?;
directories += 1;
if depth > 24
|| directories > 8192
|| !before.is_dir()
|| before.file_type().is_symlink()
|| !same_device(&root_metadata, &before)
{
return Err(error("packed tree depth or directory custody is invalid"));
}
for entry in fs::read_dir(&directory).map_err(error)? {
let entry = entry.map_err(error)?;
let path = entry.path();
let metadata = fs::symlink_metadata(&path).map_err(error)?;
if metadata.file_type().is_symlink() || !same_device(&root_metadata, &metadata) {
return Err(error(
"packed tree contains a symlink or cross-device alias",
));
}
if metadata.is_dir() {
pending.push((path, depth + 1));
continue;
}
total = total
.checked_add(metadata.len())
.ok_or_else(|| error("packed tree byte budget overflow"))?;
if files.len() >= 2048 || total > 128 * 1024 * 1024 {
return Err(error(
"packed tree exceeds file-count or aggregate-byte budget",
));
}
let bytes = read_regular(&path, MAX_DOCUMENT, LinkPolicy::Unique)?;
#[cfg(unix)]
let mode = {
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o777
};
#[cfg(not(unix))]
let mode = if metadata.permissions().readonly() {
0o444
} else {
0o666
};
let relative = path
.strip_prefix(root)
.map_err(error)?
.to_str()
.ok_or_else(|| error("packed tree path is not UTF-8"))?
.replace('\\', "/");
files.insert(relative, (mode, bytes));
}
let after = fs::symlink_metadata(&directory).map_err(error)?;
if !same_directory(&before, &after) {
return Err(error("packed tree directory changed during enumeration"));
}
}
if files.is_empty() {
return Err(error("packed tree is empty"));
}
let mut hash = Sha256::new();
for (path, (mode, bytes)) in files {
hash.update(path.as_bytes());
hash.update([0]);
hash.update(mode.to_string().as_bytes());
hash.update([0]);
hash.update(&bytes);
hash.update([0]);
}
Ok(hash
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
fn same_device(left: &Metadata, right: &Metadata) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
left.dev() == right.dev()
}
#[cfg(not(unix))]
{
let _ = (left, right);
true
}
}
fn same_directory(left: &Metadata, right: &Metadata) -> bool {
if !left.is_dir() || !right.is_dir() || left.modified().ok() != right.modified().ok() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
left.dev() == right.dev()
&& left.ino() == right.ino()
&& left.mode() == right.mode()
&& left.nlink() == right.nlink()
&& left.ctime() == right.ctime()
&& left.ctime_nsec() == right.ctime_nsec()
}
#[cfg(not(unix))]
{
left.created().ok() == right.created().ok() && left.permissions() == right.permissions()
}
}
fn validate_lifecycle(
root: &Path,
record: &CandidateRecord,
reference: &FileReference,
) -> Result<(), CliError> {
let packages = record
.packages_manifest
.as_ref()
.ok_or_else(|| error("lifecycle requires native package attestation"))?;
let report: Value = read_json_reference(reference)?;
if string(&report, "/schema")? != "shepherd.packed-dogfood-report/1"
|| string(&report, "/candidate_id")? != record.candidate_id
|| string(&report, "/version")? != env!("CARGO_PKG_VERSION")
|| string(&report, "/status")? != "complete"
|| string(&report, "/manifest_sha256")? != packages.sha256
|| canonical_location(Path::new(string(&report, "/manifest_path")?))?
!= Path::new(&packages.path)
|| string(&report, "/source/commit")? != record.source_commit
|| string(&report, "/source/tree")? != record.source_tree
|| report
.get("applicable_hosts_passed")
.and_then(Value::as_u64)
!= Some(3)
|| report
.get("applicable_hosts_required")
.and_then(Value::as_u64)
!= Some(3)
{
return Err(error(
"lifecycle report is not exact complete three-host package-bound acceptance",
));
}
let hosts = report
.get("hosts")
.and_then(Value::as_array)
.ok_or_else(|| error("lifecycle host list missing"))?;
if hosts.len() != 3 {
return Err(error("lifecycle must contain exactly three hosts"));
}
let mut names = BTreeSet::new();
let package_document: Value = read_json_reference(packages)?;
let matrix = canonical_location(Path::new(string(&report, "/matrix_root")?))?;
checked_external_directory(root, &matrix)?;
let mut host_roots: Vec<PathBuf> = Vec::new();
let mut paths = BTreeSet::new();
for host in hosts {
let name = string(host, "/harness")?;
if !["claude", "codex", "pi"].contains(&name)
|| !names.insert(name)
|| string(host, "/status")? != "passed"
|| string(host, "/binary/resolution")? != "path"
|| host.pointer("/invocation/exit").and_then(Value::as_i64) != Some(0)
{
return Err(error(
"lifecycle host identity is missing, duplicated, overridden or failed",
));
}
let artifacts = host
.get("artifacts")
.and_then(Value::as_array)
.ok_or_else(|| error("host artifacts missing"))?;
if artifacts.is_empty() {
return Err(error("host artifacts are empty"));
}
let project = Path::new(string(host, "/roots/project")?);
let session = Path::new(string(host, "/roots/session")?);
for key in ["project", "home", "cache", "session"] {
let path = canonical_location(Path::new(string(host, &format!("/roots/{key}"))?))?;
checked_external_directory(root, &path)?;
if path == matrix
|| !path.starts_with(&matrix)
|| host_roots
.iter()
.any(|other| path.starts_with(other) || other.starts_with(&path))
{
return Err(error(
"lifecycle roots are not fresh disjoint directories under the matrix",
));
}
host_roots.push(path.to_path_buf());
}
for value in artifacts {
let artifact: FileReference = serde_json::from_value(value.clone()).map_err(error)?;
if !Path::new(&artifact.path).starts_with(project) {
return Err(error("lifecycle artifact escapes its fresh project"));
}
validate_external_artifact(root, &artifact, &mut paths)?;
}
let proofs = host
.get("machine_proofs")
.and_then(Value::as_array)
.ok_or_else(|| error("lifecycle machine proofs missing"))?;
let count = host
.get("executed_cases")
.and_then(Value::as_u64)
.ok_or_else(|| error("lifecycle case count missing"))?;
let order = lifecycle_case_order(name);
if proofs.len() as u64 != count || proofs.len() != order.len() {
return Err(error(
"lifecycle case count and exact machine matrix disagree",
));
}
let mut previous = "0".repeat(64);
for (sequence, (proof, case)) in proofs.iter().zip(order).enumerate() {
let artifact = partial_reference(proof)?;
if Path::new(&artifact.path)
!= session
.join("machine-evidence")
.join(format!("{case}.json"))
{
return Err(error(
"machine proof is not the exact case file in its host session",
));
}
validate_external_artifact(root, &artifact, &mut paths)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if fs::symlink_metadata(&artifact.path)
.map_err(error)?
.permissions()
.mode()
& 0o777
!= 0o600
{
return Err(error("machine proof must retain mode 0600"));
}
}
let envelope: Value = read_json_reference(&artifact)?;
if envelope.as_object().is_none_or(|object| object.len() != 2)
|| !exact_hex(string(&envelope, "/hmac")?, 64)
{
return Err(error(
"machine proof has no exact retained recorder envelope",
));
}
let payload = envelope
.get("payload")
.ok_or_else(|| error("machine payload missing"))?;
let denied = (name == "codex" && ["dispatch", "report", "compaction"].contains(&case))
|| (name == "pi" && case == "compaction");
let status = if denied {
"observed-fail-closed"
} else {
"passed"
};
if string(payload, "/schema")? != "shepherd.packed-dogfood-machine-case/5"
|| string(payload, "/harness")? != name
|| string(payload, "/run")? != format!("dogfood-{name}")
|| string(payload, "/selection/run")? != record.run
|| string(payload, "/selection/phase")? != "planning"
|| string(payload, "/case")? != case
|| string(payload, "/status")? != status
|| payload.get("sequence").and_then(Value::as_u64) != Some(sequence as u64)
|| string(payload, "/previous_proof_sha256")? != previous
|| string(payload, "/manifest_sha256")? != packages.sha256
|| string(payload, "/candidate_sha256")?
!= record.product_manifest.artifacts.native_cli.sha256
|| string(payload, "/cwd")? != path_text(project)?
|| !["recipe_sha256", "state_before_sha256", "state_after_sha256"]
.iter()
.all(|key| {
payload
.get(*key)
.and_then(Value::as_str)
.is_some_and(|value| exact_hex(value, 64))
})
{
return Err(error(
"machine proof differs from its native candidate, case, run or hash chain",
));
}
for key in [
"case",
"status",
"exit",
"sequence",
"recipe_sha256",
"previous_proof_sha256",
"selection",
"verified_lane_input",
"review_checkpoint",
"terminal_verification",
] {
if proof.get(key) != payload.get(key) {
return Err(error(format!(
"reported machine {key} differs from retained envelope"
)));
}
}
let exit = payload
.get("exit")
.and_then(Value::as_i64)
.ok_or_else(|| error("machine proof exit missing"))?;
if (!denied && exit != 0) || (denied && exit <= 0) {
return Err(error(
"machine case did not record its required success or fail-closed exit",
));
}
let commands = payload
.get("commands")
.and_then(Value::as_array)
.filter(|commands| !commands.is_empty())
.ok_or_else(|| error("machine command results missing"))?;
let declared = proof
.get("commands")
.and_then(Value::as_array)
.ok_or_else(|| error("reported machine commands missing"))?;
if declared.len() != commands.len()
|| commands
.last()
.and_then(|command| command.get("exit"))
.and_then(Value::as_i64)
!= Some(exit)
{
return Err(error("machine command count or final exit differs"));
}
for (command, reported) in commands.iter().zip(declared) {
let argv = command
.get("command")
.and_then(Value::as_array)
.filter(|args| args.len() >= 2 && args.iter().all(Value::is_string))
.ok_or_else(|| error("machine command argv missing"))?;
if command.get("command") != Some(reported)
|| argv[0] != package_document["candidate"]["native_cli"]["path"]
|| command.get("exit").and_then(Value::as_i64).is_none()
|| !["stdout_sha256", "stderr_sha256"].iter().all(|key| {
command
.get(*key)
.and_then(Value::as_str)
.is_some_and(|value| exact_hex(value, 64))
})
|| !["stdout_bytes", "stderr_bytes"]
.iter()
.all(|key| command.get(*key).and_then(Value::as_u64).is_some())
{
return Err(error(
"machine command is not the retained candidate execution",
));
}
}
if let Some(round) = [
"review-redo-1",
"review-redo-2",
"review-redo-3",
"review-rejection-4",
]
.iter()
.position(|name| *name == case)
{
if string(payload, "/review_checkpoint/schema")?
!= "shepherd.packed-review-checkpoint/1"
|| payload
.pointer("/review_checkpoint/round")
.and_then(Value::as_u64)
!= Some(round as u64 + 1)
{
return Err(error(
"machine review checkpoint is missing or out of order",
));
}
} else if case == "review-terminal-denials"
&& (string(payload, "/terminal_verification/schema")?
!= "shepherd.review-terminal-verification/1"
|| string(payload, "/terminal_verification/proof_kind")?
!= "native-prelaunch-predicate-denials"
|| [
"broker_peer_attempted",
"provider_launch_attempted",
"subject_mutated",
]
.iter()
.any(|key| {
payload
.pointer(&format!("/terminal_verification/{key}"))
.and_then(Value::as_bool)
!= Some(false)
}))
{
return Err(error(
"terminal review proof claims unsupported broker/provider work or mutation",
));
}
previous = artifact.sha256;
}
}
Ok(())
}
fn partial_reference(value: &Value) -> Result<FileReference, CliError> {
let reference = FileReference {
path: string(value, "/path")?.into(),
sha256: string(value, "/sha256")?.into(),
bytes: value
.get("bytes")
.and_then(Value::as_u64)
.ok_or_else(|| error("artifact byte count missing"))?,
};
reference.validate().map_err(error)?;
Ok(reference)
}
fn checked_external_directory(source: &Path, path: &Path) -> Result<(), CliError> {
let canonical = canonical_location(path)?;
let path = canonical.as_path();
if path.starts_with(source) || !fs::symlink_metadata(path).map_err(error)?.is_dir() {
return Err(error(
"lifecycle directory must be external, canonical and link-free",
));
}
#[cfg(unix)]
let _ = open_directory_nofollow(path)?;
#[cfg(not(unix))]
crate::safe_fs::reject_link_components(path).map_err(error)?;
Ok(())
}
fn lifecycle_case_order(harness: &str) -> Vec<&'static str> {
let mut order = vec!["project-init", "user-home-init", "run-init", "plant"];
if harness == "pi" {
order.extend([
"engineer-orientation",
"concurrent-auditor-discovery",
"critic-revision",
"nested-project-run-custody",
]);
}
order.extend(["plan-verify", "planned", "start"]);
if harness == "codex" {
order.push("hook-guard");
}
order.extend([
"bounded-allow",
"bounded-overflow-deny",
"review-redo-1",
"review-redo-2",
"review-redo-3",
"review-rejection-4",
"review-terminal-denials",
"read-only-review",
"dispatch",
"report",
"compaction",
"stop",
"resume",
"close",
]);
order
}