@timurproko/a1 0.1.8-dev.226 → 0.1.8-dev.259

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 (94) hide show
  1. package/bin/guardian.js +4 -2
  2. package/bin/supervisor.js +1 -1
  3. package/bin/ui.js +44 -36
  4. package/bin/warmup.js +18 -9
  5. package/dist/cli/dispatch.d.ts +9 -1
  6. package/dist/cli/dispatch.js +73 -14
  7. package/dist/cli/package-messages.d.ts +26 -0
  8. package/dist/cli/package-messages.js +71 -0
  9. package/dist/cli/packages.d.ts +2 -1
  10. package/dist/cli/packages.js +12 -19
  11. package/dist/composition/owned-ui.d.ts +5 -2
  12. package/dist/composition/owned-ui.js +3 -1
  13. package/dist/contracts/agent-engine/package-ports.d.ts +6 -0
  14. package/dist/features/launch/intent.d.ts +5 -3
  15. package/dist/features/launch/intent.js +7 -2
  16. package/dist/features/owned-ui/index.d.ts +1 -0
  17. package/dist/features/owned-ui/index.js +1 -0
  18. package/dist/features/owned-ui/session-fork-prompt.d.ts +8 -0
  19. package/dist/features/owned-ui/session-fork-prompt.js +34 -0
  20. package/dist/foundation/launch-guardian/main.d.ts +2 -0
  21. package/dist/foundation/launch-guardian/main.js +8 -3
  22. package/dist/foundation/lifecycle/index.d.ts +2 -0
  23. package/dist/foundation/lifecycle/index.js +2 -0
  24. package/dist/foundation/lifecycle/paths.d.ts +2 -0
  25. package/dist/foundation/lifecycle/paths.js +9 -2
  26. package/dist/foundation/lifecycle/session-selection.d.ts +9 -0
  27. package/dist/foundation/lifecycle/session-selection.js +39 -0
  28. package/dist/foundation/lifecycle/supervisor-startup.d.ts +30 -0
  29. package/dist/foundation/lifecycle/supervisor-startup.js +106 -0
  30. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +5 -0
  31. package/dist/foundation/process-containment/darwin-process-inspector.js +7 -0
  32. package/dist/foundation/process-containment/index.d.ts +1 -0
  33. package/dist/foundation/process-containment/index.js +1 -0
  34. package/dist/foundation/process-containment/windows-process-inspector.d.ts +2 -1
  35. package/dist/foundation/process-containment/windows-process-inspector.js +4 -2
  36. package/dist/foundation/release/bootstrap.d.ts +10 -3
  37. package/dist/foundation/release/bootstrap.js +58 -25
  38. package/dist/foundation/release/dependency-layer.js +11 -10
  39. package/dist/foundation/release/immutable-platform.d.ts +6 -0
  40. package/dist/foundation/release/immutable-platform.js +13 -0
  41. package/dist/foundation/release/index.d.ts +1 -0
  42. package/dist/foundation/release/index.js +1 -0
  43. package/dist/foundation/release/release-store.d.ts +1 -1
  44. package/dist/foundation/release/release-store.js +4 -2
  45. package/dist/foundation/release/restart-certification.d.ts +67 -0
  46. package/dist/foundation/release/restart-certification.js +211 -0
  47. package/dist/foundation/release/update.js +4 -4
  48. package/dist/foundation/release/warmup.js +1 -1
  49. package/dist/foundation/startup/startup-runtime.d.ts +2 -2
  50. package/dist/foundation/startup/startup-runtime.js +1 -1
  51. package/dist/foundation/supervision/main.d.ts +1 -1
  52. package/dist/foundation/supervision/main.js +60 -30
  53. package/dist/foundation/supervision/server.js +14 -1
  54. package/dist/integrations/pi/components/shell-footer-status.js +28 -12
  55. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +4 -1
  56. package/dist/integrations/pi/components/shell-selectors-dialogs.js +2 -1
  57. package/dist/integrations/pi/components/shell-shared-facade.d.ts +5 -0
  58. package/dist/integrations/pi/engine/adapter.d.ts +6 -0
  59. package/dist/integrations/pi/engine/adapter.js +354 -53
  60. package/dist/integrations/pi/engine/index.d.ts +1 -0
  61. package/dist/integrations/pi/engine/index.js +1 -0
  62. package/dist/integrations/pi/engine/package-integration.js +31 -25
  63. package/dist/integrations/pi/engine/runtime-integration.d.ts +3 -0
  64. package/dist/integrations/pi/engine/runtime-integration.js +11 -4
  65. package/dist/integrations/pi/engine/session-selection.d.ts +28 -0
  66. package/dist/integrations/pi/engine/session-selection.js +90 -0
  67. package/dist/integrations/pi/engine/workflows.d.ts +18 -1
  68. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +5 -1
  69. package/dist/integrations/pi/session-ui/session-shell-root.js +76 -24
  70. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  71. package/dist/integrations/pi/session-ui/session-shell.js +46 -14
  72. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -1
  73. package/dist/integrations/pi/session-ui/session-viewport-controller.js +47 -18
  74. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
  75. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +166 -26
  76. package/dist/native/darwin-arm64/manifest.json +4 -4
  77. package/dist/native/darwin-arm64/process-guardian +0 -0
  78. package/dist/native/linux-x64/manifest.json +1 -1
  79. package/dist/native/linux-x64/process-guardian +0 -0
  80. package/dist/native/win32-x64/manifest.json +2 -2
  81. package/dist/native/win32-x64/process-guardian.exe +0 -0
  82. package/dist/ui/components/index.d.ts +1 -0
  83. package/dist/ui/components/index.js +1 -0
  84. package/dist/ui/components/text-selection.js +8 -35
  85. package/dist/ui/components/transcript-viewport.d.ts +8 -2
  86. package/dist/ui/components/transcript-viewport.js +8 -6
  87. package/dist/ui/components/visible-hyperlinks.d.ts +19 -0
  88. package/dist/ui/components/visible-hyperlinks.js +64 -0
  89. package/docs/architecture/process-guardian-provenance.md +3 -1
  90. package/docs/architecture/ui-reference-provenance.md +2 -0
  91. package/docs/ci-release-runbook.md +1 -1
  92. package/docs/features/launch-profiles.md +17 -1
  93. package/docs/manual-ghost-link-baseline.md +89 -0
  94. package/package.json +1 -1
