@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.
Files changed (73) hide show
  1. package/dist/bridge/bridge.base.d.ts +2 -1
  2. package/dist/bridge/bridge.service.d.ts +8 -0
  3. package/dist/bridge/bridge.service.js +184 -12
  4. package/dist/bridge/bridge.types.d.ts +37 -0
  5. package/dist/cli/cli.help.js +1 -1
  6. package/dist/cli/cli.service.js +1 -1
  7. package/dist/cli/local-config.js +2 -1
  8. package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
  9. package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
  10. package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
  11. package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
  12. package/dist/runtime/attempt-process-supervisor.js +75 -15
  13. package/dist/runtime/generated-output-reset.d.ts +10 -0
  14. package/dist/runtime/generated-output-reset.js +103 -0
  15. package/dist/runtime/host-listener-inspector.d.ts +16 -0
  16. package/dist/runtime/host-listener-inspector.js +143 -4
  17. package/dist/runtime/host-runtime-contract.d.ts +9 -1
  18. package/dist/runtime/host-runtime-contract.js +411 -6
  19. package/dist/runtime/host-runtime-lease.d.ts +12 -1
  20. package/dist/runtime/host-runtime-lease.js +271 -50
  21. package/dist/runtime/host-runtime-promotion.d.ts +2 -0
  22. package/dist/runtime/host-runtime-promotion.js +35 -4
  23. package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
  24. package/dist/runtime/node-host-runtime-adapter.js +71 -8
  25. package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
  26. package/dist/sandbox/preflight.d.ts +9 -0
  27. package/dist/sandbox/preflight.js +39 -4
  28. package/dist/sandbox/workspace.service.js +7 -2
  29. package/dist/session/managed-attempt-tools.d.ts +17 -0
  30. package/dist/session/managed-attempt-tools.js +712 -0
  31. package/dist/session/phase-completion.artifacts.js +9 -2
  32. package/dist/session/phase-completion.d.ts +11 -2
  33. package/dist/session/phase-completion.helpers.d.ts +5 -0
  34. package/dist/session/phase-completion.helpers.js +45 -9
  35. package/dist/session/phase-completion.js +12 -3
  36. package/dist/session/phase-completion.prompts.d.ts +3 -3
  37. package/dist/session/phase-completion.prompts.js +21 -6
  38. package/dist/session/phase-completion.runtime.d.ts +13 -1
  39. package/dist/session/phase-completion.runtime.js +183 -8
  40. package/dist/session/phase-completion.schemas.js +108 -3
  41. package/dist/session/phase-completion.validation.d.ts +4 -1
  42. package/dist/session/phase-completion.validation.js +144 -18
  43. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  44. package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
  45. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  46. package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
  47. package/dist/session/provider-cli/provider-cli.process.js +176 -18
  48. package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
  49. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  50. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
  51. package/dist/session/provider-cli/provider-cli.service.js +390 -83
  52. package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
  53. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  54. package/dist/session/provider-cli/provider-output-state.js +30 -2
  55. package/dist/session/session.base.d.ts +2 -1
  56. package/dist/session/session.service.d.ts +2 -1
  57. package/dist/session/session.service.js +303 -30
  58. package/dist/session/session.types.d.ts +38 -0
  59. package/dist/session/session.types.js +17 -0
  60. package/dist/shared/process/host-process-supervisor.d.ts +18 -1
  61. package/dist/shared/process/host-process-supervisor.js +94 -7
  62. package/dist/shared/process/process.d.ts +1 -0
  63. package/dist/shared/process/process.js +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/dist/worktree/artifact-injector.d.ts +3 -2
  67. package/dist/worktree/artifact-injector.js +43 -4
  68. package/dist/worktree/dependency-warmer.d.ts +10 -1
  69. package/dist/worktree/dependency-warmer.js +94 -5
  70. package/dist/worktree/worktree.service.d.ts +1 -0
  71. package/dist/worktree/worktree.service.js +12 -1
  72. package/package.json +1 -1
  73. 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 { contract: defaultHostRuntimeContract(), source: "default" };
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
- return { command, args: parsedArgs, workingDirectory: portableDirectory };
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[];