1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
//! The safety gate `tm catalog apply --prune` applies before it deletes (#391).
//!
//! Why: pruning was the ONLY asset-mutating path in the codebase with none of
//! the guards its neighbours have. `skills::deployer::deploy_one_file` refuses
//! to overwrite a file whose bytes no longer match the recorded checksum, and
//! [`crate::core::skill_repair`] backs up before every overwrite and deletes
//! nothing at all — while `prune_skills_locked` called `remove_dir_all` on any
//! managed stem the current include/exclude rule rejected. Two distinct kinds of
//! user content sat behind that call:
//!
//! 1. **A hand-edited asset.** Its checksum no longer matches the ledger, which
//! is exactly what every other path reads as "the user owns this now".
//! 2. **A pristine user-custom skill.** `skills::tiers::deploy_all_skill_tiers`
//! deploys `~/.trusty-mpm/skills/` in full and explicitly exempt from the
//! harness manifest's bundled include/exclude — but once deployed that stem
//! sits in the ledger with a perfectly matching checksum, indistinguishable
//! from a bundled one. A checksum gate alone waves it straight through, so
//! tier awareness is a SECOND, independent requirement, handled by the
//! caller (see `prune_skills`).
//!
//! What: [`agent_verdict`] and [`skill_verdict`] answer "may prune delete this?"
//! and carry the operator-facing reason when the answer is no; [`back_up_file`]
//! and [`back_up_tree`] copy what is about to go under a timestamped root named
//! by [`prune_backup_root`].
//!
//! Neither verdict ever mutates the ledger. A kept asset stays MANAGED — dropping
//! its entry would reclassify it as user-owned and freeze it against every future
//! deploy, which is the #4881 shape.
//! Test: `crate::core::update_check::apply::tests` —
//! `apply_prune_skips_hand_edited_skill`, `apply_prune_spares_user_tier_skill`,
//! `apply_prune_spares_untracked_file_in_a_managed_skill`,
//! `apply_prune_backs_up_before_removing_a_skill`,
//! `apply_prune_skips_hand_edited_agent`,
//! `apply_prune_spares_a_user_origin_agent`.
use ;
use ApplyError;
use crateAgentManifest;
use crateSkillManifest;
use crate;
/// Whether one managed asset may be deleted, and why not when it may not.
///
/// Why: "left in place" and "deleted" must be reportable separately — a silent
/// skip is how a guard becomes invisible to the operator who asked for a prune.
/// What: `Prunable`, or `Kept` carrying a reason phrased for a log line.
/// Test: every `apply_prune_*` test asserts on one branch or the other.
pub
/// Timestamped backup root for one `--prune` run.
///
/// Why: mirrors [`crate::core::skill_repair::backup_root_for`]'s convention so
/// an operator finds prune backups next to the remediation ones, and a per-run
/// directory means a second prune never overwrites the first run's copies.
/// What: `<framework-root>/backup-catalog-prune-<YYYYmmdd-HHMMSS>`. The
/// directory is NOT created here — only when something is actually backed up.
/// Test: `prune_backup_root_is_timestamped_under_the_framework_root`.
pub
/// May prune delete this managed agent file?
///
/// Why (#391): the same rule `agents::deployer` already applies before an
/// overwrite. Deleting a file whose bytes the user changed destroys work no
/// backup was asked for.
/// A checksum gate alone is not the whole rule, for the same reason it was not
/// on the skill side: it protects an EDITED file, never a pristine one the user
/// owns. Skills had to derive that from the live user source because their
/// ledger records no tier. The agent ledger already carries it —
/// `ManifestEntry::origin`, which `agents::deployer` consults through
/// `Origin::is_framework_owned` to decide refresh-vs-preserve — so this guard
/// reads the same field rather than inventing a second answer.
///
/// Nothing writes a non-`Bundled` origin today, so no live prune reaches that
/// branch. It is closed now because the day something does, it becomes a data
/// -loss path with no test and no record of why it mattered.
/// What: `Prunable` when the file is absent (prune is idempotent — there is
/// nothing to lose), or when the ledger records it framework-owned AND its
/// content still checksums to that entry. `Kept` when the ledger has no entry
/// for it, when the entry is user-owned (`User`/`Registry` — the seed-once
/// tier), when the content differs, or when it cannot be read as text.
/// Test: `apply_prune_skips_hand_edited_agent`,
/// `apply_prune_spares_a_user_origin_agent`, `apply_prune_removes_deselected`.
pub
/// May prune delete this managed skill's whole directory?
///
/// Why (#391): a skill is a DIRECTORY, so the question is not "does one file
/// match" but "is everything under it something trusty-mpm deployed and the user
/// has not touched". `remove_dir_all` takes the untracked sibling with it, which
/// is why an unclaimed file is disqualifying on its own.
/// #5224 asks the IDENTICAL question before removing a retired skill, so the
/// implementation moved to [`crate::core::skill_retire::skill_removal_verdict`]
/// and this is the thin adapter onto prune's own [`Verdict`]. Two copies of a
/// deletion gate is how the two would come to disagree about what is safe.
/// What: `Prunable` when the directory is absent, or when every file under it is
/// claimed by a ledger key belonging to `stem` AND still checksums to that key's
/// entry. `Kept` when the directory holds a file no ledger key claims, when a
/// claimed file's bytes differ, or when anything under it cannot be read. A
/// claimed file that is already gone is not disqualifying — there is nothing
/// left to lose.
/// Test: `apply_prune_skips_hand_edited_skill`,
/// `apply_prune_spares_untracked_file_in_a_managed_skill`,
/// `apply_prune_removes_deselected_skill`.
pub
/// Copy one file under the prune backup root before it is deleted.
///
/// Why (#391): the guards above decide what is safe to delete; the backup is
/// what makes a WRONG decision recoverable. `skill_repair` already treats a
/// backup as a precondition of touching operator state, and deletion is the
/// stronger operation.
/// What: copies `src` to `<backup_root>/<rel>`, creating parents. Returns the
/// backup path.
/// Test: `apply_prune_removes_deselected`.
pub
/// Copy a whole directory tree under the prune backup root before it is deleted.
///
/// Why: the skill counterpart to [`back_up_file`] — a skill's reference files
/// and carried assets go with the directory, so they must come back with it.
/// What: recursively copies `src` to `<backup_root>/<rel>`. Returns that path.
/// Test: `apply_prune_backs_up_before_removing_a_skill`.
pub
/// Recursively copy `src` onto `dest`.
///
/// Why: `std::fs` has no recursive copy, and pulling a crate in for one call
/// site is not worth the dependency.
/// What: creates `dest`, then copies each entry, recursing into directories.
/// Test: through [`back_up_tree`] in `apply_prune_backs_up_before_removing_a_skill`.
/// Create `path`'s parent directory chain.