@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.
- package/README.md +16 -4
- package/bin/guardian.js +4 -3
- package/bin/ui.js +4 -2
- package/bin/update-recovery.js +3 -1
- package/dist/composition/owned-ui.js +2 -1
- package/dist/features/launch/profile-paths.d.ts +2 -0
- package/dist/features/launch/profile-paths.js +11 -0
- package/dist/foundation/launch-context/index.d.ts +29 -0
- package/dist/foundation/launch-context/index.js +83 -0
- package/dist/foundation/launch-guardian/main.js +4 -3
- package/dist/foundation/release/bootstrap.d.ts +1 -1
- package/dist/foundation/release/bootstrap.js +18 -12
- package/dist/foundation/release/cohort-state.d.ts +1 -0
- package/dist/foundation/release/cohort-state.js +4 -0
- package/dist/foundation/release/release-gc.js +2 -2
- package/dist/foundation/release/release-store.js +10 -3
- package/dist/foundation/release/release.d.ts +3 -1
- package/dist/foundation/release/release.js +7 -3
- package/dist/foundation/release/restart-certification.d.ts +1 -0
- package/dist/foundation/release/restart-certification.js +5 -0
- package/dist/foundation/release/update-recovery.d.ts +2 -0
- package/dist/foundation/release/update-recovery.js +4 -0
- package/dist/foundation/release/update.js +5 -0
- package/dist/foundation/release/warmup.d.ts +1 -1
- package/dist/foundation/release/warmup.js +5 -4
- package/dist/foundation/startup/startup-runtime.js +13 -11
- package/dist/foundation/supervision/main.js +8 -7
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +54 -3
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
- package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
- package/dist/integrations/pi/engine/adapter.d.ts +13 -1
- package/dist/integrations/pi/engine/adapter.js +86 -25
- package/dist/integrations/pi/engine/changelog.d.ts +2 -0
- package/dist/integrations/pi/engine/changelog.js +63 -0
- package/dist/integrations/pi/engine/resources/changelog.json +12 -0
- package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
- package/dist/integrations/pi/engine/workflows.d.ts +1 -1
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
- package/dist/integrations/pi/session-ui/prompt-chips.js +37 -13
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +25 -0
- package/dist/integrations/pi/session-ui/session-shell-root.js +65 -74
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +58 -7
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +2 -35
- package/dist/product-identity.json +0 -22
- package/dist/ui/components/transcript-viewport.d.ts +8 -0
- package/dist/ui/components/transcript-viewport.js +23 -2
- package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
- package/dist/ui/components/visible-hyperlinks.js +1 -0
- package/dist/ui/settings/declarations.js +1 -0
- package/dist/ui/settings/sections.js +11 -1
- package/docs/architecture/boundaries.md +2 -1
- package/docs/architecture/internal-naming.md +89 -0
- package/docs/architecture/resource-and-data-policy.md +9 -4
- package/docs/architecture/toolchain.md +15 -1
- package/docs/features/prompt-history.md +39 -11
- 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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
12
|
-
const
|
|
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
|
|
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:
|
|
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.
|
package/bin/update-recovery.js
CHANGED
|
@@ -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:
|
|
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 {
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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 = "
|
|
21
|
-
const WORKER_RUN_ID_ENV = "
|
|
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
|
|
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
|
|
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;
|