use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::tools::WriteObserver;
pub const DEFAULT_RETAIN: usize = 50;
pub type CheckpointId = String;
fn now_ms() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
fn mint_id() -> CheckpointId {
use std::hash::BuildHasher;
let millis = now_ms();
let salt = std::collections::hash_map::RandomState::new().hash_one((
std::process::id(),
millis,
std::time::Instant::now(),
));
format!("{millis:020}-{:08x}", salt as u32)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CheckpointFileEntry {
pub path: String,
pub blob: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointManifest {
pub id: CheckpointId,
pub created_at_ms: u128,
pub label: String,
pub files: Vec<CheckpointFileEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointMeta {
pub id: CheckpointId,
pub created_at_ms: u128,
pub label: String,
pub file_count: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RestoreReport {
pub restored: Vec<String>,
pub refused: Vec<(String, String)>,
}
#[derive(Debug)]
pub struct CheckpointStore {
root: PathBuf,
}
impl CheckpointStore {
pub fn open(root: impl Into<PathBuf>) -> Result<Self> {
let root = root.into();
std::fs::create_dir_all(root.join("objects"))?;
std::fs::create_dir_all(root.join("checkpoints"))?;
Ok(CheckpointStore { root })
}
fn objects_dir(&self) -> PathBuf {
self.root.join("objects")
}
fn checkpoints_dir(&self) -> PathBuf {
self.root.join("checkpoints")
}
fn manifest_path(&self, id: &str) -> PathBuf {
self.checkpoints_dir().join(format!("{id}.json"))
}
fn blob_path(&self, hash: &str) -> PathBuf {
let prefix = &hash[..hash.len().min(2)];
self.objects_dir().join(prefix).join(hash)
}
fn validate_id(id: &str) -> Result<()> {
let bad = id.is_empty()
|| id.contains('/')
|| id.contains('\\')
|| id.contains('\0')
|| id.split(['/', '\\']).any(|c| c == ".." || c == ".")
|| Path::new(id).is_absolute()
|| id.trim() != id;
if bad {
return Err(Error::Other(format!("invalid checkpoint id: `{id}`")));
}
Ok(())
}
fn write_blob(&self, content: &[u8]) -> Result<String> {
let hash = crate::reduce::content_hash(content);
let dest = self.blob_path(&hash);
if dest.exists() {
return Ok(hash);
}
let Some(parent) = dest.parent() else {
return Err(Error::Other("blob path has no parent".to_string()));
};
std::fs::create_dir_all(parent)?;
let tmp = parent.join(format!(".tmp-{}-{}", std::process::id(), mint_id()));
std::fs::write(&tmp, content)?;
match std::fs::rename(&tmp, &dest) {
Ok(()) => {}
Err(e) if dest.exists() => {
let _ = std::fs::remove_file(&tmp);
let _ = e;
}
Err(e) => return Err(e.into()),
}
Ok(hash)
}
fn read_blob(&self, hash: &str) -> Result<Vec<u8>> {
std::fs::read(self.blob_path(hash)).map_err(Into::into)
}
fn write_manifest(&self, m: &CheckpointManifest) -> Result<()> {
Self::validate_id(&m.id)?;
let dest = self.manifest_path(&m.id);
let json = serde_json::to_vec_pretty(m).map_err(|e| Error::Other(e.to_string()))?;
let dir = self.checkpoints_dir();
std::fs::create_dir_all(&dir)?;
let tmp = dir.join(format!(".tmp-{}-{}", std::process::id(), mint_id()));
std::fs::write(&tmp, &json)?;
std::fs::rename(&tmp, &dest)?;
Ok(())
}
pub fn create_checkpoint(&self, label: &str) -> Result<CheckpointId> {
let id = mint_id();
let manifest = CheckpointManifest {
id: id.clone(),
created_at_ms: now_ms(),
label: label.to_string(),
files: Vec::new(),
};
self.write_manifest(&manifest)?;
Ok(id)
}
pub fn record_pre_image(&self, id: &str, rel: &str, content: Option<Vec<u8>>) -> Result<()> {
let mut manifest = self.manifest(id)?;
if manifest.files.iter().any(|f| f.path == rel) {
return Ok(());
}
let blob = match content {
Some(bytes) => Some(self.write_blob(&bytes)?),
None => None,
};
manifest.files.push(CheckpointFileEntry {
path: rel.to_string(),
blob,
});
self.write_manifest(&manifest)
}
pub fn manifest(&self, id: &str) -> Result<CheckpointManifest> {
Self::validate_id(id)?;
let text = std::fs::read_to_string(self.manifest_path(id))
.map_err(|e| Error::Other(format!("checkpoint `{id}` not found: {e}")))?;
serde_json::from_str(&text)
.map_err(|e| Error::Other(format!("checkpoint `{id}` manifest is corrupt: {e}")))
}
pub fn list(&self) -> Result<Vec<CheckpointMeta>> {
let dir = self.checkpoints_dir();
let mut metas = Vec::new();
if !dir.exists() {
return Ok(metas);
}
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
if let Ok(m) = serde_json::from_str::<CheckpointManifest>(&text) {
metas.push(CheckpointMeta {
id: m.id,
created_at_ms: m.created_at_ms,
label: m.label,
file_count: m.files.len(),
});
}
}
metas.sort_by(|a, b| b.id.cmp(&a.id));
Ok(metas)
}
pub fn turn_diff(&self, id: &str) -> Result<Vec<String>> {
Ok(self
.manifest(id)?
.files
.into_iter()
.map(|f| f.path)
.collect())
}
pub fn restore(
&self,
id: &str,
project_root: &Path,
protected_globs: &[String],
) -> Result<RestoreReport> {
let manifest = self.manifest(id)?;
let mut report = RestoreReport::default();
for entry in &manifest.files {
if let Some(reason) = reject_unsafe_manifest_path(&entry.path) {
report.refused.push((entry.path.clone(), reason));
continue;
}
let target = project_root.join(&entry.path);
let Some(normalized_rel) = normalized_project_rel(project_root, &target) else {
report.refused.push((
entry.path.clone(),
"refused: escapes the project root".to_string(),
));
continue;
};
if is_protected(&normalized_rel, protected_globs) {
report
.refused
.push((entry.path.clone(), "refused: protected path".to_string()));
continue;
}
if !contained(project_root, &target) {
report.refused.push((
entry.path.clone(),
"refused: escapes the project root".to_string(),
));
continue;
}
if let Some(resolved_rel) = resolved_project_rel(project_root, &target) {
if is_protected(&resolved_rel, protected_globs) {
report
.refused
.push((entry.path.clone(), "refused: protected path".to_string()));
continue;
}
}
match &entry.blob {
Some(hash) => {
let bytes = match self.read_blob(hash) {
Ok(b) => b,
Err(e) => {
report
.refused
.push((entry.path.clone(), format!("blob unreadable: {e}")));
continue;
}
};
if let Some(parent) = target.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::write(&target, &bytes) {
report
.refused
.push((entry.path.clone(), format!("write failed: {e}")));
continue;
}
}
None if target.exists() => {
if let Err(e) = std::fs::remove_file(&target) {
report
.refused
.push((entry.path.clone(), format!("delete failed: {e}")));
continue;
}
}
None => {}
}
report.restored.push(entry.path.clone());
}
Ok(report)
}
pub fn prune(&self, keep: usize) -> Result<usize> {
let mut metas = self.list()?; if metas.len() <= keep {
return Ok(0);
}
let stale = metas.split_off(keep);
let removed = stale.len();
for m in stale {
let _ = std::fs::remove_file(self.manifest_path(&m.id));
}
self.gc_unreferenced_blobs()?;
Ok(removed)
}
fn gc_unreferenced_blobs(&self) -> Result<()> {
let mut referenced: HashSet<String> = HashSet::new();
for meta in self.list()? {
if let Ok(m) = self.manifest(&meta.id) {
for f in m.files {
if let Some(b) = f.blob {
referenced.insert(b);
}
}
}
}
let objects = self.objects_dir();
if !objects.exists() {
return Ok(());
}
for entry in std::fs::read_dir(&objects)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
for inner in std::fs::read_dir(entry.path())? {
let inner = inner?;
let name = inner.file_name();
let Some(name) = name.to_str() else {
continue;
};
if name.starts_with(".tmp-") {
continue; }
if !referenced.contains(name) {
let _ = std::fs::remove_file(inner.path());
}
}
}
Ok(())
}
}
fn is_protected(rel: &str, extra_globs: &[String]) -> bool {
crate::safe_path::is_protected(rel, extra_globs)
}
fn reject_unsafe_manifest_path(rel: &str) -> Option<String> {
crate::safe_path::reject_unsafe_rel_path(rel)
}
fn normalized_project_rel(root: &Path, path: &Path) -> Option<String> {
crate::safe_path::normalized_project_rel(root, path)
}
fn contained(root: &Path, path: &Path) -> bool {
crate::safe_path::contained(root, path)
}
fn resolved_project_rel(root: &Path, path: &Path) -> Option<String> {
crate::safe_path::resolved_project_rel(root, path)
}
#[derive(Debug, Default)]
struct ObserverState {
current: Option<CheckpointId>,
captured: HashSet<String>,
}
#[derive(Debug)]
pub struct CheckpointObserver {
store: CheckpointStore,
project_root: PathBuf,
retain: usize,
protected: Vec<String>,
state: Mutex<ObserverState>,
disabled: AtomicBool,
}
impl CheckpointObserver {
pub fn new(
store: CheckpointStore,
project_root: PathBuf,
retain: usize,
protected: Vec<String>,
) -> Self {
CheckpointObserver {
store,
project_root,
retain: retain.max(1),
protected,
state: Mutex::new(ObserverState::default()),
disabled: AtomicBool::new(false),
}
}
pub fn store(&self) -> &CheckpointStore {
&self.store
}
pub fn list(&self) -> Result<Vec<CheckpointMeta>> {
self.store.list()
}
pub fn turn_diff(&self, id: &str) -> Result<Vec<String>> {
self.store.turn_diff(id)
}
pub fn restore(&self, id: &str) -> Result<RestoreReport> {
self.store.restore(id, &self.project_root, &self.protected)
}
pub fn is_disabled(&self) -> bool {
self.disabled.load(Ordering::Relaxed)
}
pub fn begin_turn(&self, label: &str) -> Option<CheckpointId> {
if self.disabled.load(Ordering::Relaxed) {
return None;
}
let short: String = label.chars().take(120).collect();
match self.store.create_checkpoint(&short) {
Ok(id) => {
if let Ok(mut st) = self.state.lock() {
st.current = Some(id.clone());
st.captured.clear();
}
if let Err(e) = self.store.prune(self.retain) {
eprintln!("warning: checkpoint: prune failed: {e}");
}
Some(id)
}
Err(e) => {
eprintln!(
"warning: checkpoint disabled for the rest of this session — \
failed to open a new checkpoint: {e}"
);
self.disabled.store(true, Ordering::Relaxed);
None
}
}
}
pub fn current(&self) -> Option<CheckpointId> {
self.state.lock().ok().and_then(|s| s.current.clone())
}
}
#[async_trait::async_trait]
impl WriteObserver for CheckpointObserver {
async fn before_write(&self, path: &Path) {
if self.disabled.load(Ordering::Relaxed) {
return;
}
if !contained(&self.project_root, path) {
return;
}
let Some(normalized) = crate::tools::normalize(path) else {
return;
};
let Some(root_normalized) = crate::tools::normalize(&self.project_root) else {
return;
};
let Ok(rel_path) = normalized.strip_prefix(&root_normalized) else {
return;
};
let rel = rel_path.to_string_lossy().replace('\\', "/");
if rel.is_empty() || rel == ".git" || rel.starts_with(".git/") {
return;
}
let mut st = match self.state.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if st.current.is_none() {
drop(st);
self.begin_turn("untracked");
st = match self.state.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
}
if st.captured.contains(&rel) {
return;
}
let Some(id) = st.current.clone() else {
return; };
let content = std::fs::read(path).ok(); match self.store.record_pre_image(&id, &rel, content) {
Ok(()) => {
st.captured.insert(rel);
}
Err(e) => {
eprintln!(
"warning: checkpoint disabled for the rest of this session — \
failed to record a snapshot: {e}"
);
self.disabled.store(true, Ordering::Relaxed);
}
}
}
async fn after_write(&self, _path: &Path) -> Option<String> {
None
}
}
fn project_tag(cwd: &Path) -> String {
use std::hash::{Hash, Hasher};
let canon = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
let mut h = std::collections::hash_map::DefaultHasher::new();
canon.hash(&mut h);
format!("{:016x}", h.finish())
}
fn default_shadow_root(cwd: &Path) -> PathBuf {
crate::agent::global_instructions_dir()
.join("checkpoints")
.join(project_tag(cwd))
}
pub fn observer_for_config(config: &crate::Config) -> Option<std::sync::Arc<CheckpointObserver>> {
if !config.checkpoint_enabled {
return None;
}
let root = config
.checkpoint_dir
.clone()
.unwrap_or_else(|| default_shadow_root(&config.cwd));
match CheckpointStore::open(&root) {
Ok(store) => Some(std::sync::Arc::new(CheckpointObserver::new(
store,
config.cwd.clone(),
config.checkpoint_retain,
config.permissions_protected_paths.clone(),
))),
Err(e) => {
eprintln!(
"warning: [capabilities.checkpoint] is enabled but the shadow store at \
{} could not be opened — checkpoint is disabled for this session: {e}",
root.display()
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"supercode-checkpoint-test-{tag}-{}-{}",
std::process::id(),
mint_id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn open_creates_objects_and_checkpoints_dirs() {
let root = tmp("open");
let store = CheckpointStore::open(&root).unwrap();
assert!(root.join("objects").is_dir());
assert!(root.join("checkpoints").is_dir());
drop(store);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn create_list_and_manifest_round_trip() {
let root = tmp("list");
let store = CheckpointStore::open(&root).unwrap();
let id1 = store.create_checkpoint("first turn").unwrap();
std::thread::sleep(std::time::Duration::from_millis(2));
let id2 = store.create_checkpoint("second turn").unwrap();
let metas = store.list().unwrap();
assert_eq!(metas.len(), 2);
assert_eq!(metas[0].id, id2);
assert_eq!(metas[1].id, id1);
assert_eq!(metas[0].label, "second turn");
let m = store.manifest(&id1).unwrap();
assert_eq!(m.id, id1);
assert!(m.files.is_empty());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn record_pre_image_is_idempotent_keeping_the_earliest() {
let root = tmp("idempotent");
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
store
.record_pre_image(&id, "a.txt", Some(b"first".to_vec()))
.unwrap();
store
.record_pre_image(&id, "a.txt", Some(b"second".to_vec()))
.unwrap();
let m = store.manifest(&id).unwrap();
assert_eq!(m.files.len(), 1);
let bytes = store
.read_blob(m.files[0].blob.as_deref().unwrap())
.unwrap();
assert_eq!(bytes, b"first");
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn blob_dedup_two_identical_contents_share_one_object() {
let root = tmp("dedup");
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
store
.record_pre_image(&id, "a.txt", Some(b"same".to_vec()))
.unwrap();
store
.record_pre_image(&id, "b.txt", Some(b"same".to_vec()))
.unwrap();
let m = store.manifest(&id).unwrap();
assert_eq!(m.files[0].blob, m.files[1].blob);
let mut count = 0;
for entry in walkdir(&root.join("objects")) {
if entry.is_file() && !entry.to_string_lossy().contains(".tmp-") {
count += 1;
}
}
assert_eq!(count, 1);
std::fs::remove_dir_all(&root).ok();
}
fn walkdir(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let Ok(rd) = std::fs::read_dir(dir) else {
return out;
};
for entry in rd.flatten() {
let p = entry.path();
if p.is_dir() {
out.extend(walkdir(&p));
} else {
out.push(p);
}
}
out
}
#[test]
fn restore_rewrites_modified_and_deletes_created_files() {
let root = tmp("restore");
let project = tmp("restore-project");
std::fs::write(project.join("existing.txt"), "modified").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
store
.record_pre_image(&id, "existing.txt", Some(b"original".to_vec()))
.unwrap();
store.record_pre_image(&id, "new.txt", None).unwrap();
std::fs::write(project.join("new.txt"), "brand new").unwrap();
let report = store.restore(&id, &project, &[]).unwrap();
assert!(report.refused.is_empty(), "{:?}", report.refused);
assert_eq!(report.restored.len(), 2);
assert_eq!(
std::fs::read_to_string(project.join("existing.txt")).unwrap(),
"original"
);
assert!(!project.join("new.txt").exists());
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn restore_refuses_a_manifest_entry_that_traverses_outside_the_project_root() {
let root = tmp("hostile");
let project = tmp("hostile-project");
std::fs::create_dir_all(&project).unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: "../../../../../../etc/passwd-supercode-test".to_string(),
blob: None,
});
store.write_manifest(&m).unwrap();
let victim = project
.parent()
.unwrap()
.parent()
.unwrap()
.join("etc/passwd-supercode-test");
assert!(
!victim.exists(),
"test precondition: victim path must not already exist"
);
let report = store.restore(&id, &project, &[]).unwrap();
assert_eq!(report.restored.len(), 0);
assert_eq!(report.refused.len(), 1);
assert!(report.refused[0].1.contains("escapes"));
assert!(
!victim.exists(),
"restore must never have written outside the project root"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn restore_refuses_dot_git_unconditionally() {
let root = tmp("gitfloor");
let project = tmp("gitfloor-project");
std::fs::create_dir_all(project.join(".git")).unwrap();
std::fs::write(project.join(".git").join("config"), "real git config").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: ".git/config".to_string(),
blob: None,
});
store.write_manifest(&m).unwrap();
let report = store.restore(&id, &project, &[]).unwrap();
assert_eq!(report.restored.len(), 0);
assert_eq!(report.refused.len(), 1);
assert!(report.refused[0].1.contains("protected"));
assert_eq!(
std::fs::read_to_string(project.join(".git").join("config")).unwrap(),
"real git config",
"the real .git must be untouched"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn restore_honors_extra_protected_globs() {
let root = tmp("protectedglob");
let project = tmp("protectedglob-project");
std::fs::write(project.join(".env"), "SECRET=1").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: ".env".to_string(),
blob: Some(store.write_blob(b"OLD=1").unwrap()),
});
store.write_manifest(&m).unwrap();
let report = store
.restore(&id, &project, &[".env*".to_string()])
.unwrap();
assert_eq!(report.restored.len(), 0);
assert_eq!(report.refused.len(), 1);
assert_eq!(
std::fs::read_to_string(project.join(".env")).unwrap(),
"SECRET=1"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn restore_refuses_traversal_into_dot_git_real_git_config_stays_untouched() {
let root = tmp("traversal-git");
let project = tmp("traversal-git-project");
std::fs::create_dir_all(project.join(".git")).unwrap();
std::fs::write(project.join(".git").join("config"), "real git config").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: "x/../.git/config".to_string(),
blob: Some(store.write_blob(b"PWNED-by-traversal").unwrap()),
});
store.write_manifest(&m).unwrap();
let report = store.restore(&id, &project, &[]).unwrap();
assert_eq!(report.restored.len(), 0, "must not restore into .git");
assert_eq!(report.refused.len(), 1);
assert!(
report.refused[0].1.contains("escapes"),
"unexpected refusal reason: {}",
report.refused[0].1
);
assert_eq!(
std::fs::read_to_string(project.join(".git").join("config")).unwrap(),
"real git config",
"the real .git/config must be untouched by the traversal entry"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn restore_refuses_traversal_bypass_of_protected_globs_dot_env_stays_untouched() {
let root = tmp("traversal-env");
let project = tmp("traversal-env-project");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(project.join(".env"), "SECRET=1").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: "x/../.env".to_string(),
blob: Some(store.write_blob(b"PWNED=1").unwrap()),
});
store.write_manifest(&m).unwrap();
let report = store
.restore(&id, &project, &[".env*".to_string()])
.unwrap();
assert_eq!(report.restored.len(), 0);
assert_eq!(report.refused.len(), 1);
assert_eq!(
std::fs::read_to_string(project.join(".env")).unwrap(),
"SECRET=1",
"the real .env must be untouched by the traversal entry"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn restore_refuses_an_absolute_manifest_path() {
let root = tmp("absolute");
let project = tmp("absolute-project");
std::fs::create_dir_all(project.join(".git")).unwrap();
std::fs::write(project.join(".git").join("config"), "real git config").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let absolute_git_config = project.join(".git").join("config");
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: absolute_git_config.to_string_lossy().to_string(),
blob: Some(store.write_blob(b"PWNED-by-absolute-path").unwrap()),
});
store.write_manifest(&m).unwrap();
let report = store.restore(&id, &project, &[]).unwrap();
assert_eq!(report.restored.len(), 0);
assert_eq!(report.refused.len(), 1);
assert!(
report.refused[0].1.contains("escapes"),
"unexpected refusal reason: {}",
report.refused[0].1
);
assert_eq!(
std::fs::read_to_string(project.join(".git").join("config")).unwrap(),
"real git config"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn restore_still_restores_a_clean_relative_nested_entry() {
let root = tmp("cleanroundtrip");
let project = tmp("cleanroundtrip-project");
std::fs::create_dir_all(project.join("src")).unwrap();
std::fs::write(project.join("src").join("main.rs"), "fn main() {}").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
store
.record_pre_image(&id, "src/main.rs", Some(b"fn old() {}".to_vec()))
.unwrap();
let report = store.restore(&id, &project, &[]).unwrap();
assert!(report.refused.is_empty(), "{:?}", report.refused);
assert_eq!(report.restored, vec!["src/main.rs".to_string()]);
assert_eq!(
std::fs::read_to_string(project.join("src").join("main.rs")).unwrap(),
"fn old() {}"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
#[cfg(unix)]
fn restore_refuses_symlink_traversal_into_dot_git() {
let root = tmp("symlink-traversal");
let project = tmp("symlink-traversal-project");
std::fs::create_dir_all(project.join(".git")).unwrap();
std::fs::write(project.join(".git").join("config"), "real git config").unwrap();
std::os::unix::fs::symlink(project.join(".git"), project.join("foo")).unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: "foo/config".to_string(),
blob: Some(store.write_blob(b"PWNED-VIA-SYMLINK").unwrap()),
});
store.write_manifest(&m).unwrap();
let report = store.restore(&id, &project, &[]).unwrap();
assert_eq!(
report.restored.len(),
0,
"must not restore through the symlink into .git"
);
assert_eq!(report.refused.len(), 1);
assert!(
report.refused[0].1.contains("protected"),
"unexpected refusal reason: {}",
report.refused[0].1
);
assert_eq!(
std::fs::read_to_string(project.join(".git").join("config")).unwrap(),
"real git config",
"the real .git/config must be byte-identical — untouched by the symlink entry"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
#[cfg(unix)]
fn restore_refuses_symlink_traversal_into_a_protected_glob() {
let root = tmp("symlink-glob");
let project = tmp("symlink-glob-project");
std::fs::create_dir_all(project.join("real_env")).unwrap();
std::fs::write(project.join("real_env").join("creds"), "real secret").unwrap();
std::os::unix::fs::symlink(project.join("real_env"), project.join("secrets")).unwrap();
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
let mut m = store.manifest(&id).unwrap();
m.files.push(CheckpointFileEntry {
path: "secrets/creds".to_string(),
blob: Some(store.write_blob(b"PWNED-VIA-SYMLINK-GLOB").unwrap()),
});
store.write_manifest(&m).unwrap();
let report = store
.restore(&id, &project, &["real_env/**".to_string()])
.unwrap();
assert_eq!(report.restored.len(), 0);
assert_eq!(report.refused.len(), 1);
assert!(
report.refused[0].1.contains("protected"),
"unexpected refusal reason: {}",
report.refused[0].1
);
assert_eq!(
std::fs::read_to_string(project.join("real_env").join("creds")).unwrap(),
"real secret",
"the real protected file must be untouched by the symlink entry"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn prune_keeps_only_the_newest_and_gcs_unreferenced_blobs() {
let root = tmp("prune");
let store = CheckpointStore::open(&root).unwrap();
for i in 0..5 {
let id = store.create_checkpoint(&format!("turn {i}")).unwrap();
store
.record_pre_image(&id, "f.txt", Some(format!("content-{i}").into_bytes()))
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(store.list().unwrap().len(), 5);
let removed = store.prune(2).unwrap();
assert_eq!(removed, 3);
let remaining = store.list().unwrap();
assert_eq!(remaining.len(), 2);
assert_eq!(remaining[0].label, "turn 4");
assert_eq!(remaining[1].label, "turn 3");
for meta in &remaining {
let m = store.manifest(&meta.id).unwrap();
for f in &m.files {
if let Some(hash) = &f.blob {
store.read_blob(hash).unwrap();
}
}
}
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn turn_diff_lists_exactly_the_captured_files() {
let root = tmp("diff");
let store = CheckpointStore::open(&root).unwrap();
let id = store.create_checkpoint("t").unwrap();
store.record_pre_image(&id, "a.rs", None).unwrap();
store
.record_pre_image(&id, "b.rs", Some(b"x".to_vec()))
.unwrap();
let mut diff = store.turn_diff(&id).unwrap();
diff.sort();
assert_eq!(diff, vec!["a.rs".to_string(), "b.rs".to_string()]);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn manifest_rejects_a_path_traversing_id() {
let root = tmp("badid");
let store = CheckpointStore::open(&root).unwrap();
assert!(store.manifest("../../etc/passwd").is_err());
assert!(store.restore("../../etc/passwd", &root, &[]).is_err());
std::fs::remove_dir_all(&root).ok();
}
#[tokio::test]
async fn observer_before_write_ignores_paths_outside_the_project_root() {
let root = tmp("obs-outside");
let project = tmp("obs-outside-project");
let outside = tmp("obs-outside-elsewhere");
std::fs::write(outside.join("victim.txt"), "do not touch").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let observer = CheckpointObserver::new(store, project.clone(), DEFAULT_RETAIN, vec![]);
observer.begin_turn("t");
observer.before_write(&outside.join("victim.txt")).await;
let id = observer.current().unwrap();
let diff = observer.store().turn_diff(&id).unwrap();
assert!(
diff.is_empty(),
"must not capture writes outside project_root"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
std::fs::remove_dir_all(&outside).ok();
}
#[tokio::test]
async fn observer_captures_only_the_first_write_to_a_path_in_a_turn() {
let root = tmp("obs-firstwrite");
let project = tmp("obs-firstwrite-project");
std::fs::write(project.join("f.txt"), "v1").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let observer = CheckpointObserver::new(store, project.clone(), DEFAULT_RETAIN, vec![]);
observer.begin_turn("t");
observer.before_write(&project.join("f.txt")).await;
std::fs::write(project.join("f.txt"), "v2").unwrap();
observer.before_write(&project.join("f.txt")).await; let id = observer.current().unwrap();
let m = observer.store().manifest(&id).unwrap();
assert_eq!(m.files.len(), 1);
let bytes = observer
.store()
.read_blob(m.files[0].blob.as_deref().unwrap())
.unwrap();
assert_eq!(bytes, b"v1", "must keep the EARLIEST pre-image, not v2");
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[tokio::test]
async fn observer_begin_turn_clears_captured_set_for_a_new_turn() {
let root = tmp("obs-newturn");
let project = tmp("obs-newturn-project");
std::fs::write(project.join("f.txt"), "v1").unwrap();
let store = CheckpointStore::open(&root).unwrap();
let observer = CheckpointObserver::new(store, project.clone(), DEFAULT_RETAIN, vec![]);
observer.begin_turn("turn 1");
observer.before_write(&project.join("f.txt")).await;
std::fs::write(project.join("f.txt"), "v2").unwrap();
observer.begin_turn("turn 2");
observer.before_write(&project.join("f.txt")).await;
let id2 = observer.current().unwrap();
let m2 = observer.store().manifest(&id2).unwrap();
assert_eq!(m2.files.len(), 1);
let bytes = observer
.store()
.read_blob(m2.files[0].blob.as_deref().unwrap())
.unwrap();
assert_eq!(
bytes, b"v2",
"turn 2's checkpoint must capture v2 as ITS pre-image"
);
std::fs::remove_dir_all(&root).ok();
std::fs::remove_dir_all(&project).ok();
}
#[test]
fn is_protected_hard_floor_covers_dot_git_regardless_of_extra_globs() {
assert!(is_protected(".git", &[]));
assert!(is_protected(".git/config", &[]));
assert!(is_protected(".git/objects/aa/bb", &[]));
assert!(!is_protected(".gitignore", &[]));
assert!(!is_protected("src/main.rs", &[]));
}
#[test]
fn contained_rejects_symlink_escape_for_an_existing_target() {
let project = tmp("symlink-project");
let outside = tmp("symlink-outside");
std::fs::write(outside.join("secret.txt"), "s").unwrap();
#[cfg(unix)]
{
std::os::unix::fs::symlink(outside.join("secret.txt"), project.join("link.txt"))
.unwrap();
assert!(!contained(&project, &project.join("link.txt")));
}
std::fs::remove_dir_all(&project).ok();
std::fs::remove_dir_all(&outside).ok();
}
#[test]
fn contained_accepts_a_brand_new_file_inside_the_root() {
let project = tmp("newfile-project");
assert!(contained(&project, &project.join("does_not_exist_yet.txt")));
assert!(contained(&project, &project.join("nested/dir/new.txt")));
std::fs::remove_dir_all(&project).ok();
}
}