@timurproko/a1 0.1.8-dev.299 → 0.1.8-dev.322

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 (73) 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/composition/owned-ui.js +2 -1
  6. package/dist/features/launch/profile-paths.d.ts +2 -0
  7. package/dist/features/launch/profile-paths.js +11 -0
  8. package/dist/foundation/launch-context/index.d.ts +29 -0
  9. package/dist/foundation/launch-context/index.js +83 -0
  10. package/dist/foundation/launch-guardian/main.js +4 -3
  11. package/dist/foundation/release/bootstrap.d.ts +1 -1
  12. package/dist/foundation/release/bootstrap.js +18 -12
  13. package/dist/foundation/release/cohort-state.d.ts +1 -0
  14. package/dist/foundation/release/cohort-state.js +4 -0
  15. package/dist/foundation/release/release-gc.js +2 -2
  16. package/dist/foundation/release/release-store.js +10 -3
  17. package/dist/foundation/release/release.d.ts +3 -1
  18. package/dist/foundation/release/release.js +7 -3
  19. package/dist/foundation/release/restart-certification.d.ts +1 -0
  20. package/dist/foundation/release/restart-certification.js +5 -0
  21. package/dist/foundation/release/update-recovery.d.ts +2 -0
  22. package/dist/foundation/release/update-recovery.js +4 -0
  23. package/dist/foundation/release/update.js +5 -0
  24. package/dist/foundation/release/warmup.d.ts +1 -1
  25. package/dist/foundation/release/warmup.js +5 -4
  26. package/dist/foundation/startup/startup-runtime.js +13 -11
  27. package/dist/foundation/supervision/main.js +8 -7
  28. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -0
  29. package/dist/integrations/pi/components/owned-editor-ux.js +54 -3
  30. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  31. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  32. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  33. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  34. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  35. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  36. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  37. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  38. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  39. package/dist/integrations/pi/engine/adapter.js +86 -25
  40. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  41. package/dist/integrations/pi/engine/changelog.js +63 -0
  42. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  43. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  44. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  45. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  46. package/dist/integrations/pi/session-ui/prompt-chips.js +37 -13
  47. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +25 -0
  48. package/dist/integrations/pi/session-ui/session-shell-root.js +65 -74
  49. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  50. package/dist/integrations/pi/session-ui/session-shell.js +58 -7
  51. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  52. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  53. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  54. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +2 -2
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/product-identity.d.ts +1 -1
  60. package/dist/product-identity.js +2 -35
  61. package/dist/product-identity.json +0 -22
  62. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  63. package/dist/ui/components/transcript-viewport.js +23 -2
  64. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  65. package/dist/ui/components/visible-hyperlinks.js +1 -0
  66. package/dist/ui/settings/declarations.js +1 -0
  67. package/dist/ui/settings/sections.js +11 -1
  68. package/docs/architecture/boundaries.md +2 -1
  69. package/docs/architecture/internal-naming.md +89 -0
  70. package/docs/architecture/resource-and-data-policy.md +9 -4
  71. package/docs/architecture/toolchain.md +15 -1
  72. package/docs/features/prompt-history.md +39 -11
  73. package/package.json +8 -3
package/README.md CHANGED
@@ -45,13 +45,25 @@ a1 pi # vanilla Pi oracle: ~/.pi/agent
45
45
  Unsupported commands exit quietly without launching anything. The removed
46
46
  `update:<preview>` forms are not aliases; use `update --develop`.
47
47
 
48
+ ## Prompt suggestions
49
+
50
+ Bare A1's `/settings` → Agent → Prompt suggestions controls faint next-prompt
51
+ predictions. It is enabled by default and uses one additional background request
52
+ with the selected model for eligible turns. Disabling it takes effect immediately.
53
+ Tab accepts a suggestion as editable text; Enter submits it afterward. The setting
54
+ remains stored in A1's profile, and `a1 pi` is unchanged.
55
+
48
56
  ## Prompt history
49
57
 
50
58
  Bare A1 recalls recent unique prompts across sessions with Up/Down. History is
51
- stored in a separate profile-local SQLite database under A1's application-data
52
- root, not `~/.a1/agent` or disposable caches. `/settings` → History controls
53
- persistence and the 10-100-entry limit (default 100), effective on next start.
54
- History is unencrypted user text; images are not reattached. See
59
+ stored in a separate profile-local SQLite database at
60
+ `~/.a1/data/history/<profile-id>.sqlite3`, or under `<A1_DATA_DIR>/history` when
61
+ explicitly overridden. Other application data stays in its existing location.
62
+ `/settings` History controls persistence and the 10-100-entry limit (default
63
+ 100), effective on next start. The new default starts fresh: old platform history
64
+ is neither imported nor automatically deleted. Ordinary upgrades and npm
65
+ uninstall/reinstall preserve saved history. History is unencrypted user text;
66
+ images are not reattached. See
55
67
  [prompt history](docs/features/prompt-history.md) for privacy, storage, and removal.
56
68
 
57
69
  ## Extensions
package/bin/guardian.js CHANGED
@@ -3,13 +3,14 @@
3
3
  const startup = await import("../dist/foundation/startup/index.js");
4
4
  startup.enableEnvironmentCompileCache(process.env);
5
5
  await startup.markStartupPhase(process.env, "guardian-start");
6
- const { fileURLToPath } = await import("node:url");
7
6
  const { resolve } = await import("node:path");
8
7
  const { parseSessionSelection } = await import("../dist/foundation/lifecycle/index.js");
9
8
  const { runLaunchGuardian } = await import("../dist/foundation/launch-guardian/index.js");
10
9
 
11
- const releaseRoot = process.env.A1_RELEASE_ROOT ?? fileURLToPath(new URL("..", import.meta.url));
12
- const profileId = process.env.A1_LAUNCH_PROFILE ?? "a1";
10
+ const { readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
11
+ const launchContext = readLaunchContext(process.env, "release");
12
+ const releaseRoot = launchContext.releaseRoot;
13
+ const profileId = readLaunchContext(process.env, "profile").launchProfile;
13
14
 
14
15
  Promise.resolve().then(() => runLaunchGuardian({
15
16
  sessionSelection: parseSessionSelection(process.argv.slice(2)),
package/bin/ui.js CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  const startup = await import("../dist/foundation/startup/index.js");
4
4
  startup.enableEnvironmentCompileCache(process.env);
5
- const profile = process.env.A1_LAUNCH_PROFILE ?? "a1";
5
+ const { readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
6
+ const launchContext = readLaunchContext(process.env, "profile");
7
+ const profile = launchContext.launchProfile;
6
8
  const { installFatalExit } = await import("../dist/foundation/terminal-cleanup/index.js");
7
9
  const { resolveProductPaths } = await import("../dist/foundation/lifecycle/index.js");
8
10
  const { join } = await import("node:path");
9
11
  let runningApplication;
10
12
  const fatal = profile === "a1" ? installFatalExit({
11
13
  directory: join(resolveProductPaths().runtimeDir, "crashes"),
12
- releaseId: process.env.A1_RELEASE_ID,
14
+ releaseId: launchContext.releaseId,
13
15
  dispose: () => runningApplication?.dispose(),
14
16
  }) : undefined;
15
17
  // Performance: begin the exact launch graph together while the trace write is pending.
@@ -140,6 +140,7 @@ async function runRecoveredCommand(value, arguments_) {
140
140
 
141
141
  async function readCapsule(path) {
142
142
  const value = JSON.parse(await readFile(path, "utf8"));
143
+ if (value.launchContract !== "neutral-launch-v1") throw new Error("Unsupported private recovery contract; stop existing processes and install directly with npm. Preserve settings, sessions, and history.");
143
144
  if (value.schema !== SCHEMA || typeof value.transactionId !== "string" || typeof value.packageName !== "string"
144
145
  || typeof value.packageName !== "string" || !value.packageName.includes("/") || typeof value.targetVersion !== "string" || typeof value.packageRoot !== "string" || typeof value.globalRoot !== "string"
145
146
  || typeof value.launcherRoot !== "string" || !Array.isArray(value.launchers) || typeof value.priorReleaseId !== "string" || typeof value.priorReleaseRoot !== "string"
@@ -178,6 +179,7 @@ async function readCapsule(path) {
178
179
  assertDirectChild(releasesRoot, priorReleaseRoot);
179
180
  if (priorReleaseRoot.split(sep).at(-1) !== value.priorReleaseId) throw new Error("A1 update recovery prior release identity is invalid");
180
181
  const priorManifest = JSON.parse(await readFile(resolve(priorReleaseRoot, value.releaseManifestName), "utf8"));
182
+ if (priorManifest.launchContract !== "neutral-launch-v1") throw new Error("Unsupported prior private launch contract; manual cutover is required.");
181
183
  if (priorManifest.releaseId !== value.priorReleaseId || priorManifest.contentDigest !== value.priorContentDigest) {
182
184
  throw new Error("A1 update recovery prior release manifest is invalid");
183
185
  }
@@ -190,7 +192,7 @@ async function installedTargetEntry(value) {
190
192
  try {
191
193
  const manifest = JSON.parse(await readFile(resolve(value.packageRoot, "package.json"), "utf8"));
192
194
  const entry = resolve(value.packageRoot, "bin", "cli.js");
193
- if (manifest.name !== value.packageName || manifest.version !== value.targetVersion || !(await lstat(entry)).isFile()) return null;
195
+ if (manifest.privateLaunchContract !== "neutral-launch-v1" || manifest.name !== value.packageName || manifest.version !== value.targetVersion || !(await lstat(entry)).isFile()) return null;
194
196
  return entry;
195
197
  } catch { return null; }
196
198
  }
@@ -1,4 +1,5 @@
1
1
  import { PromptHistoryService } from "../features/prompt-history/index.js";
2
+ import { resolvePromptHistoryDataDir } from "../features/launch/index.js";
2
3
  import { resolveProductPaths } from "../foundation/lifecycle/index.js";
3
4
  import { applyConfiguredPiTheme, getAvailablePiThemes, loadHistoryEditor } from "../integrations/pi/components/index.js";
4
5
  import { createPiEngineAdapter, } from "../integrations/pi/engine/index.js";
@@ -47,7 +48,7 @@ export async function composeOwnedUi(options = {}) {
47
48
  const promptHistory = settings === null || !ownedSurfaces || settings.value("promptHistoryEnabled") === false ? null : {
48
49
  limit: typeof historyLimit === "number" ? historyLimit : 100,
49
50
  store: new PromptHistoryService({
50
- dataDir: resolveProductPaths().dataDir,
51
+ dataDir: resolvePromptHistoryDataDir(),
51
52
  profileRoot: adapter.agentDir,
52
53
  limit: typeof historyLimit === "number" ? historyLimit : 100,
53
54
  }),
@@ -11,4 +11,6 @@ export interface LaunchProfilePathOptions {
11
11
  readonly platform?: NodeJS.Platform;
12
12
  }
13
13
  export declare function resolveLaunchProfilePaths(options?: LaunchProfilePathOptions): LaunchProfilePaths;
14
+ /** Select history storage without relocating control state or probing former stores. */
15
+ export declare function resolvePromptHistoryDataDir(options?: LaunchProfilePathOptions): string;
14
16
  export declare function configurationRootForProfile(profileId: LaunchProfileId, paths: LaunchProfilePaths): string | null;
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { posix, win32 } from "node:path";
3
3
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ import { resolveProductPaths } from "../../foundation/lifecycle/index.js";
4
5
  export function resolveLaunchProfilePaths(options = {}) {
5
6
  const environment = options.environment ?? process.env;
6
7
  const path = options.platform === "win32" || (options.platform === undefined && process.platform === "win32") ? win32 : posix;
@@ -13,6 +14,16 @@ export function resolveLaunchProfilePaths(options = {}) {
13
14
  agentProfile,
14
15
  });
15
16
  }
17
+ /** Select history storage without relocating control state or probing former stores. */
18
+ export function resolvePromptHistoryDataDir(options = {}) {
19
+ const environment = options.environment ?? process.env;
20
+ const platform = options.platform ?? process.platform;
21
+ if (environment[PRODUCT_IDENTITY.environment.dataDir] !== undefined) {
22
+ return resolveProductPaths(environment, platform).dataDir;
23
+ }
24
+ const path = platform === "win32" ? win32 : posix;
25
+ return path.join(resolveLaunchProfilePaths(options).managedStateRoot, "data");
26
+ }
16
27
  export function configurationRootForProfile(profileId, paths) {
17
28
  return profileId === "a1" ? paths.agentProfile : null;
18
29
  }
@@ -0,0 +1,29 @@
1
+ /** The one supported private process contract; public settings remain product identity data. */
2
+ export declare const PRIVATE_LAUNCH_CONTRACT = "neutral-launch-v1";
3
+ export declare const PRIVATE_ENVIRONMENT: Readonly<{
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
+ export interface LaunchContext {
12
+ readonly releaseRoot?: string;
13
+ readonly releaseId?: string;
14
+ readonly releaseDigest?: string;
15
+ readonly releaseLayers?: string;
16
+ readonly launchProfile?: "a1" | "pi";
17
+ readonly immutableWarmup?: "1";
18
+ }
19
+ export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
20
+ /** Read current private keys only, with entry-specific required fields and bounded diagnostics. */
21
+ 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. */
25
+ 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. */
27
+ export declare function assertCurrentLaunchContract(value: {
28
+ readonly launchContract?: unknown;
29
+ }): void;
@@ -0,0 +1,83 @@
1
+ /** The one supported private process contract; public settings remain product identity data. */
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
+ const PRIVATE_ENTRIES = Object.entries(PRIVATE_ENVIRONMENT);
12
+ const PRIVATE_KEYS = new Set(Object.values(PRIVATE_ENVIRONMENT));
13
+ 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. */
15
+ export function readLaunchContext(environment, requirement = "optional", platform = process.platform) {
16
+ const values = {};
17
+ // Performance: enumerate names only for Windows casing; never fetch unrelated environment values.
18
+ if (platform === "win32") {
19
+ for (const name of Object.keys(environment)) {
20
+ const logical = LOGICAL_KEYS.get(name.toUpperCase());
21
+ if (logical !== undefined)
22
+ acceptValue(values, logical, environment[name]);
23
+ }
24
+ }
25
+ else {
26
+ for (const [logical, key] of PRIVATE_ENTRIES) {
27
+ if (Object.prototype.propertyIsEnumerable.call(environment, key))
28
+ acceptValue(values, logical, environment[key]);
29
+ }
30
+ }
31
+ if (values.launchProfile !== undefined && values.launchProfile !== "a1" && values.launchProfile !== "pi") {
32
+ throw invalidContext("launchProfile", "unsupported profile");
33
+ }
34
+ if (values.immutableWarmup !== undefined && values.immutableWarmup !== "1")
35
+ throw invalidContext("immutableWarmup", "invalid permission flag");
36
+ if (values.releaseDigest !== undefined && !/^[a-f0-9]{64}$/.test(values.releaseDigest))
37
+ throw invalidContext("releaseDigest", "invalid digest");
38
+ if (requirement === "profile")
39
+ requireFields(values, ["launchProfile"]);
40
+ if (requirement === "release" || requirement === "warmup")
41
+ requireFields(values, ["releaseRoot", "releaseId", "releaseDigest", "releaseLayers"]);
42
+ if (requirement === "warmup")
43
+ requireFields(values, ["immutableWarmup"]);
44
+ return Object.freeze(values);
45
+ }
46
+ /** Rebuild owned context without inheriting a prior session's private selection. */
47
+ export function withLaunchContext(environment, context, platform = process.platform) {
48
+ const result = withoutLaunchContext(environment, platform);
49
+ for (const [logical, key] of PRIVATE_ENTRIES) {
50
+ const value = context[logical];
51
+ if (value !== undefined)
52
+ result[key] = value;
53
+ }
54
+ readLaunchContext(result, "optional", platform);
55
+ return result;
56
+ }
57
+ /** Remove only this implementation's private keys, leaving user and integration settings intact. */
58
+ export function withoutLaunchContext(environment, platform = process.platform) {
59
+ return Object.fromEntries(Object.entries(environment).filter(([key]) => !PRIVATE_KEYS.has(platform === "win32" ? key.toUpperCase() : key)));
60
+ }
61
+ /** Reject unsupported target metadata instead of negotiating or rewriting an older contract. */
62
+ export function assertCurrentLaunchContract(value) {
63
+ if (value.launchContract !== PRIVATE_LAUNCH_CONTRACT) {
64
+ 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.");
65
+ }
66
+ }
67
+ function acceptValue(values, logical, value) {
68
+ if (value === undefined)
69
+ return;
70
+ if (values[logical] !== undefined && values[logical] !== value)
71
+ throw invalidContext(logical, "conflicting environment key casing");
72
+ if (typeof value !== "string" || value.includes("\0") || (value.length === 0 && logical !== "releaseLayers"))
73
+ throw invalidContext(logical, "invalid value");
74
+ values[logical] = value;
75
+ }
76
+ function requireFields(values, fields) {
77
+ for (const field of fields)
78
+ if (values[field] === undefined)
79
+ throw invalidContext(field, "required current-contract field is missing");
80
+ }
81
+ function invalidContext(logical, reason) {
82
+ return new Error(`Invalid private launch context (${logical}): ${reason}`);
83
+ }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { PRODUCT_IDENTITY } from "../../product-identity.js";
2
+ import { readLaunchContext } from "../launch-context/index.js";
3
3
  import { resolve } from "node:path";
4
4
  import { assertLaunchProfileId, sessionSelectionArguments } from "../lifecycle/index.js";
5
5
  import { closeVerifiedContainment, DarwinNativeProcessInspector, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
@@ -20,10 +20,11 @@ export async function runLaunchGuardian(options) {
20
20
  const guardianIdentity = await inspector.observe(process.pid);
21
21
  if (!guardianIdentity)
22
22
  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]);
23
+ const context = readLaunchContext(environment);
24
+ const client = options.control ?? new SupervisorClient(context.releaseId);
24
25
  // Invariant: an instance belongs to the cohort that launched it and talks to that cohort's endpoint
25
26
  // for its whole life, whatever release becomes the active one meanwhile.
26
- const releaseId = environment[PRODUCT_IDENTITY.environment.releaseId];
27
+ const releaseId = context.releaseId;
27
28
  const endpoint = releaseId
28
29
  ? resolveCohortEndpoint(paths, releaseId, environment).endpoint
29
30
  : paths.endpoint;
@@ -36,7 +36,7 @@ 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
+ export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease, profile?: LaunchProfileId): NodeJS.ProcessEnv;
40
40
  export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number, startup?: SupervisorStartupAttempt): Promise<void>;
41
41
  export declare function readEndpointMetadata(path: string): Promise<SupervisorEndpointMetadata | null>;
42
42
  export declare function probeOwnership(metadata: SupervisorEndpointMetadata): Promise<OwnershipProbe>;
@@ -15,20 +15,23 @@ 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
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 });
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);
32
35
  // Invariant: records left by cohorts whose processes are gone say nothing about ownership, and there
33
36
  // can now be several of them. Clearing them first keeps the decision below about what is
34
37
  // actually running.
@@ -191,6 +194,7 @@ async function readInstalledVersion(packageRoot) {
191
194
  return manifest.version;
192
195
  }
193
196
  export async function certifyMaterializedRelease(release, dataDir, verification = {}) {
197
+ assertCurrentLaunchContract(release);
194
198
  if (!consumeMaterializationProof(release)) {
195
199
  await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
196
200
  }
@@ -198,6 +202,7 @@ export async function certifyMaterializedRelease(release, dataDir, verification
198
202
  }
199
203
  /** Persist current-format evidence after an authenticated parent has certified the exact release. */
200
204
  export async function recordParentCertifiedRelease(release, dataDir) {
205
+ assertCurrentLaunchContract(release);
201
206
  const path = resolve(dataDir, `certification-${release.releaseId}.json`);
202
207
  const restartSeal = await createRestartSeal(release, dataDir);
203
208
  await chmod(path, 0o600).catch(() => { });
@@ -230,22 +235,23 @@ async function launchUi(release, environment, sessionArgs) {
230
235
  const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
231
236
  return await new Promise((resolvePromise, rejectPromise) => {
232
237
  const child = spawn(process.execPath, [entry, ...sessionArgs], {
233
- env: releaseEnvironment(environment, release),
238
+ env: releaseEnvironment(environment, release, readLaunchContext(environment, "profile").launchProfile),
234
239
  stdio: "inherit",
235
240
  windowsHide: false,
236
241
  });
237
242
  child.once("error", rejectPromise);
238
- child.once("close", (code, signal) => resolvePromise(restoreAfterOwnedExit(environment[PRODUCT_IDENTITY.environment.launchProfile] === "a1", code, signal)));
243
+ child.once("close", (code, signal) => resolvePromise(restoreAfterOwnedExit(readLaunchContext(environment).launchProfile === "a1", code, signal)));
239
244
  });
240
245
  }
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
- };
246
+ export function releaseEnvironment(environment, release, profile) {
247
+ assertCurrentLaunchContract(release);
248
+ return withLaunchContext(environment, {
249
+ releaseId: release.releaseId,
250
+ releaseLayers: (release.dependencyLayers ?? []).map(layer => layer.layerId).join(","),
251
+ releaseRoot: release.releaseRoot,
252
+ releaseDigest: release.contentDigest,
253
+ ...(profile === undefined ? {} : { launchProfile: profile }),
254
+ });
249
255
  }
250
256
  export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup) {
251
257
  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;
@@ -2,6 +2,7 @@ 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";
5
6
  export const RELEASE_COHORT_SCHEMA = PRODUCT_IDENTITY.protocol.releaseCohortSchema;
6
7
  const RELEASE_ID_PATTERN = /^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/;
7
8
  /**
@@ -89,6 +90,7 @@ export class CohortStateStore {
89
90
  releaseRoot: release.releaseRoot,
90
91
  packageVersion: release.packageVersion,
91
92
  contentDigest: release.contentDigest,
93
+ ...(release.launchContract === undefined ? {} : { launchContract: release.launchContract }),
92
94
  approval: "candidate",
93
95
  materializedAt: new Date().toISOString(),
94
96
  certifiedAt: null,
@@ -120,6 +122,7 @@ export class CohortStateStore {
120
122
  const release = requiredRelease(current, releaseId);
121
123
  if (release.approval !== "approved")
122
124
  throw new Error(`cannot activate unverified release ${releaseId}`);
125
+ assertCurrentLaunchContract(release);
123
126
  const prior = current.references.active;
124
127
  return {
125
128
  ...current,
@@ -147,6 +150,7 @@ export class CohortStateStore {
147
150
  const rollback = requiredRelease(current, rollbackId);
148
151
  if (rollback.approval !== "approved")
149
152
  throw new Error(`cannot roll back to unverified release ${rollbackId}`);
153
+ assertCurrentLaunchContract(rollback);
150
154
  return {
151
155
  ...current,
152
156
  references: {
@@ -17,8 +17,8 @@ const DEFAULT_LIMITS = { maxItems: 8, maxDurationMs: 2_000, concurrency: 1 };
17
17
  const DEFAULT_WORKER_DURATION_MS = 10 * 60 * 1_000;
18
18
  const DEFAULT_NO_PROGRESS_BATCHES = 5;
19
19
  const DEFAULT_RETRY_DELAY_MS = 500;
20
- const WORKER_HOLDS_ENV = "A1_RELEASE_CLEANUP_HOLDS";
21
- const WORKER_RUN_ID_ENV = "A1_RELEASE_CLEANUP_RUN_ID";
20
+ const WORKER_HOLDS_ENV = "RELEASE_CLEANUP_HOLDS";
21
+ const WORKER_RUN_ID_ENV = "RELEASE_CLEANUP_RUN_ID";
22
22
  const WORKER_LEASE_FILENAME = "release-cleanup-worker.lock";
23
23
  /**
24
24
  * Reconcile current ownership under the cohort-state lock and durably detach obsolete releases.
@@ -3,6 +3,7 @@ import { chmod, lstat, mkdir, readFile, realpath, rename, rm, symlink, writeFile
3
3
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import { PRODUCT_PACKAGE_NAME, createReleaseIdentity, discoverReleasePayload, releaseFileIdentity, resolveWithin, } from "./release.js";
5
5
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
6
+ import { assertCurrentLaunchContract, readLaunchContext } from "../launch-context/index.js";
6
7
  import { mapWithConcurrency } from "./concurrency.js";
7
8
  import { assertImmutableFileMode } from "./immutable-platform.js";
8
9
  import { dependencyReference, materializeDependencyLayer, readCertifiedDependencyLayer, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
@@ -13,6 +14,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
13
14
  const payload = await discoverReleasePayload(packageRoot, {
14
15
  onSourceRead: (path, bytes) => options.onOperation?.({ operation: "source-read", path, bytes }),
15
16
  });
17
+ assertCurrentLaunchContract(payload);
16
18
  const storeRoot = resolve(dataDir, "releases");
17
19
  await mkdir(storeRoot, { recursive: true, mode: 0o700 });
18
20
  const dependencyPaths = payload.paths.filter(path => path.startsWith("node_modules/"));
@@ -60,7 +62,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
60
62
  options.onOperation?.({ operation: "candidate-write", path, bytes: bytes.length });
61
63
  return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
62
64
  });
63
- const identity = createReleaseIdentity(payload.packageRoot, payload.packageVersion, files, layerReferences);
65
+ const identity = createReleaseIdentity(payload.packageRoot, payload.packageVersion, files, layerReferences, payload.launchContract);
64
66
  if (layer !== null) {
65
67
  const binding = resolveWithin(candidate, "node_modules");
66
68
  const target = resolveWithin(layer.layerRoot, "node_modules");
@@ -137,7 +139,7 @@ export async function verifyMaterializedRelease(releaseRoot, expected, selectedS
137
139
  await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
138
140
  await verifyFile(canonical, file, options);
139
141
  });
140
- const recomputed = createReleaseIdentity(manifest.packageRoot, manifest.packageVersion, manifest.files, manifest.dependencyLayers ?? []).contentDigest;
142
+ const recomputed = createReleaseIdentity(manifest.packageRoot, manifest.packageVersion, manifest.files, manifest.dependencyLayers ?? [], manifest.launchContract).contentDigest;
141
143
  if (recomputed !== manifest.contentDigest)
142
144
  throw new Error(`release content digest mismatch for ${manifest.releaseId}`);
143
145
  await verifyReleaseDependencies(canonical, canonicalStoreRoot, manifest.dependencyLayers ?? [], true, options);
@@ -146,7 +148,7 @@ export async function verifyMaterializedRelease(releaseRoot, expected, selectedS
146
148
  export async function assertImmutableExecutionRoot(release, dataDir) {
147
149
  const storeRoot = await realpath(resolve(dataDir, "releases"));
148
150
  assertContained(storeRoot, release.releaseRoot, "release root is outside the selected release store");
149
- const selectedRoot = process.env[PRODUCT_IDENTITY.environment.releaseRoot];
151
+ const selectedRoot = readLaunchContext(process.env, "release").releaseRoot;
150
152
  if (!selectedRoot)
151
153
  throw new Error(PRODUCT_TEXT.diagnostic("persistent process has no immutable release root"));
152
154
  const selected = await realpath(selectedRoot);
@@ -154,6 +156,7 @@ export async function assertImmutableExecutionRoot(release, dataDir) {
154
156
  throw new Error(PRODUCT_TEXT.diagnostic("persistent process selected a different immutable release root"));
155
157
  }
156
158
  export async function resolveReleaseEntryPoint(release, entryPoint) {
159
+ assertCurrentLaunchContract(release);
157
160
  const normalized = entryPoint.split("\\").join("/").replace(/^\.\//, "");
158
161
  if (!release.files.some(file => file.path === normalized))
159
162
  throw new Error(`entry point is not in the verified release manifest: ${entryPoint}`);
@@ -200,6 +203,7 @@ function certificationReadyRelease(release) {
200
203
  return release;
201
204
  }
202
205
  function validateManifest(value) {
206
+ assertCurrentLaunchContract(value);
203
207
  if (value.packageName !== PRODUCT_PACKAGE_NAME || typeof value.packageVersion !== "string")
204
208
  throw new Error(PRODUCT_TEXT.diagnostic("release manifest metadata is invalid"));
205
209
  if (!/^[a-f0-9]{64}$/.test(value.contentDigest) || !/^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/.test(value.releaseId))
@@ -218,6 +222,9 @@ function validateManifest(value) {
218
222
  if (!Number.isSafeInteger(file.bytes) || file.bytes < 0 || !/^[a-f0-9]{64}$/.test(file.sha256))
219
223
  throw new Error(`invalid release manifest file identity: ${file.path}`);
220
224
  }
225
+ const expected = createReleaseIdentity(value.packageRoot, value.packageVersion, value.files, value.dependencyLayers ?? [], value.launchContract);
226
+ if (expected.contentDigest !== value.contentDigest)
227
+ throw new Error("release manifest content/contract digest mismatch");
221
228
  }
222
229
  function assertContained(parent, child, message) {
223
230
  const fromParent = relative(parent, child);
@@ -12,6 +12,7 @@ export interface ReleaseIdentity {
12
12
  readonly contentDigest: string;
13
13
  readonly releaseId: string;
14
14
  readonly packageRoot: string;
15
+ readonly launchContract?: string;
15
16
  readonly files: readonly ReleaseFileIdentity[];
16
17
  /** Absent for legacy full-copy releases. */
17
18
  readonly dependencyLayers?: readonly DependencyLayerReference[];
@@ -20,6 +21,7 @@ export interface DiscoveredReleasePayload {
20
21
  readonly packageRoot: string;
21
22
  readonly packageVersion: string;
22
23
  readonly paths: readonly string[];
24
+ readonly launchContract?: string;
23
25
  /** Package manifests already read to discover the dependency closure. */
24
26
  readonly cachedFiles: ReadonlyMap<string, Buffer>;
25
27
  }
@@ -34,7 +36,7 @@ export declare function discoverReleasePayload(packageRoot: string, options?: Di
34
36
  * content.
35
37
  */
36
38
  export declare function deriveReleaseIdentity(packageRoot: string): Promise<ReleaseIdentity>;
37
- export declare function createReleaseIdentity(packageRoot: string, packageVersion: string, files: readonly ReleaseFileIdentity[], dependencyLayers?: readonly DependencyLayerReference[]): ReleaseIdentity;
39
+ export declare function createReleaseIdentity(packageRoot: string, packageVersion: string, files: readonly ReleaseFileIdentity[], dependencyLayers?: readonly DependencyLayerReference[], launchContract?: string): ReleaseIdentity;
38
40
  export declare function releaseFileIdentity(path: string, bytes: Uint8Array, executable: boolean): ReleaseFileIdentity;
39
41
  export declare function digestManifestFiles(files: readonly ReleaseFileIdentity[]): string;
40
42
  export declare function resolveWithin(root: string, candidate: string): string;
@@ -31,6 +31,7 @@ export async function discoverReleasePayload(packageRoot, options = {}) {
31
31
  packageRoot: canonicalRoot,
32
32
  packageVersion: manifest.version,
33
33
  paths: [...paths].sort(),
34
+ ...(typeof manifest.privateLaunchContract === "string" ? { launchContract: manifest.privateLaunchContract } : {}),
34
35
  cachedFiles,
35
36
  };
36
37
  }
@@ -49,10 +50,12 @@ export async function deriveReleaseIdentity(packageRoot) {
49
50
  const bytes = payload.cachedFiles.get(path) ?? await readFile(absolute);
50
51
  return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
51
52
  });
52
- return createReleaseIdentity(payload.packageRoot, payload.packageVersion, files);
53
+ return createReleaseIdentity(payload.packageRoot, payload.packageVersion, files, [], payload.launchContract);
53
54
  }
54
- export function createReleaseIdentity(packageRoot, packageVersion, files, dependencyLayers = []) {
55
- const productDigest = digestManifestFiles(files);
55
+ export function createReleaseIdentity(packageRoot, packageVersion, files, dependencyLayers = [], launchContract) {
56
+ const fileDigest = digestManifestFiles(files);
57
+ const productDigest = launchContract === undefined ? fileDigest
58
+ : createHash("sha256").update(`launch-contract\0${launchContract}\0${fileDigest}`).digest("hex");
56
59
  const contentDigest = dependencyLayers.length === 0
57
60
  ? productDigest
58
61
  : createHash("sha256")
@@ -65,6 +68,7 @@ export function createReleaseIdentity(packageRoot, packageVersion, files, depend
65
68
  contentDigest,
66
69
  releaseId: `${packageVersion}-${contentDigest.slice(0, 20)}`,
67
70
  packageRoot,
71
+ ...(launchContract === undefined ? {} : { launchContract }),
68
72
  files: [...files].sort(compareReleaseFiles),
69
73
  ...(dependencyLayers.length === 0 ? {} : { dependencyLayers: [...dependencyLayers] }),
70
74
  };
@@ -25,6 +25,7 @@ interface LayerRestartEvidence extends DependencyLayerReference {
25
25
  }
26
26
  interface RestartSealCore {
27
27
  readonly version: typeof RESTART_SEAL_VERSION;
28
+ readonly launchContract: string;
28
29
  readonly platform: NodeJS.Platform;
29
30
  readonly platformPolicy: ImmutablePlatformPolicy;
30
31
  readonly releaseId: string;
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
3
  import { basename, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
+ import { assertCurrentLaunchContract } from "../launch-context/index.js";
5
6
  import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
6
7
  import { immutablePlatformPolicy } from "./immutable-platform.js";
7
8
  import { PRODUCT_PACKAGE_NAME } from "./release.js";
@@ -11,6 +12,7 @@ const RESTART_SEAL_VERSION = 1;
11
12
  const REQUIRED_LAUNCH_ENTRIES = ["bin/supervisor.js", "bin/guardian.js"];
12
13
  /** Build compact restart authority after complete content verification established read-only payload files. */
13
14
  export async function createRestartSeal(release, dataDir) {
15
+ assertCurrentLaunchContract(release);
14
16
  const policy = immutablePlatformPolicy();
15
17
  if (policy === null)
16
18
  return null;
@@ -45,6 +47,7 @@ export async function createRestartSeal(release, dataDir) {
45
47
  }));
46
48
  const core = {
47
49
  version: RESTART_SEAL_VERSION,
50
+ launchContract: release.launchContract,
48
51
  platform: process.platform,
49
52
  platformPolicy: policy,
50
53
  releaseId: release.releaseId,
@@ -81,6 +84,7 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
81
84
  throw new Error("restart certification differs from the approved release record");
82
85
  }
83
86
  const seal = normalizeRestartSeal(certification.restartSeal);
87
+ assertCurrentLaunchContract(seal);
84
88
  if (seal.platform !== process.platform || seal.platformPolicy !== immutablePlatformPolicy()) {
85
89
  throw new Error("restart certification platform immutability evidence is unsupported");
86
90
  }
@@ -129,6 +133,7 @@ export async function readRestartCertifiedRelease(record, dataDir, onEvent) {
129
133
  }
130
134
  return {
131
135
  packageName: PRODUCT_PACKAGE_NAME,
136
+ launchContract: seal.launchContract,
132
137
  packageVersion: seal.packageVersion,
133
138
  contentDigest: seal.contentDigest,
134
139
  releaseId: seal.releaseId,
@@ -1,7 +1,9 @@
1
+ import { PRIVATE_LAUNCH_CONTRACT } from "../launch-context/index.js";
1
2
  import type { UpdateTransaction, UpdateRecoveryState } from "./update-transaction.js";
2
3
  export declare const UPDATE_RECOVERY_SCHEMA: "a1-update-recovery-v1";
3
4
  export interface UpdateRecoveryCapsule {
4
5
  readonly schema: typeof UPDATE_RECOVERY_SCHEMA;
6
+ readonly launchContract: typeof PRIVATE_LAUNCH_CONTRACT;
5
7
  readonly transactionId: string;
6
8
  readonly packageName: string;
7
9
  readonly targetVersion: string;