use std::ffi::{OsStr, OsString};
use std::fs;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use chrono::Utc;
#[cfg(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
))]
use fs4::FileExt as Fs4FileExt;
use serde::{Deserialize, Serialize};
pub const CAPABILITIES_SCHEMA_V1: &str = "ee.doctor.capabilities.v1";
pub const ACTION_LINE_SCHEMA_V1: &str = "ee.doctor.action.v1";
pub const RUN_STATE_SCHEMA_V2: &str = "ee.doctor.run_state.v2";
const DOCTOR_LOCK_FILE_MARKER: &str = "ee.doctor.persistent_lock.v1\n";
static DOCTOR_RUN_ID_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
static DOCTOR_STATE_FILE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[cfg(test)]
std::thread_local! {
static DOCTOR_LOCK_BEFORE_UNLOCK_HOOK:
std::cell::RefCell<Option<Box<dyn FnOnce()>>> =
const { std::cell::RefCell::new(None) };
static DOCTOR_LOCK_FAIL_NEXT_WRITE: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
}
#[derive(Debug)]
pub enum DoctorRuntimeError {
BlastRadiusExceeded {
path: PathBuf,
allowed_roots: Vec<PathBuf>,
},
ConcurrencyLost {
lock_path: PathBuf,
holder_run_id: Option<String>,
},
BackupDirUnwritable { dir: PathBuf, source: io::Error },
SymlinkedRunRoot { path: PathBuf },
LifecycleRootChanged { path: PathBuf },
UnsafeLatestEntry {
path: PathBuf,
observed_kind: String,
},
Io { context: String, source: io::Error },
ActionsLogCorrupt { line_number: usize, reason: String },
InvalidRunId { run_id: String, reason: String },
RunArtifactInvalid { path: PathBuf, reason: String },
InvalidBlastRadius { reason: String },
DryRunNotUndoable { run_id: String },
UndoStateDrifted {
path: PathBuf,
expected_hash: String,
observed_hash: String,
},
UndoBackupCorrupt {
backup_path: PathBuf,
expected_hash: String,
observed_hash: Option<String>,
},
FinishStateUpdateFailed {
finish_error: Box<DoctorRuntimeError>,
state_error: Box<DoctorRuntimeError>,
},
NoOpIdempotent,
}
impl std::fmt::Display for DoctorRuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BlastRadiusExceeded {
path,
allowed_roots,
} => write!(
f,
"doctor refused write to {}: outside blast radius (allowed roots: {})",
path.display(),
allowed_roots
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
),
Self::ConcurrencyLost {
lock_path,
holder_run_id,
} => match holder_run_id {
Some(rid) => write!(
f,
"doctor lock held at {} (run_id={}); refusing concurrent fix",
lock_path.display(),
rid
),
None => write!(
f,
"doctor lock held at {} (holder unknown); refusing concurrent fix",
lock_path.display()
),
},
Self::BackupDirUnwritable { dir, source } => write!(
f,
"cannot write doctor backups under {}: {}",
dir.display(),
source
),
Self::SymlinkedRunRoot { path } => write!(
f,
"doctor refused lifecycle path {}: symbolic-link components are not allowed",
path.display()
),
Self::LifecycleRootChanged { path } => write!(
f,
"doctor refused lifecycle path {}: it no longer resolves to the directory opened at run start",
path.display()
),
Self::UnsafeLatestEntry {
path,
observed_kind,
} => write!(
f,
"doctor refused to replace {}: expected a symbolic link or an absent entry, observed {}",
path.display(),
observed_kind
),
Self::Io { context, source } => {
write!(f, "doctor I/O failure ({}): {}", context, source)
}
Self::ActionsLogCorrupt {
line_number,
reason,
} => write!(
f,
"actions.jsonl corrupt at line {}: {}",
line_number, reason
),
Self::InvalidRunId { run_id, reason } => {
write!(f, "invalid doctor run id {run_id:?}: {reason}")
}
Self::RunArtifactInvalid { path, reason } => write!(
f,
"doctor run artifact {} is invalid: {}",
path.display(),
reason
),
Self::InvalidBlastRadius { reason } => {
write!(f, "invalid doctor blast radius: {reason}")
}
Self::DryRunNotUndoable { run_id } => write!(
f,
"doctor run {run_id:?} is a dry-run plan and has no filesystem mutations to undo"
),
Self::UndoStateDrifted {
path,
expected_hash,
observed_hash,
} => write!(
f,
"undo: {} drifted after the doctor run (expected after_hash={}, observed={})",
path.display(),
expected_hash,
observed_hash
),
Self::UndoBackupCorrupt {
backup_path,
expected_hash,
observed_hash,
} => match observed_hash {
Some(h) => write!(
f,
"undo: backup at {} hash mismatch (expected before_hash={}, observed={})",
backup_path.display(),
expected_hash,
h
),
None => write!(
f,
"undo: backup at {} missing (expected before_hash={})",
backup_path.display(),
expected_hash
),
},
Self::FinishStateUpdateFailed {
finish_error,
state_error,
} => write!(
f,
"doctor finalization failed ({finish_error}); additionally failed to persist terminal run state ({state_error})"
),
Self::NoOpIdempotent => {
write!(f, "idempotent no-op: target already in desired state")
}
}
}
}
impl std::error::Error for DoctorRuntimeError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DoctorRuntimeFailureClass {
Configuration,
Storage,
Policy,
}
impl DoctorRuntimeError {
#[must_use]
pub const fn code(&self) -> &'static str {
match self {
Self::BlastRadiusExceeded { .. } => "doctor_blast_radius_exceeded",
Self::ConcurrencyLost { .. } => "doctor_concurrency_lost",
Self::BackupDirUnwritable { .. } => "doctor_backup_directory_unwritable",
Self::SymlinkedRunRoot { .. } => "doctor_run_root_symlink_refused",
Self::LifecycleRootChanged { .. } => "doctor_run_root_changed",
Self::UnsafeLatestEntry { .. } => "doctor_latest_entry_unsafe",
Self::Io { .. } => "doctor_runtime_io",
Self::ActionsLogCorrupt { .. } => "doctor_actions_log_corrupt",
Self::InvalidRunId { .. } => "doctor_run_id_invalid",
Self::RunArtifactInvalid { .. } => "doctor_run_artifact_invalid",
Self::InvalidBlastRadius { .. } => "doctor_blast_radius_env_invalid",
Self::DryRunNotUndoable { .. } => "doctor_dry_run_not_undoable",
Self::UndoStateDrifted { .. } => "doctor_undo_state_drifted",
Self::UndoBackupCorrupt { .. } => "doctor_undo_backup_corrupt",
Self::FinishStateUpdateFailed { .. } => "doctor_finish_state_update_failed",
Self::NoOpIdempotent => "doctor_runtime_noop",
}
}
#[must_use]
pub const fn failure_class(&self) -> DoctorRuntimeFailureClass {
match self {
Self::BlastRadiusExceeded { .. }
| Self::SymlinkedRunRoot { .. }
| Self::LifecycleRootChanged { .. }
| Self::UnsafeLatestEntry { .. } => DoctorRuntimeFailureClass::Policy,
Self::ConcurrencyLost { .. }
| Self::InvalidBlastRadius { .. }
| Self::DryRunNotUndoable { .. }
| Self::NoOpIdempotent => DoctorRuntimeFailureClass::Configuration,
Self::BackupDirUnwritable { .. }
| Self::Io { .. }
| Self::ActionsLogCorrupt { .. }
| Self::InvalidRunId { .. }
| Self::RunArtifactInvalid { .. }
| Self::UndoStateDrifted { .. }
| Self::UndoBackupCorrupt { .. }
| Self::FinishStateUpdateFailed { .. } => DoctorRuntimeFailureClass::Storage,
}
}
}
impl From<io::Error> for DoctorRuntimeError {
fn from(source: io::Error) -> Self {
Self::Io {
context: "underlying I/O".into(),
source,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Op {
WriteFile { bytes: Vec<u8> },
CreateDirAll { mode: u32 },
Chmod { mode: u32 },
QuarantineByRename { dest_under_quarantine: PathBuf },
Manual { steps: Vec<String> },
EmitDiagnostic { code: String, severity: String },
RunIndexRebuild { steps: Vec<String> },
RunGraphRefresh { steps: Vec<String> },
RunWalCheckpoint { mode: String, steps: Vec<String> },
RunMigration {
target_version: String,
steps: Vec<String>,
},
RewriteJsonl {
row_count: usize,
steps: Vec<String>,
},
AtomicRewriteToml { steps: Vec<String> },
SnapshotBackup { label: String, steps: Vec<String> },
}
impl Op {
#[must_use]
pub const fn is_writing(&self) -> bool {
matches!(
self,
Self::WriteFile { .. }
| Self::Chmod { .. }
| Self::QuarantineByRename { .. }
| Self::CreateDirAll { .. }
)
}
#[must_use]
pub const fn is_advisory(&self) -> bool {
matches!(
self,
Self::Manual { .. }
| Self::EmitDiagnostic { .. }
| Self::RunIndexRebuild { .. }
| Self::RunGraphRefresh { .. }
| Self::RunWalCheckpoint { .. }
| Self::RunMigration { .. }
| Self::RewriteJsonl { .. }
| Self::AtomicRewriteToml { .. }
| Self::SnapshotBackup { .. }
)
}
#[must_use]
pub const fn kind_str(&self) -> &'static str {
match self {
Self::WriteFile { .. } => "write_file",
Self::CreateDirAll { .. } => "create_dir_all",
Self::Chmod { .. } => "chmod",
Self::QuarantineByRename { .. } => "quarantine_by_rename",
Self::Manual { .. } => "manual",
Self::EmitDiagnostic { .. } => "emit_diagnostic",
Self::RunIndexRebuild { .. } => "run_index_rebuild",
Self::RunGraphRefresh { .. } => "run_graph_refresh",
Self::RunWalCheckpoint { .. } => "run_wal_checkpoint",
Self::RunMigration { .. } => "run_migration",
Self::RewriteJsonl { .. } => "rewrite_jsonl",
Self::AtomicRewriteToml { .. } => "atomic_rewrite_toml",
Self::SnapshotBackup { .. } => "snapshot_backup",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ActionLine {
pub schema: String,
pub run_id: String,
pub sequence: u64,
pub path: PathBuf,
pub kind: String,
pub before_hash: Option<String>,
pub after_hash: Option<String>,
pub backup_rel_path: Option<PathBuf>,
pub before_mode: Option<u32>,
pub after_mode: Option<u32>,
pub quarantine_dest_rel: Option<PathBuf>,
pub committed_at: String,
pub notes: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunState {
pub schema: String,
pub run_id: String,
pub target_sha: String,
pub workspace: PathBuf,
pub started_at: String,
pub finished_at: Option<String>,
pub status: RunStatus,
pub action_count: u64,
pub dry_run: bool,
pub blast_radius_roots: Vec<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunStatus {
Running,
CompletedOk,
CompletedPartial,
Failed,
Undone,
UndonePartial,
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[derive(Debug)]
struct DoctorLifecycleHandles {
workspace_dir: fs::File,
ee_dir: fs::File,
lock_file: fs::File,
doctor_dir: fs::File,
runs_dir: fs::File,
run_dir: fs::File,
backups_dir: fs::File,
quarantine_dir: fs::File,
}
#[cfg(windows)]
#[derive(Debug)]
struct DoctorLifecycleHandles {
lock_file: fs::File,
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
)))]
#[derive(Debug)]
struct DoctorLifecycleHandles;
#[derive(Debug)]
pub struct RunContext {
run_id: String,
workspace: PathBuf,
run_dir: PathBuf,
lock_path: PathBuf,
lifecycle: DoctorLifecycleHandles,
state: RunState,
actions_handle: Option<fs::File>,
blast_radius_roots: Vec<PathBuf>,
dry_run: bool,
lock_owned: bool,
}
impl Drop for RunContext {
fn drop(&mut self) {
if self.lock_owned {
let _ = release_doctor_lock(&self.lifecycle, &self.lock_path);
}
}
}
impl RunContext {
pub fn start(
workspace: &Path,
target_sha: &str,
blast_radius_roots: Vec<PathBuf>,
dry_run: bool,
) -> Result<Self, DoctorRuntimeError> {
let workspace_buf = canonical_doctor_workspace(workspace)?;
let workspace = workspace_buf.as_path();
let blast_radius_roots =
normalize_blast_radius_roots(workspace, &blast_radius_roots, true)?;
let run_id = derive_run_id(target_sha);
let started_at = Utc::now().to_rfc3339();
let ee_dir = workspace.join(".ee");
let lock_path = ee_dir.join(".doctor.lock");
let doctor_dir = workspace.join(".doctor");
let runs_dir = doctor_dir.join("runs");
let run_dir = runs_dir.join(&run_id);
let backups_dir = run_dir.join("backups");
let quarantine_dir = run_dir.join("quarantine");
let actions_path = run_dir.join("actions.jsonl");
let state_path = run_dir.join("state.json");
validate_doctor_lifecycle_paths([
workspace,
ee_dir.as_path(),
lock_path.as_path(),
doctor_dir.as_path(),
runs_dir.as_path(),
run_dir.as_path(),
backups_dir.as_path(),
quarantine_dir.as_path(),
actions_path.as_path(),
state_path.as_path(),
])?;
let (lifecycle, actions_handle) =
prepare_doctor_lifecycle(workspace, &run_id, &lock_path, &run_dir)?;
if let Err(error) = ensure_doctor_lifecycle_bindings(&lifecycle, workspace, &run_dir) {
let _ = release_doctor_lock(&lifecycle, &lock_path);
return Err(error);
}
let state = RunState {
schema: RUN_STATE_SCHEMA_V2.into(),
run_id: run_id.clone(),
target_sha: target_sha.into(),
workspace: workspace.to_path_buf(),
started_at,
finished_at: None,
status: RunStatus::Running,
action_count: 0,
dry_run,
blast_radius_roots: blast_radius_roots.clone(),
};
if let Err(e) = write_lifecycle_state(&lifecycle, &run_dir, &state) {
let _ = release_doctor_lock(&lifecycle, &lock_path);
return Err(e);
}
Ok(Self {
run_id,
workspace: workspace.to_path_buf(),
run_dir,
lock_path,
lifecycle,
state,
actions_handle: Some(actions_handle),
blast_radius_roots,
dry_run,
lock_owned: true,
})
}
#[must_use]
pub fn run_id(&self) -> &str {
&self.run_id
}
#[must_use]
pub fn run_dir(&self) -> &Path {
&self.run_dir
}
#[must_use]
pub const fn dry_run(&self) -> bool {
self.dry_run
}
pub fn finish(mut self, status: RunStatus) -> Result<RunSummary, DoctorRuntimeError> {
let finish_result = (|| {
ensure_doctor_lifecycle_bindings(&self.lifecycle, &self.workspace, &self.run_dir)?;
self.state.finished_at = Some(Utc::now().to_rfc3339());
self.state.status = status.clone();
write_lifecycle_state(&self.lifecycle, &self.run_dir, &self.state)?;
if let Some(mut h) = self.actions_handle.take() {
h.flush().map_err(|source| DoctorRuntimeError::Io {
context: "flush actions.jsonl".into(),
source,
})?;
}
let latest_link = self.workspace.join(".doctor").join("latest");
ensure_doctor_lifecycle_bindings(&self.lifecycle, &self.workspace, &self.run_dir)?;
publish_doctor_latest(&self.lifecycle, &self.run_id, &self.run_dir, &latest_link)?;
ensure_doctor_lifecycle_bindings(&self.lifecycle, &self.workspace, &self.run_dir)?;
release_doctor_lock(&self.lifecycle, &self.lock_path).map_err(|source| {
DoctorRuntimeError::Io {
context: format!("release doctor lock {}", self.lock_path.display()),
source,
}
})?;
self.lock_owned = false;
Ok(())
})();
if let Err(finish_error) = finish_result {
self.state
.finished_at
.get_or_insert_with(|| Utc::now().to_rfc3339());
self.state.status = RunStatus::Failed;
if let Err(state_error) =
write_lifecycle_state(&self.lifecycle, &self.run_dir, &self.state)
{
return Err(DoctorRuntimeError::FinishStateUpdateFailed {
finish_error: Box::new(finish_error),
state_error: Box::new(state_error),
});
}
return Err(finish_error);
}
Ok(RunSummary {
run_id: self.run_id.clone(),
run_dir: self.run_dir.clone(),
action_count: self.state.action_count,
status,
})
}
}
#[derive(Clone, Debug)]
pub struct RunSummary {
pub run_id: String,
pub run_dir: PathBuf,
pub action_count: u64,
pub status: RunStatus,
}
pub fn mutate(ctx: &mut RunContext, path: &Path, op: Op) -> Result<ActionLine, DoctorRuntimeError> {
ensure_doctor_lifecycle_bindings(&ctx.lifecycle, &ctx.workspace, &ctx.run_dir)?;
if op.is_writing() && !path.is_absolute() {
return Err(DoctorRuntimeError::BlastRadiusExceeded {
path: path.to_path_buf(),
allowed_roots: ctx.blast_radius_roots.clone(),
});
}
if op.is_writing() && !is_path_in_blast_radius(path, &ctx.blast_radius_roots) {
return Err(DoctorRuntimeError::BlastRadiusExceeded {
path: path.to_path_buf(),
allowed_roots: ctx.blast_radius_roots.clone(),
});
}
let before_hash = if path.exists() && path.is_file() {
Some(hash_file(path)?)
} else {
None
};
let before_mode = read_mode(path);
match &op {
Op::WriteFile { bytes } => {
if let Some(existing) = before_hash.as_deref() {
if hash_bytes(bytes) == existing {
return Err(DoctorRuntimeError::NoOpIdempotent);
}
}
}
Op::Chmod { mode } => {
#[cfg(unix)]
{
let cur = before_mode.map(|m| m & 0o7777);
let want = *mode & 0o7777;
if cur == Some(want) {
return Err(DoctorRuntimeError::NoOpIdempotent);
}
}
}
Op::CreateDirAll { .. } => {
if path.is_dir() {
return Err(DoctorRuntimeError::NoOpIdempotent);
}
}
Op::QuarantineByRename {
dest_under_quarantine,
} => {
if !path.exists() {
return Err(DoctorRuntimeError::NoOpIdempotent);
}
validate_relative_quarantine_dest(dest_under_quarantine, &ctx.run_dir)?;
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
{
let dest = ctx.run_dir.join("quarantine").join(dest_under_quarantine);
if dest.exists() {
return Err(DoctorRuntimeError::Io {
context: format!(
"quarantine destination already exists: {}",
dest.display()
),
source: io::Error::new(
io::ErrorKind::AlreadyExists,
"quarantine destination collision",
),
});
}
}
}
_ => {}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
let quarantine_destination = match &op {
Op::QuarantineByRename {
dest_under_quarantine,
} => {
let destination = prepare_doctor_relative_destination(
&ctx.lifecycle.quarantine_dir,
&ctx.run_dir.join("quarantine"),
dest_under_quarantine,
)?;
if doctor_entry_type_at(&destination.parent, destination.leaf.as_os_str())?.is_some() {
return Err(DoctorRuntimeError::Io {
context: format!(
"quarantine destination already exists: {}",
destination.display_path.display()
),
source: io::Error::new(
io::ErrorKind::AlreadyExists,
"quarantine destination collision",
),
});
}
Some(destination)
}
_ => None,
};
let backup_rel_path = if op.is_writing() && path.is_file() {
Some(stage_backup(ctx, path, &before_hash)?)
} else {
None
};
let mut after_hash: Option<String> = None;
let mut after_mode: Option<u32> = None;
let mut quarantine_dest_rel: Option<PathBuf> = None;
let mut notes: Option<String> = None;
match &op {
Op::WriteFile { bytes } => {
if !ctx.dry_run {
write_file_atomic(path, bytes).map_err(|source| DoctorRuntimeError::Io {
context: format!("WriteFile({})", path.display()),
source,
})?;
after_hash = Some(hash_file(path)?);
} else {
after_hash = Some(hash_bytes(bytes));
}
}
Op::Chmod { mode } => {
#[cfg(unix)]
{
if !ctx.dry_run {
use std::os::unix::fs::PermissionsExt as _;
let perms = fs::Permissions::from_mode(*mode);
fs::set_permissions(path, perms).map_err(|source| DoctorRuntimeError::Io {
context: format!("Chmod({})", path.display()),
source,
})?;
}
}
#[cfg(not(unix))]
{
let _ = path;
}
after_mode = Some(*mode & 0o7777);
after_hash = before_hash.clone();
}
Op::CreateDirAll { mode: _ } => {
if !ctx.dry_run {
fs::create_dir_all(path).map_err(|source| DoctorRuntimeError::Io {
context: format!("CreateDirAll({})", path.display()),
source,
})?;
}
}
Op::QuarantineByRename {
dest_under_quarantine,
} => {
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
{
use rustix::fs::RenameFlags;
let destination =
quarantine_destination
.as_ref()
.ok_or_else(|| DoctorRuntimeError::Io {
context: "prepare descriptor-anchored quarantine destination".into(),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"quarantine destination was not prepared",
),
})?;
ensure_doctor_lifecycle_bindings(&ctx.lifecycle, &ctx.workspace, &ctx.run_dir)?;
if !ctx.dry_run {
rustix::fs::renameat_with(
rustix::fs::CWD,
path,
&destination.parent,
destination.leaf.as_os_str(),
RenameFlags::NOREPLACE,
)
.map_err(|source| {
if source == rustix::io::Errno::EXIST {
DoctorRuntimeError::Io {
context: format!(
"quarantine destination already exists: {}",
destination.display_path.display()
),
source: io::Error::new(
io::ErrorKind::AlreadyExists,
"quarantine destination collision",
),
}
} else {
doctor_lifecycle_errno(
&destination.display_path,
&format!("rename {} into quarantine", path.display()),
source,
)
}
})?;
}
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
{
let dest = ctx.run_dir.join("quarantine").join(dest_under_quarantine);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).map_err(|source| DoctorRuntimeError::Io {
context: format!("create_dir_all({}) for quarantine", parent.display()),
source,
})?;
}
if !ctx.dry_run {
fs::rename(path, &dest).map_err(|source| DoctorRuntimeError::Io {
context: format!("rename({} -> {})", path.display(), dest.display()),
source,
})?;
}
}
quarantine_dest_rel = Some(dest_under_quarantine.clone());
after_hash = None;
}
Op::Manual { steps } => {
notes = Some(steps.join(" ; "));
}
Op::EmitDiagnostic { code, severity } => {
notes = Some(format!("{} severity={}", code, severity));
}
Op::RunIndexRebuild { steps } => {
notes = Some(format!("run_index_rebuild: {}", steps.join(" ; ")));
}
Op::RunGraphRefresh { steps } => {
notes = Some(format!("run_graph_refresh: {}", steps.join(" ; ")));
}
Op::RunWalCheckpoint { mode, steps } => {
notes = Some(format!(
"run_wal_checkpoint mode={} steps={}",
mode,
steps.join(" ; ")
));
}
Op::RunMigration {
target_version,
steps,
} => {
notes = Some(format!(
"run_migration target={} steps={}",
target_version,
steps.join(" ; ")
));
}
Op::RewriteJsonl { row_count, steps } => {
notes = Some(format!(
"rewrite_jsonl rows={} steps={}",
row_count,
steps.join(" ; ")
));
}
Op::AtomicRewriteToml { steps } => {
notes = Some(format!("atomic_rewrite_toml: {}", steps.join(" ; ")));
}
Op::SnapshotBackup { label, steps } => {
notes = Some(format!(
"snapshot_backup label={} steps={}",
label,
steps.join(" ; ")
));
}
}
let proposed_seq = ctx.state.action_count + 1;
let line = ActionLine {
schema: ACTION_LINE_SCHEMA_V1.into(),
run_id: ctx.run_id.clone(),
sequence: proposed_seq,
path: path.to_path_buf(),
kind: op.kind_str().into(),
before_hash,
after_hash,
backup_rel_path,
before_mode,
after_mode,
quarantine_dest_rel,
committed_at: Utc::now().to_rfc3339(),
notes,
};
if let Some(handle) = ctx.actions_handle.as_mut() {
let json = serde_json::to_string(&line).map_err(|e| DoctorRuntimeError::Io {
context: "serialize ActionLine".into(),
source: io::Error::new(io::ErrorKind::InvalidData, e),
})?;
writeln!(handle, "{}", json).map_err(|source| DoctorRuntimeError::Io {
context: "append actions.jsonl".into(),
source,
})?;
handle.flush().map_err(|source| DoctorRuntimeError::Io {
context: "flush actions.jsonl".into(),
source,
})?;
}
ctx.state.action_count = proposed_seq;
write_lifecycle_state(&ctx.lifecycle, &ctx.run_dir, &ctx.state)?;
ensure_doctor_lifecycle_bindings(&ctx.lifecycle, &ctx.workspace, &ctx.run_dir)?;
Ok(line)
}
pub fn replay_undo(run_dir: &Path) -> Result<UndoSummary, DoctorRuntimeError> {
let absolute = if run_dir.is_absolute() {
run_dir.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(run_dir))
.map_err(|source| DoctorRuntimeError::Io {
context: format!("resolve relative doctor run {}", run_dir.display()),
source,
})?
};
validate_doctor_lifecycle_paths([absolute.as_path()])?;
let run_id = absolute
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| DoctorRuntimeError::InvalidRunId {
run_id: absolute.display().to_string(),
reason: "run directory has no UTF-8 leaf identifier".into(),
})?;
validate_doctor_run_id(run_id)?;
let runs_dir = absolute
.parent()
.filter(|path| path.file_name() == Some(OsStr::new("runs")))
.ok_or_else(|| DoctorRuntimeError::RunArtifactInvalid {
path: absolute.clone(),
reason: "run directory must be a direct child of .doctor/runs".into(),
})?;
let doctor_dir = runs_dir
.parent()
.filter(|path| path.file_name() == Some(OsStr::new(".doctor")))
.ok_or_else(|| DoctorRuntimeError::RunArtifactInvalid {
path: absolute.clone(),
reason: "run directory must be a direct child of .doctor/runs".into(),
})?;
let workspace = doctor_dir
.parent()
.ok_or_else(|| DoctorRuntimeError::RunArtifactInvalid {
path: absolute.clone(),
reason: "run directory has no workspace parent".into(),
})?;
replay_undo_for_workspace(workspace, run_id)
}
pub fn replay_undo_for_workspace(
workspace: &Path,
run_id: &str,
) -> Result<UndoSummary, DoctorRuntimeError> {
let workspace = canonical_doctor_workspace(workspace)?;
let allowed_roots = blast_radius_roots_from_env(&workspace)
.map_err(|reason| DoctorRuntimeError::InvalidBlastRadius { reason })?;
replay_undo_with_authorized_roots(&workspace, run_id, &allowed_roots)
}
pub fn replay_undo_with_authorized_roots(
workspace: &Path,
run_id: &str,
allowed_roots: &[PathBuf],
) -> Result<UndoSummary, DoctorRuntimeError> {
let (run_dir, mut state) = read_doctor_run_state(workspace, run_id)?;
let workspace = canonical_doctor_workspace(workspace)?;
if state.dry_run {
return Err(DoctorRuntimeError::DryRunNotUndoable {
run_id: run_id.to_owned(),
});
}
let allowed_roots = normalize_blast_radius_roots(&workspace, allowed_roots, false)?;
let recorded_roots = validate_recorded_blast_radius_roots(&run_dir, &state.blast_radius_roots)?;
validate_doctor_lifecycle_paths([
run_dir.as_path(),
run_dir.join("state.json").as_path(),
run_dir.join("actions.jsonl").as_path(),
run_dir.join("undo_log.jsonl").as_path(),
run_dir.join("backups").as_path(),
run_dir.join("quarantine").as_path(),
])?;
let _lock_guard = acquire_undo_lock(&workspace)?;
let actions_path = run_dir.join("actions.jsonl");
let raw = read_required_doctor_jsonl_file(&actions_path, "actions.jsonl")?;
let mut lines: Vec<ActionLine> = Vec::new();
for (i, line) in raw.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
let parsed: ActionLine =
serde_json::from_str(line).map_err(|e| DoctorRuntimeError::ActionsLogCorrupt {
line_number: i + 1,
reason: e.to_string(),
})?;
lines.push(parsed);
}
let observed_action_count =
validate_undo_action_ledger(&run_dir, &state, &recorded_roots, &allowed_roots, &lines)?;
let undo_log_path = run_dir.join("undo_log.jsonl");
let already_undone_sequences =
match read_optional_doctor_jsonl_file(&undo_log_path, "undo_log.jsonl")? {
Some(raw) => validate_undo_log(&run_dir, &raw, &lines)?,
None => std::collections::HashSet::new(),
};
if observed_action_count != state.action_count {
state.action_count = observed_action_count;
persist_replay_state(&workspace, run_id, &run_dir, &state)?;
}
let mut undone = 0u64;
let mut skipped = 0u64;
let mut undo_log_options = fs::OpenOptions::new();
undo_log_options.create(true).append(true);
configure_doctor_inspect_open_no_follow(&mut undo_log_options);
let mut undo_log =
undo_log_options
.open(&undo_log_path)
.map_err(|source| DoctorRuntimeError::Io {
context: format!("open undo_log.jsonl {}", undo_log_path.display()),
source,
})?;
if !undo_log.metadata()?.is_file() {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path,
reason: "undo log is not a regular file".into(),
});
}
let persist_undo_status = |status: &RunStatus| -> Result<(), DoctorRuntimeError> {
let mut persisted_state = read_state(&run_dir)?;
validate_run_state_binding(&workspace, run_id, &run_dir, &persisted_state)?;
persisted_state.status = status.clone();
persisted_state
.finished_at
.get_or_insert_with(|| Utc::now().to_rfc3339());
persist_replay_state(&workspace, run_id, &run_dir, &persisted_state)
};
for action in lines.iter().rev() {
if already_undone_sequences.contains(&action.sequence) {
skipped += 1;
continue;
}
match undo_one(&run_dir, action) {
Ok(()) => {
undone += 1;
let entry = serde_json::json!({
"schema": "ee.doctor.undo_entry.v1",
"sequence": action.sequence,
"path": action.path.display().to_string(),
"kind": action.kind,
"undone_at": Utc::now().to_rfc3339(),
});
writeln!(undo_log, "{}", entry)?;
}
Err(e) => {
let entry = serde_json::json!({
"schema": "ee.doctor.undo_entry.v1",
"sequence": action.sequence,
"path": action.path.display().to_string(),
"kind": action.kind,
"failed_at": Utc::now().to_rfc3339(),
"error": e.to_string(),
});
writeln!(undo_log, "{}", entry)?;
persist_undo_status(&RunStatus::UndonePartial)?;
return Ok(UndoSummary {
actions_undone: undone,
actions_skipped: skipped,
status: RunStatus::UndonePartial,
first_error: Some(e.to_string()),
first_error_code: Some(e.code()),
first_error_class: Some(e.failure_class()),
});
}
}
}
persist_undo_status(&RunStatus::Undone)?;
Ok(UndoSummary {
actions_undone: undone,
actions_skipped: skipped,
status: RunStatus::Undone,
first_error: None,
first_error_code: None,
first_error_class: None,
})
}
#[derive(Clone, Debug)]
pub struct UndoSummary {
pub actions_undone: u64,
pub actions_skipped: u64,
pub status: RunStatus,
pub first_error: Option<String>,
pub first_error_code: Option<&'static str>,
pub first_error_class: Option<DoctorRuntimeFailureClass>,
}
fn persist_replay_state(
workspace: &Path,
run_id: &str,
run_dir: &Path,
state: &RunState,
) -> Result<(), DoctorRuntimeError> {
validate_run_state_binding(workspace, run_id, run_dir, state)?;
validate_doctor_lifecycle_paths([run_dir, run_dir.join("state.json").as_path()])?;
let bytes = serde_json::to_vec_pretty(state).map_err(|error| DoctorRuntimeError::Io {
context: "serialize RunState".into(),
source: io::Error::new(io::ErrorKind::InvalidData, error),
})?;
write_file_atomic(&run_dir.join("state.json"), &bytes).map_err(|source| {
DoctorRuntimeError::Io {
context: format!("write state.json {}", run_dir.join("state.json").display()),
source,
}
})
}
fn validate_relative_run_artifact_path(
run_dir: &Path,
root_name: &str,
rel: &Path,
) -> Result<(), DoctorRuntimeError> {
if rel.as_os_str().is_empty()
|| rel
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join(root_name).join(rel),
reason: format!("{root_name} path must contain only normal relative components"),
});
}
Ok(())
}
fn validate_undo_action_ledger(
run_dir: &Path,
state: &RunState,
recorded_roots: &[PathBuf],
current_roots: &[PathBuf],
lines: &[ActionLine],
) -> Result<u64, DoctorRuntimeError> {
let run_artifact_roots = [run_dir.to_path_buf()];
let observed_count =
u64::try_from(lines.len()).map_err(|_| DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join("actions.jsonl"),
reason: "action count does not fit in u64".into(),
})?;
let recoverable_append_gap = matches!(state.status, RunStatus::Running | RunStatus::Failed)
&& state.action_count.checked_add(1) == Some(observed_count);
if state.action_count != observed_count && !recoverable_append_gap {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join("actions.jsonl"),
reason: format!(
"state action_count {} does not match {} action lines",
state.action_count, observed_count
),
});
}
for (index, action) in lines.iter().enumerate() {
let line_number = index + 1;
let expected_sequence =
u64::try_from(line_number).map_err(|_| DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: "action sequence does not fit in u64".into(),
})?;
if action.schema != ACTION_LINE_SCHEMA_V1 {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: format!(
"unsupported schema {:?}; expected {ACTION_LINE_SCHEMA_V1:?}",
action.schema
),
});
}
if action.run_id != state.run_id {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: format!(
"action run_id {:?} does not match state run_id {:?}",
action.run_id, state.run_id
),
});
}
if action.sequence != expected_sequence {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: format!(
"action sequence {} is not the expected contiguous sequence {}",
action.sequence, expected_sequence
),
});
}
let mutating = match action.kind.as_str() {
"write_file" | "chmod" | "create_dir_all" | "quarantine_by_rename" => true,
"manual"
| "emit_diagnostic"
| "run_index_rebuild"
| "run_graph_refresh"
| "run_wal_checkpoint"
| "run_migration"
| "rewrite_jsonl"
| "atomic_rewrite_toml"
| "snapshot_backup" => false,
other => {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: format!("unknown action kind: {other}"),
});
}
};
if mutating {
if !action.path.is_absolute() {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: format!(
"mutating action path must be absolute: {}",
action.path.display()
),
});
}
if is_path_in_blast_radius(&action.path, &run_artifact_roots)
|| !is_path_in_blast_radius(&action.path, recorded_roots)
|| !is_path_in_blast_radius(&action.path, current_roots)
{
let mut effective_roots = Vec::new();
for recorded in recorded_roots {
for current in current_roots {
if recorded.starts_with(current) {
effective_roots.push(recorded.clone());
} else if current.starts_with(recorded) {
effective_roots.push(current.clone());
}
}
}
effective_roots.sort();
effective_roots.dedup();
return Err(DoctorRuntimeError::BlastRadiusExceeded {
path: action.path.clone(),
allowed_roots: effective_roots,
});
}
}
if let Some(rel) = action.backup_rel_path.as_deref() {
validate_relative_run_artifact_path(run_dir, "backups", rel)?;
}
if let Some(rel) = action.quarantine_dest_rel.as_deref() {
validate_relative_run_artifact_path(run_dir, "quarantine", rel)?;
}
match action.kind.as_str() {
"write_file" if action.before_hash.is_some() && action.backup_rel_path.is_none() => {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: "write_file action with before_hash requires backup_rel_path".into(),
});
}
"chmod" if action.before_mode.is_none() || action.after_mode.is_none() => {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: "chmod action requires before_mode and after_mode".into(),
});
}
"quarantine_by_rename" if action.quarantine_dest_rel.is_none() => {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number,
reason: "quarantine_by_rename requires quarantine_dest_rel".into(),
});
}
_ => {}
}
}
Ok(observed_count)
}
fn validate_undo_log(
run_dir: &Path,
raw: &str,
actions: &[ActionLine],
) -> Result<std::collections::HashSet<u64>, DoctorRuntimeError> {
let undo_log_path = run_dir.join("undo_log.jsonl");
let mut successful = std::collections::HashSet::new();
for (index, line) in raw.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
let line_number = index + 1;
let entry = serde_json::from_str::<serde_json::Value>(line).map_err(|error| {
DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!("line {line_number} is invalid JSON: {error}"),
}
})?;
if entry.get("schema").and_then(serde_json::Value::as_str)
!= Some("ee.doctor.undo_entry.v1")
{
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!("line {line_number} has an unsupported schema"),
});
}
let sequence = entry
.get("sequence")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!("line {line_number} has no integer sequence"),
})?;
let action = sequence
.checked_sub(1)
.and_then(|offset| usize::try_from(offset).ok())
.and_then(|offset| actions.get(offset))
.filter(|action| action.sequence == sequence)
.ok_or_else(|| DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!("line {line_number} references unknown sequence {sequence}"),
})?;
if successful.contains(&sequence) {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!(
"line {line_number} appears after sequence {sequence} was already completed"
),
});
}
let expected_path = action.path.display().to_string();
if entry.get("path").and_then(serde_json::Value::as_str) != Some(expected_path.as_str())
|| entry.get("kind").and_then(serde_json::Value::as_str) != Some(action.kind.as_str())
{
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!("line {line_number} does not match action sequence {sequence}"),
});
}
let completed = entry.get("undone_at").and_then(serde_json::Value::as_str);
let failed = entry.get("failed_at").and_then(serde_json::Value::as_str);
if completed.is_some() == failed.is_some() {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!(
"line {line_number} must contain exactly one of undone_at or failed_at"
),
});
}
if completed.is_some() {
successful.insert(sequence);
}
}
if let Some(first_success) = successful.iter().min().copied() {
let action_count =
u64::try_from(actions.len()).map_err(|_| DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: "action count does not fit in u64".into(),
})?;
if (first_success..=action_count).any(|sequence| !successful.contains(&sequence)) {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: "successful undo receipts must form a contiguous action suffix".into(),
});
}
let mut final_actions_by_path = std::collections::BTreeMap::new();
for action in actions {
if successful.contains(&action.sequence) && is_mutating_action_kind(&action.kind) {
final_actions_by_path
.entry(action.path.clone())
.or_insert(action);
}
}
for action in final_actions_by_path.values() {
if !action_pre_state_matches(run_dir, action)? {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: undo_log_path.clone(),
reason: format!(
"successful receipt for sequence {} does not match the live post-undo state",
action.sequence
),
});
}
}
}
Ok(successful)
}
fn is_mutating_action_kind(kind: &str) -> bool {
matches!(
kind,
"write_file" | "chmod" | "create_dir_all" | "quarantine_by_rename"
)
}
fn undo_created_quarantine_path(run_dir: &Path, action: &ActionLine, root: &str) -> PathBuf {
run_dir
.join("quarantine")
.join(root)
.join(format!("{:06}", action.sequence))
.join(sanitize_path_for_run_dir(&action.path))
}
fn action_pre_state_matches(
run_dir: &Path,
action: &ActionLine,
) -> Result<bool, DoctorRuntimeError> {
match action.kind.as_str() {
"write_file" => match action.before_hash.as_deref() {
Some(expected) => {
if !action.path.is_file() {
return Ok(false);
}
Ok(hash_file(&action.path)? == expected)
}
None => {
let quarantine = undo_created_quarantine_path(run_dir, action, "undo_created");
validate_doctor_lifecycle_paths([quarantine.as_path()])?;
Ok(!action.path.exists() && quarantine.exists())
}
},
"chmod" => {
#[cfg(unix)]
{
Ok(read_mode(&action.path).map(|mode| mode & 0o7777)
== action.before_mode.map(|mode| mode & 0o7777))
}
#[cfg(not(unix))]
{
Ok(true)
}
}
"create_dir_all" => {
let quarantine = undo_created_quarantine_path(run_dir, action, "undo_created_dirs");
validate_doctor_lifecycle_paths([quarantine.as_path()])?;
Ok(!action.path.exists() && quarantine.exists())
}
"quarantine_by_rename" => {
let source = action
.quarantine_dest_rel
.as_ref()
.map(|rel| run_dir.join("quarantine").join(rel));
if let Some(source) = source.as_deref() {
validate_doctor_lifecycle_paths([source])?;
}
let source_exists = source.as_deref().is_some_and(Path::exists);
let target_matches = match action.before_hash.as_deref() {
Some(expected) if action.path.is_file() => hash_file(&action.path)? == expected,
Some(_) => false,
None => action.path.exists(),
};
Ok(target_matches && !source_exists)
}
"manual"
| "emit_diagnostic"
| "run_index_rebuild"
| "run_graph_refresh"
| "run_wal_checkpoint"
| "run_migration"
| "rewrite_jsonl"
| "atomic_rewrite_toml"
| "snapshot_backup" => Ok(true),
other => Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number: action.sequence as usize,
reason: format!("unknown action kind: {other}"),
}),
}
}
fn undo_one(run_dir: &Path, action: &ActionLine) -> Result<(), DoctorRuntimeError> {
if action_pre_state_matches(run_dir, action)? {
return Ok(());
}
match action.kind.as_str() {
"write_file" => {
match (&action.before_hash, &action.backup_rel_path) {
(Some(expected_before), Some(rel)) => {
if action.path.exists() {
let live_hash = if action.path.is_file() {
hash_file(&action.path)?
} else {
"<directory>".to_string()
};
if Some(live_hash.as_str()) != action.after_hash.as_deref() {
return Err(DoctorRuntimeError::UndoStateDrifted {
path: action.path.clone(),
expected_hash: action.after_hash.clone().unwrap_or_default(),
observed_hash: live_hash,
});
}
}
let backup = run_dir.join("backups").join(rel);
validate_doctor_lifecycle_paths([backup.as_path()])?;
if !backup.exists() {
return Err(DoctorRuntimeError::UndoBackupCorrupt {
backup_path: backup,
expected_hash: expected_before.clone(),
observed_hash: None,
});
}
let backup_bytes = read_doctor_backup_bytes(&backup)?;
let backup_hash = hash_bytes(&backup_bytes);
if &backup_hash != expected_before {
return Err(DoctorRuntimeError::UndoBackupCorrupt {
backup_path: backup,
expected_hash: expected_before.clone(),
observed_hash: Some(backup_hash),
});
}
write_file_atomic(&action.path, &backup_bytes)?;
}
(None, _) => {
if action.path.exists() {
let live_hash = if action.path.is_file() {
hash_file(&action.path)?
} else {
"<directory>".to_string()
};
if action.after_hash.as_deref() != Some(live_hash.as_str()) {
return Err(DoctorRuntimeError::UndoStateDrifted {
path: action.path.clone(),
expected_hash: action.after_hash.clone().unwrap_or_default(),
observed_hash: live_hash,
});
}
let quarantine_dest =
undo_created_quarantine_path(run_dir, action, "undo_created");
if let Some(parent) = quarantine_dest.parent() {
fs::create_dir_all(parent)?;
}
if quarantine_dest.exists() {
return Err(DoctorRuntimeError::Io {
context: format!(
"undo quarantine collision at {}",
quarantine_dest.display()
),
source: io::Error::new(
io::ErrorKind::AlreadyExists,
"undo quarantine collision",
),
});
}
fs::rename(&action.path, &quarantine_dest)?;
}
}
(Some(_), None) => {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number: action.sequence as usize,
reason: "write_file action has before_hash but no backup_rel_path".into(),
});
}
}
}
"chmod" => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let current_mode = read_mode(&action.path).map(|mode| mode & 0o7777);
let expected_after = action.after_mode.map(|mode| mode & 0o7777);
if current_mode != expected_after {
return Err(DoctorRuntimeError::UndoStateDrifted {
path: action.path.clone(),
expected_hash: expected_after.map_or_else(
|| "<missing mode>".into(),
|mode| format!("mode {mode:o}"),
),
observed_hash: current_mode
.map_or_else(|| "<missing>".into(), |mode| format!("mode {mode:o}")),
});
}
if let Some(mode) = action.before_mode.map(|mode| mode & 0o7777) {
let perms = fs::Permissions::from_mode(mode);
fs::set_permissions(&action.path, perms)?;
}
}
#[cfg(not(unix))]
{
let _ = action;
}
}
"create_dir_all" => {
if action.path.exists() && !action.path.is_dir() {
return Err(DoctorRuntimeError::UndoStateDrifted {
path: action.path.clone(),
expected_hash: "<empty directory created by doctor>".to_owned(),
observed_hash: "<non-directory occupant>".to_owned(),
});
}
if action.path.is_dir() {
let is_empty = match fs::read_dir(&action.path) {
Ok(mut it) => match it.next() {
Some(Ok(_)) => false,
Some(Err(source)) => {
return Err(DoctorRuntimeError::Io {
context: format!(
"read_dir entry for undo of create_dir_all({})",
action.path.display()
),
source,
});
}
None => true,
},
Err(source) => {
return Err(DoctorRuntimeError::Io {
context: format!(
"read_dir for undo of create_dir_all({})",
action.path.display()
),
source,
});
}
};
if !is_empty {
return Err(DoctorRuntimeError::UndoStateDrifted {
path: action.path.clone(),
expected_hash: "<empty directory created by doctor>".to_owned(),
observed_hash: "<non-empty directory>".to_owned(),
});
}
let quarantine_dest =
undo_created_quarantine_path(run_dir, action, "undo_created_dirs");
if let Some(parent) = quarantine_dest.parent() {
fs::create_dir_all(parent)?;
}
if quarantine_dest.exists() {
return Err(DoctorRuntimeError::Io {
context: format!(
"undo quarantine collision at {}",
quarantine_dest.display()
),
source: io::Error::new(
io::ErrorKind::AlreadyExists,
"undo quarantine collision",
),
});
}
fs::rename(&action.path, &quarantine_dest)?;
}
}
"quarantine_by_rename" => {
if action.path.exists() {
let live_hash = if action.path.is_file() {
hash_file(&action.path)?
} else {
"<directory>".to_string()
};
return Err(DoctorRuntimeError::UndoStateDrifted {
path: action.path.clone(),
expected_hash: "<not present (quarantined)>".into(),
observed_hash: live_hash,
});
}
let quarantine_dest = action
.quarantine_dest_rel
.as_ref()
.map(|rel| run_dir.join("quarantine").join(rel));
if let Some(source) = quarantine_dest {
validate_doctor_lifecycle_paths([source.as_path()])?;
if source.exists() {
if let Some(parent) = action.path.parent() {
fs::create_dir_all(parent)?;
}
fs::rename(&source, &action.path)?;
}
}
}
"manual"
| "emit_diagnostic"
| "run_index_rebuild"
| "run_graph_refresh"
| "run_wal_checkpoint"
| "run_migration"
| "rewrite_jsonl"
| "atomic_rewrite_toml"
| "snapshot_backup" => {
}
other => {
return Err(DoctorRuntimeError::ActionsLogCorrupt {
line_number: action.sequence as usize,
reason: format!("unknown action kind: {}", other),
});
}
}
Ok(())
}
#[derive(Clone, Debug, Serialize)]
pub struct CapabilitiesReport {
pub schema: String,
pub doctor_version: String,
pub doctor_contract_version: String,
pub tool_version: String,
pub run_artifact_schema: String,
pub blast_radius: Vec<String>,
pub op_kinds: Vec<&'static str>,
pub exit_codes: Vec<ExitCodeEntry>,
pub env_vars: Vec<EnvVarEntry>,
pub action_line_schema: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct ExitCodeEntry {
pub code: i32,
pub name: &'static str,
pub meaning: &'static str,
}
#[derive(Clone, Debug, Serialize)]
pub struct EnvVarEntry {
pub name: &'static str,
pub purpose: &'static str,
}
impl CapabilitiesReport {
#[must_use]
pub fn build(tool_version: &str, workspace: &Path) -> Self {
let blast_radius = default_blast_radius_roots(workspace)
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>();
Self {
schema: CAPABILITIES_SCHEMA_V1.into(),
doctor_version: env!("CARGO_PKG_VERSION").into(),
doctor_contract_version: "1.0.0".into(),
tool_version: tool_version.into(),
run_artifact_schema: RUN_STATE_SCHEMA_V2.into(),
blast_radius,
op_kinds: vec![
"write_file",
"create_dir_all",
"chmod",
"quarantine_by_rename",
"manual",
"emit_diagnostic",
"run_index_rebuild",
"run_graph_refresh",
"run_wal_checkpoint",
"run_migration",
"rewrite_jsonl",
"atomic_rewrite_toml",
"snapshot_backup",
],
exit_codes: vec![
ExitCodeEntry {
code: 0,
name: "ok",
meaning: "no findings, or all fixes applied successfully",
},
ExitCodeEntry {
code: 1,
name: "usage",
meaning: "command-line usage or argument validation failed",
},
ExitCodeEntry {
code: 2,
name: "configuration",
meaning: "configuration, concurrency, or no-op precondition prevented the operation",
},
ExitCodeEntry {
code: 3,
name: "storage",
meaning: "doctor storage, ledger, backup, or state restoration failed",
},
ExitCodeEntry {
code: 4,
name: "search_index",
meaning: "search index operation failed",
},
ExitCodeEntry {
code: 5,
name: "import",
meaning: "import operation failed",
},
ExitCodeEntry {
code: 6,
name: "unsatisfied_degraded_mode",
meaning: "the requested operation cannot proceed in the current degraded mode",
},
ExitCodeEntry {
code: 7,
name: "policy_denied",
meaning: "blast-radius or another safety policy denied the operation",
},
ExitCodeEntry {
code: 8,
name: "migration_required",
meaning: "doctor refuses because `ee migrate run` is needed first",
},
],
env_vars: vec![
EnvVarEntry {
name: "EE_DOCTOR_BLAST_RADIUS",
purpose: "Override default blast radius (colon-separated abs paths)",
},
EnvVarEntry {
name: "EE_NO_COLOR",
purpose: "Disables ANSI styling on stderr (inherited from ee)",
},
],
action_line_schema: ACTION_LINE_SCHEMA_V1.into(),
}
}
}
#[must_use]
pub fn default_blast_radius_roots(workspace: &Path) -> Vec<PathBuf> {
let mut roots = vec![workspace.join(".ee"), workspace.join(".doctor")];
if let Some(home) = std::env::var_os("HOME") {
let home = PathBuf::from(home);
roots.push(home.join(".local").join("share").join("ee"));
}
roots
}
fn normalize_blast_radius_roots(
workspace: &Path,
roots: &[PathBuf],
resolve_relative_from_cwd: bool,
) -> Result<Vec<PathBuf>, DoctorRuntimeError> {
if roots.is_empty() {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: workspace.to_path_buf(),
reason: "doctor blast radius must contain at least one root".into(),
});
}
let mut normalized = Vec::with_capacity(roots.len());
for root in roots {
let absolute = if root.is_absolute() {
root.clone()
} else if resolve_relative_from_cwd {
std::env::current_dir()
.map(|current_dir| current_dir.join(root))
.map_err(|source| DoctorRuntimeError::Io {
context: format!(
"resolve relative doctor blast-radius root {}",
root.display()
),
source,
})?
} else {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: root.clone(),
reason:
"persisted and replay-time doctor blast-radius roots must be absolute paths"
.into(),
});
};
let canonical = if absolute.exists() {
fs::canonicalize(&absolute).map_err(|source| DoctorRuntimeError::Io {
context: format!(
"canonicalize doctor blast-radius root {}",
absolute.display()
),
source,
})?
} else {
nearest_existing_ancestor_canonical(&absolute).ok_or_else(|| {
DoctorRuntimeError::RunArtifactInvalid {
path: absolute.clone(),
reason: "doctor blast-radius root has no safe existing ancestor".into(),
}
})?
};
normalized.push(canonical);
}
normalized.sort();
normalized.dedup();
Ok(normalized)
}
fn validate_recorded_blast_radius_roots(
run_dir: &Path,
roots: &[PathBuf],
) -> Result<Vec<PathBuf>, DoctorRuntimeError> {
if roots.is_empty() {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join("state.json"),
reason: "run state has no recorded blast-radius roots".into(),
});
}
let mut validated = Vec::with_capacity(roots.len());
for root in roots {
if !root.is_absolute()
|| root.components().any(|component| {
!matches!(
component,
Component::Prefix(_) | Component::RootDir | Component::Normal(_)
)
})
{
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join("state.json"),
reason: format!(
"recorded blast-radius root must be an absolute normalized path: {}",
root.display()
),
});
}
validated.push(root.clone());
}
validated.sort();
validated.dedup();
Ok(validated)
}
pub fn blast_radius_roots_from_env(workspace: &Path) -> Result<Vec<PathBuf>, String> {
use crate::config::env_registry::{EnvVar, read};
match read(EnvVar::DoctorBlastRadius) {
Some(raw) if !raw.trim().is_empty() => parse_blast_radius_override(&raw),
_ => Ok(default_blast_radius_roots(workspace)),
}
}
fn parse_blast_radius_override(raw: &str) -> Result<Vec<PathBuf>, String> {
let mut roots = Vec::new();
for entry in raw.split(':') {
let entry = entry.trim();
if entry.is_empty() {
return Err(format!(
"EE_DOCTOR_BLAST_RADIUS contains an empty path segment: {raw:?}"
));
}
let path = PathBuf::from(entry);
if !path.is_absolute() {
return Err(format!(
"EE_DOCTOR_BLAST_RADIUS entries must be absolute paths; got {entry:?}"
));
}
roots.push(path);
}
Ok(roots)
}
fn canonical_doctor_workspace(workspace: &Path) -> Result<PathBuf, DoctorRuntimeError> {
let absolute = if workspace.is_absolute() {
workspace.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(workspace))
.map_err(|source| DoctorRuntimeError::Io {
context: format!("resolve relative doctor workspace {}", workspace.display()),
source,
})?
};
let metadata = fs::symlink_metadata(&absolute).map_err(|source| DoctorRuntimeError::Io {
context: format!("inspect doctor workspace {}", absolute.display()),
source,
})?;
if metadata.file_type().is_symlink() {
return Err(DoctorRuntimeError::SymlinkedRunRoot { path: absolute });
}
if !metadata.is_dir() {
return Err(DoctorRuntimeError::Io {
context: format!("inspect doctor workspace {}", absolute.display()),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"doctor workspace is not a directory",
),
});
}
fs::canonicalize(&absolute).map_err(|source| DoctorRuntimeError::Io {
context: format!("canonicalize doctor workspace {}", absolute.display()),
source,
})
}
const DOCTOR_RUN_ID_MAX_BYTES: usize = 128;
pub fn validate_doctor_run_id(run_id: &str) -> Result<(), DoctorRuntimeError> {
if run_id.is_empty() {
return Err(DoctorRuntimeError::InvalidRunId {
run_id: run_id.to_owned(),
reason: "run id must not be empty".into(),
});
}
if run_id.len() > DOCTOR_RUN_ID_MAX_BYTES {
return Err(DoctorRuntimeError::InvalidRunId {
run_id: run_id.to_owned(),
reason: format!("run id exceeds {DOCTOR_RUN_ID_MAX_BYTES} bytes"),
});
}
if run_id == "." || run_id == ".." {
return Err(DoctorRuntimeError::InvalidRunId {
run_id: run_id.to_owned(),
reason: "dot path components are not run ids".into(),
});
}
if !run_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
{
return Err(DoctorRuntimeError::InvalidRunId {
run_id: run_id.to_owned(),
reason: "allowed characters are ASCII letters, digits, '.', '_', and '-'".into(),
});
}
let mut components = Path::new(run_id).components();
if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
return Err(DoctorRuntimeError::InvalidRunId {
run_id: run_id.to_owned(),
reason: "run id must be exactly one normal path component".into(),
});
}
Ok(())
}
fn resolve_doctor_run_dir(
workspace: &Path,
run_id: &str,
) -> Result<(PathBuf, PathBuf), DoctorRuntimeError> {
validate_doctor_run_id(run_id)?;
let workspace = canonical_doctor_workspace(workspace)?;
let doctor_dir = workspace.join(".doctor");
let runs_dir = doctor_dir.join("runs");
let run_dir = runs_dir.join(run_id);
validate_doctor_lifecycle_paths([
workspace.as_path(),
doctor_dir.as_path(),
runs_dir.as_path(),
run_dir.as_path(),
run_dir.join("state.json").as_path(),
])?;
let metadata = fs::symlink_metadata(&run_dir).map_err(|source| DoctorRuntimeError::Io {
context: format!("inspect doctor run directory {}", run_dir.display()),
source,
})?;
if !metadata.is_dir() {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir,
reason: "run path is not a directory".into(),
});
}
Ok((workspace, run_dir))
}
fn validate_run_state_binding(
workspace: &Path,
run_id: &str,
run_dir: &Path,
state: &RunState,
) -> Result<(), DoctorRuntimeError> {
if state.schema != RUN_STATE_SCHEMA_V2 {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join("state.json"),
reason: format!(
"unsupported schema {:?}; expected {RUN_STATE_SCHEMA_V2:?}",
state.schema
),
});
}
if state.run_id != run_id {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join("state.json"),
reason: format!(
"state run_id {:?} does not match requested run {run_id:?}",
state.run_id
),
});
}
let state_workspace = canonical_doctor_workspace(&state.workspace)?;
if state_workspace != workspace {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: run_dir.join("state.json"),
reason: format!(
"state workspace {} does not match requested workspace {}",
state_workspace.display(),
workspace.display()
),
});
}
Ok(())
}
pub fn read_doctor_run_state(
workspace: &Path,
run_id: &str,
) -> Result<(PathBuf, RunState), DoctorRuntimeError> {
let (workspace, run_dir) = resolve_doctor_run_dir(workspace, run_id)?;
let state = read_state(&run_dir)?;
validate_run_state_binding(&workspace, run_id, &run_dir, &state)?;
Ok((run_dir, state))
}
fn validate_doctor_lifecycle_paths<'a>(
paths: impl IntoIterator<Item = &'a Path>,
) -> Result<(), DoctorRuntimeError> {
for path in paths {
match super::path_safety::first_existing_symlink_component(path) {
Ok(Some(path)) => return Err(DoctorRuntimeError::SymlinkedRunRoot { path }),
Ok(None) => {}
Err(source) => {
return Err(DoctorRuntimeError::Io {
context: format!("inspect doctor lifecycle path {}", path.display()),
source,
});
}
}
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn prepare_doctor_lifecycle(
workspace: &Path,
run_id: &str,
lock_path: &Path,
run_dir: &Path,
) -> Result<(DoctorLifecycleHandles, fs::File), DoctorRuntimeError> {
use rustix::fs::{Mode, OFlags};
let directory_flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
let workspace_fd = rustix::fs::openat(
rustix::fs::CWD,
workspace,
directory_flags,
Mode::from_raw_mode(0),
)
.map(fs::File::from)
.map_err(|source| doctor_lifecycle_errno(workspace, "open canonical workspace", source))?;
let ee_path = workspace.join(".ee");
let ee_dir =
open_or_create_doctor_directory_at(&workspace_fd, OsStr::new(".ee"), &ee_path, true)?;
let lock_file = acquire_doctor_lock_at(&ee_dir, lock_path)?;
let prepared = (|| {
let doctor_path = workspace.join(".doctor");
let doctor_dir = open_or_create_doctor_directory_at(
&workspace_fd,
OsStr::new(".doctor"),
&doctor_path,
true,
)?;
let runs_path = doctor_path.join("runs");
let runs_dir =
open_or_create_doctor_directory_at(&doctor_dir, OsStr::new("runs"), &runs_path, true)?;
let run_dir_fd =
open_or_create_doctor_directory_at(&runs_dir, OsStr::new(run_id), run_dir, false)?;
let backups_dir = open_or_create_doctor_directory_at(
&run_dir_fd,
OsStr::new("backups"),
&run_dir.join("backups"),
false,
)?;
let quarantine_dir = open_or_create_doctor_directory_at(
&run_dir_fd,
OsStr::new("quarantine"),
&run_dir.join("quarantine"),
false,
)?;
let actions_path = run_dir.join("actions.jsonl");
let actions_fd = rustix::fs::openat(
&run_dir_fd,
"actions.jsonl",
OFlags::WRONLY
| OFlags::CREATE
| OFlags::EXCL
| OFlags::APPEND
| OFlags::NOFOLLOW
| OFlags::CLOEXEC,
Mode::from_raw_mode(0o600),
)
.map_err(|source| doctor_lifecycle_errno(&actions_path, "create actions.jsonl", source))?;
Ok((
doctor_dir,
runs_dir,
run_dir_fd,
backups_dir,
quarantine_dir,
fs::File::from(actions_fd),
))
})();
match prepared {
Ok((doctor_dir, runs_dir, run_dir, backups_dir, quarantine_dir, actions_handle)) => Ok((
DoctorLifecycleHandles {
workspace_dir: workspace_fd,
ee_dir,
lock_file,
doctor_dir,
runs_dir,
run_dir,
backups_dir,
quarantine_dir,
},
actions_handle,
)),
Err(error) => {
let _ = Fs4FileExt::unlock(&lock_file);
Err(error)
}
}
}
#[cfg(windows)]
fn prepare_doctor_lifecycle(
workspace: &Path,
_run_id: &str,
lock_path: &Path,
run_dir: &Path,
) -> Result<(DoctorLifecycleHandles, fs::File), DoctorRuntimeError> {
let ee_dir = workspace.join(".ee");
fs::create_dir_all(&ee_dir).map_err(|source| DoctorRuntimeError::Io {
context: format!("create_dir_all({})", ee_dir.display()),
source,
})?;
let lock = acquire_windows_doctor_lock(lock_path)?;
let backups_dir = run_dir.join("backups");
let quarantine_dir = run_dir.join("quarantine");
for dir in [run_dir, backups_dir.as_path(), quarantine_dir.as_path()] {
if let Err(source) = fs::create_dir_all(dir) {
let _ = Fs4FileExt::unlock(&lock);
return Err(DoctorRuntimeError::BackupDirUnwritable {
dir: dir.to_path_buf(),
source,
});
}
}
let actions_path = run_dir.join("actions.jsonl");
let actions_handle = match fs::OpenOptions::new()
.create_new(true)
.append(true)
.open(&actions_path)
{
Ok(handle) => handle,
Err(source) => {
let _ = Fs4FileExt::unlock(&lock);
return Err(DoctorRuntimeError::Io {
context: format!("open actions.jsonl {}", actions_path.display()),
source,
});
}
};
Ok((DoctorLifecycleHandles { lock_file: lock }, actions_handle))
}
#[cfg(windows)]
fn configure_windows_doctor_lock_open_no_follow(options: &mut fs::OpenOptions) {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
#[cfg(windows)]
fn acquire_windows_doctor_lock(lock_path: &Path) -> Result<fs::File, DoctorRuntimeError> {
use std::os::windows::fs::MetadataExt;
if fs::symlink_metadata(lock_path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
return Err(DoctorRuntimeError::SymlinkedRunRoot {
path: lock_path.to_path_buf(),
});
}
let mut create_options = fs::OpenOptions::new();
create_options
.read(true)
.write(true)
.create_new(true)
.truncate(false);
configure_windows_doctor_lock_open_no_follow(&mut create_options);
let (mut lock, created) = match create_options.open(lock_path) {
Ok(lock) => (lock, true),
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {
let mut existing_options = fs::OpenOptions::new();
existing_options.read(true).write(true).truncate(false);
configure_windows_doctor_lock_open_no_follow(&mut existing_options);
let lock =
existing_options
.open(lock_path)
.map_err(|source| DoctorRuntimeError::Io {
context: format!(
"open existing persistent doctor lock {}",
lock_path.display()
),
source,
})?;
(lock, false)
}
Err(source) => {
return Err(DoctorRuntimeError::Io {
context: format!("create persistent doctor lock {}", lock_path.display()),
source,
});
}
};
let metadata = lock.metadata().map_err(|source| DoctorRuntimeError::Io {
context: format!("inspect persistent doctor lock {}", lock_path.display()),
source,
})?;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
if !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(DoctorRuntimeError::Io {
context: format!("inspect persistent doctor lock {}", lock_path.display()),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"doctor lock is not a regular non-reparse file",
),
});
}
acquire_doctor_advisory_lock(&lock, lock_path)?;
if created && let Err(source) = write_doctor_lock_contents(&mut lock, DOCTOR_LOCK_FILE_MARKER) {
let _ = Fs4FileExt::unlock(&lock);
return Err(DoctorRuntimeError::Io {
context: format!("initialize persistent doctor lock {}", lock_path.display()),
source,
});
}
Ok(lock)
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
)))]
fn prepare_doctor_lifecycle(
_workspace: &Path,
_run_id: &str,
lock_path: &Path,
_run_dir: &Path,
) -> Result<(DoctorLifecycleHandles, fs::File), DoctorRuntimeError> {
Err(DoctorRuntimeError::Io {
context: format!("acquire persistent doctor lock {}", lock_path.display()),
source: io::Error::new(
io::ErrorKind::Unsupported,
"doctor mutation is disabled because this platform cannot prove lock ownership",
),
})
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn open_or_create_doctor_directory_at(
parent: &fs::File,
name: &OsStr,
full_path: &Path,
allow_existing: bool,
) -> Result<fs::File, DoctorRuntimeError> {
use rustix::fs::{FileType, Mode, OFlags};
use rustix::io::Errno;
match rustix::fs::mkdirat(parent, name, Mode::from_raw_mode(0o700)) {
Ok(()) => {}
Err(source) if source == Errno::EXIST && allow_existing => {}
Err(source) if source == Errno::EXIST => {
if doctor_entry_type_at(parent, name)? == Some(FileType::Symlink) {
return Err(DoctorRuntimeError::SymlinkedRunRoot {
path: full_path.to_path_buf(),
});
}
return Err(DoctorRuntimeError::Io {
context: format!("create unique doctor directory {}", full_path.display()),
source: io::Error::new(
io::ErrorKind::AlreadyExists,
"doctor run directory already exists",
),
});
}
Err(source) => {
return Err(doctor_lifecycle_errno(
full_path,
"create doctor directory",
source,
));
}
}
rustix::fs::openat(
parent,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::from_raw_mode(0),
)
.map(fs::File::from)
.map_err(|source| doctor_lifecycle_errno(full_path, "open doctor directory", source))
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[derive(Debug)]
struct DoctorRelativeDestination {
parent: fs::File,
leaf: OsString,
display_path: PathBuf,
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn prepare_doctor_relative_destination(
root: &fs::File,
root_path: &Path,
relative: &Path,
) -> Result<DoctorRelativeDestination, DoctorRuntimeError> {
let leaf = relative
.file_name()
.filter(|name| !name.is_empty())
.ok_or_else(|| DoctorRuntimeError::BlastRadiusExceeded {
path: relative.to_path_buf(),
allowed_roots: vec![root_path.to_path_buf()],
})?
.to_os_string();
let parent_relative = relative.parent().unwrap_or_else(|| Path::new(""));
let parent = open_or_create_doctor_relative_directory(root, root_path, parent_relative)?;
Ok(DoctorRelativeDestination {
parent,
leaf,
display_path: root_path.join(relative),
})
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn open_or_create_doctor_relative_directory(
root: &fs::File,
root_path: &Path,
relative: &Path,
) -> Result<fs::File, DoctorRuntimeError> {
let mut directory = root.try_clone().map_err(|source| DoctorRuntimeError::Io {
context: format!(
"duplicate doctor directory descriptor {}",
root_path.display()
),
source,
})?;
let mut display_path = root_path.to_path_buf();
for component in relative.components() {
let Component::Normal(name) = component else {
return Err(DoctorRuntimeError::BlastRadiusExceeded {
path: relative.to_path_buf(),
allowed_roots: vec![root_path.to_path_buf()],
});
};
display_path.push(name);
directory = open_or_create_doctor_directory_at(&directory, name, &display_path, true)?;
}
Ok(directory)
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn acquire_doctor_lock_at(
ee_dir: &fs::File,
lock_path: &Path,
) -> Result<fs::File, DoctorRuntimeError> {
use rustix::fs::{Mode, OFlags};
use rustix::io::Errno;
let create_flags =
OFlags::RDWR | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC;
let (fd, created) = match rustix::fs::openat(
ee_dir,
".doctor.lock",
create_flags,
Mode::from_raw_mode(0o600),
) {
Ok(fd) => (fd, true),
Err(source) if source == Errno::EXIST => {
let fd = rustix::fs::openat(
ee_dir,
".doctor.lock",
OFlags::RDWR | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::from_raw_mode(0),
)
.map_err(|source| {
doctor_lifecycle_errno(lock_path, "open existing persistent doctor lock", source)
})?;
(fd, false)
}
Err(source) => {
return Err(doctor_lifecycle_errno(
lock_path,
"create persistent doctor lock",
source,
));
}
};
let mut lock = fs::File::from(fd);
acquire_doctor_advisory_lock(&lock, lock_path)?;
let initialized = ensure_doctor_lock_binding_at(ee_dir, &lock).and_then(|()| {
if created {
write_doctor_lock_contents(&mut lock, DOCTOR_LOCK_FILE_MARKER)
} else {
Ok(())
}
});
if let Err(source) = initialized {
let _ = Fs4FileExt::unlock(&lock);
return Err(DoctorRuntimeError::Io {
context: format!("initialize persistent doctor lock {}", lock_path.display()),
source,
});
}
Ok(lock)
}
fn acquire_doctor_advisory_lock(
lock_file: &fs::File,
lock_path: &Path,
) -> Result<(), DoctorRuntimeError> {
match Fs4FileExt::try_lock(lock_file) {
Ok(()) => Ok(()),
Err(fs4::TryLockError::WouldBlock) => Err(DoctorRuntimeError::ConcurrencyLost {
lock_path: lock_path.to_path_buf(),
holder_run_id: read_doctor_lock_holder_file(lock_file),
}),
Err(fs4::TryLockError::Error(source)) => Err(DoctorRuntimeError::Io {
context: format!("acquire persistent doctor lock {}", lock_path.display()),
source,
}),
}
}
fn read_doctor_lock_holder_file(lock_file: &fs::File) -> Option<String> {
let metadata = lock_file.metadata().ok()?;
if !metadata.is_file() || metadata.len() > DOCTOR_LOCK_FILE_INSPECT_LIMIT {
return None;
}
let mut raw = String::new();
lock_file
.take(DOCTOR_LOCK_FILE_INSPECT_LIMIT.saturating_add(1))
.read_to_string(&mut raw)
.ok()?;
if u64::try_from(raw.len()).unwrap_or(u64::MAX) > DOCTOR_LOCK_FILE_INSPECT_LIMIT {
return None;
}
doctor_lock_holder_from_raw(&raw)
}
fn write_doctor_lock_contents(lock_file: &mut fs::File, contents: &str) -> io::Result<()> {
#[cfg(test)]
if DOCTOR_LOCK_FAIL_NEXT_WRITE.with(|flag| flag.replace(false)) {
return Err(io::Error::other(
"injected doctor lock metadata write failure",
));
}
lock_file.set_len(0)?;
lock_file.seek(SeekFrom::Start(0))?;
lock_file.write_all(contents.as_bytes())?;
lock_file.flush()
}
#[cfg(test)]
fn fail_next_doctor_lock_metadata_write() {
DOCTOR_LOCK_FAIL_NEXT_WRITE.with(|flag| flag.set(true));
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn ensure_doctor_lock_binding_at(ee_dir: &fs::File, lock_file: &fs::File) -> io::Result<()> {
use rustix::fs::{AtFlags, FileType};
let expected = rustix::fs::fstat(lock_file).map_err(io::Error::from)?;
let observed = rustix::fs::statat(ee_dir, ".doctor.lock", AtFlags::SYMLINK_NOFOLLOW).map_err(
|source| {
if source == rustix::io::Errno::NOENT {
io::Error::new(
io::ErrorKind::PermissionDenied,
"doctor lock path disappeared during acquisition",
)
} else {
io::Error::from(source)
}
},
)?;
if FileType::from_raw_mode(expected.st_mode) != FileType::RegularFile
|| FileType::from_raw_mode(observed.st_mode) != FileType::RegularFile
|| observed.st_dev != expected.st_dev
|| observed.st_ino != expected.st_ino
|| expected.st_nlink != 1
|| observed.st_nlink != 1
|| expected.st_uid != rustix::process::geteuid().as_raw()
|| observed.st_uid != expected.st_uid
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"doctor lock path changed or lacks single-link process ownership",
));
}
Ok(())
}
fn doctor_lock_holder_from_raw(raw: &str) -> Option<String> {
raw.lines()
.next()
.filter(|holder| *holder != DOCTOR_LOCK_FILE_MARKER.trim_end())
.map(str::to_owned)
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn doctor_entry_type_at(
directory: &fs::File,
name: &OsStr,
) -> Result<Option<rustix::fs::FileType>, DoctorRuntimeError> {
use rustix::fs::{AtFlags, FileType};
use rustix::io::Errno;
match rustix::fs::statat(directory, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(stat) => Ok(Some(FileType::from_raw_mode(stat.st_mode))),
Err(source) if source == Errno::NOENT => Ok(None),
Err(source) => Err(DoctorRuntimeError::Io {
context: format!("inspect doctor lifecycle entry {}", name.to_string_lossy()),
source: io::Error::from(source),
}),
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn doctor_file_type_name(file_type: rustix::fs::FileType) -> &'static str {
use rustix::fs::FileType;
match file_type {
FileType::RegularFile => "regular file",
FileType::Directory => "directory",
FileType::Symlink => "symbolic link",
FileType::Fifo => "fifo",
FileType::Socket => "socket",
FileType::CharacterDevice => "character device",
FileType::BlockDevice => "block device",
FileType::Unknown => "unknown filesystem entry",
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn doctor_lifecycle_errno(
path: &Path,
operation: &str,
source: rustix::io::Errno,
) -> DoctorRuntimeError {
if matches!(source, rustix::io::Errno::LOOP | rustix::io::Errno::NOTDIR)
&& let Ok(Some(path)) = super::path_safety::first_existing_symlink_component(path)
{
return DoctorRuntimeError::SymlinkedRunRoot { path };
}
DoctorRuntimeError::Io {
context: format!("{operation} {}", path.display()),
source: io::Error::from(source),
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn ensure_doctor_lifecycle_bindings(
lifecycle: &DoctorLifecycleHandles,
workspace: &Path,
run_dir: &Path,
) -> Result<(), DoctorRuntimeError> {
let doctor_dir = workspace.join(".doctor");
let runs_dir = doctor_dir.join("runs");
ensure_doctor_directory_binding(
rustix::fs::CWD,
workspace,
&lifecycle.workspace_dir,
workspace,
)?;
ensure_doctor_directory_binding(
&lifecycle.workspace_dir,
Path::new(".ee"),
&lifecycle.ee_dir,
&workspace.join(".ee"),
)?;
ensure_doctor_directory_binding(
&lifecycle.workspace_dir,
Path::new(".doctor"),
&lifecycle.doctor_dir,
&doctor_dir,
)?;
ensure_doctor_directory_binding(
&lifecycle.doctor_dir,
Path::new("runs"),
&lifecycle.runs_dir,
&runs_dir,
)?;
let run_name = run_dir.file_name().ok_or_else(|| DoctorRuntimeError::Io {
context: format!(
"derive doctor run directory name from {}",
run_dir.display()
),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"run directory has no file name",
),
})?;
ensure_doctor_directory_binding(
&lifecycle.runs_dir,
Path::new(run_name),
&lifecycle.run_dir,
run_dir,
)?;
ensure_doctor_directory_binding(
&lifecycle.run_dir,
Path::new("backups"),
&lifecycle.backups_dir,
&run_dir.join("backups"),
)?;
ensure_doctor_directory_binding(
&lifecycle.run_dir,
Path::new("quarantine"),
&lifecycle.quarantine_dir,
&run_dir.join("quarantine"),
)
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn ensure_doctor_directory_binding<Fd: std::os::fd::AsFd>(
parent: Fd,
name: &Path,
opened: &fs::File,
full_path: &Path,
) -> Result<(), DoctorRuntimeError> {
use rustix::fs::{AtFlags, FileType};
let observed = match rustix::fs::statat(parent, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(stat) => stat,
Err(source)
if matches!(
source,
rustix::io::Errno::NOENT | rustix::io::Errno::LOOP | rustix::io::Errno::NOTDIR
) =>
{
return Err(DoctorRuntimeError::LifecycleRootChanged {
path: full_path.to_path_buf(),
});
}
Err(source) => {
return Err(DoctorRuntimeError::Io {
context: format!("inspect doctor lifecycle binding {}", full_path.display()),
source: io::Error::from(source),
});
}
};
let expected = rustix::fs::fstat(opened).map_err(|source| DoctorRuntimeError::Io {
context: format!("inspect opened doctor directory {}", full_path.display()),
source: io::Error::from(source),
})?;
if FileType::from_raw_mode(observed.st_mode) != FileType::Directory
|| observed.st_dev != expected.st_dev
|| observed.st_ino != expected.st_ino
{
return Err(DoctorRuntimeError::LifecycleRootChanged {
path: full_path.to_path_buf(),
});
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
fn ensure_doctor_lifecycle_bindings(
_lifecycle: &DoctorLifecycleHandles,
workspace: &Path,
run_dir: &Path,
) -> Result<(), DoctorRuntimeError> {
let ee_dir = workspace.join(".ee");
let doctor_dir = workspace.join(".doctor");
let backups_dir = run_dir.join("backups");
let quarantine_dir = run_dir.join("quarantine");
validate_doctor_lifecycle_paths([
workspace,
ee_dir.as_path(),
doctor_dir.as_path(),
run_dir,
backups_dir.as_path(),
quarantine_dir.as_path(),
])
}
fn write_lifecycle_state(
lifecycle: &DoctorLifecycleHandles,
run_dir: &Path,
state: &RunState,
) -> Result<(), DoctorRuntimeError> {
let bytes = serde_json::to_vec_pretty(state).map_err(|error| DoctorRuntimeError::Io {
context: "serialize RunState".into(),
source: io::Error::new(io::ErrorKind::InvalidData, error),
})?;
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
{
return write_doctor_file_atomic_at(&lifecycle.run_dir, "state.json", &bytes).map_err(
|source| DoctorRuntimeError::Io {
context: format!("write state.json {}", run_dir.join("state.json").display()),
source,
},
);
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
{
let _ = lifecycle;
let path = run_dir.join("state.json");
write_file_atomic(&path, &bytes).map_err(|source| DoctorRuntimeError::Io {
context: format!("write state.json {}", path.display()),
source,
})
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn write_doctor_file_atomic_at(directory: &fs::File, name: &str, bytes: &[u8]) -> io::Result<()> {
use rustix::fs::{Mode, OFlags};
let sequence = DOCTOR_STATE_FILE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let temporary = format!(".{name}.tmp.{}.{}", std::process::id(), sequence);
let fd = rustix::fs::openat(
directory,
temporary.as_str(),
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::from_raw_mode(0o600),
)
.map_err(io::Error::from)?;
let mut file = fs::File::from(fd);
file.write_all(bytes)?;
file.flush()?;
drop(file);
rustix::fs::renameat(directory, temporary.as_str(), directory, name).map_err(io::Error::from)
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn publish_doctor_latest(
lifecycle: &DoctorLifecycleHandles,
run_id: &str,
run_dir: &Path,
latest_link: &Path,
) -> Result<(), DoctorRuntimeError> {
use rustix::fs::{FileType, RenameFlags};
const CANDIDATE: &str = "latest-candidate";
const PREVIOUS: &str = "previous-latest";
let target = Path::new("runs").join(run_id);
rustix::fs::symlinkat(&target, &lifecycle.run_dir, CANDIDATE).map_err(|source| {
doctor_lifecycle_errno(
&latest_link.with_file_name(CANDIDATE),
"create latest candidate",
source,
)
})?;
match doctor_entry_type_at(&lifecycle.doctor_dir, OsStr::new("latest"))? {
None => rustix::fs::renameat_with(
&lifecycle.run_dir,
CANDIDATE,
&lifecycle.doctor_dir,
"latest",
RenameFlags::NOREPLACE,
)
.map_err(|source| {
if source == rustix::io::Errno::EXIST {
let observed = doctor_entry_type_at(&lifecycle.doctor_dir, OsStr::new("latest"))
.ok()
.flatten()
.map(doctor_file_type_name)
.unwrap_or("concurrently-created entry");
DoctorRuntimeError::UnsafeLatestEntry {
path: latest_link.to_path_buf(),
observed_kind: observed.to_owned(),
}
} else {
doctor_lifecycle_errno(latest_link, "publish latest", source)
}
}),
Some(FileType::Symlink) => {
rustix::fs::renameat_with(
&lifecycle.run_dir,
CANDIDATE,
&lifecycle.doctor_dir,
"latest",
RenameFlags::EXCHANGE,
)
.map_err(|source| doctor_lifecycle_errno(latest_link, "exchange latest", source))?;
let displaced = doctor_entry_type_at(&lifecycle.run_dir, OsStr::new(CANDIDATE))?;
if displaced == Some(FileType::Symlink) {
return rustix::fs::renameat_with(
&lifecycle.run_dir,
CANDIDATE,
&lifecycle.run_dir,
PREVIOUS,
RenameFlags::NOREPLACE,
)
.map_err(|source| DoctorRuntimeError::Io {
context: format!(
"preserve prior latest at {}",
run_dir.join(PREVIOUS).display()
),
source: io::Error::from(source),
});
}
let rollback = rustix::fs::renameat_with(
&lifecycle.run_dir,
CANDIDATE,
&lifecycle.doctor_dir,
"latest",
RenameFlags::EXCHANGE,
);
let observed = displaced
.map(doctor_file_type_name)
.unwrap_or("missing entry")
.to_owned();
if let Err(source) = rollback {
return Err(DoctorRuntimeError::Io {
context: format!(
"rollback concurrent latest substitution at {} (displaced {observed})",
latest_link.display()
),
source: io::Error::from(source),
});
}
Err(DoctorRuntimeError::UnsafeLatestEntry {
path: latest_link.to_path_buf(),
observed_kind: observed,
})
}
Some(file_type) => Err(DoctorRuntimeError::UnsafeLatestEntry {
path: latest_link.to_path_buf(),
observed_kind: doctor_file_type_name(file_type).to_owned(),
}),
}
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
fn publish_doctor_latest(
_lifecycle: &DoctorLifecycleHandles,
run_id: &str,
run_dir: &Path,
latest_link: &Path,
) -> Result<(), DoctorRuntimeError> {
validate_doctor_lifecycle_paths([run_dir, latest_link.parent().unwrap_or(latest_link)])?;
match fs::symlink_metadata(latest_link) {
Ok(metadata) if !metadata.file_type().is_symlink() => {
return Err(DoctorRuntimeError::UnsafeLatestEntry {
path: latest_link.to_path_buf(),
observed_kind: if metadata.is_dir() {
"directory".to_owned()
} else {
"regular file".to_owned()
},
});
}
Ok(_) => {
let previous = run_dir.join("previous-latest");
if fs::symlink_metadata(&previous).is_ok() {
return Err(DoctorRuntimeError::Io {
context: format!("preserve prior latest at {}", previous.display()),
source: io::Error::new(
io::ErrorKind::AlreadyExists,
"previous-latest run artifact already exists",
),
});
}
fs::rename(latest_link, &previous).map_err(|source| DoctorRuntimeError::Io {
context: format!(
"preserve existing latest {} at {}",
latest_link.display(),
previous.display()
),
source,
})?;
let displaced =
fs::symlink_metadata(&previous).map_err(|source| DoctorRuntimeError::Io {
context: format!("inspect preserved latest {}", previous.display()),
source,
})?;
if !displaced.file_type().is_symlink() {
let observed_kind = if displaced.is_dir() {
"directory"
} else {
"regular file"
};
let _ = fs::rename(&previous, latest_link);
return Err(DoctorRuntimeError::UnsafeLatestEntry {
path: latest_link.to_path_buf(),
observed_kind: observed_kind.to_owned(),
});
}
let target = Path::new("runs").join(run_id);
let created = create_doctor_latest_symlink(&target, latest_link);
if let Err(source) = created {
if fs::symlink_metadata(latest_link).is_err() {
let _ = fs::rename(&previous, latest_link);
}
return Err(DoctorRuntimeError::Io {
context: format!("create latest link {}", latest_link.display()),
source,
});
}
return Ok(());
}
Err(source) if source.kind() == io::ErrorKind::NotFound => {
let target = Path::new("runs").join(run_id);
create_doctor_latest_symlink(&target, latest_link).map_err(|source| {
DoctorRuntimeError::Io {
context: format!("create latest link {}", latest_link.display()),
source,
}
})?;
return Ok(());
}
Err(source) => {
return Err(DoctorRuntimeError::Io {
context: format!("inspect latest link {}", latest_link.display()),
source,
});
}
}
}
#[cfg(all(
unix,
not(any(target_os = "linux", target_os = "android", target_vendor = "apple"))
))]
fn create_doctor_latest_symlink(target: &Path, latest_link: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(target, latest_link)
}
#[cfg(windows)]
fn create_doctor_latest_symlink(target: &Path, latest_link: &Path) -> io::Result<()> {
std::os::windows::fs::symlink_dir(target, latest_link)
}
#[cfg(not(any(unix, windows)))]
fn create_doctor_latest_symlink(_target: &Path, _latest_link: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"symbolic links are unsupported on this platform",
))
}
#[cfg(test)]
fn set_doctor_lock_before_unlock_hook(hook: impl FnOnce() + 'static) {
DOCTOR_LOCK_BEFORE_UNLOCK_HOOK.with(|slot| {
*slot.borrow_mut() = Some(Box::new(hook));
});
}
#[cfg(test)]
fn run_doctor_lock_before_unlock_hook() {
DOCTOR_LOCK_BEFORE_UNLOCK_HOOK.with(|slot| {
if let Some(hook) = slot.borrow_mut().take() {
hook();
}
});
}
#[cfg(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
))]
fn release_doctor_lock(lifecycle: &DoctorLifecycleHandles, _lock_path: &Path) -> io::Result<()> {
unlock_doctor_lock_file(&lifecycle.lock_file)
}
#[cfg(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
))]
fn unlock_doctor_lock_file(lock_file: &fs::File) -> io::Result<()> {
#[cfg(test)]
run_doctor_lock_before_unlock_hook();
Fs4FileExt::unlock(lock_file)
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
)))]
fn unlock_doctor_lock_file(_lock_file: &fs::File) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"doctor lock ownership cannot be proven on this platform",
))
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
)))]
fn release_doctor_lock(_lifecycle: &DoctorLifecycleHandles, _lock_path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"doctor mutation is disabled because this platform cannot prove lock ownership",
))
}
fn derive_run_id(target_sha: &str) -> String {
let now = Utc::now();
let sequence = DOCTOR_RUN_ID_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let timestamp_nanos = now.timestamp_nanos_opt().unwrap_or_default();
let mut hasher = blake3::Hasher::new();
hasher.update(target_sha.as_bytes());
hasher.update(b"|");
hasher.update(timestamp_nanos.to_string().as_bytes());
hasher.update(b"|");
hasher.update(std::process::id().to_string().as_bytes());
hasher.update(b"|");
hasher.update(sequence.to_string().as_bytes());
let hash = hasher.finalize();
let hex = hash.to_hex();
let short = &hex.as_str()[..6];
format!(
"{}__{}__{}",
now.format("%Y-%m-%dT%H-%M-%S%.9fZ"),
sequence,
short
)
}
fn hash_file(path: &Path) -> Result<String, DoctorRuntimeError> {
let mut hasher = blake3::Hasher::new();
let mut file = fs::File::open(path).map_err(|source| DoctorRuntimeError::Io {
context: format!("open for hashing: {}", path.display()),
source,
})?;
let mut buf = [0u8; 8192];
loop {
let n = file
.read(&mut buf)
.map_err(|source| DoctorRuntimeError::Io {
context: format!("read for hashing: {}", path.display()),
source,
})?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(hasher.finalize().to_hex().to_string())
}
fn hash_bytes(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
fn read_mode(path: &Path) -> Option<u32> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
fs::metadata(path).ok().map(|m| m.permissions().mode())
}
#[cfg(not(unix))]
{
let _ = path;
None
}
}
fn validate_relative_quarantine_dest(
dest: &Path,
run_dir: &Path,
) -> Result<(), DoctorRuntimeError> {
if dest.as_os_str().is_empty() {
return Err(DoctorRuntimeError::BlastRadiusExceeded {
path: dest.to_path_buf(),
allowed_roots: vec![run_dir.join("quarantine")],
});
}
for component in dest.components() {
match component {
std::path::Component::Normal(_) => continue,
_ => {
return Err(DoctorRuntimeError::BlastRadiusExceeded {
path: dest.to_path_buf(),
allowed_roots: vec![run_dir.join("quarantine")],
});
}
}
}
Ok(())
}
fn is_path_in_blast_radius(path: &Path, roots: &[PathBuf]) -> bool {
let probe = if path.exists() {
path.canonicalize().ok()
} else {
nearest_existing_ancestor_canonical(path)
};
let probe = match probe {
Some(p) => p,
None => return false,
};
roots
.iter()
.any(|root| root.is_absolute() && probe.starts_with(root))
}
fn nearest_existing_ancestor_canonical(path: &Path) -> Option<PathBuf> {
let mut tail: Vec<OsString> = Vec::new();
let mut p = path.to_path_buf();
loop {
if p.exists() {
let mut canon = p.canonicalize().ok()?;
for component in tail.iter().rev() {
canon.push(component);
}
return Some(canon);
}
let name = match p.components().next_back()? {
Component::Normal(name) => name.to_os_string(),
Component::CurDir
| Component::ParentDir
| Component::Prefix(_)
| Component::RootDir => {
return None;
}
};
let parent = p.parent()?.to_path_buf();
tail.push(name);
p = parent;
}
}
fn write_file_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent().filter(|p| p.is_absolute()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"write_file_atomic: path lacks an absolute parent: {}",
path.display()
),
)
})?;
fs::create_dir_all(parent)?;
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
tmp.write_all(bytes)?;
tmp.flush()?;
tmp.persist(path).map_err(|e| e.error)?;
Ok(())
}
fn sanitize_path_for_run_dir(path: &Path) -> PathBuf {
let mut rel = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::Prefix(_) | std::path::Component::RootDir => continue,
std::path::Component::CurDir => continue,
std::path::Component::ParentDir => rel.push("__parent__"),
std::path::Component::Normal(s) => rel.push(s),
}
}
rel
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn stage_backup(
ctx: &RunContext,
path: &Path,
expected_hash: &Option<String>,
) -> Result<PathBuf, DoctorRuntimeError> {
use rustix::fs::{Mode, OFlags};
let path_rel = sanitize_path_for_run_dir(path);
let next_seq = ctx.state.action_count + 1;
let seq_dir = PathBuf::from(format!("{:06}", next_seq));
let rel = seq_dir.join(&path_rel);
let backups_path = ctx.run_dir.join("backups");
let mut source = if ctx.dry_run {
None
} else {
Some(
fs::File::open(path).map_err(|source| DoctorRuntimeError::Io {
context: format!("open backup source {}", path.display()),
source,
})?,
)
};
let destination =
prepare_doctor_relative_destination(&ctx.lifecycle.backups_dir, &backups_path, &rel)?;
if ctx.dry_run {
if doctor_entry_type_at(&destination.parent, destination.leaf.as_os_str())?.is_some() {
return Err(DoctorRuntimeError::Io {
context: format!(
"backup collision at sequence {} target {}: {} already exists",
next_seq,
path.display(),
destination.display_path.display()
),
source: io::Error::new(io::ErrorKind::AlreadyExists, "backup collision"),
});
}
return Ok(rel);
}
let destination_fd = rustix::fs::openat(
&destination.parent,
destination.leaf.as_os_str(),
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::from_raw_mode(0o600),
)
.map_err(|source| {
if source == rustix::io::Errno::EXIST {
DoctorRuntimeError::Io {
context: format!(
"backup collision at sequence {} target {}: {} already exists",
next_seq,
path.display(),
destination.display_path.display()
),
source: io::Error::new(io::ErrorKind::AlreadyExists, "backup collision"),
}
} else {
doctor_lifecycle_errno(&destination.display_path, "create doctor backup", source)
}
})?;
let mut destination_file = fs::File::from(destination_fd);
let mut hasher = blake3::Hasher::new();
let mut buffer = [0_u8; 8192];
let source = source.as_mut().ok_or_else(|| DoctorRuntimeError::Io {
context: format!("open backup source {}", path.display()),
source: io::Error::new(io::ErrorKind::InvalidInput, "backup source was not opened"),
})?;
loop {
let read = source
.read(&mut buffer)
.map_err(|source| DoctorRuntimeError::Io {
context: format!("read backup source {}", path.display()),
source,
})?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
destination_file
.write_all(&buffer[..read])
.map_err(|source| DoctorRuntimeError::Io {
context: format!(
"write descriptor-anchored backup {}",
destination.display_path.display()
),
source,
})?;
}
destination_file
.flush()
.map_err(|source| DoctorRuntimeError::Io {
context: format!(
"flush descriptor-anchored backup {}",
destination.display_path.display()
),
source,
})?;
if let Some(expected) = expected_hash {
let observed = hasher.finalize().to_hex().to_string();
if &observed != expected {
return Err(DoctorRuntimeError::Io {
context: format!(
"backup hash mismatch after copy ({}): expected {}, observed {}",
destination.display_path.display(),
expected,
observed
),
source: io::Error::new(io::ErrorKind::Other, "backup race"),
});
}
}
Ok(rel)
}
#[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
fn stage_backup(
ctx: &RunContext,
path: &Path,
expected_hash: &Option<String>,
) -> Result<PathBuf, DoctorRuntimeError> {
let path_rel = sanitize_path_for_run_dir(path);
let next_seq = ctx.state.action_count + 1;
let seq_dir = PathBuf::from(format!("{:06}", next_seq));
let rel = seq_dir.join(&path_rel);
let backup_path = ctx.run_dir.join("backups").join(&rel);
if backup_path.exists() {
return Err(DoctorRuntimeError::Io {
context: format!(
"backup collision at sequence {} target {}: {} already exists",
next_seq,
path.display(),
backup_path.display()
),
source: io::Error::new(io::ErrorKind::AlreadyExists, "backup collision"),
});
}
if let Some(parent) = backup_path.parent() {
fs::create_dir_all(parent)?;
}
if !ctx.dry_run {
fs::copy(path, &backup_path).map_err(|source| DoctorRuntimeError::Io {
context: format!(
"backup copy {} -> {}",
path.display(),
backup_path.display()
),
source,
})?;
if let Some(expected) = expected_hash {
let observed = hash_file(&backup_path)?;
if &observed != expected {
return Err(DoctorRuntimeError::Io {
context: format!(
"backup hash mismatch after copy ({}): expected {}, observed {}",
backup_path.display(),
expected,
observed
),
source: io::Error::new(io::ErrorKind::Other, "backup race"),
});
}
}
}
Ok(rel)
}
const DOCTOR_RUN_STATE_INSPECT_LIMIT: u64 = 4 * 1024 * 1024;
const DOCTOR_ACTION_LOG_INSPECT_LIMIT: u64 = 16 * 1024 * 1024;
const DOCTOR_LOCK_FILE_INSPECT_LIMIT: u64 = 4 * 1024;
fn read_required_doctor_jsonl_file(
path: &Path,
label: &'static str,
) -> Result<String, DoctorRuntimeError> {
let metadata = fs::symlink_metadata(path).map_err(|source| DoctorRuntimeError::Io {
context: format!("read {label} {}", path.display()),
source,
})?;
read_doctor_text_file_with_metadata(path, label, metadata, DOCTOR_ACTION_LOG_INSPECT_LIMIT)
}
fn read_optional_doctor_jsonl_file(
path: &Path,
label: &'static str,
) -> Result<Option<String>, DoctorRuntimeError> {
read_optional_doctor_text_file(path, label, DOCTOR_ACTION_LOG_INSPECT_LIMIT)
}
fn read_optional_doctor_text_file(
path: &Path,
label: &'static str,
byte_limit: u64,
) -> Result<Option<String>, DoctorRuntimeError> {
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(DoctorRuntimeError::Io {
context: format!("read {label} {}", path.display()),
source,
});
}
};
read_doctor_text_file_with_metadata(path, label, metadata, byte_limit).map(Some)
}
fn read_doctor_text_file_with_metadata(
path: &Path,
label: &'static str,
metadata: fs::Metadata,
byte_limit: u64,
) -> Result<String, DoctorRuntimeError> {
if !metadata.file_type().is_file() {
return Err(DoctorRuntimeError::Io {
context: format!("read {label} {}", path.display()),
source: io::Error::new(
io::ErrorKind::InvalidInput,
format!("{label} is not a regular file"),
),
});
}
if metadata.len() > byte_limit {
return Err(DoctorRuntimeError::Io {
context: format!("read {label} {}", path.display()),
source: io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{label} size {} exceeds {byte_limit} byte cap",
metadata.len()
),
),
});
}
let file =
open_doctor_inspect_file_for_read(path).map_err(|source| DoctorRuntimeError::Io {
context: format!("read {label} {}", path.display()),
source,
})?;
let mut raw = String::new();
file.take(byte_limit.saturating_add(1))
.read_to_string(&mut raw)
.map_err(|source| DoctorRuntimeError::Io {
context: format!("read {label} {}", path.display()),
source,
})?;
if u64::try_from(raw.len()).unwrap_or(u64::MAX) > byte_limit {
return Err(DoctorRuntimeError::Io {
context: format!("read {label} {}", path.display()),
source: io::Error::new(
io::ErrorKind::InvalidData,
format!("{label} grew past cap during read"),
),
});
}
Ok(raw)
}
fn read_doctor_backup_bytes(path: &Path) -> Result<Vec<u8>, DoctorRuntimeError> {
let metadata =
fs::symlink_metadata(path).map_err(|source| DoctorRuntimeError::UndoBackupCorrupt {
backup_path: path.to_path_buf(),
expected_hash: "<recorded before_hash>".into(),
observed_hash: Some(format!("unreadable: {source}")),
})?;
if !metadata.file_type().is_file() {
return Err(DoctorRuntimeError::RunArtifactInvalid {
path: path.to_path_buf(),
reason: "backup is not a regular file".into(),
});
}
let mut file =
open_doctor_inspect_file_for_read(path).map_err(|source| DoctorRuntimeError::Io {
context: format!("open doctor backup {}", path.display()),
source,
})?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|source| DoctorRuntimeError::Io {
context: format!("read doctor backup {}", path.display()),
source,
})?;
Ok(bytes)
}
fn open_doctor_inspect_file_for_read(path: &Path) -> io::Result<fs::File> {
let mut options = fs::OpenOptions::new();
options.read(true);
configure_doctor_inspect_open_no_follow(&mut options);
options.open(path)
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn configure_doctor_inspect_open_no_follow(options: &mut fs::OpenOptions) {
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
}
#[cfg(not(all(unix, not(any(target_os = "espidf", target_os = "horizon")))))]
fn configure_doctor_inspect_open_no_follow(_options: &mut fs::OpenOptions) {}
fn read_state(run_dir: &Path) -> Result<RunState, DoctorRuntimeError> {
let path = run_dir.join("state.json");
let metadata = fs::symlink_metadata(&path).map_err(|source| DoctorRuntimeError::Io {
context: format!("read state.json {}", path.display()),
source,
})?;
if !metadata.file_type().is_file() {
return Err(DoctorRuntimeError::Io {
context: format!("read state.json {}", path.display()),
source: io::Error::new(
io::ErrorKind::InvalidInput,
"state.json is not a regular file",
),
});
}
if metadata.len() > DOCTOR_RUN_STATE_INSPECT_LIMIT {
return Err(DoctorRuntimeError::Io {
context: format!("read state.json {}", path.display()),
source: io::Error::new(
io::ErrorKind::InvalidData,
format!(
"state.json size {} exceeds {DOCTOR_RUN_STATE_INSPECT_LIMIT} byte cap",
metadata.len()
),
),
});
}
let file =
open_doctor_inspect_file_for_read(&path).map_err(|source| DoctorRuntimeError::Io {
context: format!("read state.json {}", path.display()),
source,
})?;
let mut bytes = Vec::new();
file.take(DOCTOR_RUN_STATE_INSPECT_LIMIT.saturating_add(1))
.read_to_end(&mut bytes)
.map_err(|source| DoctorRuntimeError::Io {
context: format!("read state.json {}", path.display()),
source,
})?;
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > DOCTOR_RUN_STATE_INSPECT_LIMIT {
return Err(DoctorRuntimeError::Io {
context: format!("read state.json {}", path.display()),
source: io::Error::new(
io::ErrorKind::InvalidData,
"state.json grew past cap during read",
),
});
}
serde_json::from_slice(&bytes).map_err(|e| DoctorRuntimeError::Io {
context: format!("parse state.json {}", path.display()),
source: io::Error::new(io::ErrorKind::InvalidData, e),
})
}
struct UndoLockGuard {
lock_file: fs::File,
}
impl Drop for UndoLockGuard {
fn drop(&mut self) {
let _ = unlock_doctor_lock_file(&self.lock_file);
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn acquire_undo_lock(workspace: &Path) -> Result<UndoLockGuard, DoctorRuntimeError> {
use rustix::fs::{Mode, OFlags};
let workspace_abs = canonical_doctor_workspace(workspace)?;
let ee_dir = workspace_abs.join(".ee");
let lock_path = ee_dir.join(".doctor.lock");
validate_doctor_lifecycle_paths([
workspace_abs.as_path(),
ee_dir.as_path(),
lock_path.as_path(),
])?;
let directory_flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
let workspace_fd = rustix::fs::openat(
rustix::fs::CWD,
&workspace_abs,
directory_flags,
Mode::from_raw_mode(0),
)
.map(fs::File::from)
.map_err(|source| {
doctor_lifecycle_errno(&workspace_abs, "open canonical undo workspace", source)
})?;
let ee_dir_fd =
open_or_create_doctor_directory_at(&workspace_fd, OsStr::new(".ee"), &ee_dir, true)?;
let lock_file = acquire_doctor_lock_at(&ee_dir_fd, &lock_path)?;
Ok(UndoLockGuard { lock_file })
}
#[cfg(windows)]
fn acquire_undo_lock(workspace: &Path) -> Result<UndoLockGuard, DoctorRuntimeError> {
let workspace_abs = canonical_doctor_workspace(workspace)?;
let ee_dir = workspace_abs.join(".ee");
let lock_path = ee_dir.join(".doctor.lock");
validate_doctor_lifecycle_paths([
workspace_abs.as_path(),
ee_dir.as_path(),
lock_path.as_path(),
])?;
fs::create_dir_all(&ee_dir).map_err(|source| DoctorRuntimeError::Io {
context: format!("create_dir_all({}) for undo lock", ee_dir.display()),
source,
})?;
let lock_file = acquire_windows_doctor_lock(&lock_path)?;
Ok(UndoLockGuard { lock_file })
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
)))]
fn acquire_undo_lock(workspace: &Path) -> Result<UndoLockGuard, DoctorRuntimeError> {
Err(DoctorRuntimeError::Io {
context: format!("acquire doctor undo lock for {}", workspace.display()),
source: io::Error::new(
io::ErrorKind::Unsupported,
"doctor undo is disabled because this platform cannot prove lock ownership",
),
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn fresh_workspace() -> TempDir {
TempDir::new().expect("tempdir")
}
fn start_run(ws: &Path) -> RunContext {
let mut roots = default_blast_radius_roots(ws);
roots.push(ws.to_path_buf());
RunContext::start(ws, "deadbeefcafe", roots, false).expect("start run")
}
fn replay_test_undo(run_dir: &Path) -> Result<UndoSummary, DoctorRuntimeError> {
let run_id = run_dir
.file_name()
.and_then(|name| name.to_str())
.expect("test run id");
let workspace = run_dir
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.expect("test workspace");
let mut roots = default_blast_radius_roots(workspace);
roots.push(workspace.to_path_buf());
replay_undo_with_authorized_roots(workspace, run_id, &roots)
}
fn assert_persistent_doctor_lock_released(workspace: &Path) {
let lock_path = workspace.join(".ee").join(".doctor.lock");
assert!(
lock_path.is_file(),
"persistent doctor lock file is missing"
);
let lock = fs::OpenOptions::new()
.read(true)
.write(true)
.open(&lock_path)
.expect("open persistent doctor lock");
Fs4FileExt::try_lock(&lock).expect("persistent doctor advisory lock should be released");
Fs4FileExt::unlock(&lock).expect("unlock test doctor lock");
}
#[cfg(unix)]
#[test]
fn blast_radius_override_parses_absolute_colon_separated_paths() {
let roots = parse_blast_radius_override("/a/b:/c/d").expect("valid override");
assert_eq!(roots, vec![PathBuf::from("/a/b"), PathBuf::from("/c/d")]);
}
#[test]
fn blast_radius_override_rejects_relative_and_empty_segments() {
assert!(parse_blast_radius_override("relative/path").is_err());
assert!(parse_blast_radius_override("/abs::/tail").is_err());
assert!(parse_blast_radius_override(":/lead").is_err());
}
#[test]
fn doctor_run_id_is_one_bounded_portable_component() {
assert!(validate_doctor_run_id("2026-08-31T00-00-00.000000000Z__1__abc123").is_ok());
for invalid in [
"",
".",
"..",
"../escape",
"nested/run",
r"nested\run",
"/tmp/run",
] {
assert!(
matches!(
validate_doctor_run_id(invalid),
Err(DoctorRuntimeError::InvalidRunId { .. })
),
"accepted invalid run id {invalid:?}"
);
}
let overlong = "a".repeat(DOCTOR_RUN_ID_MAX_BYTES + 1);
assert!(matches!(
validate_doctor_run_id(&overlong),
Err(DoctorRuntimeError::InvalidRunId { .. })
));
}
#[test]
fn run_state_reader_rejects_forged_run_binding() {
let ws = fresh_workspace();
let ctx = start_run(ws.path());
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
ctx.finish(RunStatus::CompletedOk).unwrap();
let mut state = read_state(&run_dir).unwrap();
state.run_id = "different-run".into();
fs::write(
run_dir.join("state.json"),
serde_json::to_vec_pretty(&state).unwrap(),
)
.unwrap();
assert!(matches!(
read_doctor_run_state(ws.path(), &run_id),
Err(DoctorRuntimeError::RunArtifactInvalid { .. })
));
}
#[test]
fn undo_prevalidates_entire_action_ledger_before_mutating() {
let ws = fresh_workspace();
let first = ws.path().join("first.txt");
let second = ws.path().join("second.txt");
fs::write(&first, b"first-before").unwrap();
fs::write(&second, b"second-before").unwrap();
let mut ctx = start_run(ws.path());
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
mutate(
&mut ctx,
&first,
Op::WriteFile {
bytes: b"first-after".to_vec(),
},
)
.unwrap();
mutate(
&mut ctx,
&second,
Op::WriteFile {
bytes: b"second-after".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
let actions_path = run_dir.join("actions.jsonl");
let raw = fs::read_to_string(&actions_path).unwrap();
let mut actions = raw
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
let outside = ws
.path()
.parent()
.expect("workspace parent")
.join("forged-doctor-target.txt");
actions[0]["path"] = serde_json::Value::String(outside.display().to_string());
let tampered = actions
.into_iter()
.map(|action| serde_json::to_string(&action).unwrap())
.collect::<Vec<_>>()
.join("\n");
fs::write(&actions_path, format!("{tampered}\n")).unwrap();
let mut roots = default_blast_radius_roots(ws.path());
roots.push(ws.path().to_path_buf());
assert!(matches!(
replay_undo_with_authorized_roots(ws.path(), &run_id, &roots),
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
assert_eq!(fs::read(&first).unwrap(), b"first-after");
assert_eq!(fs::read(&second).unwrap(), b"second-after");
assert!(
!run_dir.join("undo_log.jsonl").exists(),
"prevalidation failure must not append an undo record"
);
}
#[test]
fn undo_recovers_durable_action_appended_before_failed_state_update() {
let ws = fresh_workspace();
let target = ws.path().join("crash-window.txt");
fs::write(&target, b"before").unwrap();
let mut ctx = start_run(ws.path());
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"after".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
let mut stale_state = read_state(&run_dir).unwrap();
stale_state.status = RunStatus::Failed;
stale_state.action_count = 0;
fs::write(
run_dir.join("state.json"),
serde_json::to_vec_pretty(&stale_state).unwrap(),
)
.unwrap();
let mut roots = default_blast_radius_roots(ws.path());
roots.push(ws.path().to_path_buf());
let summary = replay_undo_with_authorized_roots(ws.path(), &run_id, &roots).unwrap();
assert_eq!(summary.actions_undone, 1);
assert!(matches!(summary.status, RunStatus::Undone));
assert_eq!(fs::read(&target).unwrap(), b"before");
assert_eq!(read_state(&run_dir).unwrap().action_count, 1);
}
#[test]
fn undo_rejects_action_count_gap_larger_than_single_crash_window() {
let ws = fresh_workspace();
let first = ws.path().join("gap-first.txt");
let second = ws.path().join("gap-second.txt");
fs::write(&first, b"first-before").unwrap();
fs::write(&second, b"second-before").unwrap();
let mut ctx = start_run(ws.path());
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
for (path, bytes) in [
(&first, b"first-after".as_slice()),
(&second, b"second-after".as_slice()),
] {
mutate(
&mut ctx,
path,
Op::WriteFile {
bytes: bytes.to_vec(),
},
)
.unwrap();
}
ctx.finish(RunStatus::CompletedOk).unwrap();
let mut stale_state = read_state(&run_dir).unwrap();
stale_state.status = RunStatus::Failed;
stale_state.action_count = 0;
fs::write(
run_dir.join("state.json"),
serde_json::to_vec_pretty(&stale_state).unwrap(),
)
.unwrap();
let mut roots = default_blast_radius_roots(ws.path());
roots.push(ws.path().to_path_buf());
assert!(matches!(
replay_undo_with_authorized_roots(ws.path(), &run_id, &roots),
Err(DoctorRuntimeError::RunArtifactInvalid { .. })
));
assert_eq!(fs::read(&first).unwrap(), b"first-after");
assert_eq!(fs::read(&second).unwrap(), b"second-after");
assert!(!run_dir.join("undo_log.jsonl").exists());
}
#[test]
fn undo_intersects_recorded_and_current_blast_radius() {
let root = tempfile::tempdir().unwrap();
let workspace = root.path().join("workspace");
let sibling = root.path().join("sibling");
fs::create_dir(&workspace).unwrap();
fs::create_dir(&sibling).unwrap();
let original = workspace.join("original.txt");
let forged = sibling.join("forged.txt");
fs::write(&original, b"before").unwrap();
fs::write(&forged, b"peer-owned").unwrap();
let mut ctx =
RunContext::start(&workspace, "recorded-roots", vec![workspace.clone()], false)
.unwrap();
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
mutate(
&mut ctx,
&original,
Op::WriteFile {
bytes: b"after".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
let actions_path = run_dir.join("actions.jsonl");
let raw = fs::read_to_string(&actions_path).unwrap();
let mut action: serde_json::Value = serde_json::from_str(raw.trim()).unwrap();
action["path"] = serde_json::Value::String(forged.display().to_string());
fs::write(&actions_path, format!("{}\n", action)).unwrap();
assert!(matches!(
replay_undo_with_authorized_roots(&workspace, &run_id, &[root.path().to_path_buf()],),
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
assert_eq!(fs::read(&forged).unwrap(), b"peer-owned");
assert_eq!(fs::read(&original).unwrap(), b"after");
assert!(!run_dir.join("undo_log.jsonl").exists());
}
#[test]
fn undo_never_reuses_recorded_authority_missing_from_current_roots() {
let root = tempfile::tempdir().unwrap();
let workspace = root.path().join("workspace");
let external = root.path().join("external");
fs::create_dir(&workspace).unwrap();
fs::create_dir(&external).unwrap();
let target = external.join("target.txt");
fs::write(&target, b"before").unwrap();
let mut ctx = RunContext::start(
&workspace,
"current-roots",
vec![root.path().to_path_buf()],
false,
)
.unwrap();
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"after".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
assert!(matches!(
replay_undo_with_authorized_roots(
&workspace,
&run_id,
std::slice::from_ref(&workspace),
),
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
assert_eq!(fs::read(&target).unwrap(), b"after");
assert!(!run_dir.join("undo_log.jsonl").exists());
}
#[cfg(unix)]
#[test]
fn undo_rejects_recorded_root_retargeted_by_symlink() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let workspace = root.path().join("workspace");
let recorded_root = root.path().join("recorded-root");
let retained_root = root.path().join("retained-root");
let replacement_root = root.path().join("replacement-root");
fs::create_dir(&workspace).unwrap();
fs::create_dir(&recorded_root).unwrap();
fs::create_dir(&replacement_root).unwrap();
let target = recorded_root.join("target.txt");
fs::write(&target, b"before").unwrap();
let mut ctx = RunContext::start(
&workspace,
"retargeted-root",
vec![recorded_root.clone()],
false,
)
.unwrap();
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"after".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
fs::rename(&recorded_root, &retained_root).unwrap();
fs::write(replacement_root.join("target.txt"), b"after").unwrap();
symlink(&replacement_root, &recorded_root).unwrap();
assert!(matches!(
replay_undo_with_authorized_roots(
&workspace,
&run_id,
std::slice::from_ref(&recorded_root),
),
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
assert_eq!(
fs::read(replacement_root.join("target.txt")).unwrap(),
b"after"
);
assert_eq!(
fs::read(retained_root.join("target.txt")).unwrap(),
b"after"
);
assert!(!run_dir.join("undo_log.jsonl").exists());
}
#[test]
fn capabilities_report_only_advertises_wired_env_vars() {
let report = CapabilitiesReport::build("0.0.0-test", Path::new("/ws"));
let names: Vec<&str> = report.env_vars.iter().map(|entry| entry.name).collect();
assert_eq!(
names,
vec!["EE_DOCTOR_BLAST_RADIUS", "EE_NO_COLOR"],
"capabilities must only advertise env vars the runtime actually reads"
);
}
#[test]
fn write_file_creates_file_and_records_action() {
let ws = fresh_workspace();
let mut ctx = start_run(ws.path());
let target = ws.path().join("data.txt");
let line = mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"hello".to_vec(),
},
)
.expect("mutate");
assert_eq!(fs::read(&target).unwrap(), b"hello");
assert_eq!(line.kind, "write_file");
assert!(line.before_hash.is_none());
assert!(line.after_hash.is_some());
let actions_path = ctx.run_dir().join("actions.jsonl");
let raw = fs::read_to_string(&actions_path).unwrap();
assert_eq!(raw.lines().count(), 1);
}
#[test]
fn write_file_idempotent_same_bytes_returns_no_op() {
let ws = fresh_workspace();
let mut ctx = start_run(ws.path());
let target = ws.path().join("data.txt");
let _ = mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"same".to_vec(),
},
)
.expect("first");
let result = mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"same".to_vec(),
},
);
assert!(matches!(result, Err(DoctorRuntimeError::NoOpIdempotent)));
}
#[test]
fn write_file_backs_up_existing_content_before_overwrite() {
let ws = fresh_workspace();
let target = ws.path().join("data.txt");
fs::write(&target, b"original").unwrap();
let mut ctx = start_run(ws.path());
let line = mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"updated".to_vec(),
},
)
.expect("mutate");
let rel = line.backup_rel_path.expect("backup rel path");
let backup_path = ctx.run_dir().join("backups").join(&rel);
assert_eq!(fs::read(&backup_path).unwrap(), b"original");
assert_eq!(fs::read(&target).unwrap(), b"updated");
}
#[test]
fn blast_radius_refuses_writes_outside_allowed_roots() {
let ws = fresh_workspace();
let restricted = vec![ws.path().join(".ee")];
let mut ctx = RunContext::start(ws.path(), "abc1234", restricted, false).unwrap();
let outside = ws.path().parent().unwrap().join("evil.txt");
let result = mutate(
&mut ctx,
&outside,
Op::WriteFile {
bytes: b"nope".to_vec(),
},
);
assert!(matches!(
result,
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
}
#[test]
fn concurrency_second_start_refuses_with_lost() {
let ws = fresh_workspace();
let _first = start_run(ws.path());
let result = RunContext::start(
ws.path(),
"deadbeefcafe2",
vec![ws.path().to_path_buf()],
false,
);
assert!(matches!(
result,
Err(DoctorRuntimeError::ConcurrencyLost {
holder_run_id: None,
..
})
));
}
#[test]
fn concurrency_diagnostic_preserves_explicit_external_holder_label() {
let ws = fresh_workspace();
let ee_dir = ws.path().join(".ee");
fs::create_dir_all(&ee_dir).unwrap();
let lock_path = ee_dir.join(".doctor.lock");
fs::write(&lock_path, b"external-verifier-holder\n42\n").unwrap();
let lock = fs::OpenOptions::new()
.read(true)
.write(true)
.open(&lock_path)
.unwrap();
Fs4FileExt::try_lock(&lock).expect("external advisory lock should be available");
let result = RunContext::start(
ws.path(),
"contended-by-external-holder",
default_blast_radius_roots(ws.path()),
false,
);
assert!(matches!(
result,
Err(DoctorRuntimeError::ConcurrencyLost {
holder_run_id: Some(holder),
..
}) if holder == "external-verifier-holder"
));
Fs4FileExt::unlock(&lock).expect("release external advisory lock");
}
#[test]
fn reacquisition_never_overwrites_preexisting_unlocked_lock_path() {
let ws = fresh_workspace();
let ee_dir = ws.path().join(".ee");
fs::create_dir_all(&ee_dir).unwrap();
let lock_path = ee_dir.join(".doctor.lock");
let peer_bytes = b"unlocked peer replacement remains immutable";
fs::write(&lock_path, peer_bytes).unwrap();
let context = start_run(ws.path());
assert_eq!(fs::read(&lock_path).unwrap(), peer_bytes);
context.finish(RunStatus::CompletedOk).unwrap();
assert_eq!(fs::read(&lock_path).unwrap(), peer_bytes);
}
#[test]
fn finish_unlocks_persistent_lock_and_writes_state() {
let ws = fresh_workspace();
let ctx = start_run(ws.path());
let run_dir = ctx.run_dir().to_path_buf();
let lock = ws.path().join(".ee").join(".doctor.lock");
assert!(lock.exists());
ctx.finish(RunStatus::CompletedOk).expect("finish");
assert!(
lock.is_file(),
"the advisory lock file is intentionally persistent"
);
let state: RunState =
serde_json::from_slice(&fs::read(run_dir.join("state.json")).unwrap()).unwrap();
assert!(matches!(state.status, RunStatus::CompletedOk));
assert!(state.finished_at.is_some());
let next = start_run(ws.path());
next.finish(RunStatus::CompletedOk)
.expect("persistent lock must be reacquirable after finish");
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn finish_never_touches_lock_path_substituted_before_release() {
let ws = fresh_workspace();
let ctx = start_run(ws.path());
let lock = ws.path().join(".ee").join(".doctor.lock");
let retained_lock = ws.path().join(".ee").join(".doctor.lock-retained");
fs::rename(&lock, &retained_lock).expect("retain original lock inode");
let peer_bytes = b"peer-owned replacement lock";
fs::write(&lock, peer_bytes).expect("substitute peer-owned regular file");
let result = ctx.finish(RunStatus::CompletedOk);
assert!(result.is_ok(), "unlocking the retained handle must succeed");
assert_eq!(
fs::read(&lock).expect("read preserved replacement lock"),
peer_bytes,
"finish may not unlink or overwrite a substituted regular file"
);
assert!(
retained_lock.is_file(),
"the acquired lock inode remains untouched under its peer-assigned name"
);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn finish_never_touches_lock_path_substituted_during_release() {
let ws = fresh_workspace();
let ctx = start_run(ws.path());
let lock = ws.path().join(".ee").join(".doctor.lock");
let retained_lock = ws.path().join(".ee").join(".doctor.lock-retained");
let peer_bytes = b"peer replacement installed at the unlock boundary";
let hook_lock = lock.clone();
let hook_retained = retained_lock.clone();
set_doctor_lock_before_unlock_hook(move || {
fs::rename(&hook_lock, &hook_retained)
.expect("retain acquired lock at unlock boundary");
fs::write(&hook_lock, peer_bytes).expect("install peer replacement at unlock boundary");
});
ctx.finish(RunStatus::CompletedOk)
.expect("descriptor-only unlock must ignore namespace substitution");
assert_eq!(fs::read(&lock).expect("read peer replacement"), peer_bytes);
assert!(retained_lock.is_file());
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn drop_never_touches_lock_path_substituted_after_acquisition() {
let ws = fresh_workspace();
let lock = ws.path().join(".ee").join(".doctor.lock");
let retained_lock = ws.path().join(".ee").join(".doctor.lock-retained");
let peer_bytes = b"peer replacement before implicit drop";
{
let _ctx = start_run(ws.path());
fs::rename(&lock, &retained_lock).expect("retain acquired lock inode");
fs::write(&lock, peer_bytes).expect("install peer replacement");
}
assert_eq!(
fs::read(&lock).expect("read replacement after drop"),
peer_bytes
);
assert!(retained_lock.is_file());
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn completed_context_never_touches_lock_path_substituted_after_release() {
let ws = fresh_workspace();
let lock = ws.path().join(".ee").join(".doctor.lock");
let prior_lock = ws.path().join(".ee").join(".doctor.lock-prior");
start_run(ws.path())
.finish(RunStatus::CompletedOk)
.expect("finish releases advisory lock");
fs::rename(&lock, &prior_lock).expect("retain persistent lock after release");
let peer_bytes = b"peer replacement after release";
fs::write(&lock, peer_bytes).expect("install post-release replacement");
assert_eq!(fs::read(&lock).unwrap(), peer_bytes);
assert!(prior_lock.is_file());
}
#[test]
fn drop_unlocks_persistent_lock_when_finish_was_skipped() {
let ws = fresh_workspace();
let lock = ws.path().join(".ee").join(".doctor.lock");
{
let _ctx = start_run(ws.path());
assert!(lock.exists(), "lock should be held while ctx is alive");
}
assert!(
lock.is_file(),
"Drop releases the advisory lock without deleting its persistent file"
);
let ctx2 = start_run(ws.path());
ctx2.finish(RunStatus::CompletedOk).expect("finish");
assert!(lock.is_file());
}
#[test]
fn start_bounds_existing_lock_holder_read() {
let ws = fresh_workspace();
let ee_dir = ws.path().join(".ee");
fs::create_dir_all(&ee_dir).unwrap();
let lock_path = ee_dir.join(".doctor.lock");
let lock = fs::File::create(&lock_path).unwrap();
lock.set_len(DOCTOR_LOCK_FILE_INSPECT_LIMIT.saturating_add(1))
.unwrap();
Fs4FileExt::try_lock(&lock).expect("oversized advisory lock should be available");
let result = RunContext::start(
ws.path(),
"deadbeefcafe",
default_blast_radius_roots(ws.path()),
false,
);
match result {
Ok(_) => panic!("oversized doctor lock unexpectedly allowed RunContext::start"),
Err(DoctorRuntimeError::ConcurrencyLost {
lock_path: observed_lock_path,
holder_run_id,
}) => {
assert_eq!(
observed_lock_path,
fs::canonicalize(&lock_path).expect("canonicalize oversized doctor lock")
);
assert_eq!(holder_run_id, None);
}
Err(other) => {
panic!("expected oversized doctor lock to report concurrency, got {other:?}")
}
}
}
#[test]
fn failed_lock_metadata_write_never_removes_or_truncates_public_path() {
let ws = fresh_workspace();
let ee_dir = ws.path().join(".ee");
fs::create_dir_all(&ee_dir).unwrap();
let lock_path = ee_dir.join(".doctor.lock");
let original = b"peer-owned read-only lock contents";
fs::write(&lock_path, original).unwrap();
let mut read_only = fs::OpenOptions::new().read(true).open(&lock_path).unwrap();
Fs4FileExt::try_lock(&read_only).expect("read-only test advisory lock should be available");
let result = write_doctor_lock_contents(&mut read_only, "replacement\n");
assert!(
result.is_err(),
"read-only handle must reject lock metadata write"
);
assert_eq!(fs::read(&lock_path).unwrap(), original);
assert!(lock_path.is_file());
Fs4FileExt::unlock(&read_only).expect("release test advisory lock");
}
#[cfg(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
windows
))]
#[test]
fn failed_initial_lock_write_leaves_an_unlocked_reusable_persistent_file() {
let ws = fresh_workspace();
fail_next_doctor_lock_metadata_write();
let result = RunContext::start(
ws.path(),
"injected-lock-write-failure",
default_blast_radius_roots(ws.path()),
false,
);
assert!(matches!(
result,
Err(DoctorRuntimeError::Io { ref context, .. })
if context.contains("initialize persistent doctor lock")
));
assert_persistent_doctor_lock_released(ws.path());
let next = start_run(ws.path());
next.finish(RunStatus::CompletedOk)
.expect("existing persistent file must be reusable after failed initial write");
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn start_rejects_hard_linked_lock_without_overwriting_peer_inode() {
let ws = fresh_workspace();
let ee_dir = ws.path().join(".ee");
fs::create_dir_all(&ee_dir).unwrap();
let peer_path = ws.path().join("peer-owned-lock-source");
let peer_bytes = b"peer inode must remain byte-identical";
fs::write(&peer_path, peer_bytes).unwrap();
let lock_path = ee_dir.join(".doctor.lock");
fs::hard_link(&peer_path, &lock_path).unwrap();
let result = RunContext::start(
ws.path(),
"hard-linked-lock",
default_blast_radius_roots(ws.path()),
false,
);
assert!(matches!(result, Err(DoctorRuntimeError::Io { .. })));
assert_eq!(fs::read(&peer_path).unwrap(), peer_bytes);
assert_eq!(fs::read(&lock_path).unwrap(), peer_bytes);
}
#[test]
fn undo_restores_byte_identical_state_for_write_file() {
let ws = fresh_workspace();
let target = ws.path().join("data.txt");
fs::write(&target, b"original").unwrap();
let original_hash = hash_file(&target).unwrap();
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"updated_v1".to_vec(),
},
)
.unwrap();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"updated_v2".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
assert_eq!(fs::read(&target).unwrap(), b"updated_v2");
let summary = replay_test_undo(&run_dir).expect("undo");
assert_eq!(summary.actions_undone, 2);
assert_eq!(fs::read(&target).unwrap(), b"original");
assert_eq!(hash_file(&target).unwrap(), original_hash);
}
#[test]
fn undo_quarantines_files_that_didnt_exist_pre_run() {
let ws = fresh_workspace();
let target = ws.path().join("created.txt");
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"new".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
replay_test_undo(&run_dir).expect("undo");
assert!(!target.exists());
let undo_root = run_dir.join("quarantine").join("undo_created");
assert!(undo_root.is_dir());
let mut found = false;
let mut stack = vec![undo_root.clone()];
while let Some(d) = stack.pop() {
for entry in fs::read_dir(&d).unwrap().flatten() {
let p = entry.path();
if p.is_dir() {
stack.push(p);
} else if p.file_name().and_then(|s| s.to_str()) == Some("created.txt") {
found = true;
}
}
}
assert!(
found,
"quarantined created.txt not found under {}",
undo_root.display()
);
}
#[test]
fn undo_is_idempotent_when_called_twice() {
let ws = fresh_workspace();
let target = ws.path().join("d.txt");
fs::write(&target, b"orig").unwrap();
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"new".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
let s1 = replay_test_undo(&run_dir).unwrap();
assert_eq!(s1.actions_undone, 1);
let s2 = replay_test_undo(&run_dir).unwrap();
assert_eq!(s2.actions_undone, 0);
assert_eq!(s2.actions_skipped, 1);
}
#[test]
fn undo_recovers_when_inverse_completed_before_success_receipt() {
let ws = fresh_workspace();
let target = ws.path().join("inverse-before-receipt.txt");
fs::write(&target, b"before").unwrap();
let mut ctx = start_run(ws.path());
let run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"after".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
fs::write(&target, b"before").unwrap();
assert!(!run_dir.join("undo_log.jsonl").exists());
let summary = replay_test_undo(&run_dir).unwrap();
assert_eq!(summary.actions_undone, 1);
assert!(matches!(summary.status, RunStatus::Undone));
assert_eq!(fs::read(&target).unwrap(), b"before");
assert!(run_dir.join("undo_log.jsonl").is_file());
}
#[test]
fn undo_rejects_matching_success_receipt_when_live_state_is_not_undone() {
let ws = fresh_workspace();
let target = ws.path().join("forged-log-target.txt");
fs::write(&target, b"before").unwrap();
let mut ctx = start_run(ws.path());
let run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"after".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
fs::write(
run_dir.join("undo_log.jsonl"),
serde_json::json!({
"schema": "ee.doctor.undo_entry.v1",
"sequence": 1,
"path": target.display().to_string(),
"kind": "write_file",
"undone_at": Utc::now().to_rfc3339(),
})
.to_string()
+ "\n",
)
.unwrap();
assert!(matches!(
replay_test_undo(&run_dir),
Err(DoctorRuntimeError::RunArtifactInvalid { .. })
));
assert_eq!(fs::read(&target).unwrap(), b"after");
}
#[test]
fn undo_fails_closed_when_state_json_is_missing() {
let runs = fresh_workspace();
let run_dir = runs
.path()
.join(".doctor")
.join("runs")
.join("run_without_state");
fs::create_dir_all(&run_dir).unwrap();
fs::write(run_dir.join("actions.jsonl"), "").unwrap();
let result = replay_test_undo(&run_dir);
match result {
Err(DoctorRuntimeError::Io { context, source }) => {
assert!(context.contains("read state.json"), "{context}");
assert_eq!(source.kind(), io::ErrorKind::NotFound);
}
other => panic!("expected missing state.json to fail closed, got {other:?}"),
}
assert!(
!run_dir.join("undo_log.jsonl").exists(),
"undo must stop before replay artifacts are written"
);
}
#[test]
fn undo_fails_closed_when_state_json_is_corrupt() {
let runs = fresh_workspace();
let run_dir = runs
.path()
.join(".doctor")
.join("runs")
.join("run_with_corrupt_state");
fs::create_dir_all(&run_dir).unwrap();
fs::write(run_dir.join("state.json"), b"{not valid json").unwrap();
fs::write(run_dir.join("actions.jsonl"), "").unwrap();
let result = replay_test_undo(&run_dir);
match result {
Err(DoctorRuntimeError::Io { context, source }) => {
assert!(context.contains("parse state.json"), "{context}");
assert_eq!(source.kind(), io::ErrorKind::InvalidData);
}
other => panic!("expected corrupt state.json to fail closed, got {other:?}"),
}
assert!(
!run_dir.join("undo_log.jsonl").exists(),
"undo must stop before replay artifacts are written"
);
}
#[test]
fn undo_fails_closed_when_actions_jsonl_is_oversized() {
let ws = fresh_workspace();
let run_dir;
{
let ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
let actions_path = run_dir.join("actions.jsonl");
let actions = fs::File::create(&actions_path).unwrap();
actions
.set_len(DOCTOR_ACTION_LOG_INSPECT_LIMIT.saturating_add(1))
.unwrap();
let result = replay_test_undo(&run_dir);
match result {
Err(DoctorRuntimeError::Io { context, source }) => {
assert!(context.contains("read actions.jsonl"), "{context}");
assert_eq!(source.kind(), io::ErrorKind::InvalidData);
}
other => panic!("expected oversized actions.jsonl to fail closed, got {other:?}"),
}
assert!(
!run_dir.join("undo_log.jsonl").exists(),
"undo must stop before replay artifacts are written"
);
assert_persistent_doctor_lock_released(ws.path());
}
#[test]
fn undo_fails_closed_when_undo_log_jsonl_is_oversized() {
let ws = fresh_workspace();
let target = ws.path().join("data.txt");
fs::write(&target, b"original").unwrap();
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"updated".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
let undo_log_path = run_dir.join("undo_log.jsonl");
let undo_log = fs::File::create(&undo_log_path).unwrap();
undo_log
.set_len(DOCTOR_ACTION_LOG_INSPECT_LIMIT.saturating_add(1))
.unwrap();
let result = replay_test_undo(&run_dir);
match result {
Err(DoctorRuntimeError::Io { context, source }) => {
assert!(context.contains("read undo_log.jsonl"), "{context}");
assert_eq!(source.kind(), io::ErrorKind::InvalidData);
}
other => panic!("expected oversized undo_log.jsonl to fail closed, got {other:?}"),
}
assert_eq!(
fs::read(&target).unwrap(),
b"updated",
"undo must not start mutating before it can inspect the undo log"
);
assert_persistent_doctor_lock_released(ws.path());
}
#[test]
fn quarantine_by_rename_moves_file_into_run_quarantine() {
let ws = fresh_workspace();
let target = ws.path().join("trash.tmp");
fs::write(&target, b"junk").unwrap();
let mut ctx = start_run(ws.path());
let line = mutate(
&mut ctx,
&target,
Op::QuarantineByRename {
dest_under_quarantine: PathBuf::from("trash.tmp"),
},
)
.unwrap();
assert!(!target.exists());
assert!(ctx.run_dir().join("quarantine").join("trash.tmp").exists());
assert_eq!(line.kind, "quarantine_by_rename");
}
#[test]
fn manual_op_records_action_but_writes_nothing_to_disk() {
let ws = fresh_workspace();
let mut ctx = start_run(ws.path());
let target = ws.path().join("nonexistent");
let line = mutate(
&mut ctx,
&target,
Op::Manual {
steps: vec!["run X".into(), "then Y".into()],
},
)
.unwrap();
assert!(!target.exists());
assert_eq!(line.kind, "manual");
assert!(line.notes.is_some());
}
#[test]
fn capabilities_report_is_stable_and_self_describing() {
let ws = fresh_workspace();
let report = CapabilitiesReport::build("0.1.0", ws.path());
let json = serde_json::to_string_pretty(&report).unwrap();
assert!(json.contains("ee.doctor.capabilities.v1"));
assert!(json.contains("write_file"));
assert!(json.contains("quarantine_by_rename"));
assert!(json.contains("\"code\": 5"));
assert!(json.contains("configuration"));
assert!(json.contains("storage"));
assert!(json.contains("policy_denied"));
}
#[test]
fn derive_run_id_is_unique_for_fast_same_target_runs() {
let ids = (0..16)
.map(|_| derive_run_id("deadbeefcafe"))
.collect::<Vec<_>>();
let unique = ids.iter().collect::<std::collections::HashSet<_>>();
assert_eq!(
unique.len(),
ids.len(),
"same-target doctor runs must not reuse run directories: {ids:?}"
);
}
#[test]
fn dry_run_records_actions_without_touching_disk() {
let ws = fresh_workspace();
let target = ws.path().join("data.txt");
fs::write(&target, b"orig").unwrap();
let mut roots = default_blast_radius_roots(ws.path());
roots.push(ws.path().to_path_buf());
let mut ctx = RunContext::start(ws.path(), "sha", roots, true).unwrap();
let _line = mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"new".to_vec(),
},
)
.unwrap();
assert_eq!(fs::read(&target).unwrap(), b"orig");
let actions = fs::read_to_string(ctx.run_dir().join("actions.jsonl")).unwrap();
assert!(actions.contains("write_file"));
}
#[test]
fn undo_refuses_dry_run_plan_without_touching_later_state() {
let ws = fresh_workspace();
let target = ws.path().join("created-after-dry-run");
let mut roots = default_blast_radius_roots(ws.path());
roots.push(ws.path().to_path_buf());
let mut ctx = RunContext::start(ws.path(), "dry-undo", roots, true).unwrap();
let run_dir = ctx.run_dir().to_path_buf();
let run_id = ctx.run_id().to_owned();
mutate(&mut ctx, &target, Op::CreateDirAll { mode: 0o755 }).unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
fs::create_dir(&target).unwrap();
let state_before = fs::read(run_dir.join("state.json")).unwrap();
let mut current_roots = default_blast_radius_roots(ws.path());
current_roots.push(ws.path().to_path_buf());
assert!(matches!(
replay_undo_with_authorized_roots(ws.path(), &run_id, ¤t_roots),
Err(DoctorRuntimeError::DryRunNotUndoable { .. })
));
assert!(target.is_dir());
assert!(!run_dir.join("undo_log.jsonl").exists());
assert_eq!(fs::read(run_dir.join("state.json")).unwrap(), state_before);
}
#[test]
fn mutate_refuses_relative_writing_paths_before_logging_actions() {
let ws = fresh_workspace();
let cwd = std::env::current_dir().expect("current dir");
let mut ctx = RunContext::start(ws.path(), "sha", vec![cwd], true).unwrap();
let result = mutate(
&mut ctx,
Path::new("./relative-doctor-runtime-created-dir"),
Op::CreateDirAll { mode: 0o755 },
);
assert!(matches!(
result,
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
let actions = fs::read_to_string(ctx.run_dir().join("actions.jsonl")).unwrap();
assert!(
actions.is_empty(),
"relative writing paths must fail before actions are logged"
);
}
#[test]
fn create_dir_all_is_idempotent_on_existing_dir() {
let ws = fresh_workspace();
let mut ctx = start_run(ws.path());
let target = ws.path().join("subdir");
fs::create_dir_all(&target).unwrap();
let result = mutate(&mut ctx, &target, Op::CreateDirAll { mode: 0o755 });
assert!(matches!(result, Err(DoctorRuntimeError::NoOpIdempotent)));
}
#[test]
fn quarantine_refuses_path_traversal_with_parent_dir_components() {
let ws = fresh_workspace();
let target = ws.path().join("victim.txt");
fs::write(&target, b"hi").unwrap();
let mut ctx = start_run(ws.path());
let result = mutate(
&mut ctx,
&target,
Op::QuarantineByRename {
dest_under_quarantine: PathBuf::from("../../etc/passwd"),
},
);
assert!(matches!(
result,
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
assert!(target.exists());
assert_eq!(fs::read(&target).unwrap(), b"hi");
}
#[test]
fn undo_refuses_when_live_file_drifted_after_doctor_run() {
let ws = fresh_workspace();
let target = ws.path().join("data.txt");
fs::write(&target, b"original").unwrap();
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"doctor_wrote".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
fs::write(&target, b"external_writer_changed_this").unwrap();
let result = replay_test_undo(&run_dir);
let summary = result.expect("replay_undo returns Ok with partial status");
assert!(matches!(summary.status, RunStatus::UndonePartial));
assert!(summary.first_error.is_some());
let err = summary.first_error.unwrap();
assert!(
err.contains("drifted"),
"expected drift error, got: {}",
err
);
assert_eq!(fs::read(&target).unwrap(), b"external_writer_changed_this");
}
#[test]
fn undo_refuses_when_path_reoccupied_after_quarantine() {
let ws = fresh_workspace();
let victim = ws.path().join("orphan.wal");
fs::write(&victim, b"original wal").unwrap();
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&victim,
Op::QuarantineByRename {
dest_under_quarantine: PathBuf::from("orphan.wal"),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
fs::write(&victim, b"new_unrelated_file").unwrap();
let summary = replay_test_undo(&run_dir).expect("returns Ok with partial");
assert!(matches!(summary.status, RunStatus::UndonePartial));
let err = summary.first_error.unwrap();
assert!(
err.contains("drifted"),
"expected drift error, got: {}",
err
);
assert_eq!(fs::read(&victim).unwrap(), b"new_unrelated_file");
let quarantine = run_dir.join("quarantine").join("orphan.wal");
assert!(quarantine.exists());
assert_eq!(fs::read(&quarantine).unwrap(), b"original wal");
}
#[test]
fn two_writes_to_same_path_in_one_run_undo_byte_identical() {
let ws = fresh_workspace();
let target = ws.path().join("data.txt");
fs::write(&target, b"orig").unwrap();
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"v1".to_vec(),
},
)
.unwrap();
mutate(
&mut ctx,
&target,
Op::WriteFile {
bytes: b"v2".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
let b1 = run_dir.join("backups").join("000001");
let b2 = run_dir.join("backups").join("000002");
assert!(b1.is_dir(), "first backup dir missing: {}", b1.display());
assert!(b2.is_dir(), "second backup dir missing: {}", b2.display());
let summary = replay_test_undo(&run_dir).unwrap();
assert_eq!(summary.actions_undone, 2);
assert_eq!(fs::read(&target).unwrap(), b"orig");
}
#[test]
fn chmod_idempotent_when_mode_already_matches() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let ws = fresh_workspace();
let target = ws.path().join("perm.txt");
fs::write(&target, b"x").unwrap();
fs::set_permissions(&target, fs::Permissions::from_mode(0o644)).unwrap();
let mut ctx = start_run(ws.path());
let result = mutate(&mut ctx, &target, Op::Chmod { mode: 0o644 });
assert!(
matches!(result, Err(DoctorRuntimeError::NoOpIdempotent)),
"expected NoOpIdempotent, got: {:?}",
result
);
}
}
#[test]
fn write_file_atomic_refuses_path_without_parent() {
for parentless in [
Path::new(""),
Path::new("foo.txt"),
Path::new("./foo.txt"),
Path::new("../foo.txt"),
] {
let result = write_file_atomic(parentless, b"x");
assert!(
result.is_err(),
"write_file_atomic should refuse path lacking an absolute parent: {}",
parentless.display()
);
let err = result.unwrap_err();
assert!(
matches!(
err.kind(),
io::ErrorKind::InvalidInput | io::ErrorKind::NotFound
),
"unexpected error kind for {}: {:?}",
parentless.display(),
err.kind()
);
}
}
#[test]
fn two_writes_to_different_paths_with_same_basename_undo_correctly() {
let ws = fresh_workspace();
let a = ws.path().join("a/config.toml");
let b = ws.path().join("b/config.toml");
fs::create_dir_all(a.parent().unwrap()).unwrap();
fs::create_dir_all(b.parent().unwrap()).unwrap();
let run_dir;
{
let mut ctx = start_run(ws.path());
run_dir = ctx.run_dir().to_path_buf();
mutate(
&mut ctx,
&a,
Op::WriteFile {
bytes: b"contents-a".to_vec(),
},
)
.unwrap();
mutate(
&mut ctx,
&b,
Op::WriteFile {
bytes: b"contents-b".to_vec(),
},
)
.unwrap();
ctx.finish(RunStatus::CompletedOk).unwrap();
}
replay_test_undo(&run_dir).unwrap();
assert!(!a.exists());
assert!(!b.exists());
let q_root = run_dir.join("quarantine").join("undo_created");
let mut entries: Vec<String> = fs::read_dir(&q_root)
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.collect();
entries.sort();
assert_eq!(
entries,
vec!["000001".to_string(), "000002".to_string()],
"expected sequence-prefixed quarantine dirs, found: {:?}",
entries
);
}
#[test]
fn quarantine_refuses_absolute_path_destination() {
let ws = fresh_workspace();
let target = ws.path().join("victim.txt");
fs::write(&target, b"hi").unwrap();
let mut ctx = start_run(ws.path());
let result = mutate(
&mut ctx,
&target,
Op::QuarantineByRename {
dest_under_quarantine: PathBuf::from("/tmp/escape"),
},
);
assert!(matches!(
result,
Err(DoctorRuntimeError::BlastRadiusExceeded { .. })
));
assert!(target.exists());
}
}