@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53
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/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +8 -0
- package/dist/bridge/bridge.service.js +184 -12
- package/dist/bridge/bridge.types.d.ts +37 -0
- package/dist/cli/cli.help.js +1 -1
- package/dist/cli/cli.service.js +1 -1
- package/dist/cli/local-config.js +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
- package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
- package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
- package/dist/runtime/attempt-process-supervisor.js +75 -15
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +16 -0
- package/dist/runtime/host-listener-inspector.js +143 -4
- package/dist/runtime/host-runtime-contract.d.ts +9 -1
- package/dist/runtime/host-runtime-contract.js +411 -6
- package/dist/runtime/host-runtime-lease.d.ts +12 -1
- package/dist/runtime/host-runtime-lease.js +271 -50
- package/dist/runtime/host-runtime-promotion.d.ts +2 -0
- package/dist/runtime/host-runtime-promotion.js +35 -4
- package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
- package/dist/runtime/node-host-runtime-adapter.js +71 -8
- package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
- package/dist/sandbox/preflight.d.ts +9 -0
- package/dist/sandbox/preflight.js +39 -4
- package/dist/sandbox/workspace.service.js +7 -2
- package/dist/session/managed-attempt-tools.d.ts +17 -0
- package/dist/session/managed-attempt-tools.js +712 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +11 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +45 -9
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +21 -6
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +183 -8
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +144 -18
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.process.js +176 -18
- package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
- package/dist/session/provider-cli/provider-cli.service.js +390 -83
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session.base.d.ts +2 -1
- package/dist/session/session.service.d.ts +2 -1
- package/dist/session/session.service.js +303 -30
- package/dist/session/session.types.d.ts +38 -0
- package/dist/session/session.types.js +17 -0
- package/dist/shared/process/host-process-supervisor.d.ts +18 -1
- package/dist/shared/process/host-process-supervisor.js +94 -7
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +12 -1
- package/package.json +1 -1
- package/runtime/windows-job-supervisor.ps1 +89 -20
|
@@ -1,13 +1,25 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { isAlias, parseDocument, visit } from "yaml";
|
|
4
5
|
export const hostRuntimeContractPath = ".forge/forge.runtime.yml";
|
|
5
6
|
const maximumContractBytes = 64 * 1024;
|
|
6
7
|
const maximumDepth = 12;
|
|
8
|
+
const ignoredDiscoveryDirectories = new Set([
|
|
9
|
+
".forge",
|
|
10
|
+
".forge-designer",
|
|
11
|
+
".git",
|
|
12
|
+
".local",
|
|
13
|
+
"bin",
|
|
14
|
+
"coverage",
|
|
15
|
+
"dist",
|
|
16
|
+
"node_modules",
|
|
17
|
+
"obj",
|
|
18
|
+
]);
|
|
7
19
|
const topLevelKeys = new Set(["schemaVersion", "runtime"]);
|
|
8
20
|
const runtimeKeys = new Set(["hostConcurrency", "ports", "playwright", "browserProfile", "singletonResources"]);
|
|
9
21
|
const portKeys = new Set(["adapter", "launch", "env", "baseUrlEnv", "format"]);
|
|
10
|
-
const launchKeys = new Set(["command", "args", "workingDirectory"]);
|
|
22
|
+
const launchKeys = new Set(["command", "args", "workingDirectory", "environment"]);
|
|
11
23
|
const playwrightKeys = new Set(["config", "baseUrlPort", "workers"]);
|
|
12
24
|
export async function loadHostRuntimeContract(worktreePath) {
|
|
13
25
|
const target = path.join(path.resolve(worktreePath), hostRuntimeContractPath);
|
|
@@ -17,7 +29,7 @@ export async function loadHostRuntimeContract(worktreePath) {
|
|
|
17
29
|
}
|
|
18
30
|
catch (error) {
|
|
19
31
|
if (isFileNotFound(error))
|
|
20
|
-
return
|
|
32
|
+
return await discoverHostRuntimeContract(worktreePath);
|
|
21
33
|
throw error;
|
|
22
34
|
}
|
|
23
35
|
if (!targetStat.isFile() || targetStat.size < 1 || targetStat.size > maximumContractBytes) {
|
|
@@ -33,6 +45,382 @@ export async function loadHostRuntimeContract(worktreePath) {
|
|
|
33
45
|
source: "project",
|
|
34
46
|
};
|
|
35
47
|
}
|
|
48
|
+
async function discoverHostRuntimeContract(worktreePath) {
|
|
49
|
+
const root = path.resolve(worktreePath);
|
|
50
|
+
const [nodeDiscovery, aspNetDiscovery] = await Promise.all([
|
|
51
|
+
discoverNodeRuntimes(root),
|
|
52
|
+
discoverAspNetRuntimes(root),
|
|
53
|
+
]);
|
|
54
|
+
if (nodeDiscovery.ambiguous || aspNetDiscovery.ambiguous) {
|
|
55
|
+
return ambiguousRuntimeDiscovery();
|
|
56
|
+
}
|
|
57
|
+
const candidates = [...nodeDiscovery.candidates, ...aspNetDiscovery.candidates];
|
|
58
|
+
if (candidates.length === 1) {
|
|
59
|
+
const candidate = candidates[0];
|
|
60
|
+
return {
|
|
61
|
+
contract: candidate.contract,
|
|
62
|
+
source: "derived",
|
|
63
|
+
discovery: {
|
|
64
|
+
status: "derived",
|
|
65
|
+
reasonCode: candidate.reasonCode,
|
|
66
|
+
summary: candidate.summary,
|
|
67
|
+
candidateFingerprint: candidate.candidateFingerprint,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (candidates.length === 2
|
|
72
|
+
&& candidates.some(candidate => candidate.kind === "browser")
|
|
73
|
+
&& candidates.some(candidate => candidate.kind === "api")
|
|
74
|
+
&& candidates.find(candidate => candidate.kind === "browser")?.contract.runtime.playwright !== null) {
|
|
75
|
+
const browser = candidates.find(candidate => candidate.kind === "browser");
|
|
76
|
+
const api = candidates.find(candidate => candidate.kind === "api");
|
|
77
|
+
const combined = combineBrowserAndApiRuntime(browser, api);
|
|
78
|
+
return {
|
|
79
|
+
contract: combined.contract,
|
|
80
|
+
source: "derived",
|
|
81
|
+
discovery: {
|
|
82
|
+
status: "derived",
|
|
83
|
+
reasonCode: combined.reasonCode,
|
|
84
|
+
summary: combined.summary,
|
|
85
|
+
candidateFingerprint: combined.candidateFingerprint,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (candidates.length > 1) {
|
|
90
|
+
return ambiguousRuntimeDiscovery();
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
contract: defaultHostRuntimeContract(),
|
|
94
|
+
source: "default",
|
|
95
|
+
discovery: {
|
|
96
|
+
status: "unsupported",
|
|
97
|
+
reasonCode: "runtime_discovery_unsupported",
|
|
98
|
+
summary: "Forge did not find one deterministic supported application entrypoint; host application runs remain serialized.",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function discoverNodeRuntimes(root) {
|
|
103
|
+
const manifests = [...new Set([
|
|
104
|
+
path.join(root, "package.json"),
|
|
105
|
+
...await findFiles(root, file => path.basename(file).toLowerCase() === "package.json", 4, 500),
|
|
106
|
+
])];
|
|
107
|
+
const candidates = (await Promise.all(manifests.map(async (manifestPath) => await discoverNodeRuntime(root, manifestPath)))).filter((candidate) => candidate !== null);
|
|
108
|
+
return { candidates: candidates.length === 1 ? candidates : [], ambiguous: candidates.length > 1 };
|
|
109
|
+
}
|
|
110
|
+
async function discoverNodeRuntime(root, manifestPath) {
|
|
111
|
+
const source = await readBoundedOptionalFile(manifestPath, 256 * 1024);
|
|
112
|
+
if (source === null)
|
|
113
|
+
return null;
|
|
114
|
+
let manifest;
|
|
115
|
+
try {
|
|
116
|
+
manifest = JSON.parse(source);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const scripts = objectOrEmpty(manifest.scripts);
|
|
122
|
+
const dependencies = { ...objectOrEmpty(manifest.dependencies), ...objectOrEmpty(manifest.devDependencies) };
|
|
123
|
+
const dev = typeof scripts.dev === "string" ? scripts.dev.trim() : "";
|
|
124
|
+
const start = typeof scripts.start === "string" ? scripts.start.trim() : "";
|
|
125
|
+
const scriptName = dev ? "dev" : start ? "start" : null;
|
|
126
|
+
const script = scriptName ? String(scripts[scriptName]).trim() : "";
|
|
127
|
+
if (!scriptName || !script || script.length > 1024 || /(?:&&|\|\||;|\n|\r)/u.test(script))
|
|
128
|
+
return null;
|
|
129
|
+
const vite = Object.hasOwn(dependencies, "vite") || /(^|\s)vite(?:\s|$)/u.test(script);
|
|
130
|
+
const next = Object.hasOwn(dependencies, "next") || /(^|\s)next(?:\s|$)/u.test(script);
|
|
131
|
+
if (vite === next)
|
|
132
|
+
return null;
|
|
133
|
+
const packageManager = await nearestPackageManager(root, path.dirname(manifestPath));
|
|
134
|
+
if (!packageManager)
|
|
135
|
+
return null;
|
|
136
|
+
const adapter = vite ? "vite" : "next";
|
|
137
|
+
const playwrightConfigs = [...new Set((await Promise.all([
|
|
138
|
+
...["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"]
|
|
139
|
+
.flatMap(file => [file, `e2e/${file}`, `tests/e2e/${file}`])
|
|
140
|
+
.map(async (candidate) => await optionalFileExists(path.join(root, candidate)) ? path.join(root, candidate) : null),
|
|
141
|
+
findFiles(root, file => /^playwright\.config\.(?:ts|js|mjs|cjs)$/iu.test(path.basename(file)), 4, 2_000),
|
|
142
|
+
])).flat().filter((value) => value !== null))]
|
|
143
|
+
.map(config => portableRelativePath(root, config));
|
|
144
|
+
const playwright = playwrightConfigs.length === 1
|
|
145
|
+
? { config: playwrightConfigs[0], baseUrlPort: "web", workers: 1 }
|
|
146
|
+
: null;
|
|
147
|
+
const workingDirectory = portableRelativePath(root, path.dirname(manifestPath)) || ".";
|
|
148
|
+
const contract = {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
runtime: {
|
|
151
|
+
hostConcurrency: "parallel",
|
|
152
|
+
ports: [{
|
|
153
|
+
name: "web",
|
|
154
|
+
adapter,
|
|
155
|
+
launch: {
|
|
156
|
+
command: packageManager,
|
|
157
|
+
args: packageManager === "npm" ? ["run", scriptName, "--"] : ["run", scriptName],
|
|
158
|
+
workingDirectory,
|
|
159
|
+
},
|
|
160
|
+
env: "PORT",
|
|
161
|
+
baseUrlEnv: "APP_BASE_URL",
|
|
162
|
+
format: "http://{host}:{port}",
|
|
163
|
+
}],
|
|
164
|
+
playwright,
|
|
165
|
+
browserProfile: "isolated",
|
|
166
|
+
singletonResources: [],
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
kind: "browser",
|
|
171
|
+
contract,
|
|
172
|
+
reasonCode: `runtime_discovery_${adapter}`,
|
|
173
|
+
summary: `Forge derived one ${adapter === "vite" ? "Vite" : "Next.js"} application entrypoint from package.json.`,
|
|
174
|
+
candidateFingerprint: detectorFingerprint({
|
|
175
|
+
manifest: portableRelativePath(root, manifestPath),
|
|
176
|
+
source,
|
|
177
|
+
packageManager,
|
|
178
|
+
scriptName,
|
|
179
|
+
script,
|
|
180
|
+
adapter,
|
|
181
|
+
playwrightConfigs,
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function discoverAspNetRuntimes(root) {
|
|
186
|
+
const projects = await findFiles(root, file => file.toLowerCase().endsWith(".csproj"), 3, 200);
|
|
187
|
+
const webProjects = [];
|
|
188
|
+
for (const projectPath of projects) {
|
|
189
|
+
const source = await readBoundedOptionalFile(projectPath, 512 * 1024);
|
|
190
|
+
if (source && /<Project\s+Sdk=["']Microsoft\.NET\.Sdk\.Web["']/iu.test(source)) {
|
|
191
|
+
webProjects.push({ relativePath: path.relative(root, projectPath).replaceAll("\\", "/"), source });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (webProjects.length !== 1)
|
|
195
|
+
return { candidates: [], ambiguous: webProjects.length > 1 };
|
|
196
|
+
const project = webProjects[0];
|
|
197
|
+
const launchProfile = await discoverAspNetLaunchProfile(root, project.relativePath);
|
|
198
|
+
const contract = {
|
|
199
|
+
schemaVersion: 1,
|
|
200
|
+
runtime: {
|
|
201
|
+
hostConcurrency: "parallel",
|
|
202
|
+
ports: [{
|
|
203
|
+
name: "web",
|
|
204
|
+
adapter: "aspnetcore",
|
|
205
|
+
launch: {
|
|
206
|
+
command: "dotnet",
|
|
207
|
+
args: ["run", "--project", project.relativePath],
|
|
208
|
+
workingDirectory: ".",
|
|
209
|
+
...(Object.keys(launchProfile.environment).length > 0
|
|
210
|
+
? { environment: launchProfile.environment }
|
|
211
|
+
: {}),
|
|
212
|
+
},
|
|
213
|
+
env: "ASPNETCORE_URLS",
|
|
214
|
+
baseUrlEnv: "APP_BASE_URL",
|
|
215
|
+
format: "http://{host}:{port}",
|
|
216
|
+
}],
|
|
217
|
+
playwright: null,
|
|
218
|
+
browserProfile: "isolated",
|
|
219
|
+
singletonResources: [],
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
return { candidates: [{
|
|
223
|
+
kind: "api",
|
|
224
|
+
contract,
|
|
225
|
+
reasonCode: "runtime_discovery_aspnetcore",
|
|
226
|
+
summary: "Forge derived one ASP.NET Core application entrypoint from the repository project metadata.",
|
|
227
|
+
candidateFingerprint: detectorFingerprint({
|
|
228
|
+
project: project.relativePath,
|
|
229
|
+
source: project.source,
|
|
230
|
+
launchSettings: launchProfile.source,
|
|
231
|
+
launchProfile: launchProfile.name,
|
|
232
|
+
}),
|
|
233
|
+
}], ambiguous: false };
|
|
234
|
+
}
|
|
235
|
+
function combineBrowserAndApiRuntime(browser, api) {
|
|
236
|
+
const browserPort = browser.contract.runtime.ports.find(port => port.launch !== null);
|
|
237
|
+
const apiPort = api.contract.runtime.ports.find(port => port.launch !== null);
|
|
238
|
+
const combinedBrowserPort = {
|
|
239
|
+
...browserPort,
|
|
240
|
+
name: "web",
|
|
241
|
+
env: "WEB_PORT",
|
|
242
|
+
baseUrlEnv: "WEB_BASE_URL",
|
|
243
|
+
launch: browserPort.launch ? {
|
|
244
|
+
...browserPort.launch,
|
|
245
|
+
environment: {
|
|
246
|
+
...browserPort.launch.environment,
|
|
247
|
+
FORGE_API_PROXY_TARGET: "${FORGE_API_URL}",
|
|
248
|
+
FORGE_DEV_PUBLIC_ORIGIN: "${FORGE_RUNTIME_BASE_URL}",
|
|
249
|
+
},
|
|
250
|
+
} : null,
|
|
251
|
+
};
|
|
252
|
+
const combinedApiPort = {
|
|
253
|
+
...apiPort,
|
|
254
|
+
name: "api",
|
|
255
|
+
env: "ASPNETCORE_URLS",
|
|
256
|
+
baseUrlEnv: "API_BASE_URL",
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
kind: "browser",
|
|
260
|
+
contract: {
|
|
261
|
+
schemaVersion: 1,
|
|
262
|
+
runtime: {
|
|
263
|
+
hostConcurrency: "parallel",
|
|
264
|
+
ports: [combinedApiPort, combinedBrowserPort],
|
|
265
|
+
playwright: browser.contract.runtime.playwright
|
|
266
|
+
? { ...browser.contract.runtime.playwright, baseUrlPort: "web" }
|
|
267
|
+
: null,
|
|
268
|
+
browserProfile: "isolated",
|
|
269
|
+
singletonResources: [],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
reasonCode: "runtime_discovery_full_stack",
|
|
273
|
+
summary: "Forge derived one supervised API and browser application pair from repository metadata.",
|
|
274
|
+
candidateFingerprint: detectorFingerprint({
|
|
275
|
+
browser: browser.candidateFingerprint,
|
|
276
|
+
api: api.candidateFingerprint,
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function ambiguousRuntimeDiscovery() {
|
|
281
|
+
return {
|
|
282
|
+
contract: defaultHostRuntimeContract(),
|
|
283
|
+
source: "default",
|
|
284
|
+
discovery: {
|
|
285
|
+
status: "ambiguous",
|
|
286
|
+
reasonCode: "runtime_discovery_multiple_applications",
|
|
287
|
+
summary: "Forge found more than one supported application entrypoint, so host application runs remain serialized.",
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async function nearestPackageManager(root, start) {
|
|
292
|
+
let directory = path.resolve(start);
|
|
293
|
+
const resolvedRoot = path.resolve(root);
|
|
294
|
+
while (directory === resolvedRoot || directory.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
295
|
+
const lockfiles = (await Promise.all([
|
|
296
|
+
optionalFileExists(path.join(directory, "pnpm-lock.yaml")).then(exists => exists ? "pnpm" : null),
|
|
297
|
+
optionalFileExists(path.join(directory, "yarn.lock")).then(exists => exists ? "yarn" : null),
|
|
298
|
+
optionalFileExists(path.join(directory, "package-lock.json")).then(exists => exists ? "npm" : null),
|
|
299
|
+
optionalFileExists(path.join(directory, "npm-shrinkwrap.json")).then(exists => exists ? "npm" : null),
|
|
300
|
+
])).filter((value) => value !== null);
|
|
301
|
+
const packageManagers = [...new Set(lockfiles)];
|
|
302
|
+
if (packageManagers.length > 1)
|
|
303
|
+
return null;
|
|
304
|
+
if (packageManagers.length === 1)
|
|
305
|
+
return packageManagers[0];
|
|
306
|
+
if (directory === resolvedRoot)
|
|
307
|
+
break;
|
|
308
|
+
directory = path.dirname(directory);
|
|
309
|
+
}
|
|
310
|
+
return "npm";
|
|
311
|
+
}
|
|
312
|
+
function portableRelativePath(root, target) {
|
|
313
|
+
return path.relative(root, target).replaceAll("\\", "/");
|
|
314
|
+
}
|
|
315
|
+
async function discoverAspNetLaunchProfile(root, projectRelativePath) {
|
|
316
|
+
const target = path.join(root, path.dirname(projectRelativePath), "Properties", "launchSettings.json");
|
|
317
|
+
const source = await readBoundedOptionalFile(target, 512 * 1024);
|
|
318
|
+
if (!source)
|
|
319
|
+
return { name: null, source: null, environment: {} };
|
|
320
|
+
try {
|
|
321
|
+
const document = objectOrEmpty(JSON.parse(source));
|
|
322
|
+
const profiles = objectOrEmpty(document.profiles);
|
|
323
|
+
const candidates = Object.entries(profiles)
|
|
324
|
+
.map(([name, value]) => ({ name, profile: objectOrEmpty(value) }))
|
|
325
|
+
.filter(candidate => String(candidate.profile.commandName ?? "").toLowerCase() === "project")
|
|
326
|
+
.sort((left, right) => aspNetProfilePriority(left.name) - aspNetProfilePriority(right.name)
|
|
327
|
+
|| left.name.localeCompare(right.name));
|
|
328
|
+
const selected = candidates[0];
|
|
329
|
+
if (!selected)
|
|
330
|
+
return { name: null, source, environment: {} };
|
|
331
|
+
const environmentVariables = objectOrEmpty(selected.profile.environmentVariables);
|
|
332
|
+
const environment = Object.fromEntries(Object.entries(environmentVariables)
|
|
333
|
+
.filter((entry) => safeAspNetLaunchEnvironment(entry[0], entry[1]))
|
|
334
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
335
|
+
.slice(0, 64));
|
|
336
|
+
return { name: selected.name, source, environment };
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return { name: null, source, environment: {} };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function aspNetProfilePriority(name) {
|
|
343
|
+
if (name.toLowerCase() === "http")
|
|
344
|
+
return 0;
|
|
345
|
+
if (name.toLowerCase() === "https")
|
|
346
|
+
return 1;
|
|
347
|
+
return 2;
|
|
348
|
+
}
|
|
349
|
+
function safeAspNetLaunchEnvironment(name, value) {
|
|
350
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name)
|
|
351
|
+
|| typeof value !== "string"
|
|
352
|
+
|| value.length === 0
|
|
353
|
+
|| value.length > 8192)
|
|
354
|
+
return false;
|
|
355
|
+
const normalized = name.toUpperCase();
|
|
356
|
+
return !normalized.startsWith("FORGE_")
|
|
357
|
+
&& !new Set([
|
|
358
|
+
"ASPNETCORE_URLS",
|
|
359
|
+
"ASPNETCORE_HTTP_PORTS",
|
|
360
|
+
"ASPNETCORE_HTTPS_PORTS",
|
|
361
|
+
"DOTNET_URLS",
|
|
362
|
+
"HOST",
|
|
363
|
+
"PORT",
|
|
364
|
+
"PATH",
|
|
365
|
+
"HOME",
|
|
366
|
+
"TMP",
|
|
367
|
+
"TEMP",
|
|
368
|
+
"TMPDIR",
|
|
369
|
+
]).has(normalized);
|
|
370
|
+
}
|
|
371
|
+
async function findFiles(root, accepts, maximumDepth, maximumEntries) {
|
|
372
|
+
const results = [];
|
|
373
|
+
let examined = 0;
|
|
374
|
+
const visitDirectory = async (directory, depth) => {
|
|
375
|
+
if (depth > maximumDepth || examined >= maximumEntries)
|
|
376
|
+
return;
|
|
377
|
+
const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
378
|
+
for (const entry of entries) {
|
|
379
|
+
if (examined++ >= maximumEntries)
|
|
380
|
+
return;
|
|
381
|
+
if (entry.isDirectory() && ignoredDiscoveryDirectories.has(entry.name.toLowerCase()))
|
|
382
|
+
continue;
|
|
383
|
+
const target = path.join(directory, entry.name);
|
|
384
|
+
if (entry.isDirectory())
|
|
385
|
+
await visitDirectory(target, depth + 1);
|
|
386
|
+
else if (entry.isFile() && accepts(target))
|
|
387
|
+
results.push(target);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
await visitDirectory(root, 0);
|
|
391
|
+
return results.sort((left, right) => left.localeCompare(right));
|
|
392
|
+
}
|
|
393
|
+
async function readBoundedOptionalFile(target, maximumBytes) {
|
|
394
|
+
try {
|
|
395
|
+
const stat = await fs.stat(target);
|
|
396
|
+
if (!stat.isFile() || stat.size < 1 || stat.size > maximumBytes)
|
|
397
|
+
return null;
|
|
398
|
+
return await fs.readFile(target, "utf8");
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
if (isFileNotFound(error))
|
|
402
|
+
return null;
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function optionalFileExists(target) {
|
|
407
|
+
try {
|
|
408
|
+
return (await fs.stat(target)).isFile();
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (isFileNotFound(error))
|
|
412
|
+
return false;
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function objectOrEmpty(value) {
|
|
417
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
418
|
+
? value
|
|
419
|
+
: {};
|
|
420
|
+
}
|
|
421
|
+
function detectorFingerprint(value) {
|
|
422
|
+
return `sha256:${createHash("sha256").update(JSON.stringify({ adapterVersion: 2, value })).digest("hex")}`;
|
|
423
|
+
}
|
|
36
424
|
export function defaultHostRuntimeContract() {
|
|
37
425
|
return {
|
|
38
426
|
schemaVersion: 1,
|
|
@@ -162,9 +550,6 @@ function parsePorts(value) {
|
|
|
162
550
|
}
|
|
163
551
|
return { name, adapter, launch, env, baseUrlEnv, format };
|
|
164
552
|
});
|
|
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
553
|
return parsed;
|
|
169
554
|
}
|
|
170
555
|
function parsePortAdapter(value, name) {
|
|
@@ -192,7 +577,27 @@ function parseLaunch(value, adapter, name) {
|
|
|
192
577
|
const portableDirectory = launch.workingDirectory === undefined || launch.workingDirectory === null
|
|
193
578
|
? "."
|
|
194
579
|
: containedPortablePath(launch.workingDirectory, `runtime.ports.${name}.launch.workingDirectory`);
|
|
195
|
-
|
|
580
|
+
const environment = parseLaunchEnvironment(launch.environment, name);
|
|
581
|
+
return {
|
|
582
|
+
command,
|
|
583
|
+
args: parsedArgs,
|
|
584
|
+
workingDirectory: portableDirectory,
|
|
585
|
+
...(environment ? { environment } : {}),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function parseLaunchEnvironment(value, portName) {
|
|
589
|
+
if (value === undefined || value === null)
|
|
590
|
+
return undefined;
|
|
591
|
+
const environment = object(value, `runtime.ports.${portName}.launch.environment`);
|
|
592
|
+
if (Object.keys(environment).length > 64) {
|
|
593
|
+
throw new Error(`runtime_contract_invalid: runtime.ports.${portName}.launch.environment cannot contain more than 64 entries.`);
|
|
594
|
+
}
|
|
595
|
+
return Object.fromEntries(Object.entries(environment).map(([name, raw]) => {
|
|
596
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name)) {
|
|
597
|
+
throw new Error(`runtime_contract_invalid: launch environment name '${name}' is invalid.`);
|
|
598
|
+
}
|
|
599
|
+
return [name, boundedString(raw, 8192, `runtime.ports.${portName}.launch.environment.${name}`)];
|
|
600
|
+
}));
|
|
196
601
|
}
|
|
197
602
|
function containedPortablePath(value, label) {
|
|
198
603
|
const original = boundedString(value, 1024, label);
|
|
@@ -13,7 +13,9 @@ export interface HostRuntimeLeaseStart {
|
|
|
13
13
|
readonly lease: RuntimeIsolationLease;
|
|
14
14
|
readonly environment: Readonly<Record<string, string>>;
|
|
15
15
|
readonly runtimeContract: LoadedHostRuntimeContract;
|
|
16
|
+
readonly executionClass: HostRuntimeExecutionClass;
|
|
16
17
|
}
|
|
18
|
+
export type HostRuntimeExecutionClass = "analysis_only" | "managed_runtime";
|
|
17
19
|
export type HostRuntimeSafetyClass = "host_serial_only" | "host_parallel_candidate" | "host_parallel_safe";
|
|
18
20
|
export interface HostRuntimeProjectCapacitySnapshot {
|
|
19
21
|
readonly repoId: string;
|
|
@@ -22,6 +24,9 @@ export interface HostRuntimeProjectCapacitySnapshot {
|
|
|
22
24
|
readonly reservedAttempts: number;
|
|
23
25
|
readonly waitingAttempts: number;
|
|
24
26
|
readonly degradedAttempts: number;
|
|
27
|
+
readonly runtimeContractFingerprint?: string | null;
|
|
28
|
+
readonly runtimeContractSource?: "project" | "derived" | null;
|
|
29
|
+
readonly promotedAt?: string | null;
|
|
25
30
|
}
|
|
26
31
|
export interface HostRuntimeCapacitySnapshot {
|
|
27
32
|
readonly activeAttempts: number;
|
|
@@ -52,7 +57,9 @@ export interface HostRuntimeLeaseRuntime {
|
|
|
52
57
|
start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
53
58
|
startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
54
59
|
updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
|
|
60
|
+
providerProcessRunner?(dispatchAttemptId: string, fencingToken: string): ProviderProcessRunner | null;
|
|
55
61
|
release(dispatchAttemptId: string, fencingToken: string): Promise<RuntimeIsolationLease | null>;
|
|
62
|
+
invalidatePromotion?(repoId: string): Promise<void>;
|
|
56
63
|
reap?(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport>;
|
|
57
64
|
snapshot?(): HostRuntimeCapacitySnapshot;
|
|
58
65
|
}
|
|
@@ -72,6 +79,7 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
|
|
|
72
79
|
private readonly parallelProjectOwners;
|
|
73
80
|
private readonly parallelProjectWaiters;
|
|
74
81
|
private readonly degraded;
|
|
82
|
+
private readonly knownPromotions;
|
|
75
83
|
private readonly portBroker;
|
|
76
84
|
private readonly nodeAdapter;
|
|
77
85
|
constructor(stateRoot: string, portBroker?: HostPortBroker, nodeAdapter?: NodeHostRuntimeAdapter, processSupervisors?: AttemptProcessSupervisorRegistry, processRunner?: ProviderProcessRunner, listenerInspector?: HostListenerInspector, promotionStore?: HostParallelPromotionStore, playwrightAdapter?: PlaywrightHostRuntimeAdapter, processRecovery?: HostProcessRecovery);
|
|
@@ -83,6 +91,7 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
|
|
|
83
91
|
start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
84
92
|
updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
|
|
85
93
|
startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
|
|
94
|
+
providerProcessRunner(dispatchAttemptId: string, fencingToken: string): ProviderProcessRunner | null;
|
|
86
95
|
private promoteVerifiedRuntime;
|
|
87
96
|
launchNodeRuntime<T>(dispatchAttemptId: string, fencingToken: string, portName: string, command: NodeRuntimeCommand, launcher: NodeRuntimeCommandLauncher<T>, signal?: AbortSignal): Promise<HostNodeRuntimeLaunch<T>>;
|
|
88
97
|
release(dispatchAttemptId: string, fencingToken: string): Promise<RuntimeIsolationLease | null>;
|
|
@@ -109,6 +118,8 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
|
|
|
109
118
|
private cancelAdmissionWaiters;
|
|
110
119
|
private drainSingletonWaiters;
|
|
111
120
|
private drainParallelProjectWaiters;
|
|
121
|
+
private refreshKnownPromotions;
|
|
122
|
+
invalidatePromotion(repoId: string): Promise<void>;
|
|
112
123
|
private drainAdmissionWaiters;
|
|
113
124
|
}
|
|
114
125
|
export declare class HostRuntimeLeaseCleanupError extends Error {
|
|
@@ -118,5 +129,5 @@ export declare class HostRuntimeLeaseCleanupError extends Error {
|
|
|
118
129
|
export declare class HostRuntimeAdmissionExpiredError extends Error {
|
|
119
130
|
constructor();
|
|
120
131
|
}
|
|
121
|
-
export declare function hostRuntimeEventResources(lease: RuntimeIsolationLease, runtimeContract?: LoadedHostRuntimeContract): Record<string, unknown>;
|
|
132
|
+
export declare function hostRuntimeEventResources(lease: RuntimeIsolationLease, runtimeContract?: LoadedHostRuntimeContract, executionClass?: HostRuntimeExecutionClass): Record<string, unknown>;
|
|
122
133
|
export declare function hostRuntimeSingletonKeys(dispatch: RunOnceDispatchPacket): readonly string[];
|