@timurproko/a1 0.1.8-dev.299 → 0.1.8-dev.332
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/README.md +16 -4
- package/bin/guardian.js +4 -3
- package/bin/ui.js +4 -2
- package/bin/update-recovery.js +3 -1
- package/dist/cli/dispatch.js +1 -1
- package/dist/composition/owned-ui.js +2 -1
- package/dist/features/launch/profile-paths.d.ts +2 -0
- package/dist/features/launch/profile-paths.js +11 -0
- package/dist/features/owned-ui/project-trust-prompt.js +1 -1
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/service.d.ts +31 -1
- package/dist/features/prompt-history/service.js +294 -129
- package/dist/features/prompt-history/store.d.ts +2 -1
- package/dist/features/prompt-history/store.js +17 -4
- package/dist/features/prompt-history/worker.js +6 -3
- package/dist/foundation/launch-context/index.d.ts +59 -0
- package/dist/foundation/launch-context/index.js +136 -0
- package/dist/foundation/launch-guardian/main.js +6 -4
- package/dist/foundation/release/bootstrap.d.ts +6 -1
- package/dist/foundation/release/bootstrap.js +59 -13
- package/dist/foundation/release/cohort-state.d.ts +1 -0
- package/dist/foundation/release/cohort-state.js +1 -0
- package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
- package/dist/foundation/release/dependency-certification-retention.js +88 -0
- package/dist/foundation/release/dependency-certification.d.ts +24 -0
- package/dist/foundation/release/dependency-certification.js +208 -0
- package/dist/foundation/release/dependency-layer.d.ts +3 -4
- package/dist/foundation/release/dependency-layer.js +6 -37
- package/dist/foundation/release/release-gc.js +74 -46
- package/dist/foundation/release/release-store.d.ts +2 -0
- package/dist/foundation/release/release-store.js +11 -3
- package/dist/foundation/release/release.d.ts +3 -1
- package/dist/foundation/release/release.js +7 -3
- package/dist/foundation/release/restart-certification.d.ts +1 -0
- package/dist/foundation/release/restart-certification.js +17 -1
- package/dist/foundation/release/update-recovery.d.ts +2 -0
- package/dist/foundation/release/update-recovery.js +4 -0
- package/dist/foundation/release/update.js +1 -1
- package/dist/foundation/release/warmup.d.ts +1 -1
- package/dist/foundation/release/warmup.js +18 -6
- package/dist/foundation/startup/startup-runtime.js +13 -11
- package/dist/foundation/supervision/main.js +8 -7
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +105 -30
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
- package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
- package/dist/integrations/pi/engine/adapter.d.ts +33 -1
- package/dist/integrations/pi/engine/adapter.js +309 -112
- package/dist/integrations/pi/engine/changelog.d.ts +2 -0
- package/dist/integrations/pi/engine/changelog.js +63 -0
- package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
- package/dist/integrations/pi/engine/pending-delivery.js +149 -0
- package/dist/integrations/pi/engine/resources/changelog.json +12 -0
- package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
- package/dist/integrations/pi/engine/workflows.d.ts +1 -1
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
- package/dist/integrations/pi/session-ui/prompt-chips.js +74 -37
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +26 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +70 -75
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +67 -13
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -2
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +25 -20
- package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
- package/dist/integrations/pi/session-ui/text-paste.js +16 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +87 -43
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +2 -35
- package/dist/product-identity.json +1 -23
- package/dist/ui/components/transcript-viewport.d.ts +8 -0
- package/dist/ui/components/transcript-viewport.js +23 -2
- package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
- package/dist/ui/components/visible-hyperlinks.js +1 -0
- package/dist/ui/settings/declarations.js +1 -0
- package/dist/ui/settings/sections.js +11 -1
- package/docs/architecture/boundaries.md +2 -1
- package/docs/architecture/internal-naming.md +91 -0
- package/docs/architecture/resource-and-data-policy.md +9 -4
- package/docs/architecture/toolchain.md +15 -1
- package/docs/features/prompt-history.md +39 -11
- package/docs/manual-code-streaming-cleanup.md +88 -0
- package/package.json +8 -3
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ReadDependencyCertificationOptions } from "./dependency-certification.js";
|
|
2
|
+
export { dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath } from "./dependency-certification.js";
|
|
1
3
|
import { type ReleaseFileIdentity } from "./release.js";
|
|
2
4
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
3
5
|
export declare const DEPENDENCY_LAYER_SCHEMA: string;
|
|
@@ -37,9 +39,7 @@ export interface DependencyLayerOperationEvent {
|
|
|
37
39
|
readonly path: string;
|
|
38
40
|
readonly bytes: number;
|
|
39
41
|
}
|
|
40
|
-
export interface ReadCertifiedDependencyLayerOptions {
|
|
41
|
-
/** Accept and replace certification written by the immediately preceding updater format. */
|
|
42
|
-
readonly allowLegacyParentCertification?: boolean;
|
|
42
|
+
export interface ReadCertifiedDependencyLayerOptions extends ReadDependencyCertificationOptions {
|
|
43
43
|
}
|
|
44
44
|
export interface SelectedRuntimePayload {
|
|
45
45
|
readonly paths: readonly string[];
|
|
@@ -79,4 +79,3 @@ export declare function readCertifiedDependencyLayer(dataDir: string, layerId: s
|
|
|
79
79
|
/** Fully verify a layer when certification is absent or explicit tamper evidence is required. */
|
|
80
80
|
export declare function verifyDependencyLayer(dataDir: string, reference: DependencyLayerReference, onOperation?: (event: DependencyLayerOperationEvent) => void): Promise<Omit<MaterializedDependencyLayer, "reused">>;
|
|
81
81
|
export declare function dependencyReference(layer: MaterializedDependencyLayer): DependencyLayerReference;
|
|
82
|
-
export declare function dependencyLayerCertificationPath(dataDir: string, layerId: string): string;
|
|
@@ -2,7 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
5
|
-
import { assertImmutableFileMode
|
|
5
|
+
import { assertImmutableFileMode } from "./immutable-platform.js";
|
|
6
|
+
import { readDependencyCertification, writeDependencyCertification as writeLayerCertification } from "./dependency-certification.js";
|
|
7
|
+
export { dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath } from "./dependency-certification.js";
|
|
6
8
|
import { digestManifestFiles, releaseFileIdentity, resolveWithin } from "./release.js";
|
|
7
9
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
8
10
|
const LAYER_IO_CONCURRENCY = 32;
|
|
@@ -281,20 +283,9 @@ export async function readCertifiedDependencyLayer(dataDir, layerId, expected, o
|
|
|
281
283
|
if (expected && (expected.layerId !== manifest.layerId || expected.contentDigest !== manifest.contentDigest)) {
|
|
282
284
|
throw new Error(`dependency layer identity mismatch: ${layerId}`);
|
|
283
285
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const currentPlatformEvidence = certification.platform === process.platform && certification.platformPolicy === immutablePlatformPolicy();
|
|
288
|
-
// Compatibility: the updater that introduced layers certified these exact identities but did
|
|
289
|
-
// not record platform fields. Only an authenticated parent-started supervisor opts into this
|
|
290
|
-
// transition; durable/reuse readers remain strict and cannot treat the legacy marker as authority.
|
|
291
|
-
const legacyParentCertification = options.allowLegacyParentCertification === true
|
|
292
|
-
&& certification.platform === undefined && certification.platformPolicy === undefined;
|
|
293
|
-
if (!identityMatches || (!currentPlatformEvidence && !legacyParentCertification)) {
|
|
294
|
-
throw new Error(`dependency layer certification differs from manifest: ${layerId}`);
|
|
295
|
-
}
|
|
296
|
-
if (legacyParentCertification)
|
|
297
|
-
await writeLayerCertification(dataDir, manifest);
|
|
286
|
+
if (manifest.layerId !== layerId)
|
|
287
|
+
throw new Error(`dependency layer identity mismatch: ${layerId}`);
|
|
288
|
+
await readDependencyCertification(dataDir, manifest, options);
|
|
298
289
|
return { ...manifest, layerRoot };
|
|
299
290
|
}
|
|
300
291
|
/** Fully verify a layer when certification is absent or explicit tamper evidence is required. */
|
|
@@ -326,12 +317,6 @@ export async function verifyDependencyLayer(dataDir, reference, onOperation) {
|
|
|
326
317
|
export function dependencyReference(layer) {
|
|
327
318
|
return { layerId: layer.layerId, contentDigest: layer.contentDigest, binding: "node_modules" };
|
|
328
319
|
}
|
|
329
|
-
export function dependencyLayerCertificationPath(dataDir, layerId) {
|
|
330
|
-
return certificationPath(dataDir, layerId);
|
|
331
|
-
}
|
|
332
|
-
function certificationPath(dataDir, layerId) {
|
|
333
|
-
return resolve(dataDir, `dependency-layer-certification-${layerId}.json`);
|
|
334
|
-
}
|
|
335
320
|
async function reuseDependencyLayer(dataDir, identity) {
|
|
336
321
|
try {
|
|
337
322
|
return await readCertifiedDependencyLayer(dataDir, identity.layerId, identity);
|
|
@@ -343,22 +328,6 @@ async function reuseDependencyLayer(dataDir, identity) {
|
|
|
343
328
|
return verified;
|
|
344
329
|
}
|
|
345
330
|
}
|
|
346
|
-
async function writeLayerCertification(dataDir, identity) {
|
|
347
|
-
const path = certificationPath(dataDir, identity.layerId);
|
|
348
|
-
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
349
|
-
await writeFile(temporary, JSON.stringify({
|
|
350
|
-
schema: PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema,
|
|
351
|
-
layerId: identity.layerId,
|
|
352
|
-
contentDigest: identity.contentDigest,
|
|
353
|
-
platform: process.platform,
|
|
354
|
-
platformPolicy: immutablePlatformPolicy(),
|
|
355
|
-
certifiedAt: new Date().toISOString(),
|
|
356
|
-
}, null, 2), { flag: "wx", mode: 0o400 });
|
|
357
|
-
await chmod(path, 0o600).catch(() => { });
|
|
358
|
-
await rm(path, { force: true });
|
|
359
|
-
await rename(temporary, path);
|
|
360
|
-
await chmod(path, 0o400);
|
|
361
|
-
}
|
|
362
331
|
function validateLayerManifest(value) {
|
|
363
332
|
if (value.schema !== DEPENDENCY_LAYER_SCHEMA || !/^dependencies-[a-f0-9]{32}$/.test(value.layerId)
|
|
364
333
|
|| !/^[a-f0-9]{64}$/.test(value.contentDigest) || !Array.isArray(value.files) || value.files.length === 0
|
|
@@ -7,7 +7,9 @@ import { CohortStateStore, planProtectedReleases, } from "./cohort-state.js";
|
|
|
7
7
|
import { liveReleaseIds } from "./endpoints.js";
|
|
8
8
|
import { resolveProductPaths } from "../lifecycle/index.js";
|
|
9
9
|
import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
|
|
10
|
-
import { DEPENDENCY_LAYER_MANIFEST,
|
|
10
|
+
import { DEPENDENCY_LAYER_MANIFEST, readCertifiedDependencyLayer } from "./dependency-layer.js";
|
|
11
|
+
import { dependencyCertificationDirectory, managedDependencyCertificationPath } from "./dependency-certification.js";
|
|
12
|
+
import { dependencyCertificationProtection } from "./dependency-certification-retention.js";
|
|
11
13
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
12
14
|
import { cleanupUpdateRecoveryCapsules } from "./update-recovery.js";
|
|
13
15
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
@@ -17,8 +19,8 @@ const DEFAULT_LIMITS = { maxItems: 8, maxDurationMs: 2_000, concurrency: 1 };
|
|
|
17
19
|
const DEFAULT_WORKER_DURATION_MS = 10 * 60 * 1_000;
|
|
18
20
|
const DEFAULT_NO_PROGRESS_BATCHES = 5;
|
|
19
21
|
const DEFAULT_RETRY_DELAY_MS = 500;
|
|
20
|
-
const WORKER_HOLDS_ENV = "
|
|
21
|
-
const WORKER_RUN_ID_ENV = "
|
|
22
|
+
const WORKER_HOLDS_ENV = "RELEASE_CLEANUP_HOLDS";
|
|
23
|
+
const WORKER_RUN_ID_ENV = "RELEASE_CLEANUP_RUN_ID";
|
|
22
24
|
const WORKER_LEASE_FILENAME = "release-cleanup-worker.lock";
|
|
23
25
|
/**
|
|
24
26
|
* Reconcile current ownership under the cohort-state lock and durably detach obsolete releases.
|
|
@@ -483,13 +485,19 @@ async function removeAbandonedArtifact(dataDir, path, operations) {
|
|
|
483
485
|
throw new Error("abandoned artifact is outside managed candidate, trash, or certification paths");
|
|
484
486
|
}
|
|
485
487
|
async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, options) {
|
|
488
|
+
const transactionStore = options.transactionStore ?? new UpdateTransactionStore(dataDir);
|
|
489
|
+
if ((await transactionStore.read())?.status === "active")
|
|
490
|
+
return { attempted: 0, completed: 0 };
|
|
491
|
+
const protection = await dependencyCertificationProtection(dataDir, Object.keys((await store.read()).releases));
|
|
492
|
+
if (protection.uncertain)
|
|
493
|
+
return { attempted: 0, completed: 0 };
|
|
494
|
+
const { referenced } = protection;
|
|
486
495
|
const layersRoot = resolve(dataDir, "dependency-layers");
|
|
487
496
|
const layersMetadata = await lstat(layersRoot).catch(error => missingOrThrow(error));
|
|
488
497
|
if (layersMetadata === null)
|
|
489
|
-
return
|
|
498
|
+
return await collectDependencyCertificationRecords(store, dataDir, protection, maxItems, options);
|
|
490
499
|
if (!layersMetadata.isDirectory() || layersMetadata.isSymbolicLink())
|
|
491
500
|
throw new Error("dependency-layer store is not a managed directory");
|
|
492
|
-
const referenced = await referencedDependencyLayerIds(dataDir);
|
|
493
501
|
const trashRoot = resolve(layersRoot, ".trash");
|
|
494
502
|
await mkdir(trashRoot, { recursive: true, mode: 0o700 });
|
|
495
503
|
let attempted = 0;
|
|
@@ -514,7 +522,12 @@ async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, opt
|
|
|
514
522
|
for (const entry of await readdir(trashRoot, { withFileTypes: true })) {
|
|
515
523
|
candidates.push({ path: resolve(trashRoot, entry.name), layerId: entry.name.split("--", 1)[0] ?? null, inTrash: true });
|
|
516
524
|
}
|
|
517
|
-
|
|
525
|
+
const certificates = await collectDependencyCertificationRecords(store, dataDir, protection, Math.min(1, maxItems), options);
|
|
526
|
+
attempted += certificates.attempted;
|
|
527
|
+
completed += certificates.completed;
|
|
528
|
+
for (const candidate of candidates.slice(0, maxItems - attempted)) {
|
|
529
|
+
if ((await transactionStore.read())?.status === "active")
|
|
530
|
+
break;
|
|
518
531
|
if (candidate.layerId !== null && referenced.has(candidate.layerId))
|
|
519
532
|
continue;
|
|
520
533
|
attempted += 1;
|
|
@@ -541,7 +554,11 @@ async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, opt
|
|
|
541
554
|
}
|
|
542
555
|
await (options.operations?.remove ?? rm)(removalPath, { recursive: true, force: false, maxRetries: 0 });
|
|
543
556
|
if (candidate.layerId !== null && /^dependencies-[a-f0-9]{32}$/.test(candidate.layerId)) {
|
|
544
|
-
|
|
557
|
+
for (const legacy of [false, true]) {
|
|
558
|
+
const certificate = await managedDependencyCertificationPath(dataDir, candidate.layerId, legacy);
|
|
559
|
+
if (certificate !== null)
|
|
560
|
+
await (options.operations?.remove ?? rm)(certificate, { force: true });
|
|
561
|
+
}
|
|
545
562
|
}
|
|
546
563
|
completed += 1;
|
|
547
564
|
}
|
|
@@ -551,22 +568,58 @@ async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, opt
|
|
|
551
568
|
}
|
|
552
569
|
}
|
|
553
570
|
if (attempted < maxItems) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
571
|
+
const remaining = await collectDependencyCertificationRecords(store, dataDir, protection, maxItems - attempted, options);
|
|
572
|
+
attempted += remaining.attempted;
|
|
573
|
+
completed += remaining.completed;
|
|
574
|
+
}
|
|
575
|
+
return { attempted, completed };
|
|
576
|
+
}
|
|
577
|
+
async function collectDependencyCertificationRecords(store, dataDir, protection, maxItems, options) {
|
|
578
|
+
const candidates = new Map();
|
|
579
|
+
for (const entry of await readdir(dataDir, { withFileTypes: true })) {
|
|
580
|
+
const match = /^dependency-layer-certification-(dependencies-[a-f0-9]{32})\.json$/.exec(entry.name);
|
|
581
|
+
if (match && entry.isFile())
|
|
582
|
+
candidates.set(resolve(dataDir, entry.name), { layerId: match[1], legacy: true });
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const directory = await dependencyCertificationDirectory(dataDir);
|
|
586
|
+
if (directory !== null) {
|
|
587
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
588
|
+
const match = /^(dependencies-[a-f0-9]{32})\.json$/.exec(entry.name);
|
|
589
|
+
if (match && entry.isFile())
|
|
590
|
+
candidates.set(resolve(directory, entry.name), { layerId: match[1], legacy: false });
|
|
564
591
|
}
|
|
565
|
-
|
|
566
|
-
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
await store.recordCleanupFailure("dependency-certifications", "certification-discovery", error);
|
|
596
|
+
return { attempted: 0, completed: 0 };
|
|
597
|
+
}
|
|
598
|
+
const transactionStore = options.transactionStore ?? new UpdateTransactionStore(dataDir);
|
|
599
|
+
let attempted = 0;
|
|
600
|
+
let completed = 0;
|
|
601
|
+
for (const candidate of sortArtifacts([...candidates.keys()], await store.read())) {
|
|
602
|
+
if (attempted >= maxItems || (await transactionStore.read())?.status === "active")
|
|
603
|
+
break;
|
|
604
|
+
const { layerId, legacy } = candidates.get(candidate);
|
|
605
|
+
if (legacy ? protection.legacyRequired.has(layerId) : protection.referenced.has(layerId))
|
|
606
|
+
continue;
|
|
607
|
+
const layerExists = await lstat(resolve(dataDir, "dependency-layers", layerId)).catch(missingOrThrow) !== null;
|
|
608
|
+
if (!legacy && layerExists)
|
|
609
|
+
continue;
|
|
610
|
+
try {
|
|
611
|
+
if (legacy && (layerExists || protection.referenced.has(layerId))) {
|
|
612
|
+
// Security: a duplicate is disposable only with validated canonical evidence, never fallback.
|
|
613
|
+
await readCertifiedDependencyLayer(dataDir, layerId, undefined, { canonicalOnly: true });
|
|
567
614
|
}
|
|
568
|
-
|
|
569
|
-
|
|
615
|
+
attempted += 1;
|
|
616
|
+
const path = await managedDependencyCertificationPath(dataDir, layerId, legacy);
|
|
617
|
+
if (path !== null)
|
|
618
|
+
await (options.operations?.remove ?? rm)(path, { force: true });
|
|
619
|
+
completed += 1;
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
await store.recordCleanupFailure(artifactName(candidate), "dependency-layer-certification", error);
|
|
570
623
|
}
|
|
571
624
|
}
|
|
572
625
|
return { attempted, completed };
|
|
@@ -585,31 +638,6 @@ async function protectedCompileCachePaths(dataDir, state) {
|
|
|
585
638
|
}
|
|
586
639
|
return paths;
|
|
587
640
|
}
|
|
588
|
-
async function referencedDependencyLayerIds(dataDir) {
|
|
589
|
-
const referenced = new Set();
|
|
590
|
-
const releasesRoot = resolve(dataDir, "releases");
|
|
591
|
-
const roots = [];
|
|
592
|
-
for (const entry of await readdir(releasesRoot, { withFileTypes: true }).catch(() => [])) {
|
|
593
|
-
if (entry.name === ".trash") {
|
|
594
|
-
for (const trash of await readdir(resolve(releasesRoot, entry.name), { withFileTypes: true }).catch(() => [])) {
|
|
595
|
-
if (trash.isDirectory())
|
|
596
|
-
roots.push(resolve(releasesRoot, entry.name, trash.name));
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
else if (entry.isDirectory() && !entry.name.startsWith(".candidate-"))
|
|
600
|
-
roots.push(resolve(releasesRoot, entry.name));
|
|
601
|
-
}
|
|
602
|
-
for (const root of roots) {
|
|
603
|
-
try {
|
|
604
|
-
const manifest = JSON.parse(await readFile(resolve(root, RELEASE_MANIFEST_FILENAME), "utf8"));
|
|
605
|
-
for (const layer of manifest.dependencyLayers ?? [])
|
|
606
|
-
if (typeof layer.layerId === "string")
|
|
607
|
-
referenced.add(layer.layerId);
|
|
608
|
-
}
|
|
609
|
-
catch { }
|
|
610
|
-
}
|
|
611
|
-
return referenced;
|
|
612
|
-
}
|
|
613
641
|
function assertDirectChild(parent, child) {
|
|
614
642
|
const fromParent = relative(parent, child);
|
|
615
643
|
if (fromParent.length === 0 || fromParent === ".." || fromParent.startsWith(`..${sep}`) || isAbsolute(fromParent) || fromParent.includes(sep)) {
|
|
@@ -28,6 +28,8 @@ export interface CertifiedReleaseRecord {
|
|
|
28
28
|
readonly releaseRoot: string;
|
|
29
29
|
readonly packageVersion?: string;
|
|
30
30
|
readonly contentDigest: string;
|
|
31
|
+
/** Absent on records written before releases declared a handoff contract. */
|
|
32
|
+
readonly launchContract?: string;
|
|
31
33
|
}
|
|
32
34
|
/** Process-authority compatibility controls for metadata-only release loading. */
|
|
33
35
|
export type ReadCertifiedReleaseManifestOptions = ReadCertifiedDependencyLayerOptions;
|
|
@@ -3,6 +3,7 @@ import { chmod, lstat, mkdir, readFile, realpath, rename, rm, symlink, writeFile
|
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { PRODUCT_PACKAGE_NAME, createReleaseIdentity, discoverReleasePayload, releaseFileIdentity, resolveWithin, } from "./release.js";
|
|
5
5
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
|
+
import { assertCurrentLaunchContract, isCurrentLaunchContract, readLaunchContext } from "../launch-context/index.js";
|
|
6
7
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
7
8
|
import { assertImmutableFileMode } from "./immutable-platform.js";
|
|
8
9
|
import { dependencyReference, materializeDependencyLayer, readCertifiedDependencyLayer, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
|
|
@@ -13,6 +14,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
|
13
14
|
const payload = await discoverReleasePayload(packageRoot, {
|
|
14
15
|
onSourceRead: (path, bytes) => options.onOperation?.({ operation: "source-read", path, bytes }),
|
|
15
16
|
});
|
|
17
|
+
assertCurrentLaunchContract(payload);
|
|
16
18
|
const storeRoot = resolve(dataDir, "releases");
|
|
17
19
|
await mkdir(storeRoot, { recursive: true, mode: 0o700 });
|
|
18
20
|
const dependencyPaths = payload.paths.filter(path => path.startsWith("node_modules/"));
|
|
@@ -60,7 +62,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
|
60
62
|
options.onOperation?.({ operation: "candidate-write", path, bytes: bytes.length });
|
|
61
63
|
return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
|
|
62
64
|
});
|
|
63
|
-
const identity = createReleaseIdentity(payload.packageRoot, payload.packageVersion, files, layerReferences);
|
|
65
|
+
const identity = createReleaseIdentity(payload.packageRoot, payload.packageVersion, files, layerReferences, payload.launchContract);
|
|
64
66
|
if (layer !== null) {
|
|
65
67
|
const binding = resolveWithin(candidate, "node_modules");
|
|
66
68
|
const target = resolveWithin(layer.layerRoot, "node_modules");
|
|
@@ -137,7 +139,7 @@ export async function verifyMaterializedRelease(releaseRoot, expected, selectedS
|
|
|
137
139
|
await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
|
|
138
140
|
await verifyFile(canonical, file, options);
|
|
139
141
|
});
|
|
140
|
-
const recomputed = createReleaseIdentity(manifest.packageRoot, manifest.packageVersion, manifest.files, manifest.dependencyLayers ?? []).contentDigest;
|
|
142
|
+
const recomputed = createReleaseIdentity(manifest.packageRoot, manifest.packageVersion, manifest.files, manifest.dependencyLayers ?? [], manifest.launchContract).contentDigest;
|
|
141
143
|
if (recomputed !== manifest.contentDigest)
|
|
142
144
|
throw new Error(`release content digest mismatch for ${manifest.releaseId}`);
|
|
143
145
|
await verifyReleaseDependencies(canonical, canonicalStoreRoot, manifest.dependencyLayers ?? [], true, options);
|
|
@@ -146,7 +148,7 @@ export async function verifyMaterializedRelease(releaseRoot, expected, selectedS
|
|
|
146
148
|
export async function assertImmutableExecutionRoot(release, dataDir) {
|
|
147
149
|
const storeRoot = await realpath(resolve(dataDir, "releases"));
|
|
148
150
|
assertContained(storeRoot, release.releaseRoot, "release root is outside the selected release store");
|
|
149
|
-
const selectedRoot = process.env
|
|
151
|
+
const selectedRoot = readLaunchContext(process.env, "release").releaseRoot;
|
|
150
152
|
if (!selectedRoot)
|
|
151
153
|
throw new Error(PRODUCT_TEXT.diagnostic("persistent process has no immutable release root"));
|
|
152
154
|
const selected = await realpath(selectedRoot);
|
|
@@ -200,6 +202,9 @@ function certificationReadyRelease(release) {
|
|
|
200
202
|
return release;
|
|
201
203
|
}
|
|
202
204
|
function validateManifest(value) {
|
|
205
|
+
if (value.launchContract !== undefined && !isCurrentLaunchContract(value)) {
|
|
206
|
+
throw new Error(PRODUCT_TEXT.diagnostic(`release manifest declares an unsupported launch contract: ${String(value.launchContract)}`));
|
|
207
|
+
}
|
|
203
208
|
if (value.packageName !== PRODUCT_PACKAGE_NAME || typeof value.packageVersion !== "string")
|
|
204
209
|
throw new Error(PRODUCT_TEXT.diagnostic("release manifest metadata is invalid"));
|
|
205
210
|
if (!/^[a-f0-9]{64}$/.test(value.contentDigest) || !/^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/.test(value.releaseId))
|
|
@@ -218,6 +223,9 @@ function validateManifest(value) {
|
|
|
218
223
|
if (!Number.isSafeInteger(file.bytes) || file.bytes < 0 || !/^[a-f0-9]{64}$/.test(file.sha256))
|
|
219
224
|
throw new Error(`invalid release manifest file identity: ${file.path}`);
|
|
220
225
|
}
|
|
226
|
+
const expected = createReleaseIdentity(value.packageRoot, value.packageVersion, value.files, value.dependencyLayers ?? [], value.launchContract);
|
|
227
|
+
if (expected.contentDigest !== value.contentDigest)
|
|
228
|
+
throw new Error("release manifest content/contract digest mismatch");
|
|
221
229
|
}
|
|
222
230
|
function assertContained(parent, child, message) {
|
|
223
231
|
const fromParent = relative(parent, child);
|
|
@@ -12,6 +12,7 @@ export interface ReleaseIdentity {
|
|
|
12
12
|
readonly contentDigest: string;
|
|
13
13
|
readonly releaseId: string;
|
|
14
14
|
readonly packageRoot: string;
|
|
15
|
+
readonly launchContract?: string;
|
|
15
16
|
readonly files: readonly ReleaseFileIdentity[];
|
|
16
17
|
/** Absent for legacy full-copy releases. */
|
|
17
18
|
readonly dependencyLayers?: readonly DependencyLayerReference[];
|
|
@@ -20,6 +21,7 @@ export interface DiscoveredReleasePayload {
|
|
|
20
21
|
readonly packageRoot: string;
|
|
21
22
|
readonly packageVersion: string;
|
|
22
23
|
readonly paths: readonly string[];
|
|
24
|
+
readonly launchContract?: string;
|
|
23
25
|
/** Package manifests already read to discover the dependency closure. */
|
|
24
26
|
readonly cachedFiles: ReadonlyMap<string, Buffer>;
|
|
25
27
|
}
|
|
@@ -34,7 +36,7 @@ export declare function discoverReleasePayload(packageRoot: string, options?: Di
|
|
|
34
36
|
* content.
|
|
35
37
|
*/
|
|
36
38
|
export declare function deriveReleaseIdentity(packageRoot: string): Promise<ReleaseIdentity>;
|
|
37
|
-
export declare function createReleaseIdentity(packageRoot: string, packageVersion: string, files: readonly ReleaseFileIdentity[], dependencyLayers?: readonly DependencyLayerReference[]): ReleaseIdentity;
|
|
39
|
+
export declare function createReleaseIdentity(packageRoot: string, packageVersion: string, files: readonly ReleaseFileIdentity[], dependencyLayers?: readonly DependencyLayerReference[], launchContract?: string): ReleaseIdentity;
|
|
38
40
|
export declare function releaseFileIdentity(path: string, bytes: Uint8Array, executable: boolean): ReleaseFileIdentity;
|
|
39
41
|
export declare function digestManifestFiles(files: readonly ReleaseFileIdentity[]): string;
|
|
40
42
|
export declare function resolveWithin(root: string, candidate: string): string;
|
|
@@ -31,6 +31,7 @@ export async function discoverReleasePayload(packageRoot, options = {}) {
|
|
|
31
31
|
packageRoot: canonicalRoot,
|
|
32
32
|
packageVersion: manifest.version,
|
|
33
33
|
paths: [...paths].sort(),
|
|
34
|
+
...(typeof manifest.privateLaunchContract === "string" ? { launchContract: manifest.privateLaunchContract } : {}),
|
|
34
35
|
cachedFiles,
|
|
35
36
|
};
|
|
36
37
|
}
|
|
@@ -49,10 +50,12 @@ export async function deriveReleaseIdentity(packageRoot) {
|
|
|
49
50
|
const bytes = payload.cachedFiles.get(path) ?? await readFile(absolute);
|
|
50
51
|
return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
|
|
51
52
|
});
|
|
52
|
-
return createReleaseIdentity(payload.packageRoot, payload.packageVersion, files);
|
|
53
|
+
return createReleaseIdentity(payload.packageRoot, payload.packageVersion, files, [], payload.launchContract);
|
|
53
54
|
}
|
|
54
|
-
export function createReleaseIdentity(packageRoot, packageVersion, files, dependencyLayers = []) {
|
|
55
|
-
const
|
|
55
|
+
export function createReleaseIdentity(packageRoot, packageVersion, files, dependencyLayers = [], launchContract) {
|
|
56
|
+
const fileDigest = digestManifestFiles(files);
|
|
57
|
+
const productDigest = launchContract === undefined ? fileDigest
|
|
58
|
+
: createHash("sha256").update(`launch-contract\0${launchContract}\0${fileDigest}`).digest("hex");
|
|
56
59
|
const contentDigest = dependencyLayers.length === 0
|
|
57
60
|
? productDigest
|
|
58
61
|
: createHash("sha256")
|
|
@@ -65,6 +68,7 @@ export function createReleaseIdentity(packageRoot, packageVersion, files, depend
|
|
|
65
68
|
contentDigest,
|
|
66
69
|
releaseId: `${packageVersion}-${contentDigest.slice(0, 20)}`,
|
|
67
70
|
packageRoot,
|
|
71
|
+
...(launchContract === undefined ? {} : { launchContract }),
|
|
68
72
|
files: [...files].sort(compareReleaseFiles),
|
|
69
73
|
...(dependencyLayers.length === 0 ? {} : { dependencyLayers: [...dependencyLayers] }),
|
|
70
74
|
};
|
|
@@ -25,6 +25,7 @@ interface LayerRestartEvidence extends DependencyLayerReference {
|
|
|
25
25
|
}
|
|
26
26
|
interface RestartSealCore {
|
|
27
27
|
readonly version: typeof RESTART_SEAL_VERSION;
|
|
28
|
+
readonly launchContract: string;
|
|
28
29
|
readonly platform: NodeJS.Platform;
|
|
29
30
|
readonly platformPolicy: ImmutablePlatformPolicy;
|
|
30
31
|
readonly releaseId: string;
|
|
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
|
-
import {
|
|
5
|
+
import { PRIVATE_LAUNCH_CONTRACT, launchContractTarget } from "../launch-context/index.js";
|
|
6
|
+
import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath, readCertifiedDependencyLayer } from "./dependency-layer.js";
|
|
7
|
+
import { managedDependencyCertificationPath } from "./dependency-certification.js";
|
|
6
8
|
import { immutablePlatformPolicy } from "./immutable-platform.js";
|
|
7
9
|
import { PRODUCT_PACKAGE_NAME } from "./release.js";
|
|
8
10
|
import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
|
|
@@ -27,6 +29,7 @@ export async function createRestartSeal(release, dataDir) {
|
|
|
27
29
|
const dependencyLayers = await Promise.all((release.dependencyLayers ?? []).map(async (reference) => {
|
|
28
30
|
if (layersRoot === null)
|
|
29
31
|
throw new Error("restart certification has no dependency-layer root");
|
|
32
|
+
await readCertifiedDependencyLayer(canonicalData, reference.layerId, reference);
|
|
30
33
|
const layerRoot = await realpath(resolve(layersRoot, reference.layerId));
|
|
31
34
|
assertDirectChild(layersRoot, layerRoot, reference.layerId, "dependency layer");
|
|
32
35
|
const bindingPath = resolve(releaseRoot, reference.binding);
|
|
@@ -45,6 +48,7 @@ export async function createRestartSeal(release, dataDir) {
|
|
|
45
48
|
}));
|
|
46
49
|
const core = {
|
|
47
50
|
version: RESTART_SEAL_VERSION,
|
|
51
|
+
launchContract: launchContractTarget(release),
|
|
48
52
|
platform: process.platform,
|
|
49
53
|
platformPolicy: policy,
|
|
50
54
|
releaseId: release.releaseId,
|
|
@@ -81,6 +85,11 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
|
|
|
81
85
|
throw new Error("restart certification differs from the approved release record");
|
|
82
86
|
}
|
|
83
87
|
const seal = normalizeRestartSeal(certification.restartSeal);
|
|
88
|
+
// Invariant: a seal is restart authority only for a handoff this build can still
|
|
89
|
+
// perform, which is the contract it emits or the pre-cutover one it retains.
|
|
90
|
+
if (seal.launchContract !== PRIVATE_LAUNCH_CONTRACT && seal.launchContract !== "superseded") {
|
|
91
|
+
throw new Error(`restart seal declares an unsupported launch contract: ${String(seal.launchContract)}`);
|
|
92
|
+
}
|
|
84
93
|
if (seal.platform !== process.platform || seal.platformPolicy !== immutablePlatformPolicy()) {
|
|
85
94
|
throw new Error("restart certification platform immutability evidence is unsupported");
|
|
86
95
|
}
|
|
@@ -119,6 +128,12 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
|
|
|
119
128
|
assertDirectChild(layersRoot, layerRoot, layer.layerId, "dependency layer");
|
|
120
129
|
await assertPathEvidence(layer.layerRoot, onEvent);
|
|
121
130
|
await assertPathEvidence(layer.manifest, onEvent);
|
|
131
|
+
const legacyPath = legacyDependencyLayerCertificationPath(canonicalData, layer.layerId);
|
|
132
|
+
const canonicalPath = dependencyLayerCertificationPath(canonicalData, layer.layerId);
|
|
133
|
+
if (layer.certification.path !== legacyPath && layer.certification.path !== canonicalPath) {
|
|
134
|
+
throw new Error("restart dependency certification path is outside managed storage");
|
|
135
|
+
}
|
|
136
|
+
await managedDependencyCertificationPath(canonicalData, layer.layerId, layer.certification.path === legacyPath);
|
|
122
137
|
await assertPathEvidence(layer.certification, onEvent);
|
|
123
138
|
await assertPathEvidence(layer.bindingEvidence, onEvent, true);
|
|
124
139
|
const bindingTarget = await observedRealpath(layer.bindingEvidence.path, onEvent);
|
|
@@ -129,6 +144,7 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
|
|
|
129
144
|
}
|
|
130
145
|
return {
|
|
131
146
|
packageName: PRODUCT_PACKAGE_NAME,
|
|
147
|
+
launchContract: seal.launchContract,
|
|
132
148
|
packageVersion: seal.packageVersion,
|
|
133
149
|
contentDigest: seal.contentDigest,
|
|
134
150
|
releaseId: seal.releaseId,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { PRIVATE_LAUNCH_CONTRACT } from "../launch-context/index.js";
|
|
1
2
|
import type { UpdateTransaction, UpdateRecoveryState } from "./update-transaction.js";
|
|
2
3
|
export declare const UPDATE_RECOVERY_SCHEMA: "a1-update-recovery-v1";
|
|
3
4
|
export interface UpdateRecoveryCapsule {
|
|
4
5
|
readonly schema: typeof UPDATE_RECOVERY_SCHEMA;
|
|
6
|
+
readonly launchContract: typeof PRIVATE_LAUNCH_CONTRACT;
|
|
5
7
|
readonly transactionId: string;
|
|
6
8
|
readonly packageName: string;
|
|
7
9
|
readonly targetVersion: string;
|
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
7
7
|
import { processIsAlive } from "./process-cleanup.js";
|
|
8
|
+
import { PRIVATE_LAUNCH_CONTRACT, assertCurrentLaunchContract } from "../launch-context/index.js";
|
|
8
9
|
export const UPDATE_RECOVERY_SCHEMA = "a1-update-recovery-v1";
|
|
9
10
|
const WORKER_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
10
11
|
const POLL_MS = 50;
|
|
@@ -18,6 +19,7 @@ export function updateLauncherPaths(globalRoot, platform = process.platform) {
|
|
|
18
19
|
/** Validate transaction-scoped recovery authority without trusting npm temporary names. */
|
|
19
20
|
export async function readUpdateRecoveryCapsule(manifestPath, platform = process.platform) {
|
|
20
21
|
const capsule = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
22
|
+
assertCurrentLaunchContract(capsule);
|
|
21
23
|
if (capsule.schema !== UPDATE_RECOVERY_SCHEMA || !/^[0-9a-f-]{36}$/i.test(capsule.transactionId) || typeof capsule.targetVersion !== "string"
|
|
22
24
|
|| capsule.packageName !== PRODUCT_TEXT.packageName || typeof capsule.packageRoot !== "string" || typeof capsule.globalRoot !== "string"
|
|
23
25
|
|| typeof capsule.launcherRoot !== "string" || !Array.isArray(capsule.launchers) || typeof capsule.priorReleaseId !== "string"
|
|
@@ -59,6 +61,7 @@ export async function readUpdateRecoveryCapsule(manifestPath, platform = process
|
|
|
59
61
|
if (priorReleaseRoot.split(sep).at(-1) !== capsule.priorReleaseId)
|
|
60
62
|
throw new Error("recovery prior release identity is invalid");
|
|
61
63
|
const priorManifest = JSON.parse(await readFile(resolve(priorReleaseRoot, capsule.releaseManifestName), "utf8"));
|
|
64
|
+
assertCurrentLaunchContract(priorManifest);
|
|
62
65
|
if (priorManifest.releaseId !== capsule.priorReleaseId || priorManifest.contentDigest !== capsule.priorContentDigest) {
|
|
63
66
|
throw new Error("recovery prior release manifest differs from the capsule");
|
|
64
67
|
}
|
|
@@ -95,6 +98,7 @@ export async function prepareUpdateRecoveryCapsule(options) {
|
|
|
95
98
|
const npmCli = await resolveNpmCli(canonicalGlobal, options.environment ?? process.env);
|
|
96
99
|
const capsule = {
|
|
97
100
|
schema: UPDATE_RECOVERY_SCHEMA,
|
|
101
|
+
launchContract: PRIVATE_LAUNCH_CONTRACT,
|
|
98
102
|
transactionId: options.transaction.transactionId,
|
|
99
103
|
packageName: PRODUCT_TEXT.packageName,
|
|
100
104
|
targetVersion: options.transaction.targetVersion,
|
|
@@ -448,7 +448,7 @@ export async function runSelfUpdate(options) {
|
|
|
448
448
|
transaction = await transactionStore.advance("package-installed");
|
|
449
449
|
if (replacement.cancelled) {
|
|
450
450
|
progress.clear();
|
|
451
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the
|
|
451
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the launcher is available and the transaction can be resumed.")}\n`);
|
|
452
452
|
return 130;
|
|
453
453
|
}
|
|
454
454
|
if (replacement.outcome !== "installed") {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MaterializedRelease } from "./release-store.js";
|
|
2
2
|
/** Import the exact immutable startup graph in a terminal-free bounded child process. */
|
|
3
3
|
export declare function warmMaterializedRelease(release: MaterializedRelease, environment: NodeJS.ProcessEnv, timeoutMs?: number): Promise<void>;
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveReleaseEntryPoint } from "./release-store.js";
|
|
3
3
|
import { releaseEnvironment } from "./bootstrap.js";
|
|
4
|
-
import {
|
|
4
|
+
import { launchContractTarget, launchEnvironmentKeys } from "../launch-context/index.js";
|
|
5
|
+
const WARMUP_DIAGNOSTIC_LIMIT = 2_000;
|
|
6
|
+
/** Summarize the child's own failure text without letting an unbounded graph error escape. */
|
|
7
|
+
function warmupDiagnostics(text) {
|
|
8
|
+
const lines = text.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
|
|
9
|
+
return lines.length === 0 ? "" : `: ${lines.slice(0, 4).join(" | ").slice(0, WARMUP_DIAGNOSTIC_LIMIT)}`;
|
|
10
|
+
}
|
|
5
11
|
/** Import the exact immutable startup graph in a terminal-free bounded child process. */
|
|
6
12
|
export async function warmMaterializedRelease(release, environment, timeoutMs = 30_000) {
|
|
7
|
-
const entry =
|
|
13
|
+
const entry = await resolveReleaseEntryPoint(release, "bin/warmup.js");
|
|
8
14
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
15
|
+
// Rationale: the child still gets no terminal, but a discarded failure reason leaves an
|
|
16
|
+
// update reporting only an exit status. Its own diagnostics are read and bounded so the
|
|
17
|
+
// transaction journal records why the graph could not be imported.
|
|
9
18
|
const child = spawn(process.execPath, [entry], {
|
|
10
|
-
env: { ...releaseEnvironment(environment, release), [
|
|
11
|
-
stdio: "ignore",
|
|
19
|
+
env: { ...releaseEnvironment(environment, release), [launchEnvironmentKeys(launchContractTarget(release)).immutableWarmup]: "1" },
|
|
20
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
12
21
|
windowsHide: true,
|
|
13
22
|
});
|
|
23
|
+
let diagnostics = "";
|
|
24
|
+
child.stderr?.setEncoding("utf8");
|
|
25
|
+
child.stderr?.on("data", chunk => { diagnostics = `${diagnostics}${chunk}`.slice(-WARMUP_DIAGNOSTIC_LIMIT); });
|
|
14
26
|
let settled = false;
|
|
15
27
|
const finish = (error) => {
|
|
16
28
|
if (settled)
|
|
@@ -25,7 +37,7 @@ export async function warmMaterializedRelease(release, environment, timeoutMs =
|
|
|
25
37
|
child.once("error", error => finish(error));
|
|
26
38
|
child.once("close", (code, signal) => finish(code === 0
|
|
27
39
|
? undefined
|
|
28
|
-
: new Error(`immutable startup warmup exited with ${code === null ? signal ?? "unknown status" : `status ${code}`}`)));
|
|
40
|
+
: new Error(`immutable startup warmup exited with ${code === null ? signal ?? "unknown status" : `status ${code}`}${warmupDiagnostics(diagnostics)}`)));
|
|
29
41
|
const timer = setTimeout(() => {
|
|
30
42
|
child.kill();
|
|
31
43
|
finish(new Error(`immutable startup warmup exceeded ${timeoutMs}ms`));
|