@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 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 (168) hide show
  1. package/README.md +127 -0
  2. package/dist/bridge/bridge.base.d.ts +8 -1
  3. package/dist/bridge/bridge.service.d.ts +93 -3
  4. package/dist/bridge/bridge.service.js +1358 -47
  5. package/dist/bridge/bridge.types.d.ts +202 -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 +47 -1
  16. package/dist/cli/cli.service.d.ts +45 -2
  17. package/dist/cli/cli.service.js +378 -26
  18. package/dist/cli/local-config.d.ts +7 -0
  19. package/dist/cli/local-config.js +51 -3
  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/command-runner.d.ts +4 -0
  53. package/dist/sandbox/command-runner.js +47 -0
  54. package/dist/sandbox/contract.d.ts +5 -0
  55. package/dist/sandbox/contract.js +276 -0
  56. package/dist/sandbox/event-budget.d.ts +11 -0
  57. package/dist/sandbox/event-budget.js +27 -0
  58. package/dist/sandbox/image-policy.d.ts +35 -0
  59. package/dist/sandbox/image-policy.js +247 -0
  60. package/dist/sandbox/index.d.ts +5 -0
  61. package/dist/sandbox/index.js +5 -0
  62. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  63. package/dist/sandbox/lease-watchdog.js +67 -0
  64. package/dist/sandbox/preflight.d.ts +84 -0
  65. package/dist/sandbox/preflight.js +521 -0
  66. package/dist/sandbox/process.d.ts +20 -0
  67. package/dist/sandbox/process.js +209 -0
  68. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  69. package/dist/sandbox/provider-auth/codex.js +212 -0
  70. package/dist/sandbox/repository-lock.d.ts +5 -0
  71. package/dist/sandbox/repository-lock.js +121 -0
  72. package/dist/sandbox/runtime.d.ts +297 -0
  73. package/dist/sandbox/runtime.js +2338 -0
  74. package/dist/sandbox/sandbox.types.d.ts +93 -0
  75. package/dist/sandbox/sandbox.types.js +1 -0
  76. package/dist/sandbox/smoke.d.ts +76 -0
  77. package/dist/sandbox/smoke.js +380 -0
  78. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  79. package/dist/sandbox/worker-supervisor.js +36 -0
  80. package/dist/sandbox/worker.d.ts +1 -0
  81. package/dist/sandbox/worker.js +225 -0
  82. package/dist/sandbox/workspace.service.d.ts +13 -0
  83. package/dist/sandbox/workspace.service.js +72 -0
  84. package/dist/session/managed-attempt-tools.d.ts +21 -0
  85. package/dist/session/managed-attempt-tools.js +912 -0
  86. package/dist/session/managed-build-coordinator.d.ts +5 -0
  87. package/dist/session/managed-build-coordinator.js +105 -0
  88. package/dist/session/managed-build-receipt.d.ts +21 -0
  89. package/dist/session/managed-build-receipt.js +335 -0
  90. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  91. package/dist/session/phase-completion.artifacts.js +256 -26
  92. package/dist/session/phase-completion.d.ts +12 -2
  93. package/dist/session/phase-completion.helpers.d.ts +5 -0
  94. package/dist/session/phase-completion.helpers.js +51 -10
  95. package/dist/session/phase-completion.js +14 -3
  96. package/dist/session/phase-completion.prompts.d.ts +3 -3
  97. package/dist/session/phase-completion.prompts.js +24 -8
  98. package/dist/session/phase-completion.runtime.d.ts +13 -1
  99. package/dist/session/phase-completion.runtime.js +189 -8
  100. package/dist/session/phase-completion.schemas.js +117 -4
  101. package/dist/session/phase-completion.validation.d.ts +4 -1
  102. package/dist/session/phase-completion.validation.js +153 -22
  103. package/dist/session/pre-publication-validation.d.ts +46 -0
  104. package/dist/session/pre-publication-validation.js +208 -0
  105. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  106. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  107. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  108. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  109. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  110. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  111. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  112. package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
  113. package/dist/session/provider-cli/provider-cli.process.js +474 -23
  114. package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
  115. package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
  116. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  117. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  118. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  119. package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
  120. package/dist/session/provider-cli/provider-cli.service.js +646 -90
  121. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  122. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  123. package/dist/session/provider-cli/provider-output-state.js +91 -16
  124. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  125. package/dist/session/provider-cli/provider-usage.js +28 -5
  126. package/dist/session/session-redaction.d.ts +3 -0
  127. package/dist/session/session-redaction.js +38 -0
  128. package/dist/session/session.base.d.ts +6 -1
  129. package/dist/session/session.service.d.ts +8 -2
  130. package/dist/session/session.service.js +530 -42
  131. package/dist/session/session.types.d.ts +124 -1
  132. package/dist/session/session.types.js +231 -4
  133. package/dist/shared/format/format-bytes.d.ts +1 -0
  134. package/dist/shared/format/format-bytes.js +9 -0
  135. package/dist/shared/path/path.d.ts +1 -0
  136. package/dist/shared/path/path.js +21 -1
  137. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  138. package/dist/shared/process/dotnet-toolchain.js +179 -0
  139. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  140. package/dist/shared/process/host-process-recovery.js +78 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  142. package/dist/shared/process/host-process-supervisor.js +330 -0
  143. package/dist/shared/process/process.d.ts +1 -0
  144. package/dist/shared/process/process.js +1 -0
  145. package/dist/shared/process/windows-launch.d.ts +1 -1
  146. package/dist/shared/process/windows-launch.js +6 -3
  147. package/dist/shared/provider-capabilities.d.ts +1 -0
  148. package/dist/shared/provider-capabilities.js +3 -0
  149. package/dist/shared/redaction/redaction.js +16 -4
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/dist/worktree/artifact-injector.d.ts +3 -2
  153. package/dist/worktree/artifact-injector.js +43 -4
  154. package/dist/worktree/dependency-warmer.d.ts +36 -2
  155. package/dist/worktree/dependency-warmer.js +347 -11
  156. package/dist/worktree/index.d.ts +2 -1
  157. package/dist/worktree/index.js +1 -0
  158. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  159. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  160. package/dist/worktree/worktree-paths.d.ts +4 -0
  161. package/dist/worktree/worktree-paths.js +30 -0
  162. package/dist/worktree/worktree.base.d.ts +3 -0
  163. package/dist/worktree/worktree.service.d.ts +13 -2
  164. package/dist/worktree/worktree.service.js +238 -47
  165. package/dist/worktree/worktree.types.d.ts +5 -1
  166. package/package.json +8 -4
  167. package/runtime/windows-job-supervisor.ps1 +444 -0
  168. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -45,6 +45,15 @@ export function applyLocalConfigDefaults(config, local) {
45
45
  deviceName: process.env.FORGE_DEVICE_NAME ?? local.deviceName ?? config.deviceName,
46
46
  defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? local.defaultRepoRoot ?? config.defaultRepoRoot,
47
47
  cleanupWorktrees: local.cleanupWorktrees ?? config.cleanupWorktrees,
48
+ dockerEnabled: process.env.FORGE_DOCKER_SANDBOX === undefined
49
+ ? local.dockerEnabled ?? config.dockerEnabled
50
+ : config.dockerEnabled,
51
+ dockerDesktopRecoveryEnabled: process.env.FORGE_DOCKER_DESKTOP_AUTO_RECOVERY === undefined
52
+ ? local.dockerDesktopRecoveryEnabled ?? config.dockerDesktopRecoveryEnabled
53
+ : config.dockerDesktopRecoveryEnabled,
54
+ allowOpenPublicSandbox: process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX === undefined
55
+ ? local.allowOpenPublicSandbox ?? config.allowOpenPublicSandbox
56
+ : config.allowOpenPublicSandbox,
48
57
  cliProviders,
49
58
  providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
50
59
  projectLabels: local.projectLabels ?? config.projectLabels,
@@ -54,7 +63,9 @@ export function applyLocalConfigDefaults(config, local) {
54
63
  supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
55
64
  supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
56
65
  supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
57
- supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce
66
+ supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce,
67
+ supportsDraftAttachments: local.supportsDraftAttachments ?? config.supportsDraftAttachments,
68
+ supportsDraftImageAttachments: local.supportsDraftImageAttachments ?? config.supportsDraftImageAttachments
58
69
  };
59
70
  }
