@timurproko/a1 0.1.8-dev.290 → 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 (112) hide show
  1. package/README.md +21 -0
  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 +13 -1
  6. package/dist/contracts/owned-ui/index.d.ts +1 -0
  7. package/dist/contracts/owned-ui/index.js +1 -0
  8. package/dist/contracts/owned-ui/prompt-history.d.ts +37 -0
  9. package/dist/contracts/owned-ui/prompt-history.js +23 -0
  10. package/dist/features/launch/profile-paths.d.ts +2 -0
  11. package/dist/features/launch/profile-paths.js +11 -0
  12. package/dist/features/prompt-history/index.d.ts +2 -0
  13. package/dist/features/prompt-history/index.js +2 -0
  14. package/dist/features/prompt-history/paths.d.ts +4 -0
  15. package/dist/features/prompt-history/paths.js +16 -0
  16. package/dist/features/prompt-history/service.d.ts +24 -0
  17. package/dist/features/prompt-history/service.js +260 -0
  18. package/dist/features/prompt-history/store.d.ts +15 -0
  19. package/dist/features/prompt-history/store.js +166 -0
  20. package/dist/features/prompt-history/worker.d.ts +9 -0
  21. package/dist/features/prompt-history/worker.js +42 -0
  22. package/dist/foundation/launch-context/index.d.ts +29 -0
  23. package/dist/foundation/launch-context/index.js +83 -0
  24. package/dist/foundation/launch-guardian/main.js +4 -3
  25. package/dist/foundation/release/bootstrap.d.ts +1 -1
  26. package/dist/foundation/release/bootstrap.js +18 -12
  27. package/dist/foundation/release/cohort-state.d.ts +1 -0
  28. package/dist/foundation/release/cohort-state.js +4 -0
  29. package/dist/foundation/release/release-gc.js +2 -2
  30. package/dist/foundation/release/release-store.js +10 -3
  31. package/dist/foundation/release/release.d.ts +3 -1
  32. package/dist/foundation/release/release.js +7 -3
  33. package/dist/foundation/release/restart-certification.d.ts +1 -0
  34. package/dist/foundation/release/restart-certification.js +5 -0
  35. package/dist/foundation/release/update-recovery.d.ts +2 -0
  36. package/dist/foundation/release/update-recovery.js +4 -0
  37. package/dist/foundation/release/update.js +5 -0
  38. package/dist/foundation/release/warmup.d.ts +1 -1
  39. package/dist/foundation/release/warmup.js +5 -4
  40. package/dist/foundation/startup/startup-runtime.js +13 -11
  41. package/dist/foundation/supervision/main.js +8 -7
  42. package/dist/integrations/pi/components/editor-interaction.d.ts +38 -0
  43. package/dist/integrations/pi/components/editor-interaction.js +1 -0
  44. package/dist/integrations/pi/components/history-editor-loader.d.ts +5 -0
  45. package/dist/integrations/pi/components/history-editor-loader.js +5 -0
  46. package/dist/integrations/pi/components/index.d.ts +1 -0
  47. package/dist/integrations/pi/components/index.js +1 -0
  48. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  49. package/dist/integrations/pi/components/owned-editor-ux.js +95 -22
  50. package/dist/integrations/pi/components/shell-editor-autocomplete.js +12 -1
  51. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  52. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  53. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  54. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  55. package/dist/integrations/pi/components/shell-shared-facade.d.ts +15 -2
  56. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  57. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +13 -9
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +137 -133
  60. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +280 -0
  61. package/dist/integrations/pi/components/upstream/history/editor-core.js +2062 -0
  62. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +32 -0
  63. package/dist/integrations/pi/components/upstream/history/kill-ring.js +48 -0
  64. package/dist/integrations/pi/components/upstream/history/printable-key.d.ts +1 -0
  65. package/dist/integrations/pi/components/upstream/history/printable-key.js +40 -0
  66. package/dist/integrations/pi/components/upstream/history/text-helpers.d.ts +14 -0
  67. package/dist/integrations/pi/components/upstream/history/text-helpers.js +28 -0
  68. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +22 -0
  69. package/dist/integrations/pi/components/upstream/history/undo-stack.js +36 -0
  70. package/dist/integrations/pi/components/upstream/history/word-navigation.d.ts +24 -0
  71. package/dist/integrations/pi/components/upstream/history/word-navigation.js +100 -0
  72. package/dist/integrations/pi/engine/adapter.d.ts +13 -1
  73. package/dist/integrations/pi/engine/adapter.js +86 -25
  74. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  75. package/dist/integrations/pi/engine/changelog.js +63 -0
  76. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  77. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  78. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  79. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +4 -1
  80. package/dist/integrations/pi/session-ui/prompt-chips.js +40 -13
  81. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +30 -0
  82. package/dist/integrations/pi/session-ui/prompt-history-controller.js +147 -0
  83. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +33 -0
  84. package/dist/integrations/pi/session-ui/session-shell-root.js +68 -74
  85. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  86. package/dist/integrations/pi/session-ui/session-shell.js +98 -12
  87. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
  88. package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
  89. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  90. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
  91. package/dist/native/darwin-arm64/manifest.json +1 -1
  92. package/dist/native/linux-x64/manifest.json +1 -1
  93. package/dist/native/win32-x64/manifest.json +2 -2
  94. package/dist/native/win32-x64/process-guardian.exe +0 -0
  95. package/dist/product-identity.d.ts +2 -2
  96. package/dist/product-identity.js +3 -36
  97. package/dist/product-identity.json +1 -22
  98. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  99. package/dist/ui/components/transcript-viewport.js +23 -2
  100. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  101. package/dist/ui/components/visible-hyperlinks.js +1 -0
  102. package/dist/ui/settings/declarations.js +19 -0
  103. package/dist/ui/settings/sections.js +11 -1
  104. package/dist/ui/settings/session.js +13 -4
  105. package/docs/architecture/boundaries.md +2 -0
  106. package/docs/architecture/history-editor-provenance.md +96 -0
  107. package/docs/architecture/internal-naming.md +89 -0
  108. package/docs/architecture/project-structure.md +1 -0
  109. package/docs/architecture/resource-and-data-policy.md +27 -1
  110. package/docs/architecture/toolchain.md +15 -1
  111. package/docs/features/prompt-history.md +149 -0
  112. package/package.json +8 -3
@@ -0,0 +1,166 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, statSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
+ import { assertPromptHistorySubmission, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_TEXT_BYTES } from "../../contracts/owned-ui/index.js";
6
+ export class HistoryStorageError extends Error {
7
+ code;
8
+ constructor(code) {
9
+ super(`Prompt history ${code}`);
10
+ this.code = code;
11
+ }
12
+ }
13
+ const MAX_STORAGE_BYTES = 64 * 1024 * 1024;
14
+ export class PromptHistoryStore {
15
+ path;
16
+ profileId;
17
+ #database;
18
+ constructor(path, profileId, limit) {
19
+ this.path = path;
20
+ this.profileId = profileId;
21
+ if (!Number.isInteger(limit) || limit < 10 || limit > 100 || limit % 10 !== 0)
22
+ throw new HistoryStorageError("schema");
23
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
24
+ if (lstatSync(dirname(path)).isSymbolicLink())
25
+ throw new HistoryStorageError("unavailable");
26
+ if (process.platform !== "win32")
27
+ chmodSync(dirname(path), 0o700);
28
+ try {
29
+ closeSync(openSync(path, "wx", 0o600));
30
+ }
31
+ catch (error) {
32
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
33
+ throw error;
34
+ }
35
+ if (lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile())
36
+ throw new HistoryStorageError("unavailable");
37
+ if (process.platform !== "win32")
38
+ chmodSync(path, 0o600);
39
+ this.#checkSize();
40
+ this.#database = new DatabaseSync(path);
41
+ try {
42
+ this.#database.exec("PRAGMA busy_timeout = 25");
43
+ this.#transaction(() => {
44
+ const version = Number(this.#database.prepare("PRAGMA user_version").get()?.user_version);
45
+ if (version === 0) {
46
+ const tables = this.#database.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
47
+ if (tables.length !== 0)
48
+ throw new HistoryStorageError("schema");
49
+ this.#database.exec(`CREATE TABLE history_meta (id INTEGER PRIMARY KEY CHECK(id=1), schema TEXT NOT NULL, profile TEXT NOT NULL, revision INTEGER NOT NULL, limit_count INTEGER NOT NULL);
50
+ CREATE TABLE prompts (text TEXT PRIMARY KEY COLLATE BINARY, submission_id TEXT NOT NULL, sequence INTEGER NOT NULL UNIQUE, timestamp INTEGER NOT NULL, kind TEXT NOT NULL, cwd TEXT, session_id TEXT, bytes INTEGER NOT NULL CHECK(bytes>0 AND bytes<=1048576));
51
+ PRAGMA user_version=1;`);
52
+ this.#database.prepare("INSERT INTO history_meta VALUES (1,?,?,0,?)").run(PRODUCT_IDENTITY.protocol.promptHistorySchema, profileId, limit);
53
+ }
54
+ else if (version !== 1)
55
+ throw new HistoryStorageError("schema");
56
+ const meta = this.#database.prepare("SELECT schema,profile FROM history_meta WHERE id=1").get();
57
+ if (meta?.schema !== PRODUCT_IDENTITY.protocol.promptHistorySchema || meta?.profile !== profileId)
58
+ throw new HistoryStorageError("schema");
59
+ this.#database.prepare("UPDATE history_meta SET limit_count=?,revision=revision+1 WHERE id=1").run(limit);
60
+ this.#prune(limit);
61
+ });
62
+ this.#database.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA wal_autocheckpoint=128; PRAGMA journal_size_limit=8388608; PRAGMA max_page_count=8192;");
63
+ }
64
+ catch (error) {
65
+ this.#database.close();
66
+ throw error;
67
+ }
68
+ }
69
+ record(input) {
70
+ assertPromptHistorySubmission(input);
71
+ const bytes = Buffer.byteLength(input.text);
72
+ if (bytes > PROMPT_HISTORY_MAX_ENTRY_BYTES)
73
+ throw new HistoryStorageError("oversized");
74
+ this.#checkSize(true);
75
+ this.#transaction(() => {
76
+ const duplicate = this.#database.prepare("SELECT submission_id FROM prompts WHERE text=?").get(input.text);
77
+ if (duplicate?.submission_id === input.id)
78
+ return;
79
+ this.#database.exec("UPDATE history_meta SET revision=revision+1 WHERE id=1");
80
+ const meta = this.#database.prepare("SELECT revision,limit_count FROM history_meta WHERE id=1").get();
81
+ this.#database.prepare(`INSERT INTO prompts VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(text) DO UPDATE SET submission_id=excluded.submission_id,sequence=excluded.sequence,timestamp=excluded.timestamp,kind=excluded.kind,cwd=excluded.cwd,session_id=excluded.session_id,bytes=excluded.bytes`)
82
+ .run(input.text, input.id, meta.revision, input.timestamp, input.kind, bounded(input.cwd, 8192), bounded(input.sessionId, 256), bytes);
83
+ this.#prune(Number(meta.limit_count));
84
+ });
85
+ }
86
+ snapshot() {
87
+ this.#database.exec("BEGIN");
88
+ try {
89
+ const meta = this.#database.prepare("SELECT revision,limit_count FROM history_meta WHERE id=1").get();
90
+ const entries = this.#database.prepare("SELECT text,submission_id FROM prompts ORDER BY sequence DESC LIMIT 100").all();
91
+ let bytes = 0;
92
+ const result = entries.map(row => {
93
+ if (typeof row.text !== "string" || typeof row.submission_id !== "string")
94
+ throw new HistoryStorageError("corrupt");
95
+ bytes += Buffer.byteLength(row.text);
96
+ if (bytes > PROMPT_HISTORY_MAX_TEXT_BYTES)
97
+ throw new HistoryStorageError("corrupt");
98
+ return { text: row.text, submissionId: row.submission_id };
99
+ });
100
+ this.#database.exec("COMMIT");
101
+ return { revision: Number(meta.revision), limit: Number(meta.limit_count), entries: result };
102
+ }
103
+ catch (error) {
104
+ this.#database.exec("ROLLBACK");
105
+ throw error;
106
+ }
107
+ }
108
+ close() { this.#database.close(); }
109
+ #transaction(work) {
110
+ this.#database.exec("BEGIN IMMEDIATE");
111
+ try {
112
+ work();
113
+ this.#database.exec("COMMIT");
114
+ }
115
+ catch (error) {
116
+ this.#database.exec("ROLLBACK");
117
+ throw error;
118
+ }
119
+ }
120
+ #prune(limit) {
121
+ const rows = this.#database.prepare("SELECT sequence,bytes FROM prompts ORDER BY sequence DESC").all();
122
+ let bytes = 0;
123
+ for (const [index, row] of rows.entries()) {
124
+ bytes += Number(row.bytes);
125
+ if (index >= limit || bytes > PROMPT_HISTORY_MAX_TEXT_BYTES)
126
+ this.#database.prepare("DELETE FROM prompts WHERE sequence=?").run(row.sequence);
127
+ }
128
+ }
129
+ #checkSize(maintain = false) {
130
+ let size = 0;
131
+ for (const suffix of ["", "-wal", "-shm"]) {
132
+ try {
133
+ if (lstatSync(this.path + suffix).isSymbolicLink())
134
+ throw new HistoryStorageError("unavailable");
135
+ size += statSync(this.path + suffix).size;
136
+ }
137
+ catch (error) {
138
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
139
+ throw error;
140
+ }
141
+ }
142
+ if (size > MAX_STORAGE_BYTES && maintain) {
143
+ this.#database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
144
+ this.#checkSize();
145
+ }
146
+ else if (size > MAX_STORAGE_BYTES)
147
+ throw new HistoryStorageError("capacity");
148
+ }
149
+ }
150
+ function bounded(value, bytes) {
151
+ return value !== undefined && Buffer.byteLength(value) <= bytes ? value : null;
152
+ }
153
+ export function classifyHistoryError(error) {
154
+ if (error instanceof HistoryStorageError)
155
+ return error.code;
156
+ if (error instanceof Error && "errcode" in error) {
157
+ const code = Number(error.errcode) & 0xff;
158
+ if (code === 5 || code === 6)
159
+ return "busy";
160
+ if (code === 11 || code === 26)
161
+ return "corrupt";
162
+ if (code === 13)
163
+ return "capacity";
164
+ }
165
+ return "unavailable";
166
+ }
@@ -0,0 +1,9 @@
1
+ import type { PromptHistorySubmission } from "../../contracts/owned-ui/index.js";
2
+ export type HistoryWorkerRequest = {
3
+ id: number;
4
+ kind: "read" | "close";
5
+ } | {
6
+ id: number;
7
+ kind: "record";
8
+ submission: PromptHistorySubmission;
9
+ };
@@ -0,0 +1,42 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { PromptHistoryStore, classifyHistoryError } from "./store.js";
4
+ const port = parentPort;
5
+ if (port !== null) {
6
+ let store;
7
+ let chain = Promise.resolve();
8
+ const options = workerData;
9
+ const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit); });
10
+ void open.then(() => port.postMessage({ id: 0, ok: true }), error => port.postMessage({ id: 0, ok: false, code: classifyHistoryError(error) }));
11
+ port.on("message", (request) => {
12
+ chain = chain.then(async () => {
13
+ try {
14
+ await open;
15
+ if (request.kind === "close") {
16
+ store.close();
17
+ port.postMessage({ id: request.id, ok: true });
18
+ port.close();
19
+ return;
20
+ }
21
+ const value = await retry(() => request.kind === "record" ? store.record(request.submission) : store.snapshot());
22
+ port.postMessage({ id: request.id, ok: true, value });
23
+ }
24
+ catch (error) {
25
+ port.postMessage({ id: request.id, ok: false, code: classifyHistoryError(error) });
26
+ }
27
+ });
28
+ });
29
+ }
30
+ async function retry(operation) {
31
+ const deadline = performance.now() + 1000;
32
+ for (;;) {
33
+ try {
34
+ return operation();
35
+ }
36
+ catch (error) {
37
+ if (classifyHistoryError(error) !== "busy" || performance.now() >= deadline)
38
+ throw error;
39
+ await delay(25);
40
+ }
41
+ }
42
+ }
@@ -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;