@@ -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 ) ,
@@ -274,6 +276,34 @@ struct ManifestArgs {
274276 output : Option < PathBuf > ,
275277}
276278
279+ #[ derive( Args ) ]
280+ struct VerifyArgs {
281+ /// Verify a local lock file's content address (optionally against `--expect`).
282+ #[ arg( long, conflicts_with_all = [ "env" , "registry" ] ) ]
283+ lock : Option < PathBuf > ,
284+ /// Environment name to resolve from a registry (with `--registry`).
285+ #[ arg( long, requires = "registry" ) ]
286+ env : Option < String > ,
287+ /// Registry root URL to resolve from (with `--env`).
288+ #[ arg( long, requires = "env" ) ]
289+ registry : Option < String > ,
290+ /// Target platform (defaults to the current platform).
291+ #[ arg( long) ]
292+ platform : Option < String > ,
293+ /// Python axis value, if the environment fans out over python.
294+ #[ arg( long) ]
295+ python : Option < String > ,
296+ /// Variant axis value (e.g. `cpu`/`gpu`), if any.
297+ #[ arg( long) ]
298+ variant : Option < String > ,
299+ /// Version label to resolve.
300+ #[ arg( long, default_value = "latest" ) ]
301+ label : String ,
302+ /// For `--lock`: the expected `sha256-<hex>` content address to check.
303+ #[ arg( long) ]
304+ expect : Option < String > ,
305+ }
306+
277307#[ derive( Args ) ]
278308struct PublishArgs {
279309 #[ command( flatten) ]
@@ -694,6 +724,7 @@ async fn run_command(command: Command) -> CliResult {
694724 Command :: List ( args) => list ( args) ,
695725 Command :: DiffVersions ( args) => diff_versions ( args) ,
696726 Command :: Compose ( args) => compose ( args) . await ,
727+ Command :: Verify ( args) => verify ( args) ,
697728 Command :: Image ( ImageCommand :: Build ( args) ) => image_build ( args) . await ,
698729 Command :: Cache ( CacheCommand :: Clean { all } ) => cache_clean ( all) ,
699730 }
@@ -892,6 +923,72 @@ fn manifest(args: ManifestArgs) -> CliResult {
892923 Ok ( ( ) )
893924}
894925
926+ fn verify ( args : VerifyArgs ) -> CliResult {
927+ use crate :: registry:: content_address;
928+
929+ // Local mode: re-derive a lock file's content address, optionally checking
930+ // it against an expected `sha256-<hex>`.
931+ if let Some ( lock_path) = & args. lock {
932+ let bytes = std:: fs:: read ( lock_path) ?;
933+ let address = content_address ( & bytes) ;
934+ println ! ( "lock {} → {address}" , lock_path. display( ) ) ;
935+ if crate :: embed:: extract_manifest ( & bytes) ?. is_some ( ) {
936+ println ! ( " embedded manifest band: present" ) ;
937+ }
938+ if let Some ( expected) = & args. expect {
939+ if expected == & address {
940+ println ! ( " content address: OK (matches --expect)" ) ;
941+ } else {
942+ return Err ( format ! (
943+ "content address mismatch: expected {expected}, computed {address}"
944+ )
945+ . into ( ) ) ;
946+ }
947+ }
948+ return Ok ( ( ) ) ;
949+ }
950+
951+ // Registry mode: resolve the release, then re-derive and check the content
952+ // address of its lock (and manifest) against what the index records.
953+ let ( Some ( env) , Some ( registry_url) ) = ( & args. env , & args. registry ) else {
954+ return Err ( "pass --lock <file>, or --env <name> --registry <url>" . into ( ) ) ;
955+ } ;
956+ let registry = Registry :: new ( SpecStore :: new ( ) , registry_url. clone ( ) ) ;
957+ let platform = args
958+ . platform
959+ . clone ( )
960+ . unwrap_or_else ( || Platform :: current ( ) . to_string ( ) ) ;
961+ let mut coords = Coordinates :: new ( env. clone ( ) , platform) ;
962+ if let Some ( py) = & args. python {
963+ coords = coords. with_python ( py. clone ( ) ) ;
964+ }
965+ if let Some ( v) = & args. variant {
966+ coords = coords. with_variant ( v. clone ( ) ) ;
967+ }
968+ let label = Label :: parse ( & args. label ) ;
969+
970+ let release = registry. resolve ( & coords, & label) ?;
971+ println ! (
972+ "release {} {} on {}" ,
973+ release. environment, release. version, release. platform
974+ ) ;
975+
976+ // `pull` re-derives the lock's content address and rejects a mismatch, so a
977+ // successful pull is a verified lock.
978+ registry. pull ( & coords, & label) ?;
979+ println ! ( " lock {}: OK" , release. lock) ;
980+
981+ // A published lock relies on a manifest sidecar (the embedded band is
982+ // stripped on publish); verify the sidecar's content address too.
983+ if let Some ( manifest_addr) = & release. manifest {
984+ registry. pull_manifest ( & coords, & label) ?;
985+ println ! ( " manifest {manifest_addr}: OK" ) ;
986+ }
987+
988+ println ! ( "verified" ) ;
989+ Ok ( ( ) )
990+ }
991+
895992fn publish ( args : PublishArgs ) -> CliResult {
896993 let registry = args. coords . build_registry ( ) ;
897994 let coords = args. coords . coordinates ( ) ;
0 commit comments