@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.
Files changed (95) hide show
  1. package/README.md +16 -4
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/cli/dispatch.js +1 -1
  6. package/dist/composition/owned-ui.js +2 -1
  7. package/dist/features/launch/profile-paths.d.ts +2 -0
  8. package/dist/features/launch/profile-paths.js +11 -0
  9. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  10. package/dist/features/owned-ui/settings-app.js +1 -1
  11. package/dist/features/prompt-history/service.d.ts +31 -1
  12. package/dist/features/prompt-history/service.js +294 -129
  13. package/dist/features/prompt-history/store.d.ts +2 -1
  14. package/dist/features/prompt-history/store.js +17 -4
  15. package/dist/features/prompt-history/worker.js +6 -3
  16. package/dist/foundation/launch-context/index.d.ts +59 -0
  17. package/dist/foundation/launch-context/index.js +136 -0
  18. package/dist/foundation/launch-guardian/main.js +6 -4
  19. package/dist/foundation/release/bootstrap.d.ts +6 -1
  20. package/dist/foundation/release/bootstrap.js +59 -13
  21. package/dist/foundation/release/cohort-state.d.ts +1 -0
  22. package/dist/foundation/release/cohort-state.js +1 -0
  23. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  24. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  25. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  26. package/dist/foundation/release/dependency-certification.js +208 -0
  27. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  28. package/dist/foundation/release/dependency-layer.js +6 -37
  29. package/dist/foundation/release/release-gc.js +74 -46
  30. package/dist/foundation/release/release-store.d.ts +2 -0
  31. package/dist/foundation/release/release-store.js +11 -3
  32. package/dist/foundation/release/release.d.ts +3 -1
  33. package/dist/foundation/release/release.js +7 -3
  34. package/dist/foundation/release/restart-certification.d.ts +1 -0
  35. package/dist/foundation/release/restart-certification.js +17 -1
  36. package/dist/foundation/release/update-recovery.d.ts +2 -0
  37. package/dist/foundation/release/update-recovery.js +4 -0
  38. package/dist/foundation/release/update.js +1 -1
  39. package/dist/foundation/release/warmup.d.ts +1 -1
  40. package/dist/foundation/release/warmup.js +18 -6
  41. package/dist/foundation/startup/startup-runtime.js +13 -11
  42. package/dist/foundation/supervision/main.js +8 -7
  43. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  44. package/dist/integrations/pi/components/owned-editor-ux.js +105 -30
  45. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  46. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  47. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  48. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  49. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  51. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  52. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  53. package/dist/integrations/pi/engine/adapter.d.ts +33 -1
  54. package/dist/integrations/pi/engine/adapter.js +309 -112
  55. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  56. package/dist/integrations/pi/engine/changelog.js +63 -0
  57. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  58. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  59. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  60. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  61. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  62. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  63. package/dist/integrations/pi/session-ui/prompt-chips.js +74 -37
  64. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  65. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  66. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +26 -1
  67. package/dist/integrations/pi/session-ui/session-shell-root.js +70 -75
  68. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  69. package/dist/integrations/pi/session-ui/session-shell.js +67 -13
  70. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -2
  71. package/dist/integrations/pi/session-ui/session-viewport-controller.js +25 -20
  72. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  73. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  74. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -2
  75. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +87 -43
  76. package/dist/native/darwin-arm64/manifest.json +1 -1
  77. package/dist/native/linux-x64/manifest.json +1 -1
  78. package/dist/native/win32-x64/manifest.json +2 -2
  79. package/dist/native/win32-x64/process-guardian.exe +0 -0
  80. package/dist/product-identity.d.ts +1 -1
  81. package/dist/product-identity.js +2 -35
  82. package/dist/product-identity.json +1 -23
  83. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  84. package/dist/ui/components/transcript-viewport.js +23 -2
  85. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  86. package/dist/ui/components/visible-hyperlinks.js +1 -0
  87. package/dist/ui/settings/declarations.js +1 -0
  88. package/dist/ui/settings/sections.js +11 -1
  89. package/docs/architecture/boundaries.md +2 -1
  90. package/docs/architecture/internal-naming.md +91 -0
  91. package/docs/architecture/resource-and-data-policy.md +9 -4
  92. package/docs/architecture/toolchain.md +15 -1
  93. package/docs/features/prompt-history.md +39 -11
  94. package/docs/manual-code-streaming-cleanup.md +88 -0
  95. 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, immutablePlatformPolicy } from "./immutable-platform.js";
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
- const certification = JSON.parse(await readFile(certificationPath(dataDir, layerId), "utf8"));
285
- const identityMatches = certification.schema === PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema
286
- && certification.layerId === manifest.layerId && certification.contentDigest === manifest.contentDigest;
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, dependencyLayerCertificationPath } from "./dependency-layer.js";
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 = "A1_RELEASE_CLEANUP_HOLDS";
21
- const WORKER_RUN_ID_ENV = "A1_RELEASE_CLEANUP_RUN_ID";
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 { attempted: 0, completed: 0 };
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
- for (const candidate of candidates.slice(0, maxItems)) {
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
- await rm(dependencyLayerCertificationPath(dataDir, candidate.layerId), { force: true });
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
- for (const entry of await readdir(dataDir, { withFileTypes: true })) {
555
- const match = /^dependency-layer-certification-(dependencies-[a-f0-9]{32})\.json$/.exec(entry.name);
556
- if (!match || !entry.isFile() || referenced.has(match[1]))
557
- continue;
558
- if (await lstat(resolve(layersRoot, match[1])).catch(error => missingOrThrow(error)) !== null)
559
- continue;
560
- attempted += 1;
561
- try {
562
- await (options.operations?.remove ?? rm)(resolve(dataDir, entry.name), { force: true });
563
- completed += 1;
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
- catch (error) {
566
- await store.recordCleanupFailure(match[1], "dependency-layer-certification", error);
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
- if (attempted >= maxItems)
569
- break;
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[PRODUCT_IDENTITY.environment.releaseRoot];
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 productDigest = digestManifestFiles(files);
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 { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
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 A1 launcher is available and the transaction can be resumed.")}\n`);
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 { MaterializedRelease } from "./release-store.js";
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 { resolve } from "node:path";
2
+ import { resolveReleaseEntryPoint } from "./release-store.js";
3
3
  import { releaseEnvironment } from "./bootstrap.js";
4
- import { PRODUCT_IDENTITY } from "../../product-identity.js";
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 = resolve(release.releaseRoot, "bin", "warmup.js");
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), [PRODUCT_IDENTITY.environment.immutableWarmup]: "1" },
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`));