@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.9445952e33ba

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 (162) hide show
  1. package/README.md +88 -2
  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 +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1563 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  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 +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +67 -20
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +191 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +28 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +17 -8
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +132 -103
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +485 -31
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +105 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  144. package/dist/shared/process/host-process-supervisor.js +57 -9
  145. package/dist/shared/process/process.d.ts +2 -0
  146. package/dist/shared/process/process.js +12 -7
  147. package/dist/shared/provider-capabilities.d.ts +3 -0
  148. package/dist/shared/provider-capabilities.js +85 -0
  149. package/dist/shared/redaction/redaction.d.ts +1 -0
  150. package/dist/shared/redaction/redaction.js +39 -3
  151. package/dist/version.d.ts +1 -1
  152. package/dist/version.js +1 -1
  153. package/dist/worktree/dependency-warmer.d.ts +41 -9
  154. package/dist/worktree/dependency-warmer.js +497 -22
  155. package/dist/worktree/index.d.ts +1 -1
  156. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  157. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  158. package/dist/worktree/worktree.service.d.ts +7 -0
  159. package/dist/worktree/worktree.service.js +253 -21
  160. package/dist/worktree/worktree.types.d.ts +7 -1
  161. package/package.json +7 -5
  162. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -0,0 +1,341 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { ForgeFailureError } from "../session/session.types.js";
