@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
@@ -0,0 +1,136 @@
1
+ /** The contract this build emits; supersedes the pre-cutover key set it still accepts. */
2
+ export const PRIVATE_LAUNCH_CONTRACT = "neutral-launch-v1";
3
+ export const PRIVATE_ENVIRONMENT = Object.freeze({
4
+ releaseRoot: "LAUNCH_CONTEXT_RELEASE_ROOT",
5
+ releaseId: "LAUNCH_CONTEXT_RELEASE_ID",
6
+ releaseDigest: "LAUNCH_CONTEXT_RELEASE_DIGEST",
7
+ releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS",
8
+ launchProfile: "LAUNCH_CONTEXT_PROFILE",
9
+ immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
10
+ });
11
+ /**
12
+ * Keys published by releases that predate the neutral cutover. They are read when
13
+ * an older launcher hands this build a process, and written when this build
14
+ * starts a release that declares no contract of its own, so an upgrade never
15
+ * depends on both sides having been rebuilt at the same moment.
16
+ */
17
+ export const SUPERSEDED_ENVIRONMENT = Object.freeze({
18
+ releaseRoot: "A1_RELEASE_ROOT",
19
+ releaseId: "A1_RELEASE_ID",
20
+ releaseDigest: "A1_RELEASE_DIGEST",
21
+ releaseLayers: "A1_RELEASE_LAYERS",
22
+ launchProfile: "A1_LAUNCH_PROFILE",
23
+ immutableWarmup: "A1_IMMUTABLE_WARMUP",
24
+ });
25
+ const PRIVATE_ENTRIES = Object.entries(PRIVATE_ENVIRONMENT);
26
+ const SUPERSEDED_ENTRIES = Object.entries(SUPERSEDED_ENVIRONMENT);
27
+ const PRIVATE_KEYS = new Set([...Object.values(PRIVATE_ENVIRONMENT), ...Object.values(SUPERSEDED_ENVIRONMENT)]);
28
+ const LOGICAL_KEYS = new Map(PRIVATE_ENTRIES.map(([logical, key]) => [key, logical]));
29
+ const SUPERSEDED_LOGICAL_KEYS = new Map(SUPERSEDED_ENTRIES.map(([logical, key]) => [key, logical]));
30
+ /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
31
+ export function readLaunchContext(environment, requirement = "optional", platform = process.platform) {
32
+ const values = {};
33
+ const superseded = {};
34
+ // Performance: enumerate names only for Windows casing; never fetch unrelated environment values.
35
+ if (platform === "win32") {
36
+ for (const name of Object.keys(environment)) {
37
+ const upper = name.toUpperCase();
38
+ const logical = LOGICAL_KEYS.get(upper);
39
+ if (logical !== undefined)
40
+ acceptValue(values, logical, environment[name]);
41
+ const supersededLogical = SUPERSEDED_LOGICAL_KEYS.get(upper);
42
+ if (supersededLogical !== undefined)
43
+ acceptValue(superseded, supersededLogical, environment[name]);
44
+ }
45
+ }
46
+ else {
47
+ for (const [logical, key] of PRIVATE_ENTRIES) {
48
+ if (Object.prototype.propertyIsEnumerable.call(environment, key))
49
+ acceptValue(values, logical, environment[key]);
50
+ }
51
+ for (const [logical, key] of SUPERSEDED_ENTRIES) {
52
+ if (Object.prototype.propertyIsEnumerable.call(environment, key))
53
+ acceptValue(superseded, logical, environment[key]);
54
+ }
55
+ }
56
+ // Compatibility: the current key wins wherever both are present, so a launcher that
57
+ // emits this contract is never overruled by an inherited pre-cutover value.
58
+ for (const [logical, value] of Object.entries(superseded))
59
+ if (values[logical] === undefined)
60
+ values[logical] = value;
61
+ if (values.launchProfile !== undefined && values.launchProfile !== "a1" && values.launchProfile !== "pi") {
62
+ throw invalidContext("launchProfile", "unsupported profile");
63
+ }
64
+ if (values.immutableWarmup !== undefined && values.immutableWarmup !== "1")
65
+ throw invalidContext("immutableWarmup", "invalid permission flag");
66
+ if (values.releaseDigest !== undefined && !/^[a-f0-9]{64}$/.test(values.releaseDigest))
67
+ throw invalidContext("releaseDigest", "invalid digest");
68
+ if (requirement === "profile")
69
+ requireFields(values, ["launchProfile"]);
70
+ if (requirement === "release" || requirement === "warmup") {
71
+ requireFields(values, ["releaseRoot", "releaseId", "releaseDigest"]);
72
+ // Compatibility: Windows drops an empty value from a child environment block, so a
73
+ // release with no dependency layer arrives without the key it was given. The
74
+ // layer list is metadata of the release being started, never authority, and an
75
+ // absent list means the same thing the empty string does.
76
+ if (values.releaseLayers === undefined)
77
+ values.releaseLayers = "";
78
+ }
79
+ if (requirement === "warmup")
80
+ requireFields(values, ["immutableWarmup"]);
81
+ return Object.freeze(values);
82
+ }
83
+ /** The key set a process started for this release must receive. */
84
+ export function launchEnvironmentKeys(contract) {
85
+ return contract === PRIVATE_LAUNCH_CONTRACT ? PRIVATE_ENVIRONMENT : SUPERSEDED_ENVIRONMENT;
86
+ }
87
+ /** Classify metadata by the contract it declares rather than rejecting what it omits. */
88
+ export function launchContractTarget(value) {
89
+ return value.launchContract === PRIVATE_LAUNCH_CONTRACT ? PRIVATE_LAUNCH_CONTRACT : "superseded";
90
+ }
91
+ /** Whether metadata was produced by a build that declares this contract. */
92
+ export function isCurrentLaunchContract(value) {
93
+ return value.launchContract === PRIVATE_LAUNCH_CONTRACT;
94
+ }
95
+ /**
96
+ * Rebuild owned context without inheriting a prior session's private selection. The
97
+ * contract selects which key set is written, so this build can start either its
98
+ * own releases or a retained pre-cutover release it must keep serving.
99
+ */
100
+ export function withLaunchContext(environment, context, platform = process.platform, contract = PRIVATE_LAUNCH_CONTRACT) {
101
+ const result = withoutLaunchContext(environment, platform);
102
+ for (const [logical, key] of Object.entries(launchEnvironmentKeys(contract))) {
103
+ const value = context[logical];
104
+ if (value !== undefined)
105
+ result[key] = value;
106
+ }
107
+ readLaunchContext(result, "optional", platform);
108
+ return result;
109
+ }
110
+ /** Remove both key sets, leaving user and integration settings intact. */
111
+ export function withoutLaunchContext(environment, platform = process.platform) {
112
+ return Object.fromEntries(Object.entries(environment).filter(([key]) => !PRIVATE_KEYS.has(platform === "win32" ? key.toUpperCase() : key)));
113
+ }
114
+ /** Require this build's own contract where nothing older can be accepted, such as recovery capsules. */
115
+ export function assertCurrentLaunchContract(value) {
116
+ if (value.launchContract !== PRIVATE_LAUNCH_CONTRACT) {
117
+ throw new Error("Unsupported private launch contract. Stop existing processes and install the current package directly with npm; review disposable runtime/release state before any manual reset. User settings, sessions, and history must be preserved.");
118
+ }
119
+ }
120
+ function acceptValue(values, logical, value) {
121
+ if (value === undefined)
122
+ return;
123
+ if (values[logical] !== undefined && values[logical] !== value)
124
+ throw invalidContext(logical, "conflicting environment key casing");
125
+ if (typeof value !== "string" || value.includes("\0") || (value.length === 0 && logical !== "releaseLayers"))
126
+ throw invalidContext(logical, "invalid value");
127
+ values[logical] = value;
128
+ }
129
+ function requireFields(values, fields) {
130
+ for (const field of fields)
131
+ if (values[field] === undefined)
132
+ throw invalidContext(field, "required current-contract field is missing");
133
+ }
134
+ function invalidContext(logical, reason) {
135
+ return new Error(`Invalid private launch context (${logical}): ${reason}`);
136
+ }
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { PRODUCT_IDENTITY } from "../../product-identity.js";
2
+ import { PRODUCT_TEXT } from "../../product-identity.js";
3
+ import { readLaunchContext } from "../launch-context/index.js";
3
4
  import { resolve } from "node:path";
4
5
  import { assertLaunchProfileId, sessionSelectionArguments } from "../lifecycle/index.js";
5
6
  import { closeVerifiedContainment, DarwinNativeProcessInspector, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
@@ -20,10 +21,11 @@ export async function runLaunchGuardian(options) {
20
21
  const guardianIdentity = await inspector.observe(process.pid);
21
22
  if (!guardianIdentity)
22
23
  throw diagnosticError("launch guardian cannot verify its own native process identity", "PROCESS_IDENTITY_UNAVAILABLE");
23
- const client = options.control ?? new SupervisorClient(environment[PRODUCT_IDENTITY.environment.releaseId]);
24
+ const context = readLaunchContext(environment);
25
+ const client = options.control ?? new SupervisorClient(context.releaseId);
24
26
  // Invariant: an instance belongs to the cohort that launched it and talks to that cohort's endpoint
25
27
  // for its whole life, whatever release becomes the active one meanwhile.
26
- const releaseId = environment[PRODUCT_IDENTITY.environment.releaseId];
28
+ const releaseId = context.releaseId;
27
29
  const endpoint = releaseId
28
30
  ? resolveCohortEndpoint(paths, releaseId, environment).endpoint
29
31
  : paths.endpoint;
@@ -174,5 +176,5 @@ function exitCode(outcome) {
174
176
  return 1;
175
177
  }
176
178
  function diagnosticError(message, code) {
177
- return Object.assign(new Error(`A1: ${message}`), { code });
179
+ return Object.assign(new Error(`${PRODUCT_TEXT.displayName}: ${message}`), { code });
178
180
  }
@@ -36,7 +36,12 @@ export interface SupervisorStartupAttempt extends SupervisorStartupAttemptIdenti
36
36
  }>;
37
37
  }
38
38
  export declare function startSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<SupervisorStartupAttempt>;
39
- export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease): NodeJS.ProcessEnv;
39
+ /**
40
+ * Hand a release the key set its own build reads. A retained pre-cutover release is
41
+ * still startable this way, which is what lets a launch fall back to the previous
42
+ * version instead of failing while an installation is being replaced.
43
+ */
44
+ export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease, profile?: LaunchProfileId): NodeJS.ProcessEnv;
40
45
  export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number, startup?: SupervisorStartupAttempt): Promise<void>;
41
46
  export declare function readEndpointMetadata(path: string): Promise<SupervisorEndpointMetadata | null>;
42
47
  export declare function probeOwnership(metadata: SupervisorEndpointMetadata): Promise<OwnershipProbe>;
@@ -15,15 +15,15 @@ import { scheduleReleaseCleanup } from "./release-gc.js";
15
15
  import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
16
16
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
17
17
  import { markStartupPhase } from "../startup/index.js";
18
+ import { launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
18
19
  export async function runBootstrap(options) {
19
- const environment = { ...(options.environment ?? process.env) };
20
+ const environment = withLaunchContext(options.environment ?? process.env, { launchProfile: options.launchIntent?.profileId ?? "a1" });
20
21
  await markStartupPhase(environment, "bootstrap-start");
21
22
  const launchProfileId = options.launchIntent?.profileId ?? "a1";
22
23
  assertLaunchProfileId(launchProfileId);
23
24
  const sessionArgs = sessionSelectionArguments(options.launchIntent?.sessionSelection);
24
25
  if (sessionArgs.length > 0 && launchProfileId !== "a1")
25
26
  throw new Error("session selection requires the normal A1 profile");
26
- environment[PRODUCT_IDENTITY.environment.launchProfile] = launchProfileId;
27
27
  const output = options.output ?? process.stderr;
28
28
  const paths = resolveProductPaths(environment);
29
29
  await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
@@ -88,7 +88,20 @@ export async function runBootstrap(options) {
88
88
  return await launchUi(retained, environment, sessionArgs);
89
89
  }
90
90
  }
91
- const candidate = await materializeRelease(options.packageRoot, paths.dataDir);
91
+ // Concurrency: an installation being replaced is not a reason to refuse a launch. While
92
+ // npm is rewriting the package tree the candidate cannot be read consistently, so a
93
+ // launch that cannot take the installed payload starts the retained active release
94
+ // instead and leaves activation to the launch that follows the replacement.
95
+ let candidate;
96
+ try {
97
+ candidate = await materializeRelease(options.packageRoot, paths.dataDir);
98
+ }
99
+ catch (error) {
100
+ const fallback = await launchRetainedActive(state, paths, environment, sessionArgs, output);
101
+ if (fallback === null)
102
+ throw error;
103
+ return fallback;
104
+ }
92
105
  await stateStore.recordCandidate(candidate);
93
106
  state = await stateStore.read();
94
107
  if (!state.references.active) {
@@ -183,6 +196,34 @@ export async function runBootstrap(options) {
183
196
  await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, selected.releaseId, environment).endpointMetadataPath, selected, 8_000, startup);
184
197
  return await launchUi(selected, environment, sessionArgs);
185
198
  }
199
+ /**
200
+ * Start the retained active cohort when the installed payload cannot be taken. The
201
+ * release already carries certified evidence, so this reuses it rather than
202
+ * certifying anything new, and reports no selection when there is nothing safe
203
+ * to fall back to.
204
+ */
205
+ async function launchRetainedActive(state, paths, environment, sessionArgs, output) {
206
+ const activeId = state.references.active;
207
+ const active = activeId === null ? undefined : state.releases[activeId];
208
+ if (!active || active.approval !== "approved")
209
+ return null;
210
+ try {
211
+ const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
212
+ const retainedPaths = resolveCohortEndpoint(paths, retained.releaseId, environment);
213
+ const endpoint = await readEndpointMetadata(retainedPaths.endpointMetadataPath);
214
+ if (!endpoint || await probeOwnership(endpoint) !== "live-verified") {
215
+ if (endpoint)
216
+ await removeEndpointArtifacts(retainedPaths.endpointMetadataPath, retainedPaths.endpoint);
217
+ const startup = await startSupervisor(retained, environment);
218
+ await waitForVerifiedEndpoint(retainedPaths.endpointMetadataPath, retained, 8_000, startup);
219
+ }
220
+ output.write(`${PRODUCT_TEXT.diagnostic(`installation is being replaced; starting the retained release ${retained.packageVersion}`)}\n`);
221
+ return await launchUi(retained, environment, sessionArgs);
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
186
227
  async function readInstalledVersion(packageRoot) {
187
228
  const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
188
229
  if (typeof manifest.version !== "string" || manifest.version.length === 0) {
@@ -230,22 +271,27 @@ async function launchUi(release, environment, sessionArgs) {
230
271
  const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
231
272
  return await new Promise((resolvePromise, rejectPromise) => {
232
273
  const child = spawn(process.execPath, [entry, ...sessionArgs], {
233
- env: releaseEnvironment(environment, release),
274
+ env: releaseEnvironment(environment, release, readLaunchContext(environment, "profile").launchProfile),
234
275
  stdio: "inherit",
235
276
  windowsHide: false,
236
277
  });
237
278
  child.once("error", rejectPromise);
238
- child.once("close", (code, signal) => resolvePromise(restoreAfterOwnedExit(environment[PRODUCT_IDENTITY.environment.launchProfile] === "a1", code, signal)));
279
+ child.once("close", (code, signal) => resolvePromise(restoreAfterOwnedExit(readLaunchContext(environment).launchProfile === "a1", code, signal)));
239
280
  });
240
281
  }
241
- export function releaseEnvironment(environment, release) {
242
- return {
243
- ...environment,
244
- [PRODUCT_IDENTITY.environment.releaseId]: release.releaseId,
245
- [PRODUCT_IDENTITY.environment.releaseLayers]: (release.dependencyLayers ?? []).map(layer => layer.layerId).join(","),
246
- [PRODUCT_IDENTITY.environment.releaseRoot]: release.releaseRoot,
247
- [PRODUCT_IDENTITY.environment.releaseDigest]: release.contentDigest,
248
- };
282
+ /**
283
+ * Hand a release the key set its own build reads. A retained pre-cutover release is
284
+ * still startable this way, which is what lets a launch fall back to the previous
285
+ * version instead of failing while an installation is being replaced.
286
+ */
287
+ export function releaseEnvironment(environment, release, profile) {
288
+ return withLaunchContext(environment, {
289
+ releaseId: release.releaseId,
290
+ releaseLayers: (release.dependencyLayers ?? []).map(layer => layer.layerId).join(","),
291
+ releaseRoot: release.releaseRoot,
292
+ releaseDigest: release.contentDigest,
293
+ ...(profile === undefined ? {} : { launchProfile: profile }),
294
+ }, process.platform, launchContractTarget(release));
249
295
  }
250
296
  export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup) {
251
297
  const deadline = Date.now() + timeoutMs;
@@ -6,6 +6,7 @@ export interface ReleaseRecord {
6
6
  readonly releaseRoot: string;
7
7
  readonly packageVersion: string;
8
8
  readonly contentDigest: string;
9
+ readonly launchContract?: string;
9
10
  readonly approval: ReleaseApproval;
10
11
  readonly materializedAt: string;
11
12
  readonly certifiedAt: string | null;
@@ -89,6 +89,7 @@ export class CohortStateStore {
89
89
  releaseRoot: release.releaseRoot,
90
90
  packageVersion: release.packageVersion,
91
91
  contentDigest: release.contentDigest,
92
+ ...(release.launchContract === undefined ? {} : { launchContract: release.launchContract }),
92
93
  approval: "candidate",
93
94
  materializedAt: new Date().toISOString(),
94
95
  certifiedAt: null,
@@ -0,0 +1,7 @@
1
+ export interface DependencyCertificationProtection {
2
+ readonly referenced: Set<string>;
3
+ readonly legacyRequired: Set<string>;
4
+ readonly uncertain: boolean;
5
+ }
6
+ /** Inspect retained and not-yet-collected release metadata, never dependency payload bytes. */
7
+ export declare function dependencyCertificationProtection(dataDir: string, retainedReleaseIds?: readonly string[]): Promise<DependencyCertificationProtection>;
@@ -0,0 +1,88 @@
1
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
2
+ import { basename, resolve } from "node:path";
3
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ import { dependencyLayerCertificationPath } from "./dependency-certification.js";
5
+ import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
6
+ import { readRestartCertifiedRelease } from "./restart-certification.js";
7
+ /** Inspect retained and not-yet-collected release metadata, never dependency payload bytes. */
8
+ export async function dependencyCertificationProtection(dataDir, retainedReleaseIds = []) {
9
+ const referenced = new Set();
10
+ const legacyRequired = new Set();
11
+ const canonicalData = await realpath(dataDir);
12
+ const releasesRoot = resolve(canonicalData, "releases");
13
+ const roots = [];
14
+ let uncertain = false;
15
+ const rootMetadata = await lstat(releasesRoot);
16
+ if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink())
17
+ return { referenced, legacyRequired, uncertain: true };
18
+ const entries = await readdir(releasesRoot, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ if (entry.isSymbolicLink()) {
21
+ uncertain = true;
22
+ continue;
23
+ }
24
+ if (!entry.isDirectory() || entry.name.startsWith(".candidate-"))
25
+ continue;
26
+ if (entry.name === ".trash") {
27
+ for (const trash of await readdir(resolve(releasesRoot, entry.name), { withFileTypes: true })) {
28
+ if (trash.isSymbolicLink())
29
+ uncertain = true;
30
+ else if (trash.isDirectory() && !trash.name.startsWith(".candidate"))
31
+ roots.push(resolve(releasesRoot, entry.name, trash.name));
32
+ }
33
+ }
34
+ else
35
+ roots.push(resolve(releasesRoot, entry.name));
36
+ }
37
+ const discoveredIds = new Set(roots.map(root => basename(root).split("--", 1)[0]));
38
+ if (retainedReleaseIds.some(id => !discoveredIds.has(id)))
39
+ uncertain = true;
40
+ for (const root of roots) {
41
+ try {
42
+ const manifestPath = resolve(root, RELEASE_MANIFEST_FILENAME);
43
+ const metadata = await lstat(manifestPath);
44
+ if (!metadata.isFile() || metadata.isSymbolicLink())
45
+ throw new Error("release manifest is not a regular file");
46
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
47
+ if (!manifest || !/^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/.test(manifest.releaseId)
48
+ || (manifest.dependencyLayers !== undefined && !Array.isArray(manifest.dependencyLayers)))
49
+ throw new Error("invalid retained release manifest");
50
+ const seal = await canonicalConsumerSeal(canonicalData, manifest.releaseId, manifest.contentDigest);
51
+ for (const layer of manifest.dependencyLayers ?? []) {
52
+ if (!layer || !/^dependencies-[a-f0-9]{32}$/.test(layer.layerId))
53
+ throw new Error("invalid retained dependency reference");
54
+ referenced.add(layer.layerId);
55
+ const evidence = seal?.dependencyLayers.filter(item => item.layerId === layer.layerId);
56
+ if (evidence?.length !== 1 || evidence[0].contentDigest !== layer.contentDigest
57
+ || evidence[0].certification?.path !== dependencyLayerCertificationPath(canonicalData, layer.layerId)) {
58
+ // Compatibility: an absent, legacy, or uncertain seal cannot prove canonical-only use.
59
+ legacyRequired.add(layer.layerId);
60
+ }
61
+ }
62
+ }
63
+ catch {
64
+ // Security: an unreadable retained manifest might reference any layer; do not guess ownership.
65
+ uncertain = true;
66
+ }
67
+ }
68
+ return { referenced, legacyRequired, uncertain };
69
+ }
70
+ async function canonicalConsumerSeal(dataDir, releaseId, contentDigest) {
71
+ try {
72
+ const path = resolve(dataDir, `certification-${releaseId}.json`);
73
+ const metadata = await lstat(path);
74
+ if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o222) !== 0)
75
+ return null;
76
+ const document = JSON.parse(await readFile(path, "utf8"));
77
+ if (document.schema !== PRODUCT_IDENTITY.evidence.releaseCertificationSchema || document.releaseId !== releaseId
78
+ || document.contentDigest !== contentDigest || !document.restartSeal)
79
+ return null;
80
+ // Security: path claims alone do not prove a canonical-only consumer. Reuse the bounded
81
+ // restart validator so stale, unsupported, or tampered seals conservatively retain legacy files.
82
+ await readRestartCertifiedRelease({ releaseId, contentDigest, releaseRoot: resolve(dataDir, "releases", releaseId) }, dataDir);
83
+ return document.restartSeal;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
@@ -0,0 +1,24 @@
1
+ export declare const DEPENDENCY_CERTIFICATIONS_DIRECTORY = "dependency-certifications";
2
+ type LayerIdentity = {
3
+ readonly layerId: string;
4
+ readonly contentDigest: string;
5
+ };
6
+ export interface ReadDependencyCertificationOptions {
7
+ /** Only authenticated parent-started supervisors may upgrade missing platform evidence. */
8
+ readonly allowLegacyParentCertification?: boolean;
9
+ /** Cleanup must validate canonical evidence without migrating or falling back. */
10
+ readonly canonicalOnly?: boolean;
11
+ }
12
+ /** Derive the canonical record path without accepting path components as layer identities. */
13
+ export declare function dependencyLayerCertificationPath(dataDir: string, layerId: string): string;
14
+ /** Keep the historical filename solely for compatibility reads and managed cleanup. */
15
+ export declare function legacyDependencyLayerCertificationPath(dataDir: string, layerId: string): string;
16
+ /** Validate the dedicated directory before reading, creating, or deleting any contained record. */
17
+ export declare function dependencyCertificationDirectory(dataDir: string, create?: boolean): Promise<string | null>;
18
+ /** Resolve only direct regular managed records; callers never follow directory or record links. */
19
+ export declare function managedDependencyCertificationPath(dataDir: string, layerId: string, legacy?: boolean): Promise<string | null>;
20
+ /** Prefer canonical evidence; copy validated legacy evidence without changing its sealed source. */
21
+ export declare function readDependencyCertification(dataDir: string, identity: LayerIdentity, options?: ReadDependencyCertificationOptions): Promise<void>;
22
+ /** Publish certification after materialization/full verification, preserving an already-valid winner. */
23
+ export declare function writeDependencyCertification(dataDir: string, identity: LayerIdentity): Promise<void>;
24
+ export {};
@@ -0,0 +1,208 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
+ import { immutablePlatformPolicy } from "./immutable-platform.js";
6
+ export const DEPENDENCY_CERTIFICATIONS_DIRECTORY = "dependency-certifications";
7
+ /** Derive the canonical record path without accepting path components as layer identities. */
8
+ export function dependencyLayerCertificationPath(dataDir, layerId) {
9
+ assertLayerId(layerId);
10
+ return resolve(dataDir, DEPENDENCY_CERTIFICATIONS_DIRECTORY, `${layerId}.json`);
11
+ }
12
+ /** Keep the historical filename solely for compatibility reads and managed cleanup. */
13
+ export function legacyDependencyLayerCertificationPath(dataDir, layerId) {
14
+ assertLayerId(layerId);
15
+ return resolve(dataDir, `dependency-layer-certification-${layerId}.json`);
16
+ }
17
+ /** Validate the dedicated directory before reading, creating, or deleting any contained record. */
18
+ export async function dependencyCertificationDirectory(dataDir, create = false) {
19
+ const canonicalData = await realpath(dataDir);
20
+ const directory = resolve(canonicalData, DEPENDENCY_CERTIFICATIONS_DIRECTORY);
21
+ if (create)
22
+ await mkdir(directory, { mode: 0o700 }).catch(error => {
23
+ if (!hasCode(error, "EEXIST"))
24
+ throw error;
25
+ });
26
+ const metadata = await lstat(directory).catch(missingOrThrow);
27
+ if (metadata === null)
28
+ return null;
29
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(directory) !== directory) {
30
+ throw new Error("dependency certification directory is not a managed non-link directory");
31
+ }
32
+ return directory;
33
+ }
34
+ /** Resolve only direct regular managed records; callers never follow directory or record links. */
35
+ export async function managedDependencyCertificationPath(dataDir, layerId, legacy = false) {
36
+ const canonicalData = await realpath(dataDir);
37
+ const path = legacy ? legacyDependencyLayerCertificationPath(canonicalData, layerId) : dependencyLayerCertificationPath(canonicalData, layerId);
38
+ if (!legacy && await dependencyCertificationDirectory(canonicalData) === null)
39
+ return null;
40
+ const metadata = await lstat(path).catch(missingOrThrow);
41
+ if (metadata === null)
42
+ return null;
43
+ if (!metadata.isFile() || metadata.isSymbolicLink() || await realpath(path) !== path) {
44
+ throw new Error(`dependency certification is not a managed regular file: ${path}`);
45
+ }
46
+ return path;
47
+ }
48
+ /** Prefer canonical evidence; copy validated legacy evidence without changing its sealed source. */
49
+ export async function readDependencyCertification(dataDir, identity, options = {}) {
50
+ const canonical = await managedDependencyCertificationPath(dataDir, identity.layerId);
51
+ const source = canonical ?? (options.canonicalOnly ? null : await managedDependencyCertificationPath(dataDir, identity.layerId, true));
52
+ if (source === null)
53
+ throw new Error(`dependency layer certification is absent: ${identity.layerId}`);
54
+ const document = JSON.parse(await readFile(source, "utf8"));
55
+ const legacyPlatform = validateCertification(document, identity, options.allowLegacyParentCertification === true);
56
+ if (!legacyPlatform && ((await lstat(source)).mode & 0o222) !== 0)
57
+ throw new Error("dependency certification is writable");
58
+ if (legacyPlatform) {
59
+ // Compatibility: only the authenticated parent path may replace older platform-less evidence.
60
+ await writeDependencyCertification(dataDir, identity);
61
+ }
62
+ else if (canonical === null) {
63
+ await publishCertification(dataDir, identity, document, false);
64
+ }
65
+ }
66
+ /** Publish certification after materialization/full verification, preserving an already-valid winner. */
67
+ export async function writeDependencyCertification(dataDir, identity) {
68
+ await publishCertification(dataDir, identity, {
69
+ schema: PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema,
70
+ layerId: identity.layerId,
71
+ contentDigest: identity.contentDigest,
72
+ platform: process.platform,
73
+ platformPolicy: immutablePlatformPolicy(),
74
+ certifiedAt: new Date().toISOString(),
75
+ }, true);
76
+ }
77
+ async function publishCertification(dataDir, identity, document, replaceInvalid) {
78
+ const directory = (await dependencyCertificationDirectory(dataDir, true));
79
+ const path = dependencyLayerCertificationPath(dirname(directory), identity.layerId);
80
+ const lockPath = `${path}.lock`;
81
+ const releaseLock = await acquirePublicationLock(lockPath);
82
+ try {
83
+ await publishLockedCertification(dataDir, identity, document, replaceInvalid, path);
84
+ }
85
+ finally {
86
+ await releaseLock();
87
+ }
88
+ }
89
+ async function publishLockedCertification(dataDir, identity, document, replaceInvalid, path) {
90
+ const existing = await managedDependencyCertificationPath(dataDir, identity.layerId);
91
+ if (existing !== null) {
92
+ const source = await readFile(existing, "utf8");
93
+ const metadata = await lstat(existing);
94
+ try {
95
+ validateCertification(JSON.parse(source), identity);
96
+ // Invariant: full verification restores writable evidence; ordinary valid reuse is read-only.
97
+ if ((metadata.mode & 0o222) === 0)
98
+ return;
99
+ if (!replaceInvalid)
100
+ throw new Error("dependency certification is writable");
101
+ }
102
+ catch (error) {
103
+ if (!replaceInvalid)
104
+ throw error;
105
+ }
106
+ await rm(existing, { force: true });
107
+ }
108
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
109
+ try {
110
+ await writeFile(temporary, JSON.stringify(document, null, 2), { flag: "wx", mode: 0o400 });
111
+ await chmod(temporary, 0o400);
112
+ await dependencyCertificationDirectory(dataDir);
113
+ // Concurrency: the per-layer lease protects the winner check and atomic publication together.
114
+ // Rename publishes final read-only metadata; no later temporary unlink changes sealed evidence.
115
+ await rename(temporary, path);
116
+ }
117
+ finally {
118
+ await rm(temporary, { force: true });
119
+ }
120
+ }
121
+ async function acquirePublicationLock(path) {
122
+ const token = randomUUID();
123
+ const candidate = `${path}.candidate-${token}`;
124
+ await mkdir(candidate, { mode: 0o700 });
125
+ try {
126
+ await writeFile(resolve(candidate, "owner.json"), JSON.stringify({ pid: process.pid, token }), { flag: "wx", mode: 0o600 });
127
+ const deadline = Date.now() + 5_000;
128
+ while (true) {
129
+ try {
130
+ // Concurrency: a nonempty directory cannot replace another nonempty directory on either
131
+ // platform. Publishing the complete owner atomically also avoids partially written leases.
132
+ await rename(candidate, path);
133
+ return async () => {
134
+ const released = `${path}.released-${token}`;
135
+ await rename(path, released);
136
+ await rm(released, { recursive: true, force: true });
137
+ };
138
+ }
139
+ catch (error) {
140
+ if (!["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].some(code => hasCode(error, code)))
141
+ throw error;
142
+ const metadata = await lstat(path).catch(missingOrThrow);
143
+ if (metadata !== null) {
144
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
145
+ throw new Error("dependency certification publication lock is not a managed directory");
146
+ const ownerPath = resolve(path, "owner.json");
147
+ const ownerMetadata = await lstat(ownerPath).catch(missingOrThrow);
148
+ if (ownerMetadata !== null) {
149
+ if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink())
150
+ throw new Error("dependency certification publication owner is not a regular file");
151
+ const owner = await readFile(ownerPath, "utf8").then(source => JSON.parse(source)).catch(error => {
152
+ if (hasCode(error, "ENOENT"))
153
+ return null;
154
+ throw error;
155
+ });
156
+ if (owner !== null && Number.isSafeInteger(owner.pid) && owner.pid > 0 && /^[a-f0-9-]{36}$/.test(owner.token)) {
157
+ let abandoned = false;
158
+ try {
159
+ process.kill(owner.pid, 0);
160
+ }
161
+ catch (probeError) {
162
+ abandoned = hasCode(probeError, "ESRCH");
163
+ }
164
+ if (abandoned) {
165
+ // Concurrency: retain this generation's nonempty tombstone. A delayed second reclaimer
166
+ // cannot overwrite it and accidentally steal a newly acquired replacement lease.
167
+ await rename(path, `${path}.abandoned-${owner.token}`).catch(error => {
168
+ if (!["ENOENT", "EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].some(code => hasCode(error, code)))
169
+ throw error;
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+ if (Date.now() >= deadline)
176
+ throw new Error("dependency certification publication is busy");
177
+ await new Promise(resolvePromise => setTimeout(resolvePromise, 20));
178
+ }
179
+ }
180
+ }
181
+ finally {
182
+ await rm(candidate, { recursive: true, force: true });
183
+ }
184
+ }
185
+ function validateCertification(value, identity, allowLegacyPlatform = false) {
186
+ if (!value || typeof value !== "object" || Array.isArray(value))
187
+ throw new Error(`dependency layer certification differs from manifest: ${identity.layerId}`);
188
+ const certification = value;
189
+ const identityMatches = certification.schema === PRODUCT_IDENTITY.evidence.dependencyLayerCertificationSchema
190
+ && certification.layerId === identity.layerId && certification.contentDigest === identity.contentDigest;
191
+ const currentPlatform = certification.platform === process.platform && certification.platformPolicy === immutablePlatformPolicy();
192
+ const legacyPlatform = allowLegacyPlatform && certification.platform === undefined && certification.platformPolicy === undefined;
193
+ if (!identityMatches || (!currentPlatform && !legacyPlatform))
194
+ throw new Error(`dependency layer certification differs from manifest: ${identity.layerId}`);
195
+ return legacyPlatform;
196
+ }
197
+ function assertLayerId(layerId) {
198
+ if (!/^dependencies-[a-f0-9]{32}$/.test(layerId))
199
+ throw new Error(`invalid dependency layer identity: ${layerId}`);
200
+ }
201
+ function hasCode(error, code) {
202
+ return error instanceof Error && "code" in error && error.code === code;
203
+ }
204
+ function missingOrThrow(error) {
205
+ if (hasCode(error, "ENOENT"))
206
+ return null;
207
+ throw error;
208
+ }