Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
2f4ce38
Reserve HTTP listeners throughout SSH publication fixtures
forhappy Oct 4, 2026
2888af5
Gate deployment and workspace access on packed storage format
forhappy Oct 4, 2026
38ec7eb
Select packed production registry and certify repository creation
forhappy Oct 4, 2026
699810d
Register and recover exact catalog initialization commands
forhappy Oct 4, 2026
6490293
fix: retire initialization recovery pins with shared receipts
forhappy Oct 4, 2026
50b9a2b
Preserve first preparation admission across cold restart
forhappy Oct 4, 2026
dcef9c8
Register exact custody intents before namespace admission
forhappy Oct 4, 2026
1a11162
Own registered staging and preparation commands through recovery
forhappy Oct 4, 2026
26bee4f
Fence restored custody against current durable Cell ownership
forhappy Oct 4, 2026
0a33a67
Reconstruct registered staging custody and wake fenced bound workers
forhappy Oct 4, 2026
29e785d
Retire expired custody originals through bounded fair maintenance
forhappy Oct 4, 2026
da0bde9
Recover production initialization after expired custody retirement
forhappy Oct 4, 2026
b768e2b
Automatically drain staging after authenticated custody retirement
forhappy Oct 4, 2026
4d67294
Bound publication admission and dispatch across repositories
forhappy Oct 4, 2026
c325983
Own resident publication recovery through eviction and shutdown
forhappy Oct 4, 2026
45ad384
Retain certified serving generations through owned read drain
forhappy Oct 4, 2026
e9ea1b3
Register exact serving acquisition and renewal through shared custody
forhappy Oct 4, 2026
dab8bd0
Observe current serving roots and drain exact pins before closure
forhappy Oct 4, 2026
2012f86
Link the production cutover draft and preserve release status
forhappy Oct 4, 2026
b21f9d7
Own serving generation renewal and exact physical drain
forhappy Oct 4, 2026
02307fd
Pool resident serving generations and join them before Cell release
forhappy Oct 4, 2026
1e32347
Fence serving construction against shutdown and join rejected owners
forhappy Oct 4, 2026
b89d929
Serve browser refs through owned immutable joint snapshots
forhappy Oct 4, 2026
b1bd813
Serve certified native bodies through shared owned pack caches
forhappy Oct 4, 2026
cc4a963
Serve browser objects and ancestry through certified snapshots
forhappy Oct 4, 2026
6ab78d6
Serve native fetch through owned certified history workspaces
forhappy Oct 4, 2026
6101d58
Replace retired hydration cursor with pinned native catalog inputs
forhappy Oct 4, 2026
d86f315
Retain staging worker admission through physical native work
forhappy Oct 4, 2026
d0aaaae
Merge current main after packed cutover publication
forhappy Oct 5, 2026
6357f41
Bound native metadata replay and retain artifact hash ownership
forhappy Oct 5, 2026
bd8d819
Own resident staging admission and drain before repository release
forhappy Oct 5, 2026
1ab3853
Add resident workflow ownership and repair recovery fixtures
forhappy Oct 5, 2026
64481d1
Route receive-pack through resident native publication
forhappy Oct 5, 2026
79e2a3c
Reuse idle serving generations before refusing capacity
forhappy Oct 5, 2026
3e40f19
fix: preserve push failures and wait for publication admission
forhappy Oct 5, 2026
c86057c
fix: read commit checks from pinned native metadata
forhappy Oct 5, 2026
4eb4fd0
fix: preserve serving rollover observation budget
forhappy Oct 5, 2026
ef765d4
fix: authorize pull metadata with native ref snapshots
forhappy Oct 5, 2026
6fc9483
fix: read native review policy and join failed startup
forhappy Oct 5, 2026
0b8d3b5
feat: certify native merge ancestry independently of branch rules
forhappy Oct 5, 2026
2ed3d58
feat: publish reviewed merges through native root transactions
forhappy Oct 5, 2026
75814b4
fix: retire closed native merge recovery pins
forhappy Oct 5, 2026
c8c49e8
fix: publish reviewed merges through native staging
forhappy Oct 5, 2026
8230922
fix: reserve candidates against certified native refs
forhappy Oct 5, 2026
976b548
feat: verify generated candidate commits against native catalog metadata
forhappy Oct 5, 2026
7c232ea
fix: preserve live staging ownership during root recovery discovery
forhappy Oct 5, 2026
1ebb278
fix: publish symbolic HEAD with native ref snapshots atomically
forhappy Oct 5, 2026
68dad78
fix: publish generated candidates and reviewed merges through native …
forhappy Oct 6, 2026
aa86f94
fix: publish native threads and route Git streams to resident owners
forhappy Oct 6, 2026
f60cdce
fix: retain native backup graphs and await staging readiness
forhappy Oct 6, 2026
195e96d
test: verify repository data across serving custody restoration
forhappy Oct 6, 2026
759d477
fix: serve verified native fetches and qualify resident fixtures
forhappy Oct 6, 2026
772c6d0
fix: retain complete native packs for producer workspaces
forhappy Oct 6, 2026
8ec0aef
fix: accept renewed serving pins during fetch preparation
forhappy Oct 6, 2026
54ed256
fix: advertise certified refs without native pack reads
forhappy Oct 6, 2026
85abb77
test: isolate serving lease checks from cold fixture setup
forhappy Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ jobs:
sudo apt-get update
sudo apt-get install -y git-lfs openssh-client
git lfs install
- name: Report tool versions
run: |
rustc --version --verbose
cargo --version
rustup show active-toolchain
git --version
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check lints
Expand Down
18 changes: 17 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions crates/canopy-git-format/src/pack_index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ impl PackIndex {
self.ids_at(0)
}