6
+ const inFlightPrebuilds = new Map();
7
+ export function createAspNetRuntimePrebuildPlan(options) {
8
+ if (!isDotnetRun(options.command, options.args))
9
+ return null;
10
+ const projectArgument = readOptionValue(options.args, "--project");
11
+ const projectPath = path.resolve(options.cwd, projectArgument ?? ".");
12
+ const normalizedWorktree = path.resolve(options.worktreePath);
13
+ const relativeProject = path.relative(normalizedWorktree, projectPath);
14
+ if (relativeProject.startsWith("..") || path.isAbsolute(relativeProject)) {
15
+ throw new Error("host_runtime_build_invalid: the ASP.NET project must remain inside the managed worktree.");
16
+ }
17
+ const projectDirectory = projectArgument && path.extname(projectPath).toLowerCase() === ".csproj"
18
+ ? path.dirname(projectPath)
19
+ : projectPath;
20
+ const buildArgs = [
21
+ "build",
22
+ ...(projectArgument ? [projectArgument] : []),
23
+ "--nologo",
24
+ "--no-restore",
25
+ "--disable-build-servers",
26
+ "-p:UseSharedCompilation=false",
27
+ "-p:DefaultItemExcludesInProjectFolder=TestResults/**",
28
+ ...forwardBuildOptions(options.args),
29
+ ];
30
+ const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
31
+ inputCommitSha: options.inputCommitSha,
32
+ runtimeContractFingerprint: options.runtimeContractFingerprint,
33
+ project: relativeProject.replaceAll("\\", "/"),
34
+ args: buildArgs,
35
+ environment: buildEnvironmentContract(options.environment ?? {}),
36
+ })).digest("hex")}`;
37
+ return {
38
+ fingerprint,
39
+ receiptPath: path.join(projectDirectory, "obj", ".forge-runtime-build.json"),
40
+ command: options.command,
41
+ args: buildArgs,
42
+ cwd: options.cwd,
43
+ environment: options.environment ?? {},
44
+ projectPath,
45
+ worktreePath: normalizedWorktree,
46
+ inputCommitSha: options.inputCommitSha,
47
+ };
48
+ }
49
+ export async function ensureAspNetRuntimePrebuilt(plan, execute, signal) {
50
+ const pending = inFlightPrebuilds.get(plan.receiptPath);
51
+ if (pending)
52
+ return await pending;
53
+ const started = ensureAspNetRuntimePrebuiltCore(plan, execute, signal);
54
+ inFlightPrebuilds.set(plan.receiptPath, started);
55
+ try {
56
+ return await started;
57
+ }
58
+ finally {
59
+ if (inFlightPrebuilds.get(plan.receiptPath) === started)
60
+ inFlightPrebuilds.delete(plan.receiptPath);
61
+ }
62
+ }
63
+ async function ensureAspNetRuntimePrebuiltCore(plan, execute, signal) {
64
+ const inputs = await aspNetBuildInputs(plan);
65
+ const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
66
+ planFingerprint: plan.fingerprint,
67
+ ...inputs,
68
+ })).digest("hex")}`;
69
+ const receipt = await readReceipt(plan.receiptPath);
70
+ if (receipt?.fingerprint === fingerprint
71
+ && receipt.planFingerprint === plan.fingerprint
72
+ && receipt.inputCommitSha === plan.inputCommitSha
73
+ && await aspNetOutputsMatch(plan, receipt.outputs)) {
74
+ return { cacheHit: true, receiptPath: plan.receiptPath, fingerprint };
75
+ }
76
+ const result = await execute(plan, signal);
77
+ if (result.exitCode !== 0) {
78
+ const evidence = [
79
+ diagnostic("stderr", result.stderrTail),
80
+ diagnostic("stdout", result.stdoutTail),
81
+ `stdout log: ${result.stdoutLogPath}`,
82
+ `stderr log: ${result.stderrLogPath}`,
83
+ ].filter(Boolean).join("; ");
84
+ throw new ForgeFailureError("host_runtime_project_start_failed", `host_runtime_project_start_failed: dotnet build exited with code ${result.exitCode}.${evidence ? ` ${evidence}` : ""}`, "bridge");
85
+ }
86
+ await fs.mkdir(path.dirname(plan.receiptPath), { recursive: true });
87
+ const outputs = await collectAspNetBuildOutputs(plan);
88
+ if (outputs.length === 0) {
89
+ throw new Error("host_runtime_build_output_missing: dotnet build succeeded without producing a verifiable project output.");
90
+ }
91
+ const temporaryReceipt = `${plan.receiptPath}.${randomUUID()}.tmp`;
92
+ const nextReceipt = {
93
+ schemaVersion: 2,
94
+ fingerprint,
95
+ planFingerprint: plan.fingerprint,
96
+ inputCommitSha: plan.inputCommitSha,
97
+ projectPath: plan.projectPath,
98
+ ...inputs,
99
+ outputs,
100
+ completedAt: new Date().toISOString(),
101
+ };
102
+ await fs.writeFile(temporaryReceipt, `${JSON.stringify(nextReceipt)}\n`, { encoding: "utf8", mode: 0o600 });
103
+ try {
104
+ await fs.rename(temporaryReceipt, plan.receiptPath);
105
+ }
106
+ finally {
107
+ await fs.rm(temporaryReceipt, { force: true }).catch(() => undefined);
108
+ }
109
+ return { cacheHit: false, receiptPath: plan.receiptPath, fingerprint };
110
+ }
111
+ export function withAspNetPrebuiltRunArguments(command, args) {
112
+ if (!isDotnetRun(command, args)) {
113
+ throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
114
+ }
115
+ const applicationArguments = args.indexOf("--");
116
+ const insertionIndex = applicationArguments < 0 ? args.length : applicationArguments;
117
+ const beforeApplication = args.slice(0, insertionIndex);
118
+ return [
119
+ ...beforeApplication,
120
+ ...(!beforeApplication.includes("--no-build") ? ["--no-build"] : []),
121
+ ...(!beforeApplication.includes("--no-restore") ? ["--no-restore"] : []),
122
+ ...args.slice(insertionIndex),
123
+ ];
124
+ }
125
+ export async function resolveWorktreeCommitSha(worktreePath, supplied) {
126
+ if (supplied && /^[0-9a-f]{40,64}$/iu.test(supplied))
127
+ return supplied.toLowerCase();
128
+ return await new Promise((resolve, reject) => {
129
+ const child = spawn("git", ["rev-parse", "--verify", "HEAD^{commit}"], {
130
+ cwd: worktreePath,
131
+ windowsHide: true,
132
+ stdio: ["ignore", "pipe", "pipe"],
133
+ });
134
+ let stdout = "";
135
+ let stderr = "";
136
+ child.stdout.setEncoding("utf8");
137
+ child.stderr.setEncoding("utf8");
138
+ child.stdout.on("data", chunk => { stdout = `${stdout}${String(chunk)}`.slice(-256); });
139
+ child.stderr.on("data", chunk => { stderr = `${stderr}${String(chunk)}`.slice(-1_000); });
140
+ child.once("error", reject);
141
+ child.once("close", code => {
142
+ const commit = stdout.trim();
143
+ if (code === 0 && /^[0-9a-f]{40,64}$/iu.test(commit))
144
+ resolve(commit.toLowerCase());
145
+ else
146
+ reject(new Error(`host_runtime_build_commit_unavailable: git rev-parse failed (${code ?? "unknown"}): ${stderr.trim()}`));
147
+ });
148
+ });
149
+ }
150
+ function isDotnetRun(command, args) {
151
+ const executable = path.basename(command).toLowerCase();
152
+ return (executable === "dotnet" || executable === "dotnet.exe") && args[0]?.toLowerCase() === "run";
153
+ }
154
+ function readOptionValue(args, option) {
155
+ const index = args.findIndex(argument => argument === option || argument.startsWith(`${option}=`));
156
+ if (index < 0)
157
+ return null;
158
+ const argument = args[index];
159
+ return argument.startsWith(`${option}=`) ? argument.slice(option.length + 1) : args[index + 1] ?? null;
160
+ }
161
+ function forwardBuildOptions(args) {
162
+ const forwarded = [];
163
+ const optionsWithValues = new Set(["-c", "--configuration", "-f", "--framework", "-r", "--runtime", "--arch", "--os"]);
164
+ for (let index = 1; index < args.length; index += 1) {
165
+ const argument = args[index];
166
+ if (argument === "--")
167
+ break;
168
+ if (argument === "--project") {
169
+ index += 1;
170
+ continue;
171
+ }
172
+ if (argument.startsWith("--project="))
173
+ continue;
174
+ if (optionsWithValues.has(argument)) {
175
+ const value = args[index + 1];
176
+ if (value !== undefined) {
177
+ forwarded.push(argument, value);
178
+ index += 1;
179
+ }
180
+ continue;
181
+ }
182
+ if ([...optionsWithValues].some(option => argument.startsWith(`${option}=`))
183
+ || argument.startsWith("-p:")
184
+ || argument.startsWith("/p:")) {
185
+ forwarded.push(argument);
186
+ }
187
+ }
188
+ return forwarded;
189
+ }
190
+ async function readReceipt(receiptPath) {
191
+ try {
192
+ const parsed = JSON.parse(await fs.readFile(receiptPath, "utf8"));
193
+ return parsed.schemaVersion === 2
194
+ && typeof parsed.fingerprint === "string"
195
+ && typeof parsed.planFingerprint === "string"
196
+ && typeof parsed.inputCommitSha === "string"
197
+ && typeof parsed.sourceDigest === "string"
198
+ && typeof parsed.dependencyDigest === "string"
199
+ && typeof parsed.sdkVersion === "string"
200
+ && Array.isArray(parsed.outputs)
201
+ ? parsed
202
+ : null;
203
+ }
204
+ catch (error) {
205
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
206
+ return null;
207
+ throw error;
208
+ }
209
+ }
210
+ async function aspNetBuildInputs(plan) {
211
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
212
+ ? path.dirname(plan.projectPath)
213
+ : plan.projectPath;
214
+ const sourceDigest = await directoryContentDigest(projectDirectory);
215
+ const dependencyReceipt = path.join(projectDirectory, "obj", ".forge-dependency-preparation.json");
216
+ const dependencyContent = await fs.readFile(dependencyReceipt).catch(() => null);
217
+ const globalJson = await findAncestorFile(projectDirectory, plan.worktreePath, "global.json");
218
+ const globalJsonContent = globalJson ? await fs.readFile(globalJson) : null;
219
+ return {
220
+ sourceDigest,
221
+ dependencyDigest: `sha256:${createHash("sha256")
222
+ .update(dependencyContent ?? Buffer.alloc(0))
223
+ .update(globalJsonContent ?? Buffer.alloc(0))
224
+ .digest("hex")}`,
225
+ sdkVersion: await dotnetSdkVersion(plan.cwd),
226
+ };
227
+ }
228
+ async function directoryContentDigest(directory) {
229
+ const files = [];
230
+ await collectInputFiles(directory, directory, files);
231
+ const hash = createHash("sha256");
232
+ for (const file of files.sort((left, right) => left.localeCompare(right))) {
233
+ hash.update(path.relative(directory, file).replaceAll("\\", "/"));
234
+ hash.update("\0");
235
+ hash.update(await fs.readFile(file));
236
+ hash.update("\0");
237
+ }
238
+ return `sha256:${hash.digest("hex")}`;
239
+ }
240
+ async function collectInputFiles(root, directory, files) {
241
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
242
+ for (const entry of entries) {
243
+ const absolute = path.join(directory, entry.name);
244
+ if (entry.isDirectory()) {
245
+ if (!["bin", "obj", "node_modules", ".git", "TestResults"].includes(entry.name)) {
246
+ await collectInputFiles(root, absolute, files);
247
+ }
248
+ }
249
+ else if (entry.isFile()) {
250
+ files.push(absolute);
251
+ }
252
+ }
253
+ }
254
+ async function collectAspNetBuildOutputs(plan) {
255
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
256
+ ? path.dirname(plan.projectPath)
257
+ : plan.projectPath;
258
+ const outputRoot = path.join(projectDirectory, "bin");
259
+ const outputs = [];
260
+ await collectOutputFiles(outputRoot, projectDirectory, outputs);
261
+ return outputs.sort((left, right) => left.path.localeCompare(right.path));
262
+ }
263
+ async function collectOutputFiles(directory, projectDirectory, outputs) {
264
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
265
+ for (const entry of entries) {
266
+ const absolute = path.join(directory, entry.name);
267
+ if (entry.isDirectory())
268
+ await collectOutputFiles(absolute, projectDirectory, outputs);
269
+ else if (entry.isFile()) {
270
+ const stat = await fs.stat(absolute);
271
+ outputs.push({
272
+ path: path.relative(projectDirectory, absolute).replaceAll("\\", "/"),
273
+ size: stat.size,
274
+ digest: createHash("sha256").update(await fs.readFile(absolute)).digest("hex"),
275
+ });
276
+ }
277
+ }
278
+ }
279
+ async function aspNetOutputsMatch(plan, outputs) {
280
+ if (outputs.length === 0)
281
+ return false;
282
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
283
+ ? path.dirname(plan.projectPath)
284
+ : plan.projectPath;
285
+ for (const output of outputs) {
286
+ const absolute = path.resolve(projectDirectory, output.path);
287
+ const relative = path.relative(projectDirectory, absolute);
288
+ if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
289
+ return false;
290
+ const stat = await fs.stat(absolute).catch(() => null);
291
+ if (!stat?.isFile() || stat.size !== output.size)
292
+ return false;
293
+ const digest = createHash("sha256").update(await fs.readFile(absolute)).digest("hex");
294
+ if (digest !== output.digest)
295
+ return false;
296
+ }
297
+ return true;
298
+ }
299
+ async function findAncestorFile(directory, root, name) {
300
+ let current = path.resolve(directory);
301
+ const resolvedRoot = path.resolve(root);
302
+ while (true) {
303
+ const candidate = path.join(current, name);
304
+ if (await fs.stat(candidate).then(stat => stat.isFile()).catch(() => false))
305
+ return candidate;
306
+ if (current === resolvedRoot)
307
+ return null;
308
+ const parent = path.dirname(current);
309
+ if (parent === current || path.relative(resolvedRoot, parent).startsWith(".."))
310
+ return null;
311
+ current = parent;
312
+ }
313
+ }
314
+ async function dotnetSdkVersion(cwd) {
315
+ return await new Promise(resolve => {
316
+ const child = spawn("dotnet", ["--version"], { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
317
+ let output = "";
318
+ child.stdout.setEncoding("utf8");
319
+ child.stderr.setEncoding("utf8");
320
+ child.stdout.on("data", chunk => { output += String(chunk); });
321
+ child.stderr.on("data", chunk => { output += String(chunk); });
322
+ child.once("error", () => resolve("unavailable"));
323
+ child.once("close", code => resolve(code === 0 ? output.trim().slice(0, 256) : `unavailable:${code ?? "unknown"}`));
324
+ });
325
+ }
326
+ function buildEnvironmentContract(environment) {
327
+ return {
328
+ DOTNET_ROOT: environment.DOTNET_ROOT ?? process.env.DOTNET_ROOT ?? null,
329
+ DOTNET_ROLL_FORWARD: environment.DOTNET_ROLL_FORWARD ?? process.env.DOTNET_ROLL_FORWARD ?? null,
330
+ DOTNET_MULTILEVEL_LOOKUP: environment.DOTNET_MULTILEVEL_LOOKUP ?? process.env.DOTNET_MULTILEVEL_LOOKUP ?? null,
331
+ MSBuildSDKsPath: environment.MSBuildSDKsPath ?? process.env.MSBuildSDKsPath ?? null,
332
+ NUGET_PACKAGES: environment.NUGET_PACKAGES ?? process.env.NUGET_PACKAGES ?? null,
333
+ FORGE_BUILD_CACHE_EPOCH: createHash("sha256")
334
+ .update(environment.FORGE_BUILD_CACHE_EPOCH ?? process.env.FORGE_BUILD_CACHE_EPOCH ?? "default")
335
+ .digest("hex"),
336
+ };
337
+ }
338
+ function diagnostic(label, value) {
339
+ const normalized = value.replaceAll(/[\r\n\t]+/gu, " ").trim();
340
+ return normalized ? `${label} tail: ${normalized.slice(-2_000)}` : "";
341
+ }
@@ -0,0 +1,89 @@
1
+ import type { NativeTestPostgresFallback } from "./native-test-postgres-fallback.js";
2
+ /**
3
+ * The managed host attempt's database capability. The Bridge keeps the test cluster's
4
+ * administrator credential; an attempt (its agent, its application runtime and its tests)
5
+ * receives only a login for one database created for it. Forge.Api checks that login before it
6
+ * migrates (AttemptScopedDatabasePolicy.AssertBrokeredCapabilityAsync), but that check runs in
7
+ * attempt code: the boundary is what this broker issues and the cluster's pg_hba rules, which
8
+ * let an attempt role log in only to its own database.
9
+ */
10
+ export declare const attemptDatabaseCapabilityEnvironment = "FORGE_ATTEMPT_DATABASE_CAPABILITY";
11
+ export interface AttemptDatabaseCapability {
12
+ readonly databaseName: string;
13
+ readonly connectionString: string;
14
+ }
15
+ export type AttemptDatabaseReleaseOutcome = "dropped" | "missing" | "retained";
16
+ export interface AttemptDatabaseBroker {
17
+ provision(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<AttemptDatabaseCapability>;
18
+ release(dispatchAttemptId: string, fencingToken: string): Promise<AttemptDatabaseReleaseOutcome>;
19
+ /**
20
+ * Drops this broker's databases whose attempt is no longer live, asking `isLive` about each one
21
+ * just before acting on it. Returns the dropped names.
22
+ */
23
+ reapOrphans(isLive: (dispatchAttemptId: string) => boolean): Promise<readonly string[]>;
24
+ }
25
+ export declare class AttemptDatabaseBrokerError extends Error {
26
+ readonly code: string;
27
+ constructor(code: string, detail: string);
28
+ }
29
+ export interface AttemptDatabaseLogin {
30
+ readonly database: string;
31
+ readonly username: string;
32
+ readonly password: string;
33
+ }
34
+ /**
35
+ * Runs one psql script and returns its unaligned output: as the cluster administrator against
36
+ * `postgres`, or, given a login, as that login against its database.
37
+ */
38
+ export type AttemptDatabaseSqlRunner = (sql: string, signal?: AbortSignal, login?: AttemptDatabaseLogin) => Promise<string>;
39
+ /**
40
+ * The ownership marker on the attempt's role. Only a role with CREATEROLE can change a role's
41
+ * comment, so the attempt cannot forge or erase it (it can rewrite its database's comment, which
42
+ * is therefore informational). Must match AttemptScopedDatabasePolicy.BrokeredRoleMarkerPrefix.
43
+ */
44
+ export declare function brokeredDatabaseMarker(dispatchAttemptId: string, fencingToken: string, owner: string): string;
45
+ export declare function attemptDatabaseName(dispatchAttemptId: string): string;
46
+ /** Identifies one Bridge's broker on a shared cluster, so it reaps only databases it issued. */
47
+ export declare function attemptDatabaseBrokerOwner(stateRoot: string): string;
48
+ /**
49
+ * Brokers attempt databases on the dedicated native test cluster that
50
+ * scripts/setup-native-test-postgres.ps1 provisions. That cluster holds only disposable test
51
+ * databases, so even a leaked attempt capability cannot reach developer data. Each attempt
52
+ * gets a login role and a database it owns, cloned from template1 (which carries pgvector,
53
+ * an extension an unprivileged owner cannot create).
54
+ */
55
+ export declare class NativeTestClusterDatabaseBroker implements AttemptDatabaseBroker {
56
+ private readonly admin;
57
+ private readonly runSql;
58
+ private readonly owner;
59
+ private readonly delay;
60
+ /** Names being provisioned right now; the reaper leaves them alone. */
61
+ private readonly provisioning;
62
+ constructor(admin: NativeTestClusterAdmin, runSql: AttemptDatabaseSqlRunner, owner: string, delay?: (milliseconds: number) => Promise<void>);
63
+ provision(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<AttemptDatabaseCapability>;
64
+ release(dispatchAttemptId: string, fencingToken: string): Promise<AttemptDatabaseReleaseOutcome>;
65
+ reapOrphans(isLive: (dispatchAttemptId: string) => boolean): Promise<readonly string[]>;
66
+ private state;
67
+ private createDatabase;
68
+ /** Fails closed unless the cluster lets this login reach its own database and nothing else. */
69
+ private assertConfinedToOwnDatabase;
70
+ private drop;
71
+ }
72
+ export interface NativeTestClusterAdmin {
73
+ readonly host: string;
74
+ readonly port: number;
75
+ readonly username: string;
76
+ readonly password: string;
77
+ }
78
+ export declare function nativeTestClusterAdmin(fallback: NativeTestPostgresFallback): NativeTestClusterAdmin;
79
+ /** Runs SQL through the psql that ships with the PostgreSQL install hosting the native test cluster. */
80
+ export declare function psqlSqlRunner(admin: NativeTestClusterAdmin, psqlPath: string, timeoutMs?: number): AttemptDatabaseSqlRunner;
81
+ /** psql beside the cluster's pg_ctl: FORGE_NATIVE_TEST_POSTGRES_PSQL, a Windows install, or PATH. */
82
+ export declare function resolvePsqlPath(): string | null;
83
+ /**
84
+ * The broker for this host, or null when no native test cluster is provisioned. Without a
85
+ * broker, Forge.Api still refuses to start in a managed host attempt; other applications run.
86
+ */
87
+ export declare function createNativeTestClusterDatabaseBroker(readFallback: () => NativeTestPostgresFallback | null, stateRoot: string): AttemptDatabaseBroker | null;
88
+ /** The exact strings to redact wherever a capability may be printed: the whole value and its password. */
89
+ export declare function attemptDatabaseCapabilitySecrets(capability: string | undefined): readonly string[];