60
71
  export async function addRepoMapping(label, repoPath) {
@@ -86,6 +97,19 @@ export async function removeRepoMapping(label) {
86
97
  await writeLocalConfig(next);
87
98
  return { config: next, removed: after.length !== before.length };
88
99
  }
100
+ export async function setServerUrl(serverUrl) {
101
+ const normalized = normalizeServerUrl(serverUrl);
102
+ const current = await readLocalConfig();
103
+ const next = normalizeLocalConfig({ ...current, serverUrl: normalized });
104
+ await writeLocalConfig(next);
105
+ return next;
106
+ }
107
+ export async function setSandboxPolicy(dockerEnabled, allowOpenPublicSandbox) {
108
+ const current = await readLocalConfig();
109
+ const next = normalizeLocalConfig({ ...current, dockerEnabled, allowOpenPublicSandbox });
110
+ await writeLocalConfig(next);
111
+ return next;
112
+ }
89
113
  export function providerStatusesFor(providers) {
90
114
  return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
91
115
  }
@@ -104,6 +128,9 @@ function normalizeLocalConfig(config) {
104
128
  deviceName: cleanString(config.deviceName),
105
129
  defaultRepoRoot: cleanString(config.defaultRepoRoot),
106
130
  cleanupWorktrees: config.cleanupWorktrees,
131
+ dockerEnabled: config.dockerEnabled,
132
+ dockerDesktopRecoveryEnabled: config.dockerDesktopRecoveryEnabled,
133
+ allowOpenPublicSandbox: config.allowOpenPublicSandbox,
107
134
  cliProviders: cleanList(config.cliProviders),
108
135
  providerStatuses: normalizeProviderStatuses(config.providerStatuses),
109
136
  projectLabels: cleanList(config.projectLabels),
@@ -113,7 +140,9 @@ function normalizeLocalConfig(config) {
113
140
  supportsManagedWorktrees: config.supportsManagedWorktrees,
114
141
  supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
115
142
  supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
116
- supportsDraftRunOnce: config.supportsDraftRunOnce
143
+ supportsDraftRunOnce: config.supportsDraftRunOnce,
144
+ supportsDraftAttachments: config.supportsDraftAttachments,
145
+ supportsDraftImageAttachments: config.supportsDraftImageAttachments
117
146
  };
118
147
  }
119
148
  function emptyConfig() {
@@ -196,10 +225,11 @@ function normalizeQaAppLaunchTarget(value) {
196
225
  command: cleanString(value.command),
197
226
  working_directory: cleanString(value.working_directory),
198
227
  ready_url: cleanString(value.ready_url),
228
+ managed_runtime_port: cleanString(value.managed_runtime_port),
199
229
  routes,
200
230
  notes: cleanString(value.notes)
201
231
  };
202
- return normalized.url || normalized.command || normalized.ready_url || routes?.length
232
+ return normalized.url || normalized.command || normalized.ready_url || normalized.managed_runtime_port
203
233
  ? normalized
204
234
  : undefined;
205
235
  }
@@ -207,6 +237,24 @@ function cleanString(value) {
207
237
  const clean = value?.trim();
208
238
  return clean ? clean : undefined;
209
239
  }
240
+ function normalizeServerUrl(value) {
241
+ const clean = value.trim();
242
+ let parsed;
243
+ try {
244
+ parsed = new URL(clean);
245
+ }
246
+ catch {
247
+ throw new Error(`Invalid Forge server URL: ${value}`);
248
+ }
249
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
250
+ throw new Error("Forge server URL must use http or https.");
251
+ }
252
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
253
+ throw new Error("Forge server URL cannot include credentials, a query string, or a fragment.");
254
+ }
255
+ parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
256
+ return parsed.toString().replace(/\/$/u, "");
257
+ }
210
258
  function cleanList(values) {
211
259
  const clean = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
212
260
  return clean.length > 0 ? clean : undefined;
@@ -0,0 +1,36 @@
1
+ export interface AspNetRuntimePrebuildPlan {
2
+ readonly fingerprint: string;
3
+ readonly receiptPath: string;
4
+ readonly command: string;
5
+ readonly args: readonly string[];
6
+ readonly cwd: string;
7
+ readonly environment: Readonly<Record<string, string>>;
8
+ readonly projectPath: string;
9
+ readonly worktreePath: string;
10
+ readonly inputCommitSha: string;
11
+ }
12
+ export interface AspNetRuntimePrebuildResult {
13
+ readonly cacheHit: boolean;
14
+ readonly receiptPath: string;
15
+ readonly fingerprint: string;
16
+ }
17
+ export interface AspNetRuntimeBuildExecutionResult {
18
+ readonly exitCode: number;
19
+ readonly stdoutTail: string;
20
+ readonly stderrTail: string;
21
+ readonly stdoutLogPath: string;
22
+ readonly stderrLogPath: string;
23
+ }
24
+ export type AspNetRuntimeBuildExecutor = (plan: AspNetRuntimePrebuildPlan, signal?: AbortSignal) => Promise<AspNetRuntimeBuildExecutionResult>;
25
+ export declare function createAspNetRuntimePrebuildPlan(options: {
26
+ readonly command: string;
27
+ readonly args: readonly string[];
28
+ readonly cwd: string;
29
+ readonly worktreePath: string;
30
+ readonly environment?: Readonly<Record<string, string>>;
31
+ readonly inputCommitSha: string;
32
+ readonly runtimeContractFingerprint: string;
33
+ }): AspNetRuntimePrebuildPlan | null;
34
+ export declare function ensureAspNetRuntimePrebuilt(plan: AspNetRuntimePrebuildPlan, execute: AspNetRuntimeBuildExecutor, signal?: AbortSignal): Promise<AspNetRuntimePrebuildResult>;
35
+ export declare function withAspNetPrebuiltRunArguments(command: string, args: readonly string[]): readonly string[];
36
+ export declare function resolveWorktreeCommitSha(worktreePath: string, supplied?: string | null): Promise<string>;
@@ -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
+ }