@timurproko/a1 0.1.8-dev.226 → 0.1.8-dev.259
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/bin/guardian.js +4 -2
- package/bin/supervisor.js +1 -1
- package/bin/ui.js +44 -36
- package/bin/warmup.js +18 -9
- package/dist/cli/dispatch.d.ts +9 -1
- package/dist/cli/dispatch.js +73 -14
- package/dist/cli/package-messages.d.ts +26 -0
- package/dist/cli/package-messages.js +71 -0
- package/dist/cli/packages.d.ts +2 -1
- package/dist/cli/packages.js +12 -19
- package/dist/composition/owned-ui.d.ts +5 -2
- package/dist/composition/owned-ui.js +3 -1
- package/dist/contracts/agent-engine/package-ports.d.ts +6 -0
- package/dist/features/launch/intent.d.ts +5 -3
- package/dist/features/launch/intent.js +7 -2
- package/dist/features/owned-ui/index.d.ts +1 -0
- package/dist/features/owned-ui/index.js +1 -0
- package/dist/features/owned-ui/session-fork-prompt.d.ts +8 -0
- package/dist/features/owned-ui/session-fork-prompt.js +34 -0
- package/dist/foundation/launch-guardian/main.d.ts +2 -0
- package/dist/foundation/launch-guardian/main.js +8 -3
- package/dist/foundation/lifecycle/index.d.ts +2 -0
- package/dist/foundation/lifecycle/index.js +2 -0
- package/dist/foundation/lifecycle/paths.d.ts +2 -0
- package/dist/foundation/lifecycle/paths.js +9 -2
- package/dist/foundation/lifecycle/session-selection.d.ts +9 -0
- package/dist/foundation/lifecycle/session-selection.js +39 -0
- package/dist/foundation/lifecycle/supervisor-startup.d.ts +30 -0
- package/dist/foundation/lifecycle/supervisor-startup.js +106 -0
- package/dist/foundation/process-containment/darwin-process-inspector.d.ts +5 -0
- package/dist/foundation/process-containment/darwin-process-inspector.js +7 -0
- package/dist/foundation/process-containment/index.d.ts +1 -0
- package/dist/foundation/process-containment/index.js +1 -0
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +2 -1
- package/dist/foundation/process-containment/windows-process-inspector.js +4 -2
- package/dist/foundation/release/bootstrap.d.ts +10 -3
- package/dist/foundation/release/bootstrap.js +58 -25
- package/dist/foundation/release/dependency-layer.js +11 -10
- package/dist/foundation/release/immutable-platform.d.ts +6 -0
- package/dist/foundation/release/immutable-platform.js +13 -0
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/release-store.d.ts +1 -1
- package/dist/foundation/release/release-store.js +4 -2
- package/dist/foundation/release/restart-certification.d.ts +67 -0
- package/dist/foundation/release/restart-certification.js +211 -0
- package/dist/foundation/release/update.js +4 -4
- package/dist/foundation/release/warmup.js +1 -1
- package/dist/foundation/startup/startup-runtime.d.ts +2 -2
- package/dist/foundation/startup/startup-runtime.js +1 -1
- package/dist/foundation/supervision/main.d.ts +1 -1
- package/dist/foundation/supervision/main.js +60 -30
- package/dist/foundation/supervision/server.js +14 -1
- package/dist/integrations/pi/components/shell-footer-status.js +28 -12
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +4 -1
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +2 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +5 -0
- package/dist/integrations/pi/engine/adapter.d.ts +6 -0
- package/dist/integrations/pi/engine/adapter.js +354 -53
- package/dist/integrations/pi/engine/index.d.ts +1 -0
- package/dist/integrations/pi/engine/index.js +1 -0
- package/dist/integrations/pi/engine/package-integration.js +31 -25
- package/dist/integrations/pi/engine/runtime-integration.d.ts +3 -0
- package/dist/integrations/pi/engine/runtime-integration.js +11 -4
- package/dist/integrations/pi/engine/session-selection.d.ts +28 -0
- package/dist/integrations/pi/engine/session-selection.js +90 -0
- package/dist/integrations/pi/engine/workflows.d.ts +18 -1
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +5 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +76 -24
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +46 -14
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -1
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +47 -18
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +166 -26
- package/dist/native/darwin-arm64/manifest.json +4 -4
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/text-selection.js +8 -35
- package/dist/ui/components/transcript-viewport.d.ts +8 -2
- package/dist/ui/components/transcript-viewport.js +8 -6
- package/dist/ui/components/visible-hyperlinks.d.ts +19 -0
- package/dist/ui/components/visible-hyperlinks.js +64 -0
- package/docs/architecture/process-guardian-provenance.md +3 -1
- package/docs/architecture/ui-reference-provenance.md +2 -0
- package/docs/ci-release-runbook.md +1 -1
- package/docs/features/launch-profiles.md +17 -1
- package/docs/manual-ghost-link-baseline.md +89 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
/** Pre-resource, line-mode confirmation matching Pi's cross-project CLI fork decision. */
|
|
3
|
+
export function createConsoleSessionForkPrompt(options = {}) {
|
|
4
|
+
const input = options.input ?? process.stdin;
|
|
5
|
+
const output = options.output ?? process.stdout;
|
|
6
|
+
return async ({ sourceCwd }) => {
|
|
7
|
+
output.write(`Session found in different project: ${sourceCwd}\n`);
|
|
8
|
+
const wasFlowing = input.readableFlowing;
|
|
9
|
+
const readline = createInterface({ input, output });
|
|
10
|
+
try {
|
|
11
|
+
return await new Promise((resolve, reject) => {
|
|
12
|
+
const finish = (accepted) => { cleanup(); resolve(accepted); };
|
|
13
|
+
const cancel = () => finish(false);
|
|
14
|
+
const fail = (error) => { cleanup(); reject(error); };
|
|
15
|
+
const cleanup = () => {
|
|
16
|
+
readline.off("close", cancel);
|
|
17
|
+
readline.off("SIGINT", cancel);
|
|
18
|
+
input.off("error", fail);
|
|
19
|
+
};
|
|
20
|
+
readline.once("close", cancel);
|
|
21
|
+
readline.once("SIGINT", cancel);
|
|
22
|
+
input.once("error", fail);
|
|
23
|
+
readline.question("Fork this session into current directory? [y/N] ", answer => {
|
|
24
|
+
finish(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
readline.close();
|
|
30
|
+
if (wasFlowing !== true)
|
|
31
|
+
input.pause();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LaunchInstanceStopIntent, LaunchProfileId, SupervisorCommand } from "../lifecycle/index.js";
|
|
2
|
+
import { type SessionSelection } from "../lifecycle/index.js";
|
|
2
3
|
import { type NativeProcessInspector, type ProcessContainment } from "../process-containment/index.js";
|
|
3
4
|
interface GuardianControl {
|
|
4
5
|
connect(endpoint: string, timeoutMs?: number): Promise<unknown>;
|
|
@@ -19,6 +20,7 @@ export interface LaunchGuardianOptions {
|
|
|
19
20
|
readonly profileId: LaunchProfileId;
|
|
20
21
|
readonly releaseRoot: string;
|
|
21
22
|
readonly uiEntry: string;
|
|
23
|
+
readonly sessionSelection?: SessionSelection;
|
|
22
24
|
readonly environment?: NodeJS.ProcessEnv;
|
|
23
25
|
readonly cwd?: string;
|
|
24
26
|
readonly helperPath?: string;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { assertLaunchProfileId } from "../lifecycle/index.js";
|
|
5
|
-
import { closeVerifiedContainment, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
|
|
4
|
+
import { assertLaunchProfileId, sessionSelectionArguments } from "../lifecycle/index.js";
|
|
5
|
+
import { closeVerifiedContainment, DarwinNativeProcessInspector, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
|
|
6
6
|
import { SupervisorClient } from "../protocol/index.js";
|
|
7
7
|
import { resolveCohortEndpoint, resolveProductPaths } from "../supervision/index.js";
|
|
8
8
|
import { markStartupPhase } from "../startup/index.js";
|
|
9
9
|
export async function runLaunchGuardian(options) {
|
|
10
10
|
assertLaunchProfileId(options.profileId);
|
|
11
|
+
const sessionArgs = sessionSelectionArguments(options.sessionSelection);
|
|
12
|
+
if (sessionArgs.length > 0 && options.profileId !== "a1")
|
|
13
|
+
throw new Error("session selection requires the normal A1 profile");
|
|
11
14
|
const environment = { ...(options.environment ?? process.env) };
|
|
12
15
|
const paths = resolveProductPaths(environment);
|
|
13
16
|
const instanceId = randomUUID();
|
|
@@ -40,7 +43,7 @@ export async function runLaunchGuardian(options) {
|
|
|
40
43
|
});
|
|
41
44
|
let handle;
|
|
42
45
|
try {
|
|
43
|
-
handle = await containment.spawn(process.execPath, [options.uiEntry], {
|
|
46
|
+
handle = await containment.spawn(process.execPath, [options.uiEntry, ...sessionArgs], {
|
|
44
47
|
cwd: options.cwd ?? process.cwd(),
|
|
45
48
|
environment: environmentEntries(environment),
|
|
46
49
|
...(environment.TERM ? { terminalType: environment.TERM } : {}),
|
|
@@ -104,6 +107,8 @@ function platformInspector(helperPath) {
|
|
|
104
107
|
return new WindowsNativeProcessInspector(helperPath);
|
|
105
108
|
if (process.platform === "linux")
|
|
106
109
|
return new LinuxNativeProcessInspector();
|
|
110
|
+
if (process.platform === "darwin")
|
|
111
|
+
return new DarwinNativeProcessInspector(helperPath);
|
|
107
112
|
throw diagnosticError(`launch-instance process containment is unsupported on ${process.platform}`, "CONTAINMENT_UNSUPPORTED");
|
|
108
113
|
}
|
|
109
114
|
function stopRequests(client, instanceId) {
|
|
@@ -8,6 +8,8 @@ export interface ProductPaths {
|
|
|
8
8
|
/** The metadata written by releases that predate cohort-scoped identity. */
|
|
9
9
|
readonly endpointMetadataPath: string;
|
|
10
10
|
readonly endpointsDir: string;
|
|
11
|
+
/** A short, A1-managed socket directory when the host imposes a small Unix-path limit. */
|
|
12
|
+
readonly endpointDirectory?: string | null;
|
|
11
13
|
readonly supervisorLogPath: string;
|
|
12
14
|
}
|
|
13
15
|
/** Where one cohort listens, and where it records that it is listening. */
|
|
@@ -19,7 +19,7 @@ export function resolveCohortEndpoint(paths, releaseId, environment = process.en
|
|
|
19
19
|
const token = createHash("sha256").update(releaseId).digest("hex").slice(0, 16);
|
|
20
20
|
const endpoint = hostPlatform === "win32"
|
|
21
21
|
? `${paths.endpoint}-${token}`
|
|
22
|
-
: path.join(paths.runtimeDir, `${token}-${PRODUCT_IDENTITY.endpoint.unixSocketFilename}`);
|
|
22
|
+
: path.join(paths.endpointDirectory ?? paths.runtimeDir, `${token}-${PRODUCT_IDENTITY.endpoint.unixSocketFilename}`);
|
|
23
23
|
return { endpoint, endpointMetadataPath: path.join(paths.endpointsDir, `${token}.json`) };
|
|
24
24
|
}
|
|
25
25
|
export function resolveProductPaths(environment = process.env, hostPlatform = platform(), fallbackHome = homedir()) {
|
|
@@ -46,10 +46,16 @@ export function resolveProductPaths(environment = process.env, hostPlatform = pl
|
|
|
46
46
|
? path.join(environment.XDG_RUNTIME_DIR, PRODUCT_IDENTITY.state.unixControlDirectory)
|
|
47
47
|
: path.join(dataDir, "runtime")));
|
|
48
48
|
const namespace = createHash("sha256").update(runtimeDir.toLowerCase()).digest("hex").slice(0, 20);
|
|
49
|
+
// Platform: Darwin limits Unix-domain socket paths to roughly 104 bytes. Runtime roots
|
|
50
|
+
// under /var/folders can exceed that before the cohort suffix is added, so only socket
|
|
51
|
+
// files use a short owned namespace while metadata remains under the selected runtime.
|
|
52
|
+
const endpointDirectory = !endpointOverride && hostPlatform === "darwin"
|
|
53
|
+
? posix.join("/tmp", `${PRODUCT_IDENTITY.state.unixControlDirectory}-${namespace}`)
|
|
54
|
+
: null;
|
|
49
55
|
const endpoint = endpointOverride
|
|
50
56
|
?? (windows
|
|
51
57
|
? `\\\\.\\pipe\\${PRODUCT_IDENTITY.endpoint.windowsPipeStem}-${namespace}`
|
|
52
|
-
: path.join(runtimeDir, PRODUCT_IDENTITY.endpoint.unixSocketFilename));
|
|
58
|
+
: path.join(endpointDirectory ?? runtimeDir, PRODUCT_IDENTITY.endpoint.unixSocketFilename));
|
|
53
59
|
return {
|
|
54
60
|
configDir,
|
|
55
61
|
dataDir,
|
|
@@ -58,6 +64,7 @@ export function resolveProductPaths(environment = process.env, hostPlatform = pl
|
|
|
58
64
|
endpoint,
|
|
59
65
|
endpointMetadataPath: path.join(runtimeDir, PRODUCT_IDENTITY.endpoint.metadataFilename),
|
|
60
66
|
endpointsDir: path.join(runtimeDir, "endpoints"),
|
|
67
|
+
endpointDirectory,
|
|
61
68
|
supervisorLogPath: path.join(runtimeDir, PRODUCT_IDENTITY.endpoint.supervisorLogFilename),
|
|
62
69
|
};
|
|
63
70
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Invocation-scoped selection; never inferred from a parent's active-session environment. */
|
|
2
|
+
export interface SessionSelection {
|
|
3
|
+
readonly target: string;
|
|
4
|
+
readonly sessionDir?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Shared public/internal grammar, deliberately not arbitrary Pi argument passthrough. */
|
|
7
|
+
export declare function parseSessionSelection(arguments_: readonly string[]): SessionSelection | undefined;
|
|
8
|
+
/** Encode only validated values into separate argv entries; no shell quoting or evaluation. */
|
|
9
|
+
export declare function sessionSelectionArguments(selection?: SessionSelection): string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Shared public/internal grammar, deliberately not arbitrary Pi argument passthrough. */
|
|
2
|
+
export function parseSessionSelection(arguments_) {
|
|
3
|
+
if (arguments_.length === 0)
|
|
4
|
+
return undefined;
|
|
5
|
+
let target;
|
|
6
|
+
let sessionDir;
|
|
7
|
+
for (let index = 0; index < arguments_.length; index += 2) {
|
|
8
|
+
const option = arguments_[index];
|
|
9
|
+
if (option !== "--session" && option !== "--session-dir")
|
|
10
|
+
throw new Error("expected --session <path|id> with optional --session-dir <dir>");
|
|
11
|
+
const value = arguments_[index + 1];
|
|
12
|
+
if (value === undefined || value.startsWith("-") || !validValue(value))
|
|
13
|
+
throw new Error(`${option} requires a nonempty value`);
|
|
14
|
+
if (option === "--session") {
|
|
15
|
+
if (target !== undefined)
|
|
16
|
+
throw new Error("--session may occur only once");
|
|
17
|
+
target = value;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
if (sessionDir !== undefined)
|
|
21
|
+
throw new Error("--session-dir may occur only once");
|
|
22
|
+
sessionDir = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (target === undefined)
|
|
26
|
+
throw new Error("--session-dir requires --session <path|id>");
|
|
27
|
+
return Object.freeze({ target, ...(sessionDir === undefined ? {} : { sessionDir }) });
|
|
28
|
+
}
|
|
29
|
+
/** Encode only validated values into separate argv entries; no shell quoting or evaluation. */
|
|
30
|
+
export function sessionSelectionArguments(selection) {
|
|
31
|
+
if (selection === undefined)
|
|
32
|
+
return [];
|
|
33
|
+
const args = [...(selection.sessionDir === undefined ? [] : ["--session-dir", selection.sessionDir]), "--session", selection.target];
|
|
34
|
+
parseSessionSelection(args);
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
function validValue(value) {
|
|
38
|
+
return value.trim().length > 0 && value.length <= 32_768 && !/[\u0000\r\n]/.test(value);
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
declare const STARTUP_SCHEMA = "a1-supervisor-startup-v1";
|
|
2
|
+
export interface SupervisorStartupAttemptIdentity {
|
|
3
|
+
readonly attemptId: string;
|
|
4
|
+
readonly releaseId: string;
|
|
5
|
+
readonly resultPath: string;
|
|
6
|
+
}
|
|
7
|
+
export type SupervisorStartupResult = {
|
|
8
|
+
readonly schema: typeof STARTUP_SCHEMA;
|
|
9
|
+
readonly attemptId: string;
|
|
10
|
+
readonly releaseId: string;
|
|
11
|
+
readonly outcome: "ready";
|
|
12
|
+
readonly pid: number;
|
|
13
|
+
} | {
|
|
14
|
+
readonly schema: typeof STARTUP_SCHEMA;
|
|
15
|
+
readonly attemptId: string;
|
|
16
|
+
readonly releaseId: string;
|
|
17
|
+
readonly outcome: "failure";
|
|
18
|
+
readonly stage: string;
|
|
19
|
+
readonly code: string | null;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
};
|
|
22
|
+
/** Create one unguessable startup result authority under the protected runtime root. */
|
|
23
|
+
export declare function createSupervisorStartupAttempt(runtimeDir: string, releaseId: string): Promise<SupervisorStartupAttemptIdentity>;
|
|
24
|
+
export declare function supervisorStartupResultPath(runtimeDir: string, attemptId: string): string;
|
|
25
|
+
/** Atomically publish the bounded result for one detached supervisor startup attempt. */
|
|
26
|
+
export declare function publishSupervisorStartupResult(path: string, result: SupervisorStartupResult): Promise<void>;
|
|
27
|
+
export declare function readSupervisorStartupResult(path: string, attemptId: string, releaseId: string): Promise<SupervisorStartupResult | null>;
|
|
28
|
+
export declare function supervisorStartupFailure(error: unknown, attemptId: string, releaseId: string, stage: string): SupervisorStartupResult;
|
|
29
|
+
export declare function supervisorStartupReady(attemptId: string, releaseId: string): SupervisorStartupResult;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
const STARTUP_SCHEMA = "a1-supervisor-startup-v1";
|
|
5
|
+
const MAX_RESULT_BYTES = 8 * 1024;
|
|
6
|
+
const MAX_MESSAGE_LENGTH = 1_024;
|
|
7
|
+
const MAX_RETAINED_RESULTS = 16;
|
|
8
|
+
const MAX_RESULT_AGE_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
9
|
+
const ATTEMPT_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
10
|
+
/** Create one unguessable startup result authority under the protected runtime root. */
|
|
11
|
+
export async function createSupervisorStartupAttempt(runtimeDir, releaseId) {
|
|
12
|
+
const attemptId = randomUUID();
|
|
13
|
+
const directory = resolve(runtimeDir, "supervisor-startup");
|
|
14
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
15
|
+
await pruneSupervisorStartupResults(directory);
|
|
16
|
+
return { attemptId, releaseId, resultPath: supervisorStartupResultPath(runtimeDir, attemptId) };
|
|
17
|
+
}
|
|
18
|
+
export function supervisorStartupResultPath(runtimeDir, attemptId) {
|
|
19
|
+
if (!ATTEMPT_PATTERN.test(attemptId))
|
|
20
|
+
throw new TypeError("supervisor startup attempt identity is invalid");
|
|
21
|
+
return resolve(runtimeDir, "supervisor-startup", `${attemptId}.json`);
|
|
22
|
+
}
|
|
23
|
+
/** Atomically publish the bounded result for one detached supervisor startup attempt. */
|
|
24
|
+
export async function publishSupervisorStartupResult(path, result) {
|
|
25
|
+
validateSupervisorStartupResult(result, result.attemptId, result.releaseId);
|
|
26
|
+
const body = `${JSON.stringify(result)}\n`;
|
|
27
|
+
if (Buffer.byteLength(body) > MAX_RESULT_BYTES)
|
|
28
|
+
throw new Error("supervisor startup result exceeds its size bound");
|
|
29
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
30
|
+
await writeFile(temporary, body, { encoding: "utf8", mode: 0o600 });
|
|
31
|
+
await rename(temporary, path);
|
|
32
|
+
}
|
|
33
|
+
export async function readSupervisorStartupResult(path, attemptId, releaseId) {
|
|
34
|
+
let metadata;
|
|
35
|
+
try {
|
|
36
|
+
metadata = await stat(path);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isNodeError(error, "ENOENT"))
|
|
40
|
+
return null;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (!metadata.isFile() || metadata.size > MAX_RESULT_BYTES)
|
|
44
|
+
throw new Error("supervisor startup result is invalid or oversized");
|
|
45
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
46
|
+
return validateSupervisorStartupResult(value, attemptId, releaseId);
|
|
47
|
+
}
|
|
48
|
+
export function supervisorStartupFailure(error, attemptId, releaseId, stage) {
|
|
49
|
+
const source = error instanceof Error ? error : new Error(String(error));
|
|
50
|
+
return {
|
|
51
|
+
schema: STARTUP_SCHEMA,
|
|
52
|
+
attemptId,
|
|
53
|
+
releaseId,
|
|
54
|
+
outcome: "failure",
|
|
55
|
+
stage: boundedToken(stage, "unknown"),
|
|
56
|
+
code: "code" in source && typeof source.code === "string" ? boundedToken(source.code, "UNKNOWN") : null,
|
|
57
|
+
message: sanitizeMessage(source.message),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function supervisorStartupReady(attemptId, releaseId) {
|
|
61
|
+
return { schema: STARTUP_SCHEMA, attemptId, releaseId, outcome: "ready", pid: process.pid };
|
|
62
|
+
}
|
|
63
|
+
async function pruneSupervisorStartupResults(directory) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
66
|
+
const files = (await Promise.all(entries.filter(entry => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => {
|
|
67
|
+
const path = resolve(directory, entry.name);
|
|
68
|
+
const metadata = await stat(path).catch(() => null);
|
|
69
|
+
return metadata ? { path, modified: metadata.mtimeMs } : null;
|
|
70
|
+
}))).filter((file) => file !== null);
|
|
71
|
+
files.sort((left, right) => right.modified - left.modified);
|
|
72
|
+
await Promise.all(files.flatMap((file, index) => index >= MAX_RETAINED_RESULTS - 1 || now - file.modified > MAX_RESULT_AGE_MS
|
|
73
|
+
? [rm(file.path, { force: true })]
|
|
74
|
+
: []));
|
|
75
|
+
}
|
|
76
|
+
function validateSupervisorStartupResult(value, attemptId, releaseId) {
|
|
77
|
+
if (!value || typeof value !== "object")
|
|
78
|
+
throw new Error("supervisor startup result is malformed");
|
|
79
|
+
const result = value;
|
|
80
|
+
if (result.schema !== STARTUP_SCHEMA || result.attemptId !== attemptId || result.releaseId !== releaseId) {
|
|
81
|
+
throw new Error("supervisor startup result does not match the selected attempt and release");
|
|
82
|
+
}
|
|
83
|
+
if (result.outcome === "ready" && Number.isSafeInteger(result.pid) && Number(result.pid) > 0)
|
|
84
|
+
return result;
|
|
85
|
+
if (result.outcome === "failure" && typeof result.stage === "string" && result.stage.length > 0 && result.stage.length <= 128
|
|
86
|
+
&& (result.code === null || typeof result.code === "string") && typeof result.message === "string" && result.message.length <= MAX_MESSAGE_LENGTH) {
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
throw new Error("supervisor startup result is malformed");
|
|
90
|
+
}
|
|
91
|
+
function sanitizeMessage(message) {
|
|
92
|
+
let value = message.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/:\/\/[^\s/@:]+:[^\s/@]+@/g, "://<redacted>@");
|
|
93
|
+
for (const [name, secret] of Object.entries(process.env)) {
|
|
94
|
+
if (!secret || secret.length < 4 || !/(?:TOKEN|SECRET|PASSWORD|COOKIE|CREDENTIAL|AUTH)/i.test(name))
|
|
95
|
+
continue;
|
|
96
|
+
value = value.split(secret).join("<redacted>");
|
|
97
|
+
}
|
|
98
|
+
return value.trim().slice(0, MAX_MESSAGE_LENGTH) || "supervisor startup failed";
|
|
99
|
+
}
|
|
100
|
+
function boundedToken(value, fallback) {
|
|
101
|
+
const normalized = value.replace(/[^A-Za-z0-9_.:-]/g, "-").slice(0, 128);
|
|
102
|
+
return normalized || fallback;
|
|
103
|
+
}
|
|
104
|
+
function isNodeError(error, code) {
|
|
105
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
106
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { WindowsNativeProcessInspector, type InspectorCommandRunner } from "./windows-process-inspector.js";
|
|
2
|
+
/** Verifies Darwin process identity through the bounded native guardian inspection command. */
|
|
3
|
+
export declare class DarwinNativeProcessInspector extends WindowsNativeProcessInspector {
|
|
4
|
+
constructor(helperPath: string, runner?: InspectorCommandRunner);
|
|
5
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { WindowsNativeProcessInspector } from "./windows-process-inspector.js";
|
|
2
|
+
/** Verifies Darwin process identity through the bounded native guardian inspection command. */
|
|
3
|
+
export class DarwinNativeProcessInspector extends WindowsNativeProcessInspector {
|
|
4
|
+
constructor(helperPath, runner) {
|
|
5
|
+
super(helperPath, runner, "darwin-proc-start:");
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -12,7 +12,8 @@ export interface InspectorCommandRunner {
|
|
|
12
12
|
export declare class WindowsNativeProcessInspector implements NativeProcessInspector {
|
|
13
13
|
readonly helperPath: string;
|
|
14
14
|
private readonly runner;
|
|
15
|
-
|
|
15
|
+
private readonly identityPrefix;
|
|
16
|
+
constructor(helperPath: string, runner?: InspectorCommandRunner, identityPrefix?: string);
|
|
16
17
|
observe(pid: number): Promise<NativeProcessIdentity | null>;
|
|
17
18
|
matches(identity: NativeProcessIdentity): Promise<boolean>;
|
|
18
19
|
}
|
|
@@ -3,9 +3,11 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
export class WindowsNativeProcessInspector {
|
|
4
4
|
helperPath;
|
|
5
5
|
runner;
|
|
6
|
-
|
|
6
|
+
identityPrefix;
|
|
7
|
+
constructor(helperPath, runner = defaultRunner, identityPrefix = "windows-filetime:") {
|
|
7
8
|
this.helperPath = helperPath;
|
|
8
9
|
this.runner = runner;
|
|
10
|
+
this.identityPrefix = identityPrefix;
|
|
9
11
|
if (!helperPath || helperPath.includes("\0"))
|
|
10
12
|
throw new TypeError("process guardian helper path is invalid");
|
|
11
13
|
}
|
|
@@ -26,7 +28,7 @@ export class WindowsNativeProcessInspector {
|
|
|
26
28
|
catch {
|
|
27
29
|
throw Object.assign(new Error("process guardian returned malformed identity data"), { code: "PROCESS_INSPECTION_INVALID" });
|
|
28
30
|
}
|
|
29
|
-
if (!isIdentity(value) || value.pid !== pid || !value.startIdentity.startsWith(
|
|
31
|
+
if (!isIdentity(value) || value.pid !== pid || !value.startIdentity.startsWith(this.identityPrefix)) {
|
|
30
32
|
throw Object.assign(new Error("process guardian returned mismatched identity data"), { code: "PROCESS_INSPECTION_INVALID" });
|
|
31
33
|
}
|
|
32
34
|
return value;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type OwnershipProbe } from "./cohort-selection.js";
|
|
2
2
|
import { type SupervisorEndpointMetadata } from "./cohort-state.js";
|
|
3
|
-
import { resolveProductPaths, type LaunchProfileId } from "../lifecycle/index.js";
|
|
3
|
+
import { resolveProductPaths, type SessionSelection, type LaunchProfileId, type SupervisorStartupAttemptIdentity } from "../lifecycle/index.js";
|
|
4
4
|
import { cleanupProvenIdleOwner } from "./process-cleanup.js";
|
|
5
5
|
import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
|
|
6
6
|
export interface BootstrapOptions {
|
|
@@ -8,6 +8,7 @@ export interface BootstrapOptions {
|
|
|
8
8
|
readonly launchIntent?: {
|
|
9
9
|
readonly kind: "interactive";
|
|
10
10
|
readonly profileId: LaunchProfileId;
|
|
11
|
+
readonly sessionSelection?: SessionSelection;
|
|
11
12
|
};
|
|
12
13
|
readonly environment?: NodeJS.ProcessEnv;
|
|
13
14
|
readonly output?: Pick<NodeJS.WriteStream, "write">;
|
|
@@ -26,9 +27,15 @@ export interface BootstrapOptions {
|
|
|
26
27
|
}
|
|
27
28
|
export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
|
|
28
29
|
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
|
|
29
|
-
export
|
|
30
|
+
export interface SupervisorStartupAttempt extends SupervisorStartupAttemptIdentity {
|
|
31
|
+
readonly childOutcome: Promise<{
|
|
32
|
+
readonly exitCode: number | null;
|
|
33
|
+
readonly signal: NodeJS.Signals | null;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function startSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<SupervisorStartupAttempt>;
|
|
30
37
|
export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease): NodeJS.ProcessEnv;
|
|
31
|
-
export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number): Promise<void>;
|
|
38
|
+
export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number, startup?: SupervisorStartupAttempt): Promise<void>;
|
|
32
39
|
export declare function readEndpointMetadata(path: string): Promise<SupervisorEndpointMetadata | null>;
|
|
33
40
|
export declare function probeOwnership(metadata: SupervisorEndpointMetadata): Promise<OwnershipProbe>;
|
|
34
41
|
export declare function releaseVerifiedIdleOwner(metadata: SupervisorEndpointMetadata, dataDir: string, operations?: {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { connect } from "node:net";
|
|
4
4
|
import { platform } from "node:os";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { selectCohortLaunch } from "./cohort-selection.js";
|
|
7
7
|
import { CohortStateStore } from "./cohort-state.js";
|
|
8
|
-
import { assertLaunchProfileId, resolveCohortEndpoint, resolveProductPaths } from "../lifecycle/index.js";
|
|
8
|
+
import { assertLaunchProfileId, createSupervisorStartupAttempt, readSupervisorStartupResult, resolveCohortEndpoint, resolveProductPaths, sessionSelectionArguments } from "../lifecycle/index.js";
|
|
9
9
|
import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
|
|
10
10
|
import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
|
|
11
11
|
import { sweepDeadEndpoints } from "./endpoints.js";
|
|
12
12
|
import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
13
13
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
14
|
+
import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
|
|
14
15
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
15
16
|
import { markStartupPhase } from "../startup/index.js";
|
|
16
17
|
export async function runBootstrap(options) {
|
|
@@ -18,6 +19,9 @@ export async function runBootstrap(options) {
|
|
|
18
19
|
await markStartupPhase(environment, "bootstrap-start");
|
|
19
20
|
const launchProfileId = options.launchIntent?.profileId ?? "a1";
|
|
20
21
|
assertLaunchProfileId(launchProfileId);
|
|
22
|
+
const sessionArgs = sessionSelectionArguments(options.launchIntent?.sessionSelection);
|
|
23
|
+
if (sessionArgs.length > 0 && launchProfileId !== "a1")
|
|
24
|
+
throw new Error("session selection requires the normal A1 profile");
|
|
21
25
|
environment[PRODUCT_IDENTITY.environment.launchProfile] = launchProfileId;
|
|
22
26
|
const output = options.output ?? process.stderr;
|
|
23
27
|
const paths = resolveProductPaths(environment);
|
|
@@ -60,16 +64,27 @@ export async function runBootstrap(options) {
|
|
|
60
64
|
&& endpoint.contentDigest === active.contentDigest;
|
|
61
65
|
if (endpointMatches && probe === "live-verified") {
|
|
62
66
|
const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
|
|
63
|
-
return await launchUi(retained, environment);
|
|
67
|
+
return await launchUi(retained, environment, sessionArgs);
|
|
64
68
|
}
|
|
65
69
|
if (endpoint === null || probe === "dead") {
|
|
66
70
|
if (endpoint)
|
|
67
71
|
await removeEndpointArtifacts(endpointPaths.endpointMetadataPath, endpointPaths.endpoint);
|
|
68
|
-
|
|
72
|
+
await markStartupPhase(environment, "durable-validation-start");
|
|
73
|
+
const retained = await readRestartCertifiedRelease(active, paths.dataDir).catch(async () => {
|
|
74
|
+
const verified = await readMaterializedRelease(active.releaseRoot, resolve(paths.dataDir, "releases"));
|
|
75
|
+
if (verified.releaseId !== active.releaseId || verified.contentDigest !== active.contentDigest || verified.packageVersion !== active.packageVersion) {
|
|
76
|
+
throw new Error("completely verified release differs from the approved active record");
|
|
77
|
+
}
|
|
78
|
+
await certifyMaterializedRelease(verified, paths.dataDir);
|
|
79
|
+
return verified;
|
|
80
|
+
});
|
|
81
|
+
await markStartupPhase(environment, "durable-validation-complete");
|
|
69
82
|
const retainedPaths = resolveCohortEndpoint(paths, retained.releaseId, environment);
|
|
70
|
-
await
|
|
71
|
-
await
|
|
72
|
-
|
|
83
|
+
await markStartupPhase(environment, "replacement-supervisor-start");
|
|
84
|
+
const startup = await startSupervisor(retained, environment);
|
|
85
|
+
await waitForVerifiedEndpoint(retainedPaths.endpointMetadataPath, retained, 8_000, startup);
|
|
86
|
+
await markStartupPhase(environment, "replacement-supervisor-ready");
|
|
87
|
+
return await launchUi(retained, environment, sessionArgs);
|
|
73
88
|
}
|
|
74
89
|
}
|
|
75
90
|
const candidate = await materializeRelease(options.packageRoot, paths.dataDir);
|
|
@@ -126,7 +141,7 @@ export async function runBootstrap(options) {
|
|
|
126
141
|
if (decision.recordPending && endpoint) {
|
|
127
142
|
await stateStore.blockPending("candidate activation deferred by live non-resumable instances", endpoint.ownership.nonResumableInstanceIds);
|
|
128
143
|
}
|
|
129
|
-
const code = await launchUi(retained, environment);
|
|
144
|
+
const code = await launchUi(retained, environment, sessionArgs);
|
|
130
145
|
if (decision.recordPending)
|
|
131
146
|
await activatePendingAfterBlockerExit(candidate, stateStore, paths, environment);
|
|
132
147
|
return code;
|
|
@@ -163,9 +178,9 @@ export async function runBootstrap(options) {
|
|
|
163
178
|
else {
|
|
164
179
|
selected = await readMaterializedRelease(decision.releaseRoot);
|
|
165
180
|
}
|
|
166
|
-
await startSupervisor(selected, environment);
|
|
167
|
-
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, selected.releaseId, environment).endpointMetadataPath, selected, 8_000);
|
|
168
|
-
return await launchUi(selected, environment);
|
|
181
|
+
const startup = await startSupervisor(selected, environment);
|
|
182
|
+
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, selected.releaseId, environment).endpointMetadataPath, selected, 8_000, startup);
|
|
183
|
+
return await launchUi(selected, environment, sessionArgs);
|
|
169
184
|
}
|
|
170
185
|
async function readInstalledVersion(packageRoot) {
|
|
171
186
|
const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
@@ -179,34 +194,37 @@ export async function certifyMaterializedRelease(release, dataDir, verification
|
|
|
179
194
|
await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
|
|
180
195
|
}
|
|
181
196
|
const path = resolve(dataDir, `certification-${release.releaseId}.json`);
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
verifiedAt: new Date().toISOString(),
|
|
187
|
-
checks: [{ id: "immutable-content", passed: true }],
|
|
188
|
-
}, null, 2), { mode: 0o600 });
|
|
197
|
+
const restartSeal = await createRestartSeal(release, dataDir);
|
|
198
|
+
await chmod(path, 0o600).catch(() => { });
|
|
199
|
+
await writeFile(path, JSON.stringify(releaseCertificationDocument(release, restartSeal), null, 2), { mode: 0o600 });
|
|
200
|
+
await chmod(path, 0o400);
|
|
189
201
|
return path;
|
|
190
202
|
}
|
|
191
203
|
export async function startSupervisor(release, environment) {
|
|
192
204
|
const entry = await resolveReleaseEntryPoint(release, "bin/supervisor.js");
|
|
193
|
-
const
|
|
205
|
+
const paths = resolveProductPaths(environment);
|
|
206
|
+
const attempt = await createSupervisorStartupAttempt(paths.runtimeDir, release.releaseId);
|
|
207
|
+
const child = spawn(process.execPath, [entry, "--startup-attempt", attempt.attemptId], {
|
|
194
208
|
detached: true,
|
|
195
209
|
env: releaseEnvironment(environment, release),
|
|
196
210
|
stdio: "ignore",
|
|
197
211
|
windowsHide: true,
|
|
198
212
|
});
|
|
213
|
+
const childOutcome = new Promise(resolvePromise => {
|
|
214
|
+
child.once("close", (exitCode, signal) => resolvePromise({ exitCode, signal }));
|
|
215
|
+
});
|
|
199
216
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
200
217
|
child.once("spawn", resolvePromise);
|
|
201
218
|
child.once("error", rejectPromise);
|
|
202
219
|
});
|
|
203
220
|
child.unref();
|
|
221
|
+
return { ...attempt, childOutcome };
|
|
204
222
|
}
|
|
205
|
-
async function launchUi(release, environment) {
|
|
223
|
+
async function launchUi(release, environment, sessionArgs) {
|
|
206
224
|
await markStartupPhase(environment, "bootstrap-selected");
|
|
207
225
|
const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
|
|
208
226
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
209
|
-
const child = spawn(process.execPath, [entry], {
|
|
227
|
+
const child = spawn(process.execPath, [entry, ...sessionArgs], {
|
|
210
228
|
env: releaseEnvironment(environment, release),
|
|
211
229
|
stdio: "inherit",
|
|
212
230
|
windowsHide: false,
|
|
@@ -224,12 +242,27 @@ export function releaseEnvironment(environment, release) {
|
|
|
224
242
|
[PRODUCT_IDENTITY.environment.releaseDigest]: release.contentDigest,
|
|
225
243
|
};
|
|
226
244
|
}
|
|
227
|
-
export async function waitForVerifiedEndpoint(path, release, timeoutMs) {
|
|
245
|
+
export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup) {
|
|
228
246
|
const deadline = Date.now() + timeoutMs;
|
|
247
|
+
let childOutcome = null;
|
|
248
|
+
void startup?.childOutcome.then(outcome => { childOutcome = outcome; });
|
|
229
249
|
while (Date.now() < deadline) {
|
|
230
250
|
const metadata = await readEndpointMetadata(path);
|
|
231
|
-
if (metadata && metadata.releaseId === release.releaseId && await probeOwnership(metadata) === "live-verified")
|
|
251
|
+
if (metadata && metadata.releaseId === release.releaseId && await probeOwnership(metadata) === "live-verified") {
|
|
252
|
+
if (startup)
|
|
253
|
+
await rm(startup.resultPath, { force: true });
|
|
232
254
|
return;
|
|
255
|
+
}
|
|
256
|
+
if (startup) {
|
|
257
|
+
const result = await readSupervisorStartupResult(startup.resultPath, startup.attemptId, startup.releaseId);
|
|
258
|
+
if (result?.outcome === "failure") {
|
|
259
|
+
throw Object.assign(new Error(PRODUCT_TEXT.diagnostic(`supervisor startup failed at ${result.stage}: ${result.message}`)), {
|
|
260
|
+
code: result.code ?? "SUPERVISOR_STARTUP_FAILED",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (childOutcome)
|
|
265
|
+
throw new Error(PRODUCT_TEXT.diagnostic(`supervisor exited before readiness: ${JSON.stringify(childOutcome)}`));
|
|
233
266
|
await new Promise(resolvePromise => setTimeout(resolvePromise, 40));
|
|
234
267
|
}
|
|
235
268
|
throw new Error(PRODUCT_TEXT.diagnostic(`supervisor did not publish verified endpoint metadata within ${timeoutMs}ms`));
|
|
@@ -326,8 +359,8 @@ async function activatePendingAfterBlockerExit(candidate, stateStore, paths, env
|
|
|
326
359
|
return;
|
|
327
360
|
await removeEndpointArtifacts(paths.endpointMetadataPath, paths.endpoint);
|
|
328
361
|
await stateStore.activate(candidate.releaseId);
|
|
329
|
-
await startSupervisor(candidate, environment);
|
|
330
|
-
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000);
|
|
362
|
+
const startup = await startSupervisor(candidate, environment);
|
|
363
|
+
await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000, startup);
|
|
331
364
|
}
|
|
332
365
|
export async function releaseVerifiedIdleOwner(metadata, dataDir, operations = {}) {
|
|
333
366
|
try {
|