From f3aafba78751c75cb0bb6d81abcdceb168ef97cb Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:15:42 -0500 Subject: [PATCH 01/16] feat(DEN-2923): add frozen offline tool-profile replay --- src/tool_profile.rs | 883 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 883 insertions(+) create mode 100644 src/tool_profile.rs diff --git a/src/tool_profile.rs b/src/tool_profile.rs new file mode 100644 index 00000000..088507a4 --- /dev/null +++ b/src/tool_profile.rs @@ -0,0 +1,883 @@ +//! Frozen, offline replay of exact native tool artifacts into a project-local +//! executable profile. +//! +//! This is deliberately narrower than a backend resolver: it consumes only a +//! validated [`zed_interfaces::EnvironmentLock`], requires already-authenticated +//! archive bytes in Zed's local cache, reuses the existing content-addressed +//! store, and never executes installer scripts, hooks, plugins, package +//! managers, or downloaded binaries. + +use std::collections::{BTreeMap, BTreeSet}; +use std::env; +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; + +use anyhow::{Context, Result, bail, ensure}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use zed_interfaces::{ + EnvironmentLock, EnvironmentLockValidationMode, LockedArtifactFormat, LockedExecutable, + LockedTool, +}; + +use crate::pack::sha256_file; +use crate::project_lock; +use crate::store::Store; + +pub const TOOL_PROFILE_SCHEMA_V1: &str = "zed.tool-profile/v1"; +const DEFAULT_LOCK_PATH: &str = ".zed/environment.lock.toml"; +const DEFAULT_PROFILE_PATH: &str = ".zed/tools"; +const PROFILE_STATE_FILE: &str = "profile.json"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LockMode { + Local, + Portable, +} + +#[derive(Debug, Clone)] +pub struct LoadedEnvironmentLock { + pub relative_path: PathBuf, + pub lock: EnvironmentLock, + pub digest_sha256: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ToolLockSummary { + pub name: String, + pub requirement: String, + pub resolved: String, + pub backend: String, + pub target: String, + pub artifact_sha256: String, + pub artifact_size: u64, + pub executables: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ToolVerifyReceipt { + pub schema: &'static str, + pub lock: String, + pub lock_sha256: String, + pub validation: String, + pub plan_digest_sha256: String, + pub tools: usize, + pub variants: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ToolInstallReceipt { + pub schema: &'static str, + pub action: String, + pub lock: String, + pub lock_sha256: String, + pub target: String, + pub profile: String, + pub bin: String, + pub tools: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ToolProfileState { + schema: String, + lock_sha256: String, + target: String, + tools: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ToolProfileTool { + name: String, + requirement: String, + resolved: String, + backend: String, + artifact_sha256: String, + install_root: String, + executables: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ToolProfileExecutable { + name: String, + source: String, +} + +#[derive(Debug, Clone)] +struct SelectedTool<'a> { + name: &'a str, + locked: &'a LockedTool, +} + +#[derive(Debug, Clone)] +struct PreparedExecutable { + logical_name: String, + profile_name: String, + source: PathBuf, + source_relative: String, +} + +#[derive(Debug, Clone)] +struct PreparedTool<'a> { + selected: SelectedTool<'a>, + executables: Vec, +} + +pub fn default_lock_path() -> PathBuf { + PathBuf::from(DEFAULT_LOCK_PATH) +} + +pub fn default_profile_path() -> PathBuf { + PathBuf::from(DEFAULT_PROFILE_PATH) +} + +pub fn default_zed_home() -> Result { + let configured = env::var_os("ZED_PKG_HOME") + .map(PathBuf::from) + .or_else(|| dirs::home_dir().map(|home| home.join(".zed-pkg"))) + .context("could not determine Zed home; set ZED_PKG_HOME")?; + if configured.is_absolute() { + Ok(configured) + } else { + Ok(env::current_dir()?.join(configured)) + } +} + +pub fn load_environment_lock( + root: &Path, + lock_path: Option<&Path>, + mode: LockMode, + expected_plan_digest: Option<&str>, +) -> Result { + let root = root + .canonicalize() + .with_context(|| format!("canonicalizing project root `{}`", root.display()))?; + ensure!(root.is_dir(), "project root `{}` is not a directory", root.display()); + let relative_path = portable_relative_path( + lock_path.unwrap_or_else(|| Path::new(DEFAULT_LOCK_PATH)), + "environment lock", + false, + )?; + let path = existing_regular_project_file(&root, &relative_path, "environment lock")?; + let bytes = fs::read(&path) + .with_context(|| format!("reading environment lock `{}`", relative_path.display()))?; + let input = std::str::from_utf8(&bytes) + .with_context(|| format!("environment lock `{}` is not UTF-8", relative_path.display()))?; + let lock = if relative_path + .extension() + .is_some_and(|extension| extension.eq_ignore_ascii_case("json")) + { + EnvironmentLock::parse_json(input) + } else { + EnvironmentLock::parse_toml(input) + } + .with_context(|| format!("parsing environment lock `{}`", relative_path.display()))?; + lock.validate(match mode { + LockMode::Local => EnvironmentLockValidationMode::Local, + LockMode::Portable => EnvironmentLockValidationMode::Portable, + })?; + if let Some(expected) = expected_plan_digest { + lock.verify_plan_digest(expected)?; + } + let digest_sha256 = lock.normalized_digest_sha256()?; + Ok(LoadedEnvironmentLock { + relative_path, + lock, + digest_sha256, + }) +} + +pub fn verify_receipt( + loaded: &LoadedEnvironmentLock, + mode: LockMode, +) -> ToolVerifyReceipt { + ToolVerifyReceipt { + schema: TOOL_PROFILE_SCHEMA_V1, + lock: portable_display(&loaded.relative_path), + lock_sha256: loaded.digest_sha256.clone(), + validation: match mode { + LockMode::Local => "local", + LockMode::Portable => "portable", + } + .to_string(), + plan_digest_sha256: loaded.lock.plan_digest_sha256.to_ascii_lowercase(), + tools: loaded.lock.tools.len(), + variants: loaded.lock.tools.values().map(Vec::len).sum(), + } +} + +pub fn list_target( + loaded: &LoadedEnvironmentLock, + target: &str, +) -> Result> { + let selected = select_exact_target(&loaded.lock, target)?; + Ok(selected + .into_iter() + .map(|selected| summary(selected.name, selected.locked)) + .collect()) +} + +pub fn install_offline( + root: &Path, + loaded: &LoadedEnvironmentLock, + target: &str, + profile_path: Option<&Path>, + home: &Path, +) -> Result { + let root = root + .canonicalize() + .with_context(|| format!("canonicalizing project root `{}`", root.display()))?; + loaded + .lock + .validate(EnvironmentLockValidationMode::Portable) + .context("frozen tool installation requires a portable environment lock")?; + let profile_relative = portable_relative_path( + profile_path.unwrap_or_else(|| Path::new(DEFAULT_PROFILE_PATH)), + "tool profile", + false, + )?; + validate_component(target, "target")?; + let selected = select_exact_target(&loaded.lock, target)?; + let store = Store::new(home); + + project_lock::with_lock(&root, "install frozen environment tool profile", || { + install_locked( + &root, + loaded, + target, + &profile_relative, + &store, + &selected, + ) + }) +} + +fn install_locked( + root: &Path, + loaded: &LoadedEnvironmentLock, + target: &str, + profile_relative: &Path, + store: &Store, + selected: &[SelectedTool<'_>], +) -> Result { + let profile_root = ensure_directory_chain(root, profile_relative, "tool profile")?; + let version_root = ensure_real_directory(&profile_root, "v1", "tool profile version")?; + let active = version_root.join(target); + let prepared = prepare_tools(store, selected)?; + let state = profile_state(loaded, target, &prepared); + let mut state_bytes = serde_json::to_vec_pretty(&state)?; + state_bytes.push(b'\n'); + let summaries = prepared + .iter() + .map(|prepared| summary(prepared.selected.name, prepared.selected.locked)) + .collect::>(); + + if active_profile_matches(&active, &state_bytes, &prepared)? { + store.record_project( + &active, + prepared + .iter() + .map(|tool| tool.selected.locked.artifact.sha256.clone()) + .collect(), + )?; + return Ok(install_receipt( + "unchanged", + loaded, + target, + profile_relative, + summaries, + )); + } + + let staging = tempfile::Builder::new() + .prefix(&format!(".{target}.staging-")) + .tempdir_in(&version_root) + .context("creating tool-profile staging directory")?; + let staging_path = staging.path().to_path_buf(); + let staging_bin = staging_path.join("bin"); + fs::create_dir(&staging_bin)?; + for tool in &prepared { + for executable in &tool.executables { + materialize_executable(&executable.source, &staging_bin.join(&executable.profile_name))?; + } + } + let state_path = staging_path.join(PROFILE_STATE_FILE); + write_synced_file(&state_path, &state_bytes)?; + sync_directory(&staging_bin)?; + sync_directory(&staging_path)?; + + let staging_path = staging.keep(); + let backup = version_root.join(format!(".{target}.backup-{}", Uuid::new_v4())); + let had_active = match fs::symlink_metadata(&active) { + Ok(metadata) => { + ensure!( + metadata.is_dir() && !metadata.file_type().is_symlink(), + "active tool profile `{}` must be a real directory", + portable_display(&active) + ); + fs::rename(&active, &backup).with_context(|| { + format!("staging prior tool profile `{}`", portable_display(&active)) + })?; + true + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => false, + Err(error) => return Err(error).context("inspecting active tool profile"), + }; + + if let Err(error) = fs::rename(&staging_path, &active) { + if had_active { + let _ = fs::rename(&backup, &active); + } + let _ = fs::remove_dir_all(&staging_path); + return Err(error).context("activating frozen tool profile"); + } + sync_directory(&version_root)?; + + let refs = prepared + .iter() + .map(|tool| tool.selected.locked.artifact.sha256.clone()) + .collect::>(); + if let Err(error) = store.record_project(&active, refs) { + let _ = fs::remove_dir_all(&active); + if had_active { + let _ = fs::rename(&backup, &active); + } + return Err(error).context("recording live tool-profile store references"); + } + if had_active { + fs::remove_dir_all(&backup).context("removing prior tool profile")?; + } + sync_directory(&version_root)?; + + Ok(install_receipt( + "installed", + loaded, + target, + profile_relative, + summaries, + )) +} + +fn prepare_tools<'a>( + store: &Store, + selected: &[SelectedTool<'a>], +) -> Result>> { + let mut ownership = BTreeMap::::new(); + let mut prepared = Vec::with_capacity(selected.len()); + for selected in selected { + ensure!( + matches!( + selected.locked.artifact.format, + LockedArtifactFormat::TarGz | LockedArtifactFormat::Zip + ), + "tool `{}` uses unsupported offline artifact format {:?}; this slice supports tar_gz and zip", + selected.name, + selected.locked.artifact.format + ); + let archive = store.cached_artifact(&selected.locked.artifact.sha256); + let metadata = fs::symlink_metadata(&archive).with_context(|| { + format!( + "missing cached artifact {} for tool `{}`; prefetch the exact lock before offline install", + selected.locked.artifact.sha256, selected.name + ) + })?; + ensure!( + metadata.is_file() && !metadata.file_type().is_symlink(), + "cached artifact for tool `{}` must be a regular file", + selected.name + ); + ensure!( + metadata.len() == selected.locked.artifact.size, + "cached artifact size mismatch for tool `{}`: lock={}, cache={}", + selected.name, + selected.locked.artifact.size, + metadata.len() + ); + let (actual_sha256, actual_size) = sha256_file(&archive)?; + ensure!( + actual_sha256 == selected.locked.artifact.sha256, + "cached artifact hash mismatch for tool `{}`: expected {}, got {}", + selected.name, + selected.locked.artifact.sha256, + actual_sha256 + ); + ensure!( + actual_size == selected.locked.artifact.size, + "cached artifact size changed while verifying tool `{}`", + selected.name + ); + let package_root = store.add_artifact(&archive, &selected.locked.artifact.sha256)?; + let install_root = resolve_directory_beneath( + &package_root, + Path::new(&selected.locked.install.root), + &format!("tool `{}` install root", selected.name), + )?; + let mut executables = Vec::new(); + for executable in &selected.locked.install.executables { + prepare_locked_executable( + selected.name, + executable, + &install_root, + &mut ownership, + &mut executables, + )?; + } + ensure!( + !executables.is_empty(), + "tool `{}` exposes no executables in the locked profile", + selected.name + ); + prepared.push(PreparedTool { + selected: selected.clone(), + executables, + }); + } + Ok(prepared) +} + +fn prepare_locked_executable( + tool: &str, + executable: &LockedExecutable, + install_root: &Path, + ownership: &mut BTreeMap, + output: &mut Vec, +) -> Result<()> { + let source_relative = portable_relative_path( + Path::new(&executable.path), + &format!("tool `{tool}` executable `{}`", executable.name), + false, + )?; + let source = resolve_regular_file_beneath( + install_root, + &source_relative, + &format!("tool `{tool}` executable `{}`", executable.name), + )?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + ensure!( + source.metadata()?.permissions().mode() & 0o111 != 0, + "tool `{tool}` executable `{}` is not marked executable", + executable.name + ); + } + for logical_name in std::iter::once(&executable.name).chain(executable.aliases.iter()) { + validate_component(logical_name, "executable name")?; + let profile_name = profile_executable_name(logical_name, &source)?; + let key = profile_name.to_ascii_lowercase(); + if let Some(existing) = ownership.insert(key, tool.to_string()) { + bail!( + "executable `{profile_name}` is claimed by both tool `{existing}` and tool `{tool}`" + ); + } + output.push(PreparedExecutable { + logical_name: logical_name.clone(), + profile_name, + source: source.clone(), + source_relative: portable_display(&source_relative), + }); + } + output.sort_by(|left, right| left.profile_name.cmp(&right.profile_name)); + Ok(()) +} + +fn profile_state( + loaded: &LoadedEnvironmentLock, + target: &str, + prepared: &[PreparedTool<'_>], +) -> ToolProfileState { + ToolProfileState { + schema: TOOL_PROFILE_SCHEMA_V1.to_string(), + lock_sha256: loaded.digest_sha256.clone(), + target: target.to_string(), + tools: prepared + .iter() + .map(|tool| ToolProfileTool { + name: tool.selected.name.to_string(), + requirement: tool.selected.locked.requirement.clone(), + resolved: tool.selected.locked.resolved.clone(), + backend: tool.selected.locked.backend.clone(), + artifact_sha256: tool.selected.locked.artifact.sha256.clone(), + install_root: tool.selected.locked.install.root.clone(), + executables: tool + .executables + .iter() + .map(|executable| ToolProfileExecutable { + name: executable.profile_name.clone(), + source: executable.source_relative.clone(), + }) + .collect(), + }) + .collect(), + } +} + +fn active_profile_matches( + active: &Path, + expected_state: &[u8], + prepared: &[PreparedTool<'_>], +) -> Result { + let metadata = match fs::symlink_metadata(active) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(error) => return Err(error).context("inspecting active tool profile"), + }; + ensure!( + metadata.is_dir() && !metadata.file_type().is_symlink(), + "active tool profile `{}` must be a real directory", + portable_display(active) + ); + let state = active.join(PROFILE_STATE_FILE); + let state_metadata = match fs::symlink_metadata(&state) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(error) => return Err(error).context("inspecting tool profile state"), + }; + if !state_metadata.is_file() || state_metadata.file_type().is_symlink() { + return Ok(false); + } + if fs::read(&state)? != expected_state { + return Ok(false); + } + let bin = active.join("bin"); + let bin_metadata = match fs::symlink_metadata(&bin) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(error) => return Err(error).context("inspecting tool profile bin directory"), + }; + if !bin_metadata.is_dir() || bin_metadata.file_type().is_symlink() { + return Ok(false); + } + let expected = prepared + .iter() + .flat_map(|tool| tool.executables.iter().map(|executable| executable.profile_name.clone())) + .collect::>(); + let actual = fs::read_dir(&bin)? + .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned())) + .collect::>>()?; + Ok(expected == actual) +} + +fn install_receipt( + action: &str, + loaded: &LoadedEnvironmentLock, + target: &str, + profile_relative: &Path, + tools: Vec, +) -> ToolInstallReceipt { + let active_relative = profile_relative.join("v1").join(target); + ToolInstallReceipt { + schema: TOOL_PROFILE_SCHEMA_V1, + action: action.to_string(), + lock: portable_display(&loaded.relative_path), + lock_sha256: loaded.digest_sha256.clone(), + target: target.to_string(), + profile: portable_display(&active_relative), + bin: portable_display(&active_relative.join("bin")), + tools, + } +} + +fn summary(name: &str, locked: &LockedTool) -> ToolLockSummary { + let mut executables = locked + .install + .executables + .iter() + .flat_map(|executable| { + std::iter::once(executable.name.clone()).chain(executable.aliases.iter().cloned()) + }) + .collect::>(); + executables.sort(); + ToolLockSummary { + name: name.to_string(), + requirement: locked.requirement.clone(), + resolved: locked.resolved.clone(), + backend: locked.backend.clone(), + target: locked.platform.target.clone(), + artifact_sha256: locked.artifact.sha256.clone(), + artifact_size: locked.artifact.size, + executables, + } +} + +fn select_exact_target<'a>( + lock: &'a EnvironmentLock, + target: &str, +) -> Result>> { + validate_component(target, "target")?; + let mut selected = Vec::with_capacity(lock.tools.len()); + for (name, variants) in &lock.tools { + let matching = variants + .iter() + .filter(|variant| variant.platform.target == target) + .collect::>(); + ensure!( + !matching.is_empty(), + "tool `{name}` has no locked variant for target `{target}`" + ); + ensure!( + matching.len() == 1, + "tool `{name}` has {} locked variants for target `{target}`; multi-version activation is not yet certified", + matching.len() + ); + selected.push(SelectedTool { + name, + locked: matching[0], + }); + } + Ok(selected) +} + +fn materialize_executable(source: &Path, destination: &Path) -> Result<()> { + ensure!( + fs::symlink_metadata(destination) + .is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound), + "tool profile destination `{}` already exists", + portable_display(destination) + ); + #[cfg(unix)] + { + std::os::unix::fs::symlink(source, destination).with_context(|| { + format!( + "linking tool executable `{}` -> `{}`", + portable_display(destination), + portable_display(source) + ) + })?; + } + #[cfg(windows)] + { + fs::copy(source, destination).with_context(|| { + format!( + "copying tool executable `{}` -> `{}`", + portable_display(source), + portable_display(destination) + ) + })?; + File::open(destination)?.sync_all()?; + } + Ok(()) +} + +fn profile_executable_name(logical_name: &str, source: &Path) -> Result { + #[cfg(windows)] + { + if Path::new(logical_name).extension().is_none() + && let Some(extension) = source.extension().and_then(|value| value.to_str()) + && matches!(extension.to_ascii_lowercase().as_str(), "exe" | "com" | "cmd" | "bat") + { + return Ok(format!("{logical_name}.{extension}")); + } + } + Ok(logical_name.to_string()) +} + +fn resolve_directory_beneath(base: &Path, relative: &Path, field: &str) -> Result { + if relative == Path::new(".") { + return base + .canonicalize() + .with_context(|| format!("canonicalizing {field}")); + } + let relative = portable_relative_path(relative, field, false)?; + let candidate = walk_real_components(base, &relative, field)?; + ensure!(candidate.is_dir(), "{field} must be a directory"); + Ok(candidate) +} + +fn resolve_regular_file_beneath(base: &Path, relative: &Path, field: &str) -> Result { + let relative = portable_relative_path(relative, field, false)?; + let candidate = walk_real_components(base, &relative, field)?; + let metadata = fs::symlink_metadata(&candidate)?; + ensure!( + metadata.is_file() && !metadata.file_type().is_symlink(), + "{field} must be a regular file" + ); + Ok(candidate) +} + +fn walk_real_components(base: &Path, relative: &Path, field: &str) -> Result { + let canonical_base = base + .canonicalize() + .with_context(|| format!("canonicalizing {field} base"))?; + let mut current = canonical_base.clone(); + for component in relative.components() { + let Component::Normal(component) = component else { + bail!("{field} must be a portable relative path"); + }; + current.push(component); + let metadata = fs::symlink_metadata(¤t) + .with_context(|| format!("inspecting {field} `{}`", portable_display(relative)))?; + ensure!( + !metadata.file_type().is_symlink(), + "{field} must not traverse a symlink" + ); + } + let canonical = current + .canonicalize() + .with_context(|| format!("canonicalizing {field}"))?; + ensure!( + canonical.starts_with(&canonical_base), + "{field} escapes its authenticated artifact root" + ); + Ok(canonical) +} + +fn existing_regular_project_file(root: &Path, relative: &Path, field: &str) -> Result { + let candidate = walk_real_components(root, relative, field)?; + let metadata = fs::symlink_metadata(&candidate)?; + ensure!( + metadata.is_file() && !metadata.file_type().is_symlink(), + "{field} `{}` must be a regular file", + portable_display(relative) + ); + Ok(candidate) +} + +fn ensure_directory_chain(root: &Path, relative: &Path, field: &str) -> Result { + let relative = portable_relative_path(relative, field, false)?; + let mut current = root.to_path_buf(); + for component in relative.components() { + let Component::Normal(component) = component else { + bail!("{field} must be project-relative"); + }; + current.push(component); + match fs::symlink_metadata(¤t) { + Ok(metadata) => ensure!( + metadata.is_dir() && !metadata.file_type().is_symlink(), + "{field} component `{}` must be a real directory", + portable_display(¤t) + ), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + fs::create_dir(¤t).with_context(|| { + format!("creating {field} directory `{}`", portable_display(¤t)) + })?; + } + Err(error) => return Err(error).with_context(|| format!("inspecting {field}")), + } + } + Ok(current) +} + +fn ensure_real_directory(parent: &Path, name: &str, field: &str) -> Result { + validate_component(name, field)?; + let path = parent.join(name); + match fs::symlink_metadata(&path) { + Ok(metadata) => ensure!( + metadata.is_dir() && !metadata.file_type().is_symlink(), + "{field} `{}` must be a real directory", + portable_display(&path) + ), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => fs::create_dir(&path)?, + Err(error) => return Err(error).with_context(|| format!("inspecting {field}")), + } + Ok(path) +} + +fn portable_relative_path(path: &Path, field: &str, allow_dot: bool) -> Result { + ensure!(!path.as_os_str().is_empty(), "{field} path cannot be empty"); + ensure!(!path.is_absolute(), "{field} must be project-relative"); + let text = path.to_string_lossy(); + let windows_drive = text.as_bytes().get(1).is_some_and(|byte| *byte == b':') + && text.as_bytes().first().is_some_and(u8::is_ascii_alphabetic); + ensure!( + !windows_drive + && !text.starts_with('~') + && !text.starts_with("$HOME") + && !text.starts_with("${HOME}") + && !text.starts_with("%USERPROFILE%") + && !text.starts_with("//") + && !text.starts_with("\\\\"), + "{field} must be portable and project-relative" + ); + let mut output = PathBuf::new(); + for component in path.components() { + match component { + Component::Normal(component) => output.push(component), + Component::CurDir if allow_dot => {} + Component::CurDir => {} + Component::ParentDir | Component::RootDir | Component::Prefix(_) => { + bail!("{field} cannot escape the project root") + } + } + } + ensure!( + allow_dot || !output.as_os_str().is_empty(), + "{field} cannot be the project root" + ); + Ok(output) +} + +fn validate_component(value: &str, field: &str) -> Result<()> { + ensure!(!value.trim().is_empty(), "{field} cannot be empty"); + ensure!(value == value.trim(), "{field} cannot contain surrounding whitespace"); + ensure!(value != "." && value != "..", "{field} is invalid"); + ensure!( + value + .chars() + .all(|character| character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-')), + "{field} `{value}` contains unsupported characters" + ); + ensure!(value.len() <= 128, "{field} exceeds 128 bytes"); + Ok(()) +} + +fn write_synced_file(path: &Path, bytes: &[u8]) -> Result<()> { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .with_context(|| format!("creating `{}`", portable_display(path)))?; + file.write_all(bytes)?; + file.sync_all()?; + Ok(()) +} + +#[cfg(unix)] +fn sync_directory(path: &Path) -> Result<()> { + File::open(path)?.sync_all()?; + Ok(()) +} + +#[cfg(not(unix))] +fn sync_directory(_path: &Path) -> Result<()> { + Ok(()) +} + +fn portable_display(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn target_and_profile_paths_are_strict() { + assert!(validate_component("x86_64-unknown-linux-gnu", "target").is_ok()); + assert!(validate_component("../linux", "target").is_err()); + assert!(portable_relative_path(Path::new(".zed/tools"), "profile", false).is_ok()); + assert!(portable_relative_path(Path::new("../tools"), "profile", false).is_err()); + assert!(portable_relative_path(Path::new(r"C:\\tools"), "profile", false).is_err()); + } + + #[test] + fn exact_target_selection_rejects_missing_and_multiple_variants() { + let mut lock = EnvironmentLock::default(); + lock.plan_digest_sha256 = "a".repeat(64); + assert_eq!(select_exact_target(&lock, "x86_64-linux").unwrap().len(), 0); + } + + #[test] + fn profile_state_is_secret_free_by_shape() { + let state = ToolProfileState { + schema: TOOL_PROFILE_SCHEMA_V1.to_string(), + lock_sha256: "a".repeat(64), + target: "x86_64-linux".to_string(), + tools: Vec::new(), + }; + let json = serde_json::to_string(&state).unwrap(); + for forbidden in ["locator", "url", "token", "credential", "environment"] { + assert!(!json.contains(forbidden)); + } + } +} From db20663771a2e91c5f909770bf4b9af3e52e0cd5 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:16:22 -0500 Subject: [PATCH 02/16] feat(DEN-2923): add staged frozen tool-profile CLI --- src/bin/zed-tool.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/bin/zed-tool.rs diff --git a/src/bin/zed-tool.rs b/src/bin/zed-tool.rs new file mode 100644 index 00000000..fab1b30c --- /dev/null +++ b/src/bin/zed-tool.rs @@ -0,0 +1,176 @@ +use std::path::PathBuf; + +use anyhow::{Result, ensure}; +use clap::{Parser, Subcommand}; +use zed_cli::tool_profile::{ + LockMode, default_lock_path, default_profile_path, default_zed_home, install_offline, + list_target, load_environment_lock, verify_receipt, +}; + +#[derive(Debug, Parser)] +#[command( + name = "zed-tool", + version, + about = "Verify and replay exact EnvironmentLock tools without a manager or network" +)] +struct Cli { + /// Project-local EnvironmentLock TOML or JSON. + #[arg(long, env = "ZED_TOOL_LOCK", default_value_os_t = default_lock_path())] + lock: PathBuf, + + /// Emit one stable JSON document. + #[arg(long, env = "ZED_TOOL_JSON")] + json: bool, + + #[command(subcommand)] + command: ToolCommand, +} + +#[derive(Debug, Subcommand)] +enum ToolCommand { + /// Validate exact lock structure, portability, and optional plan identity. + Verify { + /// Require a machine-independent portable lock. + #[arg(long, env = "ZED_TOOL_PORTABLE")] + portable: bool, + + /// Require the lock to belong to this normalized EnvironmentPlan SHA-256. + #[arg(long, env = "ZED_TOOL_PLAN_DIGEST")] + plan_digest: Option, + }, + + /// List the exact tool variants selected for one target. + List { + /// Exact locked target identity, such as x86_64-unknown-linux-gnu. + #[arg(long, env = "ZED_TOOL_TARGET")] + target: String, + }, + + /// Replay exact cached artifacts into a project-local executable profile. + Install { + /// Exact locked target identity, such as x86_64-unknown-linux-gnu. + #[arg(long, env = "ZED_TOOL_TARGET")] + target: String, + + /// Installation is intentionally cache-only in this first slice. + #[arg(long, env = "ZED_TOOL_OFFLINE")] + offline: bool, + + /// Project-local profile root. + #[arg(long, env = "ZED_TOOL_PROFILE", default_value_os_t = default_profile_path())] + profile: PathBuf, + + /// Zed content-addressed store and cache root. + #[arg(long, env = "ZED_PKG_HOME")] + home: Option, + }, +} + +fn main() { + if let Err(error) = run(Cli::parse()) { + eprintln!("error: {error:#}"); + std::process::exit(1); + } +} + +fn run(cli: Cli) -> Result<()> { + let root = std::env::current_dir()?; + match cli.command { + ToolCommand::Verify { + portable, + plan_digest, + } => { + let mode = if portable { + LockMode::Portable + } else { + LockMode::Local + }; + let loaded = load_environment_lock( + &root, + Some(&cli.lock), + mode, + plan_digest.as_deref(), + )?; + let receipt = verify_receipt(&loaded, mode); + if cli.json { + println!("{}", serde_json::to_string_pretty(&receipt)?); + } else { + println!("verified {}", receipt.lock); + println!("lock-sha256: {}", receipt.lock_sha256); + println!("plan-sha256: {}", receipt.plan_digest_sha256); + println!("validation: {}", receipt.validation); + println!("tools: {}", receipt.tools); + println!("variants: {}", receipt.variants); + } + } + ToolCommand::List { target } => { + let loaded = load_environment_lock( + &root, + Some(&cli.lock), + LockMode::Portable, + None, + )?; + let tools = list_target(&loaded, &target)?; + if cli.json { + println!("{}", serde_json::to_string_pretty(&tools)?); + } else { + for tool in tools { + println!( + "{} {} ({}, {}) [{}]", + tool.name, + tool.resolved, + tool.backend, + tool.target, + tool.executables.join(", ") + ); + } + } + } + ToolCommand::Install { + target, + offline, + profile, + home, + } => { + ensure!( + offline, + "native tool installation currently requires `--offline`; version discovery and downloads are not yet certified" + ); + let loaded = load_environment_lock( + &root, + Some(&cli.lock), + LockMode::Portable, + None, + )?; + let home = match home { + Some(home) if home.is_absolute() => home, + Some(home) => root.join(home), + None => default_zed_home()?, + }; + let receipt = install_offline( + &root, + &loaded, + &target, + Some(&profile), + &home, + )?; + if cli.json { + println!("{}", serde_json::to_string_pretty(&receipt)?); + } else { + println!("{} tool profile {}", receipt.action, receipt.profile); + println!("bin: {}", receipt.bin); + println!("lock-sha256: {}", receipt.lock_sha256); + for tool in receipt.tools { + println!( + "{} {} ({}) [{}]", + tool.name, + tool.resolved, + tool.backend, + tool.executables.join(", ") + ); + } + } + } + } + Ok(()) +} From f69466480bab41071b448f227f43a3c987bb2950 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:16:43 -0500 Subject: [PATCH 03/16] feat(DEN-2923): export frozen tool-profile replay --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 310052d7..dfa34c7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,7 @@ pub mod release; pub mod store; pub mod task_runtime; pub mod terminal_context; +pub mod tool_profile; pub mod transaction; pub mod update; pub mod vcs; From 825880236ad0c73679adf096635a9b8b44d96f15 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:17:37 -0500 Subject: [PATCH 04/16] test(DEN-2923): add frozen offline tool-profile black-box coverage --- tests/tool_profile_cli.rs | 306 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 tests/tool_profile_cli.rs diff --git a/tests/tool_profile_cli.rs b/tests/tool_profile_cli.rs new file mode 100644 index 00000000..7a543c94 --- /dev/null +++ b/tests/tool_profile_cli.rs @@ -0,0 +1,306 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use flate2::{Compression, GzBuilder}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use zed_interfaces::{ + ENVIRONMENT_LOCK_SCHEMA_VERSION, EnvironmentLock, LockedArtifact, LockedArtifactFormat, + LockedExecutable, LockedInstall, LockedPlatform, LockedSource, LockedSourceKind, LockedTool, +}; + +fn binary() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_zed-tool")) +} + +fn target() -> &'static str { + if cfg!(windows) { + "x86_64-pc-windows-msvc" + } else if cfg!(target_os = "macos") { + "aarch64-apple-darwin" + } else { + "x86_64-unknown-linux-gnu" + } +} + +fn executable_source() -> &'static str { + if cfg!(windows) { + "bin/hello.cmd" + } else { + "bin/hello" + } +} + +fn executable_body() -> &'static [u8] { + if cfg!(windows) { + b"@echo hello\r\n" + } else { + b"#!/bin/sh\nprintf 'hello\\n'\n" + } +} + +fn archive(path: &Path, source: &str, body: &[u8]) { + let file = fs::File::create(path).unwrap(); + let encoder = GzBuilder::new() + .mtime(0) + .write(file, Compression::default()); + let mut builder = tar::Builder::new(encoder); + let mut header = tar::Header::new_gnu(); + header.set_size(body.len() as u64); + header.set_mtime(0); + header.set_uid(0); + header.set_gid(0); + header.set_mode(0o755); + header.set_cksum(); + builder + .append_data(&mut header, format!("pkg/{source}"), body) + .unwrap(); + let encoder = builder.into_inner().unwrap(); + encoder.finish().unwrap(); +} + +fn digest(path: &Path) -> (String, u64) { + let bytes = fs::read(path).unwrap(); + (hex::encode(Sha256::digest(&bytes)), bytes.len() as u64) +} + +fn locked_tool(name: &str, sha256: &str, size: u64, executable_name: &str) -> LockedTool { + LockedTool { + requirement: "1".to_string(), + resolved: "1.0.0".to_string(), + backend: "http".to_string(), + backend_version: Some("1.0.0".to_string()), + backend_options_digest_sha256: None, + source: LockedSource { + kind: LockedSourceKind::Http, + locator: format!("https://example.invalid/{name}-1.0.0.tar.gz"), + revision: None, + tree_sha256: None, + immutable: false, + portable: false, + extensions: BTreeMap::new(), + }, + artifact: LockedArtifact { + sha256: sha256.to_string(), + size, + format: LockedArtifactFormat::TarGz, + mirrors: Vec::new(), + signatures: Vec::new(), + extensions: BTreeMap::new(), + }, + platform: LockedPlatform { + target: target().to_string(), + os: Some(std::env::consts::OS.to_string()), + arch: Some(std::env::consts::ARCH.to_string()), + libc: None, + abi: None, + }, + install: LockedInstall { + root: ".".to_string(), + bin_dirs: vec!["bin".to_string()], + executables: vec![LockedExecutable { + name: executable_name.to_string(), + path: executable_source().to_string(), + aliases: vec![format!("{executable_name}-alias")], + }], + layout_digest_sha256: None, + extensions: BTreeMap::new(), + }, + extensions: BTreeMap::new(), + } +} + +fn fixture(tools: &[(&str, &str)]) -> (tempfile::TempDir, PathBuf, PathBuf, String) { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("project"); + let home = temp.path().join("home"); + fs::create_dir_all(project.join(".zed")).unwrap(); + fs::create_dir_all(home.join("cache")).unwrap(); + + let draft = temp.path().join("artifact.tar.gz"); + archive(&draft, executable_source(), executable_body()); + let (sha256, size) = digest(&draft); + fs::rename(&draft, home.join("cache").join(format!("{sha256}.tar.gz"))).unwrap(); + + let mut lock = EnvironmentLock { + schema_version: ENVIRONMENT_LOCK_SCHEMA_VERSION, + plan_digest_sha256: "a".repeat(64), + tools: BTreeMap::new(), + extensions: BTreeMap::new(), + }; + for (tool, executable) in tools { + lock.tools.insert( + (*tool).to_string(), + vec![locked_tool(tool, &sha256, size, executable)], + ); + } + fs::write( + project.join(".zed/environment.lock.toml"), + lock.to_toml_string().unwrap(), + ) + .unwrap(); + (temp, project, home, sha256) +} + +fn run(project: &Path, home: &Path, args: &[&str]) -> Output { + Command::new(binary()) + .current_dir(project) + .args(args) + .env("ZED_PKG_HOME", home) + .env_remove("ZED_TOOL_LOCK") + .env_remove("ZED_TOOL_JSON") + .env_remove("ZED_TOOL_PORTABLE") + .env_remove("ZED_TOOL_PLAN_DIGEST") + .env_remove("ZED_TOOL_TARGET") + .env_remove("ZED_TOOL_OFFLINE") + .env_remove("ZED_TOOL_PROFILE") + .output() + .unwrap() +} + +fn assert_success(output: &Output) { + assert!( + output.status.success(), + "stdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn verify_list_install_and_idempotent_replay_are_deterministic() { + let (_temp, project, home, sha256) = fixture(&[("hello", "hello")]); + + let verify = run( + &project, + &home, + &[ + "--json", + "verify", + "--portable", + "--plan-digest", + &"a".repeat(64), + ], + ); + assert_success(&verify); + let verify: Value = serde_json::from_slice(&verify.stdout).unwrap(); + assert_eq!(verify["tools"], 1); + assert_eq!(verify["validation"], "portable"); + + let list = run(&project, &home, &["--json", "list", "--target", target()]); + assert_success(&list); + let list: Value = serde_json::from_slice(&list.stdout).unwrap(); + assert_eq!(list[0]["name"], "hello"); + assert_eq!(list[0]["artifact_sha256"], sha256); + + let first = run( + &project, + &home, + &["--json", "install", "--target", target(), "--offline"], + ); + assert_success(&first); + let first: Value = serde_json::from_slice(&first.stdout).unwrap(); + assert_eq!(first["action"], "installed"); + let state = project + .join(".zed/tools/v1") + .join(target()) + .join("profile.json"); + let state_before = fs::read(&state).unwrap(); + + let second = run( + &project, + &home, + &["--json", "install", "--target", target(), "--offline"], + ); + assert_success(&second); + let second: Value = serde_json::from_slice(&second.stdout).unwrap(); + assert_eq!(second["action"], "unchanged"); + assert_eq!(fs::read(&state).unwrap(), state_before); + + let bin = project.join(second["bin"].as_str().unwrap()); + if cfg!(windows) { + assert!(bin.join("hello.cmd").is_file()); + assert!(bin.join("hello-alias.cmd").is_file()); + } else { + let output = Command::new(bin.join("hello")).output().unwrap(); + assert_success(&output); + assert_eq!(output.stdout, b"hello\n"); + assert!(bin.join("hello-alias").exists()); + } +} + +#[test] +fn missing_and_tampered_cache_fail_before_profile_mutation() { + let (_temp, project, home, sha256) = fixture(&[("hello", "hello")]); + let cache = home.join("cache").join(format!("{sha256}.tar.gz")); + let original = fs::read(&cache).unwrap(); + + fs::remove_file(&cache).unwrap(); + let missing = run( + &project, + &home, + &["install", "--target", target(), "--offline"], + ); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("prefetch")); + assert!(!project.join(".zed/tools/v1").join(target()).exists()); + + fs::write(&cache, &original).unwrap(); + let mut tampered = original; + let last = tampered.len() - 1; + tampered[last] ^= 1; + fs::write(&cache, tampered).unwrap(); + let mismatch = run( + &project, + &home, + &["install", "--target", target(), "--offline"], + ); + assert!(!mismatch.status.success()); + assert!(String::from_utf8_lossy(&mismatch.stderr).contains("hash mismatch")); + assert!(!project.join(".zed/tools/v1").join(target()).exists()); +} + +#[test] +fn executable_collisions_and_online_mode_fail_closed() { + let (_temp, project, home, _sha256) = fixture(&[("one", "shared"), ("two", "shared")]); + let collision = run( + &project, + &home, + &["install", "--target", target(), "--offline"], + ); + assert!(!collision.status.success()); + assert!(String::from_utf8_lossy(&collision.stderr).contains("claimed by both")); + assert!(!project.join(".zed/tools/v1").join(target()).exists()); + + let online = run(&project, &home, &["install", "--target", target()]); + assert!(!online.status.success()); + assert!(String::from_utf8_lossy(&online.stderr).contains("requires `--offline`")); +} + +#[test] +fn wrong_plan_digest_and_target_fail_without_mutation() { + let (_temp, project, home, _sha256) = fixture(&[("hello", "hello")]); + let digest = run( + &project, + &home, + &[ + "verify", + "--portable", + "--plan-digest", + &"b".repeat(64), + ], + ); + assert!(!digest.status.success()); + assert!(String::from_utf8_lossy(&digest.stderr).contains("plan digest")); + + let target = run( + &project, + &home, + &["install", "--target", "../host", "--offline"], + ); + assert!(!target.status.success()); + assert!(String::from_utf8_lossy(&target.stderr).contains("unsupported characters")); + assert!(!project.join(".zed/tools/v1").exists()); +} From f37111f130c051fa5c27ae01b51ec8d4fd91d343 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:17:54 -0500 Subject: [PATCH 05/16] feat(DEN-2923): define frozen tool-profile flags contract --- .tool-cli-flags.toml | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .tool-cli-flags.toml diff --git a/.tool-cli-flags.toml b/.tool-cli-flags.toml new file mode 100644 index 00000000..e49acd6b --- /dev/null +++ b/.tool-cli-flags.toml @@ -0,0 +1,79 @@ +# Dedicated flags-to-environment contract for the staged frozen tool-profile +# CLI. Canonical `zed tool` routing must reuse these exact variable names. + +[parse] +command_env = "ZED_TOOL_COMMAND" +unknown_options_env = "ZED_TOOL_UNKNOWN_OPTIONS" +errors_env = "ZED_TOOL_PARSE_ERRORS" +allow_unknown = false + +[help] +url = "https://github.com/zed-pkg/zed-cli/blob/main/docs/tool-profile.md" + +[flags.lock] +env = "ZED_TOOL_LOCK" +aliases = ["lock"] +type = "string" +default = ".zed/environment.lock.toml" +help = "Project-local EnvironmentLock TOML or JSON." + +[flags.json] +env = "ZED_TOOL_JSON" +aliases = ["json"] +type = "bool" +default = "false" +help = "Emit one stable JSON document." + +[commands.verify] +help = "Validate exact lock structure, portability, and optional plan identity." + +[commands.verify.flags.portable] +env = "ZED_TOOL_PORTABLE" +aliases = ["portable"] +type = "bool" +default = "false" +help = "Require a machine-independent portable lock." + +[commands.verify.flags.plan_digest] +env = "ZED_TOOL_PLAN_DIGEST" +aliases = ["plan-digest"] +type = "string" +help = "Require one normalized EnvironmentPlan SHA-256." + +[commands.list] +help = "List exact tool variants selected for one target." + +[commands.list.flags.target] +env = "ZED_TOOL_TARGET" +aliases = ["target"] +type = "string" +help = "Exact locked target identity." + +[commands.install] +help = "Replay exact cached artifacts into a project-local executable profile." + +[commands.install.flags.target] +env = "ZED_TOOL_TARGET" +aliases = ["target"] +type = "string" +help = "Exact locked target identity." + +[commands.install.flags.offline] +env = "ZED_TOOL_OFFLINE" +aliases = ["offline"] +type = "bool" +default = "false" +help = "Require cache-only installation; mandatory for this staged slice." + +[commands.install.flags.profile] +env = "ZED_TOOL_PROFILE" +aliases = ["profile"] +type = "string" +default = ".zed/tools" +help = "Project-local tool profile root." + +[commands.install.flags.home] +env = "ZED_PKG_HOME" +aliases = ["home"] +type = "string" +help = "Zed content-addressed store and cache root." From 16f4936b99f47ebc0584ad301ce540dfa9143626 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:18:22 -0500 Subject: [PATCH 06/16] docs(DEN-2923): document frozen offline tool profiles --- docs/tool-profile.md | 102 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 docs/tool-profile.md diff --git a/docs/tool-profile.md b/docs/tool-profile.md new file mode 100644 index 00000000..d9474138 --- /dev/null +++ b/docs/tool-profile.md @@ -0,0 +1,102 @@ +# Frozen offline tool profiles + +`zed-tool` is the staged native-tool replay surface for exact +`EnvironmentLock` files. It does not resolve versions, contact a registry, +execute a manager or plugin, run an install script, or modify a global PATH. + +The first slice proves that manager-neutral locked artifacts can be verified, +extracted through Zed's existing content-addressed store, and activated in one +project-local profile without requiring mise or another environment manager. + +## Commands + +```console +zed-tool [--lock .zed/environment.lock.toml] [--json] verify [--portable] [--plan-digest SHA] +zed-tool [--lock .zed/environment.lock.toml] [--json] list --target TARGET +zed-tool [--lock .zed/environment.lock.toml] [--json] install --target TARGET --offline [--profile .zed/tools] [--home PATH] +``` + +The dedicated flags contract is `.tool-cli-flags.toml`: + +- `ZED_TOOL_LOCK` +- `ZED_TOOL_JSON` +- `ZED_TOOL_PORTABLE` +- `ZED_TOOL_PLAN_DIGEST` +- `ZED_TOOL_TARGET` +- `ZED_TOOL_OFFLINE` +- `ZED_TOOL_PROFILE` +- `ZED_PKG_HOME` + +A future canonical `zed tool` route must reuse these names. + +## Frozen replay model + +Installation performs these steps: + +1. Canonicalize the current project and read one project-relative lock without + following a symlink outside the checkout. +2. Parse TOML or JSON through `zed_interfaces::EnvironmentLock` and require + portable validation. +3. Select exactly one locked variant per logical tool for the requested target. + Multiple active versions are rejected until precedence is explicitly + modeled and certified. +4. Reject every artifact format except `tar_gz` and `zip` in this first slice. +5. Find the authenticated bytes at + `$ZED_PKG_HOME/cache/.tar.gz`; the extension is the historical cache + convention and the extractor still identifies gzip or ZIP by magic bytes. +6. Require the cached file's exact locked size and SHA-256, then pass it through + Zed's existing per-digest locked, traversal-safe, atomic store extraction. +7. Resolve every locked executable below the authenticated install root, + rejecting missing files, symlink traversal, non-regular files, and missing + Unix execute bits. +8. Reject duplicate executable or alias ownership. Collision checks are ASCII + case-insensitive so a profile remains safe on Windows. +9. Stage a complete profile under `.zed/tools/v1/`, fsync its state, + and atomically replace the previous owned profile while holding the + checkout-local operation lock. +10. Record the active profile's artifact digests as a distinct live store + reference, so package installs and tool profiles do not overwrite each + other's GC ownership. + +On Unix, profile commands are symlinks to immutable store files. On Windows, +they are exact copied executable/script files. A logical command without an +extension inherits a locked `.exe`, `.com`, `.cmd`, or `.bat` extension on +Windows. + +## Profile state + +The deterministic `profile.json` records only: + +- schema; +- normalized environment-lock SHA-256; +- target; +- logical tool name, original requirement, exact version, and backend; +- artifact SHA-256; +- relative install root; and +- exposed command names and relative locked source paths. + +It contains no source URL, credential, environment value, timestamp, hostname, +process identifier, user-home path, or absolute store path. Replaying the same +lock and target against an intact profile reports `unchanged` and does not +rewrite profile state. + +## Fail-closed boundaries + +The staged installer rejects: + +- online mode; +- missing cached bytes; +- wrong artifact size or SHA-256; +- unsupported archive formats; +- missing target variants or multiple variants for one logical tool/target; +- executable and alias collisions; +- unsafe project, lock, profile, target, install-root, or executable paths; +- symlinked profile parents or authenticated artifact paths; +- non-regular or non-executable locked commands; +- lock/plan digest drift; and +- profile replacement or reference-recording failures. + +A failed profile commit restores the prior active profile. Download, version +selection, signatures, multiple active versions, backend plugins, update and +outdated operations, lazy shims, shell activation, and `zed dev` integration are +separate certified slices under DEN-1437 and DEN-1442. From 033e860708c3e1ad6684fe4c47d8b53aae0b42bf Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:18:46 -0500 Subject: [PATCH 07/16] ci(DEN-2923): certify frozen offline tool profiles --- .github/workflows/tool-profile.yml | 110 +++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/tool-profile.yml diff --git a/.github/workflows/tool-profile.yml b/.github/workflows/tool-profile.yml new file mode 100644 index 00000000..31aa0225 --- /dev/null +++ b/.github/workflows/tool-profile.yml @@ -0,0 +1,110 @@ +name: frozen offline tool profiles + +on: + pull_request: + paths: + - .tool-cli-flags.toml + - docs/tool-profile.md + - src/tool_profile.rs + - src/bin/zed-tool.rs + - src/lib.rs + - tests/tool_profile_cli.rs + - .github/workflows/tool-profile.yml + push: + branches: [main] + paths: + - .tool-cli-flags.toml + - docs/tool-profile.md + - src/tool_profile.rs + - src/bin/zed-tool.rs + - src/lib.rs + - tests/tool_profile_cli.rs + - .github/workflows/tool-profile.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: tool-profile-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + tool-profile: + name: frozen profile / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15, windows-2025] + steps: + - name: Check out exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + persist-credentials: false + show-progress: false + + - name: Install Rust quality components + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: stable + components: rustfmt,clippy + + - name: Validate staged flags-to-environment contract + run: | + set -euo pipefail + python - <<'PY' + import tomllib + from pathlib import Path + + data = tomllib.loads(Path('.tool-cli-flags.toml').read_text()) + observed = set() + for flag in data.get('flags', {}).values(): + observed.add(flag['env']) + for command in data.get('commands', {}).values(): + for flag in command.get('flags', {}).values(): + observed.add(flag['env']) + expected = { + 'ZED_TOOL_LOCK', + 'ZED_TOOL_JSON', + 'ZED_TOOL_PORTABLE', + 'ZED_TOOL_PLAN_DIGEST', + 'ZED_TOOL_TARGET', + 'ZED_TOOL_OFFLINE', + 'ZED_TOOL_PROFILE', + 'ZED_PKG_HOME', + } + assert observed == expected, (observed, expected) + PY + + - name: Formatting, unit, and compiled CLI tests + run: | + set -euo pipefail + cargo fmt --all --check + cargo test --locked --lib tool_profile::tests + cargo test --locked --test tool_profile_cli + + - name: Current store and lock compatibility + run: | + set -euo pipefail + cargo test --locked --lib store::tests + cargo test --locked --lib project_lock::tests + cargo test --locked --test frozen_offline_prefetch + + - name: Strict Clippy + run: | + set -euo pipefail + cargo clippy --locked --bin zed-tool --test tool_profile_cli -- -D warnings + + - name: Require a clean checkout + run: | + set -euo pipefail + git diff --exit-code + git status --porcelain=v1 --untracked-files=all | tee /tmp/tool-profile-status.txt + test ! -s /tmp/tool-profile-status.txt From b972088271096dac656106ebbb3844f423b11e1a Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 00:20:26 -0500 Subject: [PATCH 08/16] ci(DEN-2923): harden and validate frozen tool profiles --- .../workflows/repair-offline-tool-profile.yml | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/repair-offline-tool-profile.yml diff --git a/.github/workflows/repair-offline-tool-profile.yml b/.github/workflows/repair-offline-tool-profile.yml new file mode 100644 index 00000000..c1f58015 --- /dev/null +++ b/.github/workflows/repair-offline-tool-profile.yml @@ -0,0 +1,164 @@ +name: harden and validate frozen tool profiles + +on: + push: + branches: + - feat/den-2923-offline-tool-profile + paths: + - .github/workflows/repair-offline-tool-profile.yml + +permissions: + contents: write + +concurrency: + group: repair-offline-tool-profile + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + repair: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Check out exact feature branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: feat/den-2923-offline-tool-profile + persist-credentials: true + show-progress: false + + - name: Install Rust quality components + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: stable + components: rustfmt,clippy + + - name: Apply receipt and idempotence hardening + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + path = Path('src/tool_profile.rs') + text = path.read_text() + + text = text.replace("pub schema: &'static str,", "pub schema: String,") + text = text.replace( + 'schema: TOOL_PROFILE_SCHEMA_V1,', + 'schema: TOOL_PROFILE_SCHEMA_V1.to_string(),', + ) + text = text.replace(' logical_name: String,\n', '') + text = text.replace(' logical_name: logical_name.clone(),\n', '') + + selection = ''' let selected = select_exact_target(&loaded.lock, target)?; + let store = Store::new(home); + ''' + selection_hardened = ''' let selected = select_exact_target(&loaded.lock, target)?; + ensure!( + !selected.is_empty(), + "environment lock contains no tools for frozen profile installation" + ); + let store = Store::new(home); + ''' + if selection not in text: + raise SystemExit('install selection anchor not found') + text = text.replace(selection, selection_hardened, 1) + + old = ''' let actual = fs::read_dir(&bin)? + .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned())) + .collect::>>()?; + Ok(expected == actual) + } + ''' + new = ''' let actual = fs::read_dir(&bin)? + .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned())) + .collect::>>()?; + if expected != actual { + return Ok(false); + } + for executable in prepared.iter().flat_map(|tool| tool.executables.iter()) { + let destination = bin.join(&executable.profile_name); + #[cfg(unix)] + { + let metadata = fs::symlink_metadata(&destination)?; + if !metadata.file_type().is_symlink() + || fs::read_link(&destination)? != executable.source + { + return Ok(false); + } + } + #[cfg(windows)] + { + let metadata = fs::symlink_metadata(&destination)?; + if !metadata.is_file() || metadata.file_type().is_symlink() { + return Ok(false); + } + let (source_sha256, source_size) = sha256_file(&executable.source)?; + let (destination_sha256, destination_size) = sha256_file(&destination)?; + if source_sha256 != destination_sha256 || source_size != destination_size { + return Ok(false); + } + } + } + Ok(true) + } + ''' + if old not in text: + raise SystemExit('active profile comparison anchor not found') + text = text.replace(old, new, 1) + + old = '''fn profile_executable_name(logical_name: &str, source: &Path) -> Result { + #[cfg(windows)] + ''' + new = '''fn profile_executable_name(logical_name: &str, source: &Path) -> Result { + #[cfg(not(windows))] + let _ = source; + #[cfg(windows)] + ''' + if old not in text: + raise SystemExit('profile executable name anchor not found') + path.write_text(text.replace(old, new, 1)) + + binary = Path('src/bin/zed-tool.rs') + text = binary.read_text() + text = text.replace( + '#[arg(long, env = "ZED_TOOL_LOCK", default_value_os_t = default_lock_path())]', + '#[arg(long, env = "ZED_TOOL_LOCK", default_value = ".zed/environment.lock.toml")]', + ) + text = text.replace( + '#[arg(long, env = "ZED_TOOL_PROFILE", default_value_os_t = default_profile_path())]', + '#[arg(long, env = "ZED_TOOL_PROFILE", default_value = ".zed/tools")]', + ) + text = text.replace( + ' LockMode, default_lock_path, default_profile_path, default_zed_home, install_offline,\n', + ' LockMode, default_zed_home, install_offline,\n', + ) + binary.write_text(text) + PY + + - name: Format, test, and lint the feature + run: | + set -euo pipefail + cargo fmt --all + cargo fmt --all --check + cargo test --locked --lib tool_profile::tests + cargo test --locked --test tool_profile_cli + cargo test --locked --lib store::tests + cargo test --locked --lib project_lock::tests + cargo test --locked --test frozen_offline_prefetch + cargo clippy --locked --bin zed-tool --test tool_profile_cli -- -D warnings + + - name: Commit ordinary hardened source + run: | + set -euo pipefail + rm .github/workflows/repair-offline-tool-profile.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add src/tool_profile.rs src/bin/zed-tool.rs .github/workflows/repair-offline-tool-profile.yml + git diff --cached --check + git commit -m 'fix(DEN-2923): harden frozen tool-profile replay' + git push origin HEAD:feat/den-2923-offline-tool-profile From 07c5678aed49ff22d3ca47d22ebd6dee67d18db4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 05:45:54 +0000 Subject: [PATCH 09/16] fix(DEN-2923): harden frozen tool-profile replay --- .../workflows/repair-offline-tool-profile.yml | 164 ------------------ src/bin/zed-tool.rs | 41 ++--- src/tool_profile.rs | 107 ++++++++---- 3 files changed, 87 insertions(+), 225 deletions(-) delete mode 100644 .github/workflows/repair-offline-tool-profile.yml diff --git a/.github/workflows/repair-offline-tool-profile.yml b/.github/workflows/repair-offline-tool-profile.yml deleted file mode 100644 index c1f58015..00000000 --- a/.github/workflows/repair-offline-tool-profile.yml +++ /dev/null @@ -1,164 +0,0 @@ -name: harden and validate frozen tool profiles - -on: - push: - branches: - - feat/den-2923-offline-tool-profile - paths: - - .github/workflows/repair-offline-tool-profile.yml - -permissions: - contents: write - -concurrency: - group: repair-offline-tool-profile - cancel-in-progress: false - -defaults: - run: - shell: bash - -jobs: - repair: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - name: Check out exact feature branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: feat/den-2923-offline-tool-profile - persist-credentials: true - show-progress: false - - - name: Install Rust quality components - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 - with: - toolchain: stable - components: rustfmt,clippy - - - name: Apply receipt and idempotence hardening - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - - path = Path('src/tool_profile.rs') - text = path.read_text() - - text = text.replace("pub schema: &'static str,", "pub schema: String,") - text = text.replace( - 'schema: TOOL_PROFILE_SCHEMA_V1,', - 'schema: TOOL_PROFILE_SCHEMA_V1.to_string(),', - ) - text = text.replace(' logical_name: String,\n', '') - text = text.replace(' logical_name: logical_name.clone(),\n', '') - - selection = ''' let selected = select_exact_target(&loaded.lock, target)?; - let store = Store::new(home); - ''' - selection_hardened = ''' let selected = select_exact_target(&loaded.lock, target)?; - ensure!( - !selected.is_empty(), - "environment lock contains no tools for frozen profile installation" - ); - let store = Store::new(home); - ''' - if selection not in text: - raise SystemExit('install selection anchor not found') - text = text.replace(selection, selection_hardened, 1) - - old = ''' let actual = fs::read_dir(&bin)? - .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned())) - .collect::>>()?; - Ok(expected == actual) - } - ''' - new = ''' let actual = fs::read_dir(&bin)? - .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned())) - .collect::>>()?; - if expected != actual { - return Ok(false); - } - for executable in prepared.iter().flat_map(|tool| tool.executables.iter()) { - let destination = bin.join(&executable.profile_name); - #[cfg(unix)] - { - let metadata = fs::symlink_metadata(&destination)?; - if !metadata.file_type().is_symlink() - || fs::read_link(&destination)? != executable.source - { - return Ok(false); - } - } - #[cfg(windows)] - { - let metadata = fs::symlink_metadata(&destination)?; - if !metadata.is_file() || metadata.file_type().is_symlink() { - return Ok(false); - } - let (source_sha256, source_size) = sha256_file(&executable.source)?; - let (destination_sha256, destination_size) = sha256_file(&destination)?; - if source_sha256 != destination_sha256 || source_size != destination_size { - return Ok(false); - } - } - } - Ok(true) - } - ''' - if old not in text: - raise SystemExit('active profile comparison anchor not found') - text = text.replace(old, new, 1) - - old = '''fn profile_executable_name(logical_name: &str, source: &Path) -> Result { - #[cfg(windows)] - ''' - new = '''fn profile_executable_name(logical_name: &str, source: &Path) -> Result { - #[cfg(not(windows))] - let _ = source; - #[cfg(windows)] - ''' - if old not in text: - raise SystemExit('profile executable name anchor not found') - path.write_text(text.replace(old, new, 1)) - - binary = Path('src/bin/zed-tool.rs') - text = binary.read_text() - text = text.replace( - '#[arg(long, env = "ZED_TOOL_LOCK", default_value_os_t = default_lock_path())]', - '#[arg(long, env = "ZED_TOOL_LOCK", default_value = ".zed/environment.lock.toml")]', - ) - text = text.replace( - '#[arg(long, env = "ZED_TOOL_PROFILE", default_value_os_t = default_profile_path())]', - '#[arg(long, env = "ZED_TOOL_PROFILE", default_value = ".zed/tools")]', - ) - text = text.replace( - ' LockMode, default_lock_path, default_profile_path, default_zed_home, install_offline,\n', - ' LockMode, default_zed_home, install_offline,\n', - ) - binary.write_text(text) - PY - - - name: Format, test, and lint the feature - run: | - set -euo pipefail - cargo fmt --all - cargo fmt --all --check - cargo test --locked --lib tool_profile::tests - cargo test --locked --test tool_profile_cli - cargo test --locked --lib store::tests - cargo test --locked --lib project_lock::tests - cargo test --locked --test frozen_offline_prefetch - cargo clippy --locked --bin zed-tool --test tool_profile_cli -- -D warnings - - - name: Commit ordinary hardened source - run: | - set -euo pipefail - rm .github/workflows/repair-offline-tool-profile.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add src/tool_profile.rs src/bin/zed-tool.rs .github/workflows/repair-offline-tool-profile.yml - git diff --cached --check - git commit -m 'fix(DEN-2923): harden frozen tool-profile replay' - git push origin HEAD:feat/den-2923-offline-tool-profile diff --git a/src/bin/zed-tool.rs b/src/bin/zed-tool.rs index fab1b30c..a08c6663 100644 --- a/src/bin/zed-tool.rs +++ b/src/bin/zed-tool.rs @@ -3,8 +3,7 @@ use std::path::PathBuf; use anyhow::{Result, ensure}; use clap::{Parser, Subcommand}; use zed_cli::tool_profile::{ - LockMode, default_lock_path, default_profile_path, default_zed_home, install_offline, - list_target, load_environment_lock, verify_receipt, + LockMode, default_zed_home, install_offline, list_target, load_environment_lock, verify_receipt, }; #[derive(Debug, Parser)] @@ -15,7 +14,11 @@ use zed_cli::tool_profile::{ )] struct Cli { /// Project-local EnvironmentLock TOML or JSON. - #[arg(long, env = "ZED_TOOL_LOCK", default_value_os_t = default_lock_path())] + #[arg( + long, + env = "ZED_TOOL_LOCK", + default_value = ".zed/environment.lock.toml" + )] lock: PathBuf, /// Emit one stable JSON document. @@ -57,7 +60,7 @@ enum ToolCommand { offline: bool, /// Project-local profile root. - #[arg(long, env = "ZED_TOOL_PROFILE", default_value_os_t = default_profile_path())] + #[arg(long, env = "ZED_TOOL_PROFILE", default_value = ".zed/tools")] profile: PathBuf, /// Zed content-addressed store and cache root. @@ -85,12 +88,8 @@ fn run(cli: Cli) -> Result<()> { } else { LockMode::Local }; - let loaded = load_environment_lock( - &root, - Some(&cli.lock), - mode, - plan_digest.as_deref(), - )?; + let loaded = + load_environment_lock(&root, Some(&cli.lock), mode, plan_digest.as_deref())?; let receipt = verify_receipt(&loaded, mode); if cli.json { println!("{}", serde_json::to_string_pretty(&receipt)?); @@ -104,12 +103,7 @@ fn run(cli: Cli) -> Result<()> { } } ToolCommand::List { target } => { - let loaded = load_environment_lock( - &root, - Some(&cli.lock), - LockMode::Portable, - None, - )?; + let loaded = load_environment_lock(&root, Some(&cli.lock), LockMode::Portable, None)?; let tools = list_target(&loaded, &target)?; if cli.json { println!("{}", serde_json::to_string_pretty(&tools)?); @@ -136,24 +130,13 @@ fn run(cli: Cli) -> Result<()> { offline, "native tool installation currently requires `--offline`; version discovery and downloads are not yet certified" ); - let loaded = load_environment_lock( - &root, - Some(&cli.lock), - LockMode::Portable, - None, - )?; + let loaded = load_environment_lock(&root, Some(&cli.lock), LockMode::Portable, None)?; let home = match home { Some(home) if home.is_absolute() => home, Some(home) => root.join(home), None => default_zed_home()?, }; - let receipt = install_offline( - &root, - &loaded, - &target, - Some(&profile), - &home, - )?; + let receipt = install_offline(&root, &loaded, &target, Some(&profile), &home)?; if cli.json { println!("{}", serde_json::to_string_pretty(&receipt)?); } else { diff --git a/src/tool_profile.rs b/src/tool_profile.rs index 088507a4..be7542d1 100644 --- a/src/tool_profile.rs +++ b/src/tool_profile.rs @@ -57,7 +57,7 @@ pub struct ToolLockSummary { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ToolVerifyReceipt { - pub schema: &'static str, + pub schema: String, pub lock: String, pub lock_sha256: String, pub validation: String, @@ -68,7 +68,7 @@ pub struct ToolVerifyReceipt { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ToolInstallReceipt { - pub schema: &'static str, + pub schema: String, pub action: String, pub lock: String, pub lock_sha256: String, @@ -111,7 +111,6 @@ struct SelectedTool<'a> { #[derive(Debug, Clone)] struct PreparedExecutable { - logical_name: String, profile_name: String, source: PathBuf, source_relative: String, @@ -152,7 +151,11 @@ pub fn load_environment_lock( let root = root .canonicalize() .with_context(|| format!("canonicalizing project root `{}`", root.display()))?; - ensure!(root.is_dir(), "project root `{}` is not a directory", root.display()); + ensure!( + root.is_dir(), + "project root `{}` is not a directory", + root.display() + ); let relative_path = portable_relative_path( lock_path.unwrap_or_else(|| Path::new(DEFAULT_LOCK_PATH)), "environment lock", @@ -161,8 +164,12 @@ pub fn load_environment_lock( let path = existing_regular_project_file(&root, &relative_path, "environment lock")?; let bytes = fs::read(&path) .with_context(|| format!("reading environment lock `{}`", relative_path.display()))?; - let input = std::str::from_utf8(&bytes) - .with_context(|| format!("environment lock `{}` is not UTF-8", relative_path.display()))?; + let input = std::str::from_utf8(&bytes).with_context(|| { + format!( + "environment lock `{}` is not UTF-8", + relative_path.display() + ) + })?; let lock = if relative_path .extension() .is_some_and(|extension| extension.eq_ignore_ascii_case("json")) @@ -187,12 +194,9 @@ pub fn load_environment_lock( }) } -pub fn verify_receipt( - loaded: &LoadedEnvironmentLock, - mode: LockMode, -) -> ToolVerifyReceipt { +pub fn verify_receipt(loaded: &LoadedEnvironmentLock, mode: LockMode) -> ToolVerifyReceipt { ToolVerifyReceipt { - schema: TOOL_PROFILE_SCHEMA_V1, + schema: TOOL_PROFILE_SCHEMA_V1.to_string(), lock: portable_display(&loaded.relative_path), lock_sha256: loaded.digest_sha256.clone(), validation: match mode { @@ -206,10 +210,7 @@ pub fn verify_receipt( } } -pub fn list_target( - loaded: &LoadedEnvironmentLock, - target: &str, -) -> Result> { +pub fn list_target(loaded: &LoadedEnvironmentLock, target: &str) -> Result> { let selected = select_exact_target(&loaded.lock, target)?; Ok(selected .into_iter() @@ -238,17 +239,14 @@ pub fn install_offline( )?; validate_component(target, "target")?; let selected = select_exact_target(&loaded.lock, target)?; + ensure!( + !selected.is_empty(), + "environment lock contains no tools for frozen profile installation" + ); let store = Store::new(home); project_lock::with_lock(&root, "install frozen environment tool profile", || { - install_locked( - &root, - loaded, - target, - &profile_relative, - &store, - &selected, - ) + install_locked(&root, loaded, target, &profile_relative, &store, &selected) }) } @@ -298,7 +296,10 @@ fn install_locked( fs::create_dir(&staging_bin)?; for tool in &prepared { for executable in &tool.executables { - materialize_executable(&executable.source, &staging_bin.join(&executable.profile_name))?; + materialize_executable( + &executable.source, + &staging_bin.join(&executable.profile_name), + )?; } } let state_path = staging_path.join(PROFILE_STATE_FILE); @@ -471,7 +472,6 @@ fn prepare_locked_executable( ); } output.push(PreparedExecutable { - logical_name: logical_name.clone(), profile_name, source: source.clone(), source_relative: portable_display(&source_relative), @@ -550,12 +550,43 @@ fn active_profile_matches( } let expected = prepared .iter() - .flat_map(|tool| tool.executables.iter().map(|executable| executable.profile_name.clone())) + .flat_map(|tool| { + tool.executables + .iter() + .map(|executable| executable.profile_name.clone()) + }) .collect::>(); let actual = fs::read_dir(&bin)? .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned())) .collect::>>()?; - Ok(expected == actual) + if expected != actual { + return Ok(false); + } + for executable in prepared.iter().flat_map(|tool| tool.executables.iter()) { + let destination = bin.join(&executable.profile_name); + #[cfg(unix)] + { + let metadata = fs::symlink_metadata(&destination)?; + if !metadata.file_type().is_symlink() + || fs::read_link(&destination)? != executable.source + { + return Ok(false); + } + } + #[cfg(windows)] + { + let metadata = fs::symlink_metadata(&destination)?; + if !metadata.is_file() || metadata.file_type().is_symlink() { + return Ok(false); + } + let (source_sha256, source_size) = sha256_file(&executable.source)?; + let (destination_sha256, destination_size) = sha256_file(&destination)?; + if source_sha256 != destination_sha256 || source_size != destination_size { + return Ok(false); + } + } + } + Ok(true) } fn install_receipt( @@ -567,7 +598,7 @@ fn install_receipt( ) -> ToolInstallReceipt { let active_relative = profile_relative.join("v1").join(target); ToolInstallReceipt { - schema: TOOL_PROFILE_SCHEMA_V1, + schema: TOOL_PROFILE_SCHEMA_V1.to_string(), action: action.to_string(), lock: portable_display(&loaded.relative_path), lock_sha256: loaded.digest_sha256.clone(), @@ -660,11 +691,16 @@ fn materialize_executable(source: &Path, destination: &Path) -> Result<()> { } fn profile_executable_name(logical_name: &str, source: &Path) -> Result { + #[cfg(not(windows))] + let _ = source; #[cfg(windows)] { if Path::new(logical_name).extension().is_none() && let Some(extension) = source.extension().and_then(|value| value.to_str()) - && matches!(extension.to_ascii_lowercase().as_str(), "exe" | "com" | "cmd" | "bat") + && matches!( + extension.to_ascii_lowercase().as_str(), + "exe" | "com" | "cmd" | "bat" + ) { return Ok(format!("{logical_name}.{extension}")); } @@ -749,7 +785,10 @@ fn ensure_directory_chain(root: &Path, relative: &Path, field: &str) -> Result

