@loucompanion/forge-bridge 0.1.3-dev.b6be5e7f9668 → 0.1.3-dev.ba7d36726eda
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 +78 -0
- package/dist/bridge/bridge.base.d.ts +5 -1
- package/dist/bridge/bridge.service.d.ts +52 -3
- package/dist/bridge/bridge.service.js +796 -22
- package/dist/bridge/bridge.types.d.ts +135 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +356 -205
- package/dist/cli/local-config.d.ts +6 -0
- package/dist/cli/local-config.js +45 -2
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +15 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +50 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +74 -0
- package/dist/runtime/attempt-process-supervisor.js +290 -0
- package/dist/runtime/host-listener-inspector.d.ts +28 -0
- package/dist/runtime/host-listener-inspector.js +185 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +39 -0
- package/dist/runtime/host-runtime-contract.js +295 -0
- package/dist/runtime/host-runtime-lease.d.ts +122 -0
- package/dist/runtime/host-runtime-lease.js +1241 -0
- package/dist/runtime/host-runtime-promotion.d.ts +21 -0
- package/dist/runtime/host-runtime-promotion.js +128 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +44 -0
- package/dist/runtime/node-host-runtime-adapter.js +205 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +136 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +45 -0
- package/dist/sandbox/preflight.js +300 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +119 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +86 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +67 -0
- package/dist/session/phase-completion.artifacts.d.ts +10 -0
- package/dist/session/phase-completion.artifacts.js +620 -0
- package/dist/session/phase-completion.d.ts +32 -44
- package/dist/session/phase-completion.helpers.d.ts +112 -0
- package/dist/session/phase-completion.helpers.js +665 -0
- package/dist/session/phase-completion.js +28 -3485
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +220 -0
- package/dist/session/phase-completion.runtime.d.ts +36 -0
- package/dist/session/phase-completion.runtime.js +611 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +707 -0
- package/dist/session/phase-completion.validation.d.ts +34 -0
- package/dist/session/phase-completion.validation.js +724 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +778 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +329 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +34 -0
- package/dist/session/provider-cli/provider-cli.process.js +316 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +573 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +41 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +9 -53
- package/dist/session/provider-cli/provider-cli.service.js +182 -2536
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
- package/dist/session/provider-cli/provider-output-state.js +445 -0
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -0
- package/dist/session/session.base.d.ts +4 -0
- package/dist/session/session.service.d.ts +6 -1
- package/dist/session/session.service.js +202 -53
- package/dist/session/session.types.d.ts +123 -1
- package/dist/session/session.types.js +301 -3
- package/dist/shared/path/path.js +3 -1
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +40 -0
- package/dist/shared/process/host-process-supervisor.js +195 -0
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +10 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -6
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +8 -3
- package/dist/worktree/worktree.service.js +89 -52
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +6 -3
- package/runtime/windows-job-supervisor.ps1 +287 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -7,6 +7,8 @@ export interface BridgeLocalConfig {
|
|
|
7
7
|
readonly deviceName?: string;
|
|
8
8
|
readonly defaultRepoRoot?: string;
|
|
9
9
|
readonly cleanupWorktrees?: boolean;
|
|
10
|
+
readonly dockerEnabled?: boolean;
|
|
11
|
+
readonly allowOpenPublicSandbox?: boolean;
|
|
10
12
|
readonly cliProviders?: readonly string[];
|
|
11
13
|
readonly providerStatuses?: Readonly<Record<string, BridgeProviderStatus>>;
|
|
12
14
|
readonly projectLabels?: readonly string[];
|
|
@@ -17,6 +19,8 @@ export interface BridgeLocalConfig {
|
|
|
17
19
|
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
20
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
19
21
|
readonly supportsDraftRunOnce?: boolean;
|
|
22
|
+
readonly supportsDraftAttachments?: boolean;
|
|
23
|
+
readonly supportsDraftImageAttachments?: boolean;
|
|
20
24
|
}
|
|
21
25
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
22
26
|
readonly label: string;
|
|
@@ -32,4 +36,6 @@ export declare function removeRepoMapping(label: string): Promise<{
|
|
|
32
36
|
readonly config: BridgeLocalConfig;
|
|
33
37
|
readonly removed: boolean;
|
|
34
38
|
}>;
|
|
39
|
+
export declare function setServerUrl(serverUrl: string): Promise<BridgeLocalConfig>;
|
|
40
|
+
export declare function setSandboxPolicy(dockerEnabled: boolean, allowOpenPublicSandbox: boolean): Promise<BridgeLocalConfig>;
|
|
35
41
|
export declare function providerStatusesFor(providers: readonly string[]): Readonly<Record<string, BridgeProviderStatus>>;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -45,6 +45,12 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
45
45
|
deviceName: process.env.FORGE_DEVICE_NAME ?? local.deviceName ?? config.deviceName,
|
|
46
46
|
defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? local.defaultRepoRoot ?? config.defaultRepoRoot,
|
|
47
47
|
cleanupWorktrees: local.cleanupWorktrees ?? config.cleanupWorktrees,
|
|
48
|
+
dockerEnabled: process.env.FORGE_DOCKER_SANDBOX === undefined
|
|
49
|
+
? local.dockerEnabled ?? config.dockerEnabled
|
|
50
|
+
: config.dockerEnabled,
|
|
51
|
+
allowOpenPublicSandbox: process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX === undefined
|
|
52
|
+
? local.allowOpenPublicSandbox ?? config.allowOpenPublicSandbox
|
|
53
|
+
: config.allowOpenPublicSandbox,
|
|
48
54
|
cliProviders,
|
|
49
55
|
providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
|
|
50
56
|
projectLabels: local.projectLabels ?? config.projectLabels,
|
|
@@ -54,7 +60,9 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
54
60
|
supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
|
|
55
61
|
supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
|
|
56
62
|
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
|
|
57
|
-
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce
|
|
63
|
+
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce,
|
|
64
|
+
supportsDraftAttachments: local.supportsDraftAttachments ?? config.supportsDraftAttachments,
|
|
65
|
+
supportsDraftImageAttachments: local.supportsDraftImageAttachments ?? config.supportsDraftImageAttachments
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
68
|
export async function addRepoMapping(label, repoPath) {
|
|
@@ -86,6 +94,19 @@ export async function removeRepoMapping(label) {
|
|
|
86
94
|
await writeLocalConfig(next);
|
|
87
95
|
return { config: next, removed: after.length !== before.length };
|
|
88
96
|
}
|
|
97
|
+
export async function setServerUrl(serverUrl) {
|
|
98
|
+
const normalized = normalizeServerUrl(serverUrl);
|
|
99
|
+
const current = await readLocalConfig();
|
|
100
|
+
const next = normalizeLocalConfig({ ...current, serverUrl: normalized });
|
|
101
|
+
await writeLocalConfig(next);
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
export async function setSandboxPolicy(dockerEnabled, allowOpenPublicSandbox) {
|
|
105
|
+
const current = await readLocalConfig();
|
|
106
|
+
const next = normalizeLocalConfig({ ...current, dockerEnabled, allowOpenPublicSandbox });
|
|
107
|
+
await writeLocalConfig(next);
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
89
110
|
export function providerStatusesFor(providers) {
|
|
90
111
|
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
|
|
91
112
|
}
|
|
@@ -104,6 +125,8 @@ function normalizeLocalConfig(config) {
|
|
|
104
125
|
deviceName: cleanString(config.deviceName),
|
|
105
126
|
defaultRepoRoot: cleanString(config.defaultRepoRoot),
|
|
106
127
|
cleanupWorktrees: config.cleanupWorktrees,
|
|
128
|
+
dockerEnabled: config.dockerEnabled,
|
|
129
|
+
allowOpenPublicSandbox: config.allowOpenPublicSandbox,
|
|
107
130
|
cliProviders: cleanList(config.cliProviders),
|
|
108
131
|
providerStatuses: normalizeProviderStatuses(config.providerStatuses),
|
|
109
132
|
projectLabels: cleanList(config.projectLabels),
|
|
@@ -113,7 +136,9 @@ function normalizeLocalConfig(config) {
|
|
|
113
136
|
supportsManagedWorktrees: config.supportsManagedWorktrees,
|
|
114
137
|
supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
|
|
115
138
|
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
|
|
116
|
-
supportsDraftRunOnce: config.supportsDraftRunOnce
|
|
139
|
+
supportsDraftRunOnce: config.supportsDraftRunOnce,
|
|
140
|
+
supportsDraftAttachments: config.supportsDraftAttachments,
|
|
141
|
+
supportsDraftImageAttachments: config.supportsDraftImageAttachments
|
|
117
142
|
};
|
|
118
143
|
}
|
|
119
144
|
function emptyConfig() {
|
|
@@ -207,6 +232,24 @@ function cleanString(value) {
|
|
|
207
232
|
const clean = value?.trim();
|
|
208
233
|
return clean ? clean : undefined;
|
|
209
234
|
}
|
|
235
|
+
function normalizeServerUrl(value) {
|
|
236
|
+
const clean = value.trim();
|
|
237
|
+
let parsed;
|
|
238
|
+
try {
|
|
239
|
+
parsed = new URL(clean);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
throw new Error(`Invalid Forge server URL: ${value}`);
|
|
243
|
+
}
|
|
244
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
245
|
+
throw new Error("Forge server URL must use http or https.");
|
|
246
|
+
}
|
|
247
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
248
|
+
throw new Error("Forge server URL cannot include credentials, a query string, or a fragment.");
|
|
249
|
+
}
|
|
250
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
|
|
251
|
+
return parsed.toString().replace(/\/$/u, "");
|
|
252
|
+
}
|
|
210
253
|
function cleanList(values) {
|
|
211
254
|
const clean = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
|
|
212
255
|
return clean.length > 0 ? clean : undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AttemptProcessChildHandle, AttemptProcessSupervisor } from "./attempt-process-supervisor.js";
|
|
2
|
+
import type { HostPortLeaseOwner } from "./host-port-broker.js";
|
|
3
|
+
import { HostListenerInspector } from "./host-listener-inspector.js";
|
|
4
|
+
import type { HostListenerOwnership, NodeRuntimeCommandLauncher, NodeRuntimeLaunchRequest } from "./node-host-runtime-adapter.js";
|
|
5
|
+
/** Launches one explicitly configured HTTP project beneath the attempt coordinator. */
|
|
6
|
+
export declare class AttemptNodeRuntimeLauncher implements NodeRuntimeCommandLauncher<AttemptProcessChildHandle> {
|
|
7
|
+
private readonly supervisor;
|
|
8
|
+
private readonly owner;
|
|
9
|
+
private readonly logDirectory;
|
|
10
|
+
private readonly listenerInspector;
|
|
11
|
+
constructor(supervisor: AttemptProcessSupervisor, owner: HostPortLeaseOwner, logDirectory: string, listenerInspector?: HostListenerInspector);
|
|
12
|
+
launch(request: NodeRuntimeLaunchRequest, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
|
|
13
|
+
inspectListener(child: AttemptProcessChildHandle, request: NodeRuntimeLaunchRequest): Promise<HostListenerOwnership>;
|
|
14
|
+
stop(child: AttemptProcessChildHandle): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { HostListenerInspector } from "./host-listener-inspector.js";
|
|
5
|
+
/** Launches one explicitly configured HTTP project beneath the attempt coordinator. */
|
|
6
|
+
export class AttemptNodeRuntimeLauncher {
|
|
7
|
+
supervisor;
|
|
8
|
+
owner;
|
|
9
|
+
logDirectory;
|
|
10
|
+
listenerInspector;
|
|
11
|
+
constructor(supervisor, owner, logDirectory, listenerInspector = new HostListenerInspector()) {
|
|
12
|
+
this.supervisor = supervisor;
|
|
13
|
+
this.owner = owner;
|
|
14
|
+
this.logDirectory = logDirectory;
|
|
15
|
+
this.listenerInspector = listenerInspector;
|
|
16
|
+
}
|
|
17
|
+
async launch(request, signal) {
|
|
18
|
+
const childName = projectChildName(request.port.name);
|
|
19
|
+
const stdoutPath = path.join(this.logDirectory, `${childName}.stdout.log`);
|
|
20
|
+
const stderrPath = path.join(this.logDirectory, `${childName}.stderr.log`);
|
|
21
|
+
const child = await this.supervisor.startChild(childName, {
|
|
22
|
+
command: request.command,
|
|
23
|
+
args: request.args,
|
|
24
|
+
cwd: request.cwd,
|
|
25
|
+
env: request.environment,
|
|
26
|
+
captureOutput: false,
|
|
27
|
+
}, {
|
|
28
|
+
onStdoutLine: async (line) => await appendLine(stdoutPath, line),
|
|
29
|
+
onStderrLine: async (line) => await appendLine(stderrPath, line),
|
|
30
|
+
}, signal);
|
|
31
|
+
await child.ready;
|
|
32
|
+
return child;
|
|
33
|
+
}
|
|
34
|
+
async inspectListener(child, request) {
|
|
35
|
+
return await this.listenerInspector.inspect(child, request.port, this.owner);
|
|
36
|
+
}
|
|
37
|
+
async stop(child) {
|
|
38
|
+
await child.stop(this.owner.fencingToken);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function projectChildName(portName) {
|
|
42
|
+
const candidate = `project-${portName}`;
|
|
43
|
+
if (/^[a-z][a-z0-9_-]{0,31}$/u.test(candidate))
|
|
44
|
+
return candidate;
|
|
45
|
+
const digest = createHash("sha256").update(portName).digest("hex").slice(0, 16);
|
|
46
|
+
return `project-${digest}`;
|
|
47
|
+
}
|
|
48
|
+
async function appendLine(target, line) {
|
|
49
|
+
await fs.appendFile(target, `${line}\n`, { encoding: "utf8", mode: 0o600 });
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ProviderProcessHandlers, ProviderProcessRunRequest, ProviderProcessRunner, ProviderProcessRunResult } from "../session/provider-cli/provider-cli.process.js";
|
|
2
|
+
import { AttemptProcessSupervisor, type AttemptProcessSupervisorIdentity, type AttemptProcessSupervisorOptions } from "./attempt-process-supervisor.js";
|
|
3
|
+
/** Runner-local authority for active host attempt process coordinators. */
|
|
4
|
+
export declare class AttemptProcessSupervisorRegistry {
|
|
5
|
+
private readonly active;
|
|
6
|
+
register(identity: AttemptProcessSupervisorIdentity, runner: ProviderProcessRunner, options?: AttemptProcessSupervisorOptions): AttemptProcessSupervisor;
|
|
7
|
+
require(dispatchAttemptId: string, fencingToken: string): AttemptProcessSupervisor;
|
|
8
|
+
unregister(dispatchAttemptId: string, fencingToken: string): boolean;
|
|
9
|
+
has(dispatchAttemptId: string): boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Routes managed-host provider commands through the attempt's named provider
|
|
13
|
+
* child while retaining the raw runner for Docker and unmanaged execution.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AttemptCoordinatedProviderProcessRunner implements ProviderProcessRunner {
|
|
16
|
+
private readonly registry;
|
|
17
|
+
private readonly rawRunner;
|
|
18
|
+
constructor(registry: AttemptProcessSupervisorRegistry, rawRunner: ProviderProcessRunner);
|
|
19
|
+
run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AttemptProcessSupervisor, } from "./attempt-process-supervisor.js";
|
|
2
|
+
/** Runner-local authority for active host attempt process coordinators. */
|
|
3
|
+
export class AttemptProcessSupervisorRegistry {
|
|
4
|
+
active = new Map();
|
|
5
|
+
register(identity, runner, options = {}) {
|
|
6
|
+
const existing = this.active.get(identity.dispatchAttemptId);
|
|
7
|
+
if (existing) {
|
|
8
|
+
if (existing.fencingToken !== identity.fencingToken) {
|
|
9
|
+
throw new Error("attempt_process_registry_conflict: dispatch attempt is registered under a different fence.");
|
|
10
|
+
}
|
|
11
|
+
return existing.supervisor;
|
|
12
|
+
}
|
|
13
|
+
const supervisor = new AttemptProcessSupervisor(identity, runner, options);
|
|
14
|
+
this.active.set(identity.dispatchAttemptId, { fencingToken: identity.fencingToken, supervisor });
|
|
15
|
+
return supervisor;
|
|
16
|
+
}
|
|
17
|
+
require(dispatchAttemptId, fencingToken) {
|
|
18
|
+
const registered = this.active.get(dispatchAttemptId);
|
|
19
|
+
if (!registered) {
|
|
20
|
+
throw new Error("attempt_process_registry_missing: no process coordinator owns this dispatch attempt.");
|
|
21
|
+
}
|
|
22
|
+
if (registered.fencingToken !== fencingToken) {
|
|
23
|
+
throw new Error("attempt_process_registry_fenced: process coordinator fence does not match the dispatch.");
|
|
24
|
+
}
|
|
25
|
+
return registered.supervisor;
|
|
26
|
+
}
|
|
27
|
+
unregister(dispatchAttemptId, fencingToken) {
|
|
28
|
+
const registered = this.active.get(dispatchAttemptId);
|
|
29
|
+
if (!registered || registered.fencingToken !== fencingToken)
|
|
30
|
+
return false;
|
|
31
|
+
return this.active.delete(dispatchAttemptId);
|
|
32
|
+
}
|
|
33
|
+
has(dispatchAttemptId) {
|
|
34
|
+
return this.active.has(dispatchAttemptId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Routes managed-host provider commands through the attempt's named provider
|
|
39
|
+
* child while retaining the raw runner for Docker and unmanaged execution.
|
|
40
|
+
*/
|
|
41
|
+
export class AttemptCoordinatedProviderProcessRunner {
|
|
42
|
+
registry;
|
|
43
|
+
rawRunner;
|
|
44
|
+
constructor(registry, rawRunner) {
|
|
45
|
+
this.registry = registry;
|
|
46
|
+
this.rawRunner = rawRunner;
|
|
47
|
+
}
|
|
48
|
+
async run(request, handlers, signal) {
|
|
49
|
+
if (request.env?.FORGE_RUNTIME_MODE !== "host") {
|
|
50
|
+
return await this.rawRunner.run(request, handlers, signal);
|
|
51
|
+
}
|
|
52
|
+
const dispatchAttemptId = requiredEnvironment(request, "FORGE_DISPATCH_ATTEMPT_ID");
|
|
53
|
+
const fencingToken = requiredEnvironment(request, "FORGE_FENCING_TOKEN");
|
|
54
|
+
const supervisor = this.registry.require(dispatchAttemptId, fencingToken);
|
|
55
|
+
const child = await supervisor.startChild("provider", request, handlers, signal);
|
|
56
|
+
return await child.result;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function requiredEnvironment(request, name) {
|
|
60
|
+
const value = request.env?.[name]?.trim();
|
|
61
|
+
if (!value)
|
|
62
|
+
throw new Error(`attempt_process_registry_invalid: ${name} is required for managed-host execution.`);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ProviderProcessHandlers, ProviderProcessRunRequest, ProviderProcessRunner, ProviderProcessRunResult } from "../session/provider-cli/provider-cli.process.js";
|
|
2
|
+
export type AttemptProcessChildStatus = "starting" | "running" | "stopping" | "exited" | "failed";
|
|
3
|
+
export type AttemptProcessSupervisorStatus = "active" | "stopping" | "stopped" | "degraded";
|
|
4
|
+
export interface AttemptProcessSupervisorIdentity {
|
|
5
|
+
readonly processSupervisorId: string;
|
|
6
|
+
readonly dispatchAttemptId: string;
|
|
7
|
+
readonly executionLeaseId: string;
|
|
8
|
+
readonly fencingToken: string;
|
|
9
|
+
readonly logDirectory: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AttemptProcessChildLedgerEntry {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly generation: number;
|
|
14
|
+
readonly childId: string;
|
|
15
|
+
readonly processSupervisorId: string;
|
|
16
|
+
readonly metadataPath: string;
|
|
17
|
+
readonly status: AttemptProcessChildStatus;
|
|
18
|
+
readonly startedAt: string;
|
|
19
|
+
readonly settledAt?: string;
|
|
20
|
+
readonly exitCode?: number;
|
|
21
|
+
readonly failure?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AttemptProcessSupervisorLedger {
|
|
24
|
+
readonly schemaVersion: 1;
|
|
25
|
+
readonly attemptSupervisorId: string;
|
|
26
|
+
readonly dispatchAttemptId: string;
|
|
27
|
+
readonly executionLeaseId: string;
|
|
28
|
+
readonly status: AttemptProcessSupervisorStatus;
|
|
29
|
+
readonly updatedAt: string;
|
|
30
|
+
readonly children: readonly AttemptProcessChildLedgerEntry[];
|
|
31
|
+
}
|
|
32
|
+
export interface AttemptProcessChildHandle {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly generation: number;
|
|
35
|
+
readonly childId: string;
|
|
36
|
+
readonly processSupervisorId: string;
|
|
37
|
+
readonly metadataPath: string;
|
|
38
|
+
readonly ready: Promise<void>;
|
|
39
|
+
readonly result: Promise<ProviderProcessRunResult>;
|
|
40
|
+
stop(fencingToken: string): Promise<ProviderProcessRunResult>;
|
|
41
|
+
}
|
|
42
|
+
export interface AttemptProcessSupervisorOptions {
|
|
43
|
+
readonly metadataReadyTimeoutMs?: number;
|
|
44
|
+
readonly signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Owns the independently supervised process trees that belong to one host
|
|
48
|
+
* dispatch attempt. Each named child gets a distinct Windows Job Object or
|
|
49
|
+
* Unix process group through the existing process runner, while this
|
|
50
|
+
* coordinator supplies shared fencing, cancellation, and a durable ledger.
|
|
51
|
+
*/
|
|
52
|
+
export declare class AttemptProcessSupervisor {
|
|
53
|
+
private readonly runner;
|
|
54
|
+
readonly ledgerPath: string;
|
|
55
|
+
private readonly identity;
|
|
56
|
+
private readonly metadataReadyTimeoutMs;
|
|
57
|
+
private readonly records;
|
|
58
|
+
private readonly currentByName;
|
|
59
|
+
private readonly attemptController;
|
|
60
|
+
private status;
|
|
61
|
+
private persistTail;
|
|
62
|
+
private persistenceFailure;
|
|
63
|
+
private stopOperation;
|
|
64
|
+
constructor(identity: AttemptProcessSupervisorIdentity, runner: ProviderProcessRunner, options?: AttemptProcessSupervisorOptions);
|
|
65
|
+
startChild(name: string, request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
|
|
66
|
+
stopChild(name: string, generation: number, fencingToken: string, reason?: unknown): Promise<ProviderProcessRunResult>;
|
|
67
|
+
stopAll(fencingToken: string, reason?: unknown): Promise<void>;
|
|
68
|
+
private performStopAll;
|
|
69
|
+
snapshot(): AttemptProcessSupervisorLedger;
|
|
70
|
+
private observeChildReady;
|
|
71
|
+
private settleChild;
|
|
72
|
+
private assertFence;
|
|
73
|
+
private persist;
|
|
74
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const terminalChildStatuses = new Set(["exited", "failed"]);
|
|
5
|
+
/**
|
|
6
|
+
* Owns the independently supervised process trees that belong to one host
|
|
7
|
+
* dispatch attempt. Each named child gets a distinct Windows Job Object or
|
|
8
|
+
* Unix process group through the existing process runner, while this
|
|
9
|
+
* coordinator supplies shared fencing, cancellation, and a durable ledger.
|
|
10
|
+
*/
|
|
11
|
+
export class AttemptProcessSupervisor {
|
|
12
|
+
runner;
|
|
13
|
+
ledgerPath;
|
|
14
|
+
identity;
|
|
15
|
+
metadataReadyTimeoutMs;
|
|
16
|
+
records = [];
|
|
17
|
+
currentByName = new Map();
|
|
18
|
+
attemptController = new AbortController();
|
|
19
|
+
status = "active";
|
|
20
|
+
persistTail = Promise.resolve();
|
|
21
|
+
persistenceFailure;
|
|
22
|
+
stopOperation = null;
|
|
23
|
+
constructor(identity, runner, options = {}) {
|
|
24
|
+
this.runner = runner;
|
|
25
|
+
this.identity = validateIdentity(identity);
|
|
26
|
+
this.metadataReadyTimeoutMs = boundedInteger(options.metadataReadyTimeoutMs ?? 10_000, 100, 120_000, "metadataReadyTimeoutMs");
|
|
27
|
+
this.ledgerPath = path.join(this.identity.logDirectory, "attempt-process-supervisor.json");
|
|
28
|
+
if (options.signal?.aborted) {
|
|
29
|
+
this.status = "stopping";
|
|
30
|
+
this.attemptController.abort(options.signal.reason);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
options.signal?.addEventListener("abort", () => {
|
|
34
|
+
void this.stopAll(this.identity.fencingToken, options.signal?.reason).catch(error => {
|
|
35
|
+
this.persistenceFailure ??= error;
|
|
36
|
+
});
|
|
37
|
+
}, { once: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async startChild(name, request, handlers = {}, signal) {
|
|
41
|
+
assertChildName(name);
|
|
42
|
+
if (this.status !== "active" || this.attemptController.signal.aborted) {
|
|
43
|
+
throw new Error("attempt_process_supervisor_closed: the attempt no longer accepts child processes.");
|
|
44
|
+
}
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
throw cancellationError(signal);
|
|
47
|
+
const existing = this.currentByName.get(name);
|
|
48
|
+
if (existing && !terminalChildStatuses.has(existing.status)) {
|
|
49
|
+
throw new Error(`attempt_process_supervisor_conflict: child '${name}' is already active.`);
|
|
50
|
+
}
|
|
51
|
+
const generation = (existing?.generation ?? 0) + 1;
|
|
52
|
+
const childId = `${name}:${generation}`;
|
|
53
|
+
const processSupervisorId = childProcessSupervisorId(this.identity.processSupervisorId, childId);
|
|
54
|
+
const metadataPath = path.join(this.identity.logDirectory, `process-supervisor.${name}.${generation}.json`);
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const combinedSignal = signal
|
|
57
|
+
? AbortSignal.any([this.attemptController.signal, controller.signal, signal])
|
|
58
|
+
: AbortSignal.any([this.attemptController.signal, controller.signal]);
|
|
59
|
+
const childRequest = {
|
|
60
|
+
...request,
|
|
61
|
+
env: {
|
|
62
|
+
...request.env,
|
|
63
|
+
FORGE_RUNTIME_MODE: "host",
|
|
64
|
+
FORGE_RESOURCE_LEASE_ID: this.identity.executionLeaseId,
|
|
65
|
+
FORGE_EXECUTION_LEASE_ID: this.identity.executionLeaseId,
|
|
66
|
+
FORGE_DISPATCH_ATTEMPT_ID: this.identity.dispatchAttemptId,
|
|
67
|
+
FORGE_FENCING_TOKEN: this.identity.fencingToken,
|
|
68
|
+
FORGE_LOG_DIR: this.identity.logDirectory,
|
|
69
|
+
FORGE_PROCESS_SUPERVISOR_ID: processSupervisorId,
|
|
70
|
+
FORGE_PROCESS_SUPERVISOR_METADATA_PATH: metadataPath,
|
|
71
|
+
FORGE_PROCESS_CHILD_ID: childId,
|
|
72
|
+
FORGE_PROCESS_CHILD_NAME: name,
|
|
73
|
+
FORGE_PROCESS_CHILD_GENERATION: String(generation),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const startedAt = new Date().toISOString();
|
|
77
|
+
const record = {
|
|
78
|
+
name,
|
|
79
|
+
generation,
|
|
80
|
+
childId,
|
|
81
|
+
processSupervisorId,
|
|
82
|
+
metadataPath,
|
|
83
|
+
controller,
|
|
84
|
+
startedAt,
|
|
85
|
+
status: "starting",
|
|
86
|
+
result: Promise.resolve({ exitCode: 1, stdout: "", stderr: "not started" }),
|
|
87
|
+
};
|
|
88
|
+
this.records.push(record);
|
|
89
|
+
this.currentByName.set(name, record);
|
|
90
|
+
const result = this.runner.run(childRequest, handlers, combinedSignal);
|
|
91
|
+
record.result = result;
|
|
92
|
+
void result.then(value => this.settleChild(record, "exited", value.exitCode), error => this.settleChild(record, "failed", undefined, error));
|
|
93
|
+
try {
|
|
94
|
+
await this.persist();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
controller.abort("Attempt process ledger could not be persisted.");
|
|
98
|
+
await result.catch(() => undefined);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
const ready = this.observeChildReady(record);
|
|
102
|
+
void ready.catch(() => undefined);
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
generation,
|
|
106
|
+
childId,
|
|
107
|
+
processSupervisorId,
|
|
108
|
+
metadataPath,
|
|
109
|
+
ready,
|
|
110
|
+
result,
|
|
111
|
+
stop: async (fencingToken) => await this.stopChild(name, generation, fencingToken),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async stopChild(name, generation, fencingToken, reason = "Forge stopped the supervised child.") {
|
|
115
|
+
this.assertFence(fencingToken);
|
|
116
|
+
const record = this.currentByName.get(name);
|
|
117
|
+
if (!record || record.generation !== generation) {
|
|
118
|
+
throw new Error(`attempt_process_supervisor_stale: child '${name}' generation ${generation} is no longer current.`);
|
|
119
|
+
}
|
|
120
|
+
if (!terminalChildStatuses.has(record.status)) {
|
|
121
|
+
record.status = "stopping";
|
|
122
|
+
record.controller.abort(reason);
|
|
123
|
+
await this.persist();
|
|
124
|
+
}
|
|
125
|
+
return await record.result;
|
|
126
|
+
}
|
|
127
|
+
async stopAll(fencingToken, reason = "Forge stopped the supervised attempt.") {
|
|
128
|
+
this.assertFence(fencingToken);
|
|
129
|
+
if (this.status === "stopped")
|
|
130
|
+
return;
|
|
131
|
+
if (!this.stopOperation) {
|
|
132
|
+
const operation = this.performStopAll(reason);
|
|
133
|
+
this.stopOperation = operation;
|
|
134
|
+
void operation.catch(() => undefined).finally(() => {
|
|
135
|
+
if (this.stopOperation === operation && this.status !== "stopped")
|
|
136
|
+
this.stopOperation = null;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return await this.stopOperation;
|
|
140
|
+
}
|
|
141
|
+
async performStopAll(reason) {
|
|
142
|
+
this.status = "stopping";
|
|
143
|
+
this.attemptController.abort(reason);
|
|
144
|
+
const active = [...this.currentByName.values()].filter(record => !terminalChildStatuses.has(record.status));
|
|
145
|
+
for (const record of active)
|
|
146
|
+
record.status = "stopping";
|
|
147
|
+
await this.persist();
|
|
148
|
+
const results = await Promise.allSettled(active.map(record => record.result));
|
|
149
|
+
const rejected = results.filter((result) => result.status === "rejected");
|
|
150
|
+
this.status = rejected.length > 0 || this.persistenceFailure !== undefined ? "degraded" : "stopped";
|
|
151
|
+
await this.persist();
|
|
152
|
+
if (rejected.length > 0) {
|
|
153
|
+
throw new AggregateError(rejected.map(result => result.reason), "attempt_process_supervisor_cleanup_degraded");
|
|
154
|
+
}
|
|
155
|
+
if (this.persistenceFailure !== undefined)
|
|
156
|
+
throw this.persistenceFailure;
|
|
157
|
+
}
|
|
158
|
+
snapshot() {
|
|
159
|
+
return {
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
attemptSupervisorId: this.identity.processSupervisorId,
|
|
162
|
+
dispatchAttemptId: this.identity.dispatchAttemptId,
|
|
163
|
+
executionLeaseId: this.identity.executionLeaseId,
|
|
164
|
+
status: this.status,
|
|
165
|
+
updatedAt: new Date().toISOString(),
|
|
166
|
+
children: this.records.map(record => ({
|
|
167
|
+
name: record.name,
|
|
168
|
+
generation: record.generation,
|
|
169
|
+
childId: record.childId,
|
|
170
|
+
processSupervisorId: record.processSupervisorId,
|
|
171
|
+
metadataPath: record.metadataPath,
|
|
172
|
+
status: record.status,
|
|
173
|
+
startedAt: record.startedAt,
|
|
174
|
+
...(record.settledAt ? { settledAt: record.settledAt } : {}),
|
|
175
|
+
...(record.exitCode === undefined ? {} : { exitCode: record.exitCode }),
|
|
176
|
+
...(record.failure ? { failure: record.failure } : {}),
|
|
177
|
+
})),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async observeChildReady(record) {
|
|
181
|
+
const deadline = Date.now() + this.metadataReadyTimeoutMs;
|
|
182
|
+
while (Date.now() <= deadline) {
|
|
183
|
+
if (await fileExists(record.metadataPath)) {
|
|
184
|
+
if (record.status === "starting") {
|
|
185
|
+
record.status = "running";
|
|
186
|
+
await this.persist();
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (terminalChildStatuses.has(record.status)) {
|
|
191
|
+
throw new Error(`attempt_process_supervisor_start_failed: child '${record.childId}' exited before supervision metadata was produced.`);
|
|
192
|
+
}
|
|
193
|
+
await wait(25);
|
|
194
|
+
}
|
|
195
|
+
record.controller.abort("Supervised child metadata was not produced before the readiness deadline.");
|
|
196
|
+
throw new Error(`attempt_process_supervisor_start_timeout: child '${record.childId}' did not become supervised.`);
|
|
197
|
+
}
|
|
198
|
+
async settleChild(record, status, exitCode, failure) {
|
|
199
|
+
record.status = status;
|
|
200
|
+
record.settledAt = new Date().toISOString();
|
|
201
|
+
record.exitCode = exitCode;
|
|
202
|
+
record.failure = failure === undefined ? undefined : boundedFailure(failure);
|
|
203
|
+
await this.persist().catch(error => {
|
|
204
|
+
this.persistenceFailure ??= error;
|
|
205
|
+
this.status = "degraded";
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
assertFence(fencingToken) {
|
|
209
|
+
if (fencingToken !== this.identity.fencingToken) {
|
|
210
|
+
throw new Error("attempt_process_supervisor_fenced: fencing token does not own this attempt.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
persist() {
|
|
214
|
+
const snapshot = this.snapshot();
|
|
215
|
+
const operation = this.persistTail.then(async () => {
|
|
216
|
+
await ensurePrivateDirectory(this.identity.logDirectory);
|
|
217
|
+
const temporary = `${this.ledgerPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
218
|
+
try {
|
|
219
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
220
|
+
try {
|
|
221
|
+
await handle.writeFile(`${JSON.stringify(snapshot)}\n`);
|
|
222
|
+
await handle.sync();
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
await handle.close();
|
|
226
|
+
}
|
|
227
|
+
await fs.rename(temporary, this.ledgerPath);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
this.persistTail = operation.catch(() => undefined);
|
|
235
|
+
return operation;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function validateIdentity(identity) {
|
|
239
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u.test(identity.processSupervisorId)) {
|
|
240
|
+
throw new Error("attempt_process_supervisor_invalid: process supervisor id is invalid.");
|
|
241
|
+
}
|
|
242
|
+
assertUuid(identity.dispatchAttemptId, "dispatch attempt");
|
|
243
|
+
assertUuid(identity.executionLeaseId, "execution lease");
|
|
244
|
+
assertUuid(identity.fencingToken, "fencing token");
|
|
245
|
+
if (!path.isAbsolute(identity.logDirectory)) {
|
|
246
|
+
throw new Error("attempt_process_supervisor_invalid: log directory must be absolute.");
|
|
247
|
+
}
|
|
248
|
+
return { ...identity, logDirectory: path.resolve(identity.logDirectory) };
|
|
249
|
+
}
|
|
250
|
+
function assertChildName(name) {
|
|
251
|
+
if (!/^[a-z][a-z0-9_-]{0,31}$/u.test(name)) {
|
|
252
|
+
throw new Error("attempt_process_supervisor_invalid: child name must be a safe lowercase identifier.");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function childProcessSupervisorId(attemptSupervisorId, childId) {
|
|
256
|
+
const digest = createHash("sha256")
|
|
257
|
+
.update(JSON.stringify([attemptSupervisorId, childId]))
|
|
258
|
+
.digest("hex")
|
|
259
|
+
.slice(0, 32);
|
|
260
|
+
return `host-${digest}`;
|
|
261
|
+
}
|
|
262
|
+
function assertUuid(value, label) {
|
|
263
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value)) {
|
|
264
|
+
throw new Error(`attempt_process_supervisor_invalid: ${label} id must be a UUID.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function boundedInteger(value, minimum, maximum, name) {
|
|
268
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
269
|
+
throw new Error(`attempt_process_supervisor_invalid: ${name} must be between ${minimum} and ${maximum}.`);
|
|
270
|
+
}
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function boundedFailure(error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
return message.replaceAll(/[\r\n\t]+/gu, " ").slice(0, 512);
|
|
276
|
+
}
|
|
277
|
+
function cancellationError(signal) {
|
|
278
|
+
return new Error(String(signal.reason ?? "Supervised child launch was cancelled."));
|
|
279
|
+
}
|
|
280
|
+
async function ensurePrivateDirectory(directory) {
|
|
281
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
282
|
+
if (process.platform !== "win32")
|
|
283
|
+
await fs.chmod(directory, 0o700);
|
|
284
|
+
}
|
|
285
|
+
async function fileExists(target) {
|
|
286
|
+
return await fs.stat(target).then(() => true, () => false);
|
|
287
|
+
}
|
|
288
|
+
function wait(milliseconds) {
|
|
289
|
+
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
290
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AttemptProcessChildHandle } from "./attempt-process-supervisor.js";
|
|
2
|
+
import type { HostPortLeaseOwner, HostPortReservation } from "./host-port-broker.js";
|
|
3
|
+
import type { HostListenerOwnership } from "./node-host-runtime-adapter.js";
|
|
4
|
+
export interface HostInspectionCommandResult {
|
|
5
|
+
readonly exitCode: number;
|
|
6
|
+
readonly stdout: string;
|
|
7
|
+
readonly stderr: string;
|
|
8
|
+
readonly errorCode?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HostInspectionCommandRunner {
|
|
11
|
+
run(command: string, args: readonly string[]): Promise<HostInspectionCommandResult>;
|
|
12
|
+
}
|
|
13
|
+
/** Proves that a loopback listener belongs to one exact supervised child. */
|
|
14
|
+
export declare class HostListenerInspector {
|
|
15
|
+
private readonly commands;
|
|
16
|
+
private readonly platform;
|
|
17
|
+
constructor(commands?: HostInspectionCommandRunner, platform?: NodeJS.Platform);
|
|
18
|
+
inspect(child: AttemptProcessChildHandle, reservation: HostPortReservation, owner: HostPortLeaseOwner): Promise<HostListenerOwnership>;
|
|
19
|
+
private inspectWindows;
|
|
20
|
+
private inspectUnix;
|
|
21
|
+
}
|
|
22
|
+
export declare class ExecFileInspectionCommandRunner implements HostInspectionCommandRunner {
|
|
23
|
+
run(command: string, args: readonly string[]): Promise<HostInspectionCommandResult>;
|
|
24
|
+
}
|
|
25
|
+
export declare function parseWindowsListenerPids(output: string, port: number): readonly number[];
|
|
26
|
+
export declare function parseLsofPids(output: string): readonly number[];
|
|
27
|
+
export declare function parseSsPids(output: string, port: number): readonly number[];
|
|
28
|
+
export declare function parseProcessGroups(output: string): ReadonlyMap<number, number>;
|