@@ -2,6 +2,7 @@ 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
6
  import { digestManifestFiles, releaseFileIdentity, resolveWithin } from "./release.js";
6
7
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
7
8
  const LAYER_IO_CONCURRENCY = 32;
@@ -278,7 +279,8 @@ export async function readCertifiedDependencyLayer(dataDir, layerId, expected) {
278
279
  const manifest = JSON.parse(await readFile(resolve(layerRoot, DEPENDENCY_LAYER_MANIFEST), "utf8"));
279
280
  validateLayerManifest(manifest);
280
281
  const certification = JSON.parse(await readFile(certificationPath(dataDir, layerId), "utf8"));
281
- if (certification.schema !== PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema || certification.layerId !== manifest.layerId || certification.contentDigest !== manifest.contentDigest) {
282
+ if (certification.schema !== PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema || certification.layerId !== manifest.layerId || certification.contentDigest !== manifest.contentDigest
283
+ || certification.platform !== process.platform || certification.platformPolicy !== immutablePlatformPolicy()) {
282
284
  throw new Error(`dependency layer certification differs from manifest: ${layerId}`);
283
285
  }
284
286
  if (expected && (expected.layerId !== manifest.layerId || expected.contentDigest !== manifest.contentDigest)) {
@@ -300,6 +302,7 @@ export async function verifyDependencyLayer(dataDir, reference, onOperation) {
300
302
  const metadata = await lstat(path);
301
303
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== file.bytes)
302
304
  throw new Error(`dependency layer file mismatch: ${file.path}`);
305
+ assertImmutableFileMode(metadata, path);
303
306
  const bytes = await readFile(path);
304
307
  onOperation?.({ operation: "verification-read", path: file.path, bytes: bytes.length });
305
308
  const actual = releaseFileIdentity(file.path, bytes, file.executable);
@@ -338,16 +341,14 @@ async function writeLayerCertification(dataDir, identity) {
338
341
  schema: PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema,
339
342
  layerId: identity.layerId,
340
343
  contentDigest: identity.contentDigest,
344
+ platform: process.platform,
345
+ platformPolicy: immutablePlatformPolicy(),
341
346
  certifiedAt: new Date().toISOString(),
342
- }, null, 2), { flag: "wx", mode: 0o600 });
343
- try {
344
- await rename(temporary, path);
345
- }
346
- catch (error) {
347
- if (!await lstat(path).catch(() => null))
348
- throw error;
349
- await rm(temporary, { force: true });
350
- }
347
+ }, null, 2), { flag: "wx", mode: 0o400 });
348
+ await chmod(path, 0o600).catch(() => { });
349
+ await rm(path, { force: true });
350
+ await rename(temporary, path);
351
+ await chmod(path, 0o400);
351
352
  }