{ fs::create_dir(¤t).with_context(|| { - format!("creating {field} directory `{}`", portable_display(¤t)) + format!( + "creating {field} directory `{}`", + portable_display(¤t) + ) })?; } Err(error) => return Err(error).with_context(|| format!("inspecting {field}")), @@ -809,12 +848,16 @@ fn portable_relative_path(path: &Path, field: &str, allow_dot: bool) -> Result

Result<()> { ensure!(!value.trim().is_empty(), "{field} cannot be empty"); - ensure!(value == value.trim(), "{field} cannot contain surrounding whitespace"); + ensure!( + value == value.trim(), + "{field} cannot contain surrounding whitespace" + ); ensure!(value != "." && value != "..", "{field} is invalid"); ensure!( value .chars() - .all(|character| character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-')), + .all(|character| character.is_ascii_alphanumeric() + || matches!(character, '.' | '_' | '-')), "{field} `{value}` contains unsupported characters" ); ensure!(value.len() <= 128, "{field} exceeds 128 bytes"); From 9217abf6aaba6af7ba8f820a25d703a2eaddfedd Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 01:12:07 -0500 Subject: [PATCH 10/16] style(DEN-2923): apply rustfmt to tool profile CLI test --- tests/tool_profile_cli.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/tool_profile_cli.rs b/tests/tool_profile_cli.rs index 7a543c94..c378985d 100644 --- a/tests/tool_profile_cli.rs +++ b/tests/tool_profile_cli.rs @@ -285,12 +285,7 @@ fn wrong_plan_digest_and_target_fail_without_mutation() { let digest = run( &project, &home, - &[ - "verify", - "--portable", - "--plan-digest", - &"b".repeat(64), - ], + &["verify", "--portable", "--plan-digest", &"b".repeat(64)], ); assert!(!digest.status.success()); assert!(String::from_utf8_lossy(&digest.stderr).contains("plan digest")); From 4d5a18864b8a2f38947edb3e9199bea0e0a9cb1c Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 01:33:48 -0500 Subject: [PATCH 11/16] fix(DEN-2923): roll back empty profile directories on failure --- src/bin/zed-tool.rs | 93 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/src/bin/zed-tool.rs b/src/bin/zed-tool.rs index a08c6663..6f802b58 100644 --- a/src/bin/zed-tool.rs +++ b/src/bin/zed-tool.rs @@ -1,6 +1,8 @@ -use std::path::PathBuf; +use std::fs; +use std::io::ErrorKind; +use std::path::{Component, Path, PathBuf}; -use anyhow::{Result, ensure}; +use anyhow::{Context, Result, ensure}; use clap::{Parser, Subcommand}; use zed_cli::tool_profile::{ LockMode, default_zed_home, install_offline, list_target, load_environment_lock, verify_receipt, @@ -136,7 +138,18 @@ fn run(cli: Cli) -> Result<()> { Some(home) => root.join(home), None => default_zed_home()?, }; - let receipt = install_offline(&root, &loaded, &target, Some(&profile), &home)?; + let rollback_directories = absent_profile_directories(&root, &profile); + let receipt = match install_offline(&root, &loaded, &target, Some(&profile), &home) { + Ok(receipt) => receipt, + Err(error) => { + rollback_empty_profile_directories(&rollback_directories).with_context(|| { + format!( + "rolling back empty profile directories after installation failed: {error:#}" + ) + })?; + return Err(error); + } + }; if cli.json { println!("{}", serde_json::to_string_pretty(&receipt)?); } else { @@ -157,3 +170,77 @@ fn run(cli: Cli) -> Result<()> { } Ok(()) } + +/// Record only profile-directory prefixes that do not exist before installation. +/// Unsafe paths are left to the library validator and produce no rollback list. +fn absent_profile_directories(root: &Path, profile: &Path) -> Vec { + if profile.as_os_str().is_empty() || profile.is_absolute() { + return Vec::new(); + } + + let mut current = root.to_path_buf(); + let mut absent = Vec::new(); + for component in profile.components() { + let Component::Normal(segment) = component else { + return Vec::new(); + }; + current.push(segment); + if matches!( + fs::symlink_metadata(¤t), + Err(error) if error.kind() == ErrorKind::NotFound + ) { + absent.push(current.clone()); + } + } + + let version = current.join("v1"); + if matches!( + fs::symlink_metadata(&version), + Err(error) if error.kind() == ErrorKind::NotFound + ) { + absent.push(version); + } + absent +} + +/// Remove only directories that were absent before this invocation and remain +/// empty. Nonempty directories preserve forensic or concurrent state and are +/// deliberately left alone; symlink or file replacement fails closed. +fn rollback_empty_profile_directories(directories: &[PathBuf]) -> Result<()> { + for directory in directories.iter().rev() { + let metadata = match fs::symlink_metadata(directory) { + Ok(metadata) => metadata, + Err(error) if error.kind() == ErrorKind::NotFound => continue, + Err(error) => { + return Err(error).with_context(|| { + format!("inspecting rollback directory `{}`", directory.display()) + }); + } + }; + ensure!( + metadata.is_dir() && !metadata.file_type().is_symlink(), + "new profile path `{}` changed type during rollback", + directory.display() + ); + + match fs::remove_dir(directory) { + Ok(()) => {} + Err(error) if error.kind() == ErrorKind::NotFound => {} + Err(error) => { + let has_entries = fs::read_dir(directory) + .with_context(|| { + format!("reading rollback directory `{}`", directory.display()) + })? + .next() + .transpose()? + .is_some(); + if !has_entries { + return Err(error).with_context(|| { + format!("removing empty rollback directory `{}`", directory.display()) + }); + } + } + } + } + Ok(()) +} From b260d4f13d6257160343ec6c8f9f3195859748b8 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 01:34:32 -0500 Subject: [PATCH 12/16] test(DEN-2923): require full profile rollback on rejection --- tests/tool_profile_cli.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/tool_profile_cli.rs b/tests/tool_profile_cli.rs index c378985d..622656b6 100644 --- a/tests/tool_profile_cli.rs +++ b/tests/tool_profile_cli.rs @@ -169,6 +169,13 @@ fn assert_success(output: &Output) { ); } +fn assert_no_profile_state(project: &Path) { + assert!( + !project.join(".zed/tools").exists(), + "failed command left the tool-profile namespace behind" + ); +} + #[test] fn verify_list_install_and_idempotent_replay_are_deterministic() { let (_temp, project, home, sha256) = fixture(&[("hello", "hello")]); @@ -245,7 +252,7 @@ fn missing_and_tampered_cache_fail_before_profile_mutation() { ); assert!(!missing.status.success()); assert!(String::from_utf8_lossy(&missing.stderr).contains("prefetch")); - assert!(!project.join(".zed/tools/v1").join(target()).exists()); + assert_no_profile_state(&project); fs::write(&cache, &original).unwrap(); let mut tampered = original; @@ -259,7 +266,7 @@ fn missing_and_tampered_cache_fail_before_profile_mutation() { ); assert!(!mismatch.status.success()); assert!(String::from_utf8_lossy(&mismatch.stderr).contains("hash mismatch")); - assert!(!project.join(".zed/tools/v1").join(target()).exists()); + assert_no_profile_state(&project); } #[test] @@ -272,11 +279,12 @@ fn executable_collisions_and_online_mode_fail_closed() { ); assert!(!collision.status.success()); assert!(String::from_utf8_lossy(&collision.stderr).contains("claimed by both")); - assert!(!project.join(".zed/tools/v1").join(target()).exists()); + assert_no_profile_state(&project); let online = run(&project, &home, &["install", "--target", target()]); assert!(!online.status.success()); assert!(String::from_utf8_lossy(&online.stderr).contains("requires `--offline`")); + assert_no_profile_state(&project); } #[test] @@ -289,6 +297,7 @@ fn wrong_plan_digest_and_target_fail_without_mutation() { ); assert!(!digest.status.success()); assert!(String::from_utf8_lossy(&digest.stderr).contains("plan digest")); + assert_no_profile_state(&project); let target = run( &project, @@ -297,5 +306,5 @@ fn wrong_plan_digest_and_target_fail_without_mutation() { ); assert!(!target.status.success()); assert!(String::from_utf8_lossy(&target.stderr).contains("unsupported characters")); - assert!(!project.join(".zed/tools/v1").exists()); + assert_no_profile_state(&project); } From 9c325e54a4058c8e55500639f1b18caffa8973c9 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 01:38:28 -0500 Subject: [PATCH 13/16] style(DEN-2923): apply rustfmt to rollback diagnostic --- src/bin/zed-tool.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bin/zed-tool.rs b/src/bin/zed-tool.rs index 6f802b58..ed3497ae 100644 --- a/src/bin/zed-tool.rs +++ b/src/bin/zed-tool.rs @@ -236,7 +236,10 @@ fn rollback_empty_profile_directories(directories: &[PathBuf]) -> Result<()> { .is_some(); if !has_entries { return Err(error).with_context(|| { - format!("removing empty rollback directory `{}`", directory.display()) + format!( + "removing empty rollback directory `{}`", + directory.display() + ) }); } } From 11335df26dc221a26b08e8357ccf1ee94366aebd Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 10:35:18 -0500 Subject: [PATCH 14/16] ci(DEN-2923): materialize certified Windows tool-profile fix --- .../materialize-den-2923-windows-fix.yml | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/workflows/materialize-den-2923-windows-fix.yml diff --git a/.github/workflows/materialize-den-2923-windows-fix.yml b/.github/workflows/materialize-den-2923-windows-fix.yml new file mode 100644 index 00000000..1794a2be --- /dev/null +++ b/.github/workflows/materialize-den-2923-windows-fix.yml @@ -0,0 +1,109 @@ +name: materialize certified DEN-2923 Windows fix + +on: + push: + branches: + - feat/den-2923-offline-tool-profile + paths: + - .github/workflows/materialize-den-2923-windows-fix.yml + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: materialize-den-2923-windows-fix + cancel-in-progress: true + +env: + EXPECTED_PRODUCT_PARENT: 9c325e54a4058c8e55500639f1b18caffa8973c9 + EXPECTED_FORMATTED_SHA256: 2749b999c67cf2fc609db22b8cb784ce6b1db2284df376225fedbec56a41c442 + +jobs: + materialize: + name: apply certified bytes and remove carrier + runs-on: windows-2025 + timeout-minutes: 50 + steps: + - name: Check out exact carrier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 2 + persist-credentials: true + show-progress: false + + - name: Install pinned Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: stable + components: rustfmt,clippy + + - name: Apply the exact certified repair + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD^)" = "$EXPECTED_PRODUCT_PARENT" + test "$(git diff --name-only HEAD^ HEAD)" = .github/workflows/materialize-den-2923-windows-fix.yml + + python3 - <<'PY' + from pathlib import Path + + path = Path("src/tool_profile.rs") + text = path.read_text(encoding="utf-8") + + replacements = [ + ( + "use std::fs::{self, File, OpenOptions};\n", + "use std::fs::{self, OpenOptions};\n#[cfg(unix)]\nuse std::fs::File;\n", + "shared File/OpenOptions import", + ), + ( + " File::open(destination)?.sync_all()?;\n", + """ OpenOptions::new()\n .write(true)\n .open(destination)\n .with_context(|| {\n format!(\n \"opening copied tool executable `{}` for synchronization\",\n portable_display(destination)\n )\n })?\n .sync_all()\n .with_context(|| {\n format!(\n \"synchronizing copied tool executable `{}`\",\n portable_display(destination)\n )\n })?;\n""", + "read-only copied-executable sync handle", + ), + ( + """ let mut lock = EnvironmentLock::default();\n lock.plan_digest_sha256 = \"a\".repeat(64);\n assert_eq!(select_exact_target(&lock, \"x86_64-linux\").unwrap().len(), 0);\n""", + """ let lock = EnvironmentLock {\n plan_digest_sha256: \"a\".repeat(64),\n ..EnvironmentLock::default()\n };\n assert_eq!(select_exact_target(&lock, \"x86_64-linux\").unwrap().len(), 0);\n""", + "Clippy-rejected EnvironmentLock fixture", + ), + ] + for old, new, label in replacements: + if text.count(old) != 1: + raise SystemExit(f"expected exactly one {label}") + text = text.replace(old, new, 1) + + marker = """ #[test]\n fn profile_state_is_secret_free_by_shape() {\n let state = ToolProfileState {\n schema: TOOL_PROFILE_SCHEMA_V1.to_string(),\n lock_sha256: \"a\".repeat(64),\n target: \"x86_64-linux\".to_string(),\n tools: Vec::new(),\n };\n let json = serde_json::to_string(&state).unwrap();\n for forbidden in [\"locator\", \"url\", \"token\", \"credential\", \"environment\"] {\n assert!(!json.contains(forbidden));\n }\n }\n}\n""" + replacement = marker[:-2] + """\n\n #[cfg(windows)]\n #[test]\n fn copied_windows_executable_is_opened_with_sync_capable_access() {\n let root = tempfile::tempdir().unwrap();\n let source = root.path().join(\"hello.cmd\");\n let destination = root.path().join(\"hello-copy.cmd\");\n fs::write(&source, b\"@echo off\\r\\necho hello\\r\\n\").unwrap();\n\n materialize_executable(&source, &destination).unwrap();\n\n assert_eq!(fs::read(destination).unwrap(), fs::read(source).unwrap());\n }\n}\n""" + if text.count(marker) != 1: + raise SystemExit("expected exact tool-profile unit-test tail") + path.write_text(text.replace(marker, replacement, 1), encoding="utf-8") + PY + + cargo fmt --all + actual="$(sha256sum src/tool_profile.rs | cut -d' ' -f1)" + test "$actual" = "$EXPECTED_FORMATTED_SHA256" + git diff --check + + - name: Re-run exact Windows product contracts + shell: bash + run: | + set -euo pipefail + cargo fmt --all --check + cargo test --locked --lib tool_profile::tests -- --nocapture --test-threads=1 + cargo test --locked --test tool_profile_cli -- --nocapture --test-threads=1 + cargo clippy --locked --all-targets -- -D warnings + + - name: Commit only the certified product file + shell: bash + run: | + set -euo pipefail + git rm .github/workflows/materialize-den-2923-windows-fix.yml + changed="$(git diff --name-only HEAD)" + expected=$'.github/workflows/materialize-den-2923-windows-fix.yml\nsrc/tool_profile.rs' + test "$changed" = "$expected" + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add src/tool_profile.rs + git commit -m "fix(DEN-2923): synchronize copied Windows tool executables" + git push origin HEAD:feat/den-2923-offline-tool-profile From fdaf48d0ee0e132820b8fd69e0d114a365c07745 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:39:08 +0000 Subject: [PATCH 15/16] fix(DEN-2923): synchronize copied Windows tool executables --- .../materialize-den-2923-windows-fix.yml | 109 ------------------ src/tool_profile.rs | 40 ++++++- 2 files changed, 36 insertions(+), 113 deletions(-) delete mode 100644 .github/workflows/materialize-den-2923-windows-fix.yml diff --git a/.github/workflows/materialize-den-2923-windows-fix.yml b/.github/workflows/materialize-den-2923-windows-fix.yml deleted file mode 100644 index 1794a2be..00000000 --- a/.github/workflows/materialize-den-2923-windows-fix.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: materialize certified DEN-2923 Windows fix - -on: - push: - branches: - - feat/den-2923-offline-tool-profile - paths: - - .github/workflows/materialize-den-2923-windows-fix.yml - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: materialize-den-2923-windows-fix - cancel-in-progress: true - -env: - EXPECTED_PRODUCT_PARENT: 9c325e54a4058c8e55500639f1b18caffa8973c9 - EXPECTED_FORMATTED_SHA256: 2749b999c67cf2fc609db22b8cb784ce6b1db2284df376225fedbec56a41c442 - -jobs: - materialize: - name: apply certified bytes and remove carrier - runs-on: windows-2025 - timeout-minutes: 50 - steps: - - name: Check out exact carrier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 2 - persist-credentials: true - show-progress: false - - - name: Install pinned Rust toolchain - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 - with: - toolchain: stable - components: rustfmt,clippy - - - name: Apply the exact certified repair - shell: bash - run: | - set -euo pipefail - test "$(git rev-parse HEAD^)" = "$EXPECTED_PRODUCT_PARENT" - test "$(git diff --name-only HEAD^ HEAD)" = .github/workflows/materialize-den-2923-windows-fix.yml - - python3 - <<'PY' - from pathlib import Path - - path = Path("src/tool_profile.rs") - text = path.read_text(encoding="utf-8") - - replacements = [ - ( - "use std::fs::{self, File, OpenOptions};\n", - "use std::fs::{self, OpenOptions};\n#[cfg(unix)]\nuse std::fs::File;\n", - "shared File/OpenOptions import", - ), - ( - " File::open(destination)?.sync_all()?;\n", - """ OpenOptions::new()\n .write(true)\n .open(destination)\n .with_context(|| {\n format!(\n \"opening copied tool executable `{}` for synchronization\",\n portable_display(destination)\n )\n })?\n .sync_all()\n .with_context(|| {\n format!(\n \"synchronizing copied tool executable `{}`\",\n portable_display(destination)\n )\n })?;\n""", - "read-only copied-executable sync handle", - ), - ( - """ let mut lock = EnvironmentLock::default();\n lock.plan_digest_sha256 = \"a\".repeat(64);\n assert_eq!(select_exact_target(&lock, \"x86_64-linux\").unwrap().len(), 0);\n""", - """ let lock = EnvironmentLock {\n plan_digest_sha256: \"a\".repeat(64),\n ..EnvironmentLock::default()\n };\n assert_eq!(select_exact_target(&lock, \"x86_64-linux\").unwrap().len(), 0);\n""", - "Clippy-rejected EnvironmentLock fixture", - ), - ] - for old, new, label in replacements: - if text.count(old) != 1: - raise SystemExit(f"expected exactly one {label}") - text = text.replace(old, new, 1) - - marker = """ #[test]\n fn profile_state_is_secret_free_by_shape() {\n let state = ToolProfileState {\n schema: TOOL_PROFILE_SCHEMA_V1.to_string(),\n lock_sha256: \"a\".repeat(64),\n target: \"x86_64-linux\".to_string(),\n tools: Vec::new(),\n };\n let json = serde_json::to_string(&state).unwrap();\n for forbidden in [\"locator\", \"url\", \"token\", \"credential\", \"environment\"] {\n assert!(!json.contains(forbidden));\n }\n }\n}\n""" - replacement = marker[:-2] + """\n\n #[cfg(windows)]\n #[test]\n fn copied_windows_executable_is_opened_with_sync_capable_access() {\n let root = tempfile::tempdir().unwrap();\n let source = root.path().join(\"hello.cmd\");\n let destination = root.path().join(\"hello-copy.cmd\");\n fs::write(&source, b\"@echo off\\r\\necho hello\\r\\n\").unwrap();\n\n materialize_executable(&source, &destination).unwrap();\n\n assert_eq!(fs::read(destination).unwrap(), fs::read(source).unwrap());\n }\n}\n""" - if text.count(marker) != 1: - raise SystemExit("expected exact tool-profile unit-test tail") - path.write_text(text.replace(marker, replacement, 1), encoding="utf-8") - PY - - cargo fmt --all - actual="$(sha256sum src/tool_profile.rs | cut -d' ' -f1)" - test "$actual" = "$EXPECTED_FORMATTED_SHA256" - git diff --check - - - name: Re-run exact Windows product contracts - shell: bash - run: | - set -euo pipefail - cargo fmt --all --check - cargo test --locked --lib tool_profile::tests -- --nocapture --test-threads=1 - cargo test --locked --test tool_profile_cli -- --nocapture --test-threads=1 - cargo clippy --locked --all-targets -- -D warnings - - - name: Commit only the certified product file - shell: bash - run: | - set -euo pipefail - git rm .github/workflows/materialize-den-2923-windows-fix.yml - changed="$(git diff --name-only HEAD)" - expected=$'.github/workflows/materialize-den-2923-windows-fix.yml\nsrc/tool_profile.rs' - test "$changed" = "$expected" - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - git add src/tool_profile.rs - git commit -m "fix(DEN-2923): synchronize copied Windows tool executables" - git push origin HEAD:feat/den-2923-offline-tool-profile diff --git a/src/tool_profile.rs b/src/tool_profile.rs index be7542d1..601e1158 100644 --- a/src/tool_profile.rs +++ b/src/tool_profile.rs @@ -9,7 +9,9 @@ use std::collections::{BTreeMap, BTreeSet}; use std::env; -use std::fs::{self, File, OpenOptions}; +#[cfg(unix)] +use std::fs::File; +use std::fs::{self, OpenOptions}; use std::io::Write; use std::path::{Component, Path, PathBuf}; @@ -685,7 +687,22 @@ fn materialize_executable(source: &Path, destination: &Path) -> Result<()> { portable_display(destination) ) })?; - File::open(destination)?.sync_all()?; + OpenOptions::new() + .write(true) + .open(destination) + .with_context(|| { + format!( + "opening copied tool executable `{}` for synchronization", + portable_display(destination) + ) + })? + .sync_all() + .with_context(|| { + format!( + "synchronizing copied tool executable `{}`", + portable_display(destination) + ) + })?; } Ok(()) } @@ -905,8 +922,10 @@ mod tests { #[test] fn exact_target_selection_rejects_missing_and_multiple_variants() { - let mut lock = EnvironmentLock::default(); - lock.plan_digest_sha256 = "a".repeat(64); + let lock = EnvironmentLock { + plan_digest_sha256: "a".repeat(64), + ..EnvironmentLock::default() + }; assert_eq!(select_exact_target(&lock, "x86_64-linux").unwrap().len(), 0); } @@ -923,4 +942,17 @@ mod tests { assert!(!json.contains(forbidden)); } } + + #[cfg(windows)] + #[test] + fn copied_windows_executable_is_opened_with_sync_capable_access() { + let root = tempfile::tempdir().unwrap(); + let source = root.path().join("hello.cmd"); + let destination = root.path().join("hello-copy.cmd"); + fs::write(&source, b"@echo off\r\necho hello\r\n").unwrap(); + + materialize_executable(&source, &destination).unwrap(); + + assert_eq!(fs::read(destination).unwrap(), fs::read(source).unwrap()); + } } From 77a19fe3be0520b29e0c2fef2072518a0f6d043b Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Sat, 8 Aug 2026 10:43:13 -0500 Subject: [PATCH 16/16] chore(DEN-2923): re-emit certified candidate for exact-head CI