/// Scan native offsets in hash order using one fixed page. Callers may
/// build an admitted disk index of packed extents without retaining an
/// object-count-sized heap vector or performing a hash lookup per entry.
pub fn offsets(&self) -> IndexOffsets<'_> {
IndexOffsets {
index: self,
next: 0,
buffer: Box::new([0; PAGE]),
start: 0,
end: 0,
failed: false,
}
}

/// Start a bounded sequential read at a checked native ordinal. Immutable
/// metadata shards use this to cover contiguous ranges without rescanning
/// earlier index entries or materializing all IDs.
Expand Down Expand Up @@ -268,6 +282,45 @@ pub struct IndexIds<'a> {
end: usize,
failed: bool,
}

pub struct IndexOffsets<'a> {
index: &'a PackIndex,
next: u32,
buffer: Box<[u8; PAGE]>,
start: usize,
end: usize,
failed: bool,
}
impl Iterator for IndexOffsets<'_> {
type Item = io::Result<u64>;
fn next(&mut self) -> Option<Self::Item> {
if self.failed || self.next == self.index.count {
return None;
}
if self.start == self.end {
let records = (self.index.count - self.next).min((PAGE / 4) as u32) as usize;
self.end = records * 4;
self.start = 0;
if let Err(error) = read_at(
&self.index.file,
&mut self.buffer[..self.end],
self.index.offsets + u64::from(self.next) * 4,
) {
self.failed = true;
return Some(Err(error));
}
}
let mut encoded = [0; 4];
encoded.copy_from_slice(&self.buffer[self.start..self.start + 4]);
self.start += 4;
self.next += 1;
let result = self.index.offset(u32::from_be_bytes(encoded));
if result.is_err() {
self.failed = true;
}
Some(result)
}
}
impl Iterator for IndexIds<'_> {
type Item = io::Result<ObjectId>;
fn next(&mut self) -> Option<Self::Item> {
Expand Down
18 changes: 17 additions & 1 deletion crates/canopy-git-format/src/pack_index/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ fn validates_empty_and_multi_page_indexes_for_both_formats() -> io::Result<()> {
index.pack_checksum(),
ObjectId::try_from(vec![7; format.bytes()]).unwrap()
);
for (n, oid) in index.ids().enumerate() {
for (n, (oid, offset)) in index.ids().zip(index.offsets()).enumerate() {
let oid = oid?;
assert_eq!(u32::from_be_bytes(oid[..4].try_into().unwrap()), n as u32);
assert_eq!(index.find(oid)?.unwrap().offset, 12 + n as u64);
assert_eq!(index.find(oid)?.unwrap().offset, offset?);
}
// Shards start at a native ordinal, including positions crossing the
// iterator's buffer boundary. The end is a valid empty iterator.
Expand Down Expand Up @@ -97,6 +98,20 @@ fn validates_empty_and_multi_page_indexes_for_both_formats() -> io::Result<()> {
Ok(())
}

#[test]
fn offset_pages_cover_native_positions_across_a_page_boundary() -> io::Result<()> {
for format in [ObjectFormat::Sha1, ObjectFormat::Sha256] {
let index = open(&fixture(format, 20_001), format)?;
let mut count = 0;
for offset in index.offsets() {
assert_eq!(offset?, 12 + count);
count += 1;
}
assert_eq!(count, 20_001);
}
Ok(())
}

#[test]
fn rejects_truncation_and_checksum_tampering() {
for format in [ObjectFormat::Sha1, ObjectFormat::Sha256] {
Expand Down Expand Up @@ -159,6 +174,7 @@ fn supports_large_offsets_and_rejects_out_of_range_references() -> io::Result<()
rehash(&mut bytes, format);
let index = open(&bytes, format)?;
assert_eq!(index.find(format.zero())?.unwrap().offset, 0x1_0000_0010);
assert_eq!(index.offsets().next().unwrap()?, 0x1_0000_0010);
bytes[offsets..offsets + 4].copy_from_slice(&0x8000_0001_u32.to_be_bytes());
rehash(&mut bytes, format);
assert!(open(&bytes, format).is_err());
Expand Down
89 changes: 88 additions & 1 deletion crates/canopy-object-storage/src/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ impl ArtifactStore {
size: u64,
digest: [u8; 32],
input: &mut (impl AsyncRead + Unpin),
) -> Result<ArtifactDescriptor, ArtifactError> {
self.put_owned(key, size, digest, input, Arc::new(())).await
}
/// Queued hashing retains the caller's physical admission after cancellation.
/// The pin grants no artifact namespace or publication authority.
pub async fn put_owned(
&self,
key: ArtifactKey,
size: u64,
digest: [u8; 32],
input: &mut (impl AsyncRead + Unpin),
owner: Arc<dyn Send + Sync>,
) -> Result<ArtifactDescriptor, ArtifactError> {
if size > MAX_ARTIFACT_BYTES {
return Err(ArtifactError::TooLarge);
Expand All @@ -158,7 +170,8 @@ impl ArtifactStore {
uuid::Uuid::from_bytes(key.operation),
uuid::Uuid::new_v4()
));
let mut upload = external::Upload::new(Arc::clone(&self.store), stage).await?;
let mut upload =
external::Upload::new_owned(Arc::clone(&self.store), stage, owner.clone()).await?;
let result = async {
let mut hash = blake3::Hasher::new();
let mut remaining = size;
Expand All @@ -169,7 +182,9 @@ impl ArtifactStore {
tokio::time::timeout(Duration::from_secs(120), input.read_exact(&mut bytes))
.await
.map_err(|_| ArtifactError::Timeout)??;
let activity = owner.clone();
let (next, part, bytes) = tokio::task::spawn_blocking(move || {
let _activity = activity;
hash.update(&bytes);
let part = *blake3::hash(&bytes).as_bytes();
(hash, part, Bytes::from(bytes))
Expand Down Expand Up @@ -213,6 +228,14 @@ impl ArtifactStore {
&self,
key: ArtifactKey,
descriptor: ArtifactDescriptor,
) -> Result<ArtifactRead, ArtifactError> {
self.read_owned(key, descriptor, Arc::new(())).await
}
pub async fn read_owned(
&self,
key: ArtifactKey,
descriptor: ArtifactDescriptor,
owner: Arc<dyn Send + Sync>,
) -> Result<ArtifactRead, ArtifactError> {
if descriptor.size > MAX_ARTIFACT_BYTES {
return Err(ArtifactError::TooLarge);
Expand All @@ -238,7 +261,68 @@ impl ArtifactStore {
descriptor,
offset: 0,
hash: Some(blake3::Hasher::new()),
owner,
})
}

/// Authenticate the manifest before reading independently selected parts.
/// Each returned part is checked against that manifest. This capability
/// does not claim to recompute the digest of the entire artifact; callers
/// must already hold its certified descriptor and verify decoded objects.
pub async fn ranges_owned(
&self,
key: ArtifactKey,
descriptor: ArtifactDescriptor,
owner: Arc<dyn Send + Sync>,
) -> Result<ArtifactRanges, ArtifactError> {
Ok(ArtifactRanges {
read: self.read_owned(key, descriptor, owner).await?,
})
}
}

/// Independent bounded reads retain the caller's physical owner through
/// provider I/O and detached hashing. Unrequested parts are never fetched.
pub struct ArtifactRanges {
read: ArtifactRead,
}
impl ArtifactRanges {
pub async fn part(&self, index: u64) -> Result<Bytes, ArtifactError> {
self.part_owned(index, Arc::new(())).await
}
/// Cached range capabilities retain idle file admission; the active caller
/// supplies its work owner separately so an idle cache cannot pin a lease.
pub async fn part_owned(
&self,
index: u64,
work: Arc<dyn Send + Sync>,
) -> Result<Bytes, ArtifactError> {
let offset = index
.checked_mul(PART_BYTES as u64)
.filter(|offset| *offset < self.read.descriptor.size)
.ok_or(ArtifactError::Corrupt)?;
let expected = self
.read
.manifest
.part_digest(index)
.ok_or(ArtifactError::Corrupt)?;
let bytes = external::read(
self.read.store.as_ref(),
&self.read.path,
&self.read.manifest,
self.read.descriptor.size,
offset,
)
.await?;
let owner = (self.read.owner.clone(), work);
tokio::task::spawn_blocking(move || {
let _owner = owner;
if blake3::hash(&bytes).as_bytes() != &expected {
return Err(ArtifactError::Corrupt);
}
Ok(bytes)
})
.await?
}
}

Expand All @@ -251,6 +335,7 @@ pub struct ArtifactRead {
descriptor: ArtifactDescriptor,
offset: u64,
hash: Option<blake3::Hasher>,
owner: Arc<dyn Send + Sync>,
}
impl ArtifactRead {
pub fn descriptor(&self) -> ArtifactDescriptor {
Expand All @@ -273,7 +358,9 @@ impl ArtifactRead {
.manifest
.part_digest(self.offset / PART_BYTES as u64)
.ok_or(ArtifactError::Corrupt)?;
let activity = self.owner.clone();
let (next, valid, bytes) = tokio::task::spawn_blocking(move || {
let _activity = activity;
let valid = blake3::hash(&bytes).as_bytes() == &expected;
hash.update(&bytes);
(hash, valid, bytes)
Expand Down
Loading
Loading