Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
5165e07
Record failed three-node load and acknowledged creation recovery
forhappy Sep 30, 2026
59e780b
Preserve failed post-load corpus recovery and diagnostic reads
forhappy Sep 30, 2026
b3ea671
Fix delayed-startup fixture port race and bind Cellule candidate
forhappy Oct 1, 2026
878d111
Measure validated Git pack first-byte and transfer timing
forhappy Oct 1, 2026
6a58063
Record live candidate pack probe under explicit seed load
forhappy Oct 1, 2026
6199a73
Observe kernel self and reaped-child CPU with calibrated units
forhappy Oct 1, 2026
59a7307
Record live kernel CPU observation and pack rollup calibration
forhappy Oct 1, 2026
9c29627
Measure restart-bound RustFS request counters without reconfiguration
forhappy Oct 1, 2026
f7fc26f
Record live provider counter observation and independent audit
forhappy Oct 1, 2026
39829f7
Verify expired renewal replies stay fenced and arm full seed recovery
forhappy Oct 1, 2026
90d3c39
Record candidate seed failure and bound ACK recovery diagnostics
forhappy Oct 1, 2026
dafabf8
Track latest Cellule docs-only revision without replacing bound runtime
forhappy Oct 1, 2026
ef55688
docs: record audited ACK recovery and native storage trial
forhappy Oct 1, 2026
b44fac8
docs: bind full native recovery and load transitions
forhappy Oct 1, 2026
db569d4
docs: record complete native seed and owner-loss boundary
forhappy Oct 1, 2026
966da48
perf: audit campaign ledgers and record native recovery
forhappy Oct 1, 2026
5c519e7
Merge main after squash of recovery evidence PR
forhappy Oct 1, 2026
772d1dc
docs: bind post-load every-ACK recovery controller
forhappy Oct 1, 2026
669584a
docs(perf): retain RustFS OOM and bind full recovery diagnostic
forhappy Oct 1, 2026
e5ccb62
docs(perf): record RustFS allocator instrumentation gap
forhappy Oct 1, 2026
38a51dd
chore(deps): sync Cellule main and bind every-ACK recovery
forhappy Oct 1, 2026
a41131a
docs: record full post-OOM recovery and load transition
forhappy Oct 1, 2026
8e18329
test: add scheduled critical Git workflows and recovery guards
forhappy Oct 1, 2026
5b2f95c
build: sync Cellule routing changes and record failed load status
forhappy Oct 1, 2026
4a727fd
docs: record latest Cellule release and RustFS verification
forhappy Oct 1, 2026
7d09597
docs: distinguish frozen candidate from advancing upstream
forhappy Oct 1, 2026
194503c
docs: record full seed and verified evidence backups
forhappy Oct 1, 2026
d6d63a3
docs: retain failed full-corpus verification and diagnostic scope
forhappy Oct 1, 2026
f0c5d09
docs: record closed directory diagnostics and test verification
forhappy Oct 1, 2026
4651aaa
docs: record closed authentication diagnosis and candidate failure
forhappy Oct 1, 2026
e2649bc
test: reproduce directory authentication mailbox credit refusal
forhappy Oct 1, 2026
0746e40
perf: bound directory authentication query credit to its result
forhappy Oct 1, 2026
cc7130f
test: isolate parent fence checks and cover inherited fork safety
forhappy Oct 1, 2026
87889f4
fix: retain exact directory predecessor for bounded authentication
forhappy Oct 1, 2026
b32afea
perf: rescan idle inventory briefly instead of refusing settling resi…
forhappy Oct 1, 2026
eaddeef
docs: publish verified authentication and residency candidate checkpoint
forhappy Oct 1, 2026
6136dfd
test: reproduce real startup against a retained predecessor catalog
forhappy Oct 1, 2026
06834cc
fix: admit retained SQL catalog proofs without rewriting their identity
forhappy Oct 1, 2026
ae4b196
test: require unsupported persisted controls to stay unchanged on sta…
forhappy Oct 1, 2026
395111b
Reject unsupported persisted SQL controls before acquisition
forhappy Oct 1, 2026
a435e6f
Test read-only catalog admission across release transitions
forhappy Oct 1, 2026
33e1dc8
Correct catalog admission fixture lockfile path
forhappy Oct 1, 2026
5e9ca0a
Document verified retained catalog startup and safety boundaries
forhappy Oct 1, 2026
5f73709
Upgrade Cellule to origin main 0dc04a658
forhappy Oct 1, 2026
240203a
Document latest Cellule qualification and open upgrade gates
forhappy Oct 2, 2026
0498e22
Document old-binary RustFS upgrade and critical workflow recovery
forhappy Oct 2, 2026
eaebfc4
fix: retain HTTP listener reservations through supervised startup
forhappy Oct 2, 2026
4ddc742
docs: record full original-corpus maintenance recovery
forhappy Oct 2, 2026
2c897c7
docs: record controlled original-corpus release activation
forhappy Oct 2, 2026
085a278
docs: record full corpus verification and failed load arrivals
forhappy Oct 2, 2026
ec81650
docs: record completed original-corpus benchmark phases
forhappy Oct 2, 2026
842680a
build: pin Cellule to main revision 1914096
forhappy Oct 2, 2026
e175238
ci: qualify release correctness against RustFS with retained artifacts
forhappy Oct 2, 2026
51fa42e
ci: use runner AWS CLI and retain evidence before setup
forhappy Oct 2, 2026
3a02d59
docs: distinguish latest Cellule checks from frozen campaign evidence
forhappy Oct 2, 2026
5581d5c
docs: describe remaining gates independently of PR draft status
forhappy Oct 2, 2026
0ba7775
docs: record release failure and audited residency diagnostic
forhappy Oct 2, 2026
cb78793
perf: retain bounded Git failures and report closed recovery evidence
forhappy Oct 2, 2026
7497fc7
deps: update Cellule main pin and qualification status
forhappy Oct 2, 2026
e8eef77
docs: refresh Cellule qualification and recovery checkpoint
forhappy Oct 2, 2026
bbd3c54
docs: record native qualification and retained maintenance blocker
forhappy Oct 2, 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
115 changes: 115 additions & 0 deletions .github/workflows/qualify-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Qualify release correctness against RustFS