352
353
  function validateLayerManifest(value) {
353
354
  if (value.schema !== DEPENDENCY_LAYER_SCHEMA || !/^dependencies-[a-f0-9]{32}$/.test(value.layerId)
@@ -0,0 +1,6 @@
1
+ import type { Stats } from "node:fs";
2
+ export type ImmutablePlatformPolicy = "windows-readonly-content-v1" | "posix-readonly-content-v1";
3
+ /** Name the platform protection established while complete payload verification owns every file. */
4
+ export declare function immutablePlatformPolicy(platform?: NodeJS.Platform): ImmutablePlatformPolicy | null;
5
+ /** Refuse certification when a payload file is writable after immutable materialization. */
6
+ export declare function assertImmutableFileMode(metadata: Pick<Stats, "mode">, path: string): void;
@@ -0,0 +1,13 @@
1
+ /** Name the platform protection established while complete payload verification owns every file. */
2
+ export function immutablePlatformPolicy(platform = process.platform) {
3
+ if (platform === "win32")
4
+ return "windows-readonly-content-v1";
5
+ if (platform === "linux" || platform === "darwin")
6
+ return "posix-readonly-content-v1";
7
+ return null;
8
+ }
9
+ /** Refuse certification when a payload file is writable after immutable materialization. */
10
+ export function assertImmutableFileMode(metadata, path) {
11
+ if ((metadata.mode & 0o222) !== 0)
12
+ throw new Error(`immutable payload file is writable: ${path}`);
13
+ }
@@ -7,6 +7,7 @@ export * from "./process-cleanup.js";
7
7
  export * from "./release.js";
8
8
  export * from "./release-gc.js";
9
9
  export * from "./release-store.js";
10
+ export * from "./restart-certification.js";
10
11
  export * from "./stable-release.js";
11
12
  export * from "./update.js";
12
13
  export * from "./update-transaction.js";
@@ -7,6 +7,7 @@ export * from "./process-cleanup.js";
7
7
  export * from "./release.js";
8
8
  export * from "./release-gc.js";
9
9
  export * from "./release-store.js";
10
+ export * from "./restart-certification.js";
10
11
  export * from "./stable-release.js";
11
12
  export * from "./update.js";
12
13
  export * from "./update-transaction.js";
@@ -32,7 +32,7 @@ export interface CertifiedReleaseRecord {
32
32
  export declare function materializeRelease(packageRoot: string, dataDir: string, options?: MaterializeReleaseOptions): Promise<MaterializedRelease>;
33
33
  /** Consume proof that this exact object was freshly materialized or fully verified in this process. */
34
34
  export declare function consumeMaterializationProof(release: MaterializedRelease): boolean;
35
- export declare function readMaterializedRelease(releaseRoot: string): Promise<MaterializedRelease>;
35
+ export declare function readMaterializedRelease(releaseRoot: string, selectedStoreRoot?: string): Promise<MaterializedRelease>;
36
36
  /**
37
37
  * Load metadata for a release whose bytes were already certified by the
38
38
  * current parent process or an authenticated live supervisor. Callers must
@@ -4,6 +4,7 @@ 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
6
  import { mapWithConcurrency } from "./concurrency.js";
7
+ import { assertImmutableFileMode } from "./immutable-platform.js";
7
8
  import { dependencyReference, materializeDependencyLayer, readCertifiedDependencyLayer, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
8
9
  const RELEASE_FILE_IO_CONCURRENCY = 32;
9
10
  const certificationReady = new WeakSet();
@@ -94,10 +95,10 @@ export function consumeMaterializationProof(release) {
94
95
  certificationReady.delete(release);
95
96
  return true;
96
97
  }
97
- export async function readMaterializedRelease(releaseRoot) {
98
+ export async function readMaterializedRelease(releaseRoot, selectedStoreRoot) {
98
99
  const canonical = await realpath(releaseRoot);
99
100
  const manifest = JSON.parse(await readFile(resolve(canonical, RELEASE_MANIFEST_FILENAME), "utf8"));
100
- return await verifyMaterializedRelease(canonical, manifest);
101
+ return certificationReadyRelease(await verifyMaterializedRelease(canonical, manifest, selectedStoreRoot));
101
102
  }
102
103
  /**
103
104
  * Load metadata for a release whose bytes were already certified by the
@@ -187,6 +188,7 @@ async function verifyFile(root, file, options) {
187
188
  throw new Error(`release candidate is incomplete: ${file.path}`);
188
189
  if (metadata.size !== file.bytes)
189
190
  throw new Error(`release file size mismatch: ${file.path}`);
191
+ assertImmutableFileMode(metadata, path);
190
192
  const bytes = await readFile(path);
191
193
  options.onOperation?.({ operation: "verification-read", path: file.path, bytes: bytes.length });
192
194
  const digest = createHash("sha256").update(bytes).digest("hex");
@@ -0,0 +1,67 @@
1
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
2
+ import { type DependencyLayerReference } from "./dependency-layer.js";
3
+ import { type ImmutablePlatformPolicy } from "./immutable-platform.js";
4
+ import { type ReleaseFileIdentity } from "./release.js";
5
+ import { type CertifiedReleaseRecord, type MaterializedRelease } from "./release-store.js";
6
+ declare const RESTART_SEAL_VERSION = 1;
7
+ type EvidenceKind = "directory" | "file" | "binding";
8
+ interface PathEvidence {
9
+ readonly path: string;
10
+ readonly kind: EvidenceKind;
11
+ readonly device: string;
12
+ readonly inode: string;
13
+ readonly mode: number;
14
+ readonly size: string;
15
+ readonly modifiedNs: string;
16
+ readonly changedNs: string;
17
+ readonly bornNs: string;
18
+ }
19
+ interface LayerRestartEvidence extends DependencyLayerReference {
20
+ readonly layerRoot: PathEvidence;
21
+ readonly manifest: PathEvidence;
22
+ readonly certification: PathEvidence;
23
+ readonly bindingEvidence: PathEvidence;
24
+ readonly bindingTarget: string;
25
+ }
26
+ interface RestartSealCore {
27
+ readonly version: typeof RESTART_SEAL_VERSION;
28
+ readonly platform: NodeJS.Platform;
29
+ readonly platformPolicy: ImmutablePlatformPolicy;
30
+ readonly releaseId: string;
31
+ readonly packageVersion: string;
32
+ readonly contentDigest: string;
33
+ readonly packageRoot: string;
34
+ readonly releaseRoot: PathEvidence;
35
+ readonly manifest: PathEvidence;
36
+ readonly launchEntries: readonly ReleaseFileIdentity[];
37
+ readonly launchEntryEvidence: readonly PathEvidence[];
38
+ readonly dependencyLayers: readonly LayerRestartEvidence[];
39
+ readonly certifiedAt: string;
40
+ }
41
+ export interface RestartSeal extends RestartSealCore {
42
+ readonly sealDigest: string;
43
+ }
44
+ interface ReleaseCertificationDocument {
45
+ readonly schema: typeof PRODUCT_IDENTITY.evidence.releaseCertificationSchema;
46
+ readonly releaseId: string;
47
+ readonly contentDigest: string;
48
+ readonly verifiedAt: string;
49
+ readonly checks: readonly {
50
+ readonly id: string;
51
+ readonly passed: boolean;
52
+ }[];
53
+ readonly restartSeal: RestartSeal | null;
54
+ }
55
+ export interface RestartValidationEvent {
56
+ readonly operation: "restart-evidence-read";
57
+ readonly path: string;
58
+ }
59
+ /** Build compact restart authority after complete content verification established read-only payload files. */
60
+ export declare function createRestartSeal(release: MaterializedRelease, dataDir: string): Promise<RestartSeal | null>;
61
+ /** Recover an approved release from durable bounded evidence without traversing its payload. */
62
+ export declare function readRestartCertifiedRelease(record: CertifiedReleaseRecord & {
63
+ readonly diagnosticsPath?: string | null;
64
+ }, dataDir: string, onEvent?: (event: RestartValidationEvent) => void): Promise<MaterializedRelease>;
65
+ export declare function releaseCertificationDocument(release: MaterializedRelease, restartSeal: RestartSeal | null, verifiedAt?: string): ReleaseCertificationDocument;
66
+ export declare function restartSealDigest(core: RestartSealCore): string;
67
+ export {};
@@ -0,0 +1,211 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
+ import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
6
+ import { immutablePlatformPolicy } from "./immutable-platform.js";
7
+ import { PRODUCT_PACKAGE_NAME } from "./release.js";
8
+ import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
9
+ import { UpdateTransactionStore } from "./update-transaction.js";
10
+ const RESTART_SEAL_VERSION = 1;
11
+ const REQUIRED_LAUNCH_ENTRIES = ["bin/supervisor.js", "bin/guardian.js"];
12
+ /** Build compact restart authority after complete content verification established read-only payload files. */
13
+ export async function createRestartSeal(release, dataDir) {
14
+ const policy = immutablePlatformPolicy();
15
+ if (policy === null)
16
+ return null;
17
+ const canonicalData = await realpath(dataDir);
18
+ const releasesRoot = await realpath(resolve(canonicalData, "releases"));
19
+ const releaseRoot = await realpath(release.releaseRoot);
20
+ assertDirectChild(releasesRoot, releaseRoot, release.releaseId, "release");
21
+ const manifestPath = resolve(releaseRoot, RELEASE_MANIFEST_FILENAME);
22
+ const launchEntries = REQUIRED_LAUNCH_ENTRIES.map(path => release.files.find(candidate => candidate.path === path));
23
+ if (launchEntries.some(file => file === undefined))
24
+ return null;
25
+ const certifiedLaunchEntries = launchEntries;
26
+ const layersRoot = release.dependencyLayers?.length ? await realpath(resolve(canonicalData, "dependency-layers")) : null;
27
+ const dependencyLayers = await Promise.all((release.dependencyLayers ?? []).map(async (reference) => {
28
+ if (layersRoot === null)
29
+ throw new Error("restart certification has no dependency-layer root");
30
+ const layerRoot = await realpath(resolve(layersRoot, reference.layerId));
31
+ assertDirectChild(layersRoot, layerRoot, reference.layerId, "dependency layer");
32
+ const bindingPath = resolve(releaseRoot, reference.binding);
33
+ const bindingTarget = await realpath(bindingPath);
34
+ const expectedTarget = await realpath(resolve(layerRoot, "node_modules"));
35
+ if (bindingTarget !== expectedTarget)
36
+ throw new Error(`restart certification dependency binding targets unexpected content: ${bindingPath}`);
37
+ return {
38
+ ...reference,
39
+ layerRoot: await pathEvidence(layerRoot, "directory"),
40
+ manifest: await pathEvidence(resolve(layerRoot, DEPENDENCY_LAYER_MANIFEST), "file"),
41
+ certification: await pathEvidence(dependencyLayerCertificationPath(canonicalData, reference.layerId), "file"),
42
+ bindingEvidence: await pathEvidence(bindingPath, "binding", true),
43
+ bindingTarget,
44
+ };
45
+ }));
46
+ const core = {
47
+ version: RESTART_SEAL_VERSION,
48
+ platform: process.platform,
49
+ platformPolicy: policy,
50
+ releaseId: release.releaseId,
51
+ packageVersion: release.packageVersion,
52
+ contentDigest: release.contentDigest,
53
+ packageRoot: release.packageRoot,
54
+ releaseRoot: await pathEvidence(releaseRoot, "directory"),
55
+ manifest: await pathEvidence(manifestPath, "file"),
56
+ launchEntries: certifiedLaunchEntries,
57
+ launchEntryEvidence: await Promise.all(certifiedLaunchEntries.map(file => pathEvidence(resolve(releaseRoot, file.path), "file"))),
58
+ dependencyLayers,
59
+ certifiedAt: new Date().toISOString(),
60
+ };
61
+ return { ...core, sealDigest: restartSealDigest(core) };
62
+ }
63
+ /** Recover an approved release from durable bounded evidence without traversing its payload. */
64
+ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
65
+ const transaction = await new UpdateTransactionStore(dataDir).read();
66
+ if (transaction?.status === "active")
67
+ throw new Error("restart certification is unavailable during an active update transaction");
68
+ const certificationPath = resolve(dataDir, `certification-${record.releaseId}.json`);
69
+ if (record.diagnosticsPath !== undefined && record.diagnosticsPath !== certificationPath) {
70
+ throw new Error("restart certification path differs from the approved release record");
71
+ }
72
+ const certificationMetadata = await lstat(certificationPath);
73
+ onEvent?.({ operation: "restart-evidence-read", path: certificationPath });
74
+ if (!certificationMetadata.isFile() || certificationMetadata.isSymbolicLink() || (certificationMetadata.mode & 0o222) !== 0) {
75
+ throw new Error("restart certification is not an immutable regular file");
76
+ }
77
+ const certification = await readJson(certificationPath, onEvent);
78
+ if (certification.schema !== PRODUCT_IDENTITY.evidence.releaseCertificationSchema
79
+ || certification.releaseId !== record.releaseId || certification.contentDigest !== record.contentDigest
80
+ || !certification.checks.some(check => check.id === "immutable-content" && check.passed)) {
81
+ throw new Error("restart certification differs from the approved release record");
82
+ }
83
+ const seal = normalizeRestartSeal(certification.restartSeal);
84
+ if (seal.platform !== process.platform || seal.platformPolicy !== immutablePlatformPolicy()) {
85
+ throw new Error("restart certification platform immutability evidence is unsupported");
86
+ }
87
+ if (seal.releaseId !== record.releaseId || seal.contentDigest !== record.contentDigest
88
+ || resolve(seal.releaseRoot.path) !== resolve(record.releaseRoot)
89
+ || (record.packageVersion !== undefined && seal.packageVersion !== record.packageVersion)) {
90
+ throw new Error("restart seal differs from the approved release identity");
91
+ }
92
+ const { sealDigest, ...core } = seal;
93
+ if (restartSealDigest(core) !== sealDigest)
94
+ throw new Error("restart seal digest is invalid");
95
+ const canonicalData = await observedRealpath(dataDir, onEvent);
96
+ const releasesRoot = await observedRealpath(resolve(canonicalData, "releases"), onEvent);
97
+ const releaseRoot = await observedRealpath(seal.releaseRoot.path, onEvent);
98
+ assertDirectChild(releasesRoot, releaseRoot, seal.releaseId, "release");
99
+ await assertPathEvidence(seal.releaseRoot, onEvent);
100
+ await assertPathEvidence(seal.manifest, onEvent);
101
+ for (let index = 0; index < seal.launchEntries.length; index += 1) {
102
+ const file = seal.launchEntries[index];
103
+ if (!REQUIRED_LAUNCH_ENTRIES.includes(file.path)) {
104
+ throw new Error(`restart seal contains an unexpected launch entry: ${file.path}`);
105
+ }
106
+ const evidence = seal.launchEntryEvidence[index];
107
+ if (!evidence || evidence.path !== resolve(releaseRoot, file.path) || file.bytes !== Number(evidence.size)) {
108
+ throw new Error(`restart launch entry evidence differs: ${file.path}`);
109
+ }
110
+ await assertPathEvidence(evidence, onEvent);
111
+ }
112
+ if (seal.launchEntries.length !== REQUIRED_LAUNCH_ENTRIES.length)
113
+ throw new Error("restart seal launch entries are incomplete");
114
+ const layersRoot = seal.dependencyLayers.length ? await observedRealpath(resolve(canonicalData, "dependency-layers"), onEvent) : null;
115
+ for (const layer of seal.dependencyLayers) {
116
+ if (layersRoot === null)
117
+ throw new Error("restart seal has no dependency-layer root");
118
+ const layerRoot = await observedRealpath(layer.layerRoot.path, onEvent);
119
+ assertDirectChild(layersRoot, layerRoot, layer.layerId, "dependency layer");
120
+ await assertPathEvidence(layer.layerRoot, onEvent);
121
+ await assertPathEvidence(layer.manifest, onEvent);
122
+ await assertPathEvidence(layer.certification, onEvent);
123
+ await assertPathEvidence(layer.bindingEvidence, onEvent, true);
124
+ const bindingTarget = await observedRealpath(layer.bindingEvidence.path, onEvent);
125
+ const expectedTarget = await observedRealpath(resolve(layerRoot, "node_modules"), onEvent);
126
+ if (bindingTarget !== layer.bindingTarget || bindingTarget !== expectedTarget) {
127
+ throw new Error(`restart dependency binding targets unexpected content: ${layer.bindingEvidence.path}`);
128
+ }
129
+ }
130
+ return {
131
+ packageName: PRODUCT_PACKAGE_NAME,
132
+ packageVersion: seal.packageVersion,
133
+ contentDigest: seal.contentDigest,
134
+ releaseId: seal.releaseId,
135
+ packageRoot: seal.packageRoot,
136
+ files: seal.launchEntries,
137
+ ...(seal.dependencyLayers.length === 0 ? {} : {
138
+ dependencyLayers: seal.dependencyLayers.map(({ layerId, contentDigest, binding }) => ({ layerId, contentDigest, binding })),
139
+ }),
140
+ releaseRoot,
141
+ };
142
+ }
143
+ export function releaseCertificationDocument(release, restartSeal, verifiedAt = new Date().toISOString()) {
144
+ return {
145
+ schema: PRODUCT_IDENTITY.evidence.releaseCertificationSchema,
146
+ releaseId: release.releaseId,
147
+ contentDigest: release.contentDigest,
148
+ verifiedAt,
149
+ checks: [{ id: "immutable-content", passed: true }],
150
+ restartSeal,
151
+ };
152
+ }
153
+ export function restartSealDigest(core) {
154
+ return createHash("sha256").update(JSON.stringify(core)).digest("hex");
155
+ }
156
+ async function assertPathEvidence(evidence, onEvent, symbolic = false) {
157
+ const actual = await pathEvidence(evidence.path, evidence.kind, symbolic, onEvent);
158
+ if (JSON.stringify(actual) !== JSON.stringify(evidence))
159
+ throw new Error(`restart path evidence changed: ${evidence.path}`);
160
+ }
161
+ async function pathEvidence(path, kind, symbolic = false, onEvent) {
162
+ onEvent?.({ operation: "restart-evidence-read", path });
163
+ const metadata = symbolic ? await lstat(path, { bigint: true }) : await stat(path, { bigint: true });
164
+ if ((kind === "directory") !== metadata.isDirectory())
165
+ throw new Error(`restart evidence kind changed: ${path}`);
166
+ if (kind === "file" && !metadata.isFile())
167
+ throw new Error(`restart evidence file changed: ${path}`);
168
+ if (kind === "binding" && !metadata.isSymbolicLink())
169
+ throw new Error(`restart evidence binding changed: ${path}`);
170
+ if (kind === "file" && (metadata.mode & 146n) !== 0n)
171
+ throw new Error(`restart evidence file is writable: ${path}`);
172
+ return {
173
+ path: resolve(path),
174
+ kind,
175
+ device: metadata.dev.toString(),
176
+ inode: metadata.ino.toString(),
177
+ mode: Number(metadata.mode),
178
+ size: metadata.size.toString(),
179
+ modifiedNs: metadata.mtimeNs.toString(),
180
+ changedNs: metadata.ctimeNs.toString(),
181
+ bornNs: metadata.birthtimeNs.toString(),
182
+ };
183
+ }
184
+ function normalizeRestartSeal(value) {
185
+ if (!value || typeof value !== "object" || Array.isArray(value))
186
+ throw new Error("release certification has no durable restart seal");
187
+ const seal = value;
188
+ if (seal.version !== RESTART_SEAL_VERSION || typeof seal.platform !== "string" || typeof seal.platformPolicy !== "string"
189
+ || typeof seal.releaseId !== "string" || typeof seal.packageVersion !== "string" || typeof seal.contentDigest !== "string"
190
+ || typeof seal.packageRoot !== "string" || typeof seal.certifiedAt !== "string" || typeof seal.sealDigest !== "string"
191
+ || !seal.releaseRoot || !seal.manifest || !Array.isArray(seal.launchEntries) || !Array.isArray(seal.launchEntryEvidence)
192
+ || !Array.isArray(seal.dependencyLayers)) {
193
+ throw new Error("durable restart seal is invalid");
194
+ }
195
+ return seal;
196
+ }
197
+ async function readJson(path, onEvent) {
198
+ onEvent?.({ operation: "restart-evidence-read", path });
199
+ return JSON.parse(await readFile(path, "utf8"));
200
+ }
201
+ async function observedRealpath(path, onEvent) {
202
+ onEvent?.({ operation: "restart-evidence-read", path });
203
+ return await realpath(path);
204
+ }
205
+ function assertDirectChild(parent, child, expectedName, kind) {
206
+ const fromParent = relative(parent, child);
207
+ if (isAbsolute(fromParent) || fromParent === ".." || fromParent.startsWith(`..${sep}`) || fromParent.includes(sep)
208
+ || basename(child) !== expectedName) {
209
+ throw new Error(`restart ${kind} is outside its managed store: ${child}`);
210
+ }
211
+ }
@@ -167,8 +167,8 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
167
167
  onWarmup?.("started");
168
168
  await warmMaterializedRelease(candidate, environment);
169
169
  onWarmup?.("completed");
170
- await startSupervisor(candidate, environment);
171
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000);
170
+ const startup = await startSupervisor(candidate, environment);
171
+ await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000, startup);
172
172
  },
173
173
  };
174
174
  }
@@ -555,8 +555,8 @@ async function rollbackPriorCohort(dataDir, environment, priorReleaseId) {
555
555
  const paths = resolveProductPaths(environment);
556
556
  // Invariant: rollback re-points the active reference and starts the prior cohort on its own endpoint;
557
557
  // a cohort that survived the update keeps serving the work it already had.
558
- await startSupervisor(release, environment);
559
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, release.releaseId, environment).endpointMetadataPath, release, 8_000);
558
+ const startup = await startSupervisor(release, environment);
559
+ await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, release.releaseId, environment).endpointMetadataPath, release, 8_000, startup);
560
560
  return "rolled back";
561
561
  }
562
562
  function phaseBefore(current, target) {
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { releaseEnvironment } from "./bootstrap.js";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
5
  /** Import the exact immutable startup graph in a terminal-free bounded child process. */
6
- export async function warmMaterializedRelease(release, environment, timeoutMs = 15_000) {
6
+ export async function warmMaterializedRelease(release, environment, timeoutMs = 30_000) {
7
7
  const entry = resolve(release.releaseRoot, "bin", "warmup.js");
8
8
  await new Promise((resolvePromise, rejectPromise) => {
9
9
  const child = spawn(process.execPath, [entry], {
@@ -1,8 +1,8 @@
1
1
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
2
- export type StartupPhase = "command-invoked" | "bootstrap-start" | "bootstrap-selected" | "guardian-start" | "guardian-connected" | "ui-entry" | "ui-modules-loaded" | "pi-services" | "resource-discovery" | "session-created" | "settings-loaded" | "first-input-ready-render";
2
+ export type StartupPhase = "command-invoked" | "bootstrap-start" | "bootstrap-selected" | "durable-validation-start" | "durable-validation-complete" | "replacement-supervisor-start" | "replacement-supervisor-ready" | "guardian-start" | "guardian-connected" | "ui-entry" | "ui-modules-loaded" | "pi-services" | "resource-discovery" | "session-created" | "settings-loaded" | "first-input-ready-render";
3
3
  export interface StartupPerformanceEvidence {
4
4
  readonly profileId: "a1" | "pi";
5
- readonly launchKind: "post-update" | "warm";
5
+ readonly launchKind: "post-update" | "no-live-supervisor" | "warm";
6
6
  readonly events: readonly StartupTraceEvent[];
7
7
  }
8
8
  export interface StartupTraceEvent {
@@ -100,7 +100,7 @@ export function assertStartupPerformanceBudget(evidence, budgets = { postUpdateM
100
100
  const ready = events.findLast(event => event.phase === "first-input-ready-render");
101
101
  if (!ready)
102
102
  throw new Error(`startup budget failed for ${evidence.profileId}: first input-ready render was not recorded`);
103
- const budget = evidence.launchKind === "post-update" ? budgets.postUpdateMs : budgets.warmMs;
103
+ const budget = evidence.launchKind === "warm" ? budgets.warmMs : budgets.postUpdateMs;
104
104
  if (ready.elapsedMs <= budget)
105
105
  return;
106
106
  const intervals = events.map((event, index) => ({
@@ -1 +1 @@
1
- export declare function runSupervisor(): Promise<void>;
1
+ export declare function runSupervisor(arguments_?: readonly string[]): Promise<void>;
@@ -2,11 +2,12 @@ import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { assertImmutableExecutionRoot, CohortStateStore, readCertifiedReleaseManifest } from "../release/index.js";
5
+ import { publishSupervisorStartupResult, supervisorStartupFailure, supervisorStartupReady, supervisorStartupResultPath } from "../lifecycle/index.js";
5
6
  import { ControlStore } from "../storage/index.js";
6
7
  import { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
7
8
  import { SupervisorServer } from "./server.js";
8
9
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
9
- export async function runSupervisor() {
10
+ export async function runSupervisor(arguments_ = []) {
10
11
  const productPaths = resolveProductPaths();
11
12
  mkdirSync(productPaths.runtimeDir, { recursive: true, mode: 0o700 });
12
13
  mkdirSync(productPaths.endpointsDir, { recursive: true, mode: 0o700 });
@@ -15,33 +16,62 @@ export async function runSupervisor() {
15
16
  const releaseRoot = process.env[PRODUCT_IDENTITY.environment.releaseRoot];
16
17
  const releaseId = process.env[PRODUCT_IDENTITY.environment.releaseId];
17
18
  const contentDigest = process.env[PRODUCT_IDENTITY.environment.releaseDigest];
18
- if (!releaseRoot || !releaseId || !contentDigest)
19
- throw new Error(PRODUCT_TEXT.diagnostic("supervisor must be launched from a verified immutable release"));
20
- const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases"));
21
- await assertImmutableExecutionRoot(release, paths.dataDir);
22
- // Protocol: one endpoint per cohort: a superseded cohort keeps serving what it already has while the
23
- // installed release listens on its own address.
24
- paths = { ...productPaths, ...resolveCohortEndpoint(productPaths, release.releaseId) };
25
- const bootNonce = randomUUID();
26
- const store = new ControlStore(paths.databasePath, bootNonce);
27
- const cohortState = new CohortStateStore(paths.dataDir);
28
- const server = new SupervisorServer(store, paths, release, bootNonce, undefined, undefined, undefined, async () => (await cohortState.read()).references.active);
29
- await server.listen();
30
- log(`supervisor ${server.id} listening at ${paths.endpoint} (pid ${process.pid})`);
31
- let closing = false;
32
- const close = async (signal) => {
33
- if (closing)
34
- return;
35
- closing = true;
36
- log(`received ${signal}; stopping supervised process tree`);
37
- await server.close(true);
38
- process.exitCode = 0;
39
- };
40
- process.on("SIGINT", () => void close("SIGINT"));
41
- process.on("SIGTERM", () => void close("SIGTERM"));
42
- process.on("uncaughtException", error => {
43
- log(`uncaught exception: ${error.stack ?? error.message}`);
44
- process.exitCode = 1;
45
- });
46
- process.on("unhandledRejection", error => log(`unhandled rejection: ${String(error)}`));
19
+ const attemptId = startupAttempt(arguments_);
20
+ const resultPath = attemptId ? supervisorStartupResultPath(productPaths.runtimeDir, attemptId) : null;
21
+ let stage = "release-environment";
22
+ let server = null;
23
+ try {
24
+ if (!releaseRoot || !releaseId || !contentDigest)
25
+ throw new Error(PRODUCT_TEXT.diagnostic("supervisor must be launched from a verified immutable release"));
26
+ stage = "release-certification";
27
+ const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases"));
28
+ stage = "immutable-root";
29
+ await assertImmutableExecutionRoot(release, paths.dataDir);
30
+ // Protocol: one endpoint per cohort: a superseded cohort keeps serving what it already has while the
31
+ // installed release listens on its own address.
32
+ paths = { ...productPaths, ...resolveCohortEndpoint(productPaths, release.releaseId) };
33
+ stage = "storage";
34
+ const bootNonce = randomUUID();
35
+ const store = new ControlStore(paths.databasePath, bootNonce);
36
+ const cohortState = new CohortStateStore(paths.dataDir);
37
+ server = new SupervisorServer(store, paths, release, bootNonce, undefined, undefined, undefined, async () => (await cohortState.read()).references.active);
38
+ stage = "endpoint-listen";
39
+ await server.listen();
40
+ if (resultPath && attemptId)
41
+ await publishSupervisorStartupResult(resultPath, supervisorStartupReady(attemptId, release.releaseId));
42
+ log(`supervisor ${server.id} listening at ${paths.endpoint} (pid ${process.pid})`);
43
+ let closing = false;
44
+ const close = async (signal) => {
45
+ if (closing)
46
+ return;
47
+ closing = true;
48
+ log(`received ${signal}; stopping supervised process tree`);
49
+ await server?.close(true);
50
+ process.exitCode = 0;
51
+ };
52
+ process.on("SIGINT", () => void close("SIGINT"));
53
+ process.on("SIGTERM", () => void close("SIGTERM"));
54
+ process.on("uncaughtException", error => {
55
+ log(`uncaught exception: ${error.stack ?? error.message}`);
56
+ process.exitCode = 1;
57
+ });
58
+ process.on("unhandledRejection", error => log(`unhandled rejection: ${String(error)}`));
59
+ }
60
+ catch (error) {
61
+ await server?.close(true).catch(() => undefined);
62
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
63
+ log(`startup failure at ${stage}: ${message}`);
64
+ if (resultPath && attemptId && releaseId) {
65
+ await publishSupervisorStartupResult(resultPath, supervisorStartupFailure(error, attemptId, releaseId, stage)).catch(() => undefined);
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+ function startupAttempt(arguments_) {
71
+ if (arguments_.length === 0)
72
+ return null;
73
+ if (arguments_.length !== 2 || arguments_[0] !== "--startup-attempt" || !arguments_[1]) {
74
+ throw new Error(PRODUCT_TEXT.diagnostic("supervisor startup arguments are invalid"));
75
+ }
76
+ return arguments_[1];
47
77
  }
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, rename, rm, writeFile } from "node:fs/promises";
2
+ import { chmod, lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { createConnection, createServer } from "node:net";
4
4
  import { platform } from "node:os";
5
+ import { dirname } from "node:path";
5
6
  import { assertLaunchInstanceOutcome, assertNativeProcessIdentity, assertProcessContainmentIdentity, } from "../lifecycle/index.js";
6
7
  import { encodeFrame, isCommandMessage, isControlHello, LineFrameDecoder, localControlHello, MAX_CONTROL_FRAME_BYTES, negotiateControlFeatures } from "../protocol/index.js";
7
8
  import { processIsAlive } from "../release/index.js";
@@ -71,6 +72,8 @@ export class SupervisorServer {
71
72
  await mkdir(this.paths.runtimeDir, { recursive: true, mode: 0o700 });
72
73
  await mkdir(this.paths.endpointsDir, { recursive: true, mode: 0o700 });
73
74
  if (platform() !== "win32") {
75
+ if (this.paths.endpointDirectory)
76
+ await ensureManagedEndpointDirectory(dirname(this.paths.endpoint));
74
77
  if (await endpointIsLive(this.paths.endpoint))
75
78
  throw new Error(PRODUCT_TEXT.diagnostic(`supervisor already owns ${this.paths.endpoint}`));
76
79
  await rm(this.paths.endpoint, { force: true });
@@ -489,6 +492,16 @@ function sameContainmentIdentity(left, right) {
489
492
  function isMessageType(value, type) {
490
493
  return typeof value === "object" && value !== null && "type" in value && value.type === type;
491
494
  }
495
+ async function ensureManagedEndpointDirectory(path) {
496
+ await mkdir(path, { recursive: true, mode: 0o700 });
497
+ const metadata = await lstat(path);
498
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
499
+ throw new Error(`managed endpoint path is not a directory: ${path}`);
500
+ const currentUser = process.getuid?.();
501
+ if (currentUser !== undefined && metadata.uid !== currentUser)
502
+ throw new Error(`managed endpoint directory has another owner: ${path}`);
503
+ await chmod(path, 0o700);
504
+ }
492
505
  async function endpointIsLive(endpoint) {
493
506
  return await new Promise(resolve => {
494
507
  const socket = createConnection(endpoint);