Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 51 additions & 6 deletions crates/crosspack-cli/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6298,8 +6298,10 @@ description = " \n\t"
fn search_uses_configured_sources_without_registry_root() {
let layout = test_layout();
let state_root = registry_state_root(&layout);
std::fs::create_dir_all(state_root.join("cache/official/index/ripgrep"))
std::fs::create_dir_all(state_root.join("cache/official/releases/ripgrep"))
.expect("must create source cache structure");
std::fs::create_dir_all(state_root.join("cache/official/packages"))
.expect("must create package template cache structure");
std::fs::write(
state_root.join("sources.toml"),
concat!(
Expand Down Expand Up @@ -6439,7 +6441,7 @@ sha256 = "abc"
registry_state_root(&layout)
.join("cache")
.join("official")
.join("index")
.join("releases")
.join("ripgrep"),
)
.expect("must create package directory");
Expand Down Expand Up @@ -8286,7 +8288,8 @@ sha256 = "abc"
let mut path = std::env::temp_dir();
let nanos = current_unix_nanos();
path.push(format!("crosspack-cli-test-registry-{name}-{nanos}"));
std::fs::create_dir_all(path.join("index")).expect("must create index dir");
std::fs::create_dir_all(path.join("packages")).expect("must create packages dir");
std::fs::create_dir_all(path.join("releases")).expect("must create releases dir");
if with_registry_pub {
std::fs::write(path.join("registry.pub"), "test-key\n")
.expect("must write registry key");
Expand Down Expand Up @@ -8351,15 +8354,29 @@ sha256 = "abc"
let cache_root = registry_state_root(layout)
.join("cache")
.join(spec.source_name);
let package_dir = cache_root.join("index").join(spec.package_name);
let package_template_path = cache_root
.join("packages")
.join(format!("{}.toml", spec.package_name));
let package_dir = cache_root.join("releases").join(spec.package_name);
std::fs::create_dir_all(&package_dir).expect("must create package directory");
std::fs::create_dir_all(cache_root.join("packages"))
.expect("must create package template directory");

let signing_key = test_signing_key();
std::fs::write(
cache_root.join("registry.pub"),
public_key_hex(&signing_key),
)
.expect("must write registry key");
let package_template = format!("name = \"{}\"\n", spec.package_name);
std::fs::write(&package_template_path, package_template.as_bytes())
.expect("must write package template");
let package_signature = signing_key.sign(package_template.as_bytes());
std::fs::write(
package_template_path.with_extension("toml.sig"),
hex::encode(package_signature.to_bytes()),
)
.expect("must write package template signature");

let manifest = manifest_toml(
spec.package_name,
Expand Down Expand Up @@ -8388,15 +8405,29 @@ sha256 = "abc"
artifact_target: &str,
) {
let cache_root = registry_state_root(layout).join("cache").join(source_name);
let package_dir = cache_root.join("index").join(package_name);
let package_template_path = cache_root
.join("packages")
.join(format!("{package_name}.toml"));
let package_dir = cache_root.join("releases").join(package_name);
std::fs::create_dir_all(&package_dir).expect("must create package directory");
std::fs::create_dir_all(cache_root.join("packages"))
.expect("must create package template directory");

let signing_key = test_signing_key();
std::fs::write(
cache_root.join("registry.pub"),
public_key_hex(&signing_key),
)
.expect("must write registry key");
let package_template = format!("name = \"{package_name}\"\n");
std::fs::write(&package_template_path, package_template.as_bytes())
.expect("must write package template");
let package_signature = signing_key.sign(package_template.as_bytes());
std::fs::write(
package_template_path.with_extension("toml.sig"),
hex::encode(package_signature.to_bytes()),
)
.expect("must write package template signature");

let manifest = format!(
r#"
Expand Down Expand Up @@ -8493,15 +8524,29 @@ install_commands = ["sh", "-c", "{install_script}"]
);

let cache_root = registry_state_root(layout).join("cache").join(source_name);
let package_dir = cache_root.join("index").join(package_name);
let package_template_path = cache_root
.join("packages")
.join(format!("{package_name}.toml"));
let package_dir = cache_root.join("releases").join(package_name);
std::fs::create_dir_all(&package_dir).expect("must create package directory");
std::fs::create_dir_all(cache_root.join("packages"))
.expect("must create package template directory");

let signing_key = test_signing_key();
std::fs::write(
cache_root.join("registry.pub"),
public_key_hex(&signing_key),
)
.expect("must write registry key");
let package_template = format!("name = \"{package_name}\"\n");
std::fs::write(&package_template_path, package_template.as_bytes())
.expect("must write package template");
let package_signature = signing_key.sign(package_template.as_bytes());
std::fs::write(
package_template_path.with_extension("toml.sig"),
hex::encode(package_signature.to_bytes()),
)
.expect("must write package template signature");

let manifest_path = package_dir.join(format!("{version}.toml"));
std::fs::write(&manifest_path, manifest.as_bytes()).expect("must write manifest");
Expand Down
6 changes: 3 additions & 3 deletions crates/crosspack-registry/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ Registry source state, snapshot lifecycle, and manifest signature enforcement li
- `RegistrySourceRecord`: source contract (`name`, `kind`, `location`, `fingerprint_sha256`, `enabled`, `priority`).
- `update_sources` + `finalize_staged_source_update`: stage, validate, atomically swap cache, then write `snapshot.json`.
- `RegistrySourceSnapshotState`: per-source cache health (`None`, `Ready`, `Error` with reason code).
- `RegistryIndex`: reads `index/<package>/*.toml`, verifies `*.toml.sig`, parses manifests.
- `RegistryIndex`: reads `packages/<package>.toml` and `releases/<package>/*.toml`, verifies `*.toml.sig`, merges/parses manifests.
- `ConfiguredRegistryIndex`: loads enabled sources with ready snapshots only; resolves in source priority order.

## TRUST MODEL
- Trust root is per-source `registry.pub`; expected key fingerprint is pinned in `sources.toml`.
- Updates fail closed if computed key fingerprint mismatches `fingerprint_sha256`.
- Metadata validity requires both signed manifest bytes and parseable TOML payload.
- Metadata validity requires signed package/release bytes and parseable merged TOML payload.
- Signature verification uses `verify_ed25519_signature_hex` from `crosspack-security`.
- Layout is mandatory: staged source must contain `registry.pub` and `index/` before acceptance.
- Layout is mandatory: staged source must contain `registry.pub`, `packages/`, and `releases/` before acceptance.
- Snapshot readiness gate is explicit: only `snapshot.json` with `status == "ready"` is eligible for configured reads.

## CHANGE IMPACT
Expand Down
21 changes: 15 additions & 6 deletions crates/crosspack-registry/src/fs_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,19 @@ pub(crate) fn validate_staged_registry_layout(staged_root: &Path, source_name: &
);
}

let index_root = staged_root.join("index");
if !index_root.is_dir() {
let packages_root = staged_root.join("packages");
if !packages_root.is_dir() {
anyhow::bail!(
"source-snapshot-missing: source '{}' missing index/ in {}",
"source-snapshot-missing: source '{}' missing packages/ in {}",
source_name,
staged_root.display()
);
}

let releases_root = staged_root.join("releases");
if !releases_root.is_dir() {
anyhow::bail!(
"source-snapshot-missing: source '{}' missing releases/ in {}",
source_name,
staged_root.display()
);
Expand All @@ -103,14 +112,14 @@ pub(crate) fn validate_staged_registry_layout(staged_root: &Path, source_name: &
Ok(())
}

pub(crate) fn count_manifest_files(index_root: &Path) -> Result<u64> {
pub(crate) fn count_manifest_files(releases_root: &Path) -> Result<u64> {
let mut count = 0_u64;
let mut queue: VecDeque<PathBuf> = VecDeque::new();
queue.push_back(index_root.to_path_buf());
queue.push_back(releases_root.to_path_buf());

while let Some(dir) = queue.pop_front() {
for entry in fs::read_dir(&dir)
.with_context(|| format!("failed reading index directory {}", dir.display()))?
.with_context(|| format!("failed reading release directory {}", dir.display()))?
{
let entry = entry?;
let path = entry.path();
Expand Down
Loading
Loading