agentlas 1.0.65 → 1.0.67

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 (163) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +12 -4
  3. package/bin/agentlas.cjs +354 -16
  4. package/engine/acp/server.cjs +34 -3
  5. package/engine/agentlas-api-agent.cjs +151 -120
  6. package/engine/agentlas-banner.cjs +6 -2
  7. package/engine/agentlas-capabilities.cjs +17 -10
  8. package/engine/agentlas-cloud-runtime.cjs +4 -5
  9. package/engine/agentlas-config.cjs +19 -54
  10. package/engine/agentlas-core-harness.cjs +59 -5
  11. package/engine/agentlas-desktop-loadout.cjs +17 -3
  12. package/engine/agentlas-evolution.cjs +75 -18
  13. package/engine/agentlas-experience-exchange.cjs +145 -14
  14. package/engine/agentlas-experience-intake.cjs +2 -2
  15. package/engine/agentlas-i18n.cjs +0 -2
  16. package/engine/agentlas-input.cjs +273 -20
  17. package/engine/agentlas-judgment.cjs +0 -0
  18. package/engine/agentlas-mcp-env.cjs +84 -23
  19. package/engine/agentlas-mcp-wrapper.cjs +36 -4
  20. package/engine/agentlas-memory-governance.cjs +2 -1
  21. package/engine/agentlas-memory-import.cjs +49 -12
  22. package/engine/agentlas-native-host.cjs +279 -44
  23. package/engine/agentlas-onboard.cjs +23 -1
  24. package/engine/agentlas-permissions.cjs +5 -1
  25. package/engine/agentlas-sqlite-policy.cjs +40 -2
  26. package/engine/agentlas-tasks.cjs +7 -0
  27. package/engine/agentlas-tools.cjs +37 -10
  28. package/engine/agentlas-ui.cjs +26 -4
  29. package/engine/agentlas-workforce.cjs +30 -5
  30. package/engine/agentlas-workload-routing.cjs +83 -10
  31. package/engine/agentlas.cjs +21 -4
  32. package/engine/agents/builder.cjs +35 -10
  33. package/engine/agents/files.cjs +81 -10
  34. package/engine/agents/import-local.cjs +10 -2
  35. package/engine/agents/registry.cjs +10 -3
  36. package/engine/agents/router.cjs +7 -7
  37. package/engine/agents/routes.cjs +31 -9
  38. package/engine/automation/daemon.cjs +154 -40
  39. package/engine/automation/launchd.cjs +395 -16
  40. package/engine/automation/schedule.cjs +90 -3
  41. package/engine/automation/store.cjs +163 -59
  42. package/engine/browser/cdp.cjs +190 -17
  43. package/engine/browser/vault.cjs +1 -0
  44. package/engine/cli-output.cjs +96 -30
  45. package/engine/cloud/auth.cjs +131 -13
  46. package/engine/cloud/hub-client.cjs +41 -16
  47. package/engine/cloud-assets/cas.cjs +18 -9
  48. package/engine/cloud-assets/commands.cjs +108 -30
  49. package/engine/cloud-assets/package.cjs +66 -3
  50. package/engine/cloud-assets/restore.cjs +12 -9
  51. package/engine/cloud-assets/state.cjs +609 -116
  52. package/engine/commands/acp.cjs +13 -2
  53. package/engine/commands/automation.cjs +75 -24
  54. package/engine/commands/billing.cjs +6 -1
  55. package/engine/commands/browser.cjs +61 -29
  56. package/engine/commands/build.cjs +72 -14
  57. package/engine/commands/call.cjs +1 -1
  58. package/engine/commands/cd.cjs +1 -1
  59. package/engine/commands/cloud.cjs +2 -1
  60. package/engine/commands/connect.cjs +38 -13
  61. package/engine/commands/creds.cjs +74 -29
  62. package/engine/commands/doctor.cjs +17 -1
  63. package/engine/commands/document.cjs +38 -10
  64. package/engine/commands/env.cjs +6 -1
  65. package/engine/commands/firm.cjs +42 -12
  66. package/engine/commands/graph.cjs +36 -32
  67. package/engine/commands/help.cjs +11 -1
  68. package/engine/commands/hep.cjs +1 -1
  69. package/engine/commands/import.cjs +5 -4
  70. package/engine/commands/index.cjs +1 -1
  71. package/engine/commands/install.cjs +6 -4
  72. package/engine/commands/list.cjs +56 -16
  73. package/engine/commands/login.cjs +5 -0
  74. package/engine/commands/logout.cjs +6 -1
  75. package/engine/commands/mcp.cjs +150 -37
  76. package/engine/commands/memory.cjs +3 -2
  77. package/engine/commands/multimodal.cjs +12 -0
  78. package/engine/commands/native.cjs +5 -1
  79. package/engine/commands/one.cjs +78 -33
  80. package/engine/commands/plugin.cjs +13 -2
  81. package/engine/commands/project.cjs +15 -12
  82. package/engine/commands/research.cjs +18 -7
  83. package/engine/commands/roles.cjs +104 -28
  84. package/engine/commands/route.cjs +2 -2
  85. package/engine/commands/run.cjs +42 -9
  86. package/engine/commands/search.cjs +23 -2
  87. package/engine/commands/setup.cjs +6 -1
  88. package/engine/commands/storm.cjs +44 -6
  89. package/engine/commands/swarm.cjs +47 -9
  90. package/engine/commands/telegram.cjs +6 -1
  91. package/engine/commands/uninstall.cjs +12 -6
  92. package/engine/commands/update.cjs +5 -0
  93. package/engine/commands/upload.cjs +2 -1
  94. package/engine/commands/usage.cjs +6 -1
  95. package/engine/commands/version.cjs +6 -1
  96. package/engine/commands/whoami.cjs +6 -1
  97. package/engine/commands/workforce.cjs +60 -16
  98. package/engine/core/capability-grants.cjs +7 -2
  99. package/engine/core/daemon-client.cjs +36 -7
  100. package/engine/core/db.cjs +10 -2
  101. package/engine/core/desktop-core-fetch.cjs +477 -77
  102. package/engine/core/schema-ensure.cjs +23 -11
  103. package/engine/experience/build.cjs +5 -1
  104. package/engine/experience/intents.cjs +35 -82
  105. package/engine/experience/runtime.cjs +7 -1
  106. package/engine/experience/variant.cjs +58 -4
  107. package/engine/firms/orchestrate.cjs +9 -6
  108. package/engine/graph/ask-model.cjs +22 -11
  109. package/engine/graph/interview.cjs +74 -23
  110. package/engine/graph/package.cjs +63 -8
  111. package/engine/hephaestus/local-core.cjs +18 -5
  112. package/engine/hephaestus/runtime.cjs +48 -43
  113. package/engine/hub/install.cjs +484 -63
  114. package/engine/hub/plugins.cjs +391 -29
  115. package/engine/mcp/consent.cjs +25 -17
  116. package/engine/mcp/contract.cjs +229 -24
  117. package/engine/mcp/inventory.cjs +46 -10
  118. package/engine/mcp/plan.cjs +10 -1
  119. package/engine/mcp/probe.cjs +24 -9
  120. package/engine/memory-cli/curate.cjs +74 -16
  121. package/engine/oberon/common.cjs +37 -12
  122. package/engine/oberon/manifest.cjs +25 -2
  123. package/engine/oberon/outputs.cjs +49 -19
  124. package/engine/oberon/render.cjs +665 -40
  125. package/engine/project/career-graph.cjs +15 -2
  126. package/engine/project/controller.cjs +133 -15
  127. package/engine/project/credentials.cjs +382 -34
  128. package/engine/project/env-file.cjs +93 -8
  129. package/engine/project/memory-context.cjs +11 -57
  130. package/engine/project/ontology.cjs +66 -25
  131. package/engine/project/paths.cjs +42 -8
  132. package/engine/project/seed.cjs +88 -51
  133. package/engine/project/state.cjs +41 -16
  134. package/engine/project/team.cjs +26 -1
  135. package/engine/runtime-refusal.cjs +4 -1
  136. package/engine/runtimes/acp-driver.cjs +56 -9
  137. package/engine/runtimes/auth-evidence.cjs +21 -2
  138. package/engine/runtimes/detect.cjs +28 -8
  139. package/engine/runtimes/kinds.cjs +12 -4
  140. package/engine/runtimes/overrides.cjs +4 -3
  141. package/engine/runtimes/resolve.cjs +8 -7
  142. package/engine/runtimes/roles.cjs +7 -5
  143. package/engine/sessions/apply-fences.cjs +396 -56
  144. package/engine/sessions/fences.cjs +111 -27
  145. package/engine/sessions/memory-turn.cjs +62 -3
  146. package/engine/sessions/orchestrator.cjs +11 -1
  147. package/engine/sessions/prompt.cjs +66 -9
  148. package/engine/sessions/session.cjs +70 -10
  149. package/engine/sessions/sink.cjs +78 -12
  150. package/engine/sessions/store.cjs +29 -7
  151. package/engine/storm/storm.cjs +26 -3
  152. package/engine/storm/swarm.cjs +31 -3
  153. package/engine/telegram/connect.cjs +185 -22
  154. package/engine/ui/commands-catalog.cjs +1 -1
  155. package/engine/ui/palette.cjs +12 -2
  156. package/engine/ui/repl.cjs +57 -9
  157. package/engine/ui/screens.cjs +2 -4
  158. package/engine/vendor/desktop-core.manifest.json +5 -5
  159. package/engine/workforce/capture.cjs +9 -27
  160. package/engine/workforce/deps.cjs +18 -6
  161. package/engine/workforce/local-core-transport.cjs +68 -5
  162. package/package.json +1 -1
  163. package/engine/commands/career-graph.cjs +0 -51
@@ -39,6 +39,12 @@ const CLOUD_PACKAGE_HASH_V2 = "path-sha256-executable-v2";
39
39
  const CLOUD_RESTORE_MARKER_PATH = ".agentlas-cloud-package.json";
40
40
  const CLOUD_ASSET_SCOPES = new Set(["owner-private", "hub-public"]);
41
41
  const CLOUD_LOCAL_EXPERIENCE_LINEAGE_PATH = ".agentlas/experience-relations.jsonl";
42
+ const CLOUD_INSTALL_JOURNAL_MAX_BYTES = 128 * 1024;
43
+ const CLOUD_INSTALL_DB_EXPECTED_KEYS = new Set([
44
+ "id", "slug", "name", "name_en", "tagline", "tagline_en", "system_prompt",
45
+ "mcp_servers_json", "env_requirements_json", "preferred_backend", "trust_grade",
46
+ "installed_at", "tone", "visibility",
47
+ ]);
42
48
 
43
49
  function sha(value) {
44
50
  return crypto.createHash("sha256").update(value).digest("hex");
@@ -221,16 +227,107 @@ function normalizeCloudAssetDescriptor(value, label = "cloud asset descriptor")
221
227
  // ── 파일 모드 강제 (restore 스냅샷은 소유자 전용) ──
222
228
  function cloudApplyPrivateDirectoryMode(directoryPath, platform = process.platform) {
223
229
  if (platform === "win32") return;
230
+ const before = fs.lstatSync(directoryPath);
231
+ if (!before.isDirectory() || before.isSymbolicLink()) {
232
+ throw new Error(`cloud restore directory is unsafe: ${directoryPath}`);
233
+ }
224
234
  fs.chmodSync(directoryPath, 0o700);
225
- const actual = fs.statSync(directoryPath).mode & 0o777;
226
- if (actual !== 0o700) throw new Error(`cloud restore directory mode verification failed: ${directoryPath}`);
235
+ const after = fs.lstatSync(directoryPath);
236
+ if (
237
+ !after.isDirectory() || after.isSymbolicLink() ||
238
+ after.dev !== before.dev || after.ino !== before.ino ||
239
+ (after.mode & 0o777) !== 0o700
240
+ ) throw new Error(`cloud restore directory mode verification failed: ${directoryPath}`);
227
241
  }
228
242
 
229
243
  function cloudApplyPortableFileMode(filePath, mode, platform = process.platform) {
230
244
  if (platform === "win32") return;
245
+ const before = fs.lstatSync(filePath);
246
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) {
247
+ throw new Error(`cloud restore file is unsafe: ${filePath}`);
248
+ }
231
249
  fs.chmodSync(filePath, mode);
232
- const actual = fs.statSync(filePath).mode & 0o777;
233
- if (actual !== mode) throw new Error(`cloud restore file mode verification failed: ${filePath}`);
250
+ const after = fs.lstatSync(filePath);
251
+ if (
252
+ !after.isFile() || after.isSymbolicLink() || after.nlink !== 1 ||
253
+ after.dev !== before.dev || after.ino !== before.ino ||
254
+ (after.mode & 0o777) !== mode
255
+ ) throw new Error(`cloud restore file mode verification failed: ${filePath}`);
256
+ }
257
+
258
+ function cloudDirectoryAnchor(target, label, { allowMissing = true, containedBy = null } = {}) {
259
+ let stat;
260
+ try { stat = fs.lstatSync(target); } catch (error) {
261
+ if (allowMissing && error && error.code === "ENOENT") return null;
262
+ throw error;
263
+ }
264
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
265
+ throw new Error(`${label} is not a safe managed directory`);
266
+ }
267
+ let realpath;
268
+ try { realpath = fs.realpathSync.native(target); }
269
+ catch (error) { throw new Error(`${label} could not be canonicalized: ${error.message}`); }
270
+ if (containedBy && !(
271
+ realpath === containedBy.realpath || realpath.startsWith(`${containedBy.realpath}${path.sep}`)
272
+ )) {
273
+ throw new Error(`${label} escapes its managed root`);
274
+ }
275
+ return { path: target, realpath, dev: stat.dev, ino: stat.ino, nlink: stat.nlink, stat };
276
+ }
277
+
278
+ function cloudAssertDirectoryAnchor(anchor, label, containedBy = null) {
279
+ const current = cloudDirectoryAnchor(anchor.path, label, { allowMissing: false, containedBy });
280
+ if (
281
+ current.realpath !== anchor.realpath || current.dev !== anchor.dev ||
282
+ current.ino !== anchor.ino || current.nlink !== anchor.nlink
283
+ ) {
284
+ throw new Error(`${label} changed while it was being used`);
285
+ }
286
+ return current;
287
+ }
288
+
289
+ function cloudRefreshDirectoryAnchor(anchor, label, containedBy = null) {
290
+ const current = cloudDirectoryAnchor(anchor.path, label, { allowMissing: false, containedBy });
291
+ if (
292
+ current.realpath !== anchor.realpath || current.dev !== anchor.dev ||
293
+ current.ino !== anchor.ino
294
+ ) {
295
+ throw new Error(`${label} changed while it was being used`);
296
+ }
297
+ return current;
298
+ }
299
+
300
+ function cloudSameRegularFile(left, right) {
301
+ return Boolean(
302
+ left && right && left.isFile() && right.isFile() &&
303
+ !left.isSymbolicLink() && !right.isSymbolicLink() &&
304
+ left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink,
305
+ );
306
+ }
307
+
308
+ function cloudEnsurePrivateSubdirectory(root, directory) {
309
+ const relative = path.relative(root, directory);
310
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
311
+ throw new Error("cloud restore subdirectory escapes staging");
312
+ }
313
+ let current = root;
314
+ const rootAnchor = cloudDirectoryAnchor(root, "cloud restore staging", { allowMissing: false });
315
+ const managedPaths = [root];
316
+ for (const part of relative.split(path.sep).filter(Boolean)) {
317
+ current = path.join(current, part);
318
+ try { fs.mkdirSync(current, { recursive: false, mode: 0o700 }); }
319
+ catch (error) { if (!error || error.code !== "EEXIST") throw error; }
320
+ cloudApplyPrivateDirectoryMode(current);
321
+ managedPaths.push(current);
322
+ }
323
+ // Directory nlink changes when a child directory is added, so take every
324
+ // anchor after the complete path is present rather than before a descendant
325
+ // mkdir can legitimately change an ancestor's nlink.
326
+ return managedPaths.map((managedPath, index) => cloudDirectoryAnchor(
327
+ managedPath,
328
+ index === 0 ? "cloud restore staging" : "cloud restore package directory",
329
+ { allowMissing: false, ...(index === 0 ? {} : { containedBy: rootAnchor }) },
330
+ ));
234
331
  }
235
332
 
236
333
  // ── 스테이징 스냅샷 전수 검증 (심링크/특수 엔트리/모드/무결성) ──
@@ -309,10 +406,132 @@ function resolveCloudInstallPath(root, relPath) {
309
406
  }
310
407
 
311
408
  // ── 설치 저널 (crash-recovery 계약) ──
409
+ function cloudInstallLayout(slug, { createParent = false } = {}) {
410
+ if (typeof slug !== "string" || cloudSlug(slug) !== slug) {
411
+ throw new Error(`invalid cloud install slug: ${String(slug || "")}`);
412
+ }
413
+ const dataRoot = userDataDir();
414
+ if (createParent) fs.mkdirSync(dataRoot, { recursive: true, mode: 0o700 });
415
+ else if (!fs.existsSync(dataRoot)) return null;
416
+ if (!fs.statSync(dataRoot).isDirectory()) throw new Error("cloud install user data root is unsafe");
417
+ const realDataRoot = fs.realpathSync.native(dataRoot);
418
+ const parent = path.join(dataRoot, "cloud-agent-installs");
419
+ if (createParent) fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
420
+ else if (!fs.existsSync(parent)) return null;
421
+ const parentStat = fs.lstatSync(parent);
422
+ if (
423
+ !parentStat.isDirectory() || parentStat.isSymbolicLink() ||
424
+ fs.realpathSync.native(parent) !== path.join(realDataRoot, "cloud-agent-installs")
425
+ ) {
426
+ throw new Error("cloud install root is unsafe");
427
+ }
428
+ cloudApplyPrivateDirectoryMode(parent);
429
+ const destination = path.join(parent, slug);
430
+ return {
431
+ parent,
432
+ destination,
433
+ journalPath: path.join(parent, `.${slug}.install-journal.json`),
434
+ };
435
+ }
436
+
437
+ function cloudManagedDirectoryState(target, label, { allowMissing = true } = {}) {
438
+ let stat;
439
+ try { stat = fs.lstatSync(target); } catch (error) {
440
+ if (allowMissing && error && error.code === "ENOENT") return null;
441
+ throw error;
442
+ }
443
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
444
+ throw new Error(`${label} is not a safe managed directory`);
445
+ }
446
+ return stat;
447
+ }
448
+
449
+ function cloudRemoveManagedDirectory(target, label, { anchor = null, containedBy = null } = {}) {
450
+ if (anchor) {
451
+ let current;
452
+ try {
453
+ current = cloudDirectoryAnchor(target, label, { allowMissing: true, containedBy });
454
+ } catch (error) {
455
+ if (error && error.code === "ENOENT") return false;
456
+ throw error;
457
+ }
458
+ if (!current) return false;
459
+ if (
460
+ current.realpath !== anchor.realpath || current.dev !== anchor.dev ||
461
+ current.ino !== anchor.ino
462
+ ) throw new Error(`${label} changed while it was being removed`);
463
+ } else if (containedBy) {
464
+ if (!cloudDirectoryAnchor(target, label, { allowMissing: true, containedBy })) return false;
465
+ } else if (!cloudManagedDirectoryState(target, label)) return false;
466
+ fs.rmSync(target, { recursive: true, force: false });
467
+ return true;
468
+ }
469
+
470
+ function cloudRenameManagedDirectory(source, destination, label, { sourceAnchor = null, parentAnchor = null } = {}) {
471
+ if (parentAnchor) cloudRefreshDirectoryAnchor(parentAnchor, `${label} parent`);
472
+ if (sourceAnchor) {
473
+ cloudRefreshDirectoryAnchor(sourceAnchor, `${label} source`, parentAnchor);
474
+ } else {
475
+ cloudManagedDirectoryState(source, `${label} source`, { allowMissing: false });
476
+ }
477
+ if (cloudManagedDirectoryState(destination, `${label} destination`)) {
478
+ throw new Error(`${label} destination already exists`);
479
+ }
480
+ if (parentAnchor) cloudRefreshDirectoryAnchor(parentAnchor, `${label} parent`);
481
+ fs.renameSync(source, destination);
482
+ if (parentAnchor) {
483
+ cloudRefreshDirectoryAnchor(parentAnchor, `${label} parent`);
484
+ cloudDirectoryAnchor(destination, `${label} destination`, { allowMissing: false, containedBy: parentAnchor });
485
+ }
486
+ }
487
+
488
+ function cloudReadInstallJournal(journalPath) {
489
+ let fd;
490
+ try {
491
+ fd = fs.openSync(journalPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
492
+ const listed = fs.lstatSync(journalPath);
493
+ const before = fs.fstatSync(fd);
494
+ if (
495
+ !listed.isFile() || listed.isSymbolicLink() || listed.nlink !== 1 ||
496
+ !before.isFile() || before.nlink !== 1 ||
497
+ listed.dev !== before.dev || listed.ino !== before.ino ||
498
+ before.size <= 0 || before.size > CLOUD_INSTALL_JOURNAL_MAX_BYTES
499
+ ) {
500
+ throw new Error("journal is not a bounded private file");
501
+ }
502
+ const raw = fs.readFileSync(fd, "utf8");
503
+ const after = fs.fstatSync(fd);
504
+ const current = fs.lstatSync(journalPath);
505
+ if (
506
+ Buffer.byteLength(raw, "utf8") !== before.size ||
507
+ after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size ||
508
+ after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs ||
509
+ current.dev !== before.dev || current.ino !== before.ino || current.nlink !== 1
510
+ ) {
511
+ throw new Error("journal changed while it was read");
512
+ }
513
+ return JSON.parse(raw);
514
+ } finally {
515
+ if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
516
+ }
517
+ }
518
+
519
+ function cloudUnlinkInstallJournal(journalPath) {
520
+ const stat = fs.lstatSync(journalPath);
521
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
522
+ throw new Error("cloud install recovery journal is unsafe");
523
+ }
524
+ fs.unlinkSync(journalPath);
525
+ }
526
+
312
527
  function writeCloudInstallJournal(journalPath, value) {
313
- fs.mkdirSync(path.dirname(journalPath), { recursive: true, mode: 0o700 });
528
+ const match = path.basename(journalPath).match(/^\.([a-z0-9][a-z0-9-]{0,63})\.install-journal\.json$/);
529
+ const layout = match ? cloudInstallLayout(match[1], { createParent: true }) : null;
530
+ if (!layout || path.resolve(journalPath) !== path.resolve(layout.journalPath)) {
531
+ throw new Error("cloud install recovery journal path is unsafe");
532
+ }
314
533
  const temp = `${journalPath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
315
- const fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
534
+ const fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0), 0o600);
316
535
  try {
317
536
  fs.writeFileSync(fd, JSON.stringify(value, null, 2) + "\n", "utf8");
318
537
  fs.fsyncSync(fd);
@@ -320,7 +539,10 @@ function writeCloudInstallJournal(journalPath, value) {
320
539
  fs.closeSync(fd);
321
540
  }
322
541
  fs.renameSync(temp, journalPath);
323
- cloudApplyPortableFileMode(journalPath, 0o600);
542
+ const written = fs.lstatSync(journalPath);
543
+ if (!written.isFile() || written.isSymbolicLink() || written.nlink !== 1) {
544
+ throw new Error("cloud install recovery journal write was unsafe");
545
+ }
324
546
  cloudFsyncDirectory(path.dirname(journalPath));
325
547
  }
326
548
 
@@ -334,34 +556,72 @@ function cloudFsyncDirectory(directory) {
334
556
  finally { if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best-effort */ } }
335
557
  }
336
558
 
337
- function rollbackCloudInstallSwap({ destination, staging, backup, movedExisting, installed }) {
338
- if (installed && fs.existsSync(destination)) fs.rmSync(destination, { recursive: true, force: true });
339
- if (movedExisting && fs.existsSync(backup) && !fs.existsSync(destination)) fs.renameSync(backup, destination);
340
- if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
559
+ function rollbackCloudInstallSwap({
560
+ destination,
561
+ staging,
562
+ backup,
563
+ movedExisting,
564
+ installed,
565
+ parentAnchor = null,
566
+ stagingAnchor = null,
567
+ backupAnchor = null,
568
+ destinationAnchor = null,
569
+ }) {
570
+ const safeParent = parentAnchor
571
+ ? cloudRefreshDirectoryAnchor(parentAnchor, "cloud install parent rollback")
572
+ : null;
573
+ if (installed) {
574
+ cloudRemoveManagedDirectory(destination, "cloud install destination", {
575
+ anchor: destinationAnchor,
576
+ containedBy: safeParent,
577
+ });
578
+ }
579
+ if (movedExisting && cloudManagedDirectoryState(backup, "cloud install backup")) {
580
+ cloudRenameManagedDirectory(backup, destination, "cloud install rollback", {
581
+ sourceAnchor: backupAnchor,
582
+ parentAnchor: safeParent,
583
+ });
584
+ }
585
+ cloudRemoveManagedDirectory(staging, "cloud install staging", {
586
+ anchor: stagingAnchor,
587
+ containedBy: safeParent,
588
+ });
341
589
  cloudFsyncDirectory(path.dirname(destination));
342
590
  }
343
591
 
344
592
  function recoverCloudInstallJournal(db, slug) {
345
- const destination = path.join(userDataDir(), "cloud-agent-installs", slug);
346
- const parent = path.dirname(destination);
347
- const journalPath = path.join(parent, `.${path.basename(destination)}.install-journal.json`);
593
+ const layout = cloudInstallLayout(slug);
594
+ if (!layout) return;
595
+ const { destination, parent, journalPath } = layout;
348
596
  if (!fs.existsSync(journalPath)) return;
349
597
  let journal;
350
- try { journal = JSON.parse(fs.readFileSync(journalPath, "utf8")); } catch { throw new Error(`cloud install recovery journal is unreadable for ${slug}`); }
598
+ try { journal = cloudReadInstallJournal(journalPath); } catch { throw new Error(`cloud install recovery journal is unreadable for ${slug}`); }
351
599
  const safeSibling = (candidate, prefix) =>
352
- typeof candidate === "string" && path.dirname(candidate) === parent && path.basename(candidate).startsWith(prefix);
600
+ typeof candidate === "string" && path.dirname(candidate) === parent &&
601
+ (path.basename(candidate).match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[0-9]+-[0-9]+-[a-f0-9]{8}|fixture|crash)$`)) !== null);
602
+ const expected = journal && journal.dbExpected;
603
+ const expectedEntries = expected && typeof expected === "object" && !Array.isArray(expected)
604
+ ? Object.entries(expected)
605
+ : [];
353
606
  if (
354
607
  journal.schemaVersion !== 1 || journal.slug !== slug || journal.destination !== destination ||
355
608
  !["prepared", "disk-swapped-db-pending", "db-committed"].includes(journal.phase) ||
356
609
  typeof journal.hadExisting !== "boolean" ||
357
610
  !safeSibling(journal.staging, `.${path.basename(destination)}.installing-`) ||
358
- !safeSibling(journal.backup, `.${path.basename(destination)}.backup-`)
611
+ !safeSibling(journal.backup, `.${path.basename(destination)}.backup-`) ||
612
+ expectedEntries.length === 0 || expectedEntries.length > 32 ||
613
+ expectedEntries.some(([key, value]) =>
614
+ !CLOUD_INSTALL_DB_EXPECTED_KEYS.has(key) ||
615
+ (!["string", "number", "boolean"].includes(typeof value) && value !== null) ||
616
+ (typeof value === "string" && value.length > 256 * 1024)
617
+ )
359
618
  ) {
360
619
  throw new Error(`cloud install recovery journal is invalid for ${slug}`);
361
620
  }
621
+ cloudManagedDirectoryState(destination, "cloud install destination");
622
+ cloudManagedDirectoryState(journal.staging, "cloud install staging");
623
+ cloudManagedDirectoryState(journal.backup, "cloud install backup");
362
624
  const row = db.prepare("SELECT * FROM installed_agents WHERE slug=?").get(slug);
363
- const expected = journal.dbExpected && typeof journal.dbExpected === "object" ? journal.dbExpected : {};
364
- const expectedEntries = Object.entries(expected);
365
625
  const dbMatches = Boolean(row) && expectedEntries.length > 0 && expectedEntries.every(
366
626
  ([key, value]) => String(row[key] ?? "") === String(value ?? ""),
367
627
  );
@@ -370,27 +630,29 @@ function recoverCloudInstallJournal(db, slug) {
370
630
  // prepared journal always represents the pre-DB state. Cover both rename
371
631
  // crash windows: old→backup and staging→destination.
372
632
  if (journal.hadExisting) {
373
- if (fs.existsSync(journal.backup)) {
374
- if (fs.existsSync(destination)) fs.rmSync(destination, { recursive: true, force: true });
375
- fs.renameSync(journal.backup, destination);
376
- } else if (!fs.existsSync(destination)) {
633
+ if (cloudManagedDirectoryState(journal.backup, "cloud install backup")) {
634
+ cloudRemoveManagedDirectory(destination, "cloud install destination");
635
+ cloudRenameManagedDirectory(journal.backup, destination, "cloud install prepared rollback");
636
+ } else if (!cloudManagedDirectoryState(destination, "cloud install destination")) {
377
637
  throw new Error(`prepared cloud install lost both destination and backup for ${slug}`);
378
638
  }
379
639
  } else {
380
- if (fs.existsSync(journal.backup)) {
640
+ if (cloudManagedDirectoryState(journal.backup, "cloud install backup")) {
381
641
  throw new Error(`prepared first cloud install has an unexpected backup for ${slug}`);
382
642
  }
383
- if (fs.existsSync(destination)) fs.rmSync(destination, { recursive: true, force: true });
643
+ cloudRemoveManagedDirectory(destination, "cloud install destination");
384
644
  }
385
- if (fs.existsSync(journal.staging)) fs.rmSync(journal.staging, { recursive: true, force: true });
386
- } else if (journal.phase === "db-committed" || dbMatches) {
387
- if (!fs.existsSync(destination) && fs.existsSync(journal.staging)) fs.renameSync(journal.staging, destination);
388
- if (!fs.existsSync(destination)) throw new Error(`committed cloud install is missing for ${slug}`);
389
- if (fs.existsSync(journal.backup)) fs.rmSync(journal.backup, { recursive: true, force: true });
390
- if (fs.existsSync(journal.staging)) fs.rmSync(journal.staging, { recursive: true, force: true });
391
- } else if (journal.phase === "disk-swapped-db-pending") {
392
- if (!fs.existsSync(destination)) throw new Error(`pending cloud install destination is missing for ${slug}`);
393
- if (journal.hadExisting !== fs.existsSync(journal.backup)) {
645
+ cloudRemoveManagedDirectory(journal.staging, "cloud install staging");
646
+ } else if (dbMatches) {
647
+ if (!cloudManagedDirectoryState(destination, "cloud install destination") && cloudManagedDirectoryState(journal.staging, "cloud install staging")) {
648
+ cloudRenameManagedDirectory(journal.staging, destination, "cloud install committed recovery");
649
+ }
650
+ if (!cloudManagedDirectoryState(destination, "cloud install destination")) throw new Error(`committed cloud install is missing for ${slug}`);
651
+ cloudRemoveManagedDirectory(journal.backup, "cloud install backup");
652
+ cloudRemoveManagedDirectory(journal.staging, "cloud install staging");
653
+ } else if (journal.phase === "disk-swapped-db-pending" || journal.phase === "db-committed") {
654
+ if (!cloudManagedDirectoryState(destination, "cloud install destination")) throw new Error(`pending cloud install destination is missing for ${slug}`);
655
+ if (journal.hadExisting !== Boolean(cloudManagedDirectoryState(journal.backup, "cloud install backup"))) {
394
656
  throw new Error(`pending cloud install backup state is invalid for ${slug}`);
395
657
  }
396
658
  rollbackCloudInstallSwap({
@@ -401,13 +663,14 @@ function recoverCloudInstallJournal(db, slug) {
401
663
  installed: true,
402
664
  });
403
665
  }
404
- fs.unlinkSync(journalPath);
666
+ cloudUnlinkInstallJournal(journalPath);
405
667
  cloudFsyncDirectory(parent);
406
668
  }
407
669
 
408
670
  function recoverCloudInstallJournals(db) {
409
- const parent = path.join(userDataDir(), "cloud-agent-installs");
410
- if (!fs.existsSync(parent)) return 0;
671
+ const layout = cloudInstallLayout("recovery-sweep");
672
+ if (!layout) return 0;
673
+ const { parent } = layout;
411
674
  let recovered = 0;
412
675
  for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
413
676
  if (!entry.name.endsWith(".install-journal.json")) continue;
@@ -503,21 +766,35 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
503
766
  if (assetDescriptor && assetDescriptor.slug !== slug) throw new Error("restore asset descriptor slug mismatch");
504
767
  const pathConflict = cloudPortablePathConflict(pkg.files.map((file) => file && file.path));
505
768
  if (pathConflict) throw new Error(pathConflict.message);
506
- const dir = path.join(userDataDir(), "cloud-agent-installs", slug);
507
- const parent = path.dirname(dir);
508
- fs.mkdirSync(parent, { recursive: true });
769
+ const layout = cloudInstallLayout(slug, { createParent: true });
770
+ const { destination: dir, parent, journalPath: journal } = layout;
771
+ let parentAnchor = cloudDirectoryAnchor(parent, "cloud install parent", { allowMissing: false });
509
772
  const nonce = `${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
510
773
  const staging = path.join(parent, `.${path.basename(dir)}.installing-${nonce}`);
511
774
  const backup = path.join(parent, `.${path.basename(dir)}.backup-${nonce}`);
512
- const journal = path.join(parent, `.${path.basename(dir)}.install-journal.json`);
775
+ const managedAnchors = new Map();
513
776
  const seen = new Set();
514
777
  const verifiedFiles = [];
515
778
  let verifiedTotalBytes = 0;
516
779
  let movedExisting = false;
517
780
  let installed = false;
781
+ let stagingAnchor = null;
782
+ let destinationAnchor = null;
783
+ let backupAnchor = null;
784
+ let installedDestinationAnchor = null;
518
785
  try {
786
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
519
787
  fs.mkdirSync(staging, { recursive: false, mode: 0o700 });
788
+ // Creating the staging directory legitimately changes the parent's nlink;
789
+ // refresh that field while retaining the original dev/ino/realpath anchor.
790
+ parentAnchor = cloudRefreshDirectoryAnchor(parentAnchor, "cloud install parent");
520
791
  cloudApplyPrivateDirectoryMode(staging);
792
+ stagingAnchor = cloudDirectoryAnchor(staging, "cloud install staging", {
793
+ allowMissing: false,
794
+ containedBy: parentAnchor,
795
+ });
796
+ managedAnchors.set(staging, stagingAnchor);
797
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
521
798
  for (const file of pkg.files) {
522
799
  const target = resolveCloudInstallPath(staging, file.path);
523
800
  const normalizedPath = path.relative(staging, target).split(path.sep).join("/");
@@ -547,11 +824,63 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
547
824
  });
548
825
  verifiedTotalBytes += bytes.length;
549
826
  if (verifiedTotalBytes > CLOUD_MAX_TOTAL_BYTES) throw new Error("cloud package exceeds total byte limit");
550
- fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
551
- cloudApplyPrivateDirectoryMode(path.dirname(target));
827
+ for (const anchor of cloudEnsurePrivateSubdirectory(staging, path.dirname(target))) {
828
+ managedAnchors.set(anchor.path, anchor);
829
+ }
830
+ stagingAnchor = managedAnchors.get(staging);
831
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
832
+ for (const anchor of managedAnchors.values()) {
833
+ cloudAssertDirectoryAnchor(anchor, "cloud install managed directory", stagingAnchor);
834
+ }
552
835
  const mode = packageHashVersion === CLOUD_PACKAGE_HASH_V2 && file.executable ? 0o700 : 0o600;
553
- fs.writeFileSync(target, bytes, { mode });
554
- cloudApplyPortableFileMode(target, mode);
836
+ let fileFd;
837
+ let fileWritten = false;
838
+ try {
839
+ fileFd = fs.openSync(
840
+ target,
841
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0),
842
+ mode,
843
+ );
844
+ // A parent swap can happen during open. Do not write until the
845
+ // directory anchors and the opened file identity still agree.
846
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
847
+ // O_CREAT may legitimately change a directory's nlink on some
848
+ // filesystems, so refresh that field after the open while retaining
849
+ // the dev/ino/realpath identity and containment checks.
850
+ stagingAnchor = cloudRefreshDirectoryAnchor(stagingAnchor, "cloud install staging", parentAnchor);
851
+ managedAnchors.set(staging, stagingAnchor);
852
+ for (const [anchorPath, anchor] of managedAnchors.entries()) {
853
+ if (anchorPath === staging) continue;
854
+ managedAnchors.set(anchorPath, cloudRefreshDirectoryAnchor(
855
+ anchor,
856
+ "cloud install managed directory",
857
+ stagingAnchor,
858
+ ));
859
+ }
860
+ const opened = fs.fstatSync(fileFd);
861
+ const listed = fs.lstatSync(target);
862
+ if (!cloudSameRegularFile(opened, listed) || opened.size !== 0) {
863
+ throw new Error(`cloud package target changed while opening: ${file.path}`);
864
+ }
865
+ fs.writeFileSync(fileFd, bytes);
866
+ if (process.platform !== "win32") fs.fchmodSync(fileFd, mode);
867
+ fs.fsyncSync(fileFd);
868
+ fileWritten = true;
869
+ } finally {
870
+ if (fileFd !== undefined) {
871
+ if (!fileWritten) {
872
+ // O_EXCL creates a zero-byte file before the final anchor check
873
+ // can reject a swapped parent. Remove it only when the path still
874
+ // names the exact descriptor we opened; never unlink a successor.
875
+ try {
876
+ const opened = fs.fstatSync(fileFd);
877
+ const listed = fs.lstatSync(target);
878
+ if (cloudSameRegularFile(opened, listed)) fs.unlinkSync(target);
879
+ } catch { /* outer rollback retains any unknown successor */ }
880
+ }
881
+ fs.closeSync(fileFd);
882
+ }
883
+ }
555
884
  }
556
885
  const expectedPackageHash = String(pkg.packageHash || "").toLowerCase().replace(/^sha256:/, "");
557
886
  if (!/^[a-f0-9]{64}$/.test(expectedPackageHash)) {
@@ -569,9 +898,35 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
569
898
  }
570
899
  if (verifiedTotalBytes !== pkg.totalBytes) throw new Error("cloud package total byte count does not match its files");
571
900
  const restoredAt = new Date().toISOString();
572
- fs.writeFileSync(
573
- path.join(staging, ".agentlas-cloud-package.json"),
574
- JSON.stringify({
901
+ const markerPath = path.join(staging, CLOUD_RESTORE_MARKER_PATH);
902
+ stagingAnchor = cloudDirectoryAnchor(staging, "cloud install staging", {
903
+ allowMissing: false,
904
+ containedBy: parentAnchor,
905
+ });
906
+ managedAnchors.set(staging, stagingAnchor);
907
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
908
+ for (const anchor of managedAnchors.values()) {
909
+ cloudAssertDirectoryAnchor(anchor, "cloud install managed directory", stagingAnchor);
910
+ }
911
+ let markerFd;
912
+ let markerWritten = false;
913
+ try {
914
+ markerFd = fs.openSync(
915
+ markerPath,
916
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0),
917
+ 0o600,
918
+ );
919
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
920
+ // As with package files, creating the marker can update the staging
921
+ // directory nlink; refresh it before writing and keep identity strict.
922
+ stagingAnchor = cloudRefreshDirectoryAnchor(stagingAnchor, "cloud install staging", parentAnchor);
923
+ managedAnchors.set(staging, stagingAnchor);
924
+ const opened = fs.fstatSync(markerFd);
925
+ const listed = fs.lstatSync(markerPath);
926
+ if (!cloudSameRegularFile(opened, listed) || opened.size !== 0) {
927
+ throw new Error("cloud restore marker changed while opening");
928
+ }
929
+ fs.writeFileSync(markerFd, JSON.stringify({
575
930
  schemaVersion: 1,
576
931
  source: "agentlas-cloud",
577
932
  slug,
@@ -591,10 +946,26 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
591
946
  cloudAssets: { [assetDescriptor.scope]: assetDescriptor },
592
947
  } : {}),
593
948
  restoredAt,
594
- }, null, 2) + "\n",
595
- { encoding: "utf8", mode: 0o600 },
596
- );
597
- cloudApplyPortableFileMode(path.join(staging, CLOUD_RESTORE_MARKER_PATH), 0o600);
949
+ }, null, 2) + "\n", "utf8");
950
+ if (process.platform !== "win32") fs.fchmodSync(markerFd, 0o600);
951
+ fs.fsyncSync(markerFd);
952
+ markerWritten = true;
953
+ } finally {
954
+ if (markerFd !== undefined) {
955
+ if (!markerWritten) {
956
+ try {
957
+ const opened = fs.fstatSync(markerFd);
958
+ const listed = fs.lstatSync(markerPath);
959
+ if (cloudSameRegularFile(opened, listed)) fs.unlinkSync(markerPath);
960
+ } catch { /* outer rollback retains any unknown successor */ }
961
+ }
962
+ fs.closeSync(markerFd);
963
+ }
964
+ }
965
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
966
+ for (const anchor of managedAnchors.values()) {
967
+ cloudAssertDirectoryAnchor(anchor, "cloud install managed directory", stagingAnchor);
968
+ }
598
969
  cloudVerifyRestoredSnapshot(staging, verifiedFiles, {
599
970
  slug,
600
971
  packageHash: expectedPackageHash,
@@ -604,6 +975,7 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
604
975
  });
605
976
 
606
977
  if (options.deferCommit) {
978
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
607
979
  writeCloudInstallJournal(journal, {
608
980
  schemaVersion: 1,
609
981
  slug,
@@ -611,18 +983,45 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
611
983
  destination: dir,
612
984
  staging,
613
985
  backup,
614
- hadExisting: fs.existsSync(dir),
986
+ hadExisting: Boolean(cloudManagedDirectoryState(dir, "cloud install destination")),
615
987
  dbExpected: options.dbExpected || {},
616
988
  });
989
+ // The recovery journal is a file in the managed parent and may update
990
+ // that directory's nlink; refresh the field before publication checks.
991
+ parentAnchor = cloudRefreshDirectoryAnchor(parentAnchor, "cloud install parent");
617
992
  }
618
993
 
619
994
  // A Cloud agent is an immutable asset snapshot. Replace the managed install
620
995
  // as a whole so removed files and local mutations cannot leak across versions.
621
- if (fs.existsSync(dir)) {
622
- fs.renameSync(dir, backup);
996
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
997
+ for (const anchor of managedAnchors.values()) {
998
+ cloudAssertDirectoryAnchor(anchor, "cloud install managed directory", stagingAnchor);
999
+ }
1000
+ if (cloudManagedDirectoryState(dir, "cloud install destination")) {
1001
+ destinationAnchor = cloudDirectoryAnchor(dir, "cloud install destination", {
1002
+ allowMissing: false,
1003
+ containedBy: parentAnchor,
1004
+ });
1005
+ cloudRenameManagedDirectory(dir, backup, "cloud install snapshot swap", {
1006
+ sourceAnchor: destinationAnchor,
1007
+ parentAnchor,
1008
+ });
623
1009
  movedExisting = true;
1010
+ backupAnchor = cloudDirectoryAnchor(backup, "cloud install backup", {
1011
+ allowMissing: false,
1012
+ containedBy: parentAnchor,
1013
+ });
624
1014
  }
625
- fs.renameSync(staging, dir);
1015
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
1016
+ cloudRenameManagedDirectory(staging, dir, "cloud install staging swap", {
1017
+ sourceAnchor: stagingAnchor,
1018
+ parentAnchor,
1019
+ });
1020
+ cloudAssertDirectoryAnchor(parentAnchor, "cloud install parent");
1021
+ installedDestinationAnchor = cloudDirectoryAnchor(dir, "cloud install destination", {
1022
+ allowMissing: false,
1023
+ containedBy: parentAnchor,
1024
+ });
626
1025
  cloudFsyncDirectory(parent);
627
1026
  installed = true;
628
1027
  if (options.deferCommit) {
@@ -638,12 +1037,34 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
638
1037
  });
639
1038
  }
640
1039
  } catch (error) {
641
- rollbackCloudInstallSwap({ destination: dir, staging, backup, movedExisting, installed });
642
- try { if (fs.existsSync(journal)) fs.unlinkSync(journal); } catch { /* best-effort */ }
1040
+ rollbackCloudInstallSwap({
1041
+ destination: dir,
1042
+ staging,
1043
+ backup,
1044
+ movedExisting,
1045
+ installed,
1046
+ parentAnchor,
1047
+ stagingAnchor,
1048
+ backupAnchor,
1049
+ destinationAnchor: installedDestinationAnchor,
1050
+ });
1051
+ try { if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal); } catch { /* best-effort */ }
643
1052
  throw error;
644
1053
  } finally {
645
- try { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); } catch { /* best-effort */ }
646
- try { if (!options.deferCommit && installed && fs.existsSync(backup)) fs.rmSync(backup, { recursive: true, force: true }); } catch { /* best-effort */ }
1054
+ try {
1055
+ cloudRemoveManagedDirectory(staging, "cloud install staging", {
1056
+ anchor: stagingAnchor,
1057
+ containedBy: parentAnchor,
1058
+ });
1059
+ } catch { /* best-effort */ }
1060
+ try {
1061
+ if (!options.deferCommit && installed) {
1062
+ cloudRemoveManagedDirectory(backup, "cloud install backup", {
1063
+ anchor: backupAnchor,
1064
+ containedBy: parentAnchor,
1065
+ });
1066
+ }
1067
+ } catch { /* best-effort */ }
647
1068
  }
648
1069
  if (!options.deferCommit) return dir;
649
1070
  let settled = false;
@@ -661,15 +1082,15 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
661
1082
  hadExisting: movedExisting,
662
1083
  dbExpected: options.dbExpected || {},
663
1084
  });
664
- if (fs.existsSync(backup)) fs.rmSync(backup, { recursive: true, force: true });
665
- if (fs.existsSync(journal)) fs.unlinkSync(journal);
1085
+ cloudRemoveManagedDirectory(backup, "cloud install backup");
1086
+ if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal);
666
1087
  cloudFsyncDirectory(parent);
667
1088
  settled = true;
668
1089
  },
669
1090
  rollback() {
670
1091
  if (settled) return;
671
1092
  rollbackCloudInstallSwap({ destination: dir, staging, backup, movedExisting, installed });
672
- if (fs.existsSync(journal)) fs.unlinkSync(journal);
1093
+ if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal);
673
1094
  cloudFsyncDirectory(parent);
674
1095
  settled = true;
675
1096
  },