on:
workflow_dispatch:
push:
branches:
- 'codex/cellule-release-ci-*'
- 'codex/three-node-recovery-evidence'

permissions:
contents: read

defaults:
run:
shell: bash

jobs:
release-correctness:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Initialize qualification evidence before tool setup
run: |
mkdir qualification
git rev-parse HEAD > qualification/head.txt
cp .github/workflows/qualify-release.yml qualification/qualify-release.yml
- name: Install tools
run: |
rustup component add clippy rustfmt
sudo apt-get update
sudo apt-get install -y git-lfs openssh-client
aws --version
git lfs install
- name: Bind exact source and dependency inputs
run: |
cargo metadata --locked --format-version 1 > qualification/metadata.json
docker pull rustfs/rustfs:1.0.0-beta.8-glibc
python3 - <<'PY'
import hashlib
import json
from pathlib import Path
import subprocess
import tomllib

metadata = json.loads(Path('qualification/metadata.json').read_text())
manifest = tomllib.loads(Path('crates/canopy-server/Cargo.toml').read_text())
revisions = {value['rev'] for value in manifest['dependencies'].values()
if isinstance(value, dict) and value.get('git') == 'https://github.com/crabbuild/cellule.git'}
assert len(revisions) == 1
revision = revisions.pop()
packages = {package['name']: package['source'] for package in metadata['packages']
if package['name'].startswith('cellule-')}
assert set(packages) == {'cellule-app', 'cellule-host', 'cellule-ltx',
'cellule-runtime', 'cellule-store', 'cellule-types'}
assert all(source == f'git+https://github.com/crabbuild/cellule.git?rev={revision}#{revision}'
for source in packages.values())
files = subprocess.check_output(['git', 'ls-files', 'crates', 'scripts', 'Cargo.toml',
'Cargo.lock', '.github/workflows'], text=True).splitlines()
image = json.loads(subprocess.check_output(['docker', 'image', 'inspect',
'rustfs/rustfs:1.0.0-beta.8-glibc'], text=True))[0]
result = {
'head': subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(),
'cellule_revision': revision,
'packages': packages,
'source_sha256': {name: hashlib.sha256(Path(name).read_bytes()).hexdigest() for name in files},
'rustc': subprocess.check_output(['rustc', '-Vv'], text=True),
'cargo': subprocess.check_output(['cargo', '-V'], text=True).strip(),
'git': subprocess.check_output(['git', '--version'], text=True).strip(),
'rustfs_image': {'id': image['Id'], 'repo_digests': image.get('RepoDigests', [])},
'scope': 'Linux release correctness and fresh RustFS compatibility only; '
'not native Mac qualification, retained-store upgrade, recovery or reference capacity.',
}
Path('qualification/inputs.json').write_text(json.dumps(result, indent=2) + '\n')
PY
cp Cargo.lock qualification/Cargo.lock
cp .github/workflows/qualify-release.yml qualification/qualify-release.yml
- name: Check release formatting and all-target lints
run: |
cargo fmt --all -- --check
cargo clippy --release --workspace --all-targets --locked -- -D warnings 2>&1 | tee qualification/release-clippy.log
- name: Test release workspace
run: cargo test --release --workspace --locked -- --test-threads=4 2>&1 | tee qualification/release-workspace.log
- name: Check Python qualification harness
run: python3 -B -m unittest discover -s scripts -p 'test_*.py' 2>&1 | tee qualification/python-harness.log
- name: Qualify release Git compatibility against RustFS
run: python3 scripts/qualify_size.py --provider-only --release 2>&1 | tee qualification/release-rustfs.log
- name: Build and retain release executable
run: |
cargo build --release --locked --bin canopy 2>&1 | tee qualification/release-build.log
python3 - <<'PY'
import hashlib
import json
from pathlib import Path

