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.
- package/CHANGELOG.md +87 -0
- package/README.md +8 -6
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- 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 +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- 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/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +315 -32
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +80 -28
- 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 +308 -113
- 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 +67 -34
- package/engine/commands/build.cjs +42 -12
- 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 +40 -12
- 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 +8 -2
- 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 +29 -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 +230 -61
- 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 +15 -5
- package/engine/mcp/plan.cjs +5 -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 +212 -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 +187 -24
- 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,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
|
|
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 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
|
-
|
|
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
|
-
|
|
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
|
|
339
|
-
if (movedExisting &&
|
|
340
|
-
|
|
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
|
|
346
|
-
|
|
347
|
-
const
|
|
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 =
|
|
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 &&
|
|
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 (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
} else if (!
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
if (
|
|
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
|
-
|
|
558
|
+
cloudUnlinkInstallJournal(journalPath);
|
|
405
559
|
cloudFsyncDirectory(parent);
|
|
406
560
|
}
|
|
407
561
|
|
|
408
562
|
function recoverCloudInstallJournals(db) {
|
|
409
|
-
const
|
|
410
|
-
if (!
|
|
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
|
|
507
|
-
const parent =
|
|
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
|
-
|
|
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.
|
|
554
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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:
|
|
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 (
|
|
622
|
-
|
|
790
|
+
if (cloudManagedDirectoryState(dir, "cloud install destination")) {
|
|
791
|
+
cloudRenameManagedDirectory(dir, backup, "cloud install snapshot swap");
|
|
623
792
|
movedExisting = true;
|
|
624
793
|
}
|
|
625
|
-
|
|
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))
|
|
811
|
+
try { if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal); } catch { /* best-effort */ }
|
|
643
812
|
throw error;
|
|
644
813
|
} finally {
|
|
645
|
-
try {
|
|
646
|
-
try { if (!options.deferCommit && installed
|
|
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
|
-
|
|
665
|
-
if (fs.existsSync(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))
|
|
841
|
+
if (fs.existsSync(journal)) cloudUnlinkInstallJournal(journal);
|
|
673
842
|
cloudFsyncDirectory(parent);
|
|
674
843
|
settled = true;
|
|
675
844
|
},
|