@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 0.1.3-dev.ba293ccf876b

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 (189) 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 +98 -3
  4. package/dist/bridge/bridge.service.js +1425 -45
  5. package/dist/bridge/bridge.types.d.ts +205 -1
  6. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  7. package/dist/bridge/readiness-result-outbox.js +54 -0
  8. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  9. package/dist/bridge/readiness-result-worker.js +100 -0
  10. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  11. package/dist/bridge/runner-host-capabilities.js +93 -0
  12. package/dist/cleanup/cleanup.service.d.ts +14 -2
  13. package/dist/cleanup/cleanup.service.js +496 -3
  14. package/dist/cleanup/cleanup.types.d.ts +10 -0
  15. package/dist/cleanup/index.d.ts +1 -1
  16. package/dist/cleanup/index.js +1 -1
  17. package/dist/cleanup/internals.d.ts +2 -2
  18. package/dist/cleanup/internals.js +1 -1
  19. package/dist/cli/cli.help.js +47 -1
  20. package/dist/cli/cli.service.d.ts +45 -2
  21. package/dist/cli/cli.service.js +377 -27
  22. package/dist/cli/local-config.d.ts +5 -0
  23. package/dist/cli/local-config.js +45 -1
  24. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  25. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  26. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  27. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  28. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  29. package/dist/runtime/attempt-path-operation.js +94 -0
  30. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  31. package/dist/runtime/attempt-private-directory.js +54 -0
  32. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  33. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  34. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  35. package/dist/runtime/attempt-process-supervisor.js +386 -0
  36. package/dist/runtime/canonical-source-file.d.ts +3 -0
  37. package/dist/runtime/canonical-source-file.js +54 -0
  38. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  39. package/dist/runtime/docker-engine-probe.js +300 -0
  40. package/dist/runtime/generated-output-reset.d.ts +10 -0
  41. package/dist/runtime/generated-output-reset.js +103 -0
  42. package/dist/runtime/host-diagnostics.d.ts +4 -0
  43. package/dist/runtime/host-diagnostics.js +95 -0
  44. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  45. package/dist/runtime/host-listener-inspector.js +349 -0
  46. package/dist/runtime/host-port-broker.d.ts +76 -0
  47. package/dist/runtime/host-port-broker.js +333 -0
  48. package/dist/runtime/host-runtime-contract.d.ts +56 -0
  49. package/dist/runtime/host-runtime-contract.js +709 -0
  50. package/dist/runtime/host-runtime-lease.d.ts +201 -0
  51. package/dist/runtime/host-runtime-lease.js +2314 -0
  52. package/dist/runtime/host-runtime-promotion.d.ts +32 -0
  53. package/dist/runtime/host-runtime-promotion.js +206 -0
  54. package/dist/runtime/memory-capacity.d.ts +68 -0
  55. package/dist/runtime/memory-capacity.js +121 -0
  56. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  57. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  58. package/dist/runtime/pinned-directory-process.d.ts +4 -0
  59. package/dist/runtime/pinned-directory-process.js +20 -0
  60. package/dist/runtime/pinned-source-directory.d.ts +9 -0
  61. package/dist/runtime/pinned-source-directory.js +56 -0
  62. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  63. package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
  64. package/dist/runtime/prepared-runtime-identity.d.ts +3 -0
  65. package/dist/runtime/prepared-runtime-identity.js +191 -0
  66. package/dist/runtime/prepared-runtime-readiness.d.ts +22 -0
  67. package/dist/runtime/prepared-runtime-readiness.js +191 -0
  68. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  69. package/dist/runtime/runtime-isolation-lease.js +158 -0
  70. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  71. package/dist/runtime/workspace-resource-estimator.js +174 -0
  72. package/dist/sandbox/command-runner.d.ts +4 -0
  73. package/dist/sandbox/command-runner.js +47 -0
  74. package/dist/sandbox/contract.d.ts +5 -0
  75. package/dist/sandbox/contract.js +276 -0
  76. package/dist/sandbox/event-budget.d.ts +11 -0
  77. package/dist/sandbox/event-budget.js +27 -0
  78. package/dist/sandbox/image-policy.d.ts +35 -0
  79. package/dist/sandbox/image-policy.js +247 -0
  80. package/dist/sandbox/index.d.ts +5 -0
  81. package/dist/sandbox/index.js +5 -0
  82. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  83. package/dist/sandbox/lease-watchdog.js +67 -0
  84. package/dist/sandbox/preflight.d.ts +84 -0
  85. package/dist/sandbox/preflight.js +521 -0
  86. package/dist/sandbox/process.d.ts +20 -0
  87. package/dist/sandbox/process.js +209 -0
  88. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  89. package/dist/sandbox/provider-auth/codex.js +212 -0
  90. package/dist/sandbox/repository-lock.d.ts +5 -0
  91. package/dist/sandbox/repository-lock.js +121 -0
  92. package/dist/sandbox/runtime.d.ts +297 -0
  93. package/dist/sandbox/runtime.js +2338 -0
  94. package/dist/sandbox/sandbox.types.d.ts +93 -0
  95. package/dist/sandbox/sandbox.types.js +1 -0
  96. package/dist/sandbox/smoke.d.ts +76 -0
  97. package/dist/sandbox/smoke.js +380 -0
  98. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  99. package/dist/sandbox/worker-supervisor.js +36 -0
  100. package/dist/sandbox/worker.d.ts +1 -0
  101. package/dist/sandbox/worker.js +225 -0
  102. package/dist/sandbox/workspace.service.d.ts +13 -0
  103. package/dist/sandbox/workspace.service.js +72 -0
  104. package/dist/session/managed-attempt-tools.d.ts +21 -0
  105. package/dist/session/managed-attempt-tools.js +912 -0
  106. package/dist/session/managed-build-coordinator.d.ts +5 -0
  107. package/dist/session/managed-build-coordinator.js +105 -0
  108. package/dist/session/managed-build-receipt.d.ts +21 -0
  109. package/dist/session/managed-build-receipt.js +335 -0
  110. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  111. package/dist/session/phase-completion.artifacts.js +256 -26
  112. package/dist/session/phase-completion.d.ts +12 -2
  113. package/dist/session/phase-completion.helpers.d.ts +5 -0
  114. package/dist/session/phase-completion.helpers.js +51 -10
  115. package/dist/session/phase-completion.js +14 -3
  116. package/dist/session/phase-completion.prompts.d.ts +3 -3
  117. package/dist/session/phase-completion.prompts.js +24 -8
  118. package/dist/session/phase-completion.runtime.d.ts +13 -1
  119. package/dist/session/phase-completion.runtime.js +189 -8
  120. package/dist/session/phase-completion.schemas.js +117 -4
  121. package/dist/session/phase-completion.validation.d.ts +4 -1
  122. package/dist/session/phase-completion.validation.js +153 -22
  123. package/dist/session/pre-publication-validation.d.ts +47 -0
  124. package/dist/session/pre-publication-validation.js +249 -0
  125. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  126. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  127. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  128. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  129. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  130. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  131. package/dist/session/provider-cli/provider-cli.deterministic.js +68 -9
  132. package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
  133. package/dist/session/provider-cli/provider-cli.process.js +474 -23
  134. package/dist/session/provider-cli/provider-cli.prompts.d.ts +14 -2
  135. package/dist/session/provider-cli/provider-cli.prompts.js +147 -25
  136. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  137. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  138. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  139. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  140. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  141. package/dist/session/provider-cli/provider-cli.service.d.ts +25 -2
  142. package/dist/session/provider-cli/provider-cli.service.js +636 -84
  143. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  144. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  145. package/dist/session/provider-cli/provider-output-state.js +91 -16
  146. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  147. package/dist/session/provider-cli/provider-usage.js +28 -5
  148. package/dist/session/session-redaction.d.ts +3 -0
  149. package/dist/session/session-redaction.js +38 -0
  150. package/dist/session/session.base.d.ts +6 -1
  151. package/dist/session/session.service.d.ts +8 -2
  152. package/dist/session/session.service.js +531 -42
  153. package/dist/session/session.types.d.ts +115 -1
  154. package/dist/session/session.types.js +194 -3
  155. package/dist/shared/format/format-bytes.d.ts +1 -0
  156. package/dist/shared/format/format-bytes.js +9 -0
  157. package/dist/shared/git/git.js +2 -1
  158. package/dist/shared/path/path.d.ts +1 -0
  159. package/dist/shared/path/path.js +21 -1
  160. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  161. package/dist/shared/process/dotnet-toolchain.js +179 -0
  162. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  163. package/dist/shared/process/host-process-recovery.js +78 -0
  164. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  165. package/dist/shared/process/host-process-supervisor.js +330 -0
  166. package/dist/shared/process/process.d.ts +2 -0
  167. package/dist/shared/process/process.js +11 -6
  168. package/dist/shared/provider-capabilities.d.ts +1 -0
  169. package/dist/shared/provider-capabilities.js +3 -0
  170. package/dist/shared/redaction/redaction.js +12 -2
  171. package/dist/version.d.ts +1 -1
  172. package/dist/version.js +1 -1
  173. package/dist/worktree/artifact-injector.d.ts +3 -2
  174. package/dist/worktree/artifact-injector.js +43 -4
  175. package/dist/worktree/dependency-warmer.d.ts +28 -1
  176. package/dist/worktree/dependency-warmer.js +308 -9
  177. package/dist/worktree/index.d.ts +2 -1
  178. package/dist/worktree/index.js +1 -0
  179. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  180. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  181. package/dist/worktree/worktree-paths.d.ts +4 -0
  182. package/dist/worktree/worktree-paths.js +30 -0
  183. package/dist/worktree/worktree.base.d.ts +3 -0
  184. package/dist/worktree/worktree.service.d.ts +14 -2
  185. package/dist/worktree/worktree.service.js +296 -47
  186. package/dist/worktree/worktree.types.d.ts +5 -1
  187. package/package.json +8 -4
  188. package/runtime/windows-job-supervisor.ps1 +444 -0
  189. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,284 @@