inputs = json.loads(Path('qualification/inputs.json').read_text())
assert all(hashlib.sha256(Path(name).read_bytes()).hexdigest() == digest
for name, digest in inputs['source_sha256'].items())
binary = Path('target/release/canopy')
digest = hashlib.sha256(binary.read_bytes()).hexdigest()
result = {'head': inputs['head'], 'cellule_revision': inputs['cellule_revision'],
'binary_sha256': digest, 'binary_size_bytes': binary.stat().st_size,
'bound_source_unchanged': True, 'scope': inputs['scope']}
Path('qualification/release-output.json').write_text(json.dumps(result, indent=2) + '\n')
PY
sha256sum target/release/canopy > qualification/canopy.sha256
tar -czf qualification/canopy-release-linux.tar.gz -C target/release canopy
- name: Retain qualification evidence including failed attempts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-correctness-${{ github.sha }}-${{ github.run_attempt }}
path: qualification/
if-no-files-found: error
retention-days: 30
12 changes: 6 additions & 6 deletions Cargo.lock

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

10 changes: 5 additions & 5 deletions crates/canopy-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ axum = "0.8.9"
base64 = "0.22"
blake3 = "1.8"
bytes = "1.11"
cellule-app = { git = "https://github.com/crabbuild/cellule.git", rev = "0573f48998c4e5343cd8b463d79b7bc1820c923c" }
cellule-host = { git = "https://github.com/crabbuild/cellule.git", rev = "0573f48998c4e5343cd8b463d79b7bc1820c923c" }
cellule-ltx = { git = "https://github.com/crabbuild/cellule.git", rev = "0573f48998c4e5343cd8b463d79b7bc1820c923c", features = ["replica"] }
cellule-runtime = { git = "https://github.com/crabbuild/cellule.git", rev = "0573f48998c4e5343cd8b463d79b7bc1820c923c" }
cellule-store = { git = "https://github.com/crabbuild/cellule.git", rev = "0573f48998c4e5343cd8b463d79b7bc1820c923c" }
cellule-app = { git = "https://github.com/crabbuild/cellule.git", rev = "0f4ca0919b0dfe20a3dcd964d21da03135e42eed" }
cellule-host = { git = "https://github.com/crabbuild/cellule.git", rev = "0f4ca0919b0dfe20a3dcd964d21da03135e42eed" }
cellule-ltx = { git = "https://github.com/crabbuild/cellule.git", rev = "0f4ca0919b0dfe20a3dcd964d21da03135e42eed", features = ["replica"] }
cellule-runtime = { git = "https://github.com/crabbuild/cellule.git", rev = "0f4ca0919b0dfe20a3dcd964d21da03135e42eed" }
cellule-store = { git = "https://github.com/crabbuild/cellule.git", rev = "0f4ca0919b0dfe20a3dcd964d21da03135e42eed" }
ed25519-dalek = "2"
flate2 = "1.1"
futures-core = "0.3"
Expand Down
47 changes: 34 additions & 13 deletions crates/canopy-server/src/directory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ use cellule_app::{ApplicationHandle, CellType};
use cellule_runtime::{
ApplicationId, CellModule, CellTarget, Committed, Digest, Error, InvocationError,
MigrationDescriptor, ModuleDescriptor, MutationIdentity, NamespaceDescriptor, NamespaceId,
Observed, Receipt, RegistryBuilder, SqlCell, SqlModule, TenantId, cell::catalog::CatalogRole,
partition_for_shard, primitives::sql::SqlBatch, primitives::sql::SqlResultSet,
primitives::sql::SqlStatement, primitives::sql::SqlValue, primitives::sql::register_sql,
registry::OperationDescriptor,
Observed, Receipt, RegistryBuilder, SqlCell, SqlModule, TenantId,
cell::catalog::CatalogRole,
partition_for_shard,
primitives::sql::SqlBatch,
primitives::sql::SqlResultSet,
primitives::sql::SqlStatement,
primitives::sql::SqlValue,
primitives::sql::register_sql,
registry::{OperationDescriptor, RetainedCodeDescriptor},
};

