@@ -86,6 +86,8 @@ enum Command {
8686 DiffVersions ( DiffVersionsArgs ) ,
8787 /// Compose several published environments into one lock.
8888 Compose ( ComposeArgs ) ,
89+ /// Re-derive a lock's content address to verify its integrity.
90+ Verify ( VerifyArgs ) ,
8991 /// Build a container image (SIF or OCI) from a published environment.
9092 #[ command( subcommand) ]
9193 Image ( ImageCommand ) ,
@@ -278,6 +280,34 @@ struct ManifestArgs {
278280 output : Option < PathBuf > ,
279281}
280282
283+ #[ derive( Args ) ]
284+ struct VerifyArgs {
285+ /// Verify a local lock file's content address (optionally against `--expect`).
286+ #[ arg( long, conflicts_with_all = [ "env" , "registry" ] ) ]
287+ lock : Option < PathBuf > ,
288+ /// Environment name to resolve from a registry (with `--registry`).
289+ #[ arg( long, requires = "registry" ) ]
290+ env : Option < String > ,
291+ /// Registry root URL to resolve from (with `--env`).
292+ #[ arg( long, requires = "env" ) ]
293+ registry : Option < String > ,
294+ /// Target platform (defaults to the current platform).
295+ #[ arg( long) ]
296+ platform : Option < String > ,
297+ /// Python axis value, if the environment fans out over python.
298+ #[ arg( long) ]
299+ python : Option < String > ,
300+ /// Variant axis value (e.g. `cpu`/`gpu`), if any.
301+ #[ arg( long) ]
302+ variant : Option < String > ,
303+ /// Version label to resolve.
304+ #[ arg( long, default_value = "latest" ) ]
305+ label : String ,
306+ /// For `--lock`: the expected `sha256-<hex>` content address to check.
307+ #[ arg( long) ]
308+ expect : Option < String > ,
309+ }
310+
281311#[ derive( Args ) ]
282312struct PublishArgs {
283313 #[ command( flatten) ]
@@ -706,6 +736,7 @@ async fn run_command(command: Command) -> CliResult {
706736 Command :: List ( args) => list ( args) ,
707737 Command :: DiffVersions ( args) => diff_versions ( args) ,
708738 Command :: Compose ( args) => compose ( args) . await ,
739+ Command :: Verify ( args) => verify ( args) ,
709740 Command :: Image ( ImageCommand :: Build ( args) ) => image_build ( args) . await ,
710741 Command :: Cache ( CacheCommand :: Clean { all } ) => cache_clean ( all) ,
711742 }
@@ -907,6 +938,72 @@ fn manifest(args: ManifestArgs) -> CliResult {
907938 Ok ( ( ) )
908939}
909940
941+ fn verify ( args : VerifyArgs ) -> CliResult {
942+ use crate :: registry:: content_address;
943+
944+ // Local mode: re-derive a lock file's content address, optionally checking
945+ // it against an expected `sha256-<hex>`.
946+ if let Some ( lock_path) = & args. lock {
947+ let bytes = std:: fs:: read ( lock_path) ?;
948+ let address = content_address ( & bytes) ;
949+ println ! ( "lock {} → {address}" , lock_path. display( ) ) ;
950+ if crate :: embed:: extract_manifest ( & bytes) ?. is_some ( ) {
951+ println ! ( " embedded manifest band: present" ) ;
952+ }
953+ if let Some ( expected) = & args. expect {
954+ if expected == & address {
955+ println ! ( " content address: OK (matches --expect)" ) ;
956+ } else {
957+ return Err ( format ! (
958+ "content address mismatch: expected {expected}, computed {address}"
959+ )
960+ . into ( ) ) ;
961+ }
962+ }
963+ return Ok ( ( ) ) ;
964+ }
965+
966+ // Registry mode: resolve the release, then re-derive and check the content
967+ // address of its lock (and manifest) against what the index records.
968+ let ( Some ( env) , Some ( registry_url) ) = ( & args. env , & args. registry ) else {
969+ return Err ( "pass --lock <file>, or --env <name> --registry <url>" . into ( ) ) ;
970+ } ;
971+ let registry = Registry :: new ( SpecStore :: new ( ) , registry_url. clone ( ) ) ;
972+ let platform = args
973+ . platform
974+ . clone ( )
975+ . unwrap_or_else ( || Platform :: current ( ) . to_string ( ) ) ;
976+ let mut coords = Coordinates :: new ( env. clone ( ) , platform) ;
977+ if let Some ( py) = & args. python {
978+ coords = coords. with_python ( py. clone ( ) ) ;
979+ }
980+ if let Some ( v) = & args. variant {
981+ coords = coords. with_variant ( v. clone ( ) ) ;
982+ }
983+ let label = Label :: parse ( & args. label ) ;
984+
985+ let release = registry. resolve ( & coords, & label) ?;
986+ println ! (
987+ "release {} {} on {}" ,
988+ release. environment, release. version, release. platform
989+ ) ;
990+
991+ // `pull` re-derives the lock's content address and rejects a mismatch, so a
992+ // successful pull is a verified lock.
993+ registry. pull ( & coords, & label) ?;
994+ println ! ( " lock {}: OK" , release. lock) ;
995+
996+ // A published lock relies on a manifest sidecar (the embedded band is
997+ // stripped on publish); verify the sidecar's content address too.
998+ if let Some ( manifest_addr) = & release. manifest {
999+ registry. pull_manifest ( & coords, & label) ?;
1000+ println ! ( " manifest {manifest_addr}: OK" ) ;
1001+ }
1002+
1003+ println ! ( "verified" ) ;
1004+ Ok ( ( ) )
1005+ }
1006+
9101007fn publish ( args : PublishArgs ) -> CliResult {
9111008 let registry = args. coords . build_registry ( ) ;
9121009 let coords = args. coords . coordinates ( ) ;
0 commit comments