@loucompanion/forge-bridge 0.1.3-dev.9d8f2e7e5241 → 0.1.3-dev.a58d7cd1092f

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 (143) hide show
  1. package/README.md +83 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +55 -3
  4. package/dist/bridge/bridge.service.js +834 -87
  5. package/dist/bridge/bridge.types.d.ts +89 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +37 -3
  16. package/dist/cli/cli.service.d.ts +33 -2
  17. package/dist/cli/cli.service.js +209 -27
  18. package/dist/cli/local-config.d.ts +5 -0
  19. package/dist/cli/local-config.js +45 -1
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/preflight.d.ts +45 -1
  53. package/dist/sandbox/preflight.js +299 -34
  54. package/dist/sandbox/process.js +21 -2
  55. package/dist/sandbox/repository-lock.js +41 -3
  56. package/dist/sandbox/runtime.d.ts +66 -3
  57. package/dist/sandbox/runtime.js +670 -105
  58. package/dist/sandbox/sandbox.types.d.ts +10 -0
  59. package/dist/sandbox/smoke.d.ts +76 -0
  60. package/dist/sandbox/smoke.js +380 -0
  61. package/dist/sandbox/worker-supervisor.js +1 -1
  62. package/dist/sandbox/worker.js +2 -5
  63. package/dist/sandbox/workspace.service.d.ts +1 -0
  64. package/dist/sandbox/workspace.service.js +28 -5
  65. package/dist/session/managed-attempt-tools.d.ts +21 -0
  66. package/dist/session/managed-attempt-tools.js +912 -0
  67. package/dist/session/managed-build-coordinator.d.ts +5 -0
  68. package/dist/session/managed-build-coordinator.js +105 -0
  69. package/dist/session/managed-build-receipt.d.ts +21 -0
  70. package/dist/session/managed-build-receipt.js +335 -0
  71. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  72. package/dist/session/phase-completion.artifacts.js +233 -24
  73. package/dist/session/phase-completion.d.ts +12 -2
  74. package/dist/session/phase-completion.helpers.d.ts +5 -0
  75. package/dist/session/phase-completion.helpers.js +51 -10
  76. package/dist/session/phase-completion.js +14 -3
  77. package/dist/session/phase-completion.prompts.d.ts +3 -3
  78. package/dist/session/phase-completion.prompts.js +24 -8
  79. package/dist/session/phase-completion.runtime.d.ts +13 -1
  80. package/dist/session/phase-completion.runtime.js +189 -8
  81. package/dist/session/phase-completion.schemas.js +117 -4
  82. package/dist/session/phase-completion.validation.d.ts +4 -1
  83. package/dist/session/phase-completion.validation.js +153 -22
  84. package/dist/session/pre-publication-validation.d.ts +46 -0
  85. package/dist/session/pre-publication-validation.js +208 -0
  86. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  87. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  88. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  89. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  90. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  91. package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
  92. package/dist/session/provider-cli/provider-cli.process.js +463 -34
  93. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  94. package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
  95. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  96. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  97. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  98. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
  99. package/dist/session/provider-cli/provider-cli.service.js +529 -100
  100. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  101. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  102. package/dist/session/provider-cli/provider-output-state.js +91 -16
  103. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  104. package/dist/session/provider-cli/provider-usage.js +28 -5
  105. package/dist/session/session-redaction.d.ts +3 -0
  106. package/dist/session/session-redaction.js +38 -0
  107. package/dist/session/session.base.d.ts +6 -1
  108. package/dist/session/session.service.d.ts +8 -2
  109. package/dist/session/session.service.js +522 -41
  110. package/dist/session/session.types.d.ts +72 -0
  111. package/dist/session/session.types.js +74 -3
  112. package/dist/shared/format/format-bytes.d.ts +1 -0
  113. package/dist/shared/format/format-bytes.js +9 -0
  114. package/dist/shared/path/path.d.ts +1 -0
  115. package/dist/shared/path/path.js +21 -1
  116. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  117. package/dist/shared/process/dotnet-toolchain.js +179 -0
  118. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  119. package/dist/shared/process/host-process-recovery.js +78 -0
  120. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  121. package/dist/shared/process/host-process-supervisor.js +330 -0
  122. package/dist/shared/process/process.d.ts +1 -0
  123. package/dist/shared/process/process.js +1 -0
  124. package/dist/shared/redaction/redaction.js +12 -2
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/dist/worktree/artifact-injector.d.ts +3 -2
  128. package/dist/worktree/artifact-injector.js +43 -4
  129. package/dist/worktree/dependency-warmer.d.ts +28 -1
  130. package/dist/worktree/dependency-warmer.js +308 -9
  131. package/dist/worktree/index.d.ts +2 -1
  132. package/dist/worktree/index.js +1 -0
  133. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  134. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  135. package/dist/worktree/worktree-paths.d.ts +4 -0
  136. package/dist/worktree/worktree-paths.js +30 -0
  137. package/dist/worktree/worktree.base.d.ts +3 -0
  138. package/dist/worktree/worktree.service.d.ts +13 -2
  139. package/dist/worktree/worktree.service.js +238 -47
  140. package/dist/worktree/worktree.types.d.ts +5 -1
  141. package/package.json +5 -3
  142. package/runtime/windows-job-supervisor.ps1 +444 -0
  143. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,340 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ const inFlightPrebuilds = new Map();
6
+ export function createAspNetRuntimePrebuildPlan(options) {
7
+ if (!isDotnetRun(options.command, options.args))
8
+ return null;
9
+ const projectArgument = readOptionValue(options.args, "--project");
10
+ const projectPath = path.resolve(options.cwd, projectArgument ?? ".");
11
+ const normalizedWorktree = path.resolve(options.worktreePath);
12
+ const relativeProject = path.relative(normalizedWorktree, projectPath);
13
+ if (relativeProject.startsWith("..") || path.isAbsolute(relativeProject)) {
14
+ throw new Error("host_runtime_build_invalid: the ASP.NET project must remain inside the managed worktree.");
15
+ }
16
+ const projectDirectory = projectArgument && path.extname(projectPath).toLowerCase() === ".csproj"
17
+ ? path.dirname(projectPath)
18
+ : projectPath;
19
+ const buildArgs = [
20
+ "build",
21
+ ...(projectArgument ? [projectArgument] : []),
22
+ "--nologo",
23
+ "--no-restore",
24
+ "--disable-build-servers",
25
+ "-p:UseSharedCompilation=false",
26
+ "-p:DefaultItemExcludesInProjectFolder=TestResults/**",
27
+ ...forwardBuildOptions(options.args),
28
+ ];
29
+ const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
30
+ inputCommitSha: options.inputCommitSha,
31
+ runtimeContractFingerprint: options.runtimeContractFingerprint,
32
+ project: relativeProject.replaceAll("\\", "/"),
33
+ args: buildArgs,
34
+ environment: buildEnvironmentContract(options.environment ?? {}),
35
+ })).digest("hex")}`;
36
+ return {
37
+ fingerprint,
38
+ receiptPath: path.join(projectDirectory, "obj", ".forge-runtime-build.json"),
39
+ command: options.command,
40
+ args: buildArgs,
41
+ cwd: options.cwd,
42
+ environment: options.environment ?? {},
43
+ projectPath,
44
+ worktreePath: normalizedWorktree,
45
+ inputCommitSha: options.inputCommitSha,
46
+ };
47
+ }
48
+ export async function ensureAspNetRuntimePrebuilt(plan, execute, signal) {
49
+ const pending = inFlightPrebuilds.get(plan.receiptPath);
50
+ if (pending)
51
+ return await pending;
52
+ const started = ensureAspNetRuntimePrebuiltCore(plan, execute, signal);
53
+ inFlightPrebuilds.set(plan.receiptPath, started);
54
+ try {
55
+ return await started;
56
+ }
57
+ finally {
58
+ if (inFlightPrebuilds.get(plan.receiptPath) === started)
59
+ inFlightPrebuilds.delete(plan.receiptPath);
60
+ }
61
+ }
62
+ async function ensureAspNetRuntimePrebuiltCore(plan, execute, signal) {
63
+ const inputs = await aspNetBuildInputs(plan);
64
+ const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
65
+ planFingerprint: plan.fingerprint,
66
+ ...inputs,
67
+ })).digest("hex")}`;
68
+ const receipt = await readReceipt(plan.receiptPath);
69
+ if (receipt?.fingerprint === fingerprint
70
+ && receipt.planFingerprint === plan.fingerprint
71
+ && receipt.inputCommitSha === plan.inputCommitSha
72
+ && await aspNetOutputsMatch(plan, receipt.outputs)) {
73
+ return { cacheHit: true, receiptPath: plan.receiptPath, fingerprint };
74
+ }
75
+ const result = await execute(plan, signal);
76
+ if (result.exitCode !== 0) {
77
+ const evidence = [
78
+ diagnostic("stderr", result.stderrTail),
79
+ diagnostic("stdout", result.stdoutTail),
80
+ `stdout log: ${result.stdoutLogPath}`,
81
+ `stderr log: ${result.stderrLogPath}`,
82
+ ].filter(Boolean).join("; ");
83
+ throw new Error(`host_runtime_build_failed: dotnet build exited with code ${result.exitCode}.${evidence ? ` ${evidence}` : ""}`);
84
+ }
85
+ await fs.mkdir(path.dirname(plan.receiptPath), { recursive: true });
86
+ const outputs = await collectAspNetBuildOutputs(plan);
87
+ if (outputs.length === 0) {
88
+ throw new Error("host_runtime_build_output_missing: dotnet build succeeded without producing a verifiable project output.");
89
+ }
90
+ const temporaryReceipt = `${plan.receiptPath}.${randomUUID()}.tmp`;
91
+ const nextReceipt = {
92
+ schemaVersion: 2,
93
+ fingerprint,
94
+ planFingerprint: plan.fingerprint,
95
+ inputCommitSha: plan.inputCommitSha,
96
+ projectPath: plan.projectPath,
97
+ ...inputs,
98
+ outputs,
99
+ completedAt: new Date().toISOString(),
100
+ };
101
+ await fs.writeFile(temporaryReceipt, `${JSON.stringify(nextReceipt)}\n`, { encoding: "utf8", mode: 0o600 });
102
+ try {
103
+ await fs.rename(temporaryReceipt, plan.receiptPath);
104
+ }
105
+ finally {
106
+ await fs.rm(temporaryReceipt, { force: true }).catch(() => undefined);
107
+ }
108
+ return { cacheHit: false, receiptPath: plan.receiptPath, fingerprint };
109
+ }
110
+ export function withAspNetPrebuiltRunArguments(command, args) {
111
+ if (!isDotnetRun(command, args)) {
112
+ throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
113
+ }
114
+ const applicationArguments = args.indexOf("--");
115
+ const insertionIndex = applicationArguments < 0 ? args.length : applicationArguments;
116
+ const beforeApplication = args.slice(0, insertionIndex);
117
+ return [
118
+ ...beforeApplication,
119
+ ...(!beforeApplication.includes("--no-build") ? ["--no-build"] : []),
120
+ ...(!beforeApplication.includes("--no-restore") ? ["--no-restore"] : []),
121
+ ...args.slice(insertionIndex),
122
+ ];
123
+ }
124
+ export async function resolveWorktreeCommitSha(worktreePath, supplied) {
125
+ if (supplied && /^[0-9a-f]{40,64}$/iu.test(supplied))
126
+ return supplied.toLowerCase();
127
+ return await new Promise((resolve, reject) => {
128
+ const child = spawn("git", ["rev-parse", "--verify", "HEAD^{commit}"], {
129
+ cwd: worktreePath,
130
+ windowsHide: true,
131
+ stdio: ["ignore", "pipe", "pipe"],
132
+ });
133
+ let stdout = "";
134
+ let stderr = "";
135
+ child.stdout.setEncoding("utf8");
136
+ child.stderr.setEncoding("utf8");
137
+ child.stdout.on("data", chunk => { stdout = `${stdout}${String(chunk)}`.slice(-256); });
138
+ child.stderr.on("data", chunk => { stderr = `${stderr}${String(chunk)}`.slice(-1_000); });
139
+ child.once("error", reject);
140
+ child.once("close", code => {
141
+ const commit = stdout.trim();
142
+ if (code === 0 && /^[0-9a-f]{40,64}$/iu.test(commit))
143
+ resolve(commit.toLowerCase());
144
+ else
145
+ reject(new Error(`host_runtime_build_commit_unavailable: git rev-parse failed (${code ?? "unknown"}): ${stderr.trim()}`));
146
+ });
147
+ });
148
+ }
149
+ function isDotnetRun(command, args) {
150
+ const executable = path.basename(command).toLowerCase();
151
+ return (executable === "dotnet" || executable === "dotnet.exe") && args[0]?.toLowerCase() === "run";
152
+ }
153
+ function readOptionValue(args, option) {
154
+ const index = args.findIndex(argument => argument === option || argument.startsWith(`${option}=`));
155
+ if (index < 0)
156
+ return null;
157
+ const argument = args[index];
158
+ return argument.startsWith(`${option}=`) ? argument.slice(option.length + 1) : args[index + 1] ?? null;
159
+ }
160
+ function forwardBuildOptions(args) {
161
+ const forwarded = [];
162
+ const optionsWithValues = new Set(["-c", "--configuration", "-f", "--framework", "-r", "--runtime", "--arch", "--os"]);
163
+ for (let index = 1; index < args.length; index += 1) {
164
+ const argument = args[index];
165
+ if (argument === "--")
166
+ break;
167
+ if (argument === "--project") {
168
+ index += 1;
169
+ continue;
170
+ }
171
+ if (argument.startsWith("--project="))
172
+ continue;
173
+ if (optionsWithValues.has(argument)) {
174
+ const value = args[index + 1];
175
+ if (value !== undefined) {
176
+ forwarded.push(argument, value);
177
+ index += 1;
178
+ }
179
+ continue;
180
+ }
181
+ if ([...optionsWithValues].some(option => argument.startsWith(`${option}=`))
182
+ || argument.startsWith("-p:")
183
+ || argument.startsWith("/p:")) {
184
+ forwarded.push(argument);
185
+ }
186
+ }
187
+ return forwarded;
188
+ }
189
+ async function readReceipt(receiptPath) {
190
+ try {
191
+ const parsed = JSON.parse(await fs.readFile(receiptPath, "utf8"));
192
+ return parsed.schemaVersion === 2
193
+ && typeof parsed.fingerprint === "string"
194
+ && typeof parsed.planFingerprint === "string"
195
+ && typeof parsed.inputCommitSha === "string"
196
+ && typeof parsed.sourceDigest === "string"
197
+ && typeof parsed.dependencyDigest === "string"
198
+ && typeof parsed.sdkVersion === "string"
199
+ && Array.isArray(parsed.outputs)
200
+ ? parsed
201
+ : null;
202
+ }
203
+ catch (error) {
204
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
205
+ return null;
206
+ throw error;
207
+ }
208
+ }
209
+ async function aspNetBuildInputs(plan) {
210
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
211
+ ? path.dirname(plan.projectPath)
212
+ : plan.projectPath;
213
+ const sourceDigest = await directoryContentDigest(projectDirectory);
214
+ const dependencyReceipt = path.join(projectDirectory, "obj", ".forge-dependency-preparation.json");
215
+ const dependencyContent = await fs.readFile(dependencyReceipt).catch(() => null);
216
+ const globalJson = await findAncestorFile(projectDirectory, plan.worktreePath, "global.json");
217
+ const globalJsonContent = globalJson ? await fs.readFile(globalJson) : null;
218
+ return {
219
+ sourceDigest,
220
+ dependencyDigest: `sha256:${createHash("sha256")
221
+ .update(dependencyContent ?? Buffer.alloc(0))
222
+ .update(globalJsonContent ?? Buffer.alloc(0))
223
+ .digest("hex")}`,
224
+ sdkVersion: await dotnetSdkVersion(plan.cwd),
225
+ };
226
+ }
227
+ async function directoryContentDigest(directory) {
228
+ const files = [];
229
+ await collectInputFiles(directory, directory, files);
230
+ const hash = createHash("sha256");
231
+ for (const file of files.sort((left, right) => left.localeCompare(right))) {
232
+ hash.update(path.relative(directory, file).replaceAll("\\", "/"));
233
+ hash.update("\0");
234
+ hash.update(await fs.readFile(file));
235
+ hash.update("\0");
236
+ }
237
+ return `sha256:${hash.digest("hex")}`;
238
+ }
239
+ async function collectInputFiles(root, directory, files) {
240
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
241
+ for (const entry of entries) {
242
+ const absolute = path.join(directory, entry.name);
243
+ if (entry.isDirectory()) {
244
+ if (!["bin", "obj", "node_modules", ".git", "TestResults"].includes(entry.name)) {
245
+ await collectInputFiles(root, absolute, files);
246
+ }
247
+ }
248
+ else if (entry.isFile()) {
249
+ files.push(absolute);
250
+ }
251
+ }
252
+ }
253
+ async function collectAspNetBuildOutputs(plan) {
254
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
255
+ ? path.dirname(plan.projectPath)
256
+ : plan.projectPath;
257
+ const outputRoot = path.join(projectDirectory, "bin");
258
+ const outputs = [];
259
+ await collectOutputFiles(outputRoot, projectDirectory, outputs);
260
+ return outputs.sort((left, right) => left.path.localeCompare(right.path));
261
+ }
262
+ async function collectOutputFiles(directory, projectDirectory, outputs) {
263
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
264
+ for (const entry of entries) {
265
+ const absolute = path.join(directory, entry.name);
266
+ if (entry.isDirectory())
267
+ await collectOutputFiles(absolute, projectDirectory, outputs);
268
+ else if (entry.isFile()) {
269
+ const stat = await fs.stat(absolute);
270
+ outputs.push({
271
+ path: path.relative(projectDirectory, absolute).replaceAll("\\", "/"),
272
+ size: stat.size,
273
+ digest: createHash("sha256").update(await fs.readFile(absolute)).digest("hex"),
274
+ });
275
+ }
276
+ }
277
+ }
278
+ async function aspNetOutputsMatch(plan, outputs) {
279
+ if (outputs.length === 0)
280
+ return false;
281
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
282
+ ? path.dirname(plan.projectPath)
283
+ : plan.projectPath;
284
+ for (const output of outputs) {
285
+ const absolute = path.resolve(projectDirectory, output.path);
286
+ const relative = path.relative(projectDirectory, absolute);
287
+ if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
288
+ return false;
289
+ const stat = await fs.stat(absolute).catch(() => null);
290
+ if (!stat?.isFile() || stat.size !== output.size)
291
+ return false;
292
+ const digest = createHash("sha256").update(await fs.readFile(absolute)).digest("hex");
293
+ if (digest !== output.digest)
294
+ return false;
295
+ }
296
+ return true;
297
+ }
298
+ async function findAncestorFile(directory, root, name) {
299
+ let current = path.resolve(directory);
300
+ const resolvedRoot = path.resolve(root);
301
+ while (true) {
302
+ const candidate = path.join(current, name);
303
+ if (await fs.stat(candidate).then(stat => stat.isFile()).catch(() => false))
304
+ return candidate;
305
+ if (current === resolvedRoot)
306
+ return null;
307
+ const parent = path.dirname(current);
308
+ if (parent === current || path.relative(resolvedRoot, parent).startsWith(".."))
309
+ return null;
310
+ current = parent;
311
+ }
312
+ }
313
+ async function dotnetSdkVersion(cwd) {
314
+ return await new Promise(resolve => {
315
+ const child = spawn("dotnet", ["--version"], { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
316
+ let output = "";
317
+ child.stdout.setEncoding("utf8");
318
+ child.stderr.setEncoding("utf8");
319
+ child.stdout.on("data", chunk => { output += String(chunk); });
320
+ child.stderr.on("data", chunk => { output += String(chunk); });
321
+ child.once("error", () => resolve("unavailable"));
322
+ child.once("close", code => resolve(code === 0 ? output.trim().slice(0, 256) : `unavailable:${code ?? "unknown"}`));
323
+ });
324
+ }
325
+ function buildEnvironmentContract(environment) {
326
+ return {
327
+ DOTNET_ROOT: environment.DOTNET_ROOT ?? process.env.DOTNET_ROOT ?? null,
328
+ DOTNET_ROLL_FORWARD: environment.DOTNET_ROLL_FORWARD ?? process.env.DOTNET_ROLL_FORWARD ?? null,
329
+ DOTNET_MULTILEVEL_LOOKUP: environment.DOTNET_MULTILEVEL_LOOKUP ?? process.env.DOTNET_MULTILEVEL_LOOKUP ?? null,
330
+ MSBuildSDKsPath: environment.MSBuildSDKsPath ?? process.env.MSBuildSDKsPath ?? null,
331
+ NUGET_PACKAGES: environment.NUGET_PACKAGES ?? process.env.NUGET_PACKAGES ?? null,
332
+ FORGE_BUILD_CACHE_EPOCH: createHash("sha256")
333
+ .update(environment.FORGE_BUILD_CACHE_EPOCH ?? process.env.FORGE_BUILD_CACHE_EPOCH ?? "default")
334
+ .digest("hex"),
335
+ };
336
+ }
337
+ function diagnostic(label, value) {
338
+ const normalized = value.replaceAll(/[\r\n\t]+/gu, " ").trim();
339
+ return normalized ? `${label} tail: ${normalized.slice(-2_000)}` : "";
340
+ }
@@ -0,0 +1,17 @@
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, NodeRuntimeReadinessDiagnostics } 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
+ private readonly diagnosticsByChild;
12
+ constructor(supervisor: AttemptProcessSupervisor, owner: HostPortLeaseOwner, logDirectory: string, listenerInspector?: HostListenerInspector);
13
+ launch(request: NodeRuntimeLaunchRequest, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
14
+ inspectListener(child: AttemptProcessChildHandle, request: NodeRuntimeLaunchRequest): Promise<HostListenerOwnership>;
15
+ stop(child: AttemptProcessChildHandle): Promise<void>;
16
+ readinessDiagnostics(child: AttemptProcessChildHandle): Promise<NodeRuntimeReadinessDiagnostics>;
17
+ }
@@ -0,0 +1,105 @@
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
+ const diagnosticTailLength = 4_000;
6
+ /** Launches one explicitly configured HTTP project beneath the attempt coordinator. */
7
+ export class AttemptNodeRuntimeLauncher {
8
+ supervisor;
9
+ owner;
10
+ logDirectory;
11
+ listenerInspector;
12
+ diagnosticsByChild = new WeakMap();
13
+ constructor(supervisor, owner, logDirectory, listenerInspector = new HostListenerInspector()) {
14
+ this.supervisor = supervisor;
15
+ this.owner = owner;
16
+ this.logDirectory = logDirectory;
17
+ this.listenerInspector = listenerInspector;
18
+ }
19
+ async launch(request, signal) {
20
+ const childName = projectChildName(request.port.name);
21
+ const stdoutPath = path.join(this.logDirectory, `${childName}.stdout.log`);
22
+ const stderrPath = path.join(this.logDirectory, `${childName}.stderr.log`);
23
+ const diagnostics = { exited: false, stdoutTail: "", stderrTail: "", outputRevision: 0 };
24
+ let child;
25
+ try {
26
+ child = await this.supervisor.startChildReady(childName, {
27
+ command: request.command,
28
+ args: request.args,
29
+ cwd: request.cwd,
30
+ env: request.environment,
31
+ captureOutput: false,
32
+ }, {
33
+ onStdoutLine: async (line) => {
34
+ diagnostics.stdoutTail = appendTail(diagnostics.stdoutTail, line);
35
+ diagnostics.outputRevision += 1;
36
+ await appendLine(stdoutPath, line);
37
+ },
38
+ onStderrLine: async (line) => {
39
+ diagnostics.stderrTail = appendTail(diagnostics.stderrTail, line);
40
+ diagnostics.outputRevision += 1;
41
+ await appendLine(stderrPath, line);
42
+ },
43
+ }, signal, { maxStartAttempts: 3, retryDelayMs: 250 });
44
+ }
45
+ catch (error) {
46
+ throw enrichLaunchFailure(error, diagnostics);
47
+ }
48
+ this.diagnosticsByChild.set(child, diagnostics);
49
+ void child.result.then(result => {
50
+ diagnostics.exited = true;
51
+ diagnostics.exitCode = result.exitCode;
52
+ diagnostics.stdoutTail = appendTail(diagnostics.stdoutTail, result.stdout);
53
+ diagnostics.stderrTail = appendTail(diagnostics.stderrTail, result.stderr);
54
+ diagnostics.outputRevision += 1;
55
+ }, error => {
56
+ diagnostics.exited = true;
57
+ diagnostics.stderrTail = appendTail(diagnostics.stderrTail, error instanceof Error ? error.message : String(error));
58
+ diagnostics.outputRevision += 1;
59
+ });
60
+ return child;
61
+ }
62
+ async inspectListener(child, request) {
63
+ return await this.listenerInspector.inspect(child, request.port, this.owner);
64
+ }
65
+ async stop(child) {
66
+ await child.stop(this.owner.fencingToken);
67
+ }
68
+ async readinessDiagnostics(child) {
69
+ // Yield once so an already-settled result can update the diagnostic record.
70
+ await Promise.resolve();
71
+ const diagnostics = this.diagnosticsByChild.get(child);
72
+ return diagnostics ?? { exited: false };
73
+ }
74
+ }
75
+ function projectChildName(portName) {
76
+ const candidate = `project-${portName}`;
77
+ if (/^[a-z][a-z0-9_-]{0,31}$/u.test(candidate))
78
+ return candidate;
79
+ const digest = createHash("sha256").update(portName).digest("hex").slice(0, 16);
80
+ return `project-${digest}`;
81
+ }
82
+ async function appendLine(target, line) {
83
+ await fs.appendFile(target, `${line}\n`, { encoding: "utf8", mode: 0o600 });
84
+ }
85
+ function appendTail(current, value) {
86
+ if (!value)
87
+ return current;
88
+ const combined = current ? `${current}\n${value}` : value;
89
+ return combined.slice(-diagnosticTailLength);
90
+ }
91
+ function enrichLaunchFailure(error, diagnostics) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ const evidence = [
94
+ diagnostics.stderrTail ? `stderr tail: ${boundedDiagnostic(diagnostics.stderrTail)}` : "",
95
+ diagnostics.stdoutTail ? `stdout tail: ${boundedDiagnostic(diagnostics.stdoutTail)}` : "",
96
+ ].filter(Boolean);
97
+ return evidence.length > 0
98
+ ? new Error(`${message} ${evidence.join("; ")}`, { cause: error })
99
+ : error instanceof Error
100
+ ? error
101
+ : new Error(message);
102
+ }
103
+ function boundedDiagnostic(value) {
104
+ return value.replaceAll(/[\r\n\t]+/gu, " ").trim().slice(-1_000);
105
+ }
@@ -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.startChildReady("provider", request, handlers, signal, { maxStartAttempts: request.stdin === undefined ? 1 : 3, retryDelayMs: 100 });
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,97 @@
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
+ readonly peakMemoryBytes?: number;
23
+ readonly memorySampleCount?: number;
24
+ readonly memorySource?: string;
25
+ readonly memoryObservedAt?: string;
26
+ }
27
+ export interface AttemptProcessSupervisorLedger {
28
+ readonly schemaVersion: 1;
29
+ readonly attemptSupervisorId: string;
30
+ readonly dispatchAttemptId: string;
31
+ readonly executionLeaseId: string;
32
+ readonly status: AttemptProcessSupervisorStatus;
33
+ readonly updatedAt: string;
34
+ readonly children: readonly AttemptProcessChildLedgerEntry[];
35
+ }
36
+ export interface AttemptProcessChildHandle {
37
+ readonly name: string;
38
+ readonly generation: number;
39
+ readonly childId: string;
40
+ readonly processSupervisorId: string;
41
+ readonly metadataPath: string;
42
+ readonly ready: Promise<void>;
43
+ readonly result: Promise<ProviderProcessRunResult>;
44
+ stop(fencingToken: string): Promise<ProviderProcessRunResult>;
45
+ }
46
+ export interface AttemptProcessSupervisorOptions {
47
+ readonly metadataReadyTimeoutMs?: number;
48
+ readonly signal?: AbortSignal;
49
+ }
50
+ export interface AttemptProcessChildReadyOptions {
51
+ readonly maxStartAttempts?: number;
52
+ readonly retryDelayMs?: number;
53
+ }
54
+ /**
55
+ * Owns the independently supervised process trees that belong to one host
56
+ * dispatch attempt. Each named child gets a distinct Windows Job Object or
57
+ * Unix process group through the existing process runner, while this
58
+ * coordinator supplies shared fencing, cancellation, and a durable ledger.
59
+ */
60
+ export declare class AttemptProcessSupervisor {
61
+ private readonly runner;
62
+ readonly ledgerPath: string;
63
+ private readonly identity;
64
+ private readonly metadataReadyTimeoutMs;
65
+ private readonly records;
66
+ private readonly currentByName;
67
+ private readonly attemptController;
68
+ private status;
69
+ private persistTail;
70
+ private persistenceFailure;
71
+ private stopOperation;
72
+ constructor(identity: AttemptProcessSupervisorIdentity, runner: ProviderProcessRunner, options?: AttemptProcessSupervisorOptions);
73
+ startChild(name: string, request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
74
+ /**
75
+ * Starts a child and proves that the OS process tree is supervised before
76
+ * returning it to the caller. A launch that settles before producing its
77
+ * provider-ready supervision record is safe to retry: the raw runner only
78
+ * persists that record after the actual provider child has started, before
79
+ * Forge announces the provider as active.
80
+ */
81
+ startChildReady(name: string, request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal, options?: AttemptProcessChildReadyOptions): Promise<AttemptProcessChildHandle>;
82
+ stopChild(name: string, generation: number, fencingToken: string, reason?: unknown): Promise<ProviderProcessRunResult>;
83
+ stopAll(fencingToken: string, reason?: unknown): Promise<void>;
84
+ private performStopAll;
85
+ snapshot(): AttemptProcessSupervisorLedger;
86
+ providerMemoryTelemetry(): AttemptProcessTreeMemoryTelemetry | null;
87
+ private observeChildReady;
88
+ private settleChild;
89
+ private assertFence;
90
+ private persist;
91
+ }
92
+ export interface AttemptProcessTreeMemoryTelemetry {
93
+ readonly peakMemoryBytes: number;
94
+ readonly sampleCount: number;
95
+ readonly source: string;
96
+ readonly observedAt: string;
97
+ }