diff --git a/Cargo.lock b/Cargo.lock
index 6db0c65d..35bf2ce2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -399,6 +399,7 @@ dependencies = [
"ed25519-dalek 2.2.0",
"flate2",
"futures-core",
+ "futures-util",
"hex",
"http-body",
"libc",
diff --git a/crates/canopy-server/Cargo.toml b/crates/canopy-server/Cargo.toml
index 93b6e72c..3dbe6650 100644
--- a/crates/canopy-server/Cargo.toml
+++ b/crates/canopy-server/Cargo.toml
@@ -23,6 +23,7 @@ cellule-store = { git = "https://github.com/crabbuild/cellule.git", rev = "16106
ed25519-dalek = "2"
flate2 = "1.1"
futures-core = "0.3"
+futures-util = { version = "0.3", default-features = false, features = ["std"] }
hex = "0.4"
http-body = "1"
object_store = "0.14.1"
diff --git a/crates/canopy-server/src/admission.rs b/crates/canopy-server/src/admission.rs
index 98099407..6afd514d 100644
--- a/crates/canopy-server/src/admission.rs
+++ b/crates/canopy-server/src/admission.rs
@@ -26,6 +26,10 @@ pub(crate) struct AccountAdmission {
}
impl AccountAdmission {
+ pub(crate) fn available(&self) -> usize {
+ self.total.available_permits()
+ }
+
pub(crate) fn new(
limit: usize,
total_capacity: &'static str,
diff --git a/crates/canopy-server/src/deployment/mod.rs b/crates/canopy-server/src/deployment/mod.rs
index 285962dd..ec5282a8 100644
--- a/crates/canopy-server/src/deployment/mod.rs
+++ b/crates/canopy-server/src/deployment/mod.rs
@@ -19,6 +19,14 @@ mod recovery;
mod root;
pub use recovery::WorkerConfig;
+/// Incompatible repository deployment and local cache format.
+pub const STORAGE_FORMAT: &str = "canopy-pack-v1";
+
+/// Read-only admission before local reclamation, probes or Cell activation.
+pub(crate) async fn validate_service_root(store: &Store, prefix: &Path) -> Result<()> {
+ root::validate_service(store, prefix).await
+}
+
/// Application-wide admission shared by nodes and offline administration.
#[derive(Clone)]
pub struct Deployment {
diff --git a/crates/canopy-server/src/deployment/recovery.rs b/crates/canopy-server/src/deployment/recovery.rs
index 355e0e26..8300b9c9 100644
--- a/crates/canopy-server/src/deployment/recovery.rs
+++ b/crates/canopy-server/src/deployment/recovery.rs
@@ -168,7 +168,7 @@ impl Deployment {
let (module, schema) = if entry.namespace() == directory::DIRECTORY {
(DirectoryModule::NAME, directory::SCHEMA)
} else if entry.namespace() == REPOSITORIES {
- (RepositoryModule::NAME, include_str!("../schema.sql"))
+ (RepositoryModule::NAME, crate::REPOSITORY_SCHEMA)
} else {
return Err(Error::Release("unknown maintenance Cell namespace").into());
};
diff --git a/crates/canopy-server/src/deployment/root.rs b/crates/canopy-server/src/deployment/root.rs
index a6497caf..28e01d9b 100644
--- a/crates/canopy-server/src/deployment/root.rs
+++ b/crates/canopy-server/src/deployment/root.rs
@@ -70,21 +70,74 @@ pub(super) struct RootClaim {
token: ETag,
}
+#[derive(Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct RootEnvelope
{
+ format: String,
+ purpose: P,
+}
+
+fn encode(purpose: &RootPurpose) -> Result {
+ if matches!(purpose, RootPurpose::Backup { source, pin, .. } | RootPurpose::Restore { source, pin, .. }
+ if source.len() > 4096 || pin.len() > 4096)
+ {
+ return Err(Error::Backup("root reservation exceeds size limit"));
+ }
+ let body = Bytes::from(serde_json::to_vec(&RootEnvelope {
+ format: STORAGE_FORMAT.into(),
+ purpose,
+ })?);
+ if body.len() > 4096 {
+ return Err(Error::Backup("root reservation exceeds size limit"));
+ }
+ Ok(body)
+}
+
fn path(root: &Path) -> Path {
root.clone().join("canopy-root-v1.json")
}
pub(super) async fn load(store: &Store, root: &Path) -> Result