Skip to content
155 changes: 155 additions & 0 deletions crates/commandf-pkg/tests/af02_resource_runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

const VERIFIER_MANIFEST: &str = "tools/af02-verifier/Cargo.toml";
const RESOURCE_POLICY: &str = "specs/016-af-02-adversarial-test-strength/resource-policy.json";

fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("commandf-pkg must live under crates/ in the repository")
.to_path_buf()
}

fn runner_image(root: &Path) -> String {
let policy_bytes = fs::read(root.join(RESOURCE_POLICY)).expect("read AF-02 resource policy");
let policy: serde_json::Value =
serde_json::from_slice(&policy_bytes).expect("parse AF-02 resource policy");
let digest = policy
.get("runner_image_digest")
.and_then(serde_json::Value::as_str)
.expect("AF-02 resource policy must define runner_image_digest");
format!("docker.io/library/rust@{digest}")
}

fn run(root: &Path, program: &str, args: &[&str]) -> Output {
Command::new(program)
.args(args)
.current_dir(root)
.output()
.unwrap_or_else(|error| panic!("failed to run {program} {args:?}: {error}"))
}

fn verifier(root: &Path, source: &Path, output: &Path, command: &[&str]) -> Output {
let mut invocation = vec![
"run",
"--quiet",
"--locked",
"--manifest-path",
VERIFIER_MANIFEST,
"--",
"run-bounded",
RESOURCE_POLICY,
source.to_str().expect("source path must be UTF-8"),
output.to_str().expect("output path must be UTF-8"),
"--",
];
invocation.extend_from_slice(command);
run(root, "cargo", &invocation)
}

fn assert_success(output: &Output, context: &str) {
assert!(
output.status.success(),
"{context} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}

fn assert_failure_contains(output: &Output, needle: &str, context: &str) {
assert!(
!output.status.success(),
"{context} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(needle),
"{context} did not contain {needle:?}\nstdout:\n{}\nstderr:\n{stderr}",
String::from_utf8_lossy(&output.stdout)
);
}

#[test]
fn af02_resource_runner_uses_real_pinned_oci_isolation_and_bounds() {
if std::env::var_os("GITHUB_ACTIONS").as_deref() != Some(OsStr::new("true")) {
eprintln!("AF-02 real OCI qualification runs only inside GitHub Actions");
return;
}

let root = repo_root();
let runner_image = runner_image(&root);
let pull = run(&root, "docker", &["pull", runner_image.as_str()]);
assert_success(&pull, "pre-acquire pinned AF-02 runner image");

let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock must follow Unix epoch")
.as_nanos();
let scratch = std::env::temp_dir().join(format!(
"commandf-af02-resource-{}-{nonce}",
std::process::id()
));
let source = scratch.join("source");
fs::create_dir_all(&source).expect("create source fixture");
fs::write(source.join("input.txt"), b"read-only-source\n").expect("write source fixture");

let success_output = scratch.join("output-success");
fs::create_dir_all(&success_output).expect("create success output fixture");
let success = verifier(
&root,
&source,
&success_output,
&[
"bash",
"-ceu",
"printf 'bounded-ok\\n' > /output/result.txt",
],
);
assert_success(&success, "real bounded OCI success probe");
assert_eq!(success.stdout.first().copied(), Some(b'{'));
assert_eq!(
fs::read(success_output.join("result.txt")).expect("read bounded output"),
b"bounded-ok\n"
);

let temp_output = scratch.join("output-temp-limit");
fs::create_dir_all(&temp_output).expect("create temp-limit output fixture");
let temp_limit = verifier(
&root,
&source,
&temp_output,
&[
"bash",
"-ceu",
"for i in $(seq 1 1025); do : > \"/tmp/af02-$i\"; done",
],
);
assert_failure_contains(
&temp_limit,
"AF02_RESOURCE_PROBE_FAIL=TEMP_FILE_LIMIT",
"real bounded OCI temp-file negative probe",
);

let symlink_output = scratch.join("output-symlink");
fs::create_dir_all(&symlink_output).expect("create symlink output fixture");
let symlink = verifier(
&root,
&source,
&symlink_output,
&["bash", "-ceu", "ln -s /etc/passwd /output/escape"],
);
assert_failure_contains(
&symlink,
"symlink output is prohibited",
"real bounded OCI symlink negative probe",
);

fs::remove_dir_all(&scratch).expect("remove AF-02 OCI qualification fixtures");
}
1 change: 1 addition & 0 deletions tools/af02-verifier/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod authority;
pub mod canonical;
pub mod resource;
pub mod retained;
pub mod surface;
pub mod surface_proof;
32 changes: 32 additions & 0 deletions tools/af02-verifier/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::PathBuf;

use commandf_af02_verifier::authority::{project_authority, Cf06Source};
use commandf_af02_verifier::canonical::{canonical_json_bytes, parse_json_no_duplicates};
use commandf_af02_verifier::resource::{parse_resource_policy, run_bounded};
use commandf_af02_verifier::retained::{
locator_plan, project_retained, validate_and_parse, verify_artifacts, verify_workflow_run,
};
Expand Down Expand Up @@ -178,6 +179,37 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
&canonical_surface_proof_bytes(&evidence)?,
)?;
}
"parse-resource-policy" => {
let policy_path = PathBuf::from(args.next().ok_or("missing resource policy path")?);
if args.next().is_some() {
return Err("parse-resource-policy accepts exactly one path".into());
}
let policy = parse_resource_policy(&fs::read(policy_path)?)?;
let value = serde_json::to_value(policy)?;
std::io::Write::write_all(
&mut std::io::stdout().lock(),
&canonical_json_bytes(&value)?,
)?;
}
"run-bounded" => {
let policy_path = PathBuf::from(args.next().ok_or("missing resource policy path")?);
let source_dir = PathBuf::from(args.next().ok_or("missing source directory")?);
let output_dir = PathBuf::from(args.next().ok_or("missing output directory")?);
if args.next().as_deref() != Some("--") {
return Err("run-bounded requires `--` before the bounded command".into());
}
let bounded_command = args.collect::<Vec<_>>();
if bounded_command.is_empty() {
return Err("run-bounded requires a bounded command".into());
}
let policy = parse_resource_policy(&fs::read(policy_path)?)?;
let outcome = run_bounded(&policy, &source_dir, &output_dir, &bounded_command)?;
let value = serde_json::to_value(outcome)?;
std::io::Write::write_all(
&mut std::io::stdout().lock(),
&canonical_json_bytes(&value)?,
)?;
}
"verify-pr" => {
return Err(
"verify-pr is fail-closed until AF-02 T021-T025 semantic/input/base-gate enforcement is canonical"
Expand Down
Loading
Loading