1use std::path::Path;
12
13use crate::install::VerifyError;
14use crate::layer::LayerId;
15use crate::store::manifest_digest;
16use crate::verify::sign_layer_manifest;
17
18fn is_rfc3339(s: &str) -> bool {
23 let Some((date, time)) = s.split_once('T') else {
24 return false;
25 };
26 if crate::rollback::epoch_days(date).is_none() {
27 return false;
28 }
29 let b = time.as_bytes();
31 if b.len() < 9 || b[2] != b':' || b[5] != b':' {
32 return false;
33 }
34 if !b[..8].iter().enumerate().all(|(i, c)| {
35 if i == 2 || i == 5 {
36 *c == b':'
37 } else {
38 c.is_ascii_digit()
39 }
40 }) {
41 return false;
42 }
43 let rest = &time[8..];
44 rest == "Z" || rest.ends_with('Z') || rest.contains('+') || rest.matches('-').count() == 1
45}
46
47#[derive(Debug, Clone)]
49pub struct DepositSpec {
50 pub layer: LayerId,
51 pub channel: String,
53 pub counter: u64,
56 pub issued_at: String,
59 pub tools: Vec<DepositTool>,
61 pub includes: Vec<DepositInclude>,
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct DepositInclude {
68 pub digest: String,
69 pub realm: Option<String>,
70 pub layer: Option<String>,
71}
72
73#[derive(Debug, Clone)]
74pub struct DepositTool {
75 pub name: String,
76 pub version: String,
77 pub platform: Option<String>,
80 pub bytes: Vec<u8>,
81 pub source: Option<ToolSource>,
85 pub runner: Option<RunnerSpec>,
89 pub kind: Option<crate::kind::PayloadKind>,
92 pub sdk_prefix: Option<String>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
104#[serde(deny_unknown_fields)]
105pub struct RunnerSpec {
106 pub tool: String,
107 #[serde(default)]
108 pub args: Vec<String>,
109 #[serde(rename = "arg-prefix", default)]
110 pub arg_prefix: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct ToolSource {
117 pub repo: String,
119 pub release: String,
121 pub asset: String,
123 pub sha256: String,
126 #[serde(default)]
130 pub proof: Option<crate::ingest::IngestProof>,
131 #[serde(rename = "proof-signer", default)]
134 pub proof_signer: Option<String>,
135 #[serde(rename = "proof-asserts", default)]
139 pub proof_asserts: Option<String>,
140}
141
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
144pub struct DepositOptions {
145 pub force: bool,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct DepositOutcome {
154 pub digest: String,
156 pub layer: LayerId,
157 pub counter: u64,
158}
159
160#[derive(Debug, serde::Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct DepositFileSpec {
165 pub layer: String,
166 pub channel: String,
167 pub counter: u64,
168 #[serde(default, rename = "tool")]
169 pub tools: Vec<SpecTool>,
170 #[serde(default, rename = "include")]
173 pub includes: Vec<SpecInclude>,
174}
175
176#[derive(Debug, serde::Deserialize)]
179#[serde(deny_unknown_fields)]
180pub struct SpecInclude {
181 pub digest: String,
183 #[serde(default)]
185 pub realm: Option<String>,
186 #[serde(default)]
189 pub layer: Option<String>,
190}
191
192#[derive(Debug, serde::Deserialize)]
193#[serde(deny_unknown_fields)]
194pub struct SpecTool {
195 pub name: String,
196 pub version: String,
197 #[serde(default)]
198 pub platform: Option<String>,
199 pub path: String,
201 #[serde(default)]
202 pub source: Option<ToolSource>,
203 #[serde(default)]
204 pub runner: Option<RunnerSpec>,
205 #[serde(default)]
208 pub kind: Option<String>,
209 #[serde(rename = "sdk-prefix", default)]
213 pub sdk_prefix: Option<String>,
214}
215
216pub fn parse_deposit_spec(toml_text: &str) -> Result<DepositFileSpec, DepositError> {
217 toml::from_str(toml_text).map_err(|e| DepositError::Spec(e.to_string()))
218}
219
220impl From<crate::archive::LayoutWriteError> for DepositError {
221 fn from(e: crate::archive::LayoutWriteError) -> Self {
222 match e {
223 crate::archive::LayoutWriteError::Io { path, source } => {
224 DepositError::Io { path, source }
225 }
226 crate::archive::LayoutWriteError::WouldDestroy(d) => {
227 DepositError::WouldDestroySignedWork(d)
228 }
229 }
230 }
231}
232
233#[derive(Debug, thiserror::Error)]
234pub enum DepositError {
235 #[error(
236 "the envelope this deposit just signed does not verify against the key that signed \
237 it — refusing to publish an artifact no consumer could accept"
238 )]
239 SelfVerifyFailed,
240 #[error(
241 "channel {channel:?} is not one a pin can name — use `qualified` or `rolling`. \
242 Signing it would produce a layer no varve.toml could ever select."
243 )]
244 BadChannel { channel: String },
245 #[error(
246 "issued-at {issued_at:?} is not an RFC 3339 timestamp (e.g. 2026-08-01T00:00:00Z). \
247 It is signed into the manifest and drives staleness and the SBOM timestamp, so it \
248 cannot be corrected afterwards."
249 )]
250 BadIssuedAt { issued_at: String },
251 #[error("deposit spec is not valid: {0}")]
252 Spec(String),
253 #[error(transparent)]
258 WouldDestroySignedWork(#[from] crate::referrers::WouldDestroy),
259 #[error("deposit has no tools — an empty layer is not a toolchain")]
260 NoTools,
261 #[error(
262 "tool '{name}' is deposited twice for platform {platform} (versions {first} and \
263 {second}) — a tool is DISPATCHED BY NAME (`varve run {name}`, `varve which {name}`, the \
264 argv[0] shims), so one name must resolve to exactly one binary per platform. Deposit the \
265 other version as a separate layer, or give it a distinct name."
266 )]
267 DuplicateTool {
268 name: String,
269 platform: String,
270 first: String,
271 second: String,
272 },
273 #[error(
274 "payload '{name}' version {version} is deposited twice for platform {platform} — a layer \
275 may hold several VERSIONS of one name, but not one version twice: the two would be the \
276 same payload, and only one set of bytes could land."
277 )]
278 DuplicatePayload {
279 name: String,
280 version: String,
281 platform: String,
282 },
283 #[error(
284 "payload '{name}' is kind {kind} and carries `sdk-prefix` — only a tree payload (sdk) is \
285 relocated, so the prefix would be signed into the manifest, ignored by every adapter, \
286 and believed. Drop it, or deposit this payload as kind = \"sdk\"."
287 )]
288 SdkPrefixOnNonTree { name: String, kind: String },
289 #[error(
290 "sdk '{name}' version {version} declares no `sdk-prefix` — the absolute path it was \
291 BUILT for. Without it `varve export-sdk` has no relocation budget and no path to \
292 patch, so the layer would install and verify and could never be exported. Add \
293 `sdk-prefix = \"/opt/poky/4.0\"` (the path the SDK was built for) to the [[tool]] table."
294 )]
295 SdkPrefixMissing { name: String, version: String },
296 #[error(
297 "sdk '{name}' declares sdk-prefix {prefix:?}, which is not absolute — the prefix is the \
298 path PATCHED INTO the SDK's binaries, and a relative one there would resolve against \
299 whatever directory a compiler happens to run in"
300 )]
301 SdkPrefixNotAbsolute { name: String, prefix: String },
302 #[error(
303 "payload '{name}' from {repo} declares proof = \"unverified\" and records no reason — \
304 \"we could not verify this\" must never be the silent path. Nothing vouched for these \
305 bytes, so the only thing that can travel with them is WHY you shipped them anyway: set \
306 `proof-asserts` on [tool.source] to the operator's justification, which is signed into \
307 the layer where `varve inspect` and every consumer will see it."
308 )]
309 UnverifiedWithoutReason { name: String, repo: String },
310 #[error(
311 "payload '{name}' from {repo} declares proof = \"unverified\" and also names \
312 proof-signer {signer:?} — nothing vouched for these bytes, so naming an identity that \
313 did would be signed, attributable and false. Drop the signer, or declare the mechanism \
314 that actually established it."
315 )]
316 UnverifiedNamesASigner {
317 name: String,
318 repo: String,
319 signer: String,
320 },
321 #[error(
322 "payload '{name}' from {repo} carries ingestion-proof detail ({detail}) but declares no \
323 `proof` mechanism — the detail would be signed into the layer with nothing saying HOW \
324 it was established, which makes it attributable and believed rather than checkable. \
325 Declare `proof = \"cosign-sums\" | \"build-provenance\" | \"unverified\"`, or drop the \
326 detail."
327 )]
328 ProofDetailWithoutMechanism {
329 name: String,
330 repo: String,
331 detail: &'static str,
332 },
333 #[error(transparent)]
334 Sign(#[from] VerifyError),
335 #[error("io error at {path}: {source}")]
336 Io {
337 path: String,
338 #[source]
339 source: std::io::Error,
340 },
341}
342
343fn platform_label(platform: Option<&String>) -> String {
345 platform
346 .cloned()
347 .unwrap_or_else(|| "any (unstamped)".to_string())
348}
349
350fn check_identities(tools: &[&DepositTool]) -> Result<(), DepositError> {
361 let mut dispatched: std::collections::BTreeMap<(&str, Option<&String>), &str> =
363 std::collections::BTreeMap::new();
364 let mut held: std::collections::BTreeSet<(&str, &str, Option<&String>)> =
366 std::collections::BTreeSet::new();
367 for tool in tools {
368 let dispatchable = tool.kind.unwrap_or_default().is_dispatchable();
369 let platform = tool.platform.as_ref();
370 if dispatchable {
371 if let Some(first) = dispatched.insert((&tool.name, platform), &tool.version) {
372 return Err(DepositError::DuplicateTool {
373 name: tool.name.clone(),
374 platform: platform_label(platform),
375 first: first.to_string(),
376 second: tool.version.clone(),
377 });
378 }
379 } else if !held.insert((&tool.name, &tool.version, platform)) {
380 return Err(DepositError::DuplicatePayload {
381 name: tool.name.clone(),
382 version: tool.version.clone(),
383 platform: platform_label(platform),
384 });
385 }
386 }
387 Ok(())
388}
389
390fn check_sdk_prefixes(tools: &[&DepositTool]) -> Result<(), DepositError> {
399 for tool in tools {
400 let is_tree = tool.kind == Some(crate::kind::PayloadKind::Sdk);
401 match (&tool.sdk_prefix, is_tree) {
402 (Some(_), false) => {
403 return Err(DepositError::SdkPrefixOnNonTree {
404 name: tool.name.clone(),
405 kind: tool.kind.unwrap_or_default().as_str().to_string(),
406 });
407 }
408 (None, true) => {
409 return Err(DepositError::SdkPrefixMissing {
410 name: tool.name.clone(),
411 version: tool.version.clone(),
412 });
413 }
414 (Some(prefix), true) if !prefix.starts_with('/') => {
415 return Err(DepositError::SdkPrefixNotAbsolute {
416 name: tool.name.clone(),
417 prefix: prefix.clone(),
418 });
419 }
420 _ => {}
421 }
422 }
423 Ok(())
424}
425
426fn check_ingest_proofs(tools: &[&DepositTool]) -> Result<(), DepositError> {
446 for tool in tools {
447 let Some(source) = &tool.source else { continue };
448 match source.proof {
449 Some(crate::ingest::IngestProof::Unverified) => {
450 if source
451 .proof_asserts
452 .as_ref()
453 .is_none_or(|r| r.trim().is_empty())
454 {
455 return Err(DepositError::UnverifiedWithoutReason {
456 name: tool.name.clone(),
457 repo: source.repo.clone(),
458 });
459 }
460 if let Some(signer) = &source.proof_signer {
461 return Err(DepositError::UnverifiedNamesASigner {
462 name: tool.name.clone(),
463 repo: source.repo.clone(),
464 signer: signer.clone(),
465 });
466 }
467 }
468 Some(_) => {}
469 None => {
470 let detail = match (&source.proof_signer, &source.proof_asserts) {
471 (Some(_), Some(_)) => Some("proof-signer and proof-asserts"),
472 (Some(_), None) => Some("proof-signer"),
473 (None, Some(_)) => Some("proof-asserts"),
474 (None, None) => None,
475 };
476 if let Some(detail) = detail {
477 return Err(DepositError::ProofDetailWithoutMechanism {
478 name: tool.name.clone(),
479 repo: source.repo.clone(),
480 detail,
481 });
482 }
483 }
484 }
485 }
486 Ok(())
487}
488
489pub fn deposit(
492 spec: &DepositSpec,
493 signing_key: &[u8],
494 key_id: &str,
495 dest: &Path,
496) -> Result<DepositOutcome, DepositError> {
497 deposit_with_options(spec, signing_key, key_id, dest, &DepositOptions::default())
498}
499
500pub fn deposit_with_options(
502 spec: &DepositSpec,
503 signing_key: &[u8],
504 key_id: &str,
505 dest: &Path,
506 options: &DepositOptions,
507) -> Result<DepositOutcome, DepositError> {
508 crate::referrers::guard(dest, options.force)?;
514 if spec.tools.is_empty() {
515 return Err(DepositError::NoTools);
516 }
517 let mut tools: Vec<&DepositTool> = spec.tools.iter().collect();
518 tools.sort_by(|a, b| {
522 (&a.name, &a.version, &a.platform).cmp(&(&b.name, &b.version, &b.platform))
523 });
524 check_identities(&tools)?;
525 check_sdk_prefixes(&tools)?;
526 check_ingest_proofs(&tools)?;
527
528 let entries: Vec<serde_json::Value> = tools
533 .iter()
534 .map(|tool| {
535 let mut annotations = serde_json::Map::new();
536 annotations.insert("eu.pulseengine.tool".into(), tool.name.clone().into());
537 annotations.insert(
538 "eu.pulseengine.tool.version".into(),
539 tool.version.clone().into(),
540 );
541 if let Some(platform) = &tool.platform {
542 annotations.insert(
543 crate::platform::ANN_PLATFORM.into(),
544 platform.clone().into(),
545 );
546 }
547 if let Some(source) = &tool.source {
548 annotations.insert(
549 crate::bazel::ANN_SRC_REPO.into(),
550 source.repo.clone().into(),
551 );
552 annotations.insert(
553 crate::bazel::ANN_SRC_RELEASE.into(),
554 source.release.clone().into(),
555 );
556 annotations.insert(
557 crate::bazel::ANN_SRC_ASSET.into(),
558 source.asset.clone().into(),
559 );
560 annotations.insert(
561 crate::bazel::ANN_SRC_SHA256.into(),
562 source.sha256.clone().into(),
563 );
564 if let Some(proof) = source.proof {
577 annotations.insert(crate::ingest::ANN_PROOF.into(), proof.as_str().into());
578 }
579 if let Some(signer) = &source.proof_signer {
580 annotations.insert(
581 crate::ingest::ANN_PROOF_SIGNER.into(),
582 signer.clone().into(),
583 );
584 }
585 if let Some(asserts) = &source.proof_asserts {
586 annotations.insert(
587 crate::ingest::ANN_PROOF_ASSERTS.into(),
588 asserts.clone().into(),
589 );
590 }
591 }
592 if let Some(kind) = tool.kind
596 && kind != crate::kind::PayloadKind::Tool
597 {
598 annotations.insert(crate::kind::ANN_KIND.into(), kind.as_str().into());
599 }
600 if let Some(prefix) = &tool.sdk_prefix {
605 annotations.insert(
606 crate::sdkexport::ANN_SDK_PREFIX.into(),
607 prefix.clone().into(),
608 );
609 }
610 if let Some(runner) = &tool.runner {
611 annotations.insert(crate::bazel::ANN_RUNNER.into(), runner.tool.clone().into());
612 if !runner.args.is_empty() {
613 annotations.insert(
614 crate::bazel::ANN_RUNNER_ARGS.into(),
615 runner.args.join(" ").into(),
616 );
617 }
618 if let Some(prefix) = &runner.arg_prefix {
619 annotations.insert(
620 crate::bazel::ANN_RUNNER_ARG_PREFIX.into(),
621 prefix.clone().into(),
622 );
623 }
624 }
625 serde_json::json!({
626 "mediaType": "application/vnd.oci.image.manifest.v1+json",
627 "digest": manifest_digest(&tool.bytes),
628 "size": tool.bytes.len(),
629 "annotations": annotations,
630 })
631 })
632 .collect();
633 let mut entries = entries;
638 for inc in &spec.includes {
639 let mut annotations = serde_json::Map::new();
640 annotations.insert(
641 crate::kind::ANN_KIND.into(),
642 crate::kind::PayloadKind::Layer.as_str().into(),
643 );
644 if let Some(realm) = &inc.realm {
645 annotations.insert(
646 crate::compose::ANN_INCLUDE_REALM.into(),
647 realm.clone().into(),
648 );
649 }
650 if let Some(layer) = &inc.layer {
651 annotations.insert(
652 crate::compose::ANN_INCLUDE_LAYER.into(),
653 layer.clone().into(),
654 );
655 }
656 entries.push(serde_json::json!({
657 "mediaType": "application/vnd.oci.image.index.v1+json",
658 "digest": inc.digest,
659 "size": 0,
660 "annotations": annotations,
661 }));
662 }
663 if spec.channel.parse::<crate::pin::Channel>().is_err() {
667 return Err(DepositError::BadChannel {
668 channel: spec.channel.clone(),
669 });
670 }
671 if !is_rfc3339(&spec.issued_at) {
672 return Err(DepositError::BadIssuedAt {
673 issued_at: spec.issued_at.clone(),
674 });
675 }
676 let payload_json = serde_json::json!({
677 "schemaVersion": 2,
678 "mediaType": "application/vnd.oci.image.index.v1+json",
679 "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
680 "annotations": {
681 "eu.pulseengine.varve.layer": spec.layer.to_string(),
682 "eu.pulseengine.varve.line": spec.layer.line().to_string(),
683 "eu.pulseengine.varve.channel": spec.channel,
684 "eu.pulseengine.varve.counter": spec.counter.to_string(),
685 "org.opencontainers.image.created": spec.issued_at,
686 },
687 "manifests": entries,
688 });
689 let payload = serde_json::to_vec_pretty(&payload_json).expect("payload serializes");
690 let envelope = sign_layer_manifest(&payload, signing_key, key_id)?;
691 let public = &signing_key[32..];
695 match crate::verify::dsse_verify_typed(
696 envelope.as_bytes(),
697 crate::verify::LAYER_PAYLOAD_TYPE,
698 public,
699 ) {
700 Ok(back) if back == payload => {}
701 _ => return Err(DepositError::SelfVerifyFailed),
702 }
703
704 let blobs: Vec<(String, Vec<u8>)> = tools
705 .iter()
706 .map(|tool| (manifest_digest(&tool.bytes), tool.bytes.clone()))
707 .collect();
708 crate::archive::write_oci_layout(
709 &payload,
710 envelope.as_bytes(),
711 &blobs,
712 &spec.layer.to_string(),
713 &spec.channel,
714 None,
717 dest,
718 options.force,
719 )
720 .map_err(DepositError::from)?;
721
722 Ok(DepositOutcome {
723 digest: manifest_digest(&payload),
724 layer: spec.layer.clone(),
725 counter: spec.counter,
726 })
727}
728
729#[cfg(test)]
730mod producer_tests {
731 use super::*;
732
733 #[test]
735 fn a_channel_no_pin_could_name_is_refused_before_signing() {
736 assert!("qualified".parse::<crate::pin::Channel>().is_ok());
740 assert!("rolling".parse::<crate::pin::Channel>().is_ok());
741 for bad in ["stable", "Qualified", "", "beta"] {
742 assert!(
743 bad.parse::<crate::pin::Channel>().is_err(),
744 "{bad} must be refused"
745 );
746 }
747 }
748
749 #[test]
751 fn a_deposit_that_would_drop_a_referrer_is_refused_before_a_byte_is_written() {
752 use crate::verify::generate_root_keypair;
753 let (sk, _) = generate_root_keypair();
754 let tmp = tempfile::tempdir().unwrap();
755 let dest = tmp.path().join("layout");
756 let spec = super::tests::spec();
757 deposit(&spec, &sk, "k", &dest).unwrap();
758 let status = crate::linestatus::LineStatus {
761 line: "2026.08".into(),
762 counter: 1,
763 issued_at: "2026-08-07T00:00:00Z".into(),
764 support_until: None,
765 yanked: Default::default(),
766 known_problems: Vec::new(),
767 };
768 crate::linestatus::attach_to_layout(
769 &dest,
770 &"2026.08".parse().unwrap(),
771 status.sign(&sk, "k").unwrap().as_bytes(),
772 )
773 .unwrap();
774
775 let index_before = std::fs::read(dest.join("index.json")).unwrap();
776 let err = deposit(&spec, &sk, "k", &dest).expect_err("must refuse");
777 assert!(
778 matches!(&err, DepositError::WouldDestroySignedWork { .. }),
779 "got: {err}"
780 );
781 let msg = err.to_string();
782 assert!(
783 msg.contains("line-status") && msg.contains("2026.08"),
784 "{msg}"
785 );
786 assert!(msg.contains("varve attach-status"), "{msg}");
787 assert_eq!(
788 index_before,
789 std::fs::read(dest.join("index.json")).unwrap(),
790 "a refused deposit must not have touched the layout"
791 );
792
793 deposit_with_options(&spec, &sk, "k", &dest, &DepositOptions { force: true })
796 .expect("--force overrides the guard");
797 assert!(
798 crate::linestatus::read_any_from_layout(&dest)
799 .unwrap()
800 .is_none(),
801 "--force is destructive on purpose — the referrer is gone"
802 );
803 }
804
805 #[test]
807 fn depositing_into_a_fresh_or_previously_clean_directory_still_works() {
808 use crate::verify::generate_root_keypair;
813 let (sk, _) = generate_root_keypair();
814 let tmp = tempfile::tempdir().unwrap();
815 let dest = tmp.path().join("layout");
816 let spec = super::tests::spec();
817 let first = deposit(&spec, &sk, "k", &dest).unwrap();
818 let second = deposit(&spec, &sk, "k", &dest).expect("a clean layout may be re-deposited");
819 assert_eq!(first.digest, second.digest);
820 }
821
822 #[test]
824 fn issued_at_must_be_a_timestamp_not_merely_a_date() {
825 assert!(is_rfc3339("2026-08-01T00:00:00Z"));
829 assert!(is_rfc3339("2026-08-01T12:34:56.789Z"));
830 assert!(is_rfc3339("2026-08-01T12:34:56+02:00"));
831 for bad in [
832 "2026-10-01",
833 "not-a-date",
834 "",
835 "2026-08-01T",
836 "2026-13-01T00:00:00Z",
837 ] {
838 assert!(!is_rfc3339(bad), "{bad} must be refused");
839 }
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846 use crate::archive::OciLayoutSource;
847 use crate::install::{InstallPolicy, install};
848 use crate::pin::Pin;
849 use crate::rollback::HighWaterMarks;
850 use crate::store::Store;
851 use crate::verify::{PinnedKeyVerifier, generate_root_keypair};
852
853 pub(super) fn spec() -> DepositSpec {
854 DepositSpec {
855 includes: Vec::new(),
856 layer: "2026.08.0".parse().unwrap(),
857 channel: "qualified".into(),
858 counter: 1,
859 issued_at: "2026-08-07T00:00:00Z".into(),
860 tools: vec![
861 DepositTool {
862 name: "synth".into(),
863 version: "0.45.0".into(),
864 platform: None,
865 bytes: b"synth-bytes".to_vec(),
866 source: None,
867 runner: None,
868 kind: None,
869 sdk_prefix: None,
870 },
871 DepositTool {
872 name: "rivet".into(),
873 version: "0.32.0".into(),
874 platform: None,
875 bytes: b"rivet-bytes".to_vec(),
876 source: None,
877 runner: None,
878 kind: None,
879 sdk_prefix: None,
880 },
881 ],
882 }
883 }
884
885 #[test]
887 fn a_deposit_round_trips_through_the_standard_install_pipeline() {
888 let (sk, pk) = generate_root_keypair();
889 let tmp = tempfile::tempdir().unwrap();
890 let dest = tmp.path().join("deposit");
891 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
892 assert_eq!(outcome.layer.to_string(), "2026.08.0");
893
894 assert!(dest.join("oci-layout").is_file());
896 assert!(dest.join("index.json").is_file());
897
898 let pin = Pin::parse(
900 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\n",
901 "varve.toml",
902 )
903 .unwrap();
904 let root = tmp.path().join("fresh");
905 let store = Store::at(&root);
906 let mut marks = HighWaterMarks::load(&root).unwrap();
907 let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
908 let policy = InstallPolicy {
909 index: None,
910 now: "2026-08-07T00:00:00Z",
911 staleness_threshold_days: 90,
912 platform: "test-platform",
913 };
914 let installed = install(
915 &pin,
916 &OciLayoutSource::at(&dest),
917 &verifier,
918 &store,
919 &mut marks,
920 &policy,
921 )
922 .unwrap();
923 assert_eq!(installed.digest, outcome.digest);
924 assert_eq!(installed.counter, 1);
925 let entry = store.get(&installed.digest).unwrap().unwrap();
926 let checked =
927 crate::reverify::verify_installed(&store, &entry, &verifier, "test-platform").unwrap();
928 assert_eq!(checked, 2);
929 }
930
931 #[test]
933 fn the_payload_records_layer_line_channel_counter_and_tool_versions() {
934 let (sk, _) = generate_root_keypair();
935 let tmp = tempfile::tempdir().unwrap();
936 let dest = tmp.path().join("deposit");
937 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
938 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
939 let payload = std::fs::read(dest.join("blobs/sha256").join(hex)).unwrap();
940 let json: serde_json::Value = serde_json::from_slice(&payload).unwrap();
941 let ann = &json["annotations"];
942 assert_eq!(ann["eu.pulseengine.varve.layer"], "2026.08.0");
943 assert_eq!(ann["eu.pulseengine.varve.line"], "2026.08");
944 assert_eq!(ann["eu.pulseengine.varve.channel"], "qualified");
945 assert_eq!(ann["eu.pulseengine.varve.counter"], "1");
946 assert_eq!(
947 ann["org.opencontainers.image.created"],
948 "2026-08-07T00:00:00Z"
949 );
950 let entries = json["manifests"].as_array().unwrap();
951 assert_eq!(entries.len(), 2);
952 assert!(entries.iter().any(|e| {
953 e["annotations"]["eu.pulseengine.tool"] == "synth"
954 && e["annotations"]["eu.pulseengine.tool.version"] == "0.45.0"
955 }));
956 }
957
958 #[test]
960 fn identical_specs_deposit_identical_digests() {
961 let (sk, _) = generate_root_keypair();
962 let tmp = tempfile::tempdir().unwrap();
963 let a = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("a")).unwrap();
964 let b = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("b")).unwrap();
965 assert_eq!(
966 a.digest, b.digest,
967 "the payload is deterministic — the digest IS the identity"
968 );
969 }
970
971 #[test]
973 fn empty_and_duplicate_tool_lists_are_refused() {
974 let (sk, _) = generate_root_keypair();
975 let tmp = tempfile::tempdir().unwrap();
976 let mut empty = spec();
977 empty.tools.clear();
978 assert!(matches!(
979 deposit(&empty, &sk, "k", &tmp.path().join("x")).unwrap_err(),
980 DepositError::NoTools
981 ));
982 let mut dup = spec();
983 dup.tools[1].name = "synth".into();
984 let err = deposit(&dup, &sk, "k", &tmp.path().join("y")).unwrap_err();
985 assert!(
986 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
987 "got: {err}"
988 );
989 }
990
991 fn ingested(name: &str, repo: &str, proof: crate::ingest::IngestProof) -> DepositTool {
994 let mut tool = payload(name, "1.0.0", None, Some("x86_64-unknown-linux-gnu"));
995 tool.source = Some(ToolSource {
996 repo: repo.into(),
997 release: "v1.0.0".into(),
998 asset: format!("{name}-v1.0.0-x86_64-unknown-linux-gnu.tar.gz"),
999 sha256: "a".repeat(64),
1000 proof: Some(proof),
1001 proof_signer: match proof {
1002 crate::ingest::IngestProof::Unverified => None,
1003 _ => Some(format!(
1004 "https://github.com/{repo}/.github/workflows/release.yml@refs/tags/v1.0.0"
1005 )),
1006 },
1007 proof_asserts: Some(match proof {
1008 crate::ingest::IngestProof::CosignSums => {
1009 "SHA256SUMS.txt signed for this repo".to_string()
1010 }
1011 crate::ingest::IngestProof::BuildProvenance => {
1012 "built from source commit deadbeef".to_string()
1013 }
1014 crate::ingest::IngestProof::Unverified => {
1015 "NOTHING — operator opt-in: needed for the 2026.09 bring-up".to_string()
1016 }
1017 }),
1018 });
1019 tool
1020 }
1021
1022 #[test]
1024 fn the_mechanism_that_vouched_for_each_payload_is_inside_the_signed_layer() {
1025 use crate::ingest::{ANN_PROOF, ANN_PROOF_ASSERTS, ANN_PROOF_SIGNER, IngestProof};
1031 let (sk, _) = generate_root_keypair();
1032 let tmp = tempfile::tempdir().unwrap();
1033 let mut spec = spec();
1034 spec.tools = vec![
1035 ingested("rivet", "pulseengine/rivet", IngestProof::CosignSums),
1036 ingested(
1037 "wasm-tools",
1038 "bytecodealliance/wasm-tools",
1039 IngestProof::BuildProvenance,
1040 ),
1041 ingested(
1042 "wit-bindgen",
1043 "bytecodealliance/wit-bindgen",
1044 IngestProof::Unverified,
1045 ),
1046 ];
1047 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
1048 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1049 let bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
1050 let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1051 let entries = json["manifests"].as_array().unwrap();
1052 let by_name = |n: &str| {
1053 entries
1054 .iter()
1055 .find(|e| e["annotations"]["eu.pulseengine.tool"] == n)
1056 .unwrap_or_else(|| panic!("{n} is not in the payload"))
1057 .clone()
1058 };
1059
1060 let rivet = by_name("rivet");
1061 assert_eq!(rivet["annotations"][ANN_PROOF], "cosign-sums");
1062 assert_eq!(
1063 rivet["annotations"][ANN_PROOF_SIGNER],
1064 "https://github.com/pulseengine/rivet/.github/workflows/release.yml@refs/tags/v1.0.0"
1065 );
1066
1067 let wasm_tools = by_name("wasm-tools");
1068 assert_eq!(
1069 wasm_tools["annotations"][ANN_PROOF], "build-provenance",
1070 "an attested tool must not read as a cosign-signed one"
1071 );
1072 assert_eq!(
1073 wasm_tools["annotations"][ANN_PROOF_ASSERTS],
1074 "built from source commit deadbeef"
1075 );
1076
1077 let wit_bindgen = by_name("wit-bindgen");
1078 assert_eq!(wit_bindgen["annotations"][ANN_PROOF], "unverified");
1079 assert!(
1080 wit_bindgen["annotations"][ANN_PROOF_SIGNER].is_null(),
1081 "nothing vouched for it, so no signer may be named"
1082 );
1083 assert!(
1084 wit_bindgen["annotations"][ANN_PROOF_ASSERTS]
1085 .as_str()
1086 .unwrap()
1087 .contains("2026.09 bring-up"),
1088 "the recorded opt-in reason travels with the payload"
1089 );
1090 }
1091
1092 #[test]
1094 fn an_unproven_payload_may_not_be_signed_in_silently() {
1095 use crate::ingest::IngestProof;
1100 let (sk, _) = generate_root_keypair();
1101 let tmp = tempfile::tempdir().unwrap();
1102 let mut spec = spec();
1103 let mut tool = ingested(
1104 "wit-bindgen",
1105 "bytecodealliance/wit-bindgen",
1106 IngestProof::Unverified,
1107 );
1108 tool.source.as_mut().unwrap().proof_asserts = None;
1109 spec.tools = vec![tool];
1110 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1111 assert!(
1112 matches!(&err, DepositError::UnverifiedWithoutReason { name, .. } if name == "wit-bindgen"),
1113 "got: {err}"
1114 );
1115
1116 let mut spec = super::tests::spec();
1119 let mut tool = ingested(
1120 "wit-bindgen",
1121 "bytecodealliance/wit-bindgen",
1122 IngestProof::Unverified,
1123 );
1124 tool.source.as_mut().unwrap().proof_signer = Some("https://github.com/someone/".into());
1125 spec.tools = vec![tool];
1126 let err = deposit(&spec, &sk, "k", &tmp.path().join("e")).unwrap_err();
1127 assert!(
1128 matches!(&err, DepositError::UnverifiedNamesASigner { name, .. } if name == "wit-bindgen"),
1129 "got: {err}"
1130 );
1131 }
1132
1133 #[test]
1135 fn proof_detail_without_a_mechanism_is_refused_rather_than_signed() {
1136 let (sk, _) = generate_root_keypair();
1141 let tmp = tempfile::tempdir().unwrap();
1142 let mut spec = spec();
1143 let mut tool = ingested(
1144 "wasm-tools",
1145 "bytecodealliance/wasm-tools",
1146 crate::ingest::IngestProof::BuildProvenance,
1147 );
1148 tool.source.as_mut().unwrap().proof = None;
1149 spec.tools = vec![tool];
1150 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1151 assert!(
1152 matches!(&err, DepositError::ProofDetailWithoutMechanism { name, .. } if name == "wasm-tools"),
1153 "got: {err}"
1154 );
1155 }
1156
1157 #[test]
1159 fn a_layer_deposited_before_this_requirement_reads_as_unrecorded_not_as_verified() {
1160 use crate::ingest::IngestProof;
1165 use crate::manifest::ManifestEntry;
1166 let entry = ManifestEntry {
1167 digest: "sha256:abc".into(),
1168 annotations: Default::default(),
1169 };
1170 assert_eq!(entry.ingest_proof(), Ok(None));
1171 assert_eq!(IngestProof::label(None), "unrecorded");
1172
1173 let mut entry = entry;
1174 entry
1175 .annotations
1176 .insert(crate::ingest::ANN_PROOF.into(), "build-provenance".into());
1177 assert_eq!(entry.ingest_proof(), Ok(Some(IngestProof::BuildProvenance)));
1178 entry
1182 .annotations
1183 .insert(crate::ingest::ANN_PROOF.into(), "notary-v2".into());
1184 assert_eq!(
1185 entry.ingest_proof(),
1186 Err(crate::ingest::UnknownProof("notary-v2".into()))
1187 );
1188 }
1189
1190 fn payload(
1192 name: &str,
1193 version: &str,
1194 kind: Option<crate::kind::PayloadKind>,
1195 platform: Option<&str>,
1196 ) -> DepositTool {
1197 DepositTool {
1198 name: name.into(),
1199 version: version.into(),
1200 platform: platform.map(str::to_string),
1201 bytes: format!("{name}-{version}-bytes").into_bytes(),
1202 source: None,
1203 runner: None,
1204 kind,
1205 sdk_prefix: None,
1206 }
1207 }
1208
1209 #[test]
1211 fn two_versions_of_one_crate_are_a_layer_not_a_duplicate() {
1212 use crate::kind::PayloadKind;
1218 let (sk, _) = generate_root_keypair();
1219 let tmp = tempfile::tempdir().unwrap();
1220 let mut spec = spec();
1221 spec.tools = vec![
1222 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1223 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
1224 ];
1225 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
1226
1227 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1229 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
1230 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
1231 let entries = json["manifests"].as_array().unwrap();
1232 assert_eq!(entries.len(), 2);
1233 let versions: Vec<&str> = entries
1234 .iter()
1235 .map(|e| {
1236 e["annotations"]["eu.pulseengine.tool.version"]
1237 .as_str()
1238 .unwrap()
1239 })
1240 .collect();
1241 assert_eq!(versions, vec!["1.0.200", "1.0.210"]);
1242 assert_ne!(
1243 entries[0]["digest"], entries[1]["digest"],
1244 "two versions are two artifacts"
1245 );
1246 }
1247
1248 #[test]
1250 fn extensions_deposit_as_vsix_entries_at_several_versions() {
1251 use crate::kind::PayloadKind;
1259 let (sk, _) = generate_root_keypair();
1260 let tmp = tempfile::tempdir().unwrap();
1261 let mut spec = spec();
1262 spec.tools = vec![
1263 payload(
1264 "rust-lang.rust-analyzer",
1265 "0.3.2260",
1266 Some(PayloadKind::Vsix),
1267 None,
1268 ),
1269 payload(
1270 "rust-lang.rust-analyzer",
1271 "0.3.2300",
1272 Some(PayloadKind::Vsix),
1273 None,
1274 ),
1275 payload(
1276 "vadimcn.vscode-lldb",
1277 "1.11.4",
1278 Some(PayloadKind::Vsix),
1279 None,
1280 ),
1281 ];
1282 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
1283
1284 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1285 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
1286 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
1287 let entries = json["manifests"].as_array().unwrap();
1288 assert_eq!(entries.len(), 3, "all three extensions must be signed in");
1289 for e in entries {
1290 assert_eq!(
1291 e["annotations"][crate::kind::ANN_KIND],
1292 "vsix",
1293 "every entry must carry the vsix kind in the SIGNED payload"
1294 );
1295 }
1296 let ids: Vec<(&str, &str)> = entries
1297 .iter()
1298 .map(|e| {
1299 (
1300 e["annotations"]["eu.pulseengine.tool"].as_str().unwrap(),
1301 e["annotations"]["eu.pulseengine.tool.version"]
1302 .as_str()
1303 .unwrap(),
1304 )
1305 })
1306 .collect();
1307 assert_eq!(
1308 ids,
1309 vec![
1310 ("rust-lang.rust-analyzer", "0.3.2260"),
1311 ("rust-lang.rust-analyzer", "0.3.2300"),
1312 ("vadimcn.vscode-lldb", "1.11.4"),
1313 ]
1314 );
1315 assert_ne!(
1316 entries[0]["digest"], entries[1]["digest"],
1317 "two versions of one extension are two artifacts"
1318 );
1319
1320 let mut dup = spec;
1322 dup.tools = vec![
1323 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
1324 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
1325 ];
1326 let err = deposit(&dup, &sk, "k", &tmp.path().join("e")).unwrap_err();
1327 assert!(
1328 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
1329 if name == "pub.ext" && version == "1.0.0"),
1330 "got: {err}"
1331 );
1332 }
1333
1334 #[test]
1336 fn a_tool_may_not_appear_twice_under_one_name_however_its_versions_differ() {
1337 use crate::kind::PayloadKind;
1343 let (sk, _) = generate_root_keypair();
1344 let tmp = tempfile::tempdir().unwrap();
1345 let mut spec = spec();
1346 spec.tools = vec![
1347 payload("synth", "0.45.0", Some(PayloadKind::Tool), None),
1348 payload("synth", "0.46.0", None, None), ];
1350 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1351 let msg = err.to_string();
1352 assert!(
1353 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
1354 "got: {err}"
1355 );
1356 assert!(msg.contains("0.45.0") && msg.contains("0.46.0"), "{msg}");
1357 assert!(
1361 msg.contains("any"),
1362 "an unstamped entry is any-platform: {msg}"
1363 );
1364 let mut stamped = spec;
1365 for tool in &mut stamped.tools {
1366 tool.platform = Some("x86_64-unknown-linux-gnu".into());
1367 }
1368 let msg = deposit(&stamped, &sk, "k", &tmp.path().join("e"))
1369 .unwrap_err()
1370 .to_string();
1371 assert!(msg.contains("x86_64-unknown-linux-gnu"), "{msg}");
1372 }
1373
1374 #[test]
1376 fn one_version_deposited_twice_is_still_refused_and_the_error_names_it() {
1377 use crate::kind::PayloadKind;
1382 let (sk, _) = generate_root_keypair();
1383 let tmp = tempfile::tempdir().unwrap();
1384 let mut spec = spec();
1385 spec.tools = vec![
1386 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1387 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1388 ];
1389 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
1390 let msg = err.to_string();
1391 assert!(
1392 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
1393 if name == "serde" && version == "1.0.200"),
1394 "got: {err}"
1395 );
1396 assert!(msg.contains("serde") && msg.contains("1.0.200"), "{msg}");
1397 }
1398
1399 #[test]
1401 fn platform_still_separates_identities_for_both_rules() {
1402 use crate::kind::PayloadKind;
1407 let (sk, _) = generate_root_keypair();
1408 let tmp = tempfile::tempdir().unwrap();
1409 let mut spec = spec();
1410 spec.tools = vec![
1411 payload("synth", "0.45.0", None, Some("aarch64-apple-darwin")),
1412 payload("synth", "0.45.0", None, Some("x86_64-unknown-linux-gnu")),
1413 payload(
1414 "serde",
1415 "1.0.200",
1416 Some(PayloadKind::Crate),
1417 Some("aarch64-apple-darwin"),
1418 ),
1419 payload(
1420 "serde",
1421 "1.0.200",
1422 Some(PayloadKind::Crate),
1423 Some("x86_64-unknown-linux-gnu"),
1424 ),
1425 ];
1426 deposit(&spec, &sk, "k", &tmp.path().join("d")).expect(
1427 "distinct platforms, distinct
1428 identities",
1429 );
1430 }
1431
1432 #[test]
1434 fn the_signed_digest_does_not_depend_on_the_order_versions_are_listed_in() {
1435 use crate::kind::PayloadKind;
1441 let (sk, _) = generate_root_keypair();
1442 let tmp = tempfile::tempdir().unwrap();
1443 let a_first = vec![
1444 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
1445 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
1446 ];
1447 let b_first: Vec<DepositTool> = a_first.iter().rev().cloned().collect();
1448 let mut s1 = spec();
1449 s1.tools = a_first;
1450 let mut s2 = spec();
1451 s2.tools = b_first;
1452 assert_eq!(
1453 deposit(&s1, &sk, "k", &tmp.path().join("a"))
1454 .unwrap()
1455 .digest,
1456 deposit(&s2, &sk, "k", &tmp.path().join("b"))
1457 .unwrap()
1458 .digest,
1459 );
1460 }
1461}