@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,813 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { isAlias, parseDocument, visit } from "yaml";
5
+ import { readCanonicalSourceFile } from "./canonical-source-file.js";
6
+ import { isGeneratedOutputDirectoryName } from "./generated-output-reset.js";
7
+ export const hostRuntimeContractPath = ".forge/forge.runtime.yml";
8
+ export const hostRuntimeTestPostgresAdminEnvironment = "FORGE_TEST_POSTGRES_ADMIN_CONNECTION_STRING";
9
+ export const hostRuntimeLegacyTestPostgresEnvironment = "FORGE_TEST_POSTGRES_CONNECTION_STRING";
10
+ const maximumContractBytes = 64 * 1024;
11
+ const maximumDepth = 12;
12
+ // Every discovery scan stops after this many directory entries. A monorepo needs a few
13
+ // thousand; the bound only keeps a pathological tree from stalling attempt preparation.
14
+ const maximumDiscoveryEntries = 20_000;
15
+ /**
16
+ * A discovery scan reached its entry budget before seeing the whole tree, so what it did not
17
+ * find may still exist. Discovery must not treat that as absence: a partial runtime (for
18
+ * example a web app without its API) would send the attempt's traffic to the wrong server.
19
+ */
20
+ class IncompleteRuntimeDiscoveryError extends Error {
21
+ }
22
+ // Dependency, build and editor folders never hold an application entrypoint, and counting
23
+ // them would exhaust the budget and serialize a repository for no reason.
24
+ const ignoredDiscoveryDirectories = new Set([
25
+ ".forge",
26
+ ".forge-designer",
27
+ ".git",
28
+ ".gradle",
29
+ ".idea",
30
+ ".local",
31
+ ".next",
32
+ ".pnpm",
33
+ ".turbo",
34
+ ".venv",
35
+ ".vs",
36
+ ".vscode",
37
+ ".yarn",
38
+ "bin",
39
+ "build",
40
+ "coverage",
41
+ "dist",
42
+ "node_modules",
43
+ "obj",
44
+ "target",
45
+ "vendor",
46
+ "venv",
47
+ ]);
48
+ const topLevelKeys = new Set(["schemaVersion", "runtime"]);
49
+ const runtimeKeys = new Set(["hostConcurrency", "ports", "playwright", "browserProfile", "singletonResources"]);
50
+ const portKeys = new Set(["adapter", "launch", "env", "baseUrlEnv", "format"]);
51
+ const launchKeys = new Set(["command", "args", "workingDirectory", "environment"]);
52
+ const playwrightKeys = new Set(["config", "baseUrlPort", "workers"]);
53
+ export async function loadHostRuntimeContract(worktreePath, snapshot, directorySnapshot) {
54
+ worktreePath = await fs.realpath(worktreePath);
55
+ const target = path.join(path.resolve(worktreePath), hostRuntimeContractPath);
56
+ let source;
57
+ try {
58
+ const captured = snapshot ? await snapshot(target) : await readCanonicalSourceFile(target, maximumContractBytes);
59
+ if (captured === null)
60
+ return await discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot);
61
+ source = captured;
62
+ }
63
+ catch (error) {
64
+ if (isFileNotFound(error))
65
+ return await discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot);
66
+ throw error;
67
+ }
68
+ return {
69
+ contract: validateHostRuntimeContract(source.toString("utf8")),
70
+ source: "project",
71
+ };
72
+ }
73
+ async function discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot) {
74
+ const root = path.resolve(worktreePath);
75
+ let nodeDiscovery;
76
+ let aspNetDiscovery;
77
+ try {
78
+ [nodeDiscovery, aspNetDiscovery] = await Promise.all([
79
+ discoverNodeRuntimes(root, snapshot, directorySnapshot),
80
+ discoverAspNetRuntimes(root, snapshot, directorySnapshot),
81
+ ]);
82
+ }
83
+ catch (error) {
84
+ if (error instanceof IncompleteRuntimeDiscoveryError)
85
+ return incompleteRuntimeDiscovery();
86
+ throw error;
87
+ }
88
+ if (nodeDiscovery.ambiguous || aspNetDiscovery.ambiguous) {
89
+ return ambiguousRuntimeDiscovery();
90
+ }
91
+ const candidates = [...nodeDiscovery.candidates, ...aspNetDiscovery.candidates];
92
+ if (candidates.length === 1) {
93
+ const candidate = candidates[0];
94
+ return {
95
+ contract: candidate.contract,
96
+ source: "derived",
97
+ discovery: {
98
+ status: "derived",
99
+ reasonCode: candidate.reasonCode,
100
+ summary: candidate.summary,
101
+ candidateFingerprint: candidate.candidateFingerprint,
102
+ },
103
+ };
104
+ }
105
+ if (candidates.length === 2
106
+ && candidates.some(candidate => candidate.kind === "browser")
107
+ && candidates.some(candidate => candidate.kind === "api")
108
+ && candidates.find(candidate => candidate.kind === "browser")?.contract.runtime.playwright !== null) {
109
+ const browser = candidates.find(candidate => candidate.kind === "browser");
110
+ const api = candidates.find(candidate => candidate.kind === "api");
111
+ const combined = combineBrowserAndApiRuntime(browser, api);
112
+ return {
113
+ contract: combined.contract,
114
+ source: "derived",
115
+ discovery: {
116
+ status: "derived",
117
+ reasonCode: combined.reasonCode,
118
+ summary: combined.summary,
119
+ candidateFingerprint: combined.candidateFingerprint,
120
+ },
121
+ };
122
+ }
123
+ if (candidates.length > 1) {
124
+ return ambiguousRuntimeDiscovery();
125
+ }
126
+ return {
127
+ contract: defaultHostRuntimeContract(),
128
+ source: "default",
129
+ discovery: {
130
+ status: "unsupported",
131
+ reasonCode: "runtime_discovery_unsupported",
132
+ summary: "Forge did not find one deterministic supported application entrypoint; host application runs remain serialized.",
133
+ },
134
+ };
135
+ }
136
+ async function discoverNodeRuntimes(root, snapshot, directorySnapshot) {
137
+ const manifests = [...new Set([
138
+ path.join(root, "package.json"),
139
+ ...await findFiles(root, file => path.basename(file).toLowerCase() === "package.json", 4, maximumDiscoveryEntries, directorySnapshot),
140
+ ])];
141
+ const candidates = (await Promise.all(manifests.map(async (manifestPath) => await discoverNodeRuntime(root, manifestPath, snapshot, directorySnapshot)))).filter((candidate) => candidate !== null);
142
+ return { candidates: candidates.length === 1 ? candidates : [], ambiguous: candidates.length > 1 };
143
+ }
144
+ async function discoverNodeRuntime(root, manifestPath, snapshot, directorySnapshot) {
145
+ const source = await readBoundedOptionalFile(manifestPath, 256 * 1024, snapshot);
146
+ if (source === null)
147
+ return null;
148
+ let manifest;
149
+ try {
150
+ manifest = JSON.parse(source);
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ const scripts = objectOrEmpty(manifest.scripts);
156
+ const dependencies = { ...objectOrEmpty(manifest.dependencies), ...objectOrEmpty(manifest.devDependencies) };
157
+ const dev = typeof scripts.dev === "string" ? scripts.dev.trim() : "";
158
+ const start = typeof scripts.start === "string" ? scripts.start.trim() : "";
159
+ const scriptName = dev ? "dev" : start ? "start" : null;
160
+ const script = scriptName ? String(scripts[scriptName]).trim() : "";
161
+ if (!scriptName || !script || script.length > 1024 || /(?:&&|\|\||;|\n|\r)/u.test(script))
162
+ return null;
163
+ const vite = Object.hasOwn(dependencies, "vite") || /(^|\s)vite(?:\s|$)/u.test(script);
164
+ const next = Object.hasOwn(dependencies, "next") || /(^|\s)next(?:\s|$)/u.test(script);
165
+ if (vite === next)
166
+ return null;
167
+ const packageManager = await nearestPackageManager(root, path.dirname(manifestPath), snapshot);
168
+ if (!packageManager)
169
+ return null;
170
+ const adapter = vite ? "vite" : "next";
171
+ const playwrightConfigs = [...new Set((await Promise.all([
172
+ ...["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"]
173
+ .flatMap(file => [file, `e2e/${file}`, `tests/e2e/${file}`])
174
+ .map(async (candidate) => await optionalFileExists(path.join(root, candidate), snapshot) ? path.join(root, candidate) : null),
175
+ findFiles(root, file => /^playwright\.config\.(?:ts|js|mjs|cjs)$/iu.test(path.basename(file)), 4, maximumDiscoveryEntries, directorySnapshot),
176
+ ])).flat().filter((value) => value !== null))]
177
+ .map(config => portableRelativePath(root, config));
178
+ const playwright = playwrightConfigs.length === 1
179
+ ? { config: playwrightConfigs[0], baseUrlPort: "web", workers: 1 }
180
+ : null;
181
+ const workingDirectory = portableRelativePath(root, path.dirname(manifestPath)) || ".";
182
+ const contract = {
183
+ schemaVersion: 1,
184
+ runtime: {
185
+ hostConcurrency: "parallel",
186
+ ports: [{
187
+ name: "web",
188
+ adapter,
189
+ launch: {
190
+ command: packageManager,
191
+ args: packageManager === "npm" ? ["run", scriptName, "--"] : ["run", scriptName],
192
+ workingDirectory,
193
+ },
194
+ env: "PORT",
195
+ baseUrlEnv: "APP_BASE_URL",
196
+ format: "http://{host}:{port}",
197
+ }],
198
+ playwright,
199
+ browserProfile: "isolated",
200
+ singletonResources: [],
201
+ },
202
+ };
203
+ return {
204
+ kind: "browser",
205
+ contract,
206
+ reasonCode: `runtime_discovery_${adapter}`,
207
+ summary: `Forge derived one ${adapter === "vite" ? "Vite" : "Next.js"} application entrypoint from package.json.`,
208
+ candidateFingerprint: detectorFingerprint({
209
+ manifest: portableRelativePath(root, manifestPath),
210
+ source,
211
+ packageManager,
212
+ scriptName,
213
+ script,
214
+ adapter,
215
+ playwrightConfigs,
216
+ }),
217
+ };
218
+ }
219
+ async function discoverAspNetRuntimes(root, snapshot, directorySnapshot) {
220
+ // Deep enough for nested solutions (src/<area>/<Project>/<Project>.csproj); the entry budget,
221
+ // not the depth, bounds the scan, and a scan that exceeds it is reported as incomplete.
222
+ const projects = await findFiles(root, file => file.toLowerCase().endsWith(".csproj"), 6, maximumDiscoveryEntries, directorySnapshot);
223
+ const webProjects = [];
224
+ for (const projectPath of projects) {
225
+ const source = await readBoundedOptionalFile(projectPath, 512 * 1024, snapshot);
226
+ if (source && /<Project\s+Sdk=["']Microsoft\.NET\.Sdk\.Web["']/iu.test(source)) {
227
+ webProjects.push({ relativePath: path.relative(root, projectPath).replaceAll("\\", "/"), source });
228
+ }
229
+ }
230
+ if (webProjects.length !== 1)
231
+ return { candidates: [], ambiguous: webProjects.length > 1 };
232
+ const project = webProjects[0];
233
+ const launchProfile = await discoverAspNetLaunchProfile(root, project.relativePath, snapshot);
234
+ const contract = {
235
+ schemaVersion: 1,
236
+ runtime: {
237
+ hostConcurrency: "parallel",
238
+ ports: [{
239
+ name: "web",
240
+ adapter: "aspnetcore",
241
+ launch: {
242
+ command: "dotnet",
243
+ args: ["run", "--project", project.relativePath],
244
+ workingDirectory: ".",
245
+ ...(Object.keys(launchProfile.environment).length > 0
246
+ ? { environment: launchProfile.environment }
247
+ : {}),
248
+ },
249
+ env: "ASPNETCORE_URLS",
250
+ baseUrlEnv: "APP_BASE_URL",
251
+ format: "http://{host}:{port}",
252
+ }],
253
+ playwright: null,
254
+ browserProfile: "isolated",
255
+ singletonResources: [],
256
+ },
257
+ };
258
+ return { candidates: [{
259
+ kind: "api",
260
+ contract,
261
+ reasonCode: "runtime_discovery_aspnetcore",
262
+ summary: "Forge derived one ASP.NET Core application entrypoint from the repository project metadata.",
263
+ candidateFingerprint: detectorFingerprint({
264
+ project: project.relativePath,
265
+ source: project.source,
266
+ launchSettings: launchProfile.source,
267
+ launchProfile: launchProfile.name,
268
+ }),
269
+ }], ambiguous: false };
270
+ }
271
+ function combineBrowserAndApiRuntime(browser, api) {
272
+ const browserPort = browser.contract.runtime.ports.find(port => port.launch !== null);
273
+ const apiPort = api.contract.runtime.ports.find(port => port.launch !== null);
274
+ const combinedBrowserPort = {
275
+ ...browserPort,
276
+ name: "web",
277
+ env: "WEB_PORT",
278
+ baseUrlEnv: "WEB_BASE_URL",
279
+ launch: browserPort.launch ? {
280
+ ...browserPort.launch,
281
+ environment: {
282
+ ...browserPort.launch.environment,
283
+ FORGE_API_PROXY_TARGET: "${FORGE_API_URL}",
284
+ FORGE_DEV_PUBLIC_ORIGIN: "${FORGE_RUNTIME_BASE_URL}",
285
+ },
286
+ } : null,
287
+ };
288
+ const combinedApiPort = {
289
+ ...apiPort,
290
+ name: "api",
291
+ env: "ASPNETCORE_URLS",
292
+ baseUrlEnv: "API_BASE_URL",
293
+ };
294
+ return {
295
+ kind: "browser",
296
+ contract: {
297
+ schemaVersion: 1,
298
+ runtime: {
299
+ hostConcurrency: "parallel",
300
+ ports: [combinedApiPort, combinedBrowserPort],
301
+ playwright: browser.contract.runtime.playwright
302
+ ? { ...browser.contract.runtime.playwright, baseUrlPort: "web" }
303
+ : null,
304
+ browserProfile: "isolated",
305
+ singletonResources: [],
306
+ },
307
+ },
308
+ reasonCode: "runtime_discovery_full_stack",
309
+ summary: "Forge derived one supervised API and browser application pair from repository metadata.",
310
+ candidateFingerprint: detectorFingerprint({
311
+ browser: browser.candidateFingerprint,
312
+ api: api.candidateFingerprint,
313
+ }),
314
+ };
315
+ }
316
+ function incompleteRuntimeDiscovery() {
317
+ return {
318
+ contract: defaultHostRuntimeContract(),
319
+ source: "default",
320
+ discovery: {
321
+ status: "ambiguous",
322
+ reasonCode: "runtime_discovery_incomplete",
323
+ summary: "Forge could not scan the whole repository within its discovery budget, so host application runs remain serialized.",
324
+ },
325
+ };
326
+ }
327
+ function ambiguousRuntimeDiscovery() {
328
+ return {
329
+ contract: defaultHostRuntimeContract(),
330
+ source: "default",
331
+ discovery: {
332
+ status: "ambiguous",
333
+ reasonCode: "runtime_discovery_multiple_applications",
334
+ summary: "Forge found more than one supported application entrypoint, so host application runs remain serialized.",
335
+ },
336
+ };
337
+ }
338
+ async function nearestPackageManager(root, start, snapshot) {
339
+ let directory = path.resolve(start);
340
+ const resolvedRoot = path.resolve(root);
341
+ while (directory === resolvedRoot || directory.startsWith(`${resolvedRoot}${path.sep}`)) {
342
+ const lockfiles = (await Promise.all([
343
+ optionalFileExists(path.join(directory, "pnpm-lock.yaml"), snapshot).then(exists => exists ? "pnpm" : null),
344
+ optionalFileExists(path.join(directory, "yarn.lock"), snapshot).then(exists => exists ? "yarn" : null),
345
+ optionalFileExists(path.join(directory, "package-lock.json"), snapshot).then(exists => exists ? "npm" : null),
346
+ optionalFileExists(path.join(directory, "npm-shrinkwrap.json"), snapshot).then(exists => exists ? "npm" : null),
347
+ ])).filter((value) => value !== null);
348
+ const packageManagers = [...new Set(lockfiles)];
349
+ if (packageManagers.length > 1)
350
+ return null;
351
+ if (packageManagers.length === 1)
352
+ return packageManagers[0];
353
+ if (directory === resolvedRoot)
354
+ break;
355
+ directory = path.dirname(directory);
356
+ }
357
+ return "npm";
358
+ }
359
+ function portableRelativePath(root, target) {
360
+ return path.relative(root, target).replaceAll("\\", "/");
361
+ }
362
+ async function discoverAspNetLaunchProfile(root, projectRelativePath, snapshot) {
363
+ const target = path.join(root, path.dirname(projectRelativePath), "Properties", "launchSettings.json");
364
+ const source = await readBoundedOptionalFile(target, 512 * 1024, snapshot);
365
+ if (!source)
366
+ return { name: null, source: null, environment: {} };
367
+ try {
368
+ const document = objectOrEmpty(JSON.parse(source));
369
+ const profiles = objectOrEmpty(document.profiles);
370
+ const candidates = Object.entries(profiles)
371
+ .map(([name, value]) => ({ name, profile: objectOrEmpty(value) }))
372
+ .filter(candidate => String(candidate.profile.commandName ?? "").toLowerCase() === "project")
373
+ .sort((left, right) => aspNetProfilePriority(left.name) - aspNetProfilePriority(right.name)
374
+ || left.name.localeCompare(right.name));
375
+ const selected = candidates[0];
376
+ if (!selected)
377
+ return { name: null, source, environment: {} };
378
+ const environmentVariables = objectOrEmpty(selected.profile.environmentVariables);
379
+ const environment = Object.fromEntries(Object.entries(environmentVariables)
380
+ .filter((entry) => safeAspNetLaunchEnvironment(entry[0], entry[1]))
381
+ .sort(([left], [right]) => left.localeCompare(right))
382
+ .slice(0, 64));
383
+ return { name: selected.name, source, environment };
384
+ }
385
+ catch {
386
+ return { name: null, source, environment: {} };
387
+ }
388
+ }
389
+ // A project's "forge-attempt" profile configures its API for managed attempts. The
390
+ // attempt's pre-publication checks then run against the same settings the project's own
391
+ // e2e suite uses, instead of the developer's "http" profile.
392
+ const attemptLaunchProfileName = "forge-attempt";
393
+ function aspNetProfilePriority(name) {
394
+ if (name.toLowerCase() === attemptLaunchProfileName)
395
+ return -1;
396
+ if (name.toLowerCase() === "http")
397
+ return 0;
398
+ if (name.toLowerCase() === "https")
399
+ return 1;
400
+ return 2;
401
+ }
402
+ function safeAspNetLaunchEnvironment(name, value) {
403
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name)
404
+ || typeof value !== "string"
405
+ || value.length === 0
406
+ || value.length > 8192)
407
+ return false;
408
+ const normalized = name.toUpperCase();
409
+ return !normalized.startsWith("FORGE_")
410
+ && !isUnsafeDotnetEnvironmentName(name)
411
+ && !new Set([
412
+ "ASPNETCORE_URLS",
413
+ "ASPNETCORE_HTTP_PORTS",
414
+ "ASPNETCORE_HTTPS_PORTS",
415
+ "DOTNET_URLS",
416
+ "HOST",
417
+ "PORT",
418
+ "PATH",
419
+ "HOME",
420
+ "TMP",
421
+ "TEMP",
422
+ "TMPDIR",
423
+ ]).has(normalized);
424
+ }
425
+ async function findFiles(root, accepts, maximumDepth, maximumEntries, snapshot) {
426
+ const results = [];
427
+ let examined = 0;
428
+ const directories = [{ directory: root, depth: 0 }];
429
+ while (directories.length > 0) {
430
+ const current = directories.shift();
431
+ if (current.depth > maximumDepth)
432
+ continue;
433
+ const entries = snapshot
434
+ ? await snapshot(current.directory)
435
+ : await fs.readdir(current.directory, { withFileTypes: true }).then(found => found.map(entry => ({
436
+ name: entry.name,
437
+ isDirectory: entry.isDirectory(),
438
+ isFile: entry.isFile(),
439
+ }))).catch((error) => {
440
+ // A directory removed since it was listed holds nothing; one that cannot be read
441
+ // (permissions, open-file limits) may hold the entrypoint discovery is looking for.
442
+ if (isFileNotFound(error))
443
+ return [];
444
+ throw new IncompleteRuntimeDiscoveryError();
445
+ });
446
+ const orderedEntries = [...entries].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
447
+ for (const entry of orderedEntries) {
448
+ if (entry.isDirectory
449
+ && (ignoredDiscoveryDirectories.has(entry.name.toLowerCase()) || isGeneratedOutputDirectoryName(entry.name)))
450
+ continue;
451
+ if (examined++ >= maximumEntries)
452
+ throw new IncompleteRuntimeDiscoveryError();
453
+ const target = path.join(current.directory, entry.name);
454
+ if (entry.isDirectory)
455
+ directories.push({ directory: target, depth: current.depth + 1 });
456
+ else if (entry.isFile && accepts(target))
457
+ results.push(target);
458
+ }
459
+ }
460
+ return results.sort((left, right) => left.localeCompare(right));
461
+ }
462
+ async function readBoundedOptionalFile(target, maximumBytes, snapshot) {
463
+ if (snapshot) {
464
+ const source = await snapshot(target);
465
+ return source && source.length > 0 && source.length <= maximumBytes ? source.toString("utf8") : null;
466
+ }
467
+ try {
468
+ const stat = await fs.stat(target);
469
+ if (!stat.isFile() || stat.size < 1 || stat.size > maximumBytes)
470
+ return null;
471
+ return (await readCanonicalSourceFile(target, maximumBytes)).toString("utf8");
472
+ }
473
+ catch (error) {
474
+ if (isFileNotFound(error))
475
+ return null;
476
+ throw error;
477
+ }
478
+ }
479
+ async function optionalFileExists(target, snapshot) {
480
+ if (snapshot)
481
+ return await snapshot(target) !== null;
482
+ try {
483
+ return (await fs.stat(target)).isFile();
484
+ }
485
+ catch (error) {
486
+ if (isFileNotFound(error))
487
+ return false;
488
+ throw error;
489
+ }
490
+ }
491
+ function objectOrEmpty(value) {
492
+ return value && typeof value === "object" && !Array.isArray(value)
493
+ ? value
494
+ : {};
495
+ }
496
+ function detectorFingerprint(value) {
497
+ return `sha256:${createHash("sha256").update(JSON.stringify({ adapterVersion: 2, value })).digest("hex")}`;
498
+ }
499
+ export function defaultHostRuntimeContract() {
500
+ return {
501
+ schemaVersion: 1,
502
+ runtime: {
503
+ hostConcurrency: "serial",
504
+ ports: [],
505
+ playwright: null,
506
+ browserProfile: "isolated",
507
+ singletonResources: [],
508
+ },
509
+ };
510
+ }
511
+ export function validateHostRuntimeContract(source) {
512
+ const byteLength = Buffer.byteLength(source, "utf8");
513
+ if (byteLength === 0 || byteLength > maximumContractBytes) {
514
+ throw new Error(`runtime_contract_invalid: forge.runtime.yml must be 1-${maximumContractBytes} bytes.`);
515
+ }
516
+ if (Array.from(source).some(character => {
517
+ const codePoint = character.codePointAt(0) ?? 0;
518
+ return codePoint === 127 || (codePoint < 32 && codePoint !== 9 && codePoint !== 10 && codePoint !== 13);
519
+ })) {
520
+ throw new Error("runtime_contract_invalid: control characters are not allowed.");
521
+ }
522
+ const document = parseDocument(source, { uniqueKeys: true, prettyErrors: false, strict: true });
523
+ if (document.errors.length > 0) {
524
+ throw new Error(`runtime_contract_invalid: ${document.errors[0]?.message ?? "invalid YAML"}`);
525
+ }
526
+ visit(document, {
527
+ Node(_key, node) {
528
+ if (isAlias(node) || node.anchor) {
529
+ throw new Error("runtime_contract_invalid: YAML aliases and anchors are not allowed.");
530
+ }
531
+ },
532
+ });
533
+ const parsed = document.toJS({ maxAliasCount: 0 });
534
+ assertDepth(parsed, 0);
535
+ const root = object(parsed, "contract");
536
+ rejectUnknownKeys(root, topLevelKeys, "contract");
537
+ if (root.schemaVersion !== 1)
538
+ throw new Error("runtime_contract_invalid: schemaVersion must be 1.");
539
+ const runtime = object(root.runtime, "runtime");
540
+ rejectUnknownKeys(runtime, runtimeKeys, "runtime");
541
+ const hostConcurrency = runtime.hostConcurrency ?? "serial";
542
+ if (hostConcurrency !== "serial" && hostConcurrency !== "parallel" && hostConcurrency !== "disabled") {
543
+ throw new Error("runtime_contract_invalid: runtime.hostConcurrency must be serial, parallel, or disabled.");
544
+ }
545
+ const browserProfile = runtime.browserProfile ?? "isolated";
546
+ if (browserProfile !== "isolated") {
547
+ throw new Error("runtime_contract_invalid: runtime.browserProfile must be isolated.");
548
+ }
549
+ const ports = parsePorts(runtime.ports);
550
+ return {
551
+ schemaVersion: 1,
552
+ runtime: {
553
+ hostConcurrency,
554
+ ports,
555
+ playwright: parsePlaywright(runtime.playwright, ports),
556
+ browserProfile,
557
+ singletonResources: parseSingletonResources(runtime.singletonResources),
558
+ },
559
+ };
560
+ }
561
+ function parsePlaywright(value, ports) {
562
+ if (value === undefined || value === null)
563
+ return null;
564
+ const playwright = object(value, "runtime.playwright");
565
+ rejectUnknownKeys(playwright, playwrightKeys, "runtime.playwright");
566
+ const config = containedPortablePath(playwright.config, "runtime.playwright.config");
567
+ const baseUrlPort = requiredString(playwright.baseUrlPort, "runtime.playwright.baseUrlPort");
568
+ const port = ports.find(entry => entry.name === baseUrlPort);
569
+ if (!port?.launch || !port.baseUrlEnv || !port.format) {
570
+ throw new Error("runtime_contract_invalid: runtime.playwright.baseUrlPort must reference a launched HTTP port.");
571
+ }
572
+ const workers = playwright.workers === undefined || playwright.workers === null ? 1 : playwright.workers;
573
+ if (!Number.isSafeInteger(workers) || workers < 1 || workers > 16) {
574
+ throw new Error("runtime_contract_invalid: runtime.playwright.workers must be an integer between 1 and 16.");
575
+ }
576
+ return { config, baseUrlPort, workers: workers };
577
+ }
578
+ export function hostPortEnvironment(config, hostIp, hostPort) {
579
+ if (!Number.isSafeInteger(hostPort) || hostPort < 1 || hostPort > 65535) {
580
+ throw new Error("runtime_contract_invalid: assigned host port must be valid.");
581
+ }
582
+ const baseUrl = config.format
583
+ ?.replaceAll("{host}", hostIp)
584
+ .replaceAll("{port}", String(hostPort));
585
+ const environment = {
586
+ [config.env]: config.adapter === "aspnetcore" && baseUrl ? baseUrl : String(hostPort),
587
+ };
588
+ if (config.baseUrlEnv && config.format) {
589
+ environment[config.baseUrlEnv] = baseUrl;
590
+ }
591
+ return environment;
592
+ }
593
+ function parsePorts(value) {
594
+ if (value === undefined || value === null)
595
+ return [];
596
+ const ports = object(value, "runtime.ports");
597
+ if (Object.keys(ports).length > 16) {
598
+ throw new Error("runtime_contract_invalid: runtime.ports cannot contain more than 16 entries.");
599
+ }
600
+ const environmentNames = new Set();
601
+ const parsed = Object.entries(ports).sort(([left], [right]) => left.localeCompare(right)).map(([name, raw]) => {
602
+ if (!/^[a-z][a-z0-9_-]{0,63}$/u.test(name)) {
603
+ throw new Error(`runtime_contract_invalid: port name '${name}' is invalid.`);
604
+ }
605
+ const port = object(raw, `runtime.ports.${name}`);
606
+ rejectUnknownKeys(port, portKeys, `runtime.ports.${name}`);
607
+ const adapter = parsePortAdapter(port.adapter, name);
608
+ const launch = parseLaunch(port.launch, adapter, name);
609
+ const env = environmentName(port.env, `runtime.ports.${name}.env`);
610
+ if (adapter === "aspnetcore" && env !== "ASPNETCORE_URLS") {
611
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.env must be ASPNETCORE_URLS for the aspnetcore adapter.`);
612
+ }
613
+ const baseUrlEnv = port.baseUrlEnv === undefined || port.baseUrlEnv === null
614
+ ? null
615
+ : environmentName(port.baseUrlEnv, `runtime.ports.${name}.baseUrlEnv`);
616
+ if (environmentNames.has(env) || (baseUrlEnv !== null && environmentNames.has(baseUrlEnv)) || env === baseUrlEnv) {
617
+ throw new Error("runtime_contract_invalid: port environment names must be unique.");
618
+ }
619
+ environmentNames.add(env);
620
+ if (baseUrlEnv)
621
+ environmentNames.add(baseUrlEnv);
622
+ const format = parseFormat(port.format, baseUrlEnv, name);
623
+ if (launch && (!baseUrlEnv || !format)) {
624
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch requires an HTTP baseUrlEnv and format.`);
625
+ }
626
+ return { name, adapter, launch, env, baseUrlEnv, format };
627
+ });
628
+ return parsed;
629
+ }
630
+ function parsePortAdapter(value, name) {
631
+ if (value === undefined || value === null)
632
+ return null;
633
+ if (value !== "node" && value !== "vite" && value !== "next" && value !== "aspnetcore") {
634
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.adapter must be node, vite, next, or aspnetcore.`);
635
+ }
636
+ return value;
637
+ }
638
+ function parseLaunch(value, adapter, name) {
639
+ if (value === undefined || value === null)
640
+ return null;
641
+ if (!adapter) {
642
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch requires an explicit adapter.`);
643
+ }
644
+ const launch = object(value, `runtime.ports.${name}.launch`);
645
+ rejectUnknownKeys(launch, launchKeys, `runtime.ports.${name}.launch`);
646
+ const command = boundedString(launch.command, 1024, `runtime.ports.${name}.launch.command`);
647
+ const args = launch.args === undefined || launch.args === null ? [] : launch.args;
648
+ if (!Array.isArray(args) || args.length > 128) {
649
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch.args must contain at most 128 strings.`);
650
+ }
651
+ const parsedArgs = args.map((argument, index) => boundedString(argument, 4096, `runtime.ports.${name}.launch.args[${index}]`));
652
+ rejectProjectAuthoredForgeArguments(parsedArgs, adapter, name);
653
+ const portableDirectory = launch.workingDirectory === undefined || launch.workingDirectory === null
654
+ ? "."
655
+ : containedPortablePath(launch.workingDirectory, `runtime.ports.${name}.launch.workingDirectory`);
656
+ const environment = parseLaunchEnvironment(launch.environment, name);
657
+ return {
658
+ command,
659
+ args: parsedArgs,
660
+ workingDirectory: portableDirectory,
661
+ ...(environment ? { environment } : {}),
662
+ };
663
+ }
664
+ function parseLaunchEnvironment(value, portName) {
665
+ if (value === undefined || value === null)
666
+ return undefined;
667
+ const environment = object(value, `runtime.ports.${portName}.launch.environment`);
668
+ if (Object.keys(environment).length > 64) {
669
+ throw new Error(`runtime_contract_invalid: runtime.ports.${portName}.launch.environment cannot contain more than 64 entries.`);
670
+ }
671
+ return Object.fromEntries(Object.entries(environment).map(([name, raw]) => {
672
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name)) {
673
+ throw new Error(`runtime_contract_invalid: launch environment name '${name}' is invalid.`);
674
+ }
675
+ if (name.toLowerCase().startsWith("forge_") || isUnsafeDotnetEnvironmentName(name)) {
676
+ throw new Error(`runtime_contract_invalid: launch environment name '${name}' is host-owned or enables pre-startup code injection and cannot be project-authored.`);
677
+ }
678
+ return [name, boundedString(raw, 8192, `runtime.ports.${portName}.launch.environment.${name}`)];
679
+ }));
680
+ }
681
+ function isUnsafeDotnetEnvironmentName(name) {
682
+ const normalized = name.toUpperCase();
683
+ return normalized === "DOTNET_STARTUP_HOOKS"
684
+ || normalized === "DOTNET_ADDITIONAL_DEPS"
685
+ || normalized === "DOTNET_SHARED_STORE"
686
+ || normalized === "DOTNET_HOST_PATH"
687
+ || normalized === "MSBUILD_EXE_PATH"
688
+ || normalized === "MSBUILDSDKSPATH"
689
+ || normalized === "NUGET_PLUGIN_PATHS"
690
+ || normalized === "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES"
691
+ || normalized === "ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES"
692
+ || normalized.startsWith("CUSTOMBEFOREMICROSOFTCOMMON")
693
+ || normalized.startsWith("CUSTOMAFTERMICROSOFTCOMMON")
694
+ || normalized.startsWith("DOTNET_ROOT")
695
+ || normalized.startsWith("CORECLR_")
696
+ || normalized.startsWith("COR_")
697
+ || normalized.startsWith("COMPLUS_")
698
+ || normalized.startsWith("LD_")
699
+ || normalized.startsWith("DYLD_");
700
+ }
701
+ function rejectProjectAuthoredForgeArguments(args, adapter, portName) {
702
+ if (adapter !== "aspnetcore")
703
+ return;
704
+ const applicationArgumentIndex = args.indexOf("--");
705
+ if (applicationArgumentIndex < 0)
706
+ return;
707
+ const forbidden = args.slice(applicationArgumentIndex + 1).find(argument => {
708
+ const key = argument.split("=", 1)[0].replace(/^(?:--?|\/)/u, "");
709
+ return /^forge(?:[_:.-]|$)/iu.test(key);
710
+ });
711
+ if (forbidden) {
712
+ throw new Error(`runtime_contract_invalid: runtime.ports.${portName}.launch.args cannot override the host-owned FORGE_* namespace.`);
713
+ }
714
+ }
715
+ function containedPortablePath(value, label) {
716
+ const original = boundedString(value, 1024, label);
717
+ const portable = original.replaceAll("\\", "/");
718
+ if (path.posix.isAbsolute(portable)
719
+ || path.win32.isAbsolute(original)
720
+ || portable.split("/").some(segment => segment === "..")) {
721
+ throw new Error(`runtime_contract_invalid: ${label} must stay inside the worktree.`);
722
+ }
723
+ return portable;
724
+ }
725
+ function parseFormat(value, baseUrlEnv, name) {
726
+ if (baseUrlEnv === null) {
727
+ if (value !== undefined && value !== null) {
728
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format requires baseUrlEnv.`);
729
+ }
730
+ return null;
731
+ }
732
+ const format = value === undefined || value === null ? "http://{host}:{port}" : requiredString(value, `runtime.ports.${name}.format`);
733
+ if (format.length > 512 || !format.includes("{port}")) {
734
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format must include {port}.`);
735
+ }
736
+ const rendered = format.replaceAll("{host}", "127.0.0.1").replaceAll("{port}", "12345");
737
+ let url;
738
+ try {
739
+ url = new URL(rendered);
740
+ }
741
+ catch {
742
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format must produce a URL.`);
743
+ }
744
+ if ((url.protocol !== "http:" && url.protocol !== "https:") || url.hostname !== "127.0.0.1" || url.port !== "12345") {
745
+ throw new Error(`runtime_contract_invalid: runtime.ports.${name}.format must produce an HTTP loopback URL using the assigned port.`);
746
+ }
747
+ return format;
748
+ }
749
+ function parseSingletonResources(value) {
750
+ if (value === undefined || value === null)
751
+ return [];
752
+ if (!Array.isArray(value) || value.length > 32 || value.some(entry => typeof entry !== "string")) {
753
+ throw new Error("runtime_contract_invalid: runtime.singletonResources must be a string array with at most 32 entries.");
754
+ }
755
+ const resources = value.map((entry, index) => {
756
+ const resource = requiredString(entry, `runtime.singletonResources[${index}]`).toLowerCase();
757
+ if (!/^[a-z][a-z0-9_.:-]{0,127}$/u.test(resource)) {
758
+ throw new Error(`runtime_contract_invalid: singleton resource '${resource}' is invalid.`);
759
+ }
760
+ return resource;
761
+ });
762
+ if (new Set(resources).size !== resources.length) {
763
+ throw new Error("runtime_contract_invalid: runtime.singletonResources cannot contain duplicates.");
764
+ }
765
+ return resources.sort((left, right) => left.localeCompare(right));
766
+ }
767
+ function environmentName(value, label) {
768
+ const name = requiredString(value, label);
769
+ if (!/^[A-Z_][A-Z0-9_]{0,127}$/u.test(name) || name.startsWith("FORGE_") || name === "PATH" || name === "HOME") {
770
+ throw new Error(`runtime_contract_invalid: ${label} is reserved or invalid.`);
771
+ }
772
+ return name;
773
+ }
774
+ function object(value, label) {
775
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
776
+ throw new Error(`runtime_contract_invalid: ${label} must be an object.`);
777
+ }
778
+ return value;
779
+ }
780
+ function rejectUnknownKeys(value, allowed, label) {
781
+ const unknown = Object.keys(value).find(key => !allowed.has(key));
782
+ if (unknown)
783
+ throw new Error(`runtime_contract_invalid: unknown ${label} key '${unknown}'.`);
784
+ }
785
+ function requiredString(value, label) {
786
+ if (typeof value !== "string" || !value.trim()) {
787
+ throw new Error(`runtime_contract_invalid: ${label} must be a non-empty string.`);
788
+ }
789
+ return value.trim();
790
+ }
791
+ function boundedString(value, maximumLength, label) {
792
+ const text = requiredString(value, label);
793
+ if (text.length > maximumLength || Array.from(text).some(character => {
794
+ const codePoint = character.codePointAt(0) ?? 0;
795
+ return codePoint < 32 || codePoint === 127;
796
+ })) {
797
+ throw new Error(`runtime_contract_invalid: ${label} is too long or contains control characters.`);
798
+ }
799
+ return text;
800
+ }
801
+ function assertDepth(value, depth) {
802
+ if (depth > maximumDepth)
803
+ throw new Error(`runtime_contract_invalid: YAML nesting exceeds ${maximumDepth}.`);
804
+ if (Array.isArray(value)) {
805
+ value.forEach(entry => assertDepth(entry, depth + 1));
806
+ }
807
+ else if (value && typeof value === "object") {
808
+ Object.values(value).forEach(entry => assertDepth(entry, depth + 1));
809
+ }
810
+ }
811
+ function isFileNotFound(error) {
812
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
813
+ }