@@ -21,6 +21,7 @@ use crate::catalog_transaction::sync_dir;
2121const SCHEMA : & str = "st2.agent-publish.v2" ;
2222const DIGEST_SCHEMA : & str = "st2.agent-source-digest.v1" ;
2323const BUNDLE_DIGEST_DOMAIN : & [ u8 ] = b"st2.agent-publish-bundle.v1\0 " ;
24+ const CANONICAL_DECLARATION_MODE : u32 = 0o644 ;
2425
2526#[ derive( Debug , Clone ) ]
2627pub enum PublishSource {
@@ -80,6 +81,12 @@ struct Candidate {
8081 input_sha256 : String ,
8182}
8283
84+ #[ derive( Debug ) ]
85+ struct ExistingSpec {
86+ bytes : Vec < u8 > ,
87+ mode : u32 ,
88+ }
89+
8390#[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize ) ]
8491#[ serde( rename_all = "lowercase" ) ]
8592pub enum SourceKind {
@@ -124,14 +131,6 @@ impl Candidate {
124131 }
125132 } ;
126133 let spec_path = stage. path ( ) . join ( "agent.kdl" ) ;
127- anyhow:: ensure!(
128- match & source {
129- PublishSource :: Spec ( path) =>
130- path. extension( ) . and_then( |value| value. to_str( ) ) == Some ( "kdl" ) ,
131- PublishSource :: Bundle ( _) => true ,
132- } ,
133- "published spec must be canonical KDL"
134- ) ;
135134 let metadata = fs:: symlink_metadata ( & spec_path)
136135 . with_context ( || format ! ( "read candidate spec {}" , spec_path. display( ) ) ) ?;
137136 anyhow:: ensure!(
@@ -236,16 +235,22 @@ pub fn publish(request: PublishRequest) -> Result<PublishResult> {
236235 . join ( & candidate. identity ) ;
237236 let target_spec = target_dir. join ( "agent.kdl" ) ;
238237 validate_existing_ancestry ( & catalog, & target_dir) ?;
239- let before = read_regular_optional ( & target_spec) ?;
240- let same_spec = before. as_deref ( ) == Some ( candidate. bytes . as_slice ( ) ) ;
241- let before_hash = before. as_deref ( ) . map ( sha256) ;
238+ let before = read_existing_spec ( & target_spec) ?;
239+ let same_spec = before
240+ . as_ref ( )
241+ . is_some_and ( |current| current. bytes == candidate. bytes ) ;
242+ let before_hash = before. as_ref ( ) . map ( |current| sha256 ( & current. bytes ) ) ;
243+ let target_mode = before
244+ . as_ref ( )
245+ . map ( |current| current. mode )
246+ . unwrap_or ( CANONICAL_DECLARATION_MODE ) ;
242247 let after_hash = sha256 ( & candidate. bytes ) ;
243248
244249 match & request. expectation {
245250 PublishExpectation :: Absent => {
246251 if let Some ( current) = & before {
247252 anyhow:: ensure!(
248- current == & candidate. bytes,
253+ current. bytes == candidate. bytes,
249254 "publish precondition failed: {} already exists with sha256 {}" ,
250255 target_spec. display( ) ,
251256 before_hash. as_deref( ) . unwrap_or( "<unreadable>" )
@@ -331,6 +336,7 @@ pub fn publish(request: PublishRequest) -> Result<PublishResult> {
331336 & target_spec,
332337 & candidate. bytes ,
333338 before. is_some ( ) ,
339+ target_mode,
334340 ) ?;
335341 }
336342 CandidateKind :: Bundle => {
@@ -448,17 +454,24 @@ fn sha256(bytes: &[u8]) -> String {
448454 format ! ( "{:x}" , Sha256 :: digest( bytes) )
449455}
450456
451- fn read_regular_optional ( path : & Path ) -> Result < Option < Vec < u8 > > > {
452- match fs:: symlink_metadata ( path) {
453- Ok ( metadata) => {
457+ fn read_existing_spec ( path : & Path ) -> Result < Option < ExistingSpec > > {
458+ match OpenOptions :: new ( )
459+ . read ( true )
460+ . custom_flags ( libc:: O_CLOEXEC | libc:: O_NOFOLLOW | libc:: O_NONBLOCK )
461+ . open ( path)
462+ {
463+ Ok ( mut file) => {
464+ let metadata = file. metadata ( ) ?;
454465 anyhow:: ensure!(
455- metadata. is_file( ) && !metadata . file_type ( ) . is_symlink ( ) ,
466+ metadata. is_file( ) ,
456467 "publication target is not a regular file: {}" ,
457468 path. display( )
458469 ) ;
459- Ok ( Some (
460- fs:: read ( path) . with_context ( || format ! ( "read {}" , path. display( ) ) ) ?,
461- ) )
470+ let mode = metadata. permissions ( ) . mode ( ) & 0o7777 ;
471+ let mut bytes = Vec :: new ( ) ;
472+ file. read_to_end ( & mut bytes)
473+ . with_context ( || format ! ( "read {}" , path. display( ) ) ) ?;
474+ Ok ( Some ( ExistingSpec { bytes, mode } ) )
462475 }
463476 Err ( error) if error. kind ( ) == std:: io:: ErrorKind :: NotFound => Ok ( None ) ,
464477 Err ( error) => Err ( error) . with_context ( || format ! ( "read {}" , path. display( ) ) ) ,
@@ -674,6 +687,7 @@ fn atomic_write_spec(
674687 target : & Path ,
675688 bytes : & [ u8 ] ,
676689 replace : bool ,
690+ mode : u32 ,
677691) -> Result < ( ) > {
678692 let parent = target. parent ( ) . context ( "spec target has no parent" ) ?;
679693 let control = crate :: catalog_transaction:: retained_dir_path ( control_file) ?;
@@ -682,6 +696,8 @@ fn atomic_write_spec(
682696 . tempfile_in ( & control)
683697 . with_context ( || format ! ( "create temporary spec in {}" , control. display( ) ) ) ?;
684698 temp. write_all ( bytes) ?;
699+ temp. as_file ( )
700+ . set_permissions ( fs:: Permissions :: from_mode ( mode) ) ?;
685701 temp. as_file ( ) . sync_all ( ) ?;
686702 test_crash_after_temporary_write ( ) ;
687703 if replace {
0 commit comments