diff --git a/Cargo.lock b/Cargo.lock index 9fd59bd5..111d99d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2097,6 +2097,7 @@ dependencies = [ "ed25519-dalek", "epiphany-model-adapter", "epiphany-openai-adapter", + "epiphany-release-construction", "epiphany-state-model", "epiphany-tool-adapter", "fs2", @@ -2221,6 +2222,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "epiphany-release-construction" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "cultcache-rs", + "fs2", + "rmp-serde", + "serde", + "serde_json", + "sha2 0.10.9", + "tempfile", + "uuid", +] + [[package]] name = "epiphany-self-policy" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b596230e..00c58b47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "epiphany-openai-auth-spine", "epiphany-openai-codex-spine", "epiphany-openai-runtime", + "epiphany-release-construction", "epiphany-self-policy", "epiphany-state-model", "epiphany-tool-adapter", diff --git a/epiphany-core/Cargo.toml b/epiphany-core/Cargo.toml index 6484cadf..380a2f59 100644 --- a/epiphany-core/Cargo.toml +++ b/epiphany-core/Cargo.toml @@ -333,6 +333,7 @@ cultcache-rs = { path = "../vendor/cultcache-rs" } cultmesh-rs = { path = "../vendor/cultmesh-rs" } cultnet-rs = { path = "../vendor/cultnet-rs" } epiphany-openai-adapter = { path = "../epiphany-openai-adapter" } +epiphany-release-construction = { path = "../epiphany-release-construction" } epiphany-model-adapter = { path = "../epiphany-model-adapter" } epiphany-tool-adapter = { path = "../epiphany-tool-adapter" } epiphany-state-model = { path = "../epiphany-state-model" } diff --git a/epiphany-core/src/packaged_release.rs b/epiphany-core/src/packaged_release.rs index 5abc46c9..dc857e88 100644 --- a/epiphany-core/src/packaged_release.rs +++ b/epiphany-core/src/packaged_release.rs @@ -1,755 +1,13 @@ use crate::open_epiphany_cultmesh_node; -use anyhow::{Context, Result, anyhow, bail}; -use chrono::{DateTime, Utc}; -use cultcache_rs::{DatabaseEntry, SingleFileMessagePackBackingStore}; -use fs2::FileExt; -use sha2::{Digest, Sha256}; -use std::collections::{BTreeMap, BTreeSet}; -use std::fs; -use std::path::{Path, PathBuf}; -use uuid::Uuid; +use anyhow::{Context, Result, bail}; +use chrono::Utc; +use cultcache_rs::SingleFileMessagePackBackingStore; +use std::path::Path; + +pub use epiphany_release_construction::*; -pub const EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION: &str = "epiphany.packaged_release.v0"; -pub const EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION: &str = "epiphany.packaged_release_head.v0"; const RELEASE_KEY_PREFIX: &str = "epiphany-local/packaged-release/by-id/"; const RELEASE_HEAD_KEY: &str = "epiphany-local/packaged-release/current"; -pub const EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE: &str = "release-witness.ccmp"; - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct EpiphanyPackagedReleaseBinary { - pub role: String, - pub file_name: String, - pub canonical_path: String, - pub sha256: String, - pub byte_len: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "epiphany.packaged_release", - schema = "EpiphanyPackagedReleaseEntry" -)] -pub struct EpiphanyPackagedReleaseEntry { - #[cultcache(key = 0)] - pub schema_version: String, - #[cultcache(key = 1)] - pub release_id: String, - #[cultcache(key = 2)] - pub runtime_id: String, - #[cultcache(key = 3)] - pub source_commit_sha: String, - #[cultcache(key = 4)] - pub target_triple: String, - #[cultcache(key = 5)] - pub cargo_profile: String, - #[cultcache(key = 6)] - pub toolchain_fingerprint: String, - #[cultcache(key = 7)] - pub created_at_utc: String, - #[cultcache(key = 8)] - pub package_root: String, - #[cultcache(key = 9)] - pub binaries: Vec, - #[cultcache(key = 10)] - pub private_state_exposed: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "epiphany.packaged_release_head", - schema = "EpiphanyPackagedReleaseHead" -)] -pub struct EpiphanyPackagedReleaseHead { - #[cultcache(key = 0)] - pub schema_version: String, - #[cultcache(key = 1)] - pub runtime_id: String, - #[cultcache(key = 2)] - pub release_id: String, - #[cultcache(key = 3)] - pub witness_sha256: String, - #[cultcache(key = 4)] - pub published_at_utc: String, -} - -pub struct PackageReleaseRequest<'a> { - pub repo: &'a Path, - pub destination: &'a Path, - pub build_cache_root: &'a Path, - pub runtime_id: &'a str, - pub target_triple: &'a str, -} - -pub fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static str, String)> { - let file_name = |name: &str| target_binary_file_name(target_triple, name); - vec![ - ("supervisor", file_name("epiphany-daemon-supervisor")), - ("release-publisher", file_name("epiphany-release")), - ("state-steward", file_name("epiphany-state")), - ( - "semantic-projector", - file_name("epiphany-memory-semantic-projector"), - ), - ( - "workspace-coverage-projector", - file_name("epiphany-workspace-coverage-projector"), - ), - ("semantic-query", file_name("epiphany-memory-semantic")), - ("verse-query", file_name("epiphany-verse-query")), - ("repository-body", file_name("epiphany-repository-body")), - ("host-identity", file_name("epiphany-host-identity")), - ("swarm", file_name("epiphany-swarm")), - ("operator-command", file_name("epiphany-operator-command")), - ("heartbeat", file_name("epiphany-heartbeat-store")), - ("runtime-spine", file_name("epiphany-runtime-spine")), - ( - "persona-feedback-ingress", - file_name("epiphany-persona-feedback-ingress"), - ), - ("persona-service", file_name("epiphany-persona-service")), - ( - "persona-mouth-identity", - file_name("epiphany-persona-mouth-identity"), - ), - ( - "persona-discord-permit", - file_name("epiphany-persona-discord-permit"), - ), - ( - "persona-discord-permit-identity", - file_name("epiphany-persona-discord-permit-identity"), - ), - ("coordinator", file_name("epiphany-mvp-coordinator")), - ("coordinator-status", file_name("epiphany-mvp-status")), - ("frontier-proposal", file_name("epiphany-frontier-proposal")), - ("hands-action", file_name("epiphany-hands-action")), - ("model-runtime", file_name("epiphany-model-runtime")), - ("tool-mcp-runtime", file_name("epiphany-tool-mcp-runtime")), - ] -} - -fn target_binary_file_name(target_triple: &str, binary: &str) -> String { - if target_triple - .split('-') - .any(|component| component == "windows") - { - format!("{binary}.exe") - } else { - binary.to_string() - } -} - -pub fn package_epiphany_release( - request: PackageReleaseRequest<'_>, -) -> Result { - let (source_commit_sha, source_commit_time) = clean_source_commit(request.repo)?; - require_nonempty("runtime id", request.runtime_id)?; - require_nonempty("target triple", request.target_triple)?; - let toolchain = installed_toolchain()?; - fs::create_dir_all(request.destination).with_context(|| { - format!( - "failed to create release destination {}", - request.destination.display() - ) - })?; - let build_root = request.build_cache_root.to_path_buf(); - fs::create_dir_all(&build_root).with_context(|| { - format!( - "failed to create stable build cache {}", - build_root.display() - ) - })?; - let destination = canonical_path(request.destination)?; - let source_guard = ReleaseSourceGuard::prepare(request.repo, &build_root, &source_commit_sha)?; - let built_binaries = build_required_release_siblings( - &source_guard.path, - &build_root, - request.target_triple, - &toolchain.fingerprint, - &toolchain.cargo, - )?; - // Construction authority is scoped to one exact source generation. The - // shared release root remains root-owned while the builder creates only a - // private staging sibling beneath its commit-specific directory. - let commit_root = destination.join(&source_commit_sha); - fs::create_dir_all(&commit_root).with_context(|| { - format!( - "failed to create release commit root {}", - commit_root.display() - ) - })?; - let staging = commit_root.join(format!(".staging-{}", Uuid::new_v4())); - fs::create_dir(&staging) - .with_context(|| format!("failed to create release staging {}", staging.display()))?; - let result = (|| { - let mut binaries = Vec::new(); - for (role, file_name) in required_packaged_release_binaries(request.target_triple) { - let source = built_binaries - .binaries - .get(role) - .with_context(|| format!("required packaged sibling was not built: {role}"))?; - if !source.is_file() { - bail!("required packaged sibling is absent: {}", source.display()); - } - let target = staging.join(&file_name); - fs::copy(&source, &target).with_context(|| { - format!( - "failed to copy {} to {}", - source.display(), - target.display() - ) - })?; - binaries.push(binary_record(role, &file_name, &target)?); - } - binaries.sort_by(|left, right| left.role.cmp(&right.role)); - let release_id = release_id( - request.runtime_id, - &source_commit_sha, - request.target_triple, - &toolchain.fingerprint, - &binaries, - ); - let final_root = commit_root.join(&release_id); - for binary in &mut binaries { - binary.canonical_path = final_root.join(&binary.file_name).display().to_string(); - } - let witness = EpiphanyPackagedReleaseEntry { - schema_version: EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(), - release_id, - runtime_id: request.runtime_id.into(), - source_commit_sha, - target_triple: request.target_triple.into(), - cargo_profile: "release".into(), - toolchain_fingerprint: toolchain.fingerprint, - created_at_utc: source_commit_time, - package_root: final_root.display().to_string(), - binaries, - private_state_exposed: false, - }; - validate_epiphany_packaged_release(&witness)?; - write_epiphany_packaged_release_witness( - &staging.join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE), - &witness, - )?; - if final_root.exists() { - verify_epiphany_packaged_release_files(&witness)?; - fs::remove_dir_all(&staging)?; - } else { - fs::rename(&staging, &final_root) - .context("failed to atomically publish packaged release directory")?; - verify_epiphany_packaged_release_files(&witness)?; - } - Ok(witness) - })(); - if staging.exists() { - let _ = fs::remove_dir_all(staging); - } - result -} - -fn release_source_cache_identity(repo: &Path) -> Result { - let common_dir = PathBuf::from(git_output(repo, &["rev-parse", "--git-common-dir"])?.trim()); - let common_dir = if common_dir.is_absolute() { - canonical_path(&common_dir)? - } else { - canonical_path(&repo.join(common_dir))? - }; - // Linked worktrees are views of one repository and must share one exact-source - // cache. For ordinary non-bare repositories, hashing the common `.git` parent - // also preserves the cache identity used before worktree-aware ownership. - let cache_owner = if common_dir.file_name().is_some_and(|name| name == ".git") { - common_dir - .parent() - .ok_or_else(|| anyhow!("repository common directory has no owner"))? - } else { - common_dir.as_path() - }; - let mut digest = Sha256::new(); - digest.update(b"epiphany.release_source_cache.v0\0"); - digest.update(cache_owner.to_string_lossy().as_bytes()); - Ok(format!("{:x}", digest.finalize())) -} - -struct ReleaseSourceGuard { - path: PathBuf, - _source_lock: fs::File, -} - -impl ReleaseSourceGuard { - fn prepare(repo: &Path, build_root: &Path, commit: &str) -> Result { - validate_commit(commit)?; - let identity = release_source_cache_identity(repo)?; - let path = build_root.join(format!("source-{identity}")); - let lock_path = build_root.join(format!(".epiphany-release-source-{identity}.lock")); - let source_lock = fs::OpenOptions::new() - .create(true) - .read(true) - .write(true) - .open(&lock_path) - .with_context(|| { - format!("failed to open release source lock {}", lock_path.display()) - })?; - source_lock.lock_exclusive().with_context(|| { - format!( - "failed to acquire release source lock {}", - lock_path.display() - ) - })?; - - if path.exists() { - verify_cached_worktree_owner(repo, &path)?; - run_git_checked( - &path, - &["-c", "core.longpaths=true", "clean", "-ffdx"], - "failed to pre-clean cached release worktree", - )?; - run_git_checked( - &path, - &[ - "-c", - "core.longpaths=true", - "checkout", - "--detach", - "--force", - commit, - ], - "failed to move cached release worktree to exact commit", - )?; - run_git_checked( - &path, - &["-c", "core.longpaths=true", "clean", "-ffdx"], - "failed to clean cached release worktree", - )?; - } else { - let output = std::process::Command::new("git") - .args(["-c", "core.longpaths=true", "worktree", "add", "--detach"]) - .arg(&path) - .arg(commit) - .current_dir(repo) - .output()?; - if !output.status.success() { - bail!( - "failed to create cached exact-source release worktree: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - } - - run_git_checked( - &path, - &[ - "-c", - "core.longpaths=true", - "submodule", - "update", - "--init", - "--recursive", - "--force", - ], - "failed to initialize exact release submodules", - )?; - run_git_checked( - &path, - &["submodule", "foreach", "--recursive", "git reset --hard"], - "failed to reset exact release submodules", - )?; - run_git_checked( - &path, - &["submodule", "foreach", "--recursive", "git clean -ffdx"], - "failed to clean exact release submodules", - )?; - let head = git_output(&path, &["rev-parse", "HEAD"])?; - if head.trim() != commit { - bail!("cached release worktree resolved {head:?}, expected {commit}"); - } - let status = git_output( - &path, - &["status", "--porcelain=v1", "--untracked-files=all"], - )?; - if !status.trim().is_empty() { - bail!("cached release worktree is not clean after preparation: {status}"); - } - let submodule_status = git_output(&path, &["submodule", "status", "--recursive"])?; - if submodule_status - .lines() - .any(|line| matches!(line.as_bytes().first(), Some(b'-' | b'+' | b'U'))) - { - bail!("cached release worktree has inexact submodules: {submodule_status}"); - } - Ok(Self { - path, - _source_lock: source_lock, - }) - } -} - -fn verify_cached_worktree_owner(repo: &Path, worktree: &Path) -> Result<()> { - let expected = PathBuf::from(git_output(repo, &["rev-parse", "--git-common-dir"])?.trim()); - let expected = if expected.is_absolute() { - canonical_path(&expected)? - } else { - canonical_path(&repo.join(expected))? - }; - let common = PathBuf::from(git_output(worktree, &["rev-parse", "--git-common-dir"])?.trim()); - let common = if common.is_absolute() { - canonical_path(&common)? - } else { - canonical_path(&worktree.join(common))? - }; - if common != expected { - bail!( - "cached release worktree {} belongs to {}, expected {}", - worktree.display(), - common.display(), - expected.display() - ); - } - Ok(()) -} - -fn git_output(repo: &Path, args: &[&str]) -> Result { - let output = std::process::Command::new("git") - .args(["-c", "core.longpaths=true"]) - .args(args) - .current_dir(repo) - .output()?; - if !output.status.success() { - bail!( - "git {} failed: {}", - args.join(" "), - String::from_utf8_lossy(&output.stderr) - ); - } - Ok(String::from_utf8(output.stdout)?) -} - -fn run_git_checked(repo: &Path, args: &[&str], context: &str) -> Result<()> { - git_output(repo, args).with_context(|| context.to_string())?; - Ok(()) -} - -struct BuiltReleaseSiblings { - binaries: BTreeMap<&'static str, PathBuf>, - _graph_lock: fs::File, -} - -fn build_required_release_siblings( - repo: &Path, - target_root: &Path, - target: &str, - toolchain_fingerprint: &str, - cargo: &std::ffi::OsStr, -) -> Result { - verify_release_bundle_lock(repo, cargo)?; - let manifest = repo.join("Cargo.toml"); - let target_dir = release_bundle_target_dir( - target_root, - &fs::read(repo.join("Cargo.lock")).context("failed to read release bundle lockfile")?, - target, - toolchain_fingerprint, - ); - fs::create_dir_all(&target_dir).with_context(|| { - format!( - "failed to create release graph cache {}", - target_dir.display() - ) - })?; - let graph_lock_path = target_dir.join(".epiphany-release-graph.lock"); - let graph_lock = fs::OpenOptions::new() - .create(true) - .read(true) - .write(true) - .open(&graph_lock_path) - .with_context(|| { - format!( - "failed to open release graph lock {}", - graph_lock_path.display() - ) - })?; - graph_lock.lock_exclusive().with_context(|| { - format!( - "failed to acquire release graph lock {}", - graph_lock_path.display() - ) - })?; - let mut outputs = BTreeMap::new(); - let required = required_packaged_release_binaries(target); - for (role, file_name) in &required { - required_release_build_target(role)?; - outputs.insert( - *role, - target_dir.join(target).join("release").join(file_name), - ); - } - let mut command = release_build_command(cargo, &manifest, &target_dir, target, &required)?; - let status = command - .status() - .context("failed to start Epiphany release bundle build")?; - if !status.success() { - bail!("Epiphany release bundle build failed"); - } - Ok(BuiltReleaseSiblings { - binaries: outputs, - _graph_lock: graph_lock, - }) -} - -fn release_build_command( - cargo: &std::ffi::OsStr, - manifest: &Path, - target_dir: &Path, - target: &str, - required: &[(&'static str, String)], -) -> Result { - let mut command = std::process::Command::new(cargo); - command - .env("CARGO_INCREMENTAL", "0") - .arg("build") - .arg("--release") - .arg("--manifest-path") - .arg(&manifest) - .arg("--target-dir") - .arg(&target_dir) - .arg("--target") - .arg(target) - .arg("--locked") - .arg("--package") - .arg("epiphany-release-bundle") - .arg("--features") - .arg("epiphany-release-bundle/release-runtime"); - for (_, file_name) in required { - let binary_name = Path::new(file_name) - .file_stem() - .and_then(std::ffi::OsStr::to_str) - .with_context(|| format!("packaged binary name is invalid: {file_name}"))?; - command.arg("--bin").arg(binary_name); - } - Ok(command) -} - -fn verify_release_bundle_lock(repo: &Path, cargo: &std::ffi::OsStr) -> Result<()> { - let manifest = repo.join("Cargo.toml"); - if !manifest.is_file() { - bail!( - "Epiphany release bundle manifest is absent: {}", - manifest.display() - ); - } - let output = std::process::Command::new(cargo) - .arg("metadata") - .arg("--locked") - .arg("--no-deps") - .arg("--format-version") - .arg("1") - .arg("--manifest-path") - .arg(&manifest) - .output() - .context("failed to validate Epiphany release bundle lockfile")?; - if !output.status.success() { - bail!( - "Epiphany release bundle lockfile is stale: {}", - String::from_utf8_lossy(&output.stderr).trim() - ); - } - Ok(()) -} - -fn release_bundle_target_dir( - target_root: &Path, - _lockfile: &[u8], - target: &str, - toolchain_fingerprint: &str, -) -> PathBuf { - let mut digest = Sha256::new(); - digest.update(b"epiphany.release_bundle_build_cache.v1\0"); - digest.update(target.as_bytes()); - digest.update(b"\0"); - digest.update(toolchain_fingerprint.as_bytes()); - target_root.join(format!("graph-{:x}", digest.finalize())) -} - -fn required_release_build_target(role: &str) -> Result<(&'static str, &'static str)> { - match role { - "supervisor" => Ok(("epiphany-core", "epiphany-daemon-supervisor")), - "release-publisher" => Ok(("epiphany-core", "epiphany-release")), - "state-steward" => Ok(("epiphany-core", "epiphany-state")), - "semantic-projector" => Ok(("epiphany-core", "epiphany-memory-semantic-projector")), - "workspace-coverage-projector" => { - Ok(("epiphany-core", "epiphany-workspace-coverage-projector")) - } - "semantic-query" => Ok(("epiphany-core", "epiphany-memory-semantic")), - "verse-query" => Ok(("epiphany-core", "epiphany-verse-query")), - "repository-body" => Ok(("epiphany-core", "epiphany-repository-body")), - "host-identity" => Ok(("epiphany-core", "epiphany-host-identity")), - "swarm" => Ok(("epiphany-core", "epiphany-swarm")), - "operator-command" => Ok(("epiphany-core", "epiphany-operator-command")), - "heartbeat" => Ok(("epiphany-core", "epiphany-heartbeat-store")), - "runtime-spine" => Ok(("epiphany-core", "epiphany-runtime-spine")), - "persona-feedback-ingress" => Ok(("epiphany-core", "epiphany-persona-feedback-ingress")), - "persona-service" => Ok(("epiphany-openai-runtime", "epiphany-persona-service")), - "persona-mouth-identity" => Ok(("epiphany-core", "epiphany-persona-mouth-identity")), - "persona-discord-permit" => Ok(("epiphany-core", "epiphany-persona-discord-permit")), - "persona-discord-permit-identity" => { - Ok(("epiphany-core", "epiphany-persona-discord-permit-identity")) - } - "coordinator" => Ok((".", "epiphany-mvp-coordinator")), - "coordinator-status" => Ok((".", "epiphany-mvp-status")), - "frontier-proposal" => Ok(("epiphany-core", "epiphany-frontier-proposal")), - "hands-action" => Ok(("epiphany-core", "epiphany-hands-action")), - "model-runtime" => Ok(("epiphany-openai-runtime", "epiphany-model-runtime")), - "tool-mcp-runtime" => Ok(("epiphany-tool-mcp-runtime", "epiphany-tool-mcp-runtime")), - _ => bail!("unknown required release role {role}"), - } -} - -pub fn validate_epiphany_packaged_release(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> { - if entry.schema_version != EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION { - bail!("unsupported packaged release schema"); - } - require_nonempty("runtime id", &entry.runtime_id)?; - validate_commit(&entry.source_commit_sha)?; - require_nonempty("target triple", &entry.target_triple)?; - if entry.cargo_profile != "release" { - bail!("packaged release profile must be release"); - } - require_nonempty("toolchain fingerprint", &entry.toolchain_fingerprint)?; - DateTime::parse_from_rfc3339(&entry.created_at_utc)?; - if entry.private_state_exposed { - bail!("packaged release may not expose private state"); - } - let root = Path::new(&entry.package_root); - if !root.is_absolute() { - bail!("packaged release root must be absolute"); - } - let required = required_packaged_release_binaries(&entry.target_triple); - if entry.binaries.len() != required.len() { - bail!("packaged release sibling set is incomplete"); - } - let mut roles = BTreeSet::new(); - let mut names = BTreeSet::new(); - for binary in &entry.binaries { - if !roles.insert(binary.role.as_str()) || !names.insert(binary.file_name.as_str()) { - bail!("packaged release contains duplicate role or filename"); - } - if !required - .iter() - .any(|pair| pair.0 == binary.role && pair.1 == binary.file_name) - { - bail!("packaged release contains an unknown or swapped sibling role"); - } - validate_sha256(&binary.sha256)?; - if binary.byte_len == 0 { - bail!("packaged release binary is empty"); - } - if Path::new(&binary.canonical_path) != root.join(&binary.file_name) { - bail!("packaged release binary path escapes or disagrees with package root"); - } - } - let mut identity_binaries = entry.binaries.clone(); - identity_binaries.sort_by(|left, right| left.role.cmp(&right.role)); - let expected = release_id( - &entry.runtime_id, - &entry.source_commit_sha, - &entry.target_triple, - &entry.toolchain_fingerprint, - &identity_binaries, - ); - if entry.release_id != expected { - bail!("packaged release id does not authenticate sibling set"); - } - Ok(()) -} - -pub fn verify_epiphany_packaged_release_files(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> { - validate_epiphany_packaged_release(entry)?; - let root = canonical_path(&entry.package_root).context("packaged release root is absent")?; - if root != PathBuf::from(&entry.package_root) { - bail!("packaged release root is not canonical"); - } - let actual_names = fs::read_dir(&root)? - .map(|item| Ok(item?.file_name().to_string_lossy().into_owned())) - .collect::>>()?; - let mut expected_names = entry - .binaries - .iter() - .map(|binary| binary.file_name.clone()) - .collect::>(); - expected_names.insert(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE.into()); - if actual_names != expected_names { - bail!("packaged release directory is not the exact witnessed sibling set"); - } - for binary in &entry.binaries { - let path = root.join(&binary.file_name); - if canonical_path(&path)? != PathBuf::from(&binary.canonical_path) { - bail!("packaged sibling path aliases another file"); - } - let metadata = fs::metadata(&path)?; - if !metadata.is_file() - || metadata.len() != binary.byte_len - || file_sha256(&path)? != binary.sha256 - { - bail!( - "packaged sibling bytes disagree with witness: {}", - binary.role - ); - } - } - let stored = - read_epiphany_packaged_release_witness(&root.join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE))?; - if stored != *entry { - bail!("packaged release witness artifact disagrees with inspected release"); - } - Ok(()) -} - -pub fn write_epiphany_packaged_release_witness( - path: &Path, - entry: &EpiphanyPackagedReleaseEntry, -) -> Result<()> { - validate_epiphany_packaged_release(entry)?; - let bytes = rmp_serde::to_vec(entry).context("failed to encode packaged release witness")?; - fs::write(path, bytes).with_context(|| { - format!( - "failed to write packaged release witness {}", - path.display() - ) - }) -} - -pub fn read_epiphany_packaged_release_witness(path: &Path) -> Result { - let bytes = fs::read(path) - .with_context(|| format!("failed to read packaged release witness {}", path.display()))?; - let entry = rmp_serde::from_slice(&bytes).with_context(|| { - format!( - "failed to decode packaged release witness {}", - path.display() - ) - })?; - validate_epiphany_packaged_release(&entry)?; - Ok(entry) -} - -pub fn inspect_epiphany_packaged_release_witness( - witness_path: &Path, - destination: &Path, - runtime_id: &str, - source_commit: &str, -) -> Result { - let entry = read_epiphany_packaged_release_witness(witness_path)?; - if entry.runtime_id != runtime_id || entry.source_commit_sha != source_commit { - bail!("packaged release witness disagrees with authorized runtime or source commit"); - } - let destination = canonical_path(destination)?; - let package_root = canonical_path(&entry.package_root)?; - if package_root.parent().and_then(Path::parent) != Some(destination.as_path()) { - bail!("packaged release root is outside the canonical destination"); - } - if package_root.join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE) != canonical_path(witness_path)? { - bail!("packaged release witness path disagrees with package root"); - } - verify_epiphany_packaged_release_files(&entry)?; - Ok(entry) -} pub fn publish_epiphany_packaged_release( store: &Path, @@ -771,7 +29,7 @@ pub fn publish_epiphany_packaged_release( schema_version: EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION.into(), runtime_id: runtime_id.into(), release_id: entry.release_id.clone(), - witness_sha256: witness_sha256(&entry)?, + witness_sha256: epiphany_packaged_release_witness_sha256(&entry)?, published_at_utc: Utc::now().to_rfc3339(), }; let prior_head = node @@ -816,601 +74,13 @@ pub fn authenticate_epiphany_packaged_release( release_id: &str, expected_witness_sha256: &str, ) -> Result { - validate_sha256(expected_witness_sha256)?; + validate_epiphany_packaged_release_sha256(expected_witness_sha256)?; let entry = load_epiphany_packaged_release(store, runtime_id, release_id)? .context("packaged release witness is absent")?; validate_epiphany_packaged_release(&entry)?; verify_epiphany_packaged_release_files(&entry)?; - if witness_sha256(&entry)? != expected_witness_sha256 { + if epiphany_packaged_release_witness_sha256(&entry)? != expected_witness_sha256 { bail!("packaged release witness digest disagrees with pinned task authority"); } Ok(entry) } - -pub fn epiphany_packaged_release_witness_sha256( - entry: &EpiphanyPackagedReleaseEntry, -) -> Result { - witness_sha256(entry) -} - -pub fn epiphany_packaged_release_binary_path( - entry: &EpiphanyPackagedReleaseEntry, - role: &str, -) -> Result { - entry - .binaries - .iter() - .find(|binary| binary.role == role) - .map(|binary| PathBuf::from(&binary.canonical_path)) - .with_context(|| format!("packaged release lacks required role {role}")) -} - -fn clean_source_commit(repo: &Path) -> Result<(String, String)> { - let head = git(repo, &["rev-parse", "HEAD"])?; - validate_commit(&head)?; - if !git(repo, &["status", "--porcelain=v1", "--untracked-files=no"])?.is_empty() { - bail!("packaged release requires a clean tracked source commit"); - } - if git(repo, &["submodule", "status", "--recursive"])? - .lines() - .any(|line| line.starts_with(['-', '+', 'U'])) - { - bail!("packaged release requires clean initialized submodules"); - } - let committed_at = git(repo, &["show", "-s", "--format=%cI", "HEAD"])?; - DateTime::parse_from_rfc3339(&committed_at).context("Git commit time is not RFC3339")?; - Ok((head, committed_at)) -} - -fn git(repo: &Path, args: &[&str]) -> Result { - let output = std::process::Command::new("git") - .args(args) - .current_dir(repo) - .output()?; - if !output.status.success() { - bail!( - "git {} failed: {}", - args.join(" "), - String::from_utf8_lossy(&output.stderr) - ); - } - Ok(String::from_utf8(output.stdout)?.trim().to_string()) -} - -struct InstalledToolchain { - cargo: std::ffi::OsString, - fingerprint: String, -} - -fn installed_toolchain() -> Result { - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); - let cargo_version = command_version(&cargo, "cargo")?; - let rustc_version = command_version(&rustc, "rustc")?; - Ok(InstalledToolchain { - cargo: cargo.clone(), - fingerprint: format!( - "cargo-command={}\ncargo-vV:\n{}\nrustc-command={}\nrustc-vV:\n{}", - Path::new(&cargo).display(), - cargo_version, - Path::new(&rustc).display(), - rustc_version - ), - }) -} - -fn command_version(command: &std::ffi::OsStr, label: &str) -> Result { - let output = std::process::Command::new(command).arg("-vV").output()?; - if !output.status.success() { - bail!( - "{label} -vV failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - Ok(String::from_utf8(output.stdout)?.trim().to_string()) -} - -fn binary_record( - role: &str, - file_name: &str, - path: &Path, -) -> Result { - let canonical = canonical_path(path)?; - let len = fs::metadata(&canonical)?.len(); - if len == 0 { - bail!("required packaged sibling is empty: {file_name}"); - } - Ok(EpiphanyPackagedReleaseBinary { - role: role.into(), - file_name: file_name.into(), - canonical_path: canonical.display().to_string(), - sha256: file_sha256(&canonical)?, - byte_len: len, - }) -} - -fn release_id( - runtime: &str, - commit: &str, - target: &str, - toolchain: &str, - binaries: &[EpiphanyPackagedReleaseBinary], -) -> String { - let mut hash = Sha256::new(); - for value in [ - "epiphany.packaged-release.identity.v0", - runtime, - commit, - target, - "release", - toolchain, - ] { - hash.update((value.len() as u64).to_be_bytes()); - hash.update(value.as_bytes()); - } - for binary in binaries { - for value in [ - &binary.role, - &binary.file_name, - &binary.sha256, - &binary.byte_len.to_string(), - ] { - hash.update((value.len() as u64).to_be_bytes()); - hash.update(value.as_bytes()); - } - } - format!("sha256-{:x}", hash.finalize()) -} - -fn witness_sha256(entry: &EpiphanyPackagedReleaseEntry) -> Result { - Ok(format!( - "sha256-{:x}", - Sha256::digest(rmp_serde::to_vec(entry)?) - )) -} -fn file_sha256(path: &Path) -> Result { - Ok(format!("sha256-{:x}", Sha256::digest(fs::read(path)?))) -} -fn canonical_path(path: impl AsRef) -> Result { - let canonical = fs::canonicalize(path)?; - #[cfg(windows)] - { - let text = canonical.to_string_lossy(); - if let Some(rest) = text.strip_prefix(r"\\?\UNC\") { - return Ok(PathBuf::from(format!(r"\\{rest}"))); - } - if let Some(rest) = text.strip_prefix(r"\\?\") { - return Ok(PathBuf::from(rest)); - } - } - Ok(canonical) -} -fn validate_sha256(value: &str) -> Result<()> { - if value.len() != 71 - || !value.starts_with("sha256-") - || !value[7..] - .bytes() - .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) - { - bail!("invalid SHA-256 digest"); - } - Ok(()) -} -fn validate_commit(value: &str) -> Result<()> { - if value.len() != 40 || !value.bytes().all(|b| b.is_ascii_hexdigit()) { - bail!("source commit must be a full 40-hex Git object id"); - } - Ok(()) -} -fn require_nonempty(label: &str, value: &str) -> Result<()> { - if value.trim().is_empty() { - return Err(anyhow!("{label} must not be empty")); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[test] - fn every_required_release_role_has_a_build_target() { - for (role, _) in required_packaged_release_binaries("x86_64-unknown-linux-gnu") { - required_release_build_target(role).expect("required release role must resolve"); - } - } - - #[test] - fn binary_suffix_follows_requested_target_not_packager_host() { - let windows = required_packaged_release_binaries("x86_64-pc-windows-msvc"); - assert!(windows.iter().all(|(_, name)| name.ends_with(".exe"))); - let linux = required_packaged_release_binaries("x86_64-unknown-linux-gnu"); - assert!(linux.iter().all(|(_, name)| !name.ends_with(".exe"))); - } - - #[test] - fn release_bundle_cache_is_owned_by_build_graph() { - let root = Path::new("release-build-cache"); - let first = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); - let second = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); - assert_eq!(first, second); - assert!( - first - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.starts_with("graph-")) - ); - } - - #[test] - fn release_bundle_cache_is_stable_across_lockfile_edits() { - let root = Path::new("release-build-cache"); - let baseline = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); - assert_eq!( - baseline, - release_bundle_target_dir(root, b"lock-v2", "target-a", "toolchain-a") - ); - } - - #[test] - fn release_bundle_cache_separates_targets_and_toolchains() { - let root = Path::new("release-build-cache"); - let baseline = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); - assert_ne!( - baseline, - release_bundle_target_dir(root, b"lock-v1", "target-b", "toolchain-a") - ); - assert_ne!( - baseline, - release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-b") - ); - } - - #[test] - fn release_source_cache_is_stable_per_repository() { - let root = TempDir::new().expect("temporary root"); - let first = root.path().join("first"); - let second = root.path().join("second"); - let linked = root.path().join("linked"); - for repo in [&first, &second] { - fs::create_dir_all(repo).expect("repository path"); - run_git_checked(repo, &["init"], "initialize test repository") - .expect("initialized repository"); - run_git_checked( - repo, - &[ - "-c", - "user.name=Epiphany Test", - "-c", - "user.email=epiphany-test@invalid", - "commit", - "--allow-empty", - "-m", - "cache identity fixture", - ], - "commit test repository", - ) - .expect("committed repository"); - } - run_git_checked( - &first, - &[ - "worktree", - "add", - "--detach", - linked.to_str().expect("UTF-8 path"), - ], - "create linked test worktree", - ) - .expect("linked worktree"); - - assert_eq!( - release_source_cache_identity(&first).expect("first identity"), - release_source_cache_identity(&first).expect("stable first identity") - ); - assert_eq!( - release_source_cache_identity(&first).expect("main identity"), - release_source_cache_identity(&linked).expect("linked identity") - ); - assert_ne!( - release_source_cache_identity(&first).expect("first identity"), - release_source_cache_identity(&second).expect("second identity") - ); - } - - #[test] - fn release_bundle_lockfile_is_frozen() { - let core = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let repo = core - .parent() - .expect("epiphany-core has a repository parent"); - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - verify_release_bundle_lock(repo, &cargo) - .unwrap_or_else(|error| panic!("release bundle lockfile is not frozen: {error:#}")); - } - - #[test] - fn packaged_release_uses_one_deterministic_cargo_graph() { - let required = required_packaged_release_binaries("x86_64-unknown-linux-gnu"); - let command = release_build_command( - std::ffi::OsStr::new("cargo"), - Path::new("Cargo.toml"), - Path::new("target"), - "x86_64-unknown-linux-gnu", - &required, - ) - .expect("release build command"); - let args = command - .get_args() - .map(|arg| arg.to_string_lossy().into_owned()) - .collect::>(); - let envs = command - .get_envs() - .map(|(key, value)| { - ( - key.to_string_lossy().into_owned(), - value.map(|value| value.to_string_lossy().into_owned()), - ) - }) - .collect::>(); - - assert_eq!(envs.get("CARGO_INCREMENTAL"), Some(&Some("0".to_string()))); - assert_eq!(args.iter().filter(|arg| *arg == "--bin").count(), 24); - assert_eq!(args.iter().filter(|arg| *arg == "--package").count(), 1); - assert!( - args.windows(2) - .any(|pair| pair == ["--package", "epiphany-release-bundle"]) - ); - assert!(!args.iter().any(|arg| arg == "epiphany-core")); - assert!( - args.windows(2) - .any(|pair| pair == ["--features", "epiphany-release-bundle/release-runtime"]) - ); - assert!( - args.windows(2) - .any(|pair| pair == ["--bin", "epiphany-mvp-coordinator"]) - ); - assert!( - args.windows(2) - .any(|pair| pair == ["--bin", "epiphany-mvp-status"]) - ); - assert!( - args.windows(2) - .any(|pair| pair == ["--bin", "epiphany-state"]) - ); - assert!(!args.iter().any(|arg| arg == "--bins")); - } - - #[test] - fn resident_cognition_roles_keep_their_owning_manifests() { - let root_manifest = fs::read_to_string( - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("epiphany-core has a repository parent") - .join("Cargo.toml"), - ) - .expect("root release manifest"); - let packaged_roles = required_packaged_release_binaries("x86_64-unknown-linux-gnu") - .into_iter() - .map(|(role, _)| role) - .collect::>(); - for role in [ - "swarm", - "operator-command", - "heartbeat", - "runtime-spine", - "state-steward", - "coordinator", - "hands-action", - "model-runtime", - "persona-service", - "persona-mouth-identity", - "persona-discord-permit", - "persona-discord-permit-identity", - "tool-mcp-runtime", - ] { - assert!(packaged_roles.contains(role), "release omits {role}"); - } - assert_eq!( - required_release_build_target("swarm").unwrap(), - ("epiphany-core", "epiphany-swarm") - ); - assert_eq!( - required_release_build_target("operator-command").unwrap(), - ("epiphany-core", "epiphany-operator-command") - ); - assert_eq!( - required_release_build_target("heartbeat").unwrap(), - ("epiphany-core", "epiphany-heartbeat-store") - ); - assert_eq!( - required_release_build_target("runtime-spine").unwrap(), - ("epiphany-core", "epiphany-runtime-spine") - ); - assert_eq!( - required_release_build_target("state-steward").unwrap(), - ("epiphany-core", "epiphany-state") - ); - assert!(root_manifest.contains("name = \"epiphany-runtime-spine\"")); - assert_eq!( - required_release_build_target("coordinator").unwrap(), - (".", "epiphany-mvp-coordinator") - ); - assert_eq!( - required_release_build_target("hands-action").unwrap(), - ("epiphany-core", "epiphany-hands-action") - ); - assert_eq!( - required_release_build_target("model-runtime").unwrap(), - ("epiphany-openai-runtime", "epiphany-model-runtime") - ); - assert_eq!( - required_release_build_target("persona-service").unwrap(), - ("epiphany-openai-runtime", "epiphany-persona-service") - ); - assert_eq!( - required_release_build_target("persona-mouth-identity").unwrap(), - ("epiphany-core", "epiphany-persona-mouth-identity") - ); - assert_eq!( - required_release_build_target("persona-discord-permit").unwrap(), - ("epiphany-core", "epiphany-persona-discord-permit") - ); - assert_eq!( - required_release_build_target("persona-discord-permit-identity").unwrap(), - ("epiphany-core", "epiphany-persona-discord-permit-identity") - ); - assert_eq!( - required_release_build_target("tool-mcp-runtime").unwrap(), - ("epiphany-tool-mcp-runtime", "epiphany-tool-mcp-runtime") - ); - } - - #[test] - fn root_workspace_owns_first_party_dependency_resolution() { - let core = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let repo = core.parent().expect("epiphany-core repository parent"); - let root_manifest = fs::read_to_string(repo.join("Cargo.toml")).expect("root manifest"); - assert!(root_manifest.contains("[workspace]")); - for member in [ - "epiphany-core", - "epiphany-model-adapter", - "epiphany-openai-adapter", - "epiphany-openai-auth-spine", - "epiphany-openai-codex-spine", - "epiphany-openai-runtime", - "epiphany-self-policy", - "epiphany-state-model", - "epiphany-tool-adapter", - "epiphany-tool-mcp-runtime", - ] { - assert!( - root_manifest.contains(&format!("\"{member}\"")), - "root workspace omits {member}" - ); - assert!( - !repo.join(member).join("Cargo.lock").exists(), - "{member} retains a competing Cargo.lock" - ); - } - } - - #[test] - fn coordinator_policy_is_feature_gated_outside_core_binary_ownership() { - let core = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let repo = core.parent().expect("epiphany-core repository parent"); - let root_manifest = fs::read_to_string(repo.join("Cargo.toml")).expect("root manifest"); - let core_manifest = fs::read_to_string(core.join("Cargo.toml")).expect("core manifest"); - - assert!(root_manifest.contains("coordinator-runtime = [\"dep:epiphany-self-policy\"]")); - assert!(root_manifest.contains( - "release-runtime = [\"coordinator-runtime\", \"openai-runtime\", \"tool-mcp-runtime\"]" - )); - assert!(root_manifest.contains("required-features = [\"coordinator-runtime\"]")); - assert!(core_manifest.contains("autobins = false")); - assert!(!core_manifest.contains("name = \"epiphany-mvp-coordinator\"")); - assert!(!core_manifest.contains("name = \"epiphany-mvp-status\"")); - } - - fn fixture() -> (TempDir, EpiphanyPackagedReleaseEntry) { - let dir = TempDir::new().unwrap(); - let root = dir.path().join("release"); - fs::create_dir(&root).unwrap(); - let mut binaries = Vec::new(); - for (index, (role, name)) in required_packaged_release_binaries("x86_64-unknown-linux-gnu") - .into_iter() - .enumerate() - { - let path = root.join(&name); - fs::write(&path, format!("binary-{index}")).unwrap(); - binaries.push(binary_record(role, &name, &path).unwrap()); - } - binaries.sort_by(|a, b| a.role.cmp(&b.role)); - let id = release_id("runtime", &"a".repeat(40), "target", "rustc", &binaries); - let entry = EpiphanyPackagedReleaseEntry { - schema_version: EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(), - release_id: id, - runtime_id: "runtime".into(), - source_commit_sha: "a".repeat(40), - target_triple: "target".into(), - cargo_profile: "release".into(), - toolchain_fingerprint: "rustc".into(), - created_at_utc: Utc::now().to_rfc3339(), - package_root: canonical_path(root).unwrap().display().to_string(), - binaries, - private_state_exposed: false, - }; - write_epiphany_packaged_release_witness( - &Path::new(&entry.package_root).join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE), - &entry, - ) - .unwrap(); - (dir, entry) - } - - #[test] - fn exact_fixture_verifies() { - let (_d, e) = fixture(); - verify_epiphany_packaged_release_files(&e).unwrap(); - } - #[test] - fn witness_reader_refuses_tamper_and_inspector_refuses_wrong_runtime() { - let (d, e) = fixture(); - let witness = Path::new(&e.package_root).join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE); - assert!( - inspect_epiphany_packaged_release_witness( - &witness, - d.path(), - "alien-runtime", - &e.source_commit_sha, - ) - .is_err() - ); - fs::write(&witness, b"hostile witness").unwrap(); - assert!(read_epiphany_packaged_release_witness(&witness).is_err()); - } - #[test] - fn one_byte_replacement_is_rejected() { - let (_d, e) = fixture(); - fs::write(&e.binaries[0].canonical_path, "hostile").unwrap(); - assert!(verify_epiphany_packaged_release_files(&e).is_err()); - } - #[test] - fn extra_sibling_is_rejected() { - let (_d, e) = fixture(); - fs::write(Path::new(&e.package_root).join("stowaway.exe"), "x").unwrap(); - assert!(verify_epiphany_packaged_release_files(&e).is_err()); - } - #[test] - fn swapped_role_is_rejected() { - let (_d, mut e) = fixture(); - let role = e.binaries[0].role.clone(); - e.binaries[0].role = e.binaries[1].role.clone(); - e.binaries[1].role = role; - assert!(validate_epiphany_packaged_release(&e).is_err()); - } - #[test] - fn missing_resident_cognition_binary_is_rejected() { - let (_d, mut e) = fixture(); - e.binaries.retain(|binary| binary.role != "model-runtime"); - assert!(validate_epiphany_packaged_release(&e).is_err()); - } - #[test] - fn substituted_resident_cognition_binary_is_rejected() { - let (_d, mut e) = fixture(); - let model = e - .binaries - .iter_mut() - .find(|binary| binary.role == "model-runtime") - .unwrap(); - model.file_name = "epiphany-openai-runtime".into(); - assert!(validate_epiphany_packaged_release(&e).is_err()); - } - #[test] - fn counterfeit_release_id_is_rejected() { - let (_d, mut e) = fixture(); - e.release_id = format!("sha256-{}", "0".repeat(64)); - assert!(validate_epiphany_packaged_release(&e).is_err()); - } -} diff --git a/epiphany-release-construction/Cargo.toml b/epiphany-release-construction/Cargo.toml new file mode 100644 index 00000000..8f83bd2b --- /dev/null +++ b/epiphany-release-construction/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "epiphany-release-construction" +version = "0.1.0" +edition = "2024" +publish = false +autobins = false +license-file = "../LICENSE" + +[lib] +name = "epiphany_release_construction" +path = "src/lib.rs" + +[[bin]] +name = "epiphany-package" +path = "src/bin/epiphany-package.rs" + +[dependencies] +anyhow = "1" +chrono = "0.4.44" +cultcache-rs = { path = "../vendor/cultcache-rs" } +fs2 = "0.4" +rmp-serde = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +uuid = { version = "1", features = ["v4"] } + +[dev-dependencies] +tempfile = "3" diff --git a/epiphany-release-construction/src/bin/epiphany-package.rs b/epiphany-release-construction/src/bin/epiphany-package.rs new file mode 100644 index 00000000..6ff80b11 --- /dev/null +++ b/epiphany-release-construction/src/bin/epiphany-package.rs @@ -0,0 +1,97 @@ +use anyhow::{Context, Result, bail}; +use epiphany_release_construction::{ + EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE, PackageReleaseRequest, + epiphany_packaged_release_witness_sha256, inspect_epiphany_packaged_release_witness, + package_epiphany_release, +}; +use std::{env, path::PathBuf, process::Command}; + +fn main() -> Result<()> { + let mut args = env::args().skip(1); + let command = args.next().context("missing command: package or inspect")?; + let mut repo = None; + let mut destination = None; + let mut build_cache = None; + let mut witness = None; + let mut runtime = None; + let mut source_commit = None; + let mut target = None; + while let Some(flag) = args.next() { + let value = args + .next() + .with_context(|| format!("missing value for {flag}"))?; + match flag.as_str() { + "--repo" => repo = Some(PathBuf::from(value)), + "--destination" => destination = Some(PathBuf::from(value)), + "--build-cache-root" => build_cache = Some(PathBuf::from(value)), + "--witness" => witness = Some(PathBuf::from(value)), + "--runtime-id" => runtime = Some(value), + "--source-commit" => source_commit = Some(value), + "--target-triple" => target = Some(value), + _ => bail!("unknown argument {flag}"), + } + } + let destination = destination.context("missing --destination")?; + let runtime = runtime.context("missing --runtime-id")?; + let entry = match command.as_str() { + "package" => { + let repo = repo.context("missing --repo")?; + let build_cache = build_cache.context("missing --build-cache-root")?; + let target = target + .or_else(host_target) + .context("rustc did not report host target")?; + package_epiphany_release(PackageReleaseRequest { + repo: &repo, + destination: &destination, + build_cache_root: &build_cache, + runtime_id: &runtime, + target_triple: &target, + })? + } + "inspect" => { + let witness = witness.context("missing --witness")?; + let source_commit = source_commit.context("missing --source-commit")?; + inspect_epiphany_packaged_release_witness( + &witness, + &destination, + &runtime, + &source_commit, + )? + } + _ => bail!("unknown command {command}; expected package or inspect"), + }; + let digest = epiphany_packaged_release_witness_sha256(&entry)?; + println!( + "{}", + serde_json::to_string(&serde_json::json!({ + "schemaVersion": entry.schema_version, + "status": command, + "releaseId": entry.release_id, + "witnessSha256": digest, + "sourceCommit": entry.source_commit_sha, + "packageRoot": entry.package_root, + "witnessPath": PathBuf::from(&entry.package_root).join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE), + "binaryCount": entry.binaries.len(), + "privateStateExposed": false + }))? + ); + Ok(()) +} + +fn host_target() -> Option { + command_output("rustc", &["-vV"]) + .ok()? + .lines() + .find_map(|line| line.strip_prefix("host: ").map(str::to_string)) +} + +fn command_output(command: &str, args: &[&str]) -> Result { + let output = Command::new(command).args(args).output()?; + if !output.status.success() { + bail!( + "{command} failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8(output.stdout)?.trim().to_string()) +} diff --git a/epiphany-release-construction/src/lib.rs b/epiphany-release-construction/src/lib.rs new file mode 100644 index 00000000..e0d4dd95 --- /dev/null +++ b/epiphany-release-construction/src/lib.rs @@ -0,0 +1,1343 @@ +use anyhow::{Context, Result, anyhow, bail}; +use chrono::DateTime; +#[cfg(test)] +use chrono::Utc; +use cultcache_rs::DatabaseEntry; +use fs2::FileExt; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use uuid::Uuid; + +pub const EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION: &str = "epiphany.packaged_release.v0"; +pub const EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION: &str = "epiphany.packaged_release_head.v0"; +pub const EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE: &str = "release-witness.ccmp"; + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EpiphanyPackagedReleaseBinary { + pub role: String, + pub file_name: String, + pub canonical_path: String, + pub sha256: String, + pub byte_len: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.packaged_release", + schema = "EpiphanyPackagedReleaseEntry" +)] +pub struct EpiphanyPackagedReleaseEntry { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub release_id: String, + #[cultcache(key = 2)] + pub runtime_id: String, + #[cultcache(key = 3)] + pub source_commit_sha: String, + #[cultcache(key = 4)] + pub target_triple: String, + #[cultcache(key = 5)] + pub cargo_profile: String, + #[cultcache(key = 6)] + pub toolchain_fingerprint: String, + #[cultcache(key = 7)] + pub created_at_utc: String, + #[cultcache(key = 8)] + pub package_root: String, + #[cultcache(key = 9)] + pub binaries: Vec, + #[cultcache(key = 10)] + pub private_state_exposed: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.packaged_release_head", + schema = "EpiphanyPackagedReleaseHead" +)] +pub struct EpiphanyPackagedReleaseHead { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub runtime_id: String, + #[cultcache(key = 2)] + pub release_id: String, + #[cultcache(key = 3)] + pub witness_sha256: String, + #[cultcache(key = 4)] + pub published_at_utc: String, +} + +pub struct PackageReleaseRequest<'a> { + pub repo: &'a Path, + pub destination: &'a Path, + pub build_cache_root: &'a Path, + pub runtime_id: &'a str, + pub target_triple: &'a str, +} + +pub fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static str, String)> { + let file_name = |name: &str| target_binary_file_name(target_triple, name); + vec![ + ("supervisor", file_name("epiphany-daemon-supervisor")), + ("release-publisher", file_name("epiphany-release")), + ("state-steward", file_name("epiphany-state")), + ( + "semantic-projector", + file_name("epiphany-memory-semantic-projector"), + ), + ( + "workspace-coverage-projector", + file_name("epiphany-workspace-coverage-projector"), + ), + ("semantic-query", file_name("epiphany-memory-semantic")), + ("verse-query", file_name("epiphany-verse-query")), + ("repository-body", file_name("epiphany-repository-body")), + ("host-identity", file_name("epiphany-host-identity")), + ("swarm", file_name("epiphany-swarm")), + ("operator-command", file_name("epiphany-operator-command")), + ("heartbeat", file_name("epiphany-heartbeat-store")), + ("runtime-spine", file_name("epiphany-runtime-spine")), + ( + "persona-feedback-ingress", + file_name("epiphany-persona-feedback-ingress"), + ), + ("persona-service", file_name("epiphany-persona-service")), + ( + "persona-mouth-identity", + file_name("epiphany-persona-mouth-identity"), + ), + ( + "persona-discord-permit", + file_name("epiphany-persona-discord-permit"), + ), + ( + "persona-discord-permit-identity", + file_name("epiphany-persona-discord-permit-identity"), + ), + ("coordinator", file_name("epiphany-mvp-coordinator")), + ("coordinator-status", file_name("epiphany-mvp-status")), + ("frontier-proposal", file_name("epiphany-frontier-proposal")), + ("hands-action", file_name("epiphany-hands-action")), + ("model-runtime", file_name("epiphany-model-runtime")), + ("tool-mcp-runtime", file_name("epiphany-tool-mcp-runtime")), + ] +} + +fn target_binary_file_name(target_triple: &str, binary: &str) -> String { + if target_triple + .split('-') + .any(|component| component == "windows") + { + format!("{binary}.exe") + } else { + binary.to_string() + } +} + +pub fn package_epiphany_release( + request: PackageReleaseRequest<'_>, +) -> Result { + let (source_commit_sha, source_commit_time) = clean_source_commit(request.repo)?; + require_nonempty("runtime id", request.runtime_id)?; + require_nonempty("target triple", request.target_triple)?; + let toolchain = installed_toolchain()?; + fs::create_dir_all(request.destination).with_context(|| { + format!( + "failed to create release destination {}", + request.destination.display() + ) + })?; + let build_root = request.build_cache_root.to_path_buf(); + fs::create_dir_all(&build_root).with_context(|| { + format!( + "failed to create stable build cache {}", + build_root.display() + ) + })?; + let destination = canonical_path(request.destination)?; + let source_guard = ReleaseSourceGuard::prepare(request.repo, &build_root, &source_commit_sha)?; + let built_binaries = build_required_release_siblings( + &source_guard.path, + &build_root, + request.target_triple, + &toolchain.fingerprint, + &toolchain.cargo, + )?; + // Construction authority is scoped to one exact source generation. The + // shared release root remains root-owned while the builder creates only a + // private staging sibling beneath its commit-specific directory. + let commit_root = destination.join(&source_commit_sha); + fs::create_dir_all(&commit_root).with_context(|| { + format!( + "failed to create release commit root {}", + commit_root.display() + ) + })?; + let staging = commit_root.join(format!(".staging-{}", Uuid::new_v4())); + fs::create_dir(&staging) + .with_context(|| format!("failed to create release staging {}", staging.display()))?; + let result = (|| { + let mut binaries = Vec::new(); + for (role, file_name) in required_packaged_release_binaries(request.target_triple) { + let source = built_binaries + .binaries + .get(role) + .with_context(|| format!("required packaged sibling was not built: {role}"))?; + if !source.is_file() { + bail!("required packaged sibling is absent: {}", source.display()); + } + let target = staging.join(&file_name); + fs::copy(&source, &target).with_context(|| { + format!( + "failed to copy {} to {}", + source.display(), + target.display() + ) + })?; + binaries.push(binary_record(role, &file_name, &target)?); + } + binaries.sort_by(|left, right| left.role.cmp(&right.role)); + let release_id = release_id( + request.runtime_id, + &source_commit_sha, + request.target_triple, + &toolchain.fingerprint, + &binaries, + ); + let final_root = commit_root.join(&release_id); + for binary in &mut binaries { + binary.canonical_path = final_root.join(&binary.file_name).display().to_string(); + } + let witness = EpiphanyPackagedReleaseEntry { + schema_version: EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(), + release_id, + runtime_id: request.runtime_id.into(), + source_commit_sha, + target_triple: request.target_triple.into(), + cargo_profile: "release".into(), + toolchain_fingerprint: toolchain.fingerprint, + created_at_utc: source_commit_time, + package_root: final_root.display().to_string(), + binaries, + private_state_exposed: false, + }; + validate_epiphany_packaged_release(&witness)?; + write_epiphany_packaged_release_witness( + &staging.join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE), + &witness, + )?; + if final_root.exists() { + verify_epiphany_packaged_release_files(&witness)?; + fs::remove_dir_all(&staging)?; + } else { + fs::rename(&staging, &final_root) + .context("failed to atomically publish packaged release directory")?; + verify_epiphany_packaged_release_files(&witness)?; + } + Ok(witness) + })(); + if staging.exists() { + let _ = fs::remove_dir_all(staging); + } + result +} + +fn release_source_cache_identity(repo: &Path) -> Result { + let common_dir = PathBuf::from(git_output(repo, &["rev-parse", "--git-common-dir"])?.trim()); + let common_dir = if common_dir.is_absolute() { + canonical_path(&common_dir)? + } else { + canonical_path(&repo.join(common_dir))? + }; + // Linked worktrees are views of one repository and must share one exact-source + // cache. For ordinary non-bare repositories, hashing the common `.git` parent + // also preserves the cache identity used before worktree-aware ownership. + let cache_owner = if common_dir.file_name().is_some_and(|name| name == ".git") { + common_dir + .parent() + .ok_or_else(|| anyhow!("repository common directory has no owner"))? + } else { + common_dir.as_path() + }; + let mut digest = Sha256::new(); + digest.update(b"epiphany.release_source_cache.v0\0"); + digest.update(cache_owner.to_string_lossy().as_bytes()); + Ok(format!("{:x}", digest.finalize())) +} + +struct ReleaseSourceGuard { + path: PathBuf, + _source_lock: fs::File, +} + +impl ReleaseSourceGuard { + fn prepare(repo: &Path, build_root: &Path, commit: &str) -> Result { + validate_commit(commit)?; + let identity = release_source_cache_identity(repo)?; + let path = build_root.join(format!("source-{identity}")); + let lock_path = build_root.join(format!(".epiphany-release-source-{identity}.lock")); + let source_lock = fs::OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(&lock_path) + .with_context(|| { + format!("failed to open release source lock {}", lock_path.display()) + })?; + source_lock.lock_exclusive().with_context(|| { + format!( + "failed to acquire release source lock {}", + lock_path.display() + ) + })?; + + if path.exists() { + verify_cached_worktree_owner(repo, &path)?; + run_git_checked( + &path, + &["-c", "core.longpaths=true", "clean", "-ffdx"], + "failed to pre-clean cached release worktree", + )?; + run_git_checked( + &path, + &[ + "-c", + "core.longpaths=true", + "checkout", + "--detach", + "--force", + commit, + ], + "failed to move cached release worktree to exact commit", + )?; + run_git_checked( + &path, + &["-c", "core.longpaths=true", "clean", "-ffdx"], + "failed to clean cached release worktree", + )?; + } else { + let output = std::process::Command::new("git") + .args(["-c", "core.longpaths=true", "worktree", "add", "--detach"]) + .arg(&path) + .arg(commit) + .current_dir(repo) + .output()?; + if !output.status.success() { + bail!( + "failed to create cached exact-source release worktree: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + } + + run_git_checked( + &path, + &[ + "-c", + "core.longpaths=true", + "submodule", + "update", + "--init", + "--recursive", + "--force", + ], + "failed to initialize exact release submodules", + )?; + run_git_checked( + &path, + &["submodule", "foreach", "--recursive", "git reset --hard"], + "failed to reset exact release submodules", + )?; + run_git_checked( + &path, + &["submodule", "foreach", "--recursive", "git clean -ffdx"], + "failed to clean exact release submodules", + )?; + let head = git_output(&path, &["rev-parse", "HEAD"])?; + if head.trim() != commit { + bail!("cached release worktree resolved {head:?}, expected {commit}"); + } + let status = git_output( + &path, + &["status", "--porcelain=v1", "--untracked-files=all"], + )?; + if !status.trim().is_empty() { + bail!("cached release worktree is not clean after preparation: {status}"); + } + let submodule_status = git_output(&path, &["submodule", "status", "--recursive"])?; + if submodule_status + .lines() + .any(|line| matches!(line.as_bytes().first(), Some(b'-' | b'+' | b'U'))) + { + bail!("cached release worktree has inexact submodules: {submodule_status}"); + } + Ok(Self { + path, + _source_lock: source_lock, + }) + } +} + +fn verify_cached_worktree_owner(repo: &Path, worktree: &Path) -> Result<()> { + let expected = PathBuf::from(git_output(repo, &["rev-parse", "--git-common-dir"])?.trim()); + let expected = if expected.is_absolute() { + canonical_path(&expected)? + } else { + canonical_path(&repo.join(expected))? + }; + let common = PathBuf::from(git_output(worktree, &["rev-parse", "--git-common-dir"])?.trim()); + let common = if common.is_absolute() { + canonical_path(&common)? + } else { + canonical_path(&worktree.join(common))? + }; + if common != expected { + bail!( + "cached release worktree {} belongs to {}, expected {}", + worktree.display(), + common.display(), + expected.display() + ); + } + Ok(()) +} + +fn git_output(repo: &Path, args: &[&str]) -> Result { + let output = std::process::Command::new("git") + .args(["-c", "core.longpaths=true"]) + .args(args) + .current_dir(repo) + .output()?; + if !output.status.success() { + bail!( + "git {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8(output.stdout)?) +} + +fn run_git_checked(repo: &Path, args: &[&str], context: &str) -> Result<()> { + git_output(repo, args).with_context(|| context.to_string())?; + Ok(()) +} + +struct BuiltReleaseSiblings { + binaries: BTreeMap<&'static str, PathBuf>, + _graph_lock: fs::File, +} + +fn build_required_release_siblings( + repo: &Path, + target_root: &Path, + target: &str, + toolchain_fingerprint: &str, + cargo: &std::ffi::OsStr, +) -> Result { + verify_release_bundle_lock(repo, cargo)?; + let manifest = repo.join("Cargo.toml"); + let target_dir = release_bundle_target_dir( + target_root, + &fs::read(repo.join("Cargo.lock")).context("failed to read release bundle lockfile")?, + target, + toolchain_fingerprint, + ); + fs::create_dir_all(&target_dir).with_context(|| { + format!( + "failed to create release graph cache {}", + target_dir.display() + ) + })?; + let graph_lock_path = target_dir.join(".epiphany-release-graph.lock"); + let graph_lock = fs::OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(&graph_lock_path) + .with_context(|| { + format!( + "failed to open release graph lock {}", + graph_lock_path.display() + ) + })?; + graph_lock.lock_exclusive().with_context(|| { + format!( + "failed to acquire release graph lock {}", + graph_lock_path.display() + ) + })?; + let mut outputs = BTreeMap::new(); + let required = required_packaged_release_binaries(target); + for (role, file_name) in &required { + required_release_build_target(role)?; + outputs.insert( + *role, + target_dir.join(target).join("release").join(file_name), + ); + } + let mut command = release_build_command(cargo, &manifest, &target_dir, target, &required)?; + let status = command + .status() + .context("failed to start Epiphany release bundle build")?; + if !status.success() { + bail!("Epiphany release bundle build failed"); + } + Ok(BuiltReleaseSiblings { + binaries: outputs, + _graph_lock: graph_lock, + }) +} + +fn release_build_command( + cargo: &std::ffi::OsStr, + manifest: &Path, + target_dir: &Path, + target: &str, + required: &[(&'static str, String)], +) -> Result { + let mut command = std::process::Command::new(cargo); + command + .env("CARGO_INCREMENTAL", "0") + .arg("build") + .arg("--release") + .arg("--manifest-path") + .arg(&manifest) + .arg("--target-dir") + .arg(&target_dir) + .arg("--target") + .arg(target) + .arg("--locked") + .arg("--package") + .arg("epiphany-release-bundle") + .arg("--features") + .arg("epiphany-release-bundle/release-runtime"); + for (_, file_name) in required { + let binary_name = Path::new(file_name) + .file_stem() + .and_then(std::ffi::OsStr::to_str) + .with_context(|| format!("packaged binary name is invalid: {file_name}"))?; + command.arg("--bin").arg(binary_name); + } + Ok(command) +} + +fn verify_release_bundle_lock(repo: &Path, cargo: &std::ffi::OsStr) -> Result<()> { + let manifest = repo.join("Cargo.toml"); + if !manifest.is_file() { + bail!( + "Epiphany release bundle manifest is absent: {}", + manifest.display() + ); + } + let output = std::process::Command::new(cargo) + .arg("metadata") + .arg("--locked") + .arg("--no-deps") + .arg("--format-version") + .arg("1") + .arg("--manifest-path") + .arg(&manifest) + .output() + .context("failed to validate Epiphany release bundle lockfile")?; + if !output.status.success() { + bail!( + "Epiphany release bundle lockfile is stale: {}", + String::from_utf8_lossy(&output.stderr).trim() + ); + } + Ok(()) +} + +fn release_bundle_target_dir( + target_root: &Path, + _lockfile: &[u8], + target: &str, + toolchain_fingerprint: &str, +) -> PathBuf { + let mut digest = Sha256::new(); + digest.update(b"epiphany.release_bundle_build_cache.v1\0"); + digest.update(target.as_bytes()); + digest.update(b"\0"); + digest.update(toolchain_fingerprint.as_bytes()); + target_root.join(format!("graph-{:x}", digest.finalize())) +} + +fn required_release_build_target(role: &str) -> Result<(&'static str, &'static str)> { + match role { + "supervisor" => Ok(("epiphany-core", "epiphany-daemon-supervisor")), + "release-publisher" => Ok(("epiphany-core", "epiphany-release")), + "state-steward" => Ok(("epiphany-core", "epiphany-state")), + "semantic-projector" => Ok(("epiphany-core", "epiphany-memory-semantic-projector")), + "workspace-coverage-projector" => { + Ok(("epiphany-core", "epiphany-workspace-coverage-projector")) + } + "semantic-query" => Ok(("epiphany-core", "epiphany-memory-semantic")), + "verse-query" => Ok(("epiphany-core", "epiphany-verse-query")), + "repository-body" => Ok(("epiphany-core", "epiphany-repository-body")), + "host-identity" => Ok(("epiphany-core", "epiphany-host-identity")), + "swarm" => Ok(("epiphany-core", "epiphany-swarm")), + "operator-command" => Ok(("epiphany-core", "epiphany-operator-command")), + "heartbeat" => Ok(("epiphany-core", "epiphany-heartbeat-store")), + "runtime-spine" => Ok(("epiphany-core", "epiphany-runtime-spine")), + "persona-feedback-ingress" => Ok(("epiphany-core", "epiphany-persona-feedback-ingress")), + "persona-service" => Ok(("epiphany-openai-runtime", "epiphany-persona-service")), + "persona-mouth-identity" => Ok(("epiphany-core", "epiphany-persona-mouth-identity")), + "persona-discord-permit" => Ok(("epiphany-core", "epiphany-persona-discord-permit")), + "persona-discord-permit-identity" => { + Ok(("epiphany-core", "epiphany-persona-discord-permit-identity")) + } + "coordinator" => Ok((".", "epiphany-mvp-coordinator")), + "coordinator-status" => Ok((".", "epiphany-mvp-status")), + "frontier-proposal" => Ok(("epiphany-core", "epiphany-frontier-proposal")), + "hands-action" => Ok(("epiphany-core", "epiphany-hands-action")), + "model-runtime" => Ok(("epiphany-openai-runtime", "epiphany-model-runtime")), + "tool-mcp-runtime" => Ok(("epiphany-tool-mcp-runtime", "epiphany-tool-mcp-runtime")), + _ => bail!("unknown required release role {role}"), + } +} + +pub fn validate_epiphany_packaged_release(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> { + if entry.schema_version != EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION { + bail!("unsupported packaged release schema"); + } + require_nonempty("runtime id", &entry.runtime_id)?; + validate_commit(&entry.source_commit_sha)?; + require_nonempty("target triple", &entry.target_triple)?; + if entry.cargo_profile != "release" { + bail!("packaged release profile must be release"); + } + require_nonempty("toolchain fingerprint", &entry.toolchain_fingerprint)?; + DateTime::parse_from_rfc3339(&entry.created_at_utc)?; + if entry.private_state_exposed { + bail!("packaged release may not expose private state"); + } + let root = Path::new(&entry.package_root); + if !root.is_absolute() { + bail!("packaged release root must be absolute"); + } + let required = required_packaged_release_binaries(&entry.target_triple); + if entry.binaries.len() != required.len() { + bail!("packaged release sibling set is incomplete"); + } + let mut roles = BTreeSet::new(); + let mut names = BTreeSet::new(); + for binary in &entry.binaries { + if !roles.insert(binary.role.as_str()) || !names.insert(binary.file_name.as_str()) { + bail!("packaged release contains duplicate role or filename"); + } + if !required + .iter() + .any(|pair| pair.0 == binary.role && pair.1 == binary.file_name) + { + bail!("packaged release contains an unknown or swapped sibling role"); + } + validate_sha256(&binary.sha256)?; + if binary.byte_len == 0 { + bail!("packaged release binary is empty"); + } + if Path::new(&binary.canonical_path) != root.join(&binary.file_name) { + bail!("packaged release binary path escapes or disagrees with package root"); + } + } + let mut identity_binaries = entry.binaries.clone(); + identity_binaries.sort_by(|left, right| left.role.cmp(&right.role)); + let expected = release_id( + &entry.runtime_id, + &entry.source_commit_sha, + &entry.target_triple, + &entry.toolchain_fingerprint, + &identity_binaries, + ); + if entry.release_id != expected { + bail!("packaged release id does not authenticate sibling set"); + } + Ok(()) +} + +pub fn verify_epiphany_packaged_release_files(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> { + validate_epiphany_packaged_release(entry)?; + let root = canonical_path(&entry.package_root).context("packaged release root is absent")?; + if root != PathBuf::from(&entry.package_root) { + bail!("packaged release root is not canonical"); + } + let actual_names = fs::read_dir(&root)? + .map(|item| Ok(item?.file_name().to_string_lossy().into_owned())) + .collect::>>()?; + let mut expected_names = entry + .binaries + .iter() + .map(|binary| binary.file_name.clone()) + .collect::>(); + expected_names.insert(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE.into()); + if actual_names != expected_names { + bail!("packaged release directory is not the exact witnessed sibling set"); + } + for binary in &entry.binaries { + let path = root.join(&binary.file_name); + if canonical_path(&path)? != PathBuf::from(&binary.canonical_path) { + bail!("packaged sibling path aliases another file"); + } + let metadata = fs::metadata(&path)?; + if !metadata.is_file() + || metadata.len() != binary.byte_len + || file_sha256(&path)? != binary.sha256 + { + bail!( + "packaged sibling bytes disagree with witness: {}", + binary.role + ); + } + } + let stored = + read_epiphany_packaged_release_witness(&root.join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE))?; + if stored != *entry { + bail!("packaged release witness artifact disagrees with inspected release"); + } + Ok(()) +} + +pub fn write_epiphany_packaged_release_witness( + path: &Path, + entry: &EpiphanyPackagedReleaseEntry, +) -> Result<()> { + validate_epiphany_packaged_release(entry)?; + let bytes = rmp_serde::to_vec(entry).context("failed to encode packaged release witness")?; + fs::write(path, bytes).with_context(|| { + format!( + "failed to write packaged release witness {}", + path.display() + ) + }) +} + +pub fn read_epiphany_packaged_release_witness(path: &Path) -> Result { + let bytes = fs::read(path) + .with_context(|| format!("failed to read packaged release witness {}", path.display()))?; + let entry = rmp_serde::from_slice(&bytes).with_context(|| { + format!( + "failed to decode packaged release witness {}", + path.display() + ) + })?; + validate_epiphany_packaged_release(&entry)?; + Ok(entry) +} + +pub fn inspect_epiphany_packaged_release_witness( + witness_path: &Path, + destination: &Path, + runtime_id: &str, + source_commit: &str, +) -> Result { + let entry = read_epiphany_packaged_release_witness(witness_path)?; + if entry.runtime_id != runtime_id || entry.source_commit_sha != source_commit { + bail!("packaged release witness disagrees with authorized runtime or source commit"); + } + let destination = canonical_path(destination)?; + let package_root = canonical_path(&entry.package_root)?; + if package_root.parent().and_then(Path::parent) != Some(destination.as_path()) { + bail!("packaged release root is outside the canonical destination"); + } + if package_root.join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE) != canonical_path(witness_path)? { + bail!("packaged release witness path disagrees with package root"); + } + verify_epiphany_packaged_release_files(&entry)?; + Ok(entry) +} + +pub fn epiphany_packaged_release_witness_sha256( + entry: &EpiphanyPackagedReleaseEntry, +) -> Result { + witness_sha256(entry) +} + +pub fn validate_epiphany_packaged_release_sha256(value: &str) -> Result<()> { + validate_sha256(value) +} + +pub fn epiphany_packaged_release_binary_path( + entry: &EpiphanyPackagedReleaseEntry, + role: &str, +) -> Result { + entry + .binaries + .iter() + .find(|binary| binary.role == role) + .map(|binary| PathBuf::from(&binary.canonical_path)) + .with_context(|| format!("packaged release lacks required role {role}")) +} + +fn clean_source_commit(repo: &Path) -> Result<(String, String)> { + let head = git(repo, &["rev-parse", "HEAD"])?; + validate_commit(&head)?; + if !git(repo, &["status", "--porcelain=v1", "--untracked-files=no"])?.is_empty() { + bail!("packaged release requires a clean tracked source commit"); + } + if git(repo, &["submodule", "status", "--recursive"])? + .lines() + .any(|line| line.starts_with(['-', '+', 'U'])) + { + bail!("packaged release requires clean initialized submodules"); + } + let committed_at = git(repo, &["show", "-s", "--format=%cI", "HEAD"])?; + DateTime::parse_from_rfc3339(&committed_at).context("Git commit time is not RFC3339")?; + Ok((head, committed_at)) +} + +fn git(repo: &Path, args: &[&str]) -> Result { + let output = std::process::Command::new("git") + .args(args) + .current_dir(repo) + .output()?; + if !output.status.success() { + bail!( + "git {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8(output.stdout)?.trim().to_string()) +} + +struct InstalledToolchain { + cargo: std::ffi::OsString, + fingerprint: String, +} + +fn installed_toolchain() -> Result { + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let cargo_version = command_version(&cargo, "cargo")?; + let rustc_version = command_version(&rustc, "rustc")?; + Ok(InstalledToolchain { + cargo: cargo.clone(), + fingerprint: format!( + "cargo-command={}\ncargo-vV:\n{}\nrustc-command={}\nrustc-vV:\n{}", + Path::new(&cargo).display(), + cargo_version, + Path::new(&rustc).display(), + rustc_version + ), + }) +} + +fn command_version(command: &std::ffi::OsStr, label: &str) -> Result { + let output = std::process::Command::new(command).arg("-vV").output()?; + if !output.status.success() { + bail!( + "{label} -vV failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8(output.stdout)?.trim().to_string()) +} + +fn binary_record( + role: &str, + file_name: &str, + path: &Path, +) -> Result { + let canonical = canonical_path(path)?; + let len = fs::metadata(&canonical)?.len(); + if len == 0 { + bail!("required packaged sibling is empty: {file_name}"); + } + Ok(EpiphanyPackagedReleaseBinary { + role: role.into(), + file_name: file_name.into(), + canonical_path: canonical.display().to_string(), + sha256: file_sha256(&canonical)?, + byte_len: len, + }) +} + +fn release_id( + runtime: &str, + commit: &str, + target: &str, + toolchain: &str, + binaries: &[EpiphanyPackagedReleaseBinary], +) -> String { + let mut hash = Sha256::new(); + for value in [ + "epiphany.packaged-release.identity.v0", + runtime, + commit, + target, + "release", + toolchain, + ] { + hash.update((value.len() as u64).to_be_bytes()); + hash.update(value.as_bytes()); + } + for binary in binaries { + for value in [ + &binary.role, + &binary.file_name, + &binary.sha256, + &binary.byte_len.to_string(), + ] { + hash.update((value.len() as u64).to_be_bytes()); + hash.update(value.as_bytes()); + } + } + format!("sha256-{:x}", hash.finalize()) +} + +fn witness_sha256(entry: &EpiphanyPackagedReleaseEntry) -> Result { + Ok(format!( + "sha256-{:x}", + Sha256::digest(rmp_serde::to_vec(entry)?) + )) +} +fn file_sha256(path: &Path) -> Result { + Ok(format!("sha256-{:x}", Sha256::digest(fs::read(path)?))) +} +fn canonical_path(path: impl AsRef) -> Result { + let canonical = fs::canonicalize(path)?; + #[cfg(windows)] + { + let text = canonical.to_string_lossy(); + if let Some(rest) = text.strip_prefix(r"\\?\UNC\") { + return Ok(PathBuf::from(format!(r"\\{rest}"))); + } + if let Some(rest) = text.strip_prefix(r"\\?\") { + return Ok(PathBuf::from(rest)); + } + } + Ok(canonical) +} +fn validate_sha256(value: &str) -> Result<()> { + if value.len() != 71 + || !value.starts_with("sha256-") + || !value[7..] + .bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + { + bail!("invalid SHA-256 digest"); + } + Ok(()) +} +fn validate_commit(value: &str) -> Result<()> { + if value.len() != 40 || !value.bytes().all(|b| b.is_ascii_hexdigit()) { + bail!("source commit must be a full 40-hex Git object id"); + } + Ok(()) +} +fn require_nonempty(label: &str, value: &str) -> Result<()> { + if value.trim().is_empty() { + return Err(anyhow!("{label} must not be empty")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn every_required_release_role_has_a_build_target() { + for (role, _) in required_packaged_release_binaries("x86_64-unknown-linux-gnu") { + required_release_build_target(role).expect("required release role must resolve"); + } + } + + #[test] + fn binary_suffix_follows_requested_target_not_packager_host() { + let windows = required_packaged_release_binaries("x86_64-pc-windows-msvc"); + assert!(windows.iter().all(|(_, name)| name.ends_with(".exe"))); + let linux = required_packaged_release_binaries("x86_64-unknown-linux-gnu"); + assert!(linux.iter().all(|(_, name)| !name.ends_with(".exe"))); + } + + #[test] + fn release_bundle_cache_is_owned_by_build_graph() { + let root = Path::new("release-build-cache"); + let first = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); + let second = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); + assert_eq!(first, second); + assert!( + first + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("graph-")) + ); + } + + #[test] + fn release_bundle_cache_is_stable_across_lockfile_edits() { + let root = Path::new("release-build-cache"); + let baseline = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); + assert_eq!( + baseline, + release_bundle_target_dir(root, b"lock-v2", "target-a", "toolchain-a") + ); + } + + #[test] + fn release_bundle_cache_separates_targets_and_toolchains() { + let root = Path::new("release-build-cache"); + let baseline = release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-a"); + assert_ne!( + baseline, + release_bundle_target_dir(root, b"lock-v1", "target-b", "toolchain-a") + ); + assert_ne!( + baseline, + release_bundle_target_dir(root, b"lock-v1", "target-a", "toolchain-b") + ); + } + + #[test] + fn release_source_cache_is_stable_per_repository() { + let root = TempDir::new().expect("temporary root"); + let first = root.path().join("first"); + let second = root.path().join("second"); + let linked = root.path().join("linked"); + for repo in [&first, &second] { + fs::create_dir_all(repo).expect("repository path"); + run_git_checked(repo, &["init"], "initialize test repository") + .expect("initialized repository"); + run_git_checked( + repo, + &[ + "-c", + "user.name=Epiphany Test", + "-c", + "user.email=epiphany-test@invalid", + "commit", + "--allow-empty", + "-m", + "cache identity fixture", + ], + "commit test repository", + ) + .expect("committed repository"); + } + run_git_checked( + &first, + &[ + "worktree", + "add", + "--detach", + linked.to_str().expect("UTF-8 path"), + ], + "create linked test worktree", + ) + .expect("linked worktree"); + + assert_eq!( + release_source_cache_identity(&first).expect("first identity"), + release_source_cache_identity(&first).expect("stable first identity") + ); + assert_eq!( + release_source_cache_identity(&first).expect("main identity"), + release_source_cache_identity(&linked).expect("linked identity") + ); + assert_ne!( + release_source_cache_identity(&first).expect("first identity"), + release_source_cache_identity(&second).expect("second identity") + ); + } + + #[test] + fn release_bundle_lockfile_is_frozen() { + let core = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let repo = core + .parent() + .expect("epiphany-core has a repository parent"); + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + verify_release_bundle_lock(repo, &cargo) + .unwrap_or_else(|error| panic!("release bundle lockfile is not frozen: {error:#}")); + } + + #[test] + fn packaged_release_uses_one_deterministic_cargo_graph() { + let required = required_packaged_release_binaries("x86_64-unknown-linux-gnu"); + let command = release_build_command( + std::ffi::OsStr::new("cargo"), + Path::new("Cargo.toml"), + Path::new("target"), + "x86_64-unknown-linux-gnu", + &required, + ) + .expect("release build command"); + let args = command + .get_args() + .map(|arg| arg.to_string_lossy().into_owned()) + .collect::>(); + let envs = command + .get_envs() + .map(|(key, value)| { + ( + key.to_string_lossy().into_owned(), + value.map(|value| value.to_string_lossy().into_owned()), + ) + }) + .collect::>(); + + assert_eq!(envs.get("CARGO_INCREMENTAL"), Some(&Some("0".to_string()))); + assert_eq!(args.iter().filter(|arg| *arg == "--bin").count(), 24); + assert_eq!(args.iter().filter(|arg| *arg == "--package").count(), 1); + assert!( + args.windows(2) + .any(|pair| pair == ["--package", "epiphany-release-bundle"]) + ); + assert!(!args.iter().any(|arg| arg == "epiphany-core")); + assert!( + args.windows(2) + .any(|pair| pair == ["--features", "epiphany-release-bundle/release-runtime"]) + ); + assert!( + args.windows(2) + .any(|pair| pair == ["--bin", "epiphany-mvp-coordinator"]) + ); + assert!( + args.windows(2) + .any(|pair| pair == ["--bin", "epiphany-mvp-status"]) + ); + assert!( + args.windows(2) + .any(|pair| pair == ["--bin", "epiphany-state"]) + ); + assert!(!args.iter().any(|arg| arg == "--bins")); + } + + #[test] + fn resident_cognition_roles_keep_their_owning_manifests() { + let root_manifest = fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("epiphany-core has a repository parent") + .join("Cargo.toml"), + ) + .expect("root release manifest"); + let packaged_roles = required_packaged_release_binaries("x86_64-unknown-linux-gnu") + .into_iter() + .map(|(role, _)| role) + .collect::>(); + for role in [ + "swarm", + "operator-command", + "heartbeat", + "runtime-spine", + "state-steward", + "coordinator", + "hands-action", + "model-runtime", + "persona-service", + "persona-mouth-identity", + "persona-discord-permit", + "persona-discord-permit-identity", + "tool-mcp-runtime", + ] { + assert!(packaged_roles.contains(role), "release omits {role}"); + } + assert_eq!( + required_release_build_target("swarm").unwrap(), + ("epiphany-core", "epiphany-swarm") + ); + assert_eq!( + required_release_build_target("operator-command").unwrap(), + ("epiphany-core", "epiphany-operator-command") + ); + assert_eq!( + required_release_build_target("heartbeat").unwrap(), + ("epiphany-core", "epiphany-heartbeat-store") + ); + assert_eq!( + required_release_build_target("runtime-spine").unwrap(), + ("epiphany-core", "epiphany-runtime-spine") + ); + assert_eq!( + required_release_build_target("state-steward").unwrap(), + ("epiphany-core", "epiphany-state") + ); + assert!(root_manifest.contains("name = \"epiphany-runtime-spine\"")); + assert_eq!( + required_release_build_target("coordinator").unwrap(), + (".", "epiphany-mvp-coordinator") + ); + assert_eq!( + required_release_build_target("hands-action").unwrap(), + ("epiphany-core", "epiphany-hands-action") + ); + assert_eq!( + required_release_build_target("model-runtime").unwrap(), + ("epiphany-openai-runtime", "epiphany-model-runtime") + ); + assert_eq!( + required_release_build_target("persona-service").unwrap(), + ("epiphany-openai-runtime", "epiphany-persona-service") + ); + assert_eq!( + required_release_build_target("persona-mouth-identity").unwrap(), + ("epiphany-core", "epiphany-persona-mouth-identity") + ); + assert_eq!( + required_release_build_target("persona-discord-permit").unwrap(), + ("epiphany-core", "epiphany-persona-discord-permit") + ); + assert_eq!( + required_release_build_target("persona-discord-permit-identity").unwrap(), + ("epiphany-core", "epiphany-persona-discord-permit-identity") + ); + assert_eq!( + required_release_build_target("tool-mcp-runtime").unwrap(), + ("epiphany-tool-mcp-runtime", "epiphany-tool-mcp-runtime") + ); + } + + #[test] + fn root_workspace_owns_first_party_dependency_resolution() { + let core = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let repo = core.parent().expect("epiphany-core repository parent"); + let root_manifest = fs::read_to_string(repo.join("Cargo.toml")).expect("root manifest"); + assert!(root_manifest.contains("[workspace]")); + for member in [ + "epiphany-core", + "epiphany-model-adapter", + "epiphany-openai-adapter", + "epiphany-openai-auth-spine", + "epiphany-openai-codex-spine", + "epiphany-openai-runtime", + "epiphany-self-policy", + "epiphany-state-model", + "epiphany-tool-adapter", + "epiphany-tool-mcp-runtime", + ] { + assert!( + root_manifest.contains(&format!("\"{member}\"")), + "root workspace omits {member}" + ); + assert!( + !repo.join(member).join("Cargo.lock").exists(), + "{member} retains a competing Cargo.lock" + ); + } + } + + #[test] + fn coordinator_policy_is_feature_gated_outside_core_binary_ownership() { + let core = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let repo = core.parent().expect("epiphany-core repository parent"); + let root_manifest = fs::read_to_string(repo.join("Cargo.toml")).expect("root manifest"); + let core_manifest = fs::read_to_string(core.join("Cargo.toml")).expect("core manifest"); + + assert!(root_manifest.contains("coordinator-runtime = [\"dep:epiphany-self-policy\"]")); + assert!(root_manifest.contains( + "release-runtime = [\"coordinator-runtime\", \"openai-runtime\", \"tool-mcp-runtime\"]" + )); + assert!(root_manifest.contains("required-features = [\"coordinator-runtime\"]")); + assert!(core_manifest.contains("autobins = false")); + assert!(!core_manifest.contains("name = \"epiphany-mvp-coordinator\"")); + assert!(!core_manifest.contains("name = \"epiphany-mvp-status\"")); + } + + fn fixture() -> (TempDir, EpiphanyPackagedReleaseEntry) { + let dir = TempDir::new().unwrap(); + let root = dir.path().join("release"); + fs::create_dir(&root).unwrap(); + let mut binaries = Vec::new(); + for (index, (role, name)) in required_packaged_release_binaries("x86_64-unknown-linux-gnu") + .into_iter() + .enumerate() + { + let path = root.join(&name); + fs::write(&path, format!("binary-{index}")).unwrap(); + binaries.push(binary_record(role, &name, &path).unwrap()); + } + binaries.sort_by(|a, b| a.role.cmp(&b.role)); + let id = release_id("runtime", &"a".repeat(40), "target", "rustc", &binaries); + let entry = EpiphanyPackagedReleaseEntry { + schema_version: EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(), + release_id: id, + runtime_id: "runtime".into(), + source_commit_sha: "a".repeat(40), + target_triple: "target".into(), + cargo_profile: "release".into(), + toolchain_fingerprint: "rustc".into(), + created_at_utc: Utc::now().to_rfc3339(), + package_root: canonical_path(root).unwrap().display().to_string(), + binaries, + private_state_exposed: false, + }; + write_epiphany_packaged_release_witness( + &Path::new(&entry.package_root).join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE), + &entry, + ) + .unwrap(); + (dir, entry) + } + + #[test] + fn exact_fixture_verifies() { + let (_d, e) = fixture(); + verify_epiphany_packaged_release_files(&e).unwrap(); + } + #[test] + fn witness_reader_refuses_tamper_and_inspector_refuses_wrong_runtime() { + let (d, e) = fixture(); + let witness = Path::new(&e.package_root).join(EPIPHANY_PACKAGED_RELEASE_WITNESS_FILE); + assert!( + inspect_epiphany_packaged_release_witness( + &witness, + d.path(), + "alien-runtime", + &e.source_commit_sha, + ) + .is_err() + ); + fs::write(&witness, b"hostile witness").unwrap(); + assert!(read_epiphany_packaged_release_witness(&witness).is_err()); + } + #[test] + fn one_byte_replacement_is_rejected() { + let (_d, e) = fixture(); + fs::write(&e.binaries[0].canonical_path, "hostile").unwrap(); + assert!(verify_epiphany_packaged_release_files(&e).is_err()); + } + #[test] + fn extra_sibling_is_rejected() { + let (_d, e) = fixture(); + fs::write(Path::new(&e.package_root).join("stowaway.exe"), "x").unwrap(); + assert!(verify_epiphany_packaged_release_files(&e).is_err()); + } + #[test] + fn swapped_role_is_rejected() { + let (_d, mut e) = fixture(); + let role = e.binaries[0].role.clone(); + e.binaries[0].role = e.binaries[1].role.clone(); + e.binaries[1].role = role; + assert!(validate_epiphany_packaged_release(&e).is_err()); + } + #[test] + fn missing_resident_cognition_binary_is_rejected() { + let (_d, mut e) = fixture(); + e.binaries.retain(|binary| binary.role != "model-runtime"); + assert!(validate_epiphany_packaged_release(&e).is_err()); + } + #[test] + fn substituted_resident_cognition_binary_is_rejected() { + let (_d, mut e) = fixture(); + let model = e + .binaries + .iter_mut() + .find(|binary| binary.role == "model-runtime") + .unwrap(); + model.file_name = "epiphany-openai-runtime".into(); + assert!(validate_epiphany_packaged_release(&e).is_err()); + } + #[test] + fn counterfeit_release_id_is_rejected() { + let (_d, mut e) = fixture(); + e.release_id = format!("sha256-{}", "0".repeat(64)); + assert!(validate_epiphany_packaged_release(&e).is_err()); + } +}