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.
- package/CHANGELOG.md +94 -0
- package/README.md +12 -4
- package/bin/agentlas.cjs +354 -16
- package/engine/acp/server.cjs +34 -3
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +30 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +21 -4
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +96 -30
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +609 -116
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +72 -14
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +150 -37
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +35 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +484 -63
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +46 -10
- package/engine/mcp/plan.cjs +10 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +382 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
package/engine/hub/install.cjs
CHANGED
|
@@ -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
|
|
226
|
-
if (
|
|
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
|
|
233
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
346
|
-
|
|
347
|
-
const
|
|
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 =
|
|
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 &&
|
|
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 (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
} else if (!
|
|
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 (
|
|
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
|
-
|
|
643
|
+
cloudRemoveManagedDirectory(destination, "cloud install destination");
|
|
384
644
|
}
|
|
385
|
-
|
|
386
|
-
} else if (
|
|
387
|
-
if (!
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
666
|
+
cloudUnlinkInstallJournal(journalPath);
|
|
405
667
|
cloudFsyncDirectory(parent);
|
|
406
668
|
}
|
|
407
669
|
|
|
408
670
|
function recoverCloudInstallJournals(db) {
|
|
409
|
-
const
|
|
410
|
-
if (!
|
|
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
|
|
507
|
-
const parent =
|
|
508
|
-
|
|
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
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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:
|
|
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
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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({
|
|
642
|
-
|
|
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 {
|
|
646
|
-
|
|
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
|
-
|
|
665
|
-
if (fs.existsSync(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))
|
|
1093
|
+
if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal);
|
|
673
1094
|
cloudFsyncDirectory(parent);
|
|
674
1095
|
settled = true;
|
|
675
1096
|
},
|