use std::{
fs,
path::{Component, Path},
process::Command,
};
use serde_json::Value;
use shepherd::digest::{sha256_hex, sha256_path};
use crate::interface::CliError;
const RELEASED_BASELINE: &str = "d7520c110f91ae7276acd720be545fcb5e869249";
const HARNESSES: [&str; 3] = ["claude", "codex", "pi"];
const MANIFESTS: [(&str, &str); 4] = [
(
"component-runtime",
"packages/component-runtime/package.json",
),
("claude-shepherd", "packages/harness-claude/package.json"),
("codex-shepherd", "packages/harness-codex/package.json"),
("pi-shepherd", "packages/harness-pi/package.json"),
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct EvidenceSelection {
pub(super) run: String,
pub(super) evidence: String,
pub(super) packed_inputs: String,
pub(super) evidence_dir: String,
}
fn parse_evidence_relative(relative: &str) -> Result<EvidenceSelection, CliError> {
let components = Path::new(relative)
.components()
.map(|component| match component {
Component::Normal(value) => value
.to_str()
.map(str::to_owned)
.ok_or_else(|| error("evidence path must be UTF-8")),
_ => Err(error(
"evidence path must be canonical and repository-relative",
)),
})
.collect::<Result<Vec<_>, _>>()?;
let [namespace, runs, run, lanes, lane, evidence, file] = components.as_slice() else {
return Err(error(
"evidence path must name the owned active-adapters artifact",
));
};
if namespace != ".shepherd"
|| runs != "runs"
|| lanes != "lanes"
|| lane != "harness-semantics"
|| evidence != "evidence"
|| file != "active-adapters.json"
|| !super::run::is_canonical(run)
{
return Err(error(
"evidence path must name one canonical run's active-adapters artifact",
));
}
let evidence_dir = format!(".shepherd/runs/{run}/lanes/harness-semantics/evidence");
Ok(EvidenceSelection {
run: run.clone(),
evidence: format!("{evidence_dir}/active-adapters.json"),
packed_inputs: format!(".shepherd/runs/{run}/lanes/harness-semantics/packed-inputs.json"),
evidence_dir,
})
}
pub(super) fn select_evidence(
evidence_path: &Path,
root: &Path,
) -> Result<EvidenceSelection, CliError> {
let root = fs::canonicalize(root)
.map_err(|source| error(format!("cannot resolve repository root: {source}")))?;
let candidate = if evidence_path.is_absolute() {
evidence_path.to_path_buf()
} else {
root.join(evidence_path)
};
let canonical = fs::canonicalize(&candidate)
.map_err(|source| error(format!("cannot resolve selected evidence: {source}")))?;
if candidate != canonical {
return Err(error(
"selected evidence path must be canonical and contain no symlink aliases",
));
}
let relative = canonical
.strip_prefix(&root)
.map_err(|_| error("selected evidence is outside the repository root"))?
.to_str()
.ok_or_else(|| error("selected evidence path must be UTF-8"))?;
let selection = parse_evidence_relative(relative)?;
let mut walked = root;
for component in Path::new(&selection.evidence).components() {
let Component::Normal(component) = component else {
return Err(error("selected evidence path is not normalized"));
};
walked.push(component);
if path_is_link(&walked)
.map_err(|source| error(format!("cannot inspect selected evidence: {source}")))?
{
return Err(error("selected evidence path traverses a link"));
}
}
if !fs::symlink_metadata(&walked).is_ok_and(|metadata| metadata.file_type().is_file()) {
return Err(error("selected evidence must be a regular file"));
}
Ok(selection)
}
#[cfg(unix)]
fn path_is_link(path: &Path) -> std::io::Result<bool> {
fs::symlink_metadata(path).map(|metadata| metadata.file_type().is_symlink())
}
#[cfg(not(unix))]
fn path_is_link(path: &Path) -> std::io::Result<bool> {
crate::safe_fs::is_link(path)
}
pub(crate) fn validate(evidence_path: &Path, root: &Path) -> Result<(), CliError> {
let selection = select_evidence(evidence_path, root)?;
let evidence = read_json(&root.join(&selection.evidence))?;
equal(&evidence, "/schema", "shepherd.active-adapters-evidence/3")?;
validate_accepted_base(root, text(&evidence, "/accepted_base")?, RELEASED_BASELINE)?;
let candidate_tree = candidate_tree(root, &selection)?;
equal(&evidence, "/candidate_tree", &candidate_tree)?;
let input_lock = read_json(&root.join(&selection.packed_inputs))?;
equal(&input_lock, "/schema", "shepherd.packed-inputs/1")?;
if evidence.pointer("/inputs") != input_lock.pointer("/inputs") {
return Err(error(
"packed input hash evidence does not match the measured input lock",
));
}
validate_pi_observer(&evidence, root, &selection, "message_end")?;
validate_pi_observer(&evidence, root, &selection, "session_shutdown")?;
let command = evidence
.pointer("/probe_command")
.and_then(Value::as_array)
.ok_or_else(|| error("probe_command must be an array"))?;
let expected = [
"scripts/test-packed-plugin.sh",
"--evidence",
selection.evidence.as_str(),
];
if command.iter().filter_map(Value::as_str).collect::<Vec<_>>() != expected {
return Err(error(
"probe_command is not the exact packed gate invocation",
));
}
for (name, relative) in MANIFESTS {
let recorded = text(
&evidence,
&format!("/inputs/package_manifest_sha256/{name}"),
)?;
let manifest = contained(root, relative)?;
let hashed = sha256_path(&manifest)
.map_err(|e| error(format!("cannot hash {}: {e}", manifest.display())))?;
if recorded != hashed {
return Err(error(format!("packed {name} manifest hash is stale")));
}
}
let tarballs = evidence
.pointer("/inputs/tarball_sha256")
.and_then(Value::as_object)
.ok_or_else(|| error("tarball_sha256 must be an object"))?;
let expected_tarballs = [
"pzzld-component-runtime-6.6.1.tgz",
"pzzld-claude-shepherd-6.6.1.tgz",
"pzzld-codex-shepherd-6.6.1.tgz",
"pzzld-pi-shepherd-6.6.1.tgz",
];
if tarballs.len() != expected_tarballs.len()
|| expected_tarballs.iter().any(|name| {
tarballs
.get(*name)
.and_then(Value::as_str)
.is_none_or(|hash| !is_hash(hash))
})
{
return Err(error(
"active adapter evidence lacks the exact four packed tarball hashes",
));
}
for harness in HARNESSES {
validate_chain(&evidence, root, &selection, harness)?;
}
validate_matrix_evidence(&evidence, root)?;
Ok(())
}
fn validate_accepted_base(
root: &Path,
accepted_base: &str,
released_baseline: &str,
) -> Result<(), CliError> {
let is_commit_id = |value: &str| {
value.len() == 40
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
};
for (label, object) in [
("accepted_base", accepted_base),
("released baseline", released_baseline),
] {
if !is_commit_id(object) {
return Err(error(format!(
"{label} must be an exact lowercase 40-hex commit id"
)));
}
}
let git = crate::dispatch_service::trusted_git_executable()
.map_err(|source| error(format!("cannot locate trusted Git: {source}")))?;
let probe = |arguments: &[&str]| {
Command::new(&git)
.env_clear()
.args(["--no-replace-objects", "--no-lazy-fetch"])
.args(arguments)
.current_dir(root)
.output()
.map_err(|source| error(format!("cannot inspect accepted-base provenance: {source}")))
};
for (label, object) in [
("accepted_base", accepted_base),
("released baseline", released_baseline),
] {
let resolved = probe(&[
"rev-parse",
"--verify",
"--end-of-options",
&format!("{object}^{{object}}"),
])?;
if !resolved.status.success()
|| std::str::from_utf8(&resolved.stdout).map(str::trim).ok() != Some(object)
{
return Err(error(format!(
"{label} does not name the exact existing commit object"
)));
}
let kind = probe(&["cat-file", "-t", object])?;
if !kind.status.success() || kind.stdout != b"commit\n" {
return Err(error(format!(
"{label} must name a commit, not a tag, tree, or blob"
)));
}
}
let head = probe(&["rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"])?;
if !head.status.success() {
return Err(error("candidate HEAD must resolve to an existing commit"));
}
let head = String::from_utf8(head.stdout)
.map_err(|_| error("candidate HEAD commit id is not UTF-8"))?;
let head = head.trim();
if !is_commit_id(head) {
return Err(error(
"candidate HEAD is not an exact lowercase 40-hex commit id",
));
}
for (ancestor, descendant, failure) in [
(
released_baseline,
accepted_base,
"accepted_base must descend from the released baseline",
),
(
accepted_base,
head,
"accepted_base must be an ancestor of candidate HEAD",
),
] {
let ancestry = probe(&["merge-base", "--is-ancestor", ancestor, descendant])?;
match ancestry.status.code() {
Some(0) => {}
Some(1) => return Err(error(failure)),
_ => {
return Err(error(format!(
"cannot prove accepted-base ancestry: {}",
String::from_utf8_lossy(&ancestry.stderr).trim()
)));
}
}
}
Ok(())
}
fn candidate_tree(root: &Path, selection: &EvidenceSelection) -> Result<String, CliError> {
let index = std::env::temp_dir().join(format!(
"shepherd-{}-candidate-{}.index",
selection.run,
std::process::id()
));
let _ = fs::remove_file(&index);
let run = |arguments: &[&str]| -> Result<std::process::Output, CliError> {
Command::new("git")
.args(arguments)
.current_dir(root)
.env("GIT_INDEX_FILE", &index)
.output()
.map_err(|source| error(format!("cannot execute git candidate-tree probe: {source}")))
};
let result = (|| {
let commands = [
vec!["read-tree", "HEAD"],
vec!["add", "-A"],
vec!["add", "-f", "--", selection.packed_inputs.as_str()],
vec![
"rm",
"-r",
"-q",
"--cached",
"--ignore-unmatch",
"--",
selection.evidence_dir.as_str(),
],
];
for arguments in &commands {
let output = run(arguments)?;
if !output.status.success() {
return Err(error(format!(
"git candidate-tree step failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
}
let output = run(&["write-tree"])?;
if !output.status.success() {
return Err(error(format!(
"git candidate-tree write failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
let tree = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if tree.len() != 40 || !tree.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(error("git candidate-tree output is not an object id"));
}
Ok(tree)
})();
let _ = fs::remove_file(index);
result
}
fn validate_pi_observer(
evidence: &Value,
root: &Path,
selection: &EvidenceSelection,
observer: &str,
) -> Result<(), CliError> {
let prefix = format!("/pi_terminal_observers/{observer}");
equal(evidence, &format!("{prefix}/observer"), observer)?;
for phase in ["start", "stop"] {
equal(evidence, &format!("{prefix}/{phase}/request/harness"), "pi")?;
equal(
evidence,
&format!("{prefix}/{phase}/response/harness"),
"pi",
)?;
equal(
evidence,
&format!("{prefix}/{phase}/response/run"),
&selection.run,
)?;
}
equal(evidence, &format!("{prefix}/start/source"), "managed-child")?;
equal(evidence, &format!("{prefix}/start/operation"), "start")?;
equal(
evidence,
&format!("{prefix}/start/response/state"),
"active",
)?;
equal(evidence, &format!("{prefix}/stop/source"), "managed-child")?;
equal(evidence, &format!("{prefix}/stop/operation"), "stop")?;
equal(
evidence,
&format!("{prefix}/stop/response/state"),
"stopped",
)?;
if evidence
.pointer(&format!("{prefix}/start/response/revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/stop/request/expected_revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/stop/response/revision"))
.and_then(Value::as_u64)
!= Some(2)
{
return Err(error(format!(
"Pi {observer} did not prove revision-1 to revision-2 stop"
)));
}
for field in [
"run",
"harness",
"agent_id",
"agent_type",
"role",
"session_id",
] {
same(
evidence,
&format!("{prefix}/start/response/{field}"),
&format!("{prefix}/stop/response/{field}"),
observer,
)?;
}
for field in ["harness", "agent_id", "agent_type", "session_id"] {
same(
evidence,
&format!("{prefix}/start/request/{field}"),
&format!("{prefix}/stop/request/{field}"),
observer,
)?;
for phase in ["start", "stop"] {
same(
evidence,
&format!("{prefix}/{phase}/request/{field}"),
&format!("{prefix}/{phase}/response/{field}"),
observer,
)?;
}
}
let carrier = format!(
"shepherd:{}",
text(evidence, &format!("{prefix}/start/response/role"))?
);
equal(
evidence,
&format!("{prefix}/start/request/role_carrier"),
&carrier,
)?;
equal(
evidence,
&format!("{prefix}/stop/request/role_carrier"),
&carrier,
)?;
validate_completion_artifact(evidence, root, selection, &prefix)?;
let artifact = text(evidence, &format!("{prefix}/result_artifact/path"))?;
let expected = format!("{}/reports/pi-{observer}.md", selection.evidence_dir);
if artifact != expected {
return Err(error(format!(
"Pi {observer} result artifact path is not owned"
)));
}
let content = text(evidence, &format!("{prefix}/result_artifact/content"))?;
let hash = text(evidence, &format!("{prefix}/result_artifact/sha256"))?;
let bytes = fs::read(contained(root, artifact)?).map_err(|source| {
error(format!(
"cannot read Pi {observer} result artifact: {source}"
))
})?;
if bytes != content.as_bytes() || sha256_hex(&bytes) != hash {
return Err(error(format!("Pi {observer} result artifact is stale")));
}
Ok(())
}
fn validate_completion_artifact(
evidence: &Value,
root: &Path,
selection: &EvidenceSelection,
prefix: &str,
) -> Result<(), CliError> {
let start_reference = text(
evidence,
&format!("{prefix}/start/response/result_artifact"),
)?;
let stop_reference = text(evidence, &format!("{prefix}/stop/response/result_artifact"))?;
if start_reference != stop_reference {
return Err(error(format!(
"{prefix} native completion allocation changed across stop"
)));
}
if let Some(request_reference) =
evidence.pointer(&format!("{prefix}/stop/request/result_artifact"))
&& request_reference.as_str() != Some(start_reference)
{
return Err(error(format!(
"{prefix} stop request changed the native completion allocation"
)));
}
let run_prefix = format!(".shepherd/runs/{}/", selection.run);
let artifact = if start_reference.starts_with(&run_prefix) {
start_reference.to_owned()
} else if start_reference.starts_with(".shepherd/runs/") {
return Err(error(format!(
"{prefix} completion artifact belongs to a different run"
)));
} else {
format!("{run_prefix}{start_reference}")
};
if text(evidence, &format!("{prefix}/completion_artifact/path"))? != artifact {
return Err(error(format!(
"{prefix} completion artifact path is not the native allocation"
)));
}
let content = text(evidence, &format!("{prefix}/completion_artifact/content"))?;
let recorded = text(evidence, &format!("{prefix}/completion_artifact/sha256"))?;
if !is_hash(recorded) || sha256_hex(content.as_bytes()) != recorded {
return Err(error(format!(
"{prefix} completion artifact content hash is invalid"
)));
}
let _path = contained(root, &artifact)?;
let document: Value = serde_json::from_slice(content.as_bytes())
.map_err(|source| error(format!("native completion artifact is not JSON: {source}")))?;
if document.pointer("/schema").and_then(Value::as_str) != Some("shepherd.completion/1")
|| document
.pointer("/body")
.and_then(Value::as_str)
.is_none_or(str::is_empty)
{
return Err(error(format!(
"{prefix} native completion document is malformed"
)));
}
for field in ["project_id", "run", "lane", "role", "agent_id"] {
if document.get(field) != evidence.pointer(&format!("{prefix}/start/response/{field}")) {
return Err(error(format!(
"{prefix} completion identity does not match start"
)));
}
}
if document.get("nonce") != evidence.pointer(&format!("{prefix}/start/response/result_nonce")) {
return Err(error(format!(
"{prefix} completion nonce does not match start"
)));
}
Ok(())
}
fn validate_matrix_evidence(evidence: &Value, root: &Path) -> Result<(), CliError> {
for harness in HARNESSES {
let chain = format!("/real_chains/{harness}");
let matrix =
|event: &str, field: &str| format!("/matrix_evidence/{event}/{harness}/{field}");
for event in ["identity", "dispatch"] {
same(
evidence,
&matrix(event, "native_input"),
&format!("{chain}/resolve/request"),
harness,
)?;
same(
evidence,
&matrix(event, "native_result"),
&format!("{chain}/resolve/response"),
harness,
)?;
}
let lifecycle_input = evidence
.pointer(&matrix("lifecycle", "native_input"))
.ok_or_else(|| error("missing lifecycle matrix input"))?;
if lifecycle_input.get("start") != evidence.pointer(&format!("{chain}/start/request"))
|| lifecycle_input.get("stop") != evidence.pointer(&format!("{chain}/stop/request"))
{
return Err(error(format!(
"{harness} lifecycle matrix input is not the real chain"
)));
}
let lifecycle_result = evidence
.pointer(&matrix("lifecycle", "native_result"))
.ok_or_else(|| error("missing lifecycle matrix result"))?;
if lifecycle_result.get("start") != evidence.pointer(&format!("{chain}/start/response"))
|| lifecycle_result.get("stop") != evidence.pointer(&format!("{chain}/stop/response"))
{
return Err(error(format!(
"{harness} lifecycle matrix result is not the real chain"
)));
}
same(
evidence,
&matrix("guard", "native_input"),
&format!("{chain}/guard/request"),
harness,
)?;
same(
evidence,
&matrix("guard", "native_result"),
&format!("{chain}/guard/response"),
harness,
)?;
same(
evidence,
&matrix("report", "native_input"),
&format!("{chain}/stop/request"),
harness,
)?;
same(
evidence,
&matrix("report", "native_result"),
&format!("{chain}/result_artifact"),
harness,
)?;
same(
evidence,
&matrix("failure", "native_input"),
&format!("/negative_controls/{harness}/request"),
harness,
)?;
same(
evidence,
&matrix("failure", "native_result"),
&format!("/negative_controls/{harness}/result"),
harness,
)?;
for event in [
"identity",
"lifecycle",
"dispatch",
"guard",
"report",
"compaction",
"failure",
"host-limitations",
] {
same(
evidence,
&matrix(event, "negative_control"),
&format!("/negative_controls/{harness}"),
harness,
)?;
}
let rejected = evidence
.pointer(&format!("/negative_controls/{harness}"))
.ok_or_else(|| error(format!("missing {harness} negative control")))?;
for field in [
"run",
"harness",
"agent_type",
"role_carrier",
"lane",
"session_id",
"tool_call_id",
"tool_name",
"tool_input",
] {
same(
evidence,
&format!("/negative_controls/{harness}/request/{field}"),
&format!("{chain}/resolve/request/{field}"),
harness,
)?;
}
let rejected_agent = rejected
.pointer("/request/agent_id")
.and_then(Value::as_str)
.ok_or_else(|| error(format!("missing {harness} rejected agent id")))?;
let rejected_run = rejected
.pointer("/request/run")
.and_then(Value::as_str)
.ok_or_else(|| error(format!("missing {harness} rejected run")))?;
let detail = rejected
.pointer("/result/detail")
.and_then(Value::as_str)
.unwrap_or_default();
if rejected.pointer("/result/ok") != Some(&Value::Bool(false))
|| detail.is_empty()
|| !detail.contains(rejected_agent)
|| !detail.contains(rejected_run)
|| rejected.pointer("/request/agent_id")
== evidence.pointer(&format!("{chain}/start/request/agent_id"))
{
return Err(error(format!(
"{harness} negative control did not prove cross-identity rejection"
)));
}
let compaction_input = evidence
.pointer(&matrix("compaction", "native_input"))
.ok_or_else(|| error("missing compaction input"))?;
if compaction_input.get("event").and_then(Value::as_str) != Some("PreCompact") {
return Err(error(format!(
"{harness} compaction input is not native PreCompact"
)));
}
let compaction = evidence
.pointer(&matrix("compaction", "native_result"))
.ok_or_else(|| error("missing compaction result"))?;
if harness == "claude" {
if compaction.get("supported") != Some(&Value::Bool(true))
|| compaction.get("command").and_then(Value::as_str)
!= Some("hooks/tests/test_compaction_run_scope.sh")
|| !root
.join("hooks/tests/test_compaction_run_scope.sh")
.is_file()
|| compaction.get("exit").and_then(Value::as_i64) != Some(0)
|| compaction.get("output").and_then(Value::as_str)
!= Some("test_compaction_run_scope: PASS")
{
return Err(error(
"Claude compaction probe did not execute successfully",
));
}
} else if compaction.get("supported") != Some(&Value::Bool(false))
|| compaction.get("advertised") != Some(&Value::Bool(false))
{
return Err(error(format!(
"{harness} falsely advertises compaction support"
)));
}
}
validate_host_inventory(evidence, root)
}
fn validate_host_inventory(evidence: &Value, root: &Path) -> Result<(), CliError> {
for (harness, relative) in [
("claude", "hooks/hooks.json"),
("codex", "plugins/shepherd/codex/hooks/hooks.json"),
] {
let manifest = read_json(&root.join(relative))?;
let mut events = manifest
.pointer("/hooks")
.and_then(Value::as_object)
.ok_or_else(|| error(format!("{harness} hook inventory is malformed")))?
.keys()
.cloned()
.collect::<Vec<_>>();
events.sort();
let compaction = events.iter().any(|event| event == "PreCompact");
let expected = Value::Array(events.into_iter().map(Value::String).collect());
if evidence.pointer(&format!(
"/matrix_evidence/host-limitations/{harness}/native_result/events"
)) != Some(&expected)
|| evidence.pointer(&format!(
"/matrix_evidence/compaction/{harness}/native_input/advertised_events"
)) != Some(&expected)
|| evidence.pointer(&format!(
"/matrix_evidence/host-limitations/{harness}/native_result/compaction"
)) != Some(&Value::Bool(compaction))
|| evidence.pointer(&format!(
"/matrix_evidence/compaction/{harness}/native_result/supported"
)) != Some(&Value::Bool(compaction))
{
return Err(error(format!("{harness} host inventory is stale")));
}
}
let pi_manifest = read_json(&root.join("packages/harness-pi/shepherd.pi.json"))?;
let pi_hooks = pi_manifest
.pointer("/hooks")
.and_then(Value::as_object)
.ok_or_else(|| error("Pi hook inventory is malformed"))?;
let mut measured_pi_events = pi_hooks.keys().cloned().collect::<Vec<_>>();
measured_pi_events.sort();
let expected_pi_events =
Value::Array(measured_pi_events.into_iter().map(Value::String).collect());
let pi_events = evidence
.pointer("/pi_terminal_observers/registered_events")
.and_then(Value::as_array)
.ok_or_else(|| error("Pi registered event inventory is missing"))?;
if evidence.pointer("/pi_terminal_observers/registered_events") != Some(&expected_pi_events)
|| !pi_events.iter().any(|value| value == "message_end")
|| !pi_events.iter().any(|value| value == "session_shutdown")
|| pi_events.iter().any(|value| value == "precompact")
|| evidence.pointer("/matrix_evidence/host-limitations/pi/native_result/events")
!= Some(&Value::Array(pi_events.clone()))
|| evidence.pointer("/matrix_evidence/compaction/pi/native_input/advertised_events")
!= Some(&Value::Array(pi_events.clone()))
|| evidence.pointer("/matrix_evidence/host-limitations/pi/native_result/compaction")
!= Some(&Value::Bool(false))
|| evidence.pointer("/matrix_evidence/compaction/pi/native_result/supported")
!= Some(&Value::Bool(false))
{
return Err(error(
"Pi host inventory does not match registered observers",
));
}
Ok(())
}
fn validate_chain(
evidence: &Value,
root: &Path,
selection: &EvidenceSelection,
harness: &str,
) -> Result<(), CliError> {
let prefix = format!("/real_chains/{harness}");
for phase in ["start", "resolve", "stop"] {
equal(
evidence,
&format!("{prefix}/{phase}/request/harness"),
harness,
)?;
equal(
evidence,
&format!("{prefix}/{phase}/response/harness"),
harness,
)?;
equal(
evidence,
&format!("{prefix}/{phase}/request/run"),
&selection.run,
)?;
equal(
evidence,
&format!("{prefix}/{phase}/response/run"),
&selection.run,
)?;
}
equal(
evidence,
&format!("{prefix}/guard/request/dispatch/harness"),
harness,
)?;
equal(
evidence,
&format!("{prefix}/guard/request/dispatch/run"),
&selection.run,
)?;
validate_identity_continuity(evidence, &prefix, harness)?;
equal(
evidence,
&format!("{prefix}/start/response/state"),
"active",
)?;
if evidence.pointer(&format!("{prefix}/resolve/response/path_in_write_scope"))
!= Some(&Value::Bool(true))
{
return Err(error(format!(
"{harness} resolve did not prove bounded scope"
)));
}
equal(
evidence,
&format!("{prefix}/guard/response/decision"),
"allow",
)?;
equal(
evidence,
&format!("{prefix}/stop/response/state"),
"stopped",
)?;
if evidence
.pointer(&format!("{prefix}/start/response/revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/resolve/response/revision"))
.is_some()
|| evidence
.pointer(&format!("{prefix}/stop/request/expected_revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/stop/response/revision"))
.and_then(Value::as_u64)
!= Some(2)
{
return Err(error(format!(
"{harness} lifecycle revisions are not start=1 resolve=absent expected=1 stop=2"
)));
}
validate_completion_artifact(evidence, root, selection, &prefix)?;
let artifact = format!("{}/reports/{harness}-engineer.md", selection.evidence_dir);
equal(
evidence,
&format!("{prefix}/result_artifact/path"),
&artifact,
)?;
let content = text(evidence, &format!("{prefix}/result_artifact/content"))?;
let recorded = text(evidence, &format!("{prefix}/result_artifact/sha256"))?;
if !is_hash(recorded) || sha256_hex(content.as_bytes()) != recorded {
return Err(error(format!("{harness} result content hash is invalid")));
}
let path = contained(root, &artifact)?;
let bytes =
fs::read(&path).map_err(|e| error(format!("cannot read {}: {e}", path.display())))?;
if bytes != content.as_bytes() || sha256_hex(&bytes) != recorded {
return Err(error(format!(
"{harness} stopped result artifact is missing or stale"
)));
}
Ok(())
}
fn validate_identity_continuity(
evidence: &Value,
prefix: &str,
harness: &str,
) -> Result<(), CliError> {
let request_fields = [
"run",
"harness",
"agent_id",
"agent_type",
"lane",
"session_id",
];
for field in request_fields {
same(
evidence,
&format!("{prefix}/start/request/{field}"),
&format!("{prefix}/resolve/request/{field}"),
harness,
)?;
same(
evidence,
&format!("{prefix}/start/request/{field}"),
&format!("{prefix}/stop/request/{field}"),
harness,
)?;
for phase in ["start", "resolve", "stop"] {
same(
evidence,
&format!("{prefix}/{phase}/request/{field}"),
&format!("{prefix}/{phase}/response/{field}"),
harness,
)?;
}
}
let response_fields = [
"run",
"harness",
"agent_id",
"agent_type",
"role",
"lane",
"session_id",
];
for field in response_fields {
same(
evidence,
&format!("{prefix}/start/response/{field}"),
&format!("{prefix}/resolve/response/{field}"),
harness,
)?;
same(
evidence,
&format!("{prefix}/start/response/{field}"),
&format!("{prefix}/stop/response/{field}"),
harness,
)?;
}
same(
evidence,
&format!("{prefix}/resolve/response"),
&format!("{prefix}/guard/request/dispatch"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/request/tool_name"),
&format!("{prefix}/guard/request/tool_name"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/request/tool_input"),
&format!("{prefix}/guard/request/tool_input"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/request/tool_call_id"),
&format!("{prefix}/resolve/response/tool_use_id"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/response/role"),
&format!("{prefix}/guard/request/role"),
harness,
)?;
same(
evidence,
&format!("{prefix}/start/response/revision"),
&format!("{prefix}/stop/request/expected_revision"),
harness,
)?;
for (path, schema) in [
("start/response/schema", "shepherd.dispatch/3"),
("resolve/response/schema", "shepherd.identity-resolution/1"),
("stop/response/schema", "shepherd.dispatch/3"),
] {
equal(evidence, &format!("{prefix}/{path}"), schema)?;
}
let role = text(evidence, &format!("{prefix}/start/response/role"))?;
let carrier = format!("shepherd:{role}");
for phase in ["start", "resolve", "stop"] {
equal(
evidence,
&format!("{prefix}/{phase}/request/role_carrier"),
&carrier,
)?;
}
Ok(())
}
fn same(evidence: &Value, left: &str, right: &str, harness: &str) -> Result<(), CliError> {
let left_value = evidence
.pointer(left)
.ok_or_else(|| error(format!("missing identity field at {left}")))?;
let right_value = evidence
.pointer(right)
.ok_or_else(|| error(format!("missing identity field at {right}")))?;
if left_value != right_value {
return Err(error(format!(
"{harness} identity continuity mismatch between {left} and {right}"
)));
}
Ok(())
}
fn read_json(path: &Path) -> Result<Value, CliError> {
let metadata = fs::symlink_metadata(path)
.map_err(|e| error(format!("cannot inspect {}: {e}", path.display())))?;
if !metadata.file_type().is_file() {
return Err(error("evidence must be a regular file"));
}
let bytes =
fs::read(path).map_err(|e| error(format!("cannot read {}: {e}", path.display())))?;
serde_json::from_slice(&bytes).map_err(|e| error(format!("invalid evidence JSON: {e}")))
}
fn contained(root: &Path, relative: &str) -> Result<std::path::PathBuf, CliError> {
let path = Path::new(relative);
if path.is_absolute() || !path.components().all(|c| matches!(c, Component::Normal(_))) {
return Err(error(format!("path escapes repository root: {relative}")));
}
Ok(root.join(path))
}
fn equal(value: &Value, pointer: &str, expected: &str) -> Result<(), CliError> {
if text(value, pointer)? != expected {
return Err(error(format!("unexpected value at {pointer}")));
}
Ok(())
}
fn text<'a>(value: &'a Value, pointer: &str) -> Result<&'a str, CliError> {
value
.pointer(pointer)
.and_then(Value::as_str)
.ok_or_else(|| error(format!("missing string at {pointer}")))
}
fn is_hash(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
}
fn error(message: impl Into<String>) -> CliError {
CliError::message(format!("harness evidence: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
io::Write,
path::PathBuf,
process::Stdio,
sync::atomic::{AtomicU64, Ordering},
};
struct GitGraph {
root: PathBuf,
anchor: String,
accepted: String,
future: String,
unrelated: String,
tree: String,
blob: String,
tag: String,
}
fn fixture_git(root: &Path, args: &[&str], input: &[u8]) -> String {
let mut child = Command::new(
crate::dispatch_service::trusted_git_executable().expect("trusted fixture Git"),
)
.env_clear()
.current_dir(root)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("fixture Git process");
child
.stdin
.take()
.expect("fixture stdin")
.write_all(input)
.expect("fixture Git input");
let output = child.wait_with_output().expect("fixture Git output");
assert!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout)
.expect("fixture Git UTF-8")
.trim()
.to_owned()
}
fn fixture_commit(root: &Path, tree: &str, parent: Option<&str>, message: &str) -> String {
let parent = parent
.map(|value| format!("parent {value}\n"))
.unwrap_or_default();
let object = format!(
"tree {tree}\n{parent}author Fixture <fixture@example.invalid> 1700000000 +0000\ncommitter Fixture <fixture@example.invalid> 1700000000 +0000\n\n{message}\n"
);
fixture_git(
root,
&["hash-object", "-t", "commit", "-w", "--stdin"],
object.as_bytes(),
)
}
fn git_graph() -> GitGraph {
static NEXT: AtomicU64 = AtomicU64::new(0);
let root = loop {
let ordinal = NEXT.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"shepherd-accepted-base-{}-{ordinal}",
std::process::id()
));
match fs::create_dir(&root) {
Ok(()) => break fs::canonicalize(root).expect("canonical fixture"),
Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(source) => panic!("create Git graph fixture: {source}"),
}
};
fixture_git(
&root,
&[
"init",
"--quiet",
"--initial-branch=main",
"--object-format=sha1",
],
b"",
);
let tree = fixture_git(&root, &["mktree"], b"");
let blob = fixture_git(&root, &["hash-object", "-w", "--stdin"], b"fixture blob\n");
let anchor = fixture_commit(&root, &tree, None, "released anchor");
let accepted = fixture_commit(&root, &tree, Some(&anchor), "accepted branch commit");
let head = fixture_commit(&root, &tree, Some(&accepted), "current candidate");
let future = fixture_commit(&root, &tree, Some(&head), "future commit");
let unrelated = fixture_commit(&root, &tree, None, "unrelated history");
let tag_content = format!(
"object {accepted}\ntype commit\ntag accepted-tag\ntagger Fixture <fixture@example.invalid> 1700000000 +0000\n\nannotated tag\n"
);
let tag = fixture_git(
&root,
&["hash-object", "-t", "tag", "-w", "--stdin"],
tag_content.as_bytes(),
);
for (reference, object) in [
("refs/heads/main", head.as_str()),
("refs/heads/accepted", accepted.as_str()),
(
"refs/heads/ffffffffffffffffffffffffffffffffffffffff",
accepted.as_str(),
),
("refs/tags/accepted-tag", tag.as_str()),
] {
fixture_git(&root, &["update-ref", reference, object], b"");
}
GitGraph {
root,
anchor,
accepted,
future,
unrelated,
tree,
blob,
tag,
}
}
#[test]
fn accepted_base_allows_a_descendant_of_release_before_head() {
let graph = git_graph();
validate_accepted_base(&graph.root, &graph.accepted, &graph.anchor)
.expect("accepted descendant between the released anchor and HEAD");
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn accepted_base_allows_the_released_anchor_itself() {
let graph = git_graph();
validate_accepted_base(&graph.root, &graph.anchor, &graph.anchor)
.expect("released anchor is an ancestor of HEAD");
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn accepted_base_allows_the_current_head_itself() {
let graph = git_graph();
let head = fixture_git(&graph.root, &["rev-parse", "HEAD"], b"");
validate_accepted_base(&graph.root, &head, &graph.anchor)
.expect("the accepted branch can be exactly the current HEAD");
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn accepted_base_rejects_noncanonical_unknown_and_noncommit_objects() {
let graph = git_graph();
for invalid in [
String::new(),
graph.accepted.to_uppercase(),
graph.accepted[..12].to_owned(),
format!("{}0", graph.accepted),
"HEAD".into(),
"accepted".into(),
"refs/heads/accepted".into(),
"accepted-tag".into(),
format!("{}^{{commit}}", graph.accepted),
"0".repeat(40),
"f".repeat(40),
graph.tree.clone(),
graph.blob.clone(),
graph.tag.clone(),
] {
assert!(
validate_accepted_base(&graph.root, &invalid, &graph.anchor).is_err(),
"must reject non-commit identity {invalid:?}"
);
}
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn accepted_base_rejects_unrelated_history() {
let graph = git_graph();
assert!(validate_accepted_base(&graph.root, &graph.unrelated, &graph.anchor).is_err());
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn accepted_base_rejects_a_future_commit_not_ancestor_of_head() {
let graph = git_graph();
assert!(validate_accepted_base(&graph.root, &graph.future, &graph.anchor).is_err());
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn accepted_base_rejects_the_anchor_when_head_has_unrelated_history() {
let graph = git_graph();
fixture_git(
&graph.root,
&["update-ref", "refs/heads/main", &graph.unrelated],
b"",
);
assert!(
validate_accepted_base(&graph.root, &graph.anchor, &graph.anchor).is_err(),
"the immutable anchor cannot authorize an unrelated candidate HEAD"
);
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn accepted_base_rejects_an_unknown_anchor_even_when_the_values_match() {
let graph = git_graph();
let unknown = "0".repeat(40);
assert!(
validate_accepted_base(&graph.root, &unknown, &unknown).is_err(),
"matching strings do not establish an existing commit or ancestry"
);
fs::remove_dir_all(graph.root).expect("remove Git fixture");
}
#[test]
fn hashes_are_lowercase_sha256() {
assert!(is_hash(&"a".repeat(64)));
assert!(!is_hash(&"A".repeat(64)));
assert!(!is_hash("abc"));
}
#[test]
fn paths_are_repository_relative() {
assert!(contained(Path::new("/tmp/root"), "packages/a.json").is_ok());
assert!(contained(Path::new("/tmp/root"), "../a.json").is_err());
}
#[test]
fn evidence_selection_is_canonical_and_run_scoped() {
let selected = parse_evidence_relative(
".shepherd/runs/v657/lanes/harness-semantics/evidence/active-adapters.json",
)
.expect("canonical v657 evidence path");
assert_eq!(selected.run, "v657");
assert_eq!(
selected.packed_inputs,
".shepherd/runs/v657/lanes/harness-semantics/packed-inputs.json"
);
assert!(
parse_evidence_relative(
".shepherd/runs/v656/lanes/harness-semantics/evidence/active-adapters.json"
)
.is_ok()
);
for invalid in [
".shepherd/runs/v657/lanes/other/evidence/active-adapters.json",
".shepherd/runs/v657/lanes/harness-semantics/evidence/copied.json",
".shepherd/runs/v657/../v656/lanes/harness-semantics/evidence/active-adapters.json",
".shepherd/runs/V657/lanes/harness-semantics/evidence/active-adapters.json",
] {
assert!(
parse_evidence_relative(invalid).is_err(),
"must reject {invalid}"
);
}
}
#[test]
fn declarative_completion_requires_a_stopped_raw_chain() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let relative = ".shepherd/runs/v656/lanes/harness-semantics/evidence/active-adapters.json";
let mut evidence = read_json(&root.join(relative)).expect("tracked historical evidence");
evidence["real_chains"]["claude"]["stop"]["response"]["state"] =
Value::String("active".to_owned());
let selection = parse_evidence_relative(relative).expect("canonical evidence selection");
let error = validate_chain(&evidence, &root, &selection, "claude")
.expect_err("active raw chain must not satisfy completion");
let detail = error.message_text().unwrap_or("reported CLI failure");
assert!(
detail.contains("unexpected value at /real_chains/claude/stop/response/state"),
"unexpected error: {detail}"
);
}
}