@@ -95,10 +95,7 @@ pub fn verified_managed_agent_allocations(
9595 _ => continue ,
9696 } ;
9797 let _ = metadata;
98- if source. is_file ( )
99- && digest_file ( & source) ? == allocation. installed_digest
100- && digest_file ( & target) ? == allocation. installed_digest
101- {
98+ if source. is_file ( ) && digest_file ( & target) ? == allocation. installed_digest {
10299 verified. insert (
103100 target. to_string_lossy ( ) . into_owned ( ) ,
104101 source_name. trim_end_matches ( ".md" ) . to_string ( ) ,
@@ -175,51 +172,75 @@ fn repair_bundled_agent_from_dir(
175172 ) ) ;
176173 }
177174
178- let mut marker = read_current_seed_marker ( target_root) ?. unwrap_or ( SeedMarker {
179- version : 1 ,
180- install_state : InstallState :: Complete ,
181- seeded_files : BTreeSet :: new ( ) ,
182- allocations : BTreeMap :: new ( ) ,
183- } ) ;
184- if marker. version != 1 {
185- marker = SeedMarker {
186- version : 1 ,
187- install_state : InstallState :: Complete ,
188- seeded_files : BTreeSet :: new ( ) ,
189- allocations : BTreeMap :: new ( ) ,
190- } ;
191- }
175+ let mut marker = read_current_seed_marker ( target_root) ?
176+ . filter ( |marker| marker. version == 1 )
177+ . ok_or_else ( || {
178+ "Bundled agent repair requires a valid managed allocation manifest" . to_string ( )
179+ } ) ?;
192180 let source_digest = digest_file ( & source) ?;
193- let claimed = marker
194- . allocations
195- . values ( )
196- . map ( |allocation| allocation. target_file_name . clone ( ) )
197- . collect :: < BTreeSet < _ > > ( ) ;
198- let target_file_name = match marker. allocations . get ( file_name) {
199- Some ( allocation) => {
200- let target = target_root. join ( & allocation. target_file_name ) ;
201- if target. exists ( ) && digest_file ( & target) ? != allocation. installed_digest {
202- allocate_target_name ( file_name, & claimed, target_root) ?
203- } else {
204- allocation. target_file_name . clone ( )
205- }
206- }
207- None => allocate_target_name ( file_name, & claimed, target_root) ?,
181+ let allocation = marker. allocations . get ( file_name) . cloned ( ) . ok_or_else ( || {
182+ format ! ( "Bundled agent '{file_name}' has no managed allocation to repair" )
183+ } ) ?;
184+
185+ let target = target_root. join ( & allocation. target_file_name ) ;
186+ let target_matches_recorded =
187+ target. is_file ( ) && digest_file ( & target) ? == allocation. installed_digest ;
188+ let target_file_name = if allocation. status == AllocationStatus :: Pending
189+ || !target. exists ( )
190+ || target_matches_recorded
191+ {
192+ allocation. target_file_name
193+ } else {
194+ let claimed = marker
195+ . allocations
196+ . values ( )
197+ . map ( |record| record. target_file_name . clone ( ) )
198+ . collect :: < BTreeSet < _ > > ( ) ;
199+ allocate_target_name ( file_name, & claimed, target_root) ?
208200 } ;
209- let target = target_root. join ( & target_file_name) ;
210- if !target. exists ( ) || digest_file ( & target) ? != source_digest {
211- install_agent_file ( & source, & target) ?;
212- }
201+
202+ // Persist the exact repair destination before writing its file. A retry can
203+ // then adopt a matching target or safely reallocate around a late collision.
213204 marker. allocations . insert (
214205 file_name. to_string ( ) ,
215206 AllocationRecord {
216207 target_file_name : target_file_name. clone ( ) ,
217- installed_digest : source_digest,
218- status : AllocationStatus :: Installed ,
208+ installed_digest : source_digest. clone ( ) ,
209+ status : AllocationStatus :: Pending ,
219210 } ,
220211 ) ;
221- marker. seeded_files . insert ( file_name. to_string ( ) ) ;
222- marker. seeded_files . insert ( target_file_name) ;
212+ write_seed_marker ( target_root, & marker) ?;
213+
214+ let target = target_root. join ( & target_file_name) ;
215+ if !( target. is_file ( ) && digest_file ( & target) ? == source_digest) {
216+ if fs:: symlink_metadata ( & target) . is_ok ( ) {
217+ let claimed = marker
218+ . allocations
219+ . values ( )
220+ . map ( |record| record. target_file_name . clone ( ) )
221+ . collect :: < BTreeSet < _ > > ( ) ;
222+ let replacement = allocate_target_name ( file_name, & claimed, target_root) ?;
223+ marker
224+ . allocations
225+ . get_mut ( file_name)
226+ . unwrap ( )
227+ . target_file_name = replacement;
228+ write_seed_marker ( target_root, & marker) ?;
229+ }
230+ let target = target_root. join ( & marker. allocations [ file_name] . target_file_name ) ;
231+ install_agent_file ( & source, & target) ?;
232+ }
233+
234+ marker. allocations . get_mut ( file_name) . unwrap ( ) . status = AllocationStatus :: Installed ;
235+ marker. install_state = if marker
236+ . allocations
237+ . values ( )
238+ . all ( |record| record. status == AllocationStatus :: Installed )
239+ {
240+ InstallState :: Complete
241+ } else {
242+ InstallState :: Installing
243+ } ;
223244 write_seed_marker ( target_root, & marker)
224245}
225246
@@ -479,13 +500,10 @@ fn seed_bundled_agents_from_dir(
479500 if digest_file ( & target) ? != allocation. installed_digest {
480501 continue ; // User edit or replacement: preserve and relinquish management.
481502 }
482- if source_digest != allocation. installed_digest {
483- install_agent_file ( & source, & target) ?;
484- marker. allocations . get_mut ( & name) . unwrap ( ) . installed_digest =
485- source_digest. clone ( ) ;
486- write_seed_marker ( target_root, & marker) ?;
487- seeded_count += 1 ;
488- }
503+ // Packaged updates do not rewrite established copies in this
504+ // clean-install-only flow. The recorded digest remains the
505+ // ownership proof as long as the target still matches it.
506+ let _ = source_digest;
489507 }
490508 }
491509 if marker. allocations [ & name] . status == AllocationStatus :: Installed {
@@ -1087,4 +1105,85 @@ mod tests {
10871105 . starts_with( ".berd-bundled-agents-invalid-" )
10881106 } ) ) ;
10891107 }
1108+
1109+ #[ test]
1110+ fn packaged_source_change_preserves_managed_copy_and_verified_digest ( ) {
1111+ let source = tempdir ( ) . unwrap ( ) ;
1112+ let target = tempdir ( ) . unwrap ( ) ;
1113+ let original = "---\n name: Tinker\n metadata:\n berdBundled: true\n ---\n Original." ;
1114+ write_agent ( source. path ( ) , "tinker.md" , original) ;
1115+ seed_bundled_agents_from_dir ( source. path ( ) , target. path ( ) ) . unwrap ( ) ;
1116+ let recorded = read_seed_marker ( target. path ( ) ) . unwrap ( ) . allocations [ "tinker.md" ]
1117+ . installed_digest
1118+ . clone ( ) ;
1119+ write_agent (
1120+ source. path ( ) ,
1121+ "tinker.md" ,
1122+ "---\n name: Tinker\n metadata:\n berdBundled: true\n ---\n Updated package." ,
1123+ ) ;
1124+
1125+ let result = seed_bundled_agents_from_dir ( source. path ( ) , target. path ( ) ) . unwrap ( ) ;
1126+
1127+ assert_eq ! ( result. seeded_count, 0 ) ;
1128+ assert_eq ! (
1129+ fs:: read_to_string( target. path( ) . join( "tinker.md" ) ) . unwrap( ) ,
1130+ original
1131+ ) ;
1132+ assert_eq ! (
1133+ read_seed_marker( target. path( ) ) . unwrap( ) . allocations[ "tinker.md" ] . installed_digest,
1134+ recorded
1135+ ) ;
1136+ }
1137+
1138+ #[ test]
1139+ fn repair_keeps_corrupt_manifest_fail_closed ( ) {
1140+ let source = tempdir ( ) . unwrap ( ) ;
1141+ let target = tempdir ( ) . unwrap ( ) ;
1142+ write_agent (
1143+ source. path ( ) ,
1144+ "berdy.md" ,
1145+ "---\n name: Berdy\n metadata:\n berdBundled: true\n ---\n Bundled." ,
1146+ ) ;
1147+ fs:: write ( marker_path ( target. path ( ) ) , "{" ) . unwrap ( ) ;
1148+ seed_bundled_agents_from_dir ( source. path ( ) , target. path ( ) ) . unwrap ( ) ;
1149+
1150+ let error =
1151+ repair_bundled_agent_from_dir ( source. path ( ) , target. path ( ) , "berdy.md" ) . unwrap_err ( ) ;
1152+
1153+ assert ! ( error. contains( "valid managed allocation manifest" ) ) ;
1154+ assert_eq ! ( read_seed_marker( target. path( ) ) . unwrap( ) . version, 0 ) ;
1155+ assert ! ( !target. path( ) . join( "berdy.md" ) . exists( ) ) ;
1156+ }
1157+
1158+ #[ test]
1159+ fn repair_resumes_pending_allocation_without_duplicate ( ) {
1160+ let source = tempdir ( ) . unwrap ( ) ;
1161+ let target = tempdir ( ) . unwrap ( ) ;
1162+ let contents = "---\n name: Berdy\n metadata:\n berdBundled: true\n ---\n Bundled." ;
1163+ write_agent ( source. path ( ) , "berdy.md" , contents) ;
1164+ seed_bundled_agents_from_dir ( source. path ( ) , target. path ( ) ) . unwrap ( ) ;
1165+ let mut marker = read_seed_marker ( target. path ( ) ) . unwrap ( ) ;
1166+ marker. allocations . get_mut ( "berdy.md" ) . unwrap ( ) . status = AllocationStatus :: Pending ;
1167+ marker. install_state = InstallState :: Installing ;
1168+ write_seed_marker ( target. path ( ) , & marker) . unwrap ( ) ;
1169+
1170+ repair_bundled_agent_from_dir ( source. path ( ) , target. path ( ) , "berdy.md" ) . unwrap ( ) ;
1171+
1172+ let marker = read_seed_marker ( target. path ( ) ) . unwrap ( ) ;
1173+ assert_eq ! (
1174+ marker. allocations[ "berdy.md" ] . status,
1175+ AllocationStatus :: Installed
1176+ ) ;
1177+ assert_eq ! ( marker. install_state, InstallState :: Complete ) ;
1178+ assert_eq ! (
1179+ fs:: read_dir( target. path( ) )
1180+ . unwrap( )
1181+ . filter_map( Result :: ok)
1182+ . filter(
1183+ |entry| entry. path( ) . extension( ) . and_then( |value| value. to_str( ) ) == Some ( "md" )
1184+ )
1185+ . count( ) ,
1186+ 1
1187+ ) ;
1188+ }
10901189}
0 commit comments