use super::delivered::{self, Lineage, LineageRefusal};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecoveryId(String);
impl RecoveryId {
#[must_use]
pub fn parse(id: &str) -> Option<Self> {
(id.len() == 64
&& id
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()))
.then(|| Self(id.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryRefusal {
UnknownLineage,
SourceNotPresented,
SourceNoLongerAdmitted,
NotStorable,
Missing,
}
impl From<LineageRefusal> for RecoveryRefusal {
fn from(refusal: LineageRefusal) -> Self {
match refusal {
LineageRefusal::Unknown => Self::UnknownLineage,
LineageRefusal::SourceNotPresented => Self::SourceNotPresented,
LineageRefusal::SourceNoLongerAdmitted => Self::SourceNoLongerAdmitted,
}
}
}
impl std::fmt::Display for RecoveryRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::UnknownLineage => "recovery.unknown_lineage",
Self::SourceNotPresented => "recovery.source_not_presented",
Self::SourceNoLongerAdmitted => "recovery.source_no_longer_admitted",
Self::NotStorable => "recovery.not_storable",
Self::Missing => "recovery.missing",
})
}
}
fn storable_exactly(original: &str) -> bool {
!original.is_empty()
&& original.len() <= super::MAX_ARCHIVE_SIZE
&& crate::core::policy::content::protect_active(original)
.is_ok_and(|protected| &*protected == original)
&& crate::core::context_admission::recovery::admit_for_storage(original)
.is_some_and(|admitted| admitted == original)
}
fn lineage_id(lineage: &Lineage, content: &str) -> Option<String> {
let mut hash = blake3::Hasher::new();
hash.update(b"leanctx-lineage-archive-v1\0");
hash.update(lineage.identity_digest()?.as_bytes());
hash.update(b"\0");
hash.update(content.as_bytes());
Some(hash.finalize().to_hex().to_string())
}
pub fn store_recovery_with_lineage(
original: &str,
lineage: &Lineage,
) -> Result<RecoveryId, RecoveryRefusal> {
lineage.still_admitted()?;
let storable = crate::core::policy::runtime::with_source_view(|| storable_exactly(original))
.unwrap_or(false);
if !storable || !super::is_enabled() {
return Err(RecoveryRefusal::NotStorable);
}
if let Some(files) = lineage.files_only() {
return super::store_with_authority(
"recovery",
"recovery-copy",
original,
None,
Some(files),
)
.filter(|stored| !stored.truncated)
.and_then(|stored| RecoveryId::parse(&stored.id))
.inspect(issued)
.ok_or(RecoveryRefusal::NotStorable);
}
let id = lineage_id(lineage, original).ok_or(RecoveryRefusal::UnknownLineage)?;
let stored = super::with_archive_lock(|| {
let content_path = super::content_path(&id);
if content_path.exists() && super::meta_path(&id).exists() {
return Some(());
}
std::fs::create_dir_all(super::entry_dir(&id)).ok()?;
if !content_path.exists() {
crate::core::atomic_fs::try_atomic_write(&content_path, original.as_bytes(), None)
.ok()?;
#[cfg(unix)]
super::set_private_file_perms(&content_path);
}
let mut entry = super::new_entry(
&id,
"recovery",
"recovery-copy",
original,
None,
chrono::Utc::now(),
);
entry.lineage = Some(lineage.recorded_for_presenter());
if super::write_metadata(&entry).is_none() {
let _ = std::fs::remove_file(content_path);
return None;
}
Some(())
})
.flatten();
match stored {
Some(()) => RecoveryId::parse(&id)
.inspect(issued)
.ok_or(RecoveryRefusal::NotStorable),
None => Err(RecoveryRefusal::NotStorable),
}
}
fn issued(id: &RecoveryId) {
crate::core::context_admission::receipt_store::references::note_recovery(id.as_str());
}
pub fn retrieve_recovery_checked(id: &RecoveryId) -> Result<String, RecoveryRefusal> {
let entry = super::read_entry(id.as_str()).ok_or(RecoveryRefusal::Missing)?;
let Some(lineage) = entry.lineage else {
if entry.authority.is_none() {
return Err(RecoveryRefusal::UnknownLineage);
}
return super::retrieve_checked(id.as_str()).map_err(|error| match error {
super::ArchiveResolveError::Refused(_) => RecoveryRefusal::SourceNoLongerAdmitted,
super::ArchiveResolveError::Missing | super::ArchiveResolveError::Malformed => {
RecoveryRefusal::Missing
}
});
};
lineage.still_admitted()?;
let content = crate::core::atomic_fs::read_store_file(
&super::archive_base_dir(),
&super::content_path(id.as_str()),
)
.map_err(|_| RecoveryRefusal::Missing)?;
if content.len() > super::MAX_ARCHIVE_SIZE
|| lineage_id(&lineage, &content).as_deref() != Some(id.as_str())
{
return Err(RecoveryRefusal::Missing);
}
let released = crate::core::policy::runtime::with_source_view(|| {
crate::core::policy::content::protect_active(&content)
.is_ok_and(|protected| *protected == content)
&& readmitted(id, &content)
})
.unwrap_or(false);
if !released {
return Err(RecoveryRefusal::SourceNoLongerAdmitted);
}
delivered::record_lineage(&delivered::digest(&content), &lineage);
Ok(content)
}
fn readmitted(id: &RecoveryId, content: &str) -> bool {
let unchanged = crate::core::context_admission::admit_bound_context_source(
content,
&crate::core::context_admission::origin::SourceClass::Unclassified { tool: "recovery" },
)
.is_ok();
if unchanged {
let reference = format!("recovery:{}", id.as_str());
crate::core::context_admission::capture::note_source_id(&reference);
}
unchanged
}
#[must_use]
pub fn recovery_lineage(id: &RecoveryId) -> Option<Lineage> {
super::read_entry(id.as_str())?.lineage
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::*;
use crate::core::archive::authority::ArchiveAuthority;
use crate::core::archive::presented::{PresentedBinding, record_presentation};
use crate::core::policy::runtime::{REQUEST_PROJECT, TestPolicyOverride};
const GOVERNED: &str = "[context_gateway]\nmode = \"governed\"\nclassification = \"warn\"\n";
const ORIGINAL: &str = "pub fn kept() {}\n";
fn isolated(config: &str) -> crate::core::data_dir::IsolatedDataDir {
let data = crate::core::data_dir::isolated_data_dir();
let dir = crate::core::paths::config_dir_read_only().expect("isolated config dir");
std::fs::create_dir_all(&dir).expect("config dir");
std::fs::write(dir.join("config.toml"), config).expect("config");
data
}
fn project() -> (tempfile::TempDir, PathBuf) {
let directory = tempfile::tempdir().expect("project");
let root = directory.path().canonicalize().expect("canonical root");
(directory, root)
}
fn scoped<T>(root: &Path, operation: impl FnOnce() -> T) -> T {
REQUEST_PROJECT.sync_scope(std::cell::RefCell::new(Some(root.to_path_buf())), operation)
}
fn file_lineage(root: &Path, name: &str, content: &str) -> Lineage {
let path = root.join(name);
std::fs::write(&path, content).expect("source");
let admitted = crate::core::rooted_read::read_file_for_tool_rooted(
path.to_str().expect("utf-8 path"),
root.to_str().expect("utf-8 root"),
"ctx_read",
)
.expect("admitted read");
Lineage::of_files(
ArchiveAuthority::file(
root,
path.to_str().expect("utf-8 path"),
"ctx_read",
&admitted,
)
.expect("protection is active: the read is bound"),
)
}
fn stored_files(data: &crate::core::data_dir::IsolatedDataDir) -> usize {
fn count(directory: &Path) -> usize {
std::fs::read_dir(directory).map_or(0, |entries| {
entries
.flatten()
.map(|entry| {
let path = entry.path();
if path.is_dir() {
count(&path)
} else {
usize::from(path.extension().is_some_and(|ext| ext == "txt"))
}
})
.sum()
})
}
count(&data.path().join("archives"))
}
#[test]
fn a_file_bound_recovery_copy_resolves_only_while_its_source_holds() {
let _data = isolated(GOVERNED);
let _community = TestPolicyOverride::set(None);
let (_directory, root) = project();
scoped(&root, || {
let lineage = file_lineage(&root, "kept.rs", ORIGINAL);
let id = store_recovery_with_lineage(ORIGINAL, &lineage).expect("stored");
assert_eq!(
store_recovery_with_lineage(ORIGINAL, &lineage).as_ref(),
Ok(&id),
"identical input, identical id"
);
assert_eq!(retrieve_recovery_checked(&id).as_deref(), Ok(ORIGINAL));
assert_eq!(
crate::core::archive::retrieve_checked(id.as_str()).as_deref(),
Ok(ORIGINAL),
"one entry for the archive and the recovery path"
);
let stub = format!("[compressed; recover with {}]", id.as_str());
let known = delivered::lineage_of(&delivered::digest(ORIGINAL)).expect("on record");
assert!(delivered::record_derived(&delivered::digest(&stub), &known));
assert_eq!(
delivered::lineage_of(&delivered::digest(&stub)),
Some(known)
);
std::fs::remove_file(root.join("kept.rs")).expect("delete the source");
assert_eq!(
retrieve_recovery_checked(&id),
Err(RecoveryRefusal::SourceNoLongerAdmitted)
);
assert_eq!(
delivered::lineage_of(&delivered::digest(&stub))
.expect("still on record")
.still_admitted(),
Err(LineageRefusal::SourceNoLongerAdmitted)
);
assert_eq!(
store_recovery_with_lineage(ORIGINAL, &lineage),
Err(RecoveryRefusal::SourceNoLongerAdmitted)
);
});
}
#[test]
fn unknown_or_restricted_lineage_stores_nothing() {
let data = isolated(GOVERNED);
let _community = TestPolicyOverride::set(None);
let (_directory, root) = project();
scoped(&root, || {
assert_eq!(
store_recovery_with_lineage(ORIGINAL, &Lineage::of_presented(Vec::new())),
Err(RecoveryRefusal::UnknownLineage)
);
let marked = "*** TOP SECRET ***\nlaunch window\n";
let restricted = file_lineage(&root, "plan.md", marked);
assert!(matches!(
store_recovery_with_lineage("launch window\n", &restricted),
Err(RecoveryRefusal::SourceNoLongerAdmitted | RecoveryRefusal::NotStorable)
));
let clean = file_lineage(&root, "kept.rs", ORIGINAL);
assert_eq!(
store_recovery_with_lineage(marked, &clean),
Err(RecoveryRefusal::NotStorable)
);
assert_eq!(stored_files(&data), 0, "nothing on disk");
assert!(RecoveryId::parse("../../etc/passwd").is_none());
let absent = RecoveryId::parse(&"0".repeat(64)).expect("canonical id");
assert_eq!(
retrieve_recovery_checked(&absent),
Err(RecoveryRefusal::Missing)
);
});
let lineage = scoped(&root, || file_lineage(&root, "kept.rs", ORIGINAL));
assert!(store_recovery_with_lineage(ORIGINAL, &lineage).is_err());
assert_eq!(stored_files(&data), 0);
}
#[test]
fn a_presented_recovery_copy_needs_a_current_presentation() {
let data = isolated("");
let _community = TestPolicyOverride::set(None);
let (_directory, root) = project();
let prepared = "## leanctx-source-v1\ninvoice ledger\n";
scoped(&root, || {
let binding =
PresentedBinding::admitted_now(&root, "ref-quartz", "source-one", "sha256:aa")
.expect("policy identity available");
let lineage = Lineage::of_presented(vec![binding.clone()]);
assert_eq!(
store_recovery_with_lineage(prepared, &lineage),
Err(RecoveryRefusal::SourceNotPresented),
"bound, but never presented"
);
assert_eq!(stored_files(&data), 0);
assert!(record_presentation(&binding));
let id = store_recovery_with_lineage(prepared, &lineage).expect("stored");
assert_eq!(
store_recovery_with_lineage(prepared, &lineage).as_ref(),
Ok(&id)
);
assert_eq!(retrieve_recovery_checked(&id).as_deref(), Ok(prepared));
assert!(
crate::core::archive::retrieve_checked(id.as_str()).is_err(),
"the ordinary archive read never serves a lineage-bound copy"
);
assert!(
crate::core::archive::list_entries(None)
.iter()
.all(|entry| entry.id != id.as_str()),
"and does not list it"
);
let presented = data
.path()
.join("archives")
.join(crate::core::archive::presented::REGISTER_DIRECTORY);
std::fs::remove_dir_all(&presented).expect("presentation register");
assert_eq!(
retrieve_recovery_checked(&id),
Err(RecoveryRefusal::SourceNotPresented)
);
assert!(record_presentation(&binding));
assert_eq!(retrieve_recovery_checked(&id).as_deref(), Ok(prepared));
let dir = crate::core::paths::config_dir_read_only().expect("config dir");
std::fs::write(
dir.join("config.toml"),
"[context_gateway]\nmode = \"governed\"\n",
)
.expect("config");
assert_eq!(
retrieve_recovery_checked(&id),
Err(RecoveryRefusal::SourceNoLongerAdmitted)
);
let renewed =
PresentedBinding::admitted_now(&root, "ref-quartz", "source-one", "sha256:aa")
.expect("policy identity available");
assert!(record_presentation(&renewed));
assert_eq!(
retrieve_recovery_checked(&id),
Err(RecoveryRefusal::SourceNoLongerAdmitted)
);
let renewed_id =
store_recovery_with_lineage(prepared, &Lineage::of_presented(vec![renewed]))
.expect("stored under the current policy");
assert_ne!(renewed_id, id);
assert_eq!(
retrieve_recovery_checked(&renewed_id).as_deref(),
Ok(prepared)
);
});
}
#[test]
fn a_presented_copy_is_readmitted_in_the_call_that_expands_it() {
use crate::core::context_admission::capture::{
ADMISSIONS, AdmissionCapture, CallSnapshot, SourceName,
};
let _data = isolated(GOVERNED);
let _community = TestPolicyOverride::set(None);
let (_directory, root) = project();
let prepared = "## leanctx-source-v1\ninvoice ledger\n";
scoped(&root, || {
let binding =
PresentedBinding::admitted_now(&root, "ref-quartz", "source-one", "sha256:aa")
.expect("policy identity available");
assert!(record_presentation(&binding));
let lineage = Lineage::of_presented(vec![binding]);
let id = store_recovery_with_lineage(prepared, &lineage).expect("stored");
let capture = AdmissionCapture::for_call(CallSnapshot::take());
let expanded =
ADMISSIONS.sync_scope(Some(capture.clone()), || retrieve_recovery_checked(&id));
assert_eq!(expanded.as_deref(), Ok(prepared));
let (named, _) = capture.source_references();
assert_eq!(named.len(), 1, "the expansion's own decision");
assert_eq!(
named[0].1,
SourceName::Id(format!("recovery:{}", id.as_str()))
);
let masked = "## leanctx-source-v1\npaid with card 4111 1111 1111 1111\n";
match store_recovery_with_lineage(masked, &lineage) {
Ok(id) => assert_eq!(
retrieve_recovery_checked(&id),
Err(RecoveryRefusal::SourceNoLongerAdmitted)
),
Err(refusal) => assert!(matches!(
refusal,
RecoveryRefusal::NotStorable | RecoveryRefusal::SourceNoLongerAdmitted
)),
}
});
}
}