rightsize 0.7.8 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/backend-docker/backend.d.ts +4 -1
  2. package/dist/backend-docker/backend.d.ts.map +1 -1
  3. package/dist/backend-docker/backend.js +4 -0
  4. package/dist/backend-docker/backend.js.map +1 -1
  5. package/dist/backend-msb/backend.d.ts +618 -68
  6. package/dist/backend-msb/backend.d.ts.map +1 -1
  7. package/dist/backend-msb/backend.js +1236 -133
  8. package/dist/backend-msb/backend.js.map +1 -1
  9. package/dist/backend-msb/commands.d.ts +160 -16
  10. package/dist/backend-msb/commands.d.ts.map +1 -1
  11. package/dist/backend-msb/commands.js +197 -31
  12. package/dist/backend-msb/commands.js.map +1 -1
  13. package/dist/backend-msb/provisioner.d.ts +1 -1
  14. package/dist/backend-msb/provisioner.d.ts.map +1 -1
  15. package/dist/backend-msb/provisioner.js +1 -1
  16. package/dist/backend-msb/provisioner.js.map +1 -1
  17. package/dist/backend-msb/restore-access-denied.d.ts +50 -0
  18. package/dist/backend-msb/restore-access-denied.d.ts.map +1 -0
  19. package/dist/backend-msb/restore-access-denied.js +52 -0
  20. package/dist/backend-msb/restore-access-denied.js.map +1 -0
  21. package/dist/backend-msb/restore-broker.d.ts +181 -0
  22. package/dist/backend-msb/restore-broker.d.ts.map +1 -0
  23. package/dist/backend-msb/restore-broker.js +264 -0
  24. package/dist/backend-msb/restore-broker.js.map +1 -0
  25. package/dist/backend-msb/sandbox-already-exists.d.ts +32 -0
  26. package/dist/backend-msb/sandbox-already-exists.d.ts.map +1 -0
  27. package/dist/backend-msb/sandbox-already-exists.js +35 -0
  28. package/dist/backend-msb/sandbox-already-exists.js.map +1 -0
  29. package/dist/backend-msb/snapshot-create.d.ts +22 -0
  30. package/dist/backend-msb/snapshot-create.d.ts.map +1 -0
  31. package/dist/backend-msb/snapshot-create.js +33 -0
  32. package/dist/backend-msb/snapshot-create.js.map +1 -0
  33. package/dist/backend-msb/snapshot-import.d.ts +30 -14
  34. package/dist/backend-msb/snapshot-import.d.ts.map +1 -1
  35. package/dist/backend-msb/snapshot-import.js +35 -17
  36. package/dist/backend-msb/snapshot-import.js.map +1 -1
  37. package/dist/backend-msb/snapshot-rm.d.ts +28 -0
  38. package/dist/backend-msb/snapshot-rm.d.ts.map +1 -0
  39. package/dist/backend-msb/snapshot-rm.js +30 -0
  40. package/dist/backend-msb/snapshot-rm.js.map +1 -0
  41. package/dist/core/backend.d.ts +53 -22
  42. package/dist/core/backend.d.ts.map +1 -1
  43. package/dist/core/checkpoint/api.d.ts +2 -2
  44. package/dist/core/checkpoint/api.js +2 -2
  45. package/dist/core/checkpoint/registry.d.ts +34 -0
  46. package/dist/core/checkpoint/registry.d.ts.map +1 -1
  47. package/dist/core/checkpoint/registry.js +20 -1
  48. package/dist/core/checkpoint/registry.js.map +1 -1
  49. package/dist/core/errors.d.ts +49 -0
  50. package/dist/core/errors.d.ts.map +1 -1
  51. package/dist/core/errors.js +61 -0
  52. package/dist/core/errors.js.map +1 -1
  53. package/dist/core/generic-container.d.ts +58 -10
  54. package/dist/core/generic-container.d.ts.map +1 -1
  55. package/dist/core/generic-container.js +222 -27
  56. package/dist/core/generic-container.js.map +1 -1
  57. package/dist/core/model.d.ts +3 -2
  58. package/dist/core/model.d.ts.map +1 -1
  59. package/dist/core/sandbox-name.d.ts +3 -0
  60. package/dist/core/sandbox-name.d.ts.map +1 -0
  61. package/dist/core/sandbox-name.js +19 -0
  62. package/dist/core/sandbox-name.js.map +1 -0
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/modules/minio.d.ts +5 -3
  68. package/dist/modules/minio.d.ts.map +1 -1
  69. package/dist/modules/minio.js +14 -4
  70. package/dist/modules/minio.js.map +1 -1
  71. package/package.json +1 -1
  72. package/dist/backend-msb/snapshot-list.d.ts +0 -43
  73. package/dist/backend-msb/snapshot-list.d.ts.map +0 -1
  74. package/dist/backend-msb/snapshot-list.js +0 -53
  75. package/dist/backend-msb/snapshot-list.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import type { SandboxBackend, SandboxHandle, FollowHandle, NetworkLink, ReaperKillCommand, BackendCapabilities } from "../core/backend.js";
2
2
  import type { ContainerSpec, ExecResult } from "../core/model.js";
3
+ import { type RestoreBrokerLauncher } from "./restore-broker.js";
3
4
  /**
4
5
  * True if `output` (an `msb run` invocation's combined output) is msb
5
6
  * refusing to run anything while its internal install lock is held. Captured
@@ -23,6 +24,25 @@ import type { ContainerSpec, ExecResult } from "../core/model.js";
23
24
  * timestamp varies per occurrence.
24
25
  */
25
26
  export declare function isMsbInstallLockActive(output: string): boolean;
27
+ /**
28
+ * Constructor options for {@link MsbCliBackend}. Every field is optional so
29
+ * every existing `new MsbCliBackend(msbPathPromise)` call site keeps
30
+ * compiling and behaving unchanged.
31
+ */
32
+ export interface MsbCliBackendOptions {
33
+ /**
34
+ * The launcher POLICY v2's broker escalation calls through for every
35
+ * brokered restore attempt (see `RestoreLaunchMode`/`restore-broker.ts`).
36
+ * Defaults to `realRestoreBroker`, the real `powershell.exe`/WMI
37
+ * implementation — production code never needs to pass this. Tests inject
38
+ * a fake here instead of driving a real `powershell.exe`, the same role
39
+ * `checkpointRebootAlreadyExistsRetryBudgetMs` already plays for the
40
+ * retry budget itself, just as a constructor option rather than an
41
+ * unsafe-cast field, since production code (not only tests) needs a way to
42
+ * supply it.
43
+ */
44
+ restoreBroker?: RestoreBrokerLauncher;
45
+ }
26
46
  /**
27
47
  * The attached-mode CLI driver: every sandbox this backend starts runs as a
28
48
  * held child process (`msb run`, no `-d`) because detached mode never
@@ -50,7 +70,22 @@ export declare class MsbCliBackend implements SandboxBackend {
50
70
  private readonly handles;
51
71
  private readonly startedNames;
52
72
  private resolvedMsbPath;
53
- constructor(msbPathPromise: Promise<string>);
73
+ private checkpointRebootAlreadyExistsRetryBudgetMs;
74
+ private checkpointRebootAlreadyExistsRetryDelayMs;
75
+ private readonly restoreBroker;
76
+ private platformOverrideForTests;
77
+ constructor(msbPathPromise: Promise<string>, options?: MsbCliBackendOptions);
78
+ /**
79
+ * Whether this process should treat itself as running on Windows for
80
+ * POLICY v2's broker-escalation gate — `process.platform === "win32"` in
81
+ * production, or `platformOverrideForTests` when a test has set it. The
82
+ * ONLY thing this gates is whether `rebootUnderFreshName`/
83
+ * `retryRestoreAfterAccessDenied` ever set `RestoreLaunchMode` to
84
+ * `"broker"`; `bootOnce`/`bootRestoreOnce` themselves stay platform-
85
+ * agnostic dispatchers on whatever mode they're handed, so the broker path
86
+ * itself is exercisable on any host once a test picks it explicitly.
87
+ */
88
+ private isWindowsPlatform;
54
89
  private msbPath;
55
90
  create(spec: ContainerSpec): Promise<SandboxHandle>;
56
91
  /**
@@ -88,34 +123,288 @@ export declare class MsbCliBackend implements SandboxBackend {
88
123
  * knows: the install-lock poll, the one-shot state-database retry, and the
89
124
  * one-shot image-cache heal. Both the ordinary `start()` path and the
90
125
  * checkpoint cycle's post-snapshot reboot come through here — a reboot
91
- * from a snapshot is as exposed to msb's transients as any other boot,
92
- * and skipping the classification there turned a passing install-lock
93
- * poll into an immediate checkpoint failure on a live Windows run.
126
+ * from a snapshot (now `msb restore ...`, see `bootRestoreOnce`) is
127
+ * as exposed to msb's transients as any other boot, and skipping the
128
+ * classification there turned a passing install-lock poll into an
129
+ * immediate checkpoint failure on a live Windows run. The image-cache heal
130
+ * targets `handle.spec.image`, which for a restore is the checkpoint ref
131
+ * GenericContainer.fromCheckpoint() threaded through as the builder's
132
+ * "image" — never a real OCI reference `msb image remove` can act on
133
+ * meaningfully; this predates the 0.7.1 migration (the same was true of
134
+ * `--from-snapshot` boots) and is unchanged here.
135
+ *
136
+ * `retryAccessDenied` (default `true`) gates ONLY the
137
+ * `RestoreAccessDeniedError` branch below — every other classified retry
138
+ * always runs regardless. The ordinary `start()` path (including a plain
139
+ * `GenericContainer.fromCheckpoint(cp).start()` restore) leaves it at the
140
+ * default, delegating to `retryRestoreAfterAccessDenied` — see that
141
+ * method's own doc for why it, like `createCheckpoint`'s own reboot, must
142
+ * mint a fresh name for every retry rather than reusing the one that just
143
+ * failed: the live-verified dossier this backend's fresh-naming behavior
144
+ * is built from says a restore that fails PAST msb's own artifact
145
+ * validation (this access-denied signature included) leaves the attempted
146
+ * name behind as a stopped sandbox record, so a same-name retry only ever
147
+ * collides with that record instead of retrying the actual transient.
148
+ * `createCheckpoint`'s own reboot (`rebootUnderFreshName`) passes `false`:
149
+ * it owns this error class itself, one layer up, via its own fresh-naming
150
+ * loop, and needs this error to propagate on the first hit rather than
151
+ * being retried here at all.
152
+ *
153
+ * `restoreLaunchMode` (default `"direct"`) is threaded straight down to
154
+ * every `bootOnce` call this method makes, including its own internal
155
+ * install-lock/state-db/image-cache retries — POLICY v2's broker
156
+ * escalation, once triggered by the caller (`rebootUnderFreshName`/
157
+ * `retryRestoreAfterAccessDenied`), applies to EVERY remaining attempt of
158
+ * that reboot, not just the ones those callers themselves loop over, so an
159
+ * install-lock hit mid-brokered-attempt must retry brokered too rather
160
+ * than silently reverting to a direct spawn. See `RestoreLaunchMode`'s own
161
+ * doc.
94
162
  */
95
163
  private bootClassified;
96
164
  /**
97
- * One boot attempt: spawns the attached `msb run` child and polls until the
98
- * sandbox reaches Running. `state.attached` and (for non-keepAlive specs)
99
- * `startedNames` are populated only on success; on any failure the child is reaped here (for
165
+ * One boot attempt, dispatched by shape: an ordinary spec (no
166
+ * `checkpointRef`) drives `run()`'s ATTACHED supervision model
167
+ * (`bootRunOnce`); a checkpoint-restore spec drives `msb restore`'s
168
+ * fundamentally different DETACHED shape (`bootRestoreOnce`) — see each
169
+ * method's own doc. Resets the per-attempt diagnostics tail before either:
170
+ * a retried boot must not blend its tail with the failed attempt's.
171
+ * `restoreLaunchMode` is passed straight through to `bootRestoreOnce`
172
+ * (irrelevant to `bootRunOnce`'s own ordinary `run` path, which never
173
+ * brokers — see `RestoreLaunchMode`'s own doc).
174
+ */
175
+ private bootOnce;
176
+ /**
177
+ * One RUN boot attempt: spawns ATTACHED `msb run`, which stays alive as
178
+ * the sandbox's own supervisor for its whole lifetime, and polls
179
+ * `msb ls --format json` until the name shows Running.
180
+ *
181
+ * `state.attached` and (for non-keepAlive specs) `startedNames` are
182
+ * populated only on success; on any failure the child is reaped here (for
100
183
  * the classified early-exit failures it has already exited; a readiness
101
184
  * timeout leaves it alive and it is hard-killed) so a failed attempt leaves
102
185
  * no live process or registered cleanup state behind — the caller owns
103
- * retry policy, never cleanup (an exit-0 fast-exit success is the one
104
- * exception: it IS success, so `state.attached`/`startedNames` ARE
105
- * populated for it — see below). An early exit is classified from the
106
- * child's combined output: the image-cache-corruption signature throws
107
- * `ImageCacheCorruptionError` (the one failure `start()` heals and
108
- * retries), a host-port bind conflict throws `PortBindConflictError`, and
109
- * otherwise if the exit code was 0 `isCompletedFastExit` gets a chance
110
- * to reclassify the exit as a workload that ran to completion before this
111
- * poll loop could ever observe Running (msb 0.6.16's convergent-lifecycle
112
- * rework; see that method's own doc); anything else surfaces the raw
113
- * output as an ordinary boot failure, unchanged.
186
+ * retry policy, never cleanup (an exit-0 success without a live child is
187
+ * the one exception: the fast-exit case below, see `isCompletedFastExit`'s
188
+ * own doc). An early exit is classified from the child's combined output:
189
+ * the image-cache-corruption signature throws `ImageCacheCorruptionError`
190
+ * (the one failure `start()` heals and retries), a host-port bind conflict
191
+ * throws `PortBindConflictError`, and otherwise if the exit code was 0 —
192
+ * `isCompletedFastExit` gets a chance to reclassify the exit as a workload
193
+ * that ran to completion before this poll loop could ever observe Running
194
+ * (msb 0.6.16's convergent-lifecycle rework; see that method's own doc);
195
+ * anything else surfaces the raw output as an ordinary boot failure,
196
+ * unchanged.
114
197
  */
115
- private bootOnce;
198
+ private bootRunOnce;
199
+ /**
200
+ * One RESTORE boot attempt. `msb restore` is not `run`'s attached
201
+ * supervision model at all — EMPIRICALLY VERIFIED against msb 0.7.1 (its
202
+ * own `restore.rs` doc: "Restore a snapshot into a new detached
203
+ * sandbox"), the restore CLI process exits — typically within seconds,
204
+ * often with little or no stdout — the INSTANT activation succeeds, while
205
+ * the sandbox keeps booting in the background and only reaches Running
206
+ * some time after that (live-confirmed: `msb ls` shows Running, and exec
207
+ * works, only once the restore process has already exited). A clean exit
208
+ * is therefore not itself a completed boot, and a nonzero exit is msb's
209
+ * own failure signal with the reason on stdout/stderr — the inverse of
210
+ * `run`'s attached child, whose exit before Running always means failure.
211
+ *
212
+ * Two phases follow directly from those two facts:
213
+ * 1. Spawn `msb restore ...` and wait for IT to exit, bounded by
214
+ * `FIRST_RUN_PULL_TIMEOUT_MS` — the same boot budget `bootRunOnce`
215
+ * polls against; a restore's own activation is exposed to the same
216
+ * "this could be a slow cold operation" uncertainty a pull is.
217
+ * Classified from the combined output the same way any boot's early
218
+ * exit is (install-lock, state-db, image-cache-corruption,
219
+ * port-bind-conflict — see `bootClassified`'s own doc on why a
220
+ * restore boot needs the identical classification an ordinary one
221
+ * gets) — a nonzero, unclassified exit throws an ordinary
222
+ * `BackendError` carrying the output, same shape as `bootRunOnce`'s.
223
+ * 2. Once it exits 0, poll `msb ls` for the sandbox to reach Running —
224
+ * the same `READINESS_POLL_MS` cadence and a fresh instance of the
225
+ * identical `FIRST_RUN_PULL_TIMEOUT_MS` budget the attached path's
226
+ * own poll uses (so a restore boot gets the identical readiness
227
+ * patience an ordinary one does, counted from when there is
228
+ * actually something to poll for rather than shaved out of phase
229
+ * 1's own budget). The sandbox settling on exactly `"Stopped"`, or
230
+ * its name disappearing from a `msb ls` listing that itself came
231
+ * back, is a definite failure the moment it is observed — never
232
+ * worth waiting out the rest of the budget for, unlike a status
233
+ * that simply hasn't reached Running yet.
234
+ * 3. The `ls` probe itself failing to run or return in time (spawn
235
+ * error, or the same `LOGS_TIMEOUT_MS` `invoke` enforces on every
236
+ * other probe) is a different thing entirely from the sandbox's name
237
+ * being missing from a listing that DID come back, even though both
238
+ * collapse to the same `undefined` once the probe's own promise is
239
+ * caught — see `isCompletedFastExit`'s own doc, whose sibling
240
+ * contract this mirrors: a probe failure must never be upgraded into
241
+ * a false, specific diagnosis ("disappeared from msb ls entirely" is
242
+ * exactly that kind of false claim when the truth is just that `ls`
243
+ * itself didn't answer in time). A failed probe is therefore treated
244
+ * as "not yet confirmed either way" and retried within budget —
245
+ * install-lock races and an overloaded msb daemon are the same
246
+ * transient shapes this file already documents for `run`/`restore`/
247
+ * `stop`, and one `ls` hiccup during the readiness window must not
248
+ * turn into a spurious restore failure.
249
+ *
250
+ * The `msb restore` CLI process itself is never held onto — it has already
251
+ * exited by the time phase 2 even starts, and msb itself (out-of-process)
252
+ * is the sandbox's own supervisor from here on, the same as it always was.
253
+ * But a restored sandbox reaches Running with ONLY its guest agent inside
254
+ * — the captured workload never re-executes on its own (EMPIRICALLY
255
+ * VERIFIED against msb 0.7.1) — so once Running is confirmed, this method
256
+ * calls `reviveWorkload` to start it itself BEFORE returning: THAT call is
257
+ * what populates `state.attached` (a workload-revival `msb exec` child,
258
+ * not the restore CLI process), and `stop()`'s ordinary attached-child
259
+ * handling (exit-based death detection, the SIGKILL escalation) applies to
260
+ * IT exactly as it always did for `bootRunOnce`'s own attached `msb run`
261
+ * child. See `reviveWorkload`'s own doc for the full revival contract,
262
+ * including the typed error a checkpoint predating workload-cmdline
263
+ * capture throws instead of booting silently idle.
264
+ *
265
+ * Phase 1 (spawning the restore itself and reading back its own exit code
266
+ * + combined output) is dispatched by `launchMode` to either
267
+ * `launchRestoreDirect` (the unconditional spawn this method has always
268
+ * used) or `launchRestoreViaBroker` (POLICY v2's Windows job-object
269
+ * escape hatch — see `RestoreLaunchMode`/`restore-broker.ts`'s own doc).
270
+ * Both return the exact same shape, classified identically right below —
271
+ * a brokered attempt's output goes through the SAME predicates
272
+ * (`isImageCacheCorruption`, `isRestoreAccessDeniedFailure`, ...) a direct
273
+ * attempt's always has, since the underlying `msb restore` invocation and
274
+ * its failure signatures are identical either way; only how its stdout/
275
+ * stderr and exit code got back to this process differs. A broker launch
276
+ * can also come back `"unconfirmed"` (its own `ecFile` never appeared
277
+ * within its bound) — see `RestoreBrokerUnconfirmed`'s own doc — which
278
+ * skips classification entirely and falls straight through to phase 2
279
+ * exactly as a confirmed exit 0 does, since that phase's own `msb ls` poll
280
+ * is activation-gated and settles the question on its own regardless.
281
+ */
282
+ private bootRestoreOnce;
283
+ /**
284
+ * `bootRestoreOnce`'s own DIRECT launch — the unconditional child-process
285
+ * spawn every restore attempt used before POLICY v2's broker escalation
286
+ * existed, extracted unchanged from that method's own former body so the
287
+ * two `RestoreLaunchMode`s share one classification point (see
288
+ * `bootRestoreOnce`'s own doc). Bounded by the same `FIRST_RUN_PULL_TIMEOUT_MS`
289
+ * budget `bootRunOnce` polls against, and always resolves `"completed"` —
290
+ * a direct spawn has no analog of the broker's own `"unconfirmed"` outcome,
291
+ * since this process reads the child's real exit code directly.
292
+ */
293
+ private launchRestoreDirect;
294
+ /**
295
+ * `bootRestoreOnce`'s own BROKERED launch — POLICY v2's Windows job-object
296
+ * escape hatch (see `RestoreLaunchMode`/`restore-broker.ts`'s own module
297
+ * doc on the root cause and the live-validated WMI mitigation). Calls the
298
+ * injected `restoreBroker` seam with the exact same argv a direct attempt
299
+ * would spawn (`MsbCommands.restore`, unmodified) and the same
300
+ * `FIRST_RUN_PULL_TIMEOUT_MS` budget.
301
+ *
302
+ * The broker's own result is either passed straight through
303
+ * (`"completed"`/`"unconfirmed"`, both appended to `state.logTail` for the
304
+ * same pre-Running diagnostics purpose `drainTail` serves on the direct
305
+ * path) or, if the broker call itself REJECTS — broker infrastructure
306
+ * failing (a missing `powershell.exe`, a script-file write failure, WMI
307
+ * refusing to even create the process; see `RestoreBrokerLauncher`'s own
308
+ * doc on what counts as this vs. an ordinary brokered-restore failure) —
309
+ * caught here and turned into a one-attempt fallback to
310
+ * `launchRestoreDirect` instead (POLICY v2 point 5: the broker must never
311
+ * become a new single point of failure). That fallback is scoped to THIS
312
+ * attempt only; it does not touch the caller's own `RestoreLaunchMode` for
313
+ * whatever attempt comes after it.
314
+ */
315
+ private launchRestoreViaBroker;
316
+ /**
317
+ * Renders a restore boot's post-mortem message once its poll for Running
318
+ * has definitely failed (a settled `"Stopped"`, the name disappearing
319
+ * from `msb ls` outright, or the readiness budget itself running out).
320
+ * `msb restore` itself already exited 0 by this point, so the CLI
321
+ * process's own output (already surfaced for a nonzero exit, above) has
322
+ * nothing left to add — `msb logs --source system` is the one channel
323
+ * that can still show what happened to the sandbox's own background boot
324
+ * (see `MsbCommands.systemLog`'s own doc and `isCompletedFastExit`, its
325
+ * other caller). Best-effort: a failed probe falls back to a plain "no
326
+ * output" note rather than letting a diagnostics-gathering failure itself
327
+ * throw and mask the real one.
328
+ */
329
+ private restoreBootFailureMessage;
330
+ /**
331
+ * Starts the workload a restore itself never re-runs. EMPIRICALLY VERIFIED
332
+ * against msb 0.7.1: a restored sandbox reaches Running with ONLY its
333
+ * guest agent inside (`guest ps` shows `/init.krun` and kernel threads —
334
+ * the captured workload command does not re-execute; `msb start`/`msb
335
+ * logs` on such a sandbox are equally idle/empty). Only ever called from
336
+ * `bootRestoreOnce`, once it has confirmed Running — see this class's own
337
+ * doc on `bootRestoreOnce`.
338
+ *
339
+ * Spawns a LONG-LIVED, attached `msb exec [-e K=V]... <name> -- <argv>`
340
+ * session (`MsbCommands.execWithEnv`, env from `handle.spec.env` — a
341
+ * restore's own `msb restore` has no `-e`/`--env` flag at all, so this exec
342
+ * is the one place a restored sandbox's guest ever sees it again) — this
343
+ * becomes the sandbox's own workload from here on, and exec sessions are
344
+ * exactly what msb's own log capture records (the primary session's
345
+ * stdout/stderr land in `exec.log`, served by `msb logs`/`-f` — see
346
+ * `MsbCommands.exec`'s own doc). This exec child slots into the EXACT SAME
347
+ * `state.attached` role `bootRunOnce`'s own attached `msb run` child fills
348
+ * for an ordinary boot: child-exit-based death detection, reap-on-stop
349
+ * (`stop()`'s SIGKILL escalation), and every other attached-child teardown
350
+ * semantic apply to it unchanged — the detached-restore round left that
351
+ * slot merely optional, never removed it.
352
+ *
353
+ * `handle.spec.command` is ALREADY the fully-resolved workload argv by the
354
+ * time this runs, in priority order: an explicit command the source
355
+ * container carried, or — when it had none — the guest cmdline
356
+ * `createCheckpoint` captured at checkpoint time, merged in by
357
+ * `createCheckpoint` itself (for its own immediate reboot) or by
358
+ * `fromCheckpointRegistryEntry` (for a registry-mediated restore, same or
359
+ * later process — see both functions' own docs). `undefined` here means
360
+ * NEITHER source exists — an old registry entry predating capture, or one
361
+ * whose capture attempt itself failed — so this throws
362
+ * `CheckpointWorkloadCommandMissingError` itself rather than depending on
363
+ * a caller to have checked first: never boot a restored sandbox silently
364
+ * idle.
365
+ *
366
+ * The exec child gets a brief settle window (`EXEC_REVIVE_SETTLE_MS`) to
367
+ * prove it isn't an immediate boot failure before this returns success —
368
+ * mirroring `bootRunOnce`'s own exit-vs-Running race, except a restore's
369
+ * exec session has no separate "Running" signal of its own to poll for
370
+ * (the SANDBOX already reports Running regardless of whether this exec
371
+ * succeeds), so a settle window is what that race collapses to here. An
372
+ * exit observed within the window is classified exactly like
373
+ * `bootRunOnce`'s own early exit: exit 0 counts as success only when
374
+ * `isCompletedFastExit` ALSO confirms it (the sandbox itself settled
375
+ * Stopped with the boot-completion marker — the repo's existing fast-exit-
376
+ * completion semantics, reused verbatim); any other exit — nonzero, or
377
+ * exit 0 without that confirmation — throws a `BackendError` carrying the
378
+ * exec child's own output, the same failure shape an attached run's early
379
+ * exit already has.
380
+ */
381
+ private reviveWorkload;
382
+ /**
383
+ * Best-effort: execs `CAPTURE_WORKLOAD_CMDLINE_SCRIPT` in `handle`'s guest
384
+ * and parses its stdout via `parseCapturedWorkloadCmdline`. Only ever
385
+ * called from `createCheckpoint`, BEFORE it stops the sandbox (see that
386
+ * method's own doc), and only when `handle.spec.command` is undefined — an
387
+ * explicit command needs no capture. Never throws: an exec failure
388
+ * (nonzero exit, the exec channel itself erroring) and unparseable output
389
+ * both resolve `undefined` rather than failing the checkpoint — the
390
+ * captured cmdline is a best-effort fallback, and its absence is fully
391
+ * handled later, at restore time (`reviveWorkload` throws
392
+ * `CheckpointWorkloadCommandMissingError` when neither an explicit nor a
393
+ * captured command exists).
394
+ */
395
+ private captureGuestWorkloadCmdline;
396
+ /**
397
+ * SPI implementation of `SandboxBackend.capturedWorkloadCommand` — see its
398
+ * own doc. Reads back whatever `createCheckpoint` most recently stashed on
399
+ * this sandbox's `HandleState` (keyed by name, so it survives the
400
+ * stop/snapshot/reboot cycle's own handle-object churn); `undefined` if
401
+ * this sandbox was never checkpointed, or its checkpoint needed no
402
+ * capture, or the capture attempt failed.
403
+ */
404
+ capturedWorkloadCommand(handle: SandboxHandle): ReadonlyArray<string> | undefined;
116
405
  /**
117
406
  * The fast-exit post-mortem classification: only ever consulted from
118
- * `bootOnce` above, and only once the attached `msb run` child has already
407
+ * `bootRunOnce` above, and only once the attached `msb run` child has already
119
408
  * exited with code 0 before Running was observed. msb 0.6.16's
120
409
  * convergent-lifecycle rework means a workload that finishes quickly is
121
410
  * never observed `"Running"` at all — only `"Starting"`, then the attached
@@ -139,30 +428,237 @@ export declare class MsbCliBackend implements SandboxBackend {
139
428
  private isCompletedFastExit;
140
429
  stop(handle: SandboxHandle): Promise<void>;
141
430
  remove(handle: SandboxHandle): Promise<void>;
431
+ /**
432
+ * `bootClassified`'s own retry for a `RestoreAccessDeniedError` hit on the
433
+ * ordinary `start()`/`fromCheckpoint().start()` path (its default
434
+ * `retryAccessDenied: true`) — the sibling of `rebootUnderFreshName`
435
+ * (`createCheckpoint`'s own reboot retry) for the SAME classified failure,
436
+ * built from the SAME live-verified policy: `msb restore --name X`
437
+ * validates the artifact FIRST — an integrity failure exits 1 and leaves
438
+ * NO sandbox record — but a failure AFTER validation (this access-denied
439
+ * signature included) leaves `X` behind as a STOPPED SANDBOX RECORD, and
440
+ * any retry of `restore --name X` then fails outright with msb's own
441
+ * "already exists" refusal rather than hitting the transient a second
442
+ * time. A same-name retry (what this method replaces) therefore only ever
443
+ * proves that collision — never the actual transient — so this mints a
444
+ * NEW name from `nextSandboxName()` for every retry instead, the same
445
+ * generator `GenericContainer.start()`'s own boot loop and
446
+ * `rebootUnderFreshName` both use, tracking it in the reaper ledger BEFORE
447
+ * that attempt's restore runs and best-effort `msb rm`-ing (then
448
+ * untracking) a failed attempt's own name before advancing — identical
449
+ * per-attempt bookkeeping to `rebootUnderFreshName`, just owned one layer
450
+ * lower.
451
+ *
452
+ * The difference from `rebootUnderFreshName` is entirely about WHO owns
453
+ * the re-keying, not the retry policy itself: `createCheckpoint` calls
454
+ * `rebootUnderFreshName` with no live sandbox left at all (the source was
455
+ * already stopped and removed as part of the snapshot step), so EVERY
456
+ * attempt — including the first — mints a fresh name, and the winning
457
+ * handle is handed back for `createCheckpoint` itself to re-key. Here,
458
+ * `handle` is already live under its own name when `first` (the initial
459
+ * `RestoreAccessDeniedError`) is caught, and `bootClassified`'s caller
460
+ * (`start()`) has no re-keying step of its own — it just awaits
461
+ * `bootClassified` and returns — so this method re-keys
462
+ * `this.handles`/`this.startedNames` AND mutates the caller's own `handle`
463
+ * object IN PLACE itself on success, the same "one carve-out"
464
+ * `SandboxHandle`'s own interface doc allows for `createCheckpoint`'s
465
+ * reboot (see that method's own doc on `handle.id`/`handle.spec`
466
+ * mutation).
467
+ *
468
+ * `handle`'s own original name was already tracked in the reaper ledger by
469
+ * `GenericContainer.start()`'s own boot loop before `create()`/`start()`
470
+ * ever ran (mirroring `rebootUnderFreshName`'s treatment of the checkpoint
471
+ * reboot's own `originalName`) — this method best-effort `msb rm`'s it
472
+ * once `first` is confirmed retryable, but deliberately leaves its own
473
+ * ledger entry alone, for the same not-found-tolerant sweep to find,
474
+ * rather than untracking a name this method never tracked itself.
475
+ *
476
+ * Retries up to `RESTORE_ACCESS_DENIED_RETRY_LIMIT` times,
477
+ * `RESTORE_ACCESS_DENIED_RETRY_DELAY_MS` apart — unchanged from the
478
+ * same-name retry this replaces. Each attempt goes through
479
+ * `bootClassified` itself with `retryAccessDenied: false` (so a
480
+ * `StateDbError`/`InstallLockActiveError`/`ImageCacheCorruptionError` hit
481
+ * mid-retry still gets its own ordinary classified handling; only
482
+ * `RestoreAccessDeniedError`'s own retry is owned here) and treats either
483
+ * `RestoreAccessDeniedError` or `SandboxAlreadyExistsError` as retryable —
484
+ * a freshly minted name should never collide, but `rebootUnderFreshName`'s
485
+ * own live-CI evidence says a loaded host can still refuse one, and the
486
+ * SAME fresh-naming response (rm the failed name, mint another) covers
487
+ * both signatures identically. Anything else propagates immediately,
488
+ * unretried.
489
+ *
490
+ * POLICY v2's broker escalation applies here too, on the exact same terms
491
+ * as `rebootUnderFreshName`'s own (see that method's own doc): this
492
+ * method is only ever entered once the reboot's first, always-direct
493
+ * attempt has already hit `RestoreAccessDeniedError` (`first`), so on a
494
+ * Windows host (`isWindowsPlatform()`) every retry THIS method itself
495
+ * performs is, by construction, an attempt after that trigger — there is
496
+ * no "first attempt still direct" sub-case to preserve inside this loop
497
+ * the way `rebootUnderFreshName` has to across its own. `launchMode` is
498
+ * therefore decided once, before the loop, not re-evaluated per attempt.
499
+ */
500
+ private retryRestoreAfterAccessDenied;
501
+ /**
502
+ * `createCheckpoint`'s own reboot step. Mints a NEW sandbox name from
503
+ * `nextSandboxName()` (the same generator `GenericContainer.start()`'s own
504
+ * boot loop uses) for EVERY attempt, tracks it in the reaper ledger BEFORE
505
+ * that attempt's restore runs (mirroring `GenericContainer.start()`'s own
506
+ * `trackSandbox` call, keepAlive-excluded the same way), and retries two
507
+ * classified failure signatures on that basis instead of surfacing either
508
+ * immediately: msb's "sandbox already exists" refusal
509
+ * (`SandboxAlreadyExistsError`, see `isSandboxAlreadyExistsFailure`'s own
510
+ * doc) and its Windows access-denied refusal on the just-written snapshot
511
+ * artifact (`RestoreAccessDeniedError`, see `isRestoreAccessDeniedFailure`'s
512
+ * own doc — retried here rather than inside `bootClassified` itself, via
513
+ * that method's own `retryAccessDenied: false`, see its doc).
514
+ *
515
+ * LIVE-VERIFIED POLICY this retry is built from: `msb restore --name X`
516
+ * validates the artifact FIRST — an integrity failure exits 1 and leaves NO
517
+ * sandbox record — but a failure AFTER validation (a block-device open
518
+ * PermissionDenied on unix; `RestoreAccessDeniedError`'s own signature on
519
+ * Windows) leaves `X` behind as a STOPPED SANDBOX RECORD visible in `msb
520
+ * ls`, and any retry of `restore --name X` then fails with msb's own
521
+ * "already exists" refusal — so a same-name retry of EITHER classified
522
+ * failure only ever collides with itself. Reusing a failed attempt's name
523
+ * is therefore never safe (`msb rm` alone is not reliably enough either: on
524
+ * Windows it frees only the DB record, not the on-disk directory, which can
525
+ * outlive it well past this budget — see the fresh-name reboot's own doc on
526
+ * `createCheckpoint`) — the only universally safe policy is a NEW name per
527
+ * attempt, same as this backend's ordinary `start()` boot loop already uses
528
+ * for its own, unrelated port-conflict retries. On a classified failure
529
+ * this best-effort `msb rm`'s the failed attempt's own name anyway (result
530
+ * ignored — cheap cleanup, and correctness no longer depends on it now
531
+ * that the next attempt never reuses that name) and untracks it from the
532
+ * ledger, before advancing to a freshly minted name for the next attempt.
533
+ *
534
+ * `this.checkpointRebootAlreadyExistsRetryBudgetMs`/`_RetryDelayMs` back
535
+ * this loop's overall deadline/delay rather than the bare
536
+ * `CHECKPOINT_REBOOT_ALREADY_EXISTS_RETRY_BUDGET_MS`/`_DELAY_MS` constants
537
+ * directly, so a budget-exhaustion test can shrink them to milliseconds
538
+ * instead of actually blocking for the real ~30s — see those fields' own
539
+ * doc. Only `SandboxAlreadyExistsError`/`RestoreAccessDeniedError` are
540
+ * retried here; any other failure `bootClassified` throws (on the first
541
+ * attempt or a later one) propagates immediately, unretried, WITHOUT
542
+ * advancing to a new name — `createCheckpoint`'s own caller untracks that
543
+ * attempt's name on this path (see its own catch block). Not reached by
544
+ * the ordinary `start()` path: a `GenericContainer.fromCheckpoint(cp)
545
+ * .start()` restore's own `RestoreAccessDeniedError` is retried by its own
546
+ * sibling, `retryRestoreAfterAccessDenied` (see that method's own doc) —
547
+ * same fresh-naming policy, applied one layer lower since that path has no
548
+ * separate caller left to hand a replacement handle to for re-keying.
549
+ *
550
+ * Returns the WINNING attempt's own `SandboxHandle` (a fresh object, never
551
+ * `undefined`) — `createCheckpoint` re-keys its runtime registries and the
552
+ * caller's live handle from this, not from a name minted up front.
553
+ *
554
+ * POLICY v2's broker escalation (see `RestoreLaunchMode`/`restore-broker.ts`'s
555
+ * own module doc on the Windows job-object root cause and the live-
556
+ * validated WMI mitigation) lives here: `launchMode` starts `"direct"` —
557
+ * the first attempt of ANY reboot is always a direct spawn, unconditionally,
558
+ * even on Windows — and flips to `"broker"` the moment an attempt hits
559
+ * `RestoreAccessDeniedError` on a Windows host (`isWindowsPlatform()`);
560
+ * every attempt from then on, for the rest of THIS reboot, launches
561
+ * brokered instead, never downgrading back to direct even if a later
562
+ * attempt's own failure is only `SandboxAlreadyExistsError` (still
563
+ * retried, but no longer the trigger — only access-denied escalates).
564
+ * Off Windows, `launchMode` never leaves `"direct"` at all — this reboot's
565
+ * whole retry loop behaves exactly as it did before this policy existed.
566
+ */
567
+ private rebootUnderFreshName;
142
568
  /**
143
569
  * The stop/snapshot/reboot cycle: `msb stop <name>` (reusing this
144
570
  * backend's own `stop()`, which also quiesces the attached child and any
145
- * network-link tunnels), `msb snapshot create --from <name> <ref>`, then
146
- * `msb rm <name>` followed by a fresh ATTACHED boot of the SAME name from
147
- * that snapshot never `msb start`. Upstream's detached-start path
148
- * (`Sandbox::start_detached`) passes `CREATE_BREAKAWAY_FROM_JOB` on
149
- * Windows, which `ERROR_ACCESS_DENIED`s outright whenever the msb CLI runs
150
- * inside a job object that doesn't grant breakaway rights — a Gradle/
151
- * cargo/node test runner on a hosted Windows runner, or any process that
152
- * embeds this library inside its own restrictive job objectand that
153
- * denial is deterministic, not transient, so no retry shape fixes it.
154
- * Attached `msb run` (this backend's normal boot, including `--from-snapshot`
155
- * boots) never hits it, which is why the reboot reuses `bootOnce` instead
156
- * of resuming in place: the stopped sandbox's disk state is already IN the
157
- * snapshot, so `rm`-ing it first and booting a fresh attached sandbox
158
- * under the same name/ports/env/memory (via a spec identical to
159
- * `handle.spec` except `checkpointRef` set to the new `ref`) reproduces
160
- * the exact same observable contract. `bootOnce` swaps `state.attached` to
161
- * the freshly spawned child itself; nothing about `this.handles`/
162
- * `startedNames` or the reaping ledger changes, since the name never
163
- * changed. Its workload restarts from scratch (the VM reboots), which is
164
- * why `capabilities.checkpointRestartsWorkload` is `true` here and the
165
- * generic layer re-runs the wait strategy after this returns.
571
+ * network-link tunnels), `msb snapshot create --from-sandbox <name> <ref>`
572
+ * (whose printed artifact path never `ref` itself becomes the
573
+ * EFFECTIVE checkpoint ref this method returns; see
574
+ * `parseSnapshotCreateArtifactPath`), then `msb rm <name>` followed by
575
+ * `msb restore <effective-ref> --name <fresh-name>` of a FRESH name from
576
+ * that snapshot (via `rebootUnderFreshName`/`bootRestoreOnce`, see
577
+ * `MsbCommands.restore`'s own doc no `--disk-only`, which a disk-scope
578
+ * snapshot rejects) never `msb start`. The fresh namenever the
579
+ * original is minted by `nextSandboxName()` (the same generator
580
+ * `GenericContainer.start()`'s own ordinary boot loop uses; see
581
+ * `core/sandbox-name.ts`), and a NEW one is minted for EVERY attempt, never
582
+ * just once for the whole reboot: EMPIRICALLY VERIFIED against a real msb
583
+ * 0.7.1 binary, a restore that fails PAST msb's own artifact validation
584
+ * (its Windows access-denied signature, `RestoreAccessDeniedError`) leaves
585
+ * its `--name` behind as a STOPPED SANDBOX RECORD, and any retry under that
586
+ * SAME name then collides with msb's own restore-time collision check
587
+ * (`existing.is_some() || dir_exists`) immediately CONFIRMED on Windows
588
+ * CI, where five checkpoint tests collided on their fresh reboot names for
589
+ * the retry loop's ENTIRE budget once the first attempt hit that
590
+ * access-denied failure (see `CHECKPOINT_REBOOT_ALREADY_EXISTS_RETRY_BUDGET_MS`'s
591
+ * own doc). A same-name retry therefore only ever retries through a race of
592
+ * its own making; a NEW name per attempt sidesteps it structurally — a name
593
+ * nothing else has ever used can never collide with a record or a lingering
594
+ * directory left by a name nothing will ever restore under again (see
595
+ * `rebootUnderFreshName`'s own doc for the full retry policy, including its
596
+ * best-effort `msb rm` of each failed attempt's own name). On success, this method
597
+ * mutates `handle.id` and `handle.spec` (name only — see below) IN PLACE
598
+ * on the caller's own `handle`, so every subsequent operation against it
599
+ * (exec/logs/stop/rm, and — one layer up, via the SAME `SandboxHandle`
600
+ * reference — `GenericContainer.checkpoint()`'s own post-reboot
601
+ * `installNetworkLinks`/wait-strategy re-run and `capturedWorkloadCommand()`
602
+ * call) targets the sandbox actually running now. `SandboxBackend`'s own
603
+ * interface doc calls `SandboxHandle` "immutable" — this is the one
604
+ * carve-out, and only this method makes it: a checkpoint reboot is the one
605
+ * operation that changes what sandbox a handle even refers to. `ports`,
606
+ * `env`, and `memoryLimitMb` are untouched by the rename — same ports, env,
607
+ * and memory ceiling as before the reboot, only the name differs.
608
+ * Upstream's
609
+ * detached-start path (`Sandbox::start_detached`) passes
610
+ * `CREATE_BREAKAWAY_FROM_JOB` on Windows, which `ERROR_ACCESS_DENIED`s
611
+ * outright whenever the msb CLI runs inside a job object that doesn't
612
+ * grant breakaway rights — a Gradle/cargo/node test runner on a hosted
613
+ * Windows runner, or any process that embeds this library inside its own
614
+ * restrictive job object — and that denial is deterministic, not
615
+ * transient, so no retry shape fixes it. Before 0.7.1, attached `msb run`
616
+ * (including its `--from-snapshot` boots) never hit this, which is why the
617
+ * reboot has always gone through the classified boot path (`bootClassified`)
618
+ * rather than `msb start`. `msb restore` has no attached/detached
619
+ * distinction of its own — its CLI process always calls `sandbox.detach()`
620
+ * internally and exits once activation is confirmed (see
621
+ * `bootRestoreOnce`'s own doc on why its exit is a SUCCESS signal, not
622
+ * something to reap) — so whether it shares upstream's
623
+ * `CREATE_BREAKAWAY_FROM_JOB` path on Windows is NOT verified here: this
624
+ * backend never runs the real msb binary (see this repo's hard "never boot
625
+ * a sandbox" rule), so this is a real open question for CI, not something
626
+ * this migration could confirm either way. If it recurs, it will surface
627
+ * as an ordinary unclassified `BackendError` out of `bootRestoreOnce`
628
+ * (deterministic exit-code failure, not one of the classified transients
629
+ * above), the same shape a genuine breakaway denial always took.
630
+ * `rm`-ing the sandbox first and restoring a fresh one under a fresh
631
+ * name, same ports/memory (via a spec identical to `handle.spec` except
632
+ * `name`/`checkpointRef` set to the fresh name and the EFFECTIVE ref this
633
+ * method discovers, below — env is no longer threaded through at all, see
634
+ * `MsbCommands.restore`) reproduces the exact same observable contract
635
+ * MODULO the name itself, which was always an implementation detail, not
636
+ * part of what a checkpoint promises to preserve. This backend's own
637
+ * `handles`/`startedNames` registries ARE re-keyed from the original name
638
+ * to the WINNING attempt's name (see below), and each attempt's own fresh
639
+ * name is tracked in the reaping ledger before that attempt's restore is
640
+ * even attempted, exactly like an ordinary `create()` — a failed attempt's
641
+ * name is untracked again by `rebootUnderFreshName` itself, while the
642
+ * original name's own ledger entry is deliberately left alone throughout,
643
+ * for the ledger's existing not-found-tolerant sweep to find (it was
644
+ * already `msb rm`-ed above). Its workload restarts from scratch (the VM
645
+ * reboots), which is why `capabilities.checkpointRestartsWorkload` is
646
+ * `true` here and the generic layer re-runs the wait strategy after this
647
+ * returns — AFTER `bootRestoreOnce` has already revived the workload
648
+ * itself via `reviveWorkload` (see that method's own doc): `msb restore`
649
+ * boots the reboot idle, only the guest agent inside, so `bootRestoreOnce`
650
+ * no longer leaves `state.attached` untouched the way it did before this
651
+ * revival step existed — it now carries the workload-revival exec child,
652
+ * the same attached-child slot `bootRunOnce`'s own `msb run` child fills
653
+ * for an ordinary boot.
654
+ *
655
+ * Before stopping the source sandbox, this also best-effort captures its
656
+ * guest workload cmdline (`captureGuestWorkloadCmdline`) when
657
+ * `handle.spec.command` is undefined — the image's own default entrypoint
658
+ * was running, so there is no explicit command for `reviveWorkload` to
659
+ * fall back on at either this method's own immediate reboot or a later,
660
+ * registry-mediated restore. See that method's own doc for why a capture
661
+ * failure never fails the checkpoint itself.
166
662
  *
167
663
  * If the snapshot step fails, the sandbox is left stopped — no
168
664
  * best-effort restart, since that restart would itself be the broken `msb
@@ -174,19 +670,52 @@ export declare class MsbCliBackend implements SandboxBackend {
174
670
  *
175
671
  * Refuses outright, before any of the above, when `handle.spec.tmpfsRootMb`
176
672
  * is set: a tmpfs root has nothing on disk for a snapshot to capture.
673
+ *
674
+ * `ref` is the WORKING ref this method is asked to checkpoint under — its
675
+ * basename becomes the snapshot create call's `<name>` argument (still
676
+ * meaningful: it lands in msb's own index, see `MsbCommands.snapshotCreate`'s
677
+ * own doc) and, for a path ref, its dirname becomes `--dest-dir`. It is
678
+ * NOT necessarily where the artifact ends up: EMPIRICALLY VERIFIED against
679
+ * a real msb 0.7.1 binary, `snapshot create` always writes under
680
+ * `<destDir-or-default>/<sandbox>/snap_<32-hex-digest>`, a path `name`
681
+ * never determines. This method therefore parses that real artifact path
682
+ * back out of the command's own stdout (last non-empty line, required to
683
+ * be absolute — see `parseSnapshotCreateArtifactPath`) and returns THAT as
684
+ * the EFFECTIVE ref — the one used for the reboot below and the one the
685
+ * caller (`GenericContainer.checkpoint()`) stores in the registry and
686
+ * hands back on the `Checkpoint` it returns. Malformed or unrecognizable
687
+ * output (empty, no absolute last line) throws a `BackendError` quoting
688
+ * the raw, unparsed output verbatim rather than guessing a ref.
177
689
  */
178
- createCheckpoint(handle: SandboxHandle, ref: string): Promise<void>;
690
+ createCheckpoint(handle: SandboxHandle, ref: string): Promise<string>;
179
691
  /**
180
- * Best-effort `msb snapshot rm <basename(ref)>` — "not found" is success,
181
- * the same contract as `removeByName`. msb's own removal deletes both its
182
- * index entry and the dest-dir artifact for a path ref, but afterwards this
183
- * also best-effort recursively deletes the ref path itself: if msb's index
184
- * ever loses track of an artifact without deleting it, the directory would
185
- * otherwise linger under the cache dir forever. That recursive delete is
186
- * gated on `looksLikeCheckpointArtifactDir` first a `ref` is caller-
187
- * supplied (a corrupt registry entry, a hand-edited env var, …), and a
188
- * `fs.rm(ref, { recursive: true })` on an unverified path would happily
189
- * wipe out an arbitrary directory that merely happens to share its name.
692
+ * Best-effort `msb snapshot rm <ref> -f` — "not found" is success, the
693
+ * same contract as `removeByName`. `ref` is passed FULL, never reduced to
694
+ * `path.basename(ref)`: EMPIRICALLY VERIFIED against a real msb 0.7.1
695
+ * binary, name-based removal does not resolve at all the artifact PATH
696
+ * is the only address that reliably works (see `MsbCommands.snapshotRemove`'s
697
+ * own doc). Every real caller already hands this the EFFECTIVE ref
698
+ * `createCheckpoint` returned (an absolute path since 0.7.1), so this is
699
+ * also simply correct for the common case, not just a defensive choice.
700
+ *
701
+ * One failure shape is deliberately NOT swallowed alongside "not found":
702
+ * msb refuses to remove a snapshot that is still the current HEAD of
703
+ * older siblings from the same source sandbox (see
704
+ * `isSnapshotHeadRemovalRefused`) — that refusal propagates as a
705
+ * `BackendError` naming msb's own remedy, rather than silently doing
706
+ * nothing, since automatic head rotation is out of scope for this method
707
+ * (see the checkpoints guide's cleanup section for the documented
708
+ * limitation).
709
+ *
710
+ * msb's own removal deletes both its index entry and the dest-dir
711
+ * artifact for a path ref, but afterwards this also best-effort
712
+ * recursively deletes the ref path itself: if msb's index ever loses
713
+ * track of an artifact without deleting it, the directory would otherwise
714
+ * linger under the cache dir forever. That recursive delete is gated on
715
+ * `looksLikeCheckpointArtifactDir` first — a `ref` is caller-supplied (a
716
+ * corrupt registry entry, a hand-edited env var, …), and a `fs.rm(ref, {
717
+ * recursive: true })` on an unverified path would happily wipe out an
718
+ * arbitrary directory that merely happens to share its name.
190
719
  */
191
720
  removeCheckpoint(ref: string): Promise<void>;
192
721
  /**
@@ -233,24 +762,45 @@ export declare class MsbCliBackend implements SandboxBackend {
233
762
  */
234
763
  exportCheckpoint(ref: string, destFile: string): Promise<void>;
235
764
  /**
236
- * `msb snapshot load <archive>`, then resolves the EFFECTIVE ref: msb
237
- * writes the import under a digest-derived directory name it never lets
238
- * the caller choose (never the archive's own recorded `ref`), parsed from
239
- * the printed artifact path's basename (see `parseImportedDigestDirName`).
765
+ * `msb snapshot load <archive> --dest <checkpointsDir>`, then resolves the
766
+ * EFFECTIVE ref: EMPIRICALLY VERIFIED against a real msb 0.7.1 binary, a
767
+ * successful load prints a `group msb-<hex>: head snap_<digest>
768
+ * (Initialized)` line, a digest line, and finally the loaded artifact's
769
+ * own absolute path as its LAST line — `<checkpointsDir>/<generated-
770
+ * group>/snap_<digest>`, never the archive's own recorded `ref` — parsed
771
+ * back out by `parseImportedArtifactPath` (the same last-non-empty-line,
772
+ * must-be-absolute defensive pattern `parseSnapshotCreateArtifactPath`
773
+ * uses for `snapshot create`). `checkpointsDir` is always this backend's
774
+ * own checkpoints cache directory (`<cacheDir>/checkpoints`, the same
775
+ * directory `checkpointRef()` mints created-checkpoint refs under) —
776
+ * omitting `--dest` would import into msb's own global default snapshot
777
+ * store instead, outside anywhere this library looks for or cleans up
778
+ * checkpoints. Created fresh (mkdir -p) before the load, since nothing
779
+ * guarantees a caller ever created a checkpoint locally first (an
780
+ * imported-only machine has no reason to have this directory yet).
781
+ *
782
+ * This replaces the pre-0.7.1 shape entirely: `snapshot load` used to
783
+ * write under a digest-derived directory name with no `--dest` of its
784
+ * own, so this method had to parse out just that bare NAME and then
785
+ * separately CONFIRM it via `msb snapshot list --format json` before
786
+ * handing it back (the only way to get a usable ref at all, since the
787
+ * bare name alone still had to resolve against msb's default store). 0.7.1
788
+ * prints the full, directly-usable, already-`--dest`-scoped path as part
789
+ * of `load`'s own output, so that whole list-and-confirm round trip is
790
+ * gone — dead code that this migration deletes outright, not merely
791
+ * bypassed.
792
+ *
240
793
  * An already-exists failure — msb's own content-addressed dedup — is
241
794
  * treated as success, since the artifact is already present under that
242
- * digest either way; any OTHER import failure surfaces msb's own stderr.
243
- * The digest-dir basename is then CONFIRMED present via `msb snapshot
244
- * list --format json` (matching it against each entry's
245
- * `name`/`artifact_path`) and returned as-is never the entry's `digest`
246
- * field. Live-verified against msb 0.6.8: the full `sha256:<64hex>`
247
- * digest does not resolve as a snapshot ref at all (`msb snapshot inspect
248
- * sha256:<full>` fails "snapshot not found"); only the digest-dir name
249
- * resolves for `inspect`/`rm`/`run --from-snapshot`, so it not the full
250
- * digest — is the ref this must hand back for `hasCheckpoint` and every
251
- * other snapshot-ref call to keep working. `_ref` (the archive's own
252
- * recorded ref) is unused here — msb's importer never takes one, unlike
253
- * docker's, where the effective ref really is the ref passed in.
795
+ * digest either way, and its stderr is parsed the same way as a success's
796
+ * stdout; any OTHER import failure surfaces msb's own stderr in a
797
+ * `BackendError`, and unparseable output (no recognizable absolute last
798
+ * line e.g. only the "group ... (Initialized)" line, msb's OWN wording
799
+ * for a state that is not itself a path) throws a `BackendError` quoting
800
+ * the raw output verbatim rather than misreading a status line as a ref.
801
+ * `_ref` (the archive's own recorded ref) is unused here — msb's importer
802
+ * never takes one, unlike docker's, where the effective ref really is the
803
+ * ref passed in.
254
804
  */
255
805
  importCheckpoint(srcFile: string, _ref: string): Promise<string>;
256
806
  private runningSandboxNames;