Skip to content

Commit fdcbb90

Browse files
committed
fix(agents): keep repair within managed lifecycle
1 parent 04646a3 commit fdcbb90

1 file changed

Lines changed: 147 additions & 48 deletions

File tree

src-tauri/src/services/bundled_agents.rs

Lines changed: 147 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = "---\nname: Tinker\nmetadata:\n berdBundled: true\n---\nOriginal.";
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+
"---\nname: Tinker\nmetadata:\n berdBundled: true\n---\nUpdated 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+
"---\nname: Berdy\nmetadata:\n berdBundled: true\n---\nBundled.",
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 = "---\nname: Berdy\nmetadata:\n berdBundled: true\n---\nBundled.";
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

Comments
 (0)