@timurproko/a1 0.1.8-dev.322 → 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 (52) hide show
  1. package/dist/cli/dispatch.js +1 -1
  2. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  3. package/dist/features/owned-ui/settings-app.js +1 -1
  4. package/dist/features/prompt-history/service.d.ts +31 -1
  5. package/dist/features/prompt-history/service.js +294 -129
  6. package/dist/features/prompt-history/store.d.ts +2 -1
  7. package/dist/features/prompt-history/store.js +17 -4
  8. package/dist/features/prompt-history/worker.js +6 -3
  9. package/dist/foundation/launch-context/index.d.ts +36 -6
  10. package/dist/foundation/launch-context/index.js +64 -11
  11. package/dist/foundation/launch-guardian/main.js +2 -1
  12. package/dist/foundation/release/bootstrap.d.ts +5 -0
  13. package/dist/foundation/release/bootstrap.js +49 -9
  14. package/dist/foundation/release/cohort-state.js +0 -3
  15. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  16. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  17. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  18. package/dist/foundation/release/dependency-certification.js +208 -0
  19. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  20. package/dist/foundation/release/dependency-layer.js +6 -37
  21. package/dist/foundation/release/release-gc.js +72 -44
  22. package/dist/foundation/release/release-store.d.ts +2 -0
  23. package/dist/foundation/release/release-store.js +4 -3
  24. package/dist/foundation/release/restart-certification.js +16 -5
  25. package/dist/foundation/release/update.js +1 -6
  26. package/dist/foundation/release/warmup.js +16 -5
  27. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  28. package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
  29. package/dist/integrations/pi/engine/adapter.d.ts +20 -0
  30. package/dist/integrations/pi/engine/adapter.js +223 -87
  31. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  32. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  33. package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
  34. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  35. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  36. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
  37. package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
  38. package/dist/integrations/pi/session-ui/session-shell.js +9 -6
  39. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
  40. package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
  41. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  42. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  43. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
  44. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
  45. package/dist/native/darwin-arm64/manifest.json +1 -1
  46. package/dist/native/linux-x64/manifest.json +1 -1
  47. package/dist/native/win32-x64/manifest.json +2 -2
  48. package/dist/native/win32-x64/process-guardian.exe +0 -0
  49. package/dist/product-identity.json +1 -1
  50. package/docs/architecture/internal-naming.md +5 -3
  51. package/docs/manual-code-streaming-cleanup.md +88 -0
  52. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { parentPort, workerData } from "node:worker_threads";
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
- import { PromptHistoryStore, classifyHistoryError } from "./store.js";
3
+ import { HistoryStorageError, PromptHistoryStore, classifyHistoryError } from "./store.js";
4
4
  const port = parentPort;
5
5
  if (port !== null) {
6
6
  let store;
@@ -22,7 +22,8 @@ if (port !== null) {
22
22
  port.postMessage({ id: request.id, ok: true, value });
23
23
  }
24
24
  catch (error) {
25
- port.postMessage({ id: request.id, ok: false, code: classifyHistoryError(error) });
25
+ port.postMessage({ id: request.id, ok: false, code: classifyHistoryError(error),
26
+ certainty: error instanceof HistoryStorageError ? error.certainty : "unknown" });
26
27
  }
27
28
  });
28
29
  });
@@ -34,7 +35,9 @@ async function retry(operation) {
34
35
  return operation();
35
36
  }
36
37
  catch (error) {
37
- if (classifyHistoryError(error) !== "busy" || performance.now() >= deadline)
38
+ if (classifyHistoryError(error) !== "busy"
39
+ || error instanceof HistoryStorageError && error.certainty === "unknown"
40
+ || performance.now() >= deadline)
38
41
  throw error;
39
42
  await delay(25);
40
43
  }
@@ -1,4 +1,4 @@
1
- /** The one supported private process contract; public settings remain product identity data. */
1
+ /** The contract this build emits; supersedes the pre-cutover key set it still accepts. */
2
2
  export declare const PRIVATE_LAUNCH_CONTRACT = "neutral-launch-v1";
3
3
  export declare const PRIVATE_ENVIRONMENT: Readonly<{
4
4
  releaseRoot: "LAUNCH_CONTEXT_RELEASE_ROOT";
@@ -8,6 +8,22 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE";
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
10
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 declare const SUPERSEDED_ENVIRONMENT: Readonly<{
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
+ /** A release either declares the current contract or predates contract declaration. */
26
+ export type LaunchContractTarget = typeof PRIVATE_LAUNCH_CONTRACT | "superseded";
11
27
  export interface LaunchContext {
12
28
  readonly releaseRoot?: string;
13
29
  readonly releaseId?: string;
@@ -17,13 +33,27 @@ export interface LaunchContext {
17
33
  readonly immutableWarmup?: "1";
18
34
  }
19
35
  export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
20
- /** Read current private keys only, with entry-specific required fields and bounded diagnostics. */
36
+ /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
21
37
  export declare function readLaunchContext(environment: NodeJS.ProcessEnv, requirement?: LaunchContextRequirement, platform?: NodeJS.Platform): LaunchContext;
22
- /** Rebuild owned context without inheriting a prior session's private selection. */
23
- export declare function withLaunchContext(environment: NodeJS.ProcessEnv, context: LaunchContext, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
24
- /** Remove only this implementation's private keys, leaving user and integration settings intact. */
38
+ /** The key set a process started for this release must receive. */
39
+ export declare function launchEnvironmentKeys(contract: LaunchContractTarget): Readonly<Record<string, string>>;
40
+ /** Classify metadata by the contract it declares rather than rejecting what it omits. */
41
+ export declare function launchContractTarget(value: {
42
+ readonly launchContract?: unknown;
43
+ }): LaunchContractTarget;
44
+ /** Whether metadata was produced by a build that declares this contract. */
45
+ export declare function isCurrentLaunchContract(value: {
46
+ readonly launchContract?: unknown;
47
+ }): boolean;
48
+ /**
49
+ * Rebuild owned context without inheriting a prior session's private selection. The
50
+ * contract selects which key set is written, so this build can start either its
51
+ * own releases or a retained pre-cutover release it must keep serving.
52
+ */
53
+ export declare function withLaunchContext(environment: NodeJS.ProcessEnv, context: LaunchContext, platform?: NodeJS.Platform, contract?: LaunchContractTarget): NodeJS.ProcessEnv;
54
+ /** Remove both key sets, leaving user and integration settings intact. */
25
55
  export declare function withoutLaunchContext(environment: NodeJS.ProcessEnv, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
26
- /** Reject unsupported target metadata instead of negotiating or rewriting an older contract. */
56
+ /** Require this build's own contract where nothing older can be accepted, such as recovery capsules. */
27
57
  export declare function assertCurrentLaunchContract(value: {
28
58
  readonly launchContract?: unknown;
29
59
  }): void;
@@ -1,4 +1,4 @@
1
- /** The one supported private process contract; public settings remain product identity data. */
1
+ /** The contract this build emits; supersedes the pre-cutover key set it still accepts. */
2
2
  export const PRIVATE_LAUNCH_CONTRACT = "neutral-launch-v1";
3
3
  export const PRIVATE_ENVIRONMENT = Object.freeze({
4
4
  releaseRoot: "LAUNCH_CONTEXT_RELEASE_ROOT",
@@ -8,18 +8,39 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE",
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
10
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
+ });
11
25
  const PRIVATE_ENTRIES = Object.entries(PRIVATE_ENVIRONMENT);
12
- const PRIVATE_KEYS = new Set(Object.values(PRIVATE_ENVIRONMENT));
26
+ const SUPERSEDED_ENTRIES = Object.entries(SUPERSEDED_ENVIRONMENT);
27
+ const PRIVATE_KEYS = new Set([...Object.values(PRIVATE_ENVIRONMENT), ...Object.values(SUPERSEDED_ENVIRONMENT)]);
13
28
  const LOGICAL_KEYS = new Map(PRIVATE_ENTRIES.map(([logical, key]) => [key, logical]));
14
- /** Read current private keys only, with entry-specific required fields and bounded diagnostics. */
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. */
15
31
  export function readLaunchContext(environment, requirement = "optional", platform = process.platform) {
16
32
  const values = {};
33
+ const superseded = {};
17
34
  // Performance: enumerate names only for Windows casing; never fetch unrelated environment values.
18
35
  if (platform === "win32") {
19
36
  for (const name of Object.keys(environment)) {
20
- const logical = LOGICAL_KEYS.get(name.toUpperCase());
37
+ const upper = name.toUpperCase();
38
+ const logical = LOGICAL_KEYS.get(upper);
21
39
  if (logical !== undefined)
22
40
  acceptValue(values, logical, environment[name]);
41
+ const supersededLogical = SUPERSEDED_LOGICAL_KEYS.get(upper);
42
+ if (supersededLogical !== undefined)
43
+ acceptValue(superseded, supersededLogical, environment[name]);
23
44
  }
24
45
  }
25
46
  else {
@@ -27,7 +48,16 @@ export function readLaunchContext(environment, requirement = "optional", platfor
27
48
  if (Object.prototype.propertyIsEnumerable.call(environment, key))
28
49
  acceptValue(values, logical, environment[key]);
29
50
  }
51
+ for (const [logical, key] of SUPERSEDED_ENTRIES) {
52
+ if (Object.prototype.propertyIsEnumerable.call(environment, key))
53
+ acceptValue(superseded, logical, environment[key]);
54
+ }
30
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;
31
61
  if (values.launchProfile !== undefined && values.launchProfile !== "a1" && values.launchProfile !== "pi") {
32
62
  throw invalidContext("launchProfile", "unsupported profile");
33
63
  }
@@ -37,16 +67,39 @@ export function readLaunchContext(environment, requirement = "optional", platfor
37
67
  throw invalidContext("releaseDigest", "invalid digest");
38
68
  if (requirement === "profile")
39
69
  requireFields(values, ["launchProfile"]);
40
- if (requirement === "release" || requirement === "warmup")
41
- requireFields(values, ["releaseRoot", "releaseId", "releaseDigest", "releaseLayers"]);
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
+ }
42
79
  if (requirement === "warmup")
43
80
  requireFields(values, ["immutableWarmup"]);
44
81
  return Object.freeze(values);
45
82
  }
46
- /** Rebuild owned context without inheriting a prior session's private selection. */
47
- export function withLaunchContext(environment, context, platform = process.platform) {
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) {
48
101
  const result = withoutLaunchContext(environment, platform);
49
- for (const [logical, key] of PRIVATE_ENTRIES) {
102
+ for (const [logical, key] of Object.entries(launchEnvironmentKeys(contract))) {
50
103
  const value = context[logical];
51
104
  if (value !== undefined)
52
105
  result[key] = value;
@@ -54,11 +107,11 @@ export function withLaunchContext(environment, context, platform = process.platf
54
107
  readLaunchContext(result, "optional", platform);
55
108
  return result;
56
109
  }
57
- /** Remove only this implementation's private keys, leaving user and integration settings intact. */
110
+ /** Remove both key sets, leaving user and integration settings intact. */
58
111
  export function withoutLaunchContext(environment, platform = process.platform) {
59
112
  return Object.fromEntries(Object.entries(environment).filter(([key]) => !PRIVATE_KEYS.has(platform === "win32" ? key.toUpperCase() : key)));
60
113
  }
61
- /** Reject unsupported target metadata instead of negotiating or rewriting an older contract. */
114
+ /** Require this build's own contract where nothing older can be accepted, such as recovery capsules. */
62
115
  export function assertCurrentLaunchContract(value) {
63
116
  if (value.launchContract !== PRIVATE_LAUNCH_CONTRACT) {
64
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.");
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { PRODUCT_TEXT } from "../../product-identity.js";
2
3
  import { readLaunchContext } from "../launch-context/index.js";
3
4
  import { resolve } from "node:path";
4
5
  import { assertLaunchProfileId, sessionSelectionArguments } from "../lifecycle/index.js";
@@ -175,5 +176,5 @@ function exitCode(outcome) {
175
176
  return 1;
176
177
  }
177
178
  function diagnosticError(message, code) {
178
- return Object.assign(new Error(`A1: ${message}`), { code });
179
+ return Object.assign(new Error(`${PRODUCT_TEXT.displayName}: ${message}`), { code });
179
180
  }
@@ -36,6 +36,11 @@ export interface SupervisorStartupAttempt extends SupervisorStartupAttemptIdenti
36
36
  }>;
37
37
  }
38
38
  export declare function startSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<SupervisorStartupAttempt>;
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
+ */
39
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>;
@@ -15,7 +15,7 @@ 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 { assertCurrentLaunchContract, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
18
+ import { launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
19
19
  export async function runBootstrap(options) {
20
20
  const environment = withLaunchContext(options.environment ?? process.env, { launchProfile: options.launchIntent?.profileId ?? "a1" });
21
21
  await markStartupPhase(environment, "bootstrap-start");
@@ -29,9 +29,6 @@ export async function runBootstrap(options) {
29
29
  await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
30
30
  const stateStore = new CohortStateStore(paths.dataDir);
31
31
  let state = await stateStore.read();
32
- const activeRecord = state.references.active === null ? undefined : state.releases[state.references.active];
33
- if (activeRecord)
34
- assertCurrentLaunchContract(activeRecord);
35
32
  // Invariant: records left by cohorts whose processes are gone say nothing about ownership, and there
36
33
  // can now be several of them. Clearing them first keeps the decision below about what is
37
34
  // actually running.
@@ -91,7 +88,20 @@ export async function runBootstrap(options) {
91
88
  return await launchUi(retained, environment, sessionArgs);
92
89
  }
93
90
  }
94
- 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
+ }
95
105
  await stateStore.recordCandidate(candidate);
96
106
  state = await stateStore.read();
97
107
  if (!state.references.active) {
@@ -186,6 +196,34 @@ export async function runBootstrap(options) {
186
196
  await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, selected.releaseId, environment).endpointMetadataPath, selected, 8_000, startup);
187
197
  return await launchUi(selected, environment, sessionArgs);
188
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
+ }
189
227
  async function readInstalledVersion(packageRoot) {
190
228
  const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
191
229
  if (typeof manifest.version !== "string" || manifest.version.length === 0) {
@@ -194,7 +232,6 @@ async function readInstalledVersion(packageRoot) {
194
232
  return manifest.version;
195
233
  }
196
234
  export async function certifyMaterializedRelease(release, dataDir, verification = {}) {
197
- assertCurrentLaunchContract(release);
198
235
  if (!consumeMaterializationProof(release)) {
199
236
  await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
200
237
  }
@@ -202,7 +239,6 @@ export async function certifyMaterializedRelease(release, dataDir, verification
202
239
  }
203
240
  /** Persist current-format evidence after an authenticated parent has certified the exact release. */
204
241
  export async function recordParentCertifiedRelease(release, dataDir) {
205
- assertCurrentLaunchContract(release);
206
242
  const path = resolve(dataDir, `certification-${release.releaseId}.json`);
207
243
  const restartSeal = await createRestartSeal(release, dataDir);
208
244
  await chmod(path, 0o600).catch(() => { });
@@ -243,15 +279,19 @@ async function launchUi(release, environment, sessionArgs) {
243
279
  child.once("close", (code, signal) => resolvePromise(restoreAfterOwnedExit(readLaunchContext(environment).launchProfile === "a1", code, signal)));
244
280
  });
245
281
  }
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
+ */
246
287
  export function releaseEnvironment(environment, release, profile) {
247
- assertCurrentLaunchContract(release);
248
288
  return withLaunchContext(environment, {
249
289
  releaseId: release.releaseId,
250
290
  releaseLayers: (release.dependencyLayers ?? []).map(layer => layer.layerId).join(","),
251
291
  releaseRoot: release.releaseRoot,
252
292
  releaseDigest: release.contentDigest,
253
293
  ...(profile === undefined ? {} : { launchProfile: profile }),
254
- });
294
+ }, process.platform, launchContractTarget(release));
255
295
  }
256
296
  export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup) {
257
297
  const deadline = Date.now() + timeoutMs;
@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
- import { assertCurrentLaunchContract } from "../launch-context/index.js";
6
5
  export const RELEASE_COHORT_SCHEMA = PRODUCT_IDENTITY.protocol.releaseCohortSchema;
7
6
  const RELEASE_ID_PATTERN = /^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/;
8
7
  /**
@@ -122,7 +121,6 @@ export class CohortStateStore {
122
121
  const release = requiredRelease(current, releaseId);
123
122
  if (release.approval !== "approved")
124
123
  throw new Error(`cannot activate unverified release ${releaseId}`);
125
- assertCurrentLaunchContract(release);
126
124
  const prior = current.references.active;
127
125
  return {
128
126
  ...current,
@@ -150,7 +148,6 @@ export class CohortStateStore {
150
148
  const rollback = requiredRelease(current, rollbackId);
151
149
  if (rollback.approval !== "approved")
152
150
  throw new Error(`cannot roll back to unverified release ${rollbackId}`);
153
- assertCurrentLaunchContract(rollback);
154
151
  return {
155
152
  ...current,
156
153
  references: {
@@ -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 {};