@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.1ff37038366e

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 (221) hide show
  1. package/README.md +165 -1
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +9 -1
  5. package/dist/bridge/bridge.service.d.ts +129 -3
  6. package/dist/bridge/bridge.service.js +2427 -70
  7. package/dist/bridge/bridge.types.d.ts +218 -2
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +14 -2
  21. package/dist/cleanup/cleanup.service.js +496 -3
  22. package/dist/cleanup/cleanup.types.d.ts +10 -0
  23. package/dist/cleanup/index.d.ts +1 -1
  24. package/dist/cleanup/index.js +1 -1
  25. package/dist/cleanup/internals.d.ts +2 -2
  26. package/dist/cleanup/internals.js +1 -1
  27. package/dist/cli/cli.help.d.ts +2 -0
  28. package/dist/cli/cli.help.js +217 -0
  29. package/dist/cli/cli.service.d.ts +55 -7
  30. package/dist/cli/cli.service.js +429 -219
  31. package/dist/cli/local-config.d.ts +7 -0
  32. package/dist/cli/local-config.js +51 -3
  33. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  34. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  35. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  36. package/dist/runtime/attempt-database-broker.js +390 -0
  37. package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
  38. package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
  39. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  40. package/dist/runtime/attempt-path-operation.js +94 -0
  41. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  42. package/dist/runtime/attempt-private-directory.js +54 -0
  43. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  44. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  45. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  46. package/dist/runtime/attempt-process-supervisor.js +397 -0
  47. package/dist/runtime/canonical-source-file.d.ts +3 -0
  48. package/dist/runtime/canonical-source-file.js +54 -0
  49. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  50. package/dist/runtime/docker-engine-probe.js +333 -0
  51. package/dist/runtime/generated-output-reset.d.ts +20 -0
  52. package/dist/runtime/generated-output-reset.js +157 -0
  53. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  54. package/dist/runtime/host-database-capability-boundary.js +62 -0
  55. package/dist/runtime/host-diagnostics.d.ts +11 -0
  56. package/dist/runtime/host-diagnostics.js +191 -0
  57. package/dist/runtime/host-execution-root.d.ts +4 -0
  58. package/dist/runtime/host-execution-root.js +39 -0
  59. package/dist/runtime/host-listener-inspector.d.ts +51 -0
  60. package/dist/runtime/host-listener-inspector.js +385 -0
  61. package/dist/runtime/host-port-broker.d.ts +76 -0
  62. package/dist/runtime/host-port-broker.js +333 -0
  63. package/dist/runtime/host-runtime-contract.d.ts +58 -0
  64. package/dist/runtime/host-runtime-contract.js +813 -0
  65. package/dist/runtime/host-runtime-lease.d.ts +217 -0
  66. package/dist/runtime/host-runtime-lease.js +2487 -0
  67. package/dist/runtime/host-runtime-promotion.d.ts +32 -0
  68. package/dist/runtime/host-runtime-promotion.js +206 -0
  69. package/dist/runtime/memory-capacity.d.ts +70 -0
  70. package/dist/runtime/memory-capacity.js +123 -0
  71. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  72. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  73. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  74. package/dist/runtime/node-host-runtime-adapter.js +285 -0
  75. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  76. package/dist/runtime/pinned-directory-process.js +61 -0
  77. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  78. package/dist/runtime/pinned-source-directory.js +94 -0
  79. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  80. package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
  81. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  82. package/dist/runtime/prepared-runtime-identity.js +218 -0
  83. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  84. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  85. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  86. package/dist/runtime/runtime-isolation-lease.js +161 -0
  87. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  88. package/dist/runtime/workspace-resource-estimator.js +174 -0
  89. package/dist/sandbox/command-runner.d.ts +4 -0
  90. package/dist/sandbox/command-runner.js +47 -0
  91. package/dist/sandbox/contract.d.ts +5 -0
  92. package/dist/sandbox/contract.js +276 -0
  93. package/dist/sandbox/event-budget.d.ts +11 -0
  94. package/dist/sandbox/event-budget.js +27 -0
  95. package/dist/sandbox/image-policy.d.ts +35 -0
  96. package/dist/sandbox/image-policy.js +247 -0
  97. package/dist/sandbox/index.d.ts +5 -0
  98. package/dist/sandbox/index.js +5 -0
  99. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  100. package/dist/sandbox/lease-watchdog.js +67 -0
  101. package/dist/sandbox/preflight.d.ts +84 -0
  102. package/dist/sandbox/preflight.js +521 -0
  103. package/dist/sandbox/process.d.ts +20 -0
  104. package/dist/sandbox/process.js +209 -0
  105. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  106. package/dist/sandbox/provider-auth/codex.js +212 -0
  107. package/dist/sandbox/repository-lock.d.ts +6 -0
  108. package/dist/sandbox/repository-lock.js +128 -0
  109. package/dist/sandbox/runtime.d.ts +302 -0
  110. package/dist/sandbox/runtime.js +2367 -0
  111. package/dist/sandbox/sandbox.types.d.ts +93 -0
  112. package/dist/sandbox/sandbox.types.js +1 -0
  113. package/dist/sandbox/smoke.d.ts +76 -0
  114. package/dist/sandbox/smoke.js +380 -0
  115. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  116. package/dist/sandbox/worker-supervisor.js +36 -0
  117. package/dist/sandbox/worker.d.ts +1 -0
  118. package/dist/sandbox/worker.js +225 -0
  119. package/dist/sandbox/workspace.service.d.ts +13 -0
  120. package/dist/sandbox/workspace.service.js +74 -0
  121. package/dist/session/managed-attempt-tools.d.ts +28 -0
  122. package/dist/session/managed-attempt-tools.js +1015 -0
  123. package/dist/session/managed-build-coordinator.d.ts +5 -0
  124. package/dist/session/managed-build-coordinator.js +105 -0
  125. package/dist/session/managed-build-receipt.d.ts +21 -0
  126. package/dist/session/managed-build-receipt.js +337 -0
  127. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  128. package/dist/session/managed-verification-timeouts.js +17 -0
  129. package/dist/session/phase-completion.artifacts.d.ts +27 -0
  130. package/dist/session/phase-completion.artifacts.js +829 -0
  131. package/dist/session/phase-completion.d.ts +53 -42
  132. package/dist/session/phase-completion.helpers.d.ts +117 -0
  133. package/dist/session/phase-completion.helpers.js +707 -0
  134. package/dist/session/phase-completion.js +125 -3007
  135. package/dist/session/phase-completion.prompts.d.ts +14 -0
  136. package/dist/session/phase-completion.prompts.js +233 -0
  137. package/dist/session/phase-completion.runtime.d.ts +49 -0
  138. package/dist/session/phase-completion.runtime.js +840 -0
  139. package/dist/session/phase-completion.schemas.d.ts +3 -0
  140. package/dist/session/phase-completion.schemas.js +859 -0
  141. package/dist/session/phase-completion.validation.d.ts +38 -0
  142. package/dist/session/phase-completion.validation.js +918 -0
  143. package/dist/session/pre-publication-validation.d.ts +75 -0
  144. package/dist/session/pre-publication-validation.js +661 -0
  145. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  146. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  147. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  148. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  149. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
  150. package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
  151. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  152. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  153. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  154. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  155. package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
  156. package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
  157. package/dist/session/provider-cli/provider-cli.process.js +874 -0
  158. package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
  159. package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
  160. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  161. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  162. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  163. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  164. package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
  165. package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
  166. package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
  167. package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
  168. package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
  169. package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
  170. package/dist/session/provider-cli/provider-output-state.js +817 -0
  171. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  172. package/dist/session/provider-cli/provider-usage.js +28 -5
  173. package/dist/session/session-redaction.d.ts +3 -0
  174. package/dist/session/session-redaction.js +448 -0
  175. package/dist/session/session.base.d.ts +6 -1
  176. package/dist/session/session.service.d.ts +8 -2
  177. package/dist/session/session.service.js +586 -59
  178. package/dist/session/session.types.d.ts +324 -3
  179. package/dist/session/session.types.js +939 -10
  180. package/dist/session/unreferenced-exports.d.ts +12 -0
  181. package/dist/session/unreferenced-exports.js +59 -0
  182. package/dist/shared/format/format-bytes.d.ts +1 -0
  183. package/dist/shared/format/format-bytes.js +9 -0
  184. package/dist/shared/git/git.js +2 -1
  185. package/dist/shared/path/path.d.ts +1 -0
  186. package/dist/shared/path/path.js +21 -1
  187. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  188. package/dist/shared/process/dotnet-toolchain.js +307 -0
  189. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  190. package/dist/shared/process/host-process-recovery.js +78 -0
  191. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  192. package/dist/shared/process/host-process-supervisor.js +330 -0
  193. package/dist/shared/process/process.d.ts +3 -0
  194. package/dist/shared/process/process.js +13 -7
  195. package/dist/shared/process/windows-launch.d.ts +8 -0
  196. package/dist/shared/process/windows-launch.js +41 -0
  197. package/dist/shared/provider-capabilities.d.ts +4 -0
  198. package/dist/shared/provider-capabilities.js +88 -0
  199. package/dist/shared/redaction/redaction.d.ts +1 -0
  200. package/dist/shared/redaction/redaction.js +47 -4
  201. package/dist/version.d.ts +1 -1
  202. package/dist/version.js +1 -1
  203. package/dist/worktree/artifact-injector.d.ts +3 -2
  204. package/dist/worktree/artifact-injector.js +82 -10
  205. package/dist/worktree/dependency-warmer.d.ts +53 -1
  206. package/dist/worktree/dependency-warmer.js +619 -11
  207. package/dist/worktree/index.d.ts +2 -1
  208. package/dist/worktree/index.js +1 -0
  209. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  210. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  211. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  212. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  213. package/dist/worktree/worktree-paths.d.ts +4 -0
  214. package/dist/worktree/worktree-paths.js +30 -0
  215. package/dist/worktree/worktree.base.d.ts +3 -0
  216. package/dist/worktree/worktree.service.d.ts +16 -3
  217. package/dist/worktree/worktree.service.js +334 -54
  218. package/dist/worktree/worktree.types.d.ts +9 -1
  219. package/package.json +12 -7
  220. package/runtime/windows-job-supervisor.ps1 +722 -0
  221. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,285 @@