1
+ import path from "node:path";
2
+ import { withAspNetPrebuiltRunArguments } from "./aspnet-runtime-prebuild.js";
3
+ import { hostPortEnvironment, } from "./host-runtime-contract.js";
4
+ /**
5
+ * Launches an explicitly configured HTTP project command beneath the caller's
6
+ * attempt supervisor. It never shells a command and never promotes a project
7
+ * by framework detection alone.
8
+ */
9
+ export class NodeHostRuntimeAdapter {
10
+ broker;
11
+ readinessTimeoutMs;
12
+ hardReadinessTimeoutMs;
13
+ readinessPollIntervalMs;
14
+ httpProbeTimeoutMs;
15
+ probeHttp;
16
+ constructor(broker, options = {}) {
17
+ this.broker = broker;
18
+ this.readinessTimeoutMs = boundedInteger(options.readinessTimeoutMs ?? 120_000, 100, 120_000, "readinessTimeoutMs");
19
+ this.hardReadinessTimeoutMs = boundedInteger(options.hardReadinessTimeoutMs ?? 600_000, this.readinessTimeoutMs, 900_000, "hardReadinessTimeoutMs");
20
+ this.readinessPollIntervalMs = boundedInteger(options.readinessPollIntervalMs ?? 100, 10, 5_000, "readinessPollIntervalMs");
21
+ this.httpProbeTimeoutMs = boundedInteger(options.httpProbeTimeoutMs ?? 1_000, 50, 10_000, "httpProbeTimeoutMs");
22
+ this.probeHttp = options.probeHttp ?? probeHttpReadiness;
23
+ }
24
+ async launch(owner, portConfig, command, launcher, signal) {
25
+ const framework = portConfig.adapter;
26
+ if (framework !== "node" && framework !== "vite" && framework !== "next" && framework !== "aspnetcore") {
27
+ throw new Error("node_runtime_adapter_invalid: port config must explicitly select node, vite, next, or aspnetcore.");
28
+ }
29
+ validateCommand(command);
30
+ if (!portConfig.baseUrlEnv || !portConfig.format) {
31
+ throw new Error("node_runtime_adapter_invalid: an HTTP baseUrlEnv and loopback format are required.");
32
+ }
33
+ let activeRequest = null;
34
+ const launched = await this.broker.launchWithRetry(owner, {
35
+ name: portConfig.name,
36
+ protocol: "tcp",
37
+ }, {
38
+ launch: async (port, launchSignal) => {
39
+ const request = buildNodeRuntimeLaunchRequest(framework, portConfig, command, port);
40
+ activeRequest = request;
41
+ return await launcher.launch(request, launchSignal);
42
+ },
43
+ verifyOwnership: async (value, _port, verifySignal) => {
44
+ if (!activeRequest)
45
+ throw new Error("node_runtime_adapter_state: launch request was not recorded.");
46
+ return await this.waitUntilReady(value, activeRequest, launcher, verifySignal);
47
+ },
48
+ stop: launcher.stop,
49
+ }, signal);
50
+ if (!activeRequest)
51
+ throw new Error("node_runtime_adapter_state: active launch request is missing.");
52
+ return { ...launched, request: activeRequest };
53
+ }
54
+ async waitUntilReady(value, request, launcher, signal) {
55
+ const startedAt = Date.now();
56
+ const hardDeadline = startedAt + this.hardReadinessTimeoutMs;
57
+ let idleDeadline = startedAt + this.readinessTimeoutMs;
58
+ let outputRevision = -1;
59
+ let observedOwnership = "unbound";
60
+ while (Date.now() <= idleDeadline && Date.now() <= hardDeadline) {
61
+ if (signal?.aborted)
62
+ throw cancellationError(signal);
63
+ const diagnostics = await launcher.readinessDiagnostics?.(value);
64
+ if (diagnostics?.exited) {
65
+ throw new Error(readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics));
66
+ }
67
+ if (diagnostics?.outputRevision !== undefined && diagnostics.outputRevision > outputRevision) {
68
+ outputRevision = diagnostics.outputRevision;
69
+ idleDeadline = Math.min(hardDeadline, Date.now() + this.readinessTimeoutMs);
70
+ }
71
+ const ownership = await launcher.inspectListener(value, request, signal);
72
+ observedOwnership = ownership;
73
+ if (ownership === "foreign")
74
+ return false;
75
+ if (ownership === "owned" && await this.probeHttp(request.baseUrl, this.httpProbeTimeoutMs, signal)) {
76
+ return true;
77
+ }
78
+ await wait(this.readinessPollIntervalMs, signal);
79
+ }
80
+ const diagnostics = await launcher.readinessDiagnostics?.(value);
81
+ if (diagnostics?.exited) {
82
+ throw new Error(readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics));
83
+ }
84
+ const code = observedOwnership === "owned"
85
+ ? "host_runtime_http_readiness_timeout"
86
+ : "host_runtime_listener_timeout";
87
+ const reachedHardDeadline = Date.now() >= hardDeadline;
88
+ const detail = observedOwnership === "owned"
89
+ ? reachedHardDeadline
90
+ ? `the supervised process owned its assigned loopback port but HTTP readiness did not succeed within the ${this.hardReadinessTimeoutMs}ms hard limit`
91
+ : `the supervised process owned its assigned loopback port but made no readiness progress for ${this.readinessTimeoutMs}ms`
92
+ : reachedHardDeadline
93
+ ? `the supervised process did not bind its assigned loopback port within the ${this.hardReadinessTimeoutMs}ms hard limit`
94
+ : `the supervised process did not bind its assigned loopback port and made no output progress for ${this.readinessTimeoutMs}ms`;
95
+ throw new Error(readinessFailure(code, detail, request, diagnostics));
96
+ }
97
+ }
98
+ function readinessFailure(code, detail, request, diagnostics) {
99
+ const evidence = [
100
+ boundedDiagnostic("stderr", diagnostics?.stderrTail),
101
+ boundedDiagnostic("stdout", diagnostics?.stdoutTail),
102
+ ].filter((value) => value !== null);
103
+ return `${code}: ${detail} (${request.baseUrl}).${evidence.length > 0 ? ` ${evidence.join(" ")}` : ""}`;
104
+ }
105
+ function boundedDiagnostic(label, value) {
106
+ const normalized = value?.replaceAll(/\s+/gu, " ").trim();
107
+ if (!normalized)
108
+ return null;
109
+ return `${label} tail: ${normalized.slice(-2_000)}`;
110
+ }
111
+ export function buildNodeRuntimeLaunchRequest(framework, portConfig, command, port) {
112
+ validateCommand(command);
113
+ if (port.hostIp !== "127.0.0.1" || port.protocol !== "tcp") {
114
+ throw new Error("node_runtime_adapter_invalid: project adapters require a TCP IPv4 loopback reservation.");
115
+ }
116
+ if (!portConfig.baseUrlEnv || !portConfig.format) {
117
+ throw new Error("node_runtime_adapter_invalid: an HTTP baseUrlEnv and loopback format are required.");
118
+ }
119
+ rejectConflictingFrameworkArguments(framework, command.args);
120
+ const portEnvironment = hostPortEnvironment(portConfig, port.hostIp, port.hostPort);
121
+ const baseUrl = portEnvironment[portConfig.baseUrlEnv];
122
+ if (!baseUrl)
123
+ throw new Error("node_runtime_adapter_invalid: base URL environment could not be rendered.");
124
+ const args = framework === "aspnetcore"
125
+ ? aspNetCoreArguments(command.command, command.args)
126
+ : [...command.args, ...frameworkArguments(framework, port.hostIp, port.hostPort)];
127
+ const adapterEnvironment = framework === "aspnetcore"
128
+ ? {
129
+ ASPNETCORE_URLS: baseUrl,
130
+ Hosting__Port: String(port.hostPort),
131
+ Harness__Reconciler__Hosted: "false",
132
+ Projects__RuntimeReadiness__Hosted: "false",
133
+ Runners__ServerLocalHeartbeat__Hosted: "false",
134
+ FORGE_RUNTIME_MODE: "host",
135
+ FORGE_DISPATCH_ATTEMPT_ID: port.dispatchAttemptId,
136
+ DOTNET_CLI_USE_MSBUILD_SERVER: "0",
137
+ DOTNET_NOLOGO: "1",
138
+ }
139
+ : { HOST: port.hostIp };
140
+ const forgeEnvironment = {
141
+ FORGE_RUNTIME_PORT_NAME: portConfig.name,
142
+ FORGE_RUNTIME_PORT: String(port.hostPort),
143
+ FORGE_RUNTIME_BASE_URL: baseUrl,
144
+ };
145
+ const expansionScope = {
146
+ ...command.environment,
147
+ ...portEnvironment,
148
+ ...adapterEnvironment,
149
+ ...forgeEnvironment,
150
+ };
151
+ return {
152
+ framework,
153
+ command: command.command,
154
+ args,
155
+ cwd: path.resolve(command.cwd),
156
+ environment: {
157
+ ...expandCommandEnvironment(command.environment, expansionScope),
158
+ ...portEnvironment,
159
+ ...adapterEnvironment,
160
+ ...forgeEnvironment,
161
+ },
162
+ port,
163
+ baseUrl,
164
+ };
165
+ }
166
+ function expandCommandEnvironment(environment, scope) {
167
+ return Object.fromEntries(Object.entries(environment ?? {}).map(([name, value]) => [
168
+ name,
169
+ value.replaceAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (_match, variable) => {
170
+ const resolved = scope[variable];
171
+ if (!resolved) {
172
+ throw new Error(`node_runtime_adapter_invalid: launch environment references unavailable variable '${variable}'.`);
173
+ }
174
+ return resolved;
175
+ }),
176
+ ]));
177
+ }
178
+ function frameworkArguments(framework, hostIp, hostPort) {
179
+ if (framework === "vite")
180
+ return ["--host", hostIp, "--port", String(hostPort), "--strictPort"];
181
+ if (framework === "next")
182
+ return ["--hostname", hostIp, "--port", String(hostPort)];
183
+ return [];
184
+ }
185
+ function rejectConflictingFrameworkArguments(framework, args) {
186
+ if (framework === "aspnetcore") {
187
+ const forbidden = new Set(["--urls", "--launch-profile", "-lp", "--no-launch-profile"]);
188
+ const conflict = args.find(argument => forbidden.has(argument)
189
+ || [...forbidden].some(flag => argument.startsWith(`${flag}=`)));
190
+ if (conflict) {
191
+ throw new Error(`node_runtime_adapter_invalid: command already supplies Forge-owned argument '${conflict}'.`);
192
+ }
193
+ return;
194
+ }
195
+ if (framework === "node")
196
+ return;
197
+ const forbidden = framework === "vite"
198
+ ? new Set(["--host", "--port", "--strictPort"])
199
+ : new Set(["--hostname", "--port", "-H", "-p"]);
200
+ const conflict = args.find(argument => forbidden.has(argument) || [...forbidden].some(flag => argument.startsWith(`${flag}=`)));
201
+ if (conflict) {
202
+ throw new Error(`node_runtime_adapter_invalid: command already supplies Forge-owned argument '${conflict}'.`);
203
+ }
204
+ }
205
+ function aspNetCoreArguments(command, args) {
206
+ const executable = path.basename(command).toLowerCase();
207
+ if (executable !== "dotnet" && executable !== "dotnet.exe") {
208
+ throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires the dotnet executable.");
209
+ }
210
+ if (args[0]?.toLowerCase() !== "run") {
211
+ throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
212
+ }
213
+ const prebuiltArgs = withAspNetPrebuiltRunArguments(command, args);
214
+ const applicationArguments = prebuiltArgs.indexOf("--");
215
+ const insertionIndex = applicationArguments < 0 ? prebuiltArgs.length : applicationArguments;
216
+ return [
217
+ ...prebuiltArgs.slice(0, insertionIndex),
218
+ "--disable-build-servers",
219
+ "-p:UseSharedCompilation=false",
220
+ "-p:DefaultItemExcludesInProjectFolder=TestResults/**",
221
+ "--no-launch-profile",
222
+ ...prebuiltArgs.slice(insertionIndex),
223
+ ];
224
+ }
225
+ function validateCommand(command) {
226
+ if (!boundedText(command.command, 1024)
227
+ || !path.isAbsolute(command.cwd)
228
+ || command.args.length > 128
229
+ || command.args.some(argument => !boundedText(argument, 4096))) {
230
+ throw new Error("node_runtime_adapter_invalid: command, arguments, or absolute working directory are invalid.");
231
+ }
232
+ for (const [name, value] of Object.entries(command.environment ?? {})) {
233
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name) || !boundedText(value, 8192)) {
234
+ throw new Error("node_runtime_adapter_invalid: command environment is invalid.");
235
+ }
236
+ }
237
+ }
238
+ async function probeHttpReadiness(url, timeoutMs, signal) {
239
+ const timeout = AbortSignal.timeout(timeoutMs);
240
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
241
+ try {
242
+ const response = await fetch(url, { redirect: "manual", signal: combined });
243
+ await response.body?.cancel().catch(() => undefined);
244
+ return true;
245
+ }
246
+ catch (_error) {
247
+ if (signal?.aborted)
248
+ throw cancellationError(signal);
249
+ return false;
250
+ }
251
+ }
252
+ function wait(milliseconds, signal) {
253
+ if (signal?.aborted)
254
+ return Promise.reject(cancellationError(signal));
255
+ return new Promise((resolve, reject) => {
256
+ const timer = setTimeout(() => {
257
+ signal?.removeEventListener("abort", abort);
258
+ resolve();
259
+ }, milliseconds);
260
+ const abort = () => {
261
+ clearTimeout(timer);
262
+ reject(cancellationError(signal));
263
+ };
264
+ signal?.addEventListener("abort", abort, { once: true });
265
+ });
266
+ }
267
+ function boundedInteger(value, minimum, maximum, name) {
268
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
269
+ throw new Error(`node_runtime_adapter_invalid: ${name} must be between ${minimum} and ${maximum}.`);
270
+ }
271
+ return value;
272
+ }
273
+ function boundedText(value, maximumLength) {
274
+ return typeof value === "string"
275
+ && value.length > 0
276
+ && value.length <= maximumLength
277
+ && Array.from(value).every(character => {
278
+ const codePoint = character.codePointAt(0) ?? 0;
279
+ return codePoint > 31 && codePoint !== 127;
280
+ });
281
+ }
282
+ function cancellationError(signal) {
283
+ return new Error(String(signal?.reason ?? "Node runtime launch cancelled."));
284
+ }
@@ -0,0 +1,4 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ export type PinnedDirectoryIdentity = Pick<BigIntStats, "dev" | "ino">;
3
+ /** Keep filesystem stalls off the Bridge event loop. The worker pins and checks cwd. */
4
+ export declare function runPinnedDirectoryProcess(parent: string, identity: PinnedDirectoryIdentity, worker: string, operation: object, bytes?: Buffer, maxBuffer?: number): Promise<string>;
@@ -0,0 +1,20 @@
1
+ import childProcess from "node:child_process";
2
+ /** Keep filesystem stalls off the Bridge event loop. The worker pins and checks cwd. */
3
+ export async function runPinnedDirectoryProcess(parent, identity, worker, operation, bytes, maxBuffer = 16 * 1024) {
4
+ const environment = { ...process.env };
5
+ delete environment.NODE_OPTIONS;
6
+ const header = `${JSON.stringify({ parent, dev: identity.dev.toString(), ino: identity.ino.toString(), ...operation })}\n`;
7
+ return await new Promise((resolve, reject) => {
8
+ const child = childProcess.execFile(process.execPath, ["--input-type=commonjs", "-e", worker], {
9
+ cwd: parent, env: environment, windowsHide: true, timeout: 10_000, maxBuffer,
10
+ }, (error, stdout, stderr) => {
11
+ if (error)
12
+ reject(new Error(`pinned_directory_operation_failed: ${stderr.trim() || error.message}`));
13
+ else
14
+ resolve(stdout);
15
+ });
16
+ child.stdin.on("error", () => undefined);
17
+ child.stdin.write(header);
18
+ child.stdin.end(bytes);
19
+ });
20
+ }
@@ -0,0 +1,9 @@
1
+ import { 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
+ /** Enumerate from a cwd whose inode is checked by the worker, not by reopening a validated pathname. */
9
+ export declare function readPinnedSourceDirectory(directory: string, identity: PinnedDirectoryIdentity): Promise<PinnedSourceEntry[]>;
@@ -0,0 +1,56 @@
1
+ import { 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) {
4
+ let output;
5
+ try {
6
+ output = await runPinnedDirectoryProcess(directory, identity, worker, { kind: "source_inventory" }, undefined, 16 * 1024 * 1024);
7
+ }
8
+ catch (error) {
9
+ throw new Error(`host_diagnostics_source_invalid: pinned directory enumeration failed: ${String(error)}`, { cause: error });
10
+ }
11
+ const decoded = JSON.parse(output);
12
+ if (!Array.isArray(decoded) || decoded.length > 10000) {
13
+ throw new Error("host_diagnostics_budget: evidence inventory exceeds retention budget.");
14
+ }
15
+ return decoded.map(entry => {
16
+ if (!isSingleComponent(entry.name) || !["directory", "file", "symlink", "other"].includes(entry.kind)
17
+ || !/^\d+$/u.test(entry.dev) || !/^\d+$/u.test(entry.ino) || !/^\d+$/u.test(entry.size)) {
18
+ throw new Error("host_diagnostics_source_invalid: invalid pinned directory inventory.");
19
+ }
20
+ return {
21
+ name: entry.name, kind: entry.kind, identity: { dev: BigInt(entry.dev), ino: BigInt(entry.ino) },
22
+ size: BigInt(entry.size),
23
+ };
24
+ });
25
+ }
26
+ function isSingleComponent(value) {
27
+ return Boolean(value) && value !== "." && value !== ".."
28
+ && !["/", ":", "\\", "\0"].some(character => value.includes(character));
29
+ }
30
+ const worker = String.raw `
31
+ const fs = require('node:fs');
32
+ const input = fs.readFileSync(0);
33
+ const boundary = input.indexOf(10);
34
+ const job = JSON.parse(input.subarray(0, boundary).toString('utf8'));
35
+ const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
36
+ const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
37
+ const expected = { dev: BigInt(job.dev), ino: BigInt(job.ino) };
38
+ const verify = () => {
39
+ const current = fs.statSync('.', { bigint: true });
40
+ if (!current.isDirectory() || !same(current, expected) || !samePath(fs.realpathSync('.'), job.parent))
41
+ throw new Error('source directory identity changed');
42
+ };
43
+ try {
44
+ if (job.kind !== 'source_inventory') throw new Error('invalid operation');
45
+ verify();
46
+ const entries = fs.readdirSync('.');
47
+ if (entries.length > 10000) throw new Error('evidence inventory exceeds retention budget');
48
+ const inventory = entries.map(name => {
49
+ const stat = fs.lstatSync(name, { bigint: true });
50
+ const kind = stat.isSymbolicLink() ? 'symlink' : stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'other';
51
+ return { name, kind, dev: stat.dev.toString(), ino: stat.ino.toString(), size: stat.size.toString() };
52
+ });
53
+ verify();
54
+ process.stdout.write(JSON.stringify(inventory));
55
+ } catch (error) { process.stderr.write(error.message); process.exitCode = 1; }
56
+ `;
@@ -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
+ }
@@ -0,0 +1,268 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { assertCanonicalDirectory, createPrivateCanonicalDirectory } from "./attempt-private-directory.js";
6
+ import { readCanonicalSourceFile } from "./canonical-source-file.js";
7
+ import { runAttemptPathOperation } from "./attempt-path-operation.js";
8
+ const maximumConfigBytes = 1024 * 1024;
9
+ /**
10
+ * Prepares a finite Playwright test boundary for a managed-host attempt.
11
+ * The generated overlay points tests at Forge's verified listener, disables
12
+ * Playwright-managed web servers, and keeps writable artifacts attempt-local.
13
+ */
14
+ export class PlaywrightHostRuntimeAdapter {
15
+ async prepare(config, context) {
16
+ const worktreePath = path.resolve(context.worktreePath);
17
+ const sourceConfig = path.resolve(worktreePath, config.config);
18
+ const [realWorktree, realSource] = await Promise.all([
19
+ fs.realpath(worktreePath),
20
+ fs.realpath(sourceConfig),
21
+ ]);
22
+ assertContained(realWorktree, realSource, "project Playwright config");
23
+ const sourceBytes = await readCanonicalSourceFile(sourceConfig, maximumConfigBytes);
24
+ if (sourceBytes.length < 1) {
25
+ throw new Error(`playwright_runtime_adapter_invalid: project config must be a regular 1-${maximumConfigBytes} byte file.`);
26
+ }
27
+ const sourceConfigText = sourceBytes.toString("utf8");
28
+ const projectNames = [...sourceConfigText.matchAll(/\bname\s*:\s*["']([^"']+)["']/gu)]
29
+ .map(match => match[1])
30
+ .filter((name, index, names) => names.indexOf(name) === index);
31
+ await ensureAttemptWritableDirectories([
32
+ context.temporaryDirectory,
33
+ context.testOutputDirectory,
34
+ context.browserProfileDirectory,
35
+ ], realWorktree);
36
+ const temporaryDirectory = await realContainedDirectory(context.temporaryDirectory, realWorktree, "temporary directory");
37
+ const testOutputDirectory = await realContainedDirectory(context.testOutputDirectory, realWorktree, "test output directory");
38
+ const browserProfileDirectory = await realContainedDirectory(context.browserProfileDirectory, realWorktree, "browser profile directory");
39
+ const attemptRoot = path.dirname(temporaryDirectory);
40
+ if (path.dirname(testOutputDirectory) !== attemptRoot || path.dirname(browserProfileDirectory) !== attemptRoot) {
41
+ throw new Error("playwright_runtime_adapter_invalid: writable directories must belong to the same attempt root.");
42
+ }
43
+ const baseUrl = loopbackHttpUrl(context.baseUrl);
44
+ if (!/^[0-9a-f]{32}$/u.test(context.ownershipToken)) {
45
+ throw new Error("playwright_runtime_adapter_invalid: ownership token must be a normalized dispatch attempt UUID.");
46
+ }
47
+ const overlayPath = path.join(temporaryDirectory, "forge.playwright.config.mjs");
48
+ const browserCache = await resolvePlaywrightBrowserCache(attemptRoot);
49
+ const browserInstallCommand = await resolvePlaywrightBrowserInstallCommand(context.worktreePath, path.dirname(realSource));
50
+ await writeAttemptOverlay(overlayPath, renderOverlay({
51
+ projectConfigUrl: pathToFileURL(realSource).href,
52
+ projectConfigDirectory: path.dirname(realSource),
53
+ testOutputDirectory,
54
+ baseUrl,
55
+ workers: config.workers,
56
+ projectNames,
57
+ }));
58
+ return {
59
+ configPath: overlayPath,
60
+ environment: {
61
+ FORGE_PLAYWRIGHT_CONFIG: overlayPath,
62
+ FORGE_PLAYWRIGHT_BASE_URL: baseUrl,
63
+ FORGE_E2E_WEB_URL: baseUrl,
64
+ FORGE_E2E_OWNERSHIP_MODE: "attempt",
65
+ FORGE_E2E_OWNERSHIP_TOKEN: context.ownershipToken,
66
+ FORGE_PLAYWRIGHT_BROWSER_READY: browserCache.ready ? "true" : "false",
67
+ FORGE_PLAYWRIGHT_BROWSER_INSTALL_COMMAND: browserInstallCommand,
68
+ PLAYWRIGHT_BROWSERS_PATH: browserCache.path,
69
+ PLAYWRIGHT_HTML_OUTPUT_DIR: path.join(testOutputDirectory, "html-report"),
70
+ PLAYWRIGHT_BLOB_OUTPUT_DIR: path.join(testOutputDirectory, "blob-report"),
71
+ PLAYWRIGHT_JUNIT_OUTPUT_NAME: path.join(testOutputDirectory, "results.xml"),
72
+ PLAYWRIGHT_JSON_OUTPUT_NAME: path.join(testOutputDirectory, "results.json"),
73
+ FORGE_BROWSER_PROFILE_DIR: browserProfileDirectory,
74
+ FORGE_TEST_OUTPUT_DIR: testOutputDirectory,
75
+ },
76
+ };
77
+ }
78
+ }
79
+ async function ensureAttemptWritableDirectories(targets, realWorktree) {
80
+ const resolvedTargets = targets.map(target => path.resolve(target));
81
+ const attemptRoots = new Set(resolvedTargets.map(target => path.dirname(target)));
82
+ if (attemptRoots.size !== 1) {
83
+ throw new Error("playwright_runtime_adapter_invalid: writable directories must belong to the same attempt root.");
84
+ }
85
+ const attemptRoot = [...attemptRoots][0];
86
+ if (!attemptRoot || attemptRoot === realWorktree || isContained(realWorktree, attemptRoot)) {
87
+ throw new Error("playwright_runtime_adapter_invalid: writable directories must be attempt-private and outside the worktree.");
88
+ }
89
+ await assertCanonicalDirectory(attemptRoot, true);
90
+ for (const target of resolvedTargets) {
91
+ if (target === realWorktree || isContained(realWorktree, target)) {
92
+ throw new Error("playwright_runtime_adapter_invalid: writable directories must be attempt-private and outside the worktree.");
93
+ }
94
+ await assertCanonicalDirectory(target, true);
95
+ }
96
+ await createPrivateCanonicalDirectory(attemptRoot);
97
+ for (const target of resolvedTargets)
98
+ await createPrivateCanonicalDirectory(target);
99
+ }
100
+ async function writeAttemptOverlay(target, content) {
101
+ const parent = path.dirname(target);
102
+ const identity = await assertCanonicalDirectory(parent);
103
+ if (!identity)
104
+ throw new Error("playwright_runtime_adapter_invalid: overlay parent is missing.");
105
+ await runAttemptPathOperation(parent, identity, { kind: "overlay", name: path.basename(target), content });
106
+ }
107
+ async function resolvePlaywrightBrowserInstallCommand(worktreePath, configDirectory) {
108
+ const relativeDirectory = path.relative(worktreePath, configDirectory).replaceAll(path.sep, "/") || ".";
109
+ if (await isFile(path.join(configDirectory, "pnpm-lock.yaml"))) {
110
+ return `pnpm --dir ${quoteCommandArgument(relativeDirectory)} exec playwright install chromium`;
111
+ }
112
+ if (await isFile(path.join(configDirectory, "yarn.lock"))) {
113
+ return `yarn --cwd ${quoteCommandArgument(relativeDirectory)} playwright install chromium`;
114
+ }
115
+ return `npm --prefix ${quoteCommandArgument(relativeDirectory)} exec playwright install chromium`;
116
+ }
117
+ async function isFile(candidate) {
118
+ try {
119
+ return (await fs.stat(candidate)).isFile();
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ function quoteCommandArgument(value) {
126
+ return /[\s"']/u.test(value) ? JSON.stringify(value) : value;
127
+ }
128
+ async function resolvePlaywrightBrowserCache(attemptRoot) {
129
+ const candidates = [
130
+ process.env.PLAYWRIGHT_BROWSERS_PATH,
131
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "ms-playwright") : null,
132
+ path.join(os.homedir(), ".cache", "ms-playwright"),
133
+ ].filter((candidate) => Boolean(candidate?.trim()));
134
+ for (const candidate of [...new Set(candidates.map(candidate => path.resolve(candidate)))]) {
135
+ if (await hasChromiumExecutable(candidate)) {
136
+ return { path: candidate, ready: true };
137
+ }
138
+ }
139
+ const attemptCache = path.join(attemptRoot, "cache", "ms-playwright");
140
+ await createPrivateCanonicalDirectory(attemptCache);
141
+ return { path: attemptCache, ready: false };
142
+ }
143
+ async function hasChromiumExecutable(cachePath) {
144
+ let entries;
145
+ try {
146
+ entries = await fs.readdir(cachePath, { withFileTypes: true });
147
+ }
148
+ catch {
149
+ return false;
150
+ }
151
+ const executableSuffixes = process.platform === "win32"
152
+ ? [path.join("chrome-win64", "chrome.exe"), path.join("chrome-win", "chrome.exe")]
153
+ : process.platform === "darwin"
154
+ ? [path.join("chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium")]
155
+ : [path.join("chrome-linux", "chrome"), path.join("chrome-linux64", "chrome")];
156
+ for (const entry of entries) {
157
+ if (!entry.isDirectory() || !entry.name.startsWith("chromium-"))
158
+ continue;
159
+ for (const suffix of executableSuffixes) {
160
+ try {
161
+ const stat = await fs.stat(path.join(cachePath, entry.name, suffix));
162
+ if (stat.isFile())
163
+ return true;
164
+ }
165
+ catch {
166
+ // Continue through known Playwright layouts.
167
+ }
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+ function renderOverlay(values) {
173
+ return [
174
+ "// Generated by forge-bridge for one fenced host attempt.",
175
+ `// forge-playwright-projects: ${JSON.stringify(values.projectNames)}`,
176
+ "import path from \"node:path\";",
177
+ `import projectConfig from ${JSON.stringify(values.projectConfigUrl)};`,
178
+ `const projectConfigDirectory = ${JSON.stringify(values.projectConfigDirectory)};`,
179
+ "const resolveConfigPath = value => typeof value === \"string\" && !path.isAbsolute(value)",
180
+ " ? path.resolve(projectConfigDirectory, value)",
181
+ " : value;",
182
+ "const resolveModulePath = value => typeof value === \"string\" && value.startsWith(\".\")",
183
+ " ? path.resolve(projectConfigDirectory, value)",
184
+ " : value;",
185
+ "const resolveUse = use => ({",
186
+ " ...(use ?? {}),",
187
+ " storageState: typeof use?.storageState === \"string\" ? resolveConfigPath(use.storageState) : use?.storageState,",
188
+ " baseURL: " + JSON.stringify(values.baseUrl) + ",",
189
+ "});",
190
+ "const forgeUse = resolveUse(projectConfig.use);",
191
+ "const forgeProjects = Array.isArray(projectConfig.projects)",
192
+ " ? projectConfig.projects.map(project => ({",
193
+ " ...project,",
194
+ " testDir: resolveConfigPath(project.testDir),",
195
+ " snapshotDir: resolveConfigPath(project.snapshotDir),",
196
+ " use: resolveUse(project.use),",
197
+ " }))",
198
+ " : projectConfig.projects;",
199
+ "const forgeReporter = Array.isArray(projectConfig.reporter)",
200
+ " ? projectConfig.reporter.map(reporter => Array.isArray(reporter)",
201
+ " ? [resolveModulePath(reporter[0]), ...reporter.slice(1)]",
202
+ " : resolveModulePath(reporter))",
203
+ " : resolveModulePath(projectConfig.reporter);",
204
+ "const forgeProviderOnlySpecs = (() => {",
205
+ " try {",
206
+ " const parsed = JSON.parse(process.env.FORGE_CI_REPAIR_PROVIDER_ONLY_SPECS ?? '[]');",
207
+ " return Array.isArray(parsed) ? parsed.filter(value => typeof value === 'string' && value.length > 0) : [];",
208
+ " } catch {",
209
+ " return [];",
210
+ " }",
211
+ "})();",
212
+ "const forgeTestIgnore = forgeProviderOnlySpecs.length === 0",
213
+ " ? projectConfig.testIgnore",
214
+ " : [...(Array.isArray(projectConfig.testIgnore) ? projectConfig.testIgnore : projectConfig.testIgnore ? [projectConfig.testIgnore] : []), ...forgeProviderOnlySpecs];",
215
+ "export default {",
216
+ " ...projectConfig,",
217
+ " testDir: resolveConfigPath(projectConfig.testDir),",
218
+ " snapshotDir: resolveConfigPath(projectConfig.snapshotDir),",
219
+ " globalSetup: resolveModulePath(projectConfig.globalSetup),",
220
+ " globalTeardown: resolveModulePath(projectConfig.globalTeardown),",
221
+ " reporter: forgeReporter,",
222
+ " testIgnore: forgeTestIgnore,",
223
+ ` outputDir: ${JSON.stringify(values.testOutputDirectory)},`,
224
+ ` workers: ${values.workers},`,
225
+ " webServer: undefined,",
226
+ " use: forgeUse,",
227
+ " projects: forgeProjects,",
228
+ "};",
229
+ "",
230
+ ].join("\n");
231
+ }
232
+ async function realContainedDirectory(target, realWorktree, label) {
233
+ await assertCanonicalDirectory(target);
234
+ const realTarget = await fs.realpath(path.resolve(target));
235
+ if (realTarget === realWorktree || isContained(realWorktree, realTarget)) {
236
+ throw new Error(`playwright_runtime_adapter_invalid: ${label} must be attempt-private and outside the worktree.`);
237
+ }
238
+ const stat = await fs.stat(realTarget);
239
+ if (!stat.isDirectory())
240
+ throw new Error(`playwright_runtime_adapter_invalid: ${label} must be a directory.`);
241
+ return realTarget;
242
+ }
243
+ function assertContained(parent, child, label) {
244
+ if (!isContained(parent, child)) {
245
+ throw new Error(`playwright_runtime_adapter_invalid: ${label} must stay inside the prepared worktree.`);
246
+ }
247
+ }
248
+ function isContained(parent, child) {
249
+ const relative = path.relative(parent, child);
250
+ return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
251
+ }
252
+ function loopbackHttpUrl(value) {
253
+ let url;
254
+ try {
255
+ url = new URL(value);
256
+ }
257
+ catch {
258
+ throw new Error("playwright_runtime_adapter_invalid: base URL must be an HTTP loopback URL.");
259
+ }
260
+ if ((url.protocol !== "http:" && url.protocol !== "https:")
261
+ || url.hostname !== "127.0.0.1"
262
+ || !url.port
263
+ || url.username
264
+ || url.password) {
265
+ throw new Error("playwright_runtime_adapter_invalid: base URL must be an HTTP loopback URL.");
266
+ }
267
+ return url.href.replace(/\/$/u, "");
268
+ }