use crate::{CanopyApplication, ReadIdentity, validate_repository_id};
Expand All @@ -31,7 +36,24 @@ pub const SCHEMA: &str = include_str!("../directory_schema.sql");
pub const REPOSITORY_PAGE_SIZE: usize = 32;

const COMMANDS: [OperationDescriptor; 2] = [operation(1), operation(3)];
const QUERIES: [OperationDescriptor; 2] = [operation(2), operation(4)];
const QUERIES: [OperationDescriptor; 3] = [
operation(2),
operation(4),
timed_sql::AUTHENTICATE_OPERATION,
];

// The selected c51 release used the same schema and credential contracts,
// before the separately bounded authentication query was added. Keep that
// exact code executable while persisted Cells roll to the new descriptor.
const RETAINED_CODES: [RetainedCodeDescriptor; 1] = [RetainedCodeDescriptor {
code: Digest::from_bytes([
0xf7, 0x25, 0x4e, 0xda, 0x9d, 0x5d, 0x33, 0x95, 0x66, 0xf4, 0x54, 0x57, 0x50, 0x26, 0x18,
0xad, 0x13, 0xcb, 0xbf, 0x6e, 0x5a, 0x74, 0x59, 0x5f, 0x5b, 0x3c, 0xe4, 0x66, 0x53, 0xea,
0x12, 0xf1,
]),
schema_min: 1,
schema_max: 1,
}];

const fn operation(id: u32) -> OperationDescriptor {
OperationDescriptor {
Expand Down Expand Up @@ -72,7 +94,7 @@ impl CellModule for DirectoryModule {
source.update(SCHEMA.as_bytes());
Digest::from_bytes(*source.finalize().as_bytes())
},
retained_codes: &[],
retained_codes: &RETAINED_CODES,
schema_min: 1,
schema_max: 1,
migrations: MIGRATIONS.get_or_init(|| {
Expand Down Expand Up @@ -100,7 +122,8 @@ impl CellModule for DirectoryModule {
fn register(self, registry: &mut RegistryBuilder) -> cellule_runtime::Result<()> {
register_sql::<Self>(registry)?;
registry.bind_command::<timed_sql::CredentialCommand>()?;
registry.bind_query::<timed_sql::CredentialQuery>()
registry.bind_query::<timed_sql::CredentialQuery>()?;
registry.bind_query::<timed_sql::AuthenticateQuery>()
}
}

Expand Down Expand Up @@ -305,12 +328,10 @@ impl DirectoryCell {
token_digest: [u8; 32],
minimum: Option<Receipt>,
) -> Result<Observed<Option<Principal>>, InvocationError<Vec<SqlResultSet>>> {
let result = self.credential_query(minimum, SqlBatch {
statements: vec![SqlStatement {
sql: "SELECT a.name, t.scope, t.id FROM access_tokens AS t JOIN accounts AS a ON a.name = t.account WHERE t.digest = ?2 AND t.enabled = 1 AND (t.expires_ms IS NULL OR t.expires_ms > ?1) AND a.enabled = 1".into(),
parameters: vec![SqlValue::Blob(token_digest.to_vec())],
}],
}).await?;
let result = self
.application
.query::<timed_sql::AuthenticateQuery>(&self.target, minimum, token_digest.to_vec())
.await?;
let principal = result
.output
.first()
Expand Down
88 changes: 88 additions & 0 deletions crates/canopy-server/src/directory/timed_sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,48 @@ use cellule_runtime::{
Command, Query, registry::CommandContext, registry::CommandResult, registry::QueryContext,
};

// Authentication returns at most one row: a validated 64-byte account name,
// a five-byte scope and a 16-byte token ID. Do not reserve the generic 1-MiB
// SQL result ceiling for every small credential decision. Generic credential
// pages retain their existing operation IDs and bounds.
pub(super) const AUTHENTICATE_OPERATION: OperationDescriptor = OperationDescriptor {
id: 5,
codec_version: 1,
schema_min: 1,
schema_max: 1,
input_limit: 36, // Canonical Vec<u8>: four-byte length plus SHA-256 digest.
output_limit: 256,
};

pub(super) struct AuthenticateQuery;

impl Query for AuthenticateQuery {
const MODULE: &'static str = DirectoryModule::NAME;
const ID: u32 = AUTHENTICATE_OPERATION.id;
const CODEC_VERSION: u32 = 1;
type Input = Vec<u8>;
type Output = Vec<SqlResultSet>;

fn execute(
context: &mut QueryContext<'_>,
input: Self::Input,
) -> cellule_runtime::Result<Self::Output> {
if input.len() != 32 {
return Err(Error::Command("invalid authentication digest length"));
}
let batch = bind_time(
SqlBatch {
statements: vec![SqlStatement {
sql: "SELECT a.name, t.scope, t.id FROM access_tokens AS t JOIN accounts AS a ON a.name = t.account WHERE t.digest = ?2 AND t.enabled = 1 AND (t.expires_ms IS NULL OR t.expires_ms > ?1) AND a.enabled = 1".into(),
parameters: vec![SqlValue::Blob(input)],
}],
},
context.now_ms(),
)?;
context.sql(&batch)
}
}

// Credential decisions use one owner timestamp for the whole transaction.
// Cellule samples context time before queueing; refresh it to fence expired
// requests without racing separate decision/update statements.
Expand Down Expand Up @@ -79,3 +121,49 @@ impl DirectoryCell {
.await
}
}

#[cfg(test)]
mod tests {
use super::*;
use cellule_runtime::codec::{BoundedDecoder, BoundedEncoder, WireValue};

#[test]
fn authentication_bounds_cover_the_largest_valid_principal()
-> Result<(), Box<dyn std::error::Error>> {
let digest = vec![7; 32];
let mut input = BoundedEncoder::new(AUTHENTICATE_OPERATION.input_limit)?;
digest.encode(&mut input)?;
let encoded = input.finish();
assert_eq!(encoded.len(), 36);
assert_eq!(
Vec::<u8>::decode(&mut BoundedDecoder::new(&encoded, 36)?)?,
digest
);
assert!(
vec![7_u8; 33]
.encode(&mut BoundedEncoder::new(36)?)
.is_err()
);

let name = "a".repeat(64);
validate_component(&name)?;
let result = vec![SqlResultSet {
columns: vec!["name".into(), "scope".into(), "id".into()],
rows: vec![vec![
SqlValue::Text(name),
SqlValue::Text(TokenScope::Admin.as_str().into()),
SqlValue::Blob(vec![8; 16]),
]],
rows_affected: 0,
}];
let mut output = BoundedEncoder::new(AUTHENTICATE_OPERATION.output_limit)?;
result.encode(&mut output)?;
let encoded = output.finish();
assert!(encoded.len() <= 256);
assert_eq!(
Vec::<SqlResultSet>::decode(&mut BoundedDecoder::new(&encoded, 256)?)?,
result
);
Ok(())
}
}
Loading
Loading