1
+ import path from "node:path";
2
+ import { ForgeFailureError } from "../session/session.types.js";
3
+ import { withAspNetPrebuiltRunArguments } from "./aspnet-runtime-prebuild.js";
4
+ import { hostPortEnvironment, } from "./host-runtime-contract.js";
5
+ /**
6
+ * Launches an explicitly configured HTTP project command beneath the caller's
7
+ * attempt supervisor. It never shells a command and never promotes a project
8
+ * by framework detection alone.
9
+ */
10
+ export class NodeHostRuntimeAdapter {
11
+ broker;
12
+ readinessTimeoutMs;
13
+ hardReadinessTimeoutMs;
14
+ readinessPollIntervalMs;
15
+ httpProbeTimeoutMs;
16
+ probeHttp;
17
+ constructor(broker, options = {}) {
18
+ this.broker = broker;
19
+ this.readinessTimeoutMs = boundedInteger(options.readinessTimeoutMs ?? 120_000, 100, 120_000, "readinessTimeoutMs");
20
+ this.hardReadinessTimeoutMs = boundedInteger(options.hardReadinessTimeoutMs ?? 600_000, this.readinessTimeoutMs, 900_000, "hardReadinessTimeoutMs");
21
+ this.readinessPollIntervalMs = boundedInteger(options.readinessPollIntervalMs ?? 100, 10, 5_000, "readinessPollIntervalMs");
22
+ this.httpProbeTimeoutMs = boundedInteger(options.httpProbeTimeoutMs ?? 1_000, 50, 10_000, "httpProbeTimeoutMs");
23
+ this.probeHttp = options.probeHttp ?? probeHttpReadiness;
24
+ }
25
+ async launch(owner, portConfig, command, launcher, signal) {
26
+ const framework = portConfig.adapter;
27
+ if (framework !== "node" && framework !== "vite" && framework !== "next" && framework !== "aspnetcore") {
28
+ throw new Error("node_runtime_adapter_invalid: port config must explicitly select node, vite, next, or aspnetcore.");
29
+ }
30
+ validateCommand(command);
31
+ if (!portConfig.baseUrlEnv || !portConfig.format) {
32
+ throw new Error("node_runtime_adapter_invalid: an HTTP baseUrlEnv and loopback format are required.");
33
+ }
34
+ let activeRequest = null;
35
+ const launched = await this.broker.launchWithRetry(owner, {
36
+ name: portConfig.name,
37
+ protocol: "tcp",
38
+ }, {
39
+ launch: async (port, launchSignal) => {
40
+ const request = buildNodeRuntimeLaunchRequest(framework, portConfig, command, port);
41
+ activeRequest = request;
42
+ return await launcher.launch(request, launchSignal);
43
+ },
44
+ verifyOwnership: async (value, _port, verifySignal) => {
45
+ if (!activeRequest)
46
+ throw new Error("node_runtime_adapter_state: launch request was not recorded.");
47
+ return await this.waitUntilReady(value, activeRequest, launcher, verifySignal);
48
+ },
49
+ stop: launcher.stop,
50
+ }, signal);
51
+ if (!activeRequest)
52
+ throw new Error("node_runtime_adapter_state: active launch request is missing.");
53
+ return { ...launched, request: activeRequest };
54
+ }
55
+ async waitUntilReady(value, request, launcher, signal) {
56
+ const startedAt = Date.now();
57
+ const hardDeadline = startedAt + this.hardReadinessTimeoutMs;
58
+ let idleDeadline = startedAt + this.readinessTimeoutMs;
59
+ let outputRevision = -1;
60
+ let observedOwnership = "unbound";
61
+ while (Date.now() <= idleDeadline && Date.now() <= hardDeadline) {
62
+ if (signal?.aborted)
63
+ throw cancellationError(signal);
64
+ const diagnostics = await launcher.readinessDiagnostics?.(value);
65
+ if (diagnostics?.exited) {
66
+ throw new ForgeFailureError("host_runtime_process_exited", readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics), "bridge");
67
+ }
68
+ if (diagnostics?.outputRevision !== undefined && diagnostics.outputRevision > outputRevision) {
69
+ outputRevision = diagnostics.outputRevision;
70
+ idleDeadline = Math.min(hardDeadline, Date.now() + this.readinessTimeoutMs);
71
+ }
72
+ const ownership = await launcher.inspectListener(value, request, signal);
73
+ observedOwnership = ownership;
74
+ if (ownership === "foreign")
75
+ return false;
76
+ if (ownership === "owned" && await this.probeHttp(request.baseUrl, this.httpProbeTimeoutMs, signal)) {
77
+ return true;
78
+ }
79
+ await wait(this.readinessPollIntervalMs, signal);
80
+ }
81
+ const diagnostics = await launcher.readinessDiagnostics?.(value);
82
+ if (diagnostics?.exited) {
83
+ throw new ForgeFailureError("host_runtime_process_exited", readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics), "bridge");
84
+ }
85
+ const code = observedOwnership === "owned"
86
+ ? "host_runtime_http_readiness_timeout"
87
+ : "host_runtime_listener_timeout";
88
+ const reachedHardDeadline = Date.now() >= hardDeadline;
89
+ const detail = observedOwnership === "owned"
90
+ ? reachedHardDeadline
91
+ ? `the supervised process owned its assigned loopback port but HTTP readiness did not succeed within the ${this.hardReadinessTimeoutMs}ms hard limit`
92
+ : `the supervised process owned its assigned loopback port but made no readiness progress for ${this.readinessTimeoutMs}ms`
93
+ : reachedHardDeadline
94
+ ? `the supervised process did not bind its assigned loopback port within the ${this.hardReadinessTimeoutMs}ms hard limit`
95
+ : `the supervised process did not bind its assigned loopback port and made no output progress for ${this.readinessTimeoutMs}ms`;
96
+ throw new ForgeFailureError(code, readinessFailure(code, detail, request, diagnostics), "bridge");
97
+ }
98
+ }
99
+ function readinessFailure(code, detail, request, diagnostics) {
100
+ const evidence = [
101
+ boundedDiagnostic("stderr", diagnostics?.stderrTail),
102
+ boundedDiagnostic("stdout", diagnostics?.stdoutTail),
103
+ ].filter((value) => value !== null);
104
+ return `${code}: ${detail} (${request.baseUrl}).${evidence.length > 0 ? ` ${evidence.join(" ")}` : ""}`;
105
+ }
106
+ function boundedDiagnostic(label, value) {
107
+ const normalized = value?.replaceAll(/\s+/gu, " ").trim();
108
+ if (!normalized)
109
+ return null;
110
+ return `${label} tail: ${normalized.slice(-2_000)}`;
111
+ }
112
+ export function buildNodeRuntimeLaunchRequest(framework, portConfig, command, port) {
113
+ validateCommand(command);
114
+ if (port.hostIp !== "127.0.0.1" || port.protocol !== "tcp") {
115
+ throw new Error("node_runtime_adapter_invalid: project adapters require a TCP IPv4 loopback reservation.");
116
+ }
117
+ if (!portConfig.baseUrlEnv || !portConfig.format) {
118
+ throw new Error("node_runtime_adapter_invalid: an HTTP baseUrlEnv and loopback format are required.");
119
+ }
120
+ rejectConflictingFrameworkArguments(framework, command.args);
121
+ const portEnvironment = hostPortEnvironment(portConfig, port.hostIp, port.hostPort);
122
+ const baseUrl = portEnvironment[portConfig.baseUrlEnv];
123
+ if (!baseUrl)
124
+ throw new Error("node_runtime_adapter_invalid: base URL environment could not be rendered.");
125
+ const args = framework === "aspnetcore"
126
+ ? aspNetCoreArguments(command.command, command.args)
127
+ : [...command.args, ...frameworkArguments(framework, port.hostIp, port.hostPort)];
128
+ const adapterEnvironment = framework === "aspnetcore"
129
+ ? {
130
+ ASPNETCORE_URLS: baseUrl,
131
+ Hosting__Port: String(port.hostPort),
132
+ Harness__Reconciler__Hosted: "false",
133
+ Projects__RuntimeReadiness__Hosted: "false",
134
+ Runners__ServerLocalHeartbeat__Hosted: "false",
135
+ FORGE_RUNTIME_MODE: "host",
136
+ FORGE_DISPATCH_ATTEMPT_ID: port.dispatchAttemptId,
137
+ DOTNET_CLI_USE_MSBUILD_SERVER: "0",
138
+ DOTNET_NOLOGO: "1",
139
+ }
140
+ : { HOST: port.hostIp };
141
+ const forgeEnvironment = {
142
+ FORGE_RUNTIME_PORT_NAME: portConfig.name,
143
+ FORGE_RUNTIME_PORT: String(port.hostPort),
144
+ FORGE_RUNTIME_BASE_URL: baseUrl,
145
+ };
146
+ const expansionScope = {
147
+ ...command.environment,
148
+ ...portEnvironment,
149
+ ...adapterEnvironment,
150
+ ...forgeEnvironment,
151
+ };
152
+ return {
153
+ framework,
154
+ command: command.command,
155
+ args,
156
+ cwd: path.resolve(command.cwd),
157
+ environment: {
158
+ ...expandCommandEnvironment(command.environment, expansionScope),
159
+ ...portEnvironment,
160
+ ...adapterEnvironment,
161
+ ...forgeEnvironment,
162
+ },
163
+ port,
164
+ baseUrl,
165
+ };
166
+ }
167
+ function expandCommandEnvironment(environment, scope) {
168
+ return Object.fromEntries(Object.entries(environment ?? {}).map(([name, value]) => [
169
+ name,
170
+ value.replaceAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (_match, variable) => {
171
+ const resolved = scope[variable];
172
+ if (!resolved) {
173
+ throw new Error(`node_runtime_adapter_invalid: launch environment references unavailable variable '${variable}'.`);
174
+ }
175
+ return resolved;
176
+ }),
177
+ ]));
178
+ }
179
+ function frameworkArguments(framework, hostIp, hostPort) {
180
+ if (framework === "vite")
181
+ return ["--host", hostIp, "--port", String(hostPort), "--strictPort"];
182
+ if (framework === "next")
183
+ return ["--hostname", hostIp, "--port", String(hostPort)];
184
+ return [];
185
+ }
186
+ function rejectConflictingFrameworkArguments(framework, args) {
187
+ if (framework === "aspnetcore") {
188
+ const forbidden = new Set(["--urls", "--launch-profile", "-lp", "--no-launch-profile"]);
189
+ const conflict = args.find(argument => forbidden.has(argument)
190
+ || [...forbidden].some(flag => argument.startsWith(`${flag}=`)));
191
+ if (conflict) {
192
+ throw new Error(`node_runtime_adapter_invalid: command already supplies Forge-owned argument '${conflict}'.`);
193
+ }
194
+ return;
195
+ }
196
+ if (framework === "node")
197
+ return;
198
+ const forbidden = framework === "vite"
199
+ ? new Set(["--host", "--port", "--strictPort"])
200
+ : new Set(["--hostname", "--port", "-H", "-p"]);
201
+ const conflict = args.find(argument => forbidden.has(argument) || [...forbidden].some(flag => argument.startsWith(`${flag}=`)));
202
+ if (conflict) {
203
+ throw new Error(`node_runtime_adapter_invalid: command already supplies Forge-owned argument '${conflict}'.`);
204
+ }
205
+ }
206
+ function aspNetCoreArguments(command, args) {
207
+ const executable = path.basename(command).toLowerCase();
208
+ if (executable !== "dotnet" && executable !== "dotnet.exe") {
209
+ throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires the dotnet executable.");
210
+ }
211
+ if (args[0]?.toLowerCase() !== "run") {
212
+ throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
213
+ }
214
+ const prebuiltArgs = withAspNetPrebuiltRunArguments(command, args);
215
+ const applicationArguments = prebuiltArgs.indexOf("--");
216
+ const insertionIndex = applicationArguments < 0 ? prebuiltArgs.length : applicationArguments;
217
+ return [
218
+ ...prebuiltArgs.slice(0, insertionIndex),
219
+ "--disable-build-servers",
220
+ "-p:UseSharedCompilation=false",
221
+ "-p:DefaultItemExcludesInProjectFolder=TestResults/**",
222
+ "--no-launch-profile",
223
+ ...prebuiltArgs.slice(insertionIndex),
224
+ ];
225
+ }
226
+ function validateCommand(command) {
227
+ if (!boundedText(command.command, 1024)
228
+ || !path.isAbsolute(command.cwd)
229
+ || command.args.length > 128
230
+ || command.args.some(argument => !boundedText(argument, 4096))) {
231
+ throw new Error("node_runtime_adapter_invalid: command, arguments, or absolute working directory are invalid.");
232
+ }
233
+ for (const [name, value] of Object.entries(command.environment ?? {})) {
234
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name) || !boundedText(value, 8192)) {
235
+ throw new Error("node_runtime_adapter_invalid: command environment is invalid.");
236
+ }
237
+ }
238
+ }
239
+ async function probeHttpReadiness(url, timeoutMs, signal) {
240
+ const timeout = AbortSignal.timeout(timeoutMs);
241
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
242
+ try {
243
+ const response = await fetch(url, { redirect: "manual", signal: combined });
244
+ await response.body?.cancel().catch(() => undefined);
245
+ return true;
246
+ }
247
+ catch (_error) {
248
+ if (signal?.aborted)
249
+ throw cancellationError(signal);
250
+ return false;
251
+ }
252
+ }
253
+ function wait(milliseconds, signal) {
254
+ if (signal?.aborted)
255
+ return Promise.reject(cancellationError(signal));
256
+ return new Promise((resolve, reject) => {
257
+ const timer = setTimeout(() => {
258
+ signal?.removeEventListener("abort", abort);
259
+ resolve();
260
+ }, milliseconds);
261
+ const abort = () => {
262
+ clearTimeout(timer);
263
+ reject(cancellationError(signal));
264
+ };
265
+ signal?.addEventListener("abort", abort, { once: true });
266
+ });
267
+ }
268
+ function boundedInteger(value, minimum, maximum, name) {
269
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
270
+ throw new Error(`node_runtime_adapter_invalid: ${name} must be between ${minimum} and ${maximum}.`);
271
+ }
272
+ return value;
273
+ }
274
+ function boundedText(value, maximumLength) {
275
+ return typeof value === "string"
276
+ && value.length > 0
277
+ && value.length <= maximumLength
278
+ && Array.from(value).every(character => {
279
+ const codePoint = character.codePointAt(0) ?? 0;
280
+ return codePoint > 31 && codePoint !== 127;
281
+ });
282
+ }
283
+ function cancellationError(signal) {
284
+ return new Error(String(signal?.reason ?? "Node runtime launch cancelled."));
285
+ }
@@ -0,0 +1,9 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ export type PinnedDirectoryIdentity = Pick<BigIntStats, "dev" | "ino">;
3
+ export declare class PinnedDirectoryProcessError extends Error {
4
+ readonly failureKind: "failed" | "timeout";
5
+ readonly terminationConfirmed: boolean;
6
+ constructor(message: string, failureKind: "failed" | "timeout", options?: ErrorOptions, terminationConfirmed?: boolean);
7
+ }
8
+ /** Keep filesystem stalls off the Bridge event loop. The worker pins and checks cwd. */
9
+ export declare function runPinnedDirectoryProcess(parent: string, identity: PinnedDirectoryIdentity, worker: string, operation: object, bytes?: Buffer, maxBuffer?: number, timeoutMs?: number, terminationConfirmationMs?: number): Promise<string>;
@@ -0,0 +1,61 @@
1
+ import childProcess from "node:child_process";
2
+ export class PinnedDirectoryProcessError extends Error {
3
+ failureKind;
4
+ terminationConfirmed;
5
+ constructor(message, failureKind, options, terminationConfirmed = true) {
6
+ super(`pinned_directory_operation_${failureKind}: ${message}`, options);
7
+ this.failureKind = failureKind;
8
+ this.terminationConfirmed = terminationConfirmed;
9
+ this.name = "PinnedDirectoryProcessError";
10
+ }
11
+ }
12
+ /** Keep filesystem stalls off the Bridge event loop. The worker pins and checks cwd. */
13
+ export async function runPinnedDirectoryProcess(parent, identity, worker, operation, bytes, maxBuffer = 16 * 1024, timeoutMs = 30_000, terminationConfirmationMs = 2_000) {
14
+ const environment = { ...process.env };
15
+ delete environment.NODE_OPTIONS;
16
+ const header = `${JSON.stringify({ parent, dev: identity.dev.toString(), ino: identity.ino.toString(), ...operation })}\n`;
17
+ return await new Promise((resolve, reject) => {
18
+ let settled = false;
19
+ let timedOut = false;
20
+ const deadline = { timer: undefined };
21
+ const termination = { timer: undefined };
22
+ const child = childProcess.execFile(process.execPath, ["--input-type=commonjs", "-e", worker], {
23
+ cwd: parent, env: environment, windowsHide: true, maxBuffer,
24
+ }, (error, stdout, stderr) => {
25
+ if (settled)
26
+ return;
27
+ settled = true;
28
+ if (deadline.timer)
29
+ clearTimeout(deadline.timer);
30
+ if (termination.timer)
31
+ clearTimeout(termination.timer);
32
+ if (timedOut) {
33
+ reject(new PinnedDirectoryProcessError(`operation exceeded ${timeoutMs} ms`, "timeout", error ? { cause: error } : undefined));
34
+ return;
35
+ }
36
+ if (error) {
37
+ reject(new PinnedDirectoryProcessError(stderr.trim() || error.message, "failed", { cause: error }));
38
+ }
39
+ else
40
+ resolve(stdout);
41
+ });
42
+ deadline.timer = setTimeout(() => {
43
+ if (settled)
44
+ return;
45
+ timedOut = true;
46
+ child.kill("SIGKILL");
47
+ // Wait for execFile's callback so callers know the helper has released
48
+ // its pinned cwd before they remove attempt state. A mocked or wedged
49
+ // child still gets one bounded post-kill confirmation window.
50
+ termination.timer = setTimeout(() => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ reject(new PinnedDirectoryProcessError(`operation exceeded ${timeoutMs} ms and termination was not confirmed`, "timeout", undefined, false));
55
+ }, terminationConfirmationMs);
56
+ }, timeoutMs);
57
+ child.stdin.on("error", () => undefined);
58
+ child.stdin.write(header);
59
+ child.stdin.end(bytes);
60
+ });
61
+ }
@@ -0,0 +1,17 @@
1
+ import { runPinnedDirectoryProcess, type PinnedDirectoryIdentity } from "./pinned-directory-process.js";
2
+ export interface PinnedSourceEntry {
3
+ name: string;
4
+ kind: "directory" | "file" | "symlink" | "other";
5
+ identity: PinnedDirectoryIdentity;
6
+ size: bigint;
7
+ }
8
+ type PinnedDirectoryProcessRunner = typeof runPinnedDirectoryProcess;
9
+ export interface PinnedSourceDirectoryReadOptions {
10
+ /** Production retries are Windows-only; tests may override this explicitly. */
11
+ readonly maxAttempts?: number;
12
+ readonly processRunner?: PinnedDirectoryProcessRunner;
13
+ }
14
+ /** Enumerate from a cwd whose inode is checked by the worker, not by reopening a validated pathname. */
15
+ export declare function readPinnedSourceDirectory(directory: string, identity: PinnedDirectoryIdentity, timeoutMs?: number, terminationConfirmationMs?: number, options?: PinnedSourceDirectoryReadOptions): Promise<PinnedSourceEntry[]>;
16
+ export declare function decodePinnedSourceDirectoryInventory(output: string): PinnedSourceEntry[];
17
+ export {};
@@ -0,0 +1,94 @@
1
+ import { PinnedDirectoryProcessError, runPinnedDirectoryProcess, } from "./pinned-directory-process.js";
2
+ /** Enumerate from a cwd whose inode is checked by the worker, not by reopening a validated pathname. */
3
+ export async function readPinnedSourceDirectory(directory, identity, timeoutMs = 30_000, terminationConfirmationMs = 2_000, options = {}) {
4
+ const configuredAttempts = options.maxAttempts ?? (process.platform === "win32" ? 2 : 1);
5
+ const maxAttempts = Math.max(1, Math.min(3, Math.floor(configuredAttempts), Math.floor(timeoutMs)));
6
+ const attemptBudgetMs = Math.max(1, Math.floor(timeoutMs / maxAttempts));
7
+ const attemptTerminationConfirmationMs = Math.min(Math.max(0, terminationConfirmationMs), Math.max(0, attemptBudgetMs - 1));
8
+ const attemptTimeoutMs = Math.max(1, attemptBudgetMs - attemptTerminationConfirmationMs);
9
+ const processRunner = options.processRunner ?? runPinnedDirectoryProcess;
10
+ let lastError;
11
+ let attemptsStarted = 0;
12
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
13
+ attemptsStarted = attempt;
14
+ try {
15
+ const output = await processRunner(directory, identity, worker, { kind: "source_inventory" }, undefined, 16 * 1024 * 1024, attemptTimeoutMs, attemptTerminationConfirmationMs);
16
+ return decodePinnedSourceDirectoryInventory(output);
17
+ }
18
+ catch (error) {
19
+ lastError = error;
20
+ if (!(error instanceof PinnedDirectoryProcessError)
21
+ || error.failureKind !== "timeout"
22
+ || !error.terminationConfirmed
23
+ || attempt === maxAttempts) {
24
+ break;
25
+ }
26
+ }
27
+ }
28
+ {
29
+ const error = lastError;
30
+ const detail = error instanceof Error ? error.message : String(error);
31
+ if (detail.includes("evidence inventory exceeds retention budget")) {
32
+ throw new Error("host_diagnostics_budget: evidence inventory exceeds retention budget.", { cause: error });
33
+ }
34
+ if (detail.includes("source directory identity changed") || detail.includes("invalid operation")) {
35
+ throw new Error(`host_diagnostics_source_invalid: pinned directory enumeration failed: ${detail}`, { cause: error });
36
+ }
37
+ const reason = error instanceof PinnedDirectoryProcessError && error.failureKind === "timeout"
38
+ ? `pinned directory enumeration timed out after ${attemptsStarted} attempt${attemptsStarted === 1 ? "" : "s"} within ${timeoutMs} ms recovery budget`
39
+ : "pinned directory enumeration was temporarily unavailable";
40
+ throw new Error(`host_diagnostics_temporary_unavailable: ${reason}: ${detail}`, { cause: error });
41
+ }
42
+ }
43
+ export function decodePinnedSourceDirectoryInventory(output) {
44
+ const decoded = JSON.parse(output);
45
+ if (!Array.isArray(decoded) || decoded.length > 10000) {
46
+ throw new Error("host_diagnostics_budget: evidence inventory exceeds retention budget.");
47
+ }
48
+ return decoded.map(entry => {
49
+ if (!isSingleComponent(entry.name) || !["directory", "file", "symlink", "other"].includes(entry.kind)
50
+ || !isSignedFileIdentity(entry.dev) || !isSignedFileIdentity(entry.ino) || !/^\d+$/u.test(entry.size)) {
51
+ throw new Error("host_diagnostics_source_invalid: invalid pinned directory inventory.");
52
+ }
53
+ return {
54
+ name: entry.name, kind: entry.kind, identity: { dev: BigInt(entry.dev), ino: BigInt(entry.ino) },
55
+ size: BigInt(entry.size),
56
+ };
57
+ }).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
58
+ }
59
+ function isSignedFileIdentity(value) {
60
+ // Node exposes Windows FILE_ID_128 low words through signed bigint stat fields.
61
+ // A valid NTFS identity can therefore be negative when its high bit is set.
62
+ return /^-?\d{1,64}$/u.test(value);
63
+ }
64
+ function isSingleComponent(value) {
65
+ return Boolean(value) && value !== "." && value !== ".."
66
+ && !["/", ":", "\\", "\0"].some(character => value.includes(character));
67
+ }
68
+ const worker = String.raw `
69
+ const fs = require('node:fs');
70
+ const input = fs.readFileSync(0);
71
+ const boundary = input.indexOf(10);
72
+ const job = JSON.parse(input.subarray(0, boundary).toString('utf8'));
73
+ const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
74
+ const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
75
+ const expected = { dev: BigInt(job.dev), ino: BigInt(job.ino) };
76
+ const verify = () => {
77
+ const current = fs.statSync('.', { bigint: true });
78
+ if (!current.isDirectory() || !same(current, expected) || !samePath(fs.realpathSync('.'), job.parent))
79
+ throw new Error('source directory identity changed');
80
+ };
81
+ try {
82
+ if (job.kind !== 'source_inventory') throw new Error('invalid operation');
83
+ verify();
84
+ const entries = fs.readdirSync('.');
85
+ if (entries.length > 10000) throw new Error('evidence inventory exceeds retention budget');
86
+ const inventory = entries.map(name => {
87
+ const stat = fs.lstatSync(name, { bigint: true });
88
+ const kind = stat.isSymbolicLink() ? 'symlink' : stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'other';
89
+ return { name, kind, dev: stat.dev.toString(), ino: stat.ino.toString(), size: stat.size.toString() };
90
+ });
91
+ verify();
92
+ process.stdout.write(JSON.stringify(inventory));
93
+ } catch (error) { process.stderr.write(error.message); process.exitCode = 1; }
94
+ `;
@@ -0,0 +1,21 @@
1
+ import type { HostRuntimePlaywrightConfig } from "./host-runtime-contract.js";
2
+ export interface PlaywrightHostRuntimeContext {
3
+ readonly worktreePath: string;
4
+ readonly temporaryDirectory: string;
5
+ readonly testOutputDirectory: string;
6
+ readonly browserProfileDirectory: string;
7
+ readonly baseUrl: string;
8
+ readonly ownershipToken: string;
9
+ }
10
+ export interface PreparedPlaywrightHostRuntime {
11
+ readonly configPath: string;
12
+ readonly environment: Readonly<Record<string, string>>;
13
+ }
14
+ /**
15
+ * Prepares a finite Playwright test boundary for a managed-host attempt.
16
+ * The generated overlay points tests at Forge's verified listener, disables
17
+ * Playwright-managed web servers, and keeps writable artifacts attempt-local.
18
+ */
19
+ export declare class PlaywrightHostRuntimeAdapter {
20
+ prepare(config: HostRuntimePlaywrightConfig, context: PlaywrightHostRuntimeContext): Promise<PreparedPlaywrightHostRuntime>;
21
+ }