@timurproko/a1 0.1.1-dev.1 → 0.1.1-dev.11
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 +8 -43
- package/bin/guardian.js +22 -0
- package/bin/{a1-ui.js → ui.js} +4 -8
- package/dist/native/darwin-arm64/manifest.json +20 -0
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/linux-x64/manifest.json +20 -0
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +20 -0
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/src/composition/index.d.ts +26 -0
- package/dist/src/composition/index.js +94 -2
- package/dist/src/features/launch/initialize-profile.js +23 -2
- package/dist/src/features/launch/profiles.d.ts +1 -1
- package/dist/src/features/launch/profiles.js +2 -2
- package/dist/src/features/launch/runtime-selection.d.ts +13 -8
- package/dist/src/features/launch/runtime-selection.js +6 -6
- package/dist/src/features/owned-ui/index.d.ts +1 -0
- package/dist/src/features/owned-ui/index.js +1 -0
- package/dist/src/features/owned-ui/run.d.ts +6 -0
- package/dist/src/features/owned-ui/run.js +3 -1
- package/dist/src/features/owned-ui/settings-app.d.ts +22 -0
- package/dist/src/features/owned-ui/settings-app.js +633 -0
- package/dist/src/foundation/agent-engine-contracts/domain.d.ts +26 -0
- package/dist/src/foundation/launch-guardian/index.d.ts +1 -0
- package/dist/src/foundation/launch-guardian/index.js +1 -0
- package/dist/src/foundation/launch-guardian/main.d.ts +31 -0
- package/dist/src/foundation/launch-guardian/main.js +165 -0
- package/dist/src/foundation/lifecycle/commands.d.ts +40 -0
- package/dist/src/foundation/lifecycle/commands.js +1 -0
- package/dist/src/foundation/lifecycle/index.d.ts +2 -0
- package/dist/src/foundation/lifecycle/index.js +2 -0
- package/dist/src/foundation/lifecycle/launch-instance.d.ts +46 -0
- package/dist/src/foundation/lifecycle/launch-instance.js +134 -0
- package/dist/src/foundation/lifecycle/model.d.ts +6 -42
- package/dist/src/foundation/owned-ui-settings/declarations.d.ts +13 -0
- package/dist/src/foundation/owned-ui-settings/declarations.js +39 -0
- package/dist/src/foundation/owned-ui-settings/index.d.ts +6 -0
- package/dist/src/foundation/owned-ui-settings/index.js +6 -0
- package/dist/src/foundation/owned-ui-settings/migrations.d.ts +13 -0
- package/dist/src/foundation/owned-ui-settings/migrations.js +28 -0
- package/dist/src/foundation/owned-ui-settings/resolution.d.ts +45 -0
- package/dist/src/foundation/owned-ui-settings/resolution.js +147 -0
- package/dist/src/foundation/owned-ui-settings/sections.d.ts +70 -0
- package/dist/src/foundation/owned-ui-settings/sections.js +93 -0
- package/dist/src/foundation/owned-ui-settings/session.d.ts +44 -0
- package/dist/src/foundation/owned-ui-settings/session.js +145 -0
- package/dist/src/foundation/owned-ui-settings/store.d.ts +28 -0
- package/dist/src/foundation/owned-ui-settings/store.js +90 -0
- package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -0
- package/dist/src/foundation/pi-component-adapter/index.js +1 -0
- package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.d.ts +9 -1
- package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +10 -8
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.d.ts +8 -0
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.js +12 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +17 -3
- package/dist/src/foundation/pi-engine-adapter/adapter.js +80 -7
- package/dist/src/foundation/pi-engine-adapter/pi-settings-metadata.json +374 -0
- package/dist/src/foundation/pi-engine-adapter/public-main-entry.js +10 -1
- package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +40 -2
- package/dist/src/foundation/pi-engine-adapter/settings-integration.js +172 -24
- package/dist/src/foundation/pi-engine-adapter/workflows.d.ts +9 -0
- package/dist/src/foundation/pi-owned-ui-integration/index.d.ts +1 -0
- package/dist/src/foundation/pi-owned-ui-integration/index.js +1 -0
- package/dist/src/foundation/pi-owned-ui-integration/route-host.d.ts +29 -0
- package/dist/src/foundation/pi-owned-ui-integration/route-host.js +1 -0
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +71 -1
- package/dist/src/foundation/pi-tui-runtime-adapter/adapter.d.ts +6 -0
- package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +9 -0
- package/dist/src/foundation/process-containment/artifact.d.ts +1 -0
- package/dist/src/foundation/process-containment/artifact.js +34 -0
- package/dist/src/foundation/process-containment/contracts.d.ts +31 -0
- package/dist/src/foundation/process-containment/contracts.js +35 -0
- package/dist/src/foundation/process-containment/index.d.ts +5 -0
- package/dist/src/foundation/process-containment/index.js +5 -0
- package/dist/src/foundation/process-containment/linux-process-inspector.d.ts +6 -0
- package/dist/src/foundation/process-containment/linux-process-inspector.js +26 -0
- package/dist/src/foundation/process-containment/native-guardian-containment.d.ts +16 -0
- package/dist/src/foundation/process-containment/native-guardian-containment.js +139 -0
- package/dist/src/foundation/process-containment/windows-process-inspector.d.ts +17 -0
- package/dist/src/foundation/process-containment/windows-process-inspector.js +53 -0
- package/dist/src/foundation/protocol/client.d.ts +2 -1
- package/dist/src/foundation/protocol/client.js +3 -0
- package/dist/src/foundation/protocol/messages.d.ts +5 -5
- package/dist/src/foundation/protocol/messages.js +1 -1
- package/dist/src/foundation/release/bootstrap.d.ts +2 -2
- package/dist/src/foundation/release/bootstrap.js +69 -18
- package/dist/src/foundation/release/cohort-selection.js +3 -3
- package/dist/src/foundation/release/cohort-state.d.ts +3 -2
- package/dist/src/foundation/release/concurrency.d.ts +1 -0
- package/dist/src/foundation/release/concurrency.js +17 -0
- package/dist/src/foundation/release/process-cleanup.d.ts +1 -1
- package/dist/src/foundation/release/process-cleanup.js +2 -2
- package/dist/src/foundation/release/release-store.d.ts +35 -2
- package/dist/src/foundation/release/release-store.js +90 -31
- package/dist/src/foundation/release/release.d.ts +15 -0
- package/dist/src/foundation/release/release.js +65 -36
- package/dist/src/foundation/release/update.d.ts +19 -2
- package/dist/src/foundation/release/update.js +132 -16
- package/dist/src/foundation/storage/control-store.d.ts +8 -7
- package/dist/src/foundation/storage/control-store.js +105 -55
- package/dist/src/foundation/supervision/main.js +6 -5
- package/dist/src/foundation/supervision/server.d.ts +4 -2
- package/dist/src/foundation/supervision/server.js +254 -87
- package/dist/src/foundation/ui-apps/contracts.d.ts +42 -0
- package/dist/src/foundation/ui-apps/contracts.js +12 -0
- package/dist/src/foundation/ui-apps/host.d.ts +42 -0
- package/dist/src/foundation/ui-apps/host.js +152 -0
- package/dist/src/foundation/ui-apps/index.d.ts +3 -0
- package/dist/src/foundation/ui-apps/index.js +3 -0
- package/dist/src/foundation/ui-apps/registry.d.ts +11 -0
- package/dist/src/foundation/ui-apps/registry.js +32 -0
- package/dist/src/foundation/ui-components/dialog-panel.d.ts +36 -0
- package/dist/src/foundation/ui-components/dialog-panel.js +47 -0
- package/dist/src/foundation/ui-components/frame.d.ts +21 -0
- package/dist/src/foundation/ui-components/frame.js +54 -0
- package/dist/src/foundation/ui-components/index.d.ts +18 -0
- package/dist/src/foundation/ui-components/index.js +18 -0
- package/dist/src/foundation/ui-components/label.d.ts +8 -0
- package/dist/src/foundation/ui-components/label.js +36 -0
- package/dist/src/foundation/ui-components/line-input.d.ts +74 -0
- package/dist/src/foundation/ui-components/line-input.js +245 -0
- package/dist/src/foundation/ui-components/list-block.d.ts +66 -0
- package/dist/src/foundation/ui-components/list-block.js +176 -0
- package/dist/src/foundation/ui-components/list-view.d.ts +66 -0
- package/dist/src/foundation/ui-components/list-view.js +76 -0
- package/dist/src/foundation/ui-components/mouse.d.ts +15 -0
- package/dist/src/foundation/ui-components/mouse.js +46 -0
- package/dist/src/foundation/ui-components/pane.d.ts +33 -0
- package/dist/src/foundation/ui-components/pane.js +7 -0
- package/dist/src/foundation/ui-components/revision.d.ts +35 -0
- package/dist/src/foundation/ui-components/revision.js +79 -0
- package/dist/src/foundation/ui-components/scrollbar.d.ts +62 -0
- package/dist/src/foundation/ui-components/scrollbar.js +102 -0
- package/dist/src/foundation/ui-components/shortcuts.d.ts +57 -0
- package/dist/src/foundation/ui-components/shortcuts.js +81 -0
- package/dist/src/foundation/ui-components/spans.d.ts +7 -0
- package/dist/src/foundation/ui-components/spans.js +59 -0
- package/dist/src/foundation/ui-components/status-line.d.ts +16 -0
- package/dist/src/foundation/ui-components/status-line.js +14 -0
- package/dist/src/foundation/ui-components/stepper.d.ts +24 -0
- package/dist/src/foundation/ui-components/stepper.js +33 -0
- package/dist/src/foundation/ui-components/surface.d.ts +20 -0
- package/dist/src/foundation/ui-components/surface.js +38 -0
- package/dist/src/foundation/ui-components/text.d.ts +24 -0
- package/dist/src/foundation/ui-components/text.js +123 -0
- package/dist/src/foundation/ui-components/theme.d.ts +23 -0
- package/dist/src/foundation/ui-components/theme.js +13 -0
- package/dist/src/foundation/ui-components/value-menu.d.ts +41 -0
- package/dist/src/foundation/ui-components/value-menu.js +33 -0
- package/dist/src/product-identity.d.ts +2 -2
- package/dist/src/product-identity.js +2 -1
- package/dist/src/product-identity.json +8 -5
- package/docs/architecture/boundaries.md +31 -13
- package/docs/architecture/process-guardian-provenance.md +26 -0
- package/docs/architecture/toolchain.md +9 -3
- package/docs/architecture/ui-reference-provenance.md +50 -0
- package/docs/ci-release-runbook.md +67 -0
- package/docs/features/launch-profiles.md +7 -1
- package/docs/manual-launch-instance-acceptance.md +66 -0
- package/docs/manual-terminal-colour-check.md +55 -0
- package/package.json +20 -14
- package/dist/src/composition/transparent-runtime.d.ts +0 -6
- package/dist/src/composition/transparent-runtime.js +0 -19
- package/dist/src/foundation/transparent-terminal/command-resolution.d.ts +0 -13
- package/dist/src/foundation/transparent-terminal/command-resolution.js +0 -69
- package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +0 -32
- package/dist/src/foundation/transparent-terminal/foreground-broker.js +0 -87
- package/dist/src/foundation/transparent-terminal/index.d.ts +0 -4
- package/dist/src/foundation/transparent-terminal/index.js +0 -4
- package/dist/src/foundation/transparent-terminal/main.d.ts +0 -9
- package/dist/src/foundation/transparent-terminal/main.js +0 -94
- package/dist/src/foundation/transparent-terminal/native-launcher.d.ts +0 -25
- package/dist/src/foundation/transparent-terminal/native-launcher.js +0 -146
- /package/bin/{a1.js → cli.js} +0 -0
- /package/bin/{a1-supervisor.js → supervisor.js} +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NativeProcessIdentity } from "../lifecycle/index.js";
|
|
2
|
+
import type { NativeProcessInspector } from "./contracts.js";
|
|
3
|
+
export declare class LinuxNativeProcessInspector implements NativeProcessInspector {
|
|
4
|
+
observe(pid: number): Promise<NativeProcessIdentity | null>;
|
|
5
|
+
matches(identity: NativeProcessIdentity): Promise<boolean>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export class LinuxNativeProcessInspector {
|
|
3
|
+
async observe(pid) {
|
|
4
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
5
|
+
throw new TypeError("inspected PID must be a positive safe integer");
|
|
6
|
+
let stat;
|
|
7
|
+
try {
|
|
8
|
+
stat = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
12
|
+
return null;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
16
|
+
const fields = commandEnd >= 0 ? stat.slice(commandEnd + 1).trim().split(/\s+/) : [];
|
|
17
|
+
const startTicks = fields[19];
|
|
18
|
+
if (!startTicks || !/^\d+$/.test(startTicks))
|
|
19
|
+
throw new Error("process stat has no valid start-time field");
|
|
20
|
+
return { pid, startIdentity: `linux-proc-start:${startTicks}` };
|
|
21
|
+
}
|
|
22
|
+
async matches(identity) {
|
|
23
|
+
const observed = await this.observe(identity.pid);
|
|
24
|
+
return observed !== null && observed.startIdentity === identity.startIdentity;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { NativeProcessIdentity, ProcessContainmentIdentity } from "../lifecycle/index.js";
|
|
2
|
+
import type { ContainedProcessHandle, ContainedSpawnOptions, ProcessContainment } from "./contracts.js";
|
|
3
|
+
export declare class NativeGuardianContainment implements ProcessContainment {
|
|
4
|
+
#private;
|
|
5
|
+
readonly instanceId: string;
|
|
6
|
+
readonly helperPath: string;
|
|
7
|
+
readonly statusPath: string;
|
|
8
|
+
constructor(instanceId: string, helperPath: string, statusPath: string);
|
|
9
|
+
get identity(): ProcessContainmentIdentity;
|
|
10
|
+
spawn(executable: string, arguments_: readonly string[], options: ContainedSpawnOptions): Promise<ContainedProcessHandle>;
|
|
11
|
+
contains(identity: NativeProcessIdentity): Promise<boolean>;
|
|
12
|
+
stop(force: boolean): Promise<void>;
|
|
13
|
+
waitForEmpty(timeoutMs: number): Promise<boolean>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function resolveProcessGuardianPath(releaseRoot: string, environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, architecture?: string): string;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
|
+
export class NativeGuardianContainment {
|
|
6
|
+
instanceId;
|
|
7
|
+
helperPath;
|
|
8
|
+
statusPath;
|
|
9
|
+
#identity;
|
|
10
|
+
#helper = null;
|
|
11
|
+
#rootIdentity = null;
|
|
12
|
+
#outcome = null;
|
|
13
|
+
constructor(instanceId, helperPath, statusPath) {
|
|
14
|
+
this.instanceId = instanceId;
|
|
15
|
+
this.helperPath = helperPath;
|
|
16
|
+
this.statusPath = statusPath;
|
|
17
|
+
this.#identity = { provider: "native-guardian-pending", token: instanceId };
|
|
18
|
+
}
|
|
19
|
+
get identity() {
|
|
20
|
+
return this.#identity;
|
|
21
|
+
}
|
|
22
|
+
async spawn(executable, arguments_, options) {
|
|
23
|
+
if (this.#helper)
|
|
24
|
+
throw new Error("process containment already has a root runtime");
|
|
25
|
+
await mkdir(dirname(this.statusPath), { recursive: true, mode: 0o700 });
|
|
26
|
+
await rm(this.statusPath, { force: true });
|
|
27
|
+
const helper = spawn(this.helperPath, [
|
|
28
|
+
"--parent-pid", String(process.pid),
|
|
29
|
+
"--instance", this.instanceId,
|
|
30
|
+
"--status-file", this.statusPath,
|
|
31
|
+
"--", executable, ...arguments_,
|
|
32
|
+
], {
|
|
33
|
+
cwd: options.cwd,
|
|
34
|
+
env: { ...options.environment, ...(options.terminalType ? { TERM: options.terminalType } : {}) },
|
|
35
|
+
shell: false,
|
|
36
|
+
stdio: "inherit",
|
|
37
|
+
windowsHide: false,
|
|
38
|
+
});
|
|
39
|
+
this.#helper = helper;
|
|
40
|
+
this.#outcome = helperOutcome(helper);
|
|
41
|
+
const status = await waitForReadyStatus(this.statusPath, this.#outcome, 5_000);
|
|
42
|
+
this.#rootIdentity = { pid: status.pid, startIdentity: status.startIdentity };
|
|
43
|
+
this.#identity = { provider: status.containmentProvider, token: status.containmentToken };
|
|
44
|
+
return { identity: this.#rootIdentity, outcome: this.#outcome };
|
|
45
|
+
}
|
|
46
|
+
async contains(identity) {
|
|
47
|
+
return this.#rootIdentity !== null
|
|
48
|
+
&& identity.pid === this.#rootIdentity.pid
|
|
49
|
+
&& identity.startIdentity === this.#rootIdentity.startIdentity;
|
|
50
|
+
}
|
|
51
|
+
async stop(force) {
|
|
52
|
+
const root = this.#rootIdentity;
|
|
53
|
+
if (!root)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
process.kill(root.pid, force ? "SIGKILL" : "SIGTERM");
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}
|
|
60
|
+
async waitForEmpty(timeoutMs) {
|
|
61
|
+
if (!this.#helper || this.#helper.exitCode !== null || this.#helper.signalCode !== null)
|
|
62
|
+
return true;
|
|
63
|
+
return await new Promise(resolvePromise => {
|
|
64
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
65
|
+
timer.unref();
|
|
66
|
+
const finish = (empty) => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
this.#helper?.off("close", onClose);
|
|
69
|
+
resolvePromise(empty);
|
|
70
|
+
};
|
|
71
|
+
const onClose = () => finish(true);
|
|
72
|
+
this.#helper?.once("close", onClose);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async close() {
|
|
76
|
+
const helper = this.#helper;
|
|
77
|
+
if (helper && helper.exitCode === null && helper.signalCode === null)
|
|
78
|
+
helper.kill("SIGKILL");
|
|
79
|
+
await this.waitForEmpty(1_500);
|
|
80
|
+
await rm(this.statusPath, { force: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function resolveProcessGuardianPath(releaseRoot, environment = process.env, platform = process.platform, architecture = process.arch) {
|
|
84
|
+
const override = environment[PRODUCT_IDENTITY.environment.processGuardianPath]?.trim();
|
|
85
|
+
if (override)
|
|
86
|
+
return resolve(override);
|
|
87
|
+
const executable = platform === "win32"
|
|
88
|
+
? `${PRODUCT_IDENTITY.artifacts.processGuardianExecutable}.exe`
|
|
89
|
+
: PRODUCT_IDENTITY.artifacts.processGuardianExecutable;
|
|
90
|
+
return resolve(releaseRoot, "dist", "native", `${platform}-${architecture}`, executable);
|
|
91
|
+
}
|
|
92
|
+
function helperOutcome(helper) {
|
|
93
|
+
return new Promise(resolvePromise => {
|
|
94
|
+
let settled = false;
|
|
95
|
+
const settle = (outcome) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
resolvePromise(outcome);
|
|
100
|
+
};
|
|
101
|
+
helper.once("error", error => settle({ kind: "guardian-error", message: error.message, code: errorCode(error) }));
|
|
102
|
+
helper.once("close", (code, signal) => {
|
|
103
|
+
if (signal)
|
|
104
|
+
settle({ kind: "signaled", signal });
|
|
105
|
+
else
|
|
106
|
+
settle({ kind: "exited", exitCode: code ?? 1 });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function waitForReadyStatus(path, outcome, timeoutMs) {
|
|
111
|
+
const deadline = Date.now() + timeoutMs;
|
|
112
|
+
let terminalOutcome = null;
|
|
113
|
+
void outcome.then(value => { terminalOutcome = value; });
|
|
114
|
+
while (Date.now() < deadline) {
|
|
115
|
+
try {
|
|
116
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
117
|
+
if (isReadyStatus(value))
|
|
118
|
+
return value;
|
|
119
|
+
throw new Error("process guardian published invalid ready status");
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
if (terminalOutcome)
|
|
126
|
+
throw new Error(`process guardian exited before readiness: ${JSON.stringify(terminalOutcome)}`);
|
|
127
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 20));
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`process guardian did not publish readiness within ${timeoutMs}ms`);
|
|
130
|
+
}
|
|
131
|
+
function isReadyStatus(value) {
|
|
132
|
+
return Number.isSafeInteger(value.pid) && Number(value.pid) > 0
|
|
133
|
+
&& typeof value.startIdentity === "string" && value.startIdentity.length > 0
|
|
134
|
+
&& typeof value.containmentProvider === "string" && value.containmentProvider.length > 0
|
|
135
|
+
&& typeof value.containmentToken === "string" && value.containmentToken.length > 0;
|
|
136
|
+
}
|
|
137
|
+
function errorCode(error) {
|
|
138
|
+
return "code" in error && typeof error.code === "string" ? error.code : null;
|
|
139
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NativeProcessIdentity } from "../lifecycle/index.js";
|
|
2
|
+
import type { NativeProcessInspector } from "./contracts.js";
|
|
3
|
+
export interface InspectorCommandResult {
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
readonly stdout: string;
|
|
6
|
+
readonly stderr: string;
|
|
7
|
+
}
|
|
8
|
+
export interface InspectorCommandRunner {
|
|
9
|
+
run(executable: string, arguments_: readonly string[]): Promise<InspectorCommandResult>;
|
|
10
|
+
}
|
|
11
|
+
export declare class WindowsNativeProcessInspector implements NativeProcessInspector {
|
|
12
|
+
readonly helperPath: string;
|
|
13
|
+
private readonly runner;
|
|
14
|
+
constructor(helperPath: string, runner?: InspectorCommandRunner);
|
|
15
|
+
observe(pid: number): Promise<NativeProcessIdentity | null>;
|
|
16
|
+
matches(identity: NativeProcessIdentity): Promise<boolean>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
export class WindowsNativeProcessInspector {
|
|
3
|
+
helperPath;
|
|
4
|
+
runner;
|
|
5
|
+
constructor(helperPath, runner = defaultRunner) {
|
|
6
|
+
this.helperPath = helperPath;
|
|
7
|
+
this.runner = runner;
|
|
8
|
+
if (!helperPath || helperPath.includes("\0"))
|
|
9
|
+
throw new TypeError("process guardian helper path is invalid");
|
|
10
|
+
}
|
|
11
|
+
async observe(pid) {
|
|
12
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
13
|
+
throw new TypeError("inspected PID must be a positive safe integer");
|
|
14
|
+
const result = await this.runner.run(this.helperPath, ["--inspect-pid", String(pid)]);
|
|
15
|
+
if (result.exitCode === 3)
|
|
16
|
+
return null;
|
|
17
|
+
if (result.exitCode !== 0) {
|
|
18
|
+
const detail = result.stderr.trim() || `helper exited ${result.exitCode}`;
|
|
19
|
+
throw Object.assign(new Error(`cannot inspect native process identity: ${detail}`), { code: "PROCESS_INSPECTION_FAILED" });
|
|
20
|
+
}
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = JSON.parse(result.stdout);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw Object.assign(new Error("process guardian returned malformed identity data"), { code: "PROCESS_INSPECTION_INVALID" });
|
|
27
|
+
}
|
|
28
|
+
if (!isIdentity(value) || value.pid !== pid || !value.startIdentity.startsWith("windows-filetime:")) {
|
|
29
|
+
throw Object.assign(new Error("process guardian returned mismatched identity data"), { code: "PROCESS_INSPECTION_INVALID" });
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
async matches(identity) {
|
|
34
|
+
const observed = await this.observe(identity.pid);
|
|
35
|
+
return observed !== null && observed.startIdentity === identity.startIdentity;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const defaultRunner = {
|
|
39
|
+
async run(executable, arguments_) {
|
|
40
|
+
return await new Promise(resolvePromise => {
|
|
41
|
+
execFile(executable, [...arguments_], { windowsHide: true, timeout: 2_000, maxBuffer: 8 * 1_024 }, (error, stdout, stderr) => {
|
|
42
|
+
const exitCode = error && "code" in error && typeof error.code === "number" ? error.code : error ? 1 : 0;
|
|
43
|
+
resolvePromise({ exitCode, stdout, stderr });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
function isIdentity(value) {
|
|
49
|
+
return typeof value === "object" && value !== null
|
|
50
|
+
&& "pid" in value && Number.isSafeInteger(value.pid) && Number(value.pid) > 0
|
|
51
|
+
&& "startIdentity" in value && typeof value.startIdentity === "string"
|
|
52
|
+
&& value.startIdentity.length > 0 && value.startIdentity.length <= 512 && !value.startIdentity.includes("\0");
|
|
53
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { CommandResult, SupervisorCommand, SupervisorSnapshot } from "../lifecycle/index.js";
|
|
2
|
+
import type { CommandResult, LaunchInstanceStopIntent, SupervisorCommand, SupervisorSnapshot } from "../lifecycle/index.js";
|
|
3
3
|
interface ClientEvents {
|
|
4
4
|
snapshot: [SupervisorSnapshot];
|
|
5
|
+
stopIntent: [LaunchInstanceStopIntent];
|
|
5
6
|
disconnect: [];
|
|
6
7
|
}
|
|
7
8
|
export declare class SupervisorClient extends EventEmitter<ClientEvents> {
|
|
@@ -91,6 +91,9 @@ export class SupervisorClient extends EventEmitter {
|
|
|
91
91
|
pending.resolve(message.result);
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
else if (message.type === "stop-launch-instance" && message.instanceId && message.requestId && message.reason) {
|
|
95
|
+
this.emit("stopIntent", message);
|
|
96
|
+
}
|
|
94
97
|
else if (message.type === "protocol-error") {
|
|
95
98
|
fail(new Error(message.message ?? "protocol error"));
|
|
96
99
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { CommandResult, SupervisorCommand, SupervisorSnapshot } from "../lifecycle/index.js";
|
|
1
|
+
import type { CommandResult, LaunchInstanceStopIntent, SupervisorCommand, SupervisorSnapshot } from "../lifecycle/index.js";
|
|
2
2
|
export declare const CONTROL_ENVELOPE: string;
|
|
3
3
|
export declare const CONTROL_ENVELOPE_REVISION: 1;
|
|
4
|
-
export declare const REQUIRED_CONTROL_FEATURES: readonly ["handshake.features.v1", "snapshot.revision.v1", "commands.request-identity.v1", "generation.lifecycle.v1", "
|
|
4
|
+
export declare const REQUIRED_CONTROL_FEATURES: readonly ["handshake.features.v1", "snapshot.revision.v1", "commands.request-identity.v1", "generation.lifecycle.v1", "launch.instance-lifecycle.v1"];
|
|
5
5
|
export declare const OPTIONAL_CONTROL_FEATURES: readonly ["messages.unknown-additive-ignore.v1", "snapshot.resynchronize.v1"];
|
|
6
6
|
export type ControlFeature = string;
|
|
7
7
|
export declare const GENERATED_CONTRACT_DIGEST: string;
|
|
@@ -41,12 +41,12 @@ export type ServerMessage = {
|
|
|
41
41
|
readonly type: "release-ownership-result";
|
|
42
42
|
readonly released: boolean;
|
|
43
43
|
readonly reason: string;
|
|
44
|
-
readonly
|
|
44
|
+
readonly liveInstanceIds: readonly string[];
|
|
45
45
|
} | {
|
|
46
46
|
readonly type: "release-update-result";
|
|
47
47
|
readonly accepted: boolean;
|
|
48
48
|
readonly reason: string;
|
|
49
|
-
readonly
|
|
49
|
+
readonly liveInstanceIds: readonly string[];
|
|
50
50
|
} | ({
|
|
51
51
|
readonly type: "server-hello";
|
|
52
52
|
readonly supervisorId: string;
|
|
@@ -60,7 +60,7 @@ export type ServerMessage = {
|
|
|
60
60
|
} | {
|
|
61
61
|
readonly type: "command-result";
|
|
62
62
|
readonly result: CommandResult;
|
|
63
|
-
} | {
|
|
63
|
+
} | LaunchInstanceStopIntent | {
|
|
64
64
|
readonly type: "protocol-error";
|
|
65
65
|
readonly code: string;
|
|
66
66
|
readonly message: string;
|
|
@@ -7,7 +7,7 @@ export const REQUIRED_CONTROL_FEATURES = [
|
|
|
7
7
|
"snapshot.revision.v1",
|
|
8
8
|
"commands.request-identity.v1",
|
|
9
9
|
"generation.lifecycle.v1",
|
|
10
|
-
"
|
|
10
|
+
"launch.instance-lifecycle.v1",
|
|
11
11
|
];
|
|
12
12
|
export const OPTIONAL_CONTROL_FEATURES = [
|
|
13
13
|
"messages.unknown-additive-ignore.v1",
|
|
@@ -2,7 +2,7 @@ import { type OwnershipProbe } from "./cohort-selection.js";
|
|
|
2
2
|
import { type SupervisorEndpointMetadata } from "./cohort-state.js";
|
|
3
3
|
import { type LaunchProfileId } from "../lifecycle/index.js";
|
|
4
4
|
import { cleanupProvenIdleOwner } from "./process-cleanup.js";
|
|
5
|
-
import { type MaterializedRelease } from "./release-store.js";
|
|
5
|
+
import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
|
|
6
6
|
export interface BootstrapOptions {
|
|
7
7
|
readonly packageRoot: string;
|
|
8
8
|
readonly launchIntent?: {
|
|
@@ -15,7 +15,7 @@ export interface BootstrapOptions {
|
|
|
15
15
|
readonly output?: Pick<NodeJS.WriteStream, "write">;
|
|
16
16
|
}
|
|
17
17
|
export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
|
|
18
|
-
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string): Promise<string>;
|
|
18
|
+
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
|
|
19
19
|
export declare function startSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<void>;
|
|
20
20
|
export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease): NodeJS.ProcessEnv;
|
|
21
21
|
export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number): Promise<void>;
|
|
@@ -8,7 +8,7 @@ import { CohortStateStore } from "./cohort-state.js";
|
|
|
8
8
|
import { assertLaunchProfileId, resolveProductPaths } from "../lifecycle/index.js";
|
|
9
9
|
import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
|
|
10
10
|
import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
|
|
11
|
-
import { materializeRelease, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
11
|
+
import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
12
12
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
13
13
|
export async function runBootstrap(options) {
|
|
14
14
|
const environment = { ...(options.environment ?? process.env) };
|
|
@@ -18,21 +18,44 @@ export async function runBootstrap(options) {
|
|
|
18
18
|
const output = options.output ?? process.stderr;
|
|
19
19
|
const paths = resolveProductPaths(environment);
|
|
20
20
|
await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
21
|
-
const candidate = await materializeRelease(options.packageRoot, paths.dataDir);
|
|
22
21
|
const stateStore = new CohortStateStore(paths.dataDir);
|
|
23
|
-
await stateStore.recordCandidate(candidate);
|
|
24
22
|
let state = await stateStore.read();
|
|
23
|
+
let endpoint = await readEndpointMetadata(paths.endpointMetadataPath);
|
|
24
|
+
let probe = endpoint ? await probeOwnership(endpoint) : "dead";
|
|
25
|
+
const installedVersion = await readInstalledVersion(options.packageRoot);
|
|
26
|
+
const activeId = state.references.active;
|
|
27
|
+
const active = activeId === null ? undefined : state.releases[activeId];
|
|
28
|
+
if (active?.approval === "approved" && active.packageVersion === installedVersion) {
|
|
29
|
+
const endpointMatches = endpoint?.releaseId === active.releaseId
|
|
30
|
+
&& endpoint.releaseRoot === active.releaseRoot
|
|
31
|
+
&& endpoint.contentDigest === active.contentDigest;
|
|
32
|
+
if (endpointMatches && probe === "live-verified") {
|
|
33
|
+
const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
|
|
34
|
+
return await launchUi(retained, environment);
|
|
35
|
+
}
|
|
36
|
+
if (endpoint === null || probe === "dead") {
|
|
37
|
+
if (endpoint)
|
|
38
|
+
await removeEndpointArtifacts(paths.endpointMetadataPath, paths.endpoint);
|
|
39
|
+
const retained = await readMaterializedRelease(active.releaseRoot);
|
|
40
|
+
await startSupervisor(retained, environment);
|
|
41
|
+
await waitForVerifiedEndpoint(paths.endpointMetadataPath, retained, 8_000);
|
|
42
|
+
return await launchUi(retained, environment);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const candidate = await materializeRelease(options.packageRoot, paths.dataDir);
|
|
46
|
+
await stateStore.recordCandidate(candidate);
|
|
47
|
+
state = await stateStore.read();
|
|
25
48
|
if (!state.references.active) {
|
|
26
49
|
const diagnosticsPath = await certifyMaterializedRelease(candidate, paths.dataDir);
|
|
27
50
|
await stateStore.approve(candidate.releaseId, diagnosticsPath);
|
|
28
51
|
await stateStore.activate(candidate.releaseId);
|
|
29
52
|
state = await stateStore.read();
|
|
30
53
|
}
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
endpoint = await readEndpointMetadata(paths.endpointMetadataPath);
|
|
55
|
+
probe = endpoint ? await probeOwnership(endpoint) : "dead";
|
|
33
56
|
let decision = selectCohortLaunch(candidate, state, endpoint, probe);
|
|
34
57
|
if (decision.action === "blocked") {
|
|
35
|
-
await stateStore.blockPending(decision.reason, endpoint?.ownership.
|
|
58
|
+
await stateStore.blockPending(decision.reason, endpoint?.ownership.liveInstanceIds ?? []);
|
|
36
59
|
output.write(`${PRODUCT_TEXT.diagnostic(`startup is blocked to preserve uncertain live ownership: ${decision.reason}`)}\n`);
|
|
37
60
|
return 1;
|
|
38
61
|
}
|
|
@@ -50,7 +73,7 @@ export async function runBootstrap(options) {
|
|
|
50
73
|
if (decision.action === "launch-retained-ui") {
|
|
51
74
|
const retained = await verifyMaterializedRelease(decision.releaseRoot, undefined, resolve(paths.dataDir, "releases"));
|
|
52
75
|
if (decision.recordPending && endpoint) {
|
|
53
|
-
await stateStore.blockPending("candidate activation deferred by live non-resumable
|
|
76
|
+
await stateStore.blockPending("candidate activation deferred by live non-resumable instances", endpoint.ownership.nonResumableInstanceIds);
|
|
54
77
|
}
|
|
55
78
|
const code = await launchUi(retained, environment);
|
|
56
79
|
if (decision.recordPending)
|
|
@@ -62,12 +85,12 @@ export async function runBootstrap(options) {
|
|
|
62
85
|
await stateStore.approve(candidate.releaseId, diagnosticsPath);
|
|
63
86
|
const released = await requestIdleOwnershipRelease(endpoint, candidate.releaseId, 2_000);
|
|
64
87
|
if (!released) {
|
|
65
|
-
await stateStore.blockPending("idle supervisor did not verify ownership release", endpoint.ownership.
|
|
88
|
+
await stateStore.blockPending("idle supervisor did not verify ownership release", endpoint.ownership.liveInstanceIds);
|
|
66
89
|
output.write(`${PRODUCT_TEXT.diagnostic("could not verify idle cohort ownership release; the retained release remains selected.")}\n`);
|
|
67
90
|
return 1;
|
|
68
91
|
}
|
|
69
92
|
if (!await releaseVerifiedIdleOwner(endpoint, paths.dataDir)) {
|
|
70
|
-
await stateStore.blockPending("idle supervisor acknowledged release but did not terminate", endpoint.ownership.
|
|
93
|
+
await stateStore.blockPending("idle supervisor acknowledged release but did not terminate", endpoint.ownership.liveInstanceIds);
|
|
71
94
|
output.write(`${PRODUCT_TEXT.diagnostic("could not complete bounded idle cohort shutdown; the candidate remains pending.")}\n`);
|
|
72
95
|
return 1;
|
|
73
96
|
}
|
|
@@ -93,8 +116,17 @@ export async function runBootstrap(options) {
|
|
|
93
116
|
await waitForVerifiedEndpoint(paths.endpointMetadataPath, selected, 8_000);
|
|
94
117
|
return await launchUi(selected, environment);
|
|
95
118
|
}
|
|
96
|
-
|
|
97
|
-
await
|
|
119
|
+
async function readInstalledVersion(packageRoot) {
|
|
120
|
+
const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
121
|
+
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
122
|
+
throw new Error(PRODUCT_TEXT.diagnostic("package metadata has no version"));
|
|
123
|
+
}
|
|
124
|
+
return manifest.version;
|
|
125
|
+
}
|
|
126
|
+
export async function certifyMaterializedRelease(release, dataDir, verification = {}) {
|
|
127
|
+
if (!consumeMaterializationProof(release)) {
|
|
128
|
+
await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
|
|
129
|
+
}
|
|
98
130
|
const path = resolve(dataDir, `certification-${release.releaseId}.json`);
|
|
99
131
|
await writeFile(path, JSON.stringify({
|
|
100
132
|
schema: PRODUCT_IDENTITY.evidence.releaseCertificationSchema,
|
|
@@ -106,7 +138,7 @@ export async function certifyMaterializedRelease(release, dataDir) {
|
|
|
106
138
|
return path;
|
|
107
139
|
}
|
|
108
140
|
export async function startSupervisor(release, environment) {
|
|
109
|
-
const entry = await resolveReleaseEntryPoint(release, "bin/
|
|
141
|
+
const entry = await resolveReleaseEntryPoint(release, "bin/supervisor.js");
|
|
110
142
|
const child = spawn(process.execPath, [entry], {
|
|
111
143
|
detached: true,
|
|
112
144
|
env: releaseEnvironment(environment, release),
|
|
@@ -120,7 +152,7 @@ export async function startSupervisor(release, environment) {
|
|
|
120
152
|
child.unref();
|
|
121
153
|
}
|
|
122
154
|
async function launchUi(release, environment) {
|
|
123
|
-
const entry = await resolveReleaseEntryPoint(release, "bin/
|
|
155
|
+
const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
|
|
124
156
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
125
157
|
const child = spawn(process.execPath, [entry], {
|
|
126
158
|
env: releaseEnvironment(environment, release),
|
|
@@ -152,13 +184,29 @@ export async function waitForVerifiedEndpoint(path, release, timeoutMs) {
|
|
|
152
184
|
export async function readEndpointMetadata(path) {
|
|
153
185
|
try {
|
|
154
186
|
const value = JSON.parse(await readFile(path, "utf8"));
|
|
187
|
+
const ownership = value.ownership;
|
|
188
|
+
const liveInstanceIds = Array.isArray(ownership?.liveInstanceIds)
|
|
189
|
+
? ownership.liveInstanceIds
|
|
190
|
+
: Array.isArray(ownership?.liveGenerationIds) ? ownership.liveGenerationIds : null;
|
|
191
|
+
const nonResumableInstanceIds = Array.isArray(ownership?.nonResumableInstanceIds)
|
|
192
|
+
? ownership.nonResumableInstanceIds
|
|
193
|
+
: Array.isArray(ownership?.nonResumableGenerationIds) ? ownership.nonResumableGenerationIds : null;
|
|
194
|
+
const uncertainInstanceIds = Array.isArray(ownership?.uncertainInstanceIds) ? ownership.uncertainInstanceIds : [];
|
|
155
195
|
if (!value || value.schema !== PRODUCT_IDENTITY.protocol.supervisorSchema || typeof value.supervisorId !== "string" || typeof value.endpoint !== "string" || !Number.isSafeInteger(value.pid)
|
|
156
196
|
|| typeof value.pidStartIdentity !== "string" || typeof value.bootNonce !== "string" || typeof value.releaseId !== "string"
|
|
157
|
-
|| typeof value.releaseRoot !== "string" || typeof value.contentDigest !== "string" || !
|
|
158
|
-
|| !
|
|
197
|
+
|| typeof value.releaseRoot !== "string" || typeof value.contentDigest !== "string" || !ownership
|
|
198
|
+
|| !isStringArray(liveInstanceIds) || !isStringArray(nonResumableInstanceIds) || !isStringArray(uncertainInstanceIds)) {
|
|
159
199
|
return null;
|
|
160
200
|
}
|
|
161
|
-
return
|
|
201
|
+
return {
|
|
202
|
+
...value,
|
|
203
|
+
ownership: {
|
|
204
|
+
...ownership,
|
|
205
|
+
liveInstanceIds,
|
|
206
|
+
nonResumableInstanceIds,
|
|
207
|
+
uncertainInstanceIds,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
162
210
|
}
|
|
163
211
|
catch {
|
|
164
212
|
return null;
|
|
@@ -211,11 +259,11 @@ async function activatePendingAfterBlockerExit(candidate, stateStore, paths, env
|
|
|
211
259
|
let endpoint = null;
|
|
212
260
|
while (Date.now() < deadline) {
|
|
213
261
|
endpoint = await readEndpointMetadata(paths.endpointMetadataPath);
|
|
214
|
-
if (!endpoint || endpoint.ownership.
|
|
262
|
+
if (!endpoint || endpoint.ownership.liveInstanceIds.length === 0)
|
|
215
263
|
break;
|
|
216
264
|
await new Promise(resolvePromise => setTimeout(resolvePromise, 40));
|
|
217
265
|
}
|
|
218
|
-
if (!endpoint || endpoint.ownership.
|
|
266
|
+
if (!endpoint || endpoint.ownership.liveInstanceIds.length > 0 || await probeOwnership(endpoint) !== "live-verified")
|
|
219
267
|
return;
|
|
220
268
|
const diagnosticsPath = await certifyMaterializedRelease(candidate, paths.dataDir);
|
|
221
269
|
await stateStore.approve(candidate.releaseId, diagnosticsPath);
|
|
@@ -268,6 +316,9 @@ async function requestIdleOwnershipRelease(metadata, candidateReleaseId, timeout
|
|
|
268
316
|
setTimeout(() => finish(false), timeoutMs).unref();
|
|
269
317
|
});
|
|
270
318
|
}
|
|
319
|
+
function isStringArray(value) {
|
|
320
|
+
return Array.isArray(value) && value.every(item => typeof item === "string" && item.length > 0);
|
|
321
|
+
}
|
|
271
322
|
export async function waitForProcessExit(pid, timeoutMs) {
|
|
272
323
|
const deadline = Date.now() + timeoutMs;
|
|
273
324
|
while (Date.now() < deadline) {
|
|
@@ -10,7 +10,7 @@ export function selectCohortLaunch(candidate, state, endpoint, probe) {
|
|
|
10
10
|
pid: endpoint.pid,
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
if (endpoint.releaseId !== candidate.releaseId && endpoint.ownership.
|
|
13
|
+
if (endpoint.releaseId !== candidate.releaseId && endpoint.ownership.liveInstanceIds.length === 0) {
|
|
14
14
|
return {
|
|
15
15
|
action: "replace-idle-cohort",
|
|
16
16
|
releaseId: candidate.releaseId,
|
|
@@ -30,12 +30,12 @@ export function selectCohortLaunch(candidate, state, endpoint, probe) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
if (endpoint && probe === "unresponsive") {
|
|
33
|
-
if (endpoint.ownership.
|
|
33
|
+
if (endpoint.ownership.liveInstanceIds.length > 0 || endpoint.ownership.nonResumableInstanceIds.length > 0) {
|
|
34
34
|
return {
|
|
35
35
|
action: "blocked",
|
|
36
36
|
releaseId: endpoint.releaseId,
|
|
37
37
|
releaseRoot: endpoint.releaseRoot,
|
|
38
|
-
reason: "ownership of live
|
|
38
|
+
reason: "ownership of live launch instances is uncertain; preserving the unresponsive supervisor",
|
|
39
39
|
pid: endpoint.pid,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
@@ -32,8 +32,9 @@ export interface CohortState {
|
|
|
32
32
|
}
|
|
33
33
|
export interface EndpointOwnership {
|
|
34
34
|
readonly state: "idle" | "busy" | "draining" | "released" | "blocked";
|
|
35
|
-
readonly
|
|
36
|
-
readonly
|
|
35
|
+
readonly liveInstanceIds: readonly string[];
|
|
36
|
+
readonly nonResumableInstanceIds: readonly string[];
|
|
37
|
+
readonly uncertainInstanceIds: readonly string[];
|
|
37
38
|
}
|
|
38
39
|
export interface SupervisorEndpointMetadata {
|
|
39
40
|
readonly schema: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mapWithConcurrency<Value, Result>(values: readonly Value[], concurrency: number, operation: (value: Value, index: number) => Promise<Result>): Promise<Result[]>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export async function mapWithConcurrency(values, concurrency, operation) {
|
|
2
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1)
|
|
3
|
+
throw new RangeError("concurrency must be a positive safe integer");
|
|
4
|
+
const results = new Array(values.length);
|
|
5
|
+
let nextIndex = 0;
|
|
6
|
+
const worker = async () => {
|
|
7
|
+
while (true) {
|
|
8
|
+
const index = nextIndex;
|
|
9
|
+
nextIndex += 1;
|
|
10
|
+
if (index >= values.length)
|
|
11
|
+
return;
|
|
12
|
+
results[index] = await operation(values[index], index);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker));
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
@@ -9,6 +9,6 @@ export declare function processIsAlive(pid: number): boolean;
|
|
|
9
9
|
export declare function cleanupProvenIdleOwner(metadata: SupervisorEndpointMetadata, graceMs?: number): Promise<CleanupDiagnostics>;
|
|
10
10
|
export declare function cleanupVerifiedOwner(metadata: SupervisorEndpointMetadata, options?: {
|
|
11
11
|
readonly graceMs?: number;
|
|
12
|
-
readonly
|
|
12
|
+
readonly allowLiveInstances?: boolean;
|
|
13
13
|
readonly reason?: "stale-idle-owner" | "explicit-update" | "legacy-mutable-install";
|
|
14
14
|
}): Promise<CleanupDiagnostics>;
|
|
@@ -11,11 +11,11 @@ export function processIsAlive(pid) {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
export async function cleanupProvenIdleOwner(metadata, graceMs = 1_500) {
|
|
14
|
-
return await cleanupVerifiedOwner(metadata, { graceMs,
|
|
14
|
+
return await cleanupVerifiedOwner(metadata, { graceMs, allowLiveInstances: false });
|
|
15
15
|
}
|
|
16
16
|
export async function cleanupVerifiedOwner(metadata, options = {}) {
|
|
17
17
|
const graceMs = options.graceMs ?? 1_500;
|
|
18
|
-
if (!options.
|
|
18
|
+
if (!options.allowLiveInstances && (metadata.ownership.liveInstanceIds.length > 0 || metadata.ownership.nonResumableInstanceIds.length > 0)) {
|
|
19
19
|
throw new Error("refusing to terminate an owner with recorded live generations");
|
|
20
20
|
}
|
|
21
21
|
const started = Date.now();
|