agentlas 1.0.64 → 1.0.66

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 (164) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +8 -6
  3. package/bin/agentlas.cjs +341 -16
  4. package/engine/acp/server.cjs +27 -1
  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 +29 -5
  30. package/engine/agentlas-workload-routing.cjs +83 -10
  31. package/engine/agentlas.cjs +10 -3
  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/bootstrap-schema.sql +4 -4
  43. package/engine/browser/cdp.cjs +315 -32
  44. package/engine/browser/vault.cjs +1 -0
  45. package/engine/cli-output.cjs +80 -28
  46. package/engine/cloud/auth.cjs +131 -13
  47. package/engine/cloud/hub-client.cjs +41 -16
  48. package/engine/cloud-assets/cas.cjs +18 -9
  49. package/engine/cloud-assets/commands.cjs +108 -30
  50. package/engine/cloud-assets/package.cjs +66 -3
  51. package/engine/cloud-assets/restore.cjs +12 -9
  52. package/engine/cloud-assets/state.cjs +308 -113
  53. package/engine/commands/acp.cjs +13 -2
  54. package/engine/commands/automation.cjs +75 -24
  55. package/engine/commands/billing.cjs +6 -1
  56. package/engine/commands/browser.cjs +67 -34
  57. package/engine/commands/build.cjs +42 -12
  58. package/engine/commands/call.cjs +1 -1
  59. package/engine/commands/cd.cjs +1 -1
  60. package/engine/commands/cloud.cjs +2 -1
  61. package/engine/commands/connect.cjs +38 -13
  62. package/engine/commands/creds.cjs +74 -29
  63. package/engine/commands/doctor.cjs +17 -1
  64. package/engine/commands/document.cjs +40 -12
  65. package/engine/commands/env.cjs +6 -1
  66. package/engine/commands/firm.cjs +42 -12
  67. package/engine/commands/graph.cjs +36 -32
  68. package/engine/commands/help.cjs +11 -1
  69. package/engine/commands/hep.cjs +1 -1
  70. package/engine/commands/import.cjs +5 -4
  71. package/engine/commands/index.cjs +1 -1
  72. package/engine/commands/install.cjs +6 -4
  73. package/engine/commands/list.cjs +56 -16
  74. package/engine/commands/login.cjs +5 -0
  75. package/engine/commands/logout.cjs +6 -1
  76. package/engine/commands/mcp.cjs +8 -2
  77. package/engine/commands/memory.cjs +3 -2
  78. package/engine/commands/multimodal.cjs +12 -0
  79. package/engine/commands/native.cjs +5 -1
  80. package/engine/commands/one.cjs +78 -33
  81. package/engine/commands/plugin.cjs +13 -2
  82. package/engine/commands/project.cjs +15 -12
  83. package/engine/commands/research.cjs +18 -7
  84. package/engine/commands/roles.cjs +104 -28
  85. package/engine/commands/route.cjs +2 -2
  86. package/engine/commands/run.cjs +42 -9
  87. package/engine/commands/search.cjs +23 -2
  88. package/engine/commands/setup.cjs +6 -1
  89. package/engine/commands/storm.cjs +44 -6
  90. package/engine/commands/swarm.cjs +47 -9
  91. package/engine/commands/telegram.cjs +6 -1
  92. package/engine/commands/uninstall.cjs +12 -6
  93. package/engine/commands/update.cjs +5 -0
  94. package/engine/commands/upload.cjs +2 -1
  95. package/engine/commands/usage.cjs +6 -1
  96. package/engine/commands/version.cjs +6 -1
  97. package/engine/commands/whoami.cjs +6 -1
  98. package/engine/commands/workforce.cjs +60 -16
  99. package/engine/core/capability-grants.cjs +7 -2
  100. package/engine/core/daemon-client.cjs +36 -7
  101. package/engine/core/db.cjs +10 -2
  102. package/engine/core/desktop-core-fetch.cjs +477 -77
  103. package/engine/core/schema-ensure.cjs +23 -11
  104. package/engine/experience/build.cjs +5 -1
  105. package/engine/experience/intents.cjs +29 -82
  106. package/engine/experience/runtime.cjs +7 -1
  107. package/engine/experience/variant.cjs +58 -4
  108. package/engine/firms/orchestrate.cjs +9 -6
  109. package/engine/graph/ask-model.cjs +22 -11
  110. package/engine/graph/interview.cjs +74 -23
  111. package/engine/graph/package.cjs +63 -8
  112. package/engine/hephaestus/local-core.cjs +18 -5
  113. package/engine/hephaestus/runtime.cjs +48 -43
  114. package/engine/hub/install.cjs +230 -61
  115. package/engine/hub/plugins.cjs +391 -29
  116. package/engine/mcp/consent.cjs +25 -17
  117. package/engine/mcp/contract.cjs +229 -24
  118. package/engine/mcp/inventory.cjs +15 -5
  119. package/engine/mcp/plan.cjs +5 -1
  120. package/engine/mcp/probe.cjs +24 -9
  121. package/engine/memory-cli/curate.cjs +74 -16
  122. package/engine/oberon/common.cjs +37 -12
  123. package/engine/oberon/manifest.cjs +25 -2
  124. package/engine/oberon/outputs.cjs +49 -19
  125. package/engine/oberon/render.cjs +665 -40
  126. package/engine/project/career-graph.cjs +15 -2
  127. package/engine/project/controller.cjs +133 -15
  128. package/engine/project/credentials.cjs +212 -34
  129. package/engine/project/env-file.cjs +93 -8
  130. package/engine/project/memory-context.cjs +11 -57
  131. package/engine/project/ontology.cjs +66 -25
  132. package/engine/project/paths.cjs +42 -8
  133. package/engine/project/seed.cjs +88 -51
  134. package/engine/project/state.cjs +41 -16
  135. package/engine/project/team.cjs +26 -1
  136. package/engine/runtime-refusal.cjs +4 -1
  137. package/engine/runtimes/acp-driver.cjs +56 -9
  138. package/engine/runtimes/auth-evidence.cjs +21 -2
  139. package/engine/runtimes/detect.cjs +28 -8
  140. package/engine/runtimes/kinds.cjs +12 -4
  141. package/engine/runtimes/overrides.cjs +4 -3
  142. package/engine/runtimes/resolve.cjs +8 -7
  143. package/engine/runtimes/roles.cjs +7 -5
  144. package/engine/sessions/apply-fences.cjs +396 -56
  145. package/engine/sessions/fences.cjs +111 -27
  146. package/engine/sessions/memory-turn.cjs +62 -3
  147. package/engine/sessions/orchestrator.cjs +11 -1
  148. package/engine/sessions/prompt.cjs +66 -9
  149. package/engine/sessions/session.cjs +70 -10
  150. package/engine/sessions/sink.cjs +78 -12
  151. package/engine/sessions/store.cjs +29 -7
  152. package/engine/storm/storm.cjs +26 -3
  153. package/engine/storm/swarm.cjs +31 -3
  154. package/engine/telegram/connect.cjs +187 -24
  155. package/engine/ui/commands-catalog.cjs +1 -1
  156. package/engine/ui/palette.cjs +12 -2
  157. package/engine/ui/repl.cjs +57 -9
  158. package/engine/ui/screens.cjs +2 -4
  159. package/engine/vendor/desktop-core.manifest.json +5 -5
  160. package/engine/workforce/capture.cjs +9 -27
  161. package/engine/workforce/deps.cjs +18 -6
  162. package/engine/workforce/local-core-transport.cjs +68 -5
  163. package/package.json +1 -1
  164. 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,48 @@ 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 cloudEnsurePrivateSubdirectory(root, directory) {
259
+ const relative = path.relative(root, directory);
260
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
261
+ throw new Error("cloud restore subdirectory escapes staging");
262
+ }
263
+ let current = root;
264
+ cloudManagedDirectoryState(root, "cloud restore staging", { allowMissing: false });
265
+ for (const part of relative.split(path.sep).filter(Boolean)) {
266
+ current = path.join(current, part);
267
+ try { fs.mkdirSync(current, { recursive: false, mode: 0o700 }); }
268
+ catch (error) { if (!error || error.code !== "EEXIST") throw error; }
269
+ cloudManagedDirectoryState(current, "cloud restore package directory", { allowMissing: false });
270
+ cloudApplyPrivateDirectoryMode(current);
271
+ }
234
272
  }
235
273
 
236
274
  // ── 스테이징 스냅샷 전수 검증 (심링크/특수 엔트리/모드/무결성) ──
@@ -309,10 +347,107 @@ function resolveCloudInstallPath(root, relPath) {
309
347
  }
310
348
 
311
349
  // ── 설치 저널 (crash-recovery 계약) ──
350
+ function cloudInstallLayout(slug, { createParent = false } = {}) {
351
+ if (typeof slug !== "string" || cloudSlug(slug) !== slug) {
352
+ throw new Error(`invalid cloud install slug: ${String(slug || "")}`);
353
+ }
354
+ const dataRoot = userDataDir();
355
+ if (createParent) fs.mkdirSync(dataRoot, { recursive: true, mode: 0o700 });
356
+ else if (!fs.existsSync(dataRoot)) return null;
357
+ if (!fs.statSync(dataRoot).isDirectory()) throw new Error("cloud install user data root is unsafe");
358
+ const realDataRoot = fs.realpathSync.native(dataRoot);
359
+ const parent = path.join(dataRoot, "cloud-agent-installs");
360
+ if (createParent) fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
361
+ else if (!fs.existsSync(parent)) return null;
362
+ const parentStat = fs.lstatSync(parent);
363
+ if (
364
+ !parentStat.isDirectory() || parentStat.isSymbolicLink() ||
365
+ fs.realpathSync.native(parent) !== path.join(realDataRoot, "cloud-agent-installs")
366
+ ) {
367
+ throw new Error("cloud install root is unsafe");
368
+ }
369
+ cloudApplyPrivateDirectoryMode(parent);
370
+ const destination = path.join(parent, slug);
371
+ return {
372
+ parent,
373
+ destination,
374
+ journalPath: path.join(parent, `.${slug}.install-journal.json`),
375
+ };
376
+ }
377
+
378
+ function cloudManagedDirectoryState(target, label, { allowMissing = true } = {}) {
379
+ let stat;
380
+ try { stat = fs.lstatSync(target); } catch (error) {
381
+ if (allowMissing && error && error.code === "ENOENT") return null;
382
+ throw error;
383
+ }
384
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
385
+ throw new Error(`${label} is not a safe managed directory`);
386
+ }
387
+ return stat;
388
+ }
389
+
390
+ function cloudRemoveManagedDirectory(target, label) {
391
+ if (!cloudManagedDirectoryState(target, label)) return false;
392
+ fs.rmSync(target, { recursive: true, force: false });
393
+ return true;
394
+ }
395
+
396
+ function cloudRenameManagedDirectory(source, destination, label) {
397
+ cloudManagedDirectoryState(source, `${label} source`, { allowMissing: false });
398
+ if (cloudManagedDirectoryState(destination, `${label} destination`)) {
399
+ throw new Error(`${label} destination already exists`);
400
+ }
401
+ fs.renameSync(source, destination);
402
+ }
403
+
404
+ function cloudReadInstallJournal(journalPath) {
405
+ let fd;
406
+ try {
407
+ fd = fs.openSync(journalPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
408
+ const listed = fs.lstatSync(journalPath);
409
+ const before = fs.fstatSync(fd);
410
+ if (
411
+ !listed.isFile() || listed.isSymbolicLink() || listed.nlink !== 1 ||
412
+ !before.isFile() || before.nlink !== 1 ||
413
+ listed.dev !== before.dev || listed.ino !== before.ino ||
414
+ before.size <= 0 || before.size > CLOUD_INSTALL_JOURNAL_MAX_BYTES
415
+ ) {
416
+ throw new Error("journal is not a bounded private file");
417
+ }
418
+ const raw = fs.readFileSync(fd, "utf8");
419
+ const after = fs.fstatSync(fd);
420
+ const current = fs.lstatSync(journalPath);
421
+ if (
422
+ Buffer.byteLength(raw, "utf8") !== before.size ||
423
+ after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size ||
424
+ after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs ||
425
+ current.dev !== before.dev || current.ino !== before.ino || current.nlink !== 1
426
+ ) {
427
+ throw new Error("journal changed while it was read");
428
+ }
429
+ return JSON.parse(raw);
430
+ } finally {
431
+ if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
432
+ }
433
+ }
434
+
435
+ function cloudUnlinkInstallJournal(journalPath) {
436
+ const stat = fs.lstatSync(journalPath);
437
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
438
+ throw new Error("cloud install recovery journal is unsafe");
439
+ }
440
+ fs.unlinkSync(journalPath);
441
+ }
442
+
312
443
  function writeCloudInstallJournal(journalPath, value) {
313
- fs.mkdirSync(path.dirname(journalPath), { recursive: true, mode: 0o700 });
444
+ const match = path.basename(journalPath).match(/^\.([a-z0-9][a-z0-9-]{0,63})\.install-journal\.json$/);
445
+ const layout = match ? cloudInstallLayout(match[1], { createParent: true }) : null;
446
+ if (!layout || path.resolve(journalPath) !== path.resolve(layout.journalPath)) {
447
+ throw new Error("cloud install recovery journal path is unsafe");
448
+ }
314
449
  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);
450
+ const fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0), 0o600);
316
451
  try {
317
452
  fs.writeFileSync(fd, JSON.stringify(value, null, 2) + "\n", "utf8");
318
453
  fs.fsyncSync(fd);
@@ -320,7 +455,10 @@ function writeCloudInstallJournal(journalPath, value) {
320
455
  fs.closeSync(fd);
321
456
  }
322
457
  fs.renameSync(temp, journalPath);
323
- cloudApplyPortableFileMode(journalPath, 0o600);
458
+ const written = fs.lstatSync(journalPath);
459
+ if (!written.isFile() || written.isSymbolicLink() || written.nlink !== 1) {
460
+ throw new Error("cloud install recovery journal write was unsafe");
461
+ }
324
462
  cloudFsyncDirectory(path.dirname(journalPath));
325
463
  }
326
464
 
@@ -335,33 +473,47 @@ function cloudFsyncDirectory(directory) {
335
473
  }
336
474
 
337
475
  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 });
476
+ if (installed) cloudRemoveManagedDirectory(destination, "cloud install destination");
477
+ if (movedExisting && cloudManagedDirectoryState(backup, "cloud install backup")) {
478
+ cloudRenameManagedDirectory(backup, destination, "cloud install rollback");
479
+ }
480
+ cloudRemoveManagedDirectory(staging, "cloud install staging");
341
481
  cloudFsyncDirectory(path.dirname(destination));
342
482
  }
343
483
 
344
484
  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`);
485
+ const layout = cloudInstallLayout(slug);
486
+ if (!layout) return;
487
+ const { destination, parent, journalPath } = layout;
348
488
  if (!fs.existsSync(journalPath)) return;
349
489
  let journal;
350
- try { journal = JSON.parse(fs.readFileSync(journalPath, "utf8")); } catch { throw new Error(`cloud install recovery journal is unreadable for ${slug}`); }
490
+ try { journal = cloudReadInstallJournal(journalPath); } catch { throw new Error(`cloud install recovery journal is unreadable for ${slug}`); }
351
491
  const safeSibling = (candidate, prefix) =>
352
- typeof candidate === "string" && path.dirname(candidate) === parent && path.basename(candidate).startsWith(prefix);
492
+ typeof candidate === "string" && path.dirname(candidate) === parent &&
493
+ (path.basename(candidate).match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[0-9]+-[0-9]+-[a-f0-9]{8}|fixture|crash)$`)) !== null);
494
+ const expected = journal && journal.dbExpected;
495
+ const expectedEntries = expected && typeof expected === "object" && !Array.isArray(expected)
496
+ ? Object.entries(expected)
497
+ : [];
353
498
  if (
354
499
  journal.schemaVersion !== 1 || journal.slug !== slug || journal.destination !== destination ||
355
500
  !["prepared", "disk-swapped-db-pending", "db-committed"].includes(journal.phase) ||
356
501
  typeof journal.hadExisting !== "boolean" ||
357
502
  !safeSibling(journal.staging, `.${path.basename(destination)}.installing-`) ||
358
- !safeSibling(journal.backup, `.${path.basename(destination)}.backup-`)
503
+ !safeSibling(journal.backup, `.${path.basename(destination)}.backup-`) ||
504
+ expectedEntries.length === 0 || expectedEntries.length > 32 ||
505
+ expectedEntries.some(([key, value]) =>
506
+ !CLOUD_INSTALL_DB_EXPECTED_KEYS.has(key) ||
507
+ (!["string", "number", "boolean"].includes(typeof value) && value !== null) ||
508
+ (typeof value === "string" && value.length > 256 * 1024)
509
+ )
359
510
  ) {
360
511
  throw new Error(`cloud install recovery journal is invalid for ${slug}`);
361
512
  }
513
+ cloudManagedDirectoryState(destination, "cloud install destination");
514
+ cloudManagedDirectoryState(journal.staging, "cloud install staging");
515
+ cloudManagedDirectoryState(journal.backup, "cloud install backup");
362
516
  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
517
  const dbMatches = Boolean(row) && expectedEntries.length > 0 && expectedEntries.every(
366
518
  ([key, value]) => String(row[key] ?? "") === String(value ?? ""),
367
519
  );
@@ -370,27 +522,29 @@ function recoverCloudInstallJournal(db, slug) {
370
522
  // prepared journal always represents the pre-DB state. Cover both rename
371
523
  // crash windows: old→backup and staging→destination.
372
524
  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)) {
525
+ if (cloudManagedDirectoryState(journal.backup, "cloud install backup")) {
526
+ cloudRemoveManagedDirectory(destination, "cloud install destination");
527
+ cloudRenameManagedDirectory(journal.backup, destination, "cloud install prepared rollback");
528
+ } else if (!cloudManagedDirectoryState(destination, "cloud install destination")) {
377
529
  throw new Error(`prepared cloud install lost both destination and backup for ${slug}`);
378
530
  }
379
531
  } else {
380
- if (fs.existsSync(journal.backup)) {
532
+ if (cloudManagedDirectoryState(journal.backup, "cloud install backup")) {
381
533
  throw new Error(`prepared first cloud install has an unexpected backup for ${slug}`);
382
534
  }
383
- if (fs.existsSync(destination)) fs.rmSync(destination, { recursive: true, force: true });
535
+ cloudRemoveManagedDirectory(destination, "cloud install destination");
536
+ }
537
+ cloudRemoveManagedDirectory(journal.staging, "cloud install staging");
538
+ } else if (dbMatches) {
539
+ if (!cloudManagedDirectoryState(destination, "cloud install destination") && cloudManagedDirectoryState(journal.staging, "cloud install staging")) {
540
+ cloudRenameManagedDirectory(journal.staging, destination, "cloud install committed recovery");
384
541
  }
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)) {
542
+ if (!cloudManagedDirectoryState(destination, "cloud install destination")) throw new Error(`committed cloud install is missing for ${slug}`);
543
+ cloudRemoveManagedDirectory(journal.backup, "cloud install backup");
544
+ cloudRemoveManagedDirectory(journal.staging, "cloud install staging");
545
+ } else if (journal.phase === "disk-swapped-db-pending" || journal.phase === "db-committed") {
546
+ if (!cloudManagedDirectoryState(destination, "cloud install destination")) throw new Error(`pending cloud install destination is missing for ${slug}`);
547
+ if (journal.hadExisting !== Boolean(cloudManagedDirectoryState(journal.backup, "cloud install backup"))) {
394
548
  throw new Error(`pending cloud install backup state is invalid for ${slug}`);
395
549
  }
396
550
  rollbackCloudInstallSwap({
@@ -401,13 +555,14 @@ function recoverCloudInstallJournal(db, slug) {
401
555
  installed: true,
402
556
  });
403
557
  }
404
- fs.unlinkSync(journalPath);
558
+ cloudUnlinkInstallJournal(journalPath);
405
559
  cloudFsyncDirectory(parent);
406
560
  }
407
561
 
408
562
  function recoverCloudInstallJournals(db) {
409
- const parent = path.join(userDataDir(), "cloud-agent-installs");
410
- if (!fs.existsSync(parent)) return 0;
563
+ const layout = cloudInstallLayout("recovery-sweep");
564
+ if (!layout) return 0;
565
+ const { parent } = layout;
411
566
  let recovered = 0;
412
567
  for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
413
568
  if (!entry.name.endsWith(".install-journal.json")) continue;
@@ -503,13 +658,11 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
503
658
  if (assetDescriptor && assetDescriptor.slug !== slug) throw new Error("restore asset descriptor slug mismatch");
504
659
  const pathConflict = cloudPortablePathConflict(pkg.files.map((file) => file && file.path));
505
660
  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 });
661
+ const layout = cloudInstallLayout(slug, { createParent: true });
662
+ const { destination: dir, parent, journalPath: journal } = layout;
509
663
  const nonce = `${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
510
664
  const staging = path.join(parent, `.${path.basename(dir)}.installing-${nonce}`);
511
665
  const backup = path.join(parent, `.${path.basename(dir)}.backup-${nonce}`);
512
- const journal = path.join(parent, `.${path.basename(dir)}.install-journal.json`);
513
666
  const seen = new Set();
514
667
  const verifiedFiles = [];
515
668
  let verifiedTotalBytes = 0;
@@ -547,11 +700,20 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
547
700
  });
548
701
  verifiedTotalBytes += bytes.length;
549
702
  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));
703
+ cloudEnsurePrivateSubdirectory(staging, path.dirname(target));
552
704
  const mode = packageHashVersion === CLOUD_PACKAGE_HASH_V2 && file.executable ? 0o700 : 0o600;
553
- fs.writeFileSync(target, bytes, { mode });
554
- cloudApplyPortableFileMode(target, mode);
705
+ const fileFd = fs.openSync(
706
+ target,
707
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0),
708
+ mode,
709
+ );
710
+ try {
711
+ fs.writeFileSync(fileFd, bytes);
712
+ if (process.platform !== "win32") fs.fchmodSync(fileFd, mode);
713
+ fs.fsyncSync(fileFd);
714
+ } finally {
715
+ fs.closeSync(fileFd);
716
+ }
555
717
  }
556
718
  const expectedPackageHash = String(pkg.packageHash || "").toLowerCase().replace(/^sha256:/, "");
557
719
  if (!/^[a-f0-9]{64}$/.test(expectedPackageHash)) {
@@ -569,9 +731,14 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
569
731
  }
570
732
  if (verifiedTotalBytes !== pkg.totalBytes) throw new Error("cloud package total byte count does not match its files");
571
733
  const restoredAt = new Date().toISOString();
572
- fs.writeFileSync(
573
- path.join(staging, ".agentlas-cloud-package.json"),
574
- JSON.stringify({
734
+ const markerPath = path.join(staging, CLOUD_RESTORE_MARKER_PATH);
735
+ const markerFd = fs.openSync(
736
+ markerPath,
737
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0),
738
+ 0o600,
739
+ );
740
+ try {
741
+ fs.writeFileSync(markerFd, JSON.stringify({
575
742
  schemaVersion: 1,
576
743
  source: "agentlas-cloud",
577
744
  slug,
@@ -591,10 +758,12 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
591
758
  cloudAssets: { [assetDescriptor.scope]: assetDescriptor },
592
759
  } : {}),
593
760
  restoredAt,
594
- }, null, 2) + "\n",
595
- { encoding: "utf8", mode: 0o600 },
596
- );
597
- cloudApplyPortableFileMode(path.join(staging, CLOUD_RESTORE_MARKER_PATH), 0o600);
761
+ }, null, 2) + "\n", "utf8");
762
+ if (process.platform !== "win32") fs.fchmodSync(markerFd, 0o600);
763
+ fs.fsyncSync(markerFd);
764
+ } finally {
765
+ fs.closeSync(markerFd);
766
+ }
598
767
  cloudVerifyRestoredSnapshot(staging, verifiedFiles, {
599
768
  slug,
600
769
  packageHash: expectedPackageHash,
@@ -611,18 +780,18 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
611
780
  destination: dir,
612
781
  staging,
613
782
  backup,
614
- hadExisting: fs.existsSync(dir),
783
+ hadExisting: Boolean(cloudManagedDirectoryState(dir, "cloud install destination")),
615
784
  dbExpected: options.dbExpected || {},
616
785
  });
617
786
  }
618
787
 
619
788
  // A Cloud agent is an immutable asset snapshot. Replace the managed install
620
789
  // as a whole so removed files and local mutations cannot leak across versions.
621
- if (fs.existsSync(dir)) {
622
- fs.renameSync(dir, backup);
790
+ if (cloudManagedDirectoryState(dir, "cloud install destination")) {
791
+ cloudRenameManagedDirectory(dir, backup, "cloud install snapshot swap");
623
792
  movedExisting = true;
624
793
  }
625
- fs.renameSync(staging, dir);
794
+ cloudRenameManagedDirectory(staging, dir, "cloud install staging swap");
626
795
  cloudFsyncDirectory(parent);
627
796
  installed = true;
628
797
  if (options.deferCommit) {
@@ -639,11 +808,11 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
639
808
  }
640
809
  } catch (error) {
641
810
  rollbackCloudInstallSwap({ destination: dir, staging, backup, movedExisting, installed });
642
- try { if (fs.existsSync(journal)) fs.unlinkSync(journal); } catch { /* best-effort */ }
811
+ try { if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal); } catch { /* best-effort */ }
643
812
  throw error;
644
813
  } 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 */ }
814
+ try { cloudRemoveManagedDirectory(staging, "cloud install staging"); } catch { /* best-effort */ }
815
+ try { if (!options.deferCommit && installed) cloudRemoveManagedDirectory(backup, "cloud install backup"); } catch { /* best-effort */ }
647
816
  }
648
817
  if (!options.deferCommit) return dir;
649
818
  let settled = false;
@@ -661,15 +830,15 @@ function materializeCloudListing(agentId, slug, listing, options = {}) {
661
830
  hadExisting: movedExisting,
662
831
  dbExpected: options.dbExpected || {},
663
832
  });
664
- if (fs.existsSync(backup)) fs.rmSync(backup, { recursive: true, force: true });
665
- if (fs.existsSync(journal)) fs.unlinkSync(journal);
833
+ cloudRemoveManagedDirectory(backup, "cloud install backup");
834
+ if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal);
666
835
  cloudFsyncDirectory(parent);
667
836
  settled = true;
668
837
  },
669
838
  rollback() {
670
839
  if (settled) return;
671
840
  rollbackCloudInstallSwap({ destination: dir, staging, backup, movedExisting, installed });
672
- if (fs.existsSync(journal)) fs.unlinkSync(journal);
841
+ if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal);
673
842
  cloudFsyncDirectory(parent);
674
843
  settled = true;
675
844
  },