@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 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 +776 -19
- package/dist/bridge/bridge.types.d.ts +131 -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.js +46 -0
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +350 -23
- package/dist/cli/local-config.d.ts +4 -0
- package/dist/cli/local-config.js +39 -0
- 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.js +23 -2
- package/dist/session/phase-completion.helpers.js +6 -1
- package/dist/session/phase-completion.runtime.js +3 -1
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +20 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +3 -0
- package/dist/session/provider-cli/provider-cli.process.js +65 -14
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +14 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +99 -15
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.service.js +167 -28
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -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 +184 -38
- package/dist/session/session.types.d.ts +58 -1
- package/dist/session/session.types.js +172 -2
- 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/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -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 +6 -2
- package/dist/worktree/worktree.service.js +60 -43
- 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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isAlias, parseDocument, visit } from "yaml";
|
|
4
|
+
export const hostRuntimeContractPath = ".forge/forge.runtime.yml";
|
|
5
|
+
const maximumContractBytes = 64 * 1024;
|
|
6
|
+
const maximumDepth = 12;
|
|
7
|
+
const topLevelKeys = new Set(["schemaVersion", "runtime"]);
|
|
8
|
+
const runtimeKeys = new Set(["hostConcurrency", "ports", "playwright", "browserProfile", "singletonResources"]);
|
|
9
|
+
const portKeys = new Set(["adapter", "launch", "env", "baseUrlEnv", "format"]);
|
|
10
|
+
const launchKeys = new Set(["command", "args", "workingDirectory"]);
|
|
11
|
+
const playwrightKeys = new Set(["config", "baseUrlPort", "workers"]);
|
|
12
|
+
export async function loadHostRuntimeContract(worktreePath) {
|
|
13
|
+
const target = path.join(path.resolve(worktreePath), hostRuntimeContractPath);
|
|
14
|
+
let targetStat;
|
|
15
|
+
try {
|
|
16
|
+
targetStat = await fs.stat(target);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (isFileNotFound(error))
|
|
20
|
+
return { contract: defaultHostRuntimeContract(), source: "default" };
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!targetStat.isFile() || targetStat.size < 1 || targetStat.size > maximumContractBytes) {
|
|
24
|
+
throw new Error(`runtime_contract_invalid: forge.runtime.yml must be a regular 1-${maximumContractBytes} byte file.`);
|
|
25
|
+
}
|
|
26
|
+
const [realWorktree, realTarget] = await Promise.all([fs.realpath(worktreePath), fs.realpath(target)]);
|
|
27
|
+
const relative = path.relative(realWorktree, realTarget);
|
|
28
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
29
|
+
throw new Error("runtime_contract_invalid: forge.runtime.yml must remain inside the prepared worktree.");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
contract: validateHostRuntimeContract(await fs.readFile(realTarget, "utf8")),
|
|
33
|
+
source: "project",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function defaultHostRuntimeContract() {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
runtime: {
|
|
40
|
+
hostConcurrency: "serial",
|
|
41
|
+
ports: [],
|
|
42
|
+
playwright: null,
|
|
43
|
+
browserProfile: "isolated",
|
|
44
|
+
singletonResources: [],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function validateHostRuntimeContract(source) {
|
|
49
|
+
const byteLength = Buffer.byteLength(source, "utf8");
|
|
50
|
+
if (byteLength === 0 || byteLength > maximumContractBytes) {
|
|
51
|
+
throw new Error(`runtime_contract_invalid: forge.runtime.yml must be 1-${maximumContractBytes} bytes.`);
|
|
52
|
+
}
|
|
53
|
+
if (Array.from(source).some(character => {
|
|
54
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
55
|
+
return codePoint === 127 || (codePoint < 32 && codePoint !== 9 && codePoint !== 10 && codePoint !== 13);
|
|
56
|
+
})) {
|
|
57
|
+
throw new Error("runtime_contract_invalid: control characters are not allowed.");
|
|
58
|
+
}
|
|
59
|
+
const document = parseDocument(source, { uniqueKeys: true, prettyErrors: false, strict: true });
|
|
60
|
+
if (document.errors.length > 0) {
|
|
61
|
+
throw new Error(`runtime_contract_invalid: ${document.errors[0]?.message ?? "invalid YAML"}`);
|
|
62
|
+
}
|
|
63
|
+
visit(document, {
|
|
64
|
+
Node(_key, node) {
|
|
65
|
+
if (isAlias(node) || node.anchor) {
|
|
66
|
+
throw new Error("runtime_contract_invalid: YAML aliases and anchors are not allowed.");
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const parsed = document.toJS({ maxAliasCount: 0 });
|
|
71
|
+
assertDepth(parsed, 0);
|
|
72
|
+
const root = object(parsed, "contract");
|
|
73
|
+
rejectUnknownKeys(root, topLevelKeys, "contract");
|
|
74
|
+
if (root.schemaVersion !== 1)
|
|
75
|
+
throw new Error("runtime_contract_invalid: schemaVersion must be 1.");
|
|
76
|
+
const runtime = object(root.runtime, "runtime");
|
|
77
|
+
rejectUnknownKeys(runtime, runtimeKeys, "runtime");
|
|
78
|
+
const hostConcurrency = runtime.hostConcurrency ?? "serial";
|
|
79
|
+
if (hostConcurrency !== "serial" && hostConcurrency !== "parallel" && hostConcurrency !== "disabled") {
|
|
80
|
+
throw new Error("runtime_contract_invalid: runtime.hostConcurrency must be serial, parallel, or disabled.");
|
|
81
|
+
}
|
|
82
|
+
const browserProfile = runtime.browserProfile ?? "isolated";
|
|
83
|
+
if (browserProfile !== "isolated") {
|
|
84
|
+
throw new Error("runtime_contract_invalid: runtime.browserProfile must be isolated.");
|
|
85
|
+
}
|
|
86
|
+
const ports = parsePorts(runtime.ports);
|
|
87
|
+
return {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
runtime: {
|
|
90
|
+
hostConcurrency,
|
|
91
|
+
ports,
|
|
92
|
+
playwright: parsePlaywright(runtime.playwright, ports),
|
|
93
|
+
browserProfile,
|
|
94
|
+
singletonResources: parseSingletonResources(runtime.singletonResources),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function parsePlaywright(value, ports) {
|
|
99
|
+
if (value === undefined || value === null)
|
|
100
|
+
return null;
|
|
101
|
+
const playwright = object(value, "runtime.playwright");
|
|
102
|
+
rejectUnknownKeys(playwright, playwrightKeys, "runtime.playwright");
|
|
103
|
+
const config = containedPortablePath(playwright.config, "runtime.playwright.config");
|
|
104
|
+
const baseUrlPort = requiredString(playwright.baseUrlPort, "runtime.playwright.baseUrlPort");
|
|
105
|
+
const port = ports.find(entry => entry.name === baseUrlPort);
|
|
106
|
+
if (!port?.launch || !port.baseUrlEnv || !port.format) {
|
|
107
|
+
throw new Error("runtime_contract_invalid: runtime.playwright.baseUrlPort must reference a launched HTTP port.");
|
|
108
|
+
}
|
|
109
|
+
const workers = playwright.workers === undefined || playwright.workers === null ? 1 : playwright.workers;
|
|
110
|
+
if (!Number.isSafeInteger(workers) || workers < 1 || workers > 16) {
|
|
111
|
+
throw new Error("runtime_contract_invalid: runtime.playwright.workers must be an integer between 1 and 16.");
|
|
112
|
+
}
|
|
113
|
+
return { config, baseUrlPort, workers: workers };
|
|
114
|
+
}
|
|
115
|
+
export function hostPortEnvironment(config, hostIp, hostPort) {
|
|
116
|
+
if (!Number.isSafeInteger(hostPort) || hostPort < 1 || hostPort > 65535) {
|
|
117
|
+
throw new Error("runtime_contract_invalid: assigned host port must be valid.");
|
|
118
|
+
}
|
|
119
|
+
const baseUrl = config.format
|
|
120
|
+
?.replaceAll("{host}", hostIp)
|
|
121
|
+
.replaceAll("{port}", String(hostPort));
|
|
122
|
+
const environment = {
|
|
123
|
+
[config.env]: config.adapter === "aspnetcore" && baseUrl ? baseUrl : String(hostPort),
|
|
124
|
+
};
|
|
125
|
+
if (config.baseUrlEnv && config.format) {
|
|
126
|
+
environment[config.baseUrlEnv] = baseUrl;
|
|
127
|
+
}
|
|
128
|
+
return environment;
|
|
129
|
+
}
|
|
130
|
+
function parsePorts(value) {
|
|
131
|
+
if (value === undefined || value === null)
|
|
132
|
+
return [];
|
|
133
|
+
const ports = object(value, "runtime.ports");
|
|
134
|
+
if (Object.keys(ports).length > 16) {
|
|
135
|
+
throw new Error("runtime_contract_invalid: runtime.ports cannot contain more than 16 entries.");
|
|
136
|
+
}
|
|
137
|
+
const environmentNames = new Set();
|
|
138
|
+
const parsed = Object.entries(ports).sort(([left], [right]) => left.localeCompare(right)).map(([name, raw]) => {
|
|
139
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/u.test(name)) {
|
|
140
|
+
throw new Error(`runtime_contract_invalid: port name '${name}' is invalid.`);
|
|
141
|
+
}
|
|
142
|
+
const port = object(raw, `runtime.ports.${name}`);
|
|
143
|
+
rejectUnknownKeys(port, portKeys, `runtime.ports.${name}`);
|
|
144
|
+
const adapter = parsePortAdapter(port.adapter, name);
|
|
145
|
+
const launch = parseLaunch(port.launch, adapter, name);
|
|
146
|
+
const env = environmentName(port.env, `runtime.ports.${name}.env`);
|
|
147
|
+
if (adapter === "aspnetcore" && env !== "ASPNETCORE_URLS") {
|
|
148
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.env must be ASPNETCORE_URLS for the aspnetcore adapter.`);
|
|
149
|
+
}
|
|
150
|
+
const baseUrlEnv = port.baseUrlEnv === undefined || port.baseUrlEnv === null
|
|
151
|
+
? null
|
|
152
|
+
: environmentName(port.baseUrlEnv, `runtime.ports.${name}.baseUrlEnv`);
|
|
153
|
+
if (environmentNames.has(env) || (baseUrlEnv !== null && environmentNames.has(baseUrlEnv)) || env === baseUrlEnv) {
|
|
154
|
+
throw new Error("runtime_contract_invalid: port environment names must be unique.");
|
|
155
|
+
}
|
|
156
|
+
environmentNames.add(env);
|
|
157
|
+
if (baseUrlEnv)
|
|
158
|
+
environmentNames.add(baseUrlEnv);
|
|
159
|
+
const format = parseFormat(port.format, baseUrlEnv, name);
|
|
160
|
+
if (launch && (!baseUrlEnv || !format)) {
|
|
161
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch requires an HTTP baseUrlEnv and format.`);
|
|
162
|
+
}
|
|
163
|
+
return { name, adapter, launch, env, baseUrlEnv, format };
|
|
164
|
+
});
|
|
165
|
+
if (parsed.filter(port => port.launch !== null).length > 1) {
|
|
166
|
+
throw new Error("runtime_contract_invalid: only one project launch command is currently supported.");
|
|
167
|
+
}
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
170
|
+
function parsePortAdapter(value, name) {
|
|
171
|
+
if (value === undefined || value === null)
|
|
172
|
+
return null;
|
|
173
|
+
if (value !== "node" && value !== "vite" && value !== "next" && value !== "aspnetcore") {
|
|
174
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.adapter must be node, vite, next, or aspnetcore.`);
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
function parseLaunch(value, adapter, name) {
|
|
179
|
+
if (value === undefined || value === null)
|
|
180
|
+
return null;
|
|
181
|
+
if (!adapter) {
|
|
182
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch requires an explicit adapter.`);
|
|
183
|
+
}
|
|
184
|
+
const launch = object(value, `runtime.ports.${name}.launch`);
|
|
185
|
+
rejectUnknownKeys(launch, launchKeys, `runtime.ports.${name}.launch`);
|
|
186
|
+
const command = boundedString(launch.command, 1024, `runtime.ports.${name}.launch.command`);
|
|
187
|
+
const args = launch.args === undefined || launch.args === null ? [] : launch.args;
|
|
188
|
+
if (!Array.isArray(args) || args.length > 128) {
|
|
189
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch.args must contain at most 128 strings.`);
|
|
190
|
+
}
|
|
191
|
+
const parsedArgs = args.map((argument, index) => boundedString(argument, 4096, `runtime.ports.${name}.launch.args[${index}]`));
|
|
192
|
+
const portableDirectory = launch.workingDirectory === undefined || launch.workingDirectory === null
|
|
193
|
+
? "."
|
|
194
|
+
: containedPortablePath(launch.workingDirectory, `runtime.ports.${name}.launch.workingDirectory`);
|
|
195
|
+
return { command, args: parsedArgs, workingDirectory: portableDirectory };
|
|
196
|
+
}
|
|
197
|
+
function containedPortablePath(value, label) {
|
|
198
|
+
const original = boundedString(value, 1024, label);
|
|
199
|
+
const portable = original.replaceAll("\\", "/");
|
|
200
|
+
if (path.posix.isAbsolute(portable)
|
|
201
|
+
|| path.win32.isAbsolute(original)
|
|
202
|
+
|| portable.split("/").some(segment => segment === "..")) {
|
|
203
|
+
throw new Error(`runtime_contract_invalid: ${label} must stay inside the worktree.`);
|
|
204
|
+
}
|
|
205
|
+
return portable;
|
|
206
|
+
}
|
|
207
|
+
function parseFormat(value, baseUrlEnv, name) {
|
|
208
|
+
if (baseUrlEnv === null) {
|
|
209
|
+
if (value !== undefined && value !== null) {
|
|
210
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format requires baseUrlEnv.`);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const format = value === undefined || value === null ? "http://{host}:{port}" : requiredString(value, `runtime.ports.${name}.format`);
|
|
215
|
+
if (format.length > 512 || !format.includes("{port}")) {
|
|
216
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format must include {port}.`);
|
|
217
|
+
}
|
|
218
|
+
const rendered = format.replaceAll("{host}", "127.0.0.1").replaceAll("{port}", "12345");
|
|
219
|
+
let url;
|
|
220
|
+
try {
|
|
221
|
+
url = new URL(rendered);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format must produce a URL.`);
|
|
225
|
+
}
|
|
226
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.hostname !== "127.0.0.1" || url.port !== "12345") {
|
|
227
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format must produce an HTTP loopback URL using the assigned port.`);
|
|
228
|
+
}
|
|
229
|
+
return format;
|
|
230
|
+
}
|
|
231
|
+
function parseSingletonResources(value) {
|
|
232
|
+
if (value === undefined || value === null)
|
|
233
|
+
return [];
|
|
234
|
+
if (!Array.isArray(value) || value.length > 32 || value.some(entry => typeof entry !== "string")) {
|
|
235
|
+
throw new Error("runtime_contract_invalid: runtime.singletonResources must be a string array with at most 32 entries.");
|
|
236
|
+
}
|
|
237
|
+
const resources = value.map((entry, index) => {
|
|
238
|
+
const resource = requiredString(entry, `runtime.singletonResources[${index}]`).toLowerCase();
|
|
239
|
+
if (!/^[a-z][a-z0-9_.:-]{0,127}$/u.test(resource)) {
|
|
240
|
+
throw new Error(`runtime_contract_invalid: singleton resource '${resource}' is invalid.`);
|
|
241
|
+
}
|
|
242
|
+
return resource;
|
|
243
|
+
});
|
|
244
|
+
if (new Set(resources).size !== resources.length) {
|
|
245
|
+
throw new Error("runtime_contract_invalid: runtime.singletonResources cannot contain duplicates.");
|
|
246
|
+
}
|
|
247
|
+
return resources.sort((left, right) => left.localeCompare(right));
|
|
248
|
+
}
|
|
249
|
+
function environmentName(value, label) {
|
|
250
|
+
const name = requiredString(value, label);
|
|
251
|
+
if (!/^[A-Z_][A-Z0-9_]{0,127}$/u.test(name) || name.startsWith("FORGE_") || name === "PATH" || name === "HOME") {
|
|
252
|
+
throw new Error(`runtime_contract_invalid: ${label} is reserved or invalid.`);
|
|
253
|
+
}
|
|
254
|
+
return name;
|
|
255
|
+
}
|
|
256
|
+
function object(value, label) {
|
|
257
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
258
|
+
throw new Error(`runtime_contract_invalid: ${label} must be an object.`);
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
function rejectUnknownKeys(value, allowed, label) {
|
|
263
|
+
const unknown = Object.keys(value).find(key => !allowed.has(key));
|
|
264
|
+
if (unknown)
|
|
265
|
+
throw new Error(`runtime_contract_invalid: unknown ${label} key '${unknown}'.`);
|
|
266
|
+
}
|
|
267
|
+
function requiredString(value, label) {
|
|
268
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
269
|
+
throw new Error(`runtime_contract_invalid: ${label} must be a non-empty string.`);
|
|
270
|
+
}
|
|
271
|
+
return value.trim();
|
|
272
|
+
}
|
|
273
|
+
function boundedString(value, maximumLength, label) {
|
|
274
|
+
const text = requiredString(value, label);
|
|
275
|
+
if (text.length > maximumLength || Array.from(text).some(character => {
|
|
276
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
277
|
+
return codePoint < 32 || codePoint === 127;
|
|
278
|
+
})) {
|
|
279
|
+
throw new Error(`runtime_contract_invalid: ${label} is too long or contains control characters.`);
|
|
280
|
+
}
|
|
281
|
+
return text;
|
|
282
|
+
}
|
|
283
|
+
function assertDepth(value, depth) {
|
|
284
|
+
if (depth > maximumDepth)
|
|
285
|
+
throw new Error(`runtime_contract_invalid: YAML nesting exceeds ${maximumDepth}.`);
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
value.forEach(entry => assertDepth(entry, depth + 1));
|
|
288
|
+
}
|
|
289
|
+
else if (value && typeof value === "object") {
|
|
290
|
+
Object.values(value).forEach(entry => assertDepth(entry, depth + 1));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function isFileNotFound(error) {
|
|
294
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
295
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { RunOnceDispatchPacket } from "../session/session.types.js";
|
|
2
|
+
import { type ProviderProcessRunner } from "../session/provider-cli/provider-cli.process.js";
|
|
3
|
+
import { type RuntimeIsolationLease } from "./runtime-isolation-lease.js";
|
|
4
|
+
import { type LoadedHostRuntimeContract } from "./host-runtime-contract.js";
|
|
5
|
+
import { HostPortBroker } from "./host-port-broker.js";
|
|
6
|
+
import { NodeHostRuntimeAdapter, type NodeRuntimeAdapterLaunch, type NodeRuntimeCommand, type NodeRuntimeCommandLauncher } from "./node-host-runtime-adapter.js";
|
|
7
|
+
import { AttemptProcessSupervisorRegistry } from "./attempt-process-supervisor-registry.js";
|
|
8
|
+
import { HostListenerInspector } from "./host-listener-inspector.js";
|
|
9
|
+
import { PlaywrightHostRuntimeAdapter } from "./playwright-host-runtime-adapter.js";
|
|
10
|
+
import { HostParallelPromotionStore } from "./host-runtime-promotion.js";
|
|
11
|
+
import { type HostProcessRecovery } from "../shared/process/host-process-recovery.js";
|
|
12
|
+
export interface HostRuntimeLeaseStart {
|
|
13
|
+
readonly lease: RuntimeIsolationLease;
|
|
14
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
15
|
+
readonly runtimeContract: LoadedHostRuntimeContract;
|
|
16
|
+
}
|
|
17
|
+
export type HostRuntimeSafetyClass = "host_serial_only" | "host_parallel_candidate" | "host_parallel_safe";
|
|
18
|
+
export interface HostRuntimeProjectCapacitySnapshot {
|
|
19
|
+
readonly repoId: string;
|
|
20
|
+
readonly safetyClass: HostRuntimeSafetyClass;
|
|
21
|
+
readonly activeAttempts: number;
|
|
22
|
+
readonly reservedAttempts: number;
|
|
23
|
+
readonly waitingAttempts: number;
|
|
24
|
+
readonly degradedAttempts: number;
|
|
25
|
+
}
|
|
26
|
+
export interface HostRuntimeCapacitySnapshot {
|
|
27
|
+
readonly activeAttempts: number;
|
|
28
|
+
readonly reservedAttempts: number;
|
|
29
|
+
readonly waitingAttempts: number;
|
|
30
|
+
readonly degradedAttempts: number;
|
|
31
|
+
readonly exclusiveReservations: number;
|
|
32
|
+
readonly sharedReservations: number;
|
|
33
|
+
readonly projects: readonly HostRuntimeProjectCapacitySnapshot[];
|
|
34
|
+
}
|
|
35
|
+
export interface HostRuntimeReapOptions {
|
|
36
|
+
readonly runnerId: string;
|
|
37
|
+
readonly runnerInstanceId: string;
|
|
38
|
+
readonly nowMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface HostRuntimeReapReport {
|
|
41
|
+
readonly examined: number;
|
|
42
|
+
readonly reaped: number;
|
|
43
|
+
readonly processesTerminated: number;
|
|
44
|
+
readonly reusedPidsProtected: number;
|
|
45
|
+
readonly degraded: number;
|
|
46
|
+
}
|
|
47
|
+
export interface HostNodeRuntimeLaunch<T> extends NodeRuntimeAdapterLaunch<T> {
|
|
48
|
+
readonly runtimeLease: RuntimeIsolationLease;
|
|
49
|
+
}
|
|
50
|
+
export interface HostRuntimeLeaseRuntime {
|
|
51
|
+
acquire(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
|
|
52
|
+
start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
53
|
+
startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
54
|
+
updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
|
|
55
|
+
release(dispatchAttemptId: string, fencingToken: string): Promise<RuntimeIsolationLease | null>;
|
|
56
|
+
reap?(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport>;
|
|
57
|
+
snapshot?(): HostRuntimeCapacitySnapshot;
|
|
58
|
+
}
|
|
59
|
+
export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime {
|
|
60
|
+
private readonly stateRoot;
|
|
61
|
+
private readonly processSupervisors;
|
|
62
|
+
private readonly processRunner;
|
|
63
|
+
private readonly listenerInspector;
|
|
64
|
+
private readonly promotionStore;
|
|
65
|
+
private readonly playwrightAdapter;
|
|
66
|
+
private readonly processRecovery;
|
|
67
|
+
private readonly active;
|
|
68
|
+
private readonly pendingRenewals;
|
|
69
|
+
private readonly admissions;
|
|
70
|
+
private readonly singletonOwners;
|
|
71
|
+
private readonly singletonWaiters;
|
|
72
|
+
private readonly parallelProjectOwners;
|
|
73
|
+
private readonly parallelProjectWaiters;
|
|
74
|
+
private readonly degraded;
|
|
75
|
+
private readonly portBroker;
|
|
76
|
+
private readonly nodeAdapter;
|
|
77
|
+
constructor(stateRoot: string, portBroker?: HostPortBroker, nodeAdapter?: NodeHostRuntimeAdapter, processSupervisors?: AttemptProcessSupervisorRegistry, processRunner?: ProviderProcessRunner, listenerInspector?: HostListenerInspector, promotionStore?: HostParallelPromotionStore, playwrightAdapter?: PlaywrightHostRuntimeAdapter, processRecovery?: HostProcessRecovery);
|
|
78
|
+
reap(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport>;
|
|
79
|
+
snapshot(): HostRuntimeCapacitySnapshot;
|
|
80
|
+
acquire(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
|
|
81
|
+
private reserveSingletonKeys;
|
|
82
|
+
private reserveParallelProject;
|
|
83
|
+
start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
84
|
+
updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
|
|
85
|
+
startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
86
|
+
private promoteVerifiedRuntime;
|
|
87
|
+
launchNodeRuntime<T>(dispatchAttemptId: string, fencingToken: string, portName: string, command: NodeRuntimeCommand, launcher: NodeRuntimeCommandLauncher<T>, signal?: AbortSignal): Promise<HostNodeRuntimeLaunch<T>>;
|
|
88
|
+
release(dispatchAttemptId: string, fencingToken: string): Promise<RuntimeIsolationLease | null>;
|
|
89
|
+
private readLedgers;
|
|
90
|
+
private recoverPersistedProcesses;
|
|
91
|
+
private markLedgerDegraded;
|
|
92
|
+
private rememberDegraded;
|
|
93
|
+
private forgetDegraded;
|
|
94
|
+
private attemptRoot;
|
|
95
|
+
private writeLedger;
|
|
96
|
+
private ledgerPath;
|
|
97
|
+
private singletonKeysAvailable;
|
|
98
|
+
private assignSingletonKeys;
|
|
99
|
+
private releaseSingletonKeys;
|
|
100
|
+
private convertSingletonToParallelProject;
|
|
101
|
+
private parallelProjectOwned;
|
|
102
|
+
private parallelProjectAvailable;
|
|
103
|
+
private assignParallelProject;
|
|
104
|
+
private releaseParallelProjectNoDrain;
|
|
105
|
+
private releaseParallelProjects;
|
|
106
|
+
private deleteAdmission;
|
|
107
|
+
private renewAdmissionWaiterDeadline;
|
|
108
|
+
private clearPendingRenewal;
|
|
109
|
+
private cancelAdmissionWaiters;
|
|
110
|
+
private drainSingletonWaiters;
|
|
111
|
+
private drainParallelProjectWaiters;
|
|
112
|
+
private drainAdmissionWaiters;
|
|
113
|
+
}
|
|
114
|
+
export declare class HostRuntimeLeaseCleanupError extends Error {
|
|
115
|
+
readonly lease: RuntimeIsolationLease;
|
|
116
|
+
constructor(lease: RuntimeIsolationLease, cause: unknown);
|
|
117
|
+
}
|
|
118
|
+
export declare class HostRuntimeAdmissionExpiredError extends Error {
|
|
119
|
+
constructor();
|
|
120
|
+
}
|
|
121
|
+
export declare function hostRuntimeEventResources(lease: RuntimeIsolationLease, runtimeContract?: LoadedHostRuntimeContract): Record<string, unknown>;
|
|
122
|
+
export declare function hostRuntimeSingletonKeys(dispatch: RunOnceDispatchPacket): readonly string[];
|