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

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 (160) 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 +1562 -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-node-runtime-launcher.d.ts +2 -1
  30. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  31. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  32. package/dist/runtime/attempt-path-operation.js +94 -0
  33. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  34. package/dist/runtime/attempt-private-directory.js +54 -0
  35. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  36. package/dist/runtime/attempt-process-supervisor.js +67 -20
  37. package/dist/runtime/canonical-source-file.d.ts +3 -0
  38. package/dist/runtime/canonical-source-file.js +54 -0
  39. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  40. package/dist/runtime/docker-engine-probe.js +333 -0
  41. package/dist/runtime/generated-output-reset.d.ts +10 -0
  42. package/dist/runtime/generated-output-reset.js +62 -8
  43. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  44. package/dist/runtime/host-database-capability-boundary.js +62 -0
  45. package/dist/runtime/host-diagnostics.d.ts +11 -0
  46. package/dist/runtime/host-diagnostics.js +191 -0
  47. package/dist/runtime/host-execution-root.d.ts +4 -0
  48. package/dist/runtime/host-execution-root.js +39 -0
  49. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  50. package/dist/runtime/host-listener-inspector.js +121 -60
  51. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  52. package/dist/runtime/host-runtime-contract.js +107 -53
  53. package/dist/runtime/host-runtime-lease.d.ts +90 -8
  54. package/dist/runtime/host-runtime-lease.js +1097 -108
  55. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  56. package/dist/runtime/host-runtime-promotion.js +59 -12
  57. package/dist/runtime/memory-capacity.d.ts +70 -0
  58. package/dist/runtime/memory-capacity.js +123 -0
  59. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  60. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  61. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  62. package/dist/runtime/node-host-runtime-adapter.js +28 -11
  63. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  64. package/dist/runtime/pinned-directory-process.js +61 -0
  65. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  66. package/dist/runtime/pinned-source-directory.js +94 -0
  67. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  68. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  69. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  70. package/dist/runtime/prepared-runtime-identity.js +218 -0
  71. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  72. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  73. package/dist/runtime/runtime-isolation-lease.js +4 -1
  74. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  75. package/dist/runtime/workspace-resource-estimator.js +174 -0
  76. package/dist/sandbox/preflight.d.ts +31 -1
  77. package/dist/sandbox/preflight.js +237 -51
  78. package/dist/sandbox/repository-lock.d.ts +2 -1
  79. package/dist/sandbox/repository-lock.js +17 -8
  80. package/dist/sandbox/runtime.d.ts +6 -1
  81. package/dist/sandbox/runtime.js +132 -103
  82. package/dist/sandbox/sandbox.types.d.ts +7 -0
  83. package/dist/sandbox/workspace.service.js +2 -0
  84. package/dist/session/managed-attempt-tools.d.ts +17 -1
  85. package/dist/session/managed-attempt-tools.js +481 -31
  86. package/dist/session/managed-build-coordinator.d.ts +5 -0
  87. package/dist/session/managed-build-coordinator.js +105 -0
  88. package/dist/session/managed-build-receipt.d.ts +21 -0
  89. package/dist/session/managed-build-receipt.js +337 -0
  90. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  91. package/dist/session/managed-verification-timeouts.js +17 -0
  92. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  93. package/dist/session/phase-completion.artifacts.js +224 -22
  94. package/dist/session/phase-completion.d.ts +11 -1
  95. package/dist/session/phase-completion.helpers.d.ts +5 -0
  96. package/dist/session/phase-completion.helpers.js +30 -7
  97. package/dist/session/phase-completion.js +14 -3
  98. package/dist/session/phase-completion.prompts.js +17 -18
  99. package/dist/session/phase-completion.runtime.d.ts +3 -1
  100. package/dist/session/phase-completion.runtime.js +118 -16
  101. package/dist/session/phase-completion.schemas.js +49 -2
  102. package/dist/session/phase-completion.validation.d.ts +1 -0
  103. package/dist/session/phase-completion.validation.js +111 -21
  104. package/dist/session/pre-publication-validation.d.ts +75 -0
  105. package/dist/session/pre-publication-validation.js +657 -0
  106. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  107. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  108. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  109. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  110. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  111. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  112. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  113. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  114. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  115. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  116. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  117. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  118. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  119. package/dist/session/provider-cli/provider-cli.runtime.d.ts +8 -1
  120. package/dist/session/provider-cli/provider-cli.runtime.js +40 -3
  121. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  122. package/dist/session/provider-cli/provider-cli.service.js +771 -68
  123. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  124. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  125. package/dist/session/provider-cli/provider-output-state.js +361 -17
  126. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  127. package/dist/session/provider-cli/provider-usage.js +28 -5
  128. package/dist/session/session-redaction.js +434 -9
  129. package/dist/session/session.service.js +145 -23
  130. package/dist/session/session.types.d.ts +147 -2
  131. package/dist/session/session.types.js +587 -7
  132. package/dist/session/unreferenced-exports.d.ts +12 -0
  133. package/dist/session/unreferenced-exports.js +59 -0
  134. package/dist/shared/format/format-bytes.d.ts +1 -0
  135. package/dist/shared/format/format-bytes.js +9 -0
  136. package/dist/shared/git/git.js +2 -1
  137. package/dist/shared/path/path.d.ts +1 -0
  138. package/dist/shared/path/path.js +18 -0
  139. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  140. package/dist/shared/process/dotnet-toolchain.js +307 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  142. package/dist/shared/process/host-process-supervisor.js +57 -9
  143. package/dist/shared/process/process.d.ts +2 -0
  144. package/dist/shared/process/process.js +12 -7
  145. package/dist/shared/provider-capabilities.d.ts +3 -0
  146. package/dist/shared/provider-capabilities.js +85 -0
  147. package/dist/shared/redaction/redaction.d.ts +1 -0
  148. package/dist/shared/redaction/redaction.js +39 -3
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/dist/worktree/dependency-warmer.d.ts +41 -9
  152. package/dist/worktree/dependency-warmer.js +497 -22
  153. package/dist/worktree/index.d.ts +1 -1
  154. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  155. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  156. package/dist/worktree/worktree.service.d.ts +7 -0
  157. package/dist/worktree/worktree.service.js +253 -21
  158. package/dist/worktree/worktree.types.d.ts +7 -1
  159. package/package.json +7 -5
  160. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -19,6 +19,10 @@ export interface AttemptProcessChildLedgerEntry {
19
19
  readonly settledAt?: string;
20
20
  readonly exitCode?: number;
21
21
  readonly failure?: string;
22
+ readonly peakMemoryBytes?: number;
23
+ readonly memorySampleCount?: number;
24
+ readonly memorySource?: string;
25
+ readonly memoryObservedAt?: string;
22
26
  }
23
27
  export interface AttemptProcessSupervisorLedger {
24
28
  readonly schemaVersion: 1;
@@ -79,8 +83,15 @@ export declare class AttemptProcessSupervisor {
79
83
  stopAll(fencingToken: string, reason?: unknown): Promise<void>;
80
84
  private performStopAll;
81
85
  snapshot(): AttemptProcessSupervisorLedger;
86
+ providerMemoryTelemetry(): AttemptProcessTreeMemoryTelemetry | null;
82
87
  private observeChildReady;
83
88
  private settleChild;
84
89
  private assertFence;
85
90
  private persist;
86
91
  }
92
+ export interface AttemptProcessTreeMemoryTelemetry {
93
+ readonly peakMemoryBytes: number;
94
+ readonly sampleCount: number;
95
+ readonly source: string;
96
+ readonly observedAt: string;
97
+ }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { ForgeFailureError, sanitizeFailureError } from "../session/session.types.js";
4
5
  const terminalChildStatuses = new Set(["exited", "failed"]);
5
6
  const defaultMetadataReadyTimeoutMs = process.platform === "win32" ? 60_000 : 10_000;
6
7
  /**
@@ -32,9 +33,11 @@ export class AttemptProcessSupervisor {
32
33
  }
33
34
  else {
34
35
  options.signal?.addEventListener("abort", () => {
35
- void this.stopAll(this.identity.fencingToken, options.signal?.reason).catch(error => {
36
- this.persistenceFailure ??= error;
37
- });
36
+ // stopAll already persists a degraded terminal snapshot when a child
37
+ // rejects during cancellation. Do not relabel that process failure as
38
+ // a persistence failure: doing so poisons every later cleanup retry,
39
+ // even after all supervised processes have exited.
40
+ void this.stopAll(this.identity.fencingToken, options.signal?.reason).catch(() => undefined);
38
41
  }, { once: true });
39
42
  }
40
43
  }
@@ -57,8 +60,10 @@ export class AttemptProcessSupervisor {
57
60
  const combinedSignal = signal
58
61
  ? AbortSignal.any([this.attemptController.signal, controller.signal, signal])
59
62
  : AbortSignal.any([this.attemptController.signal, controller.signal]);
63
+ const failureRedactionValues = [...new Set((request.failureRedactionValues ?? []).filter(value => value.length > 0))];
64
+ const { failureRedactionValues: _failureRedactionValues, ...executableRequest } = request;
60
65
  const childRequest = {
61
- ...request,
66
+ ...executableRequest,
62
67
  env: {
63
68
  ...request.env,
64
69
  FORGE_RUNTIME_MODE: "host",
@@ -82,6 +87,7 @@ export class AttemptProcessSupervisor {
82
87
  processSupervisorId,
83
88
  metadataPath,
84
89
  controller,
90
+ failureRedactionValues,
85
91
  startedAt,
86
92
  status: "starting",
87
93
  result: Promise.resolve({ exitCode: 1, stdout: "", stderr: "not started" }),
@@ -90,7 +96,7 @@ export class AttemptProcessSupervisor {
90
96
  this.currentByName.set(name, record);
91
97
  const result = this.runner.run(childRequest, handlers, combinedSignal);
92
98
  record.result = result;
93
- void result.then(value => this.settleChild(record, "exited", value.exitCode), error => this.settleChild(record, "failed", undefined, error));
99
+ void result.then(value => this.settleChild(record, "exited", value.exitCode, undefined, value), error => this.settleChild(record, "failed", undefined, error));
94
100
  try {
95
101
  await this.persist();
96
102
  }
@@ -180,7 +186,9 @@ export class AttemptProcessSupervisor {
180
186
  record.status = "stopping";
181
187
  await this.persist();
182
188
  const results = await Promise.allSettled(active.map(record => record.result));
183
- const rejected = results.filter((result) => result.status === "rejected");
189
+ const rejected = results.flatMap((result, index) => result.status === "rejected"
190
+ ? [sanitizeFailureError(result.reason, active[index].failureRedactionValues)]
191
+ : []);
184
192
  const terminalStatus = rejected.length > 0 || this.persistenceFailure !== undefined ? "degraded" : "stopped";
185
193
  await this.persist({
186
194
  ...this.snapshot(),
@@ -189,7 +197,7 @@ export class AttemptProcessSupervisor {
189
197
  });
190
198
  this.status = terminalStatus;
191
199
  if (rejected.length > 0) {
192
- throw new AggregateError(rejected.map(result => result.reason), "attempt_process_supervisor_cleanup_degraded");
200
+ throw new AggregateError(rejected, "attempt_process_supervisor_cleanup_degraded");
193
201
  }
194
202
  if (this.persistenceFailure !== undefined)
195
203
  throw this.persistenceFailure;
@@ -213,9 +221,32 @@ export class AttemptProcessSupervisor {
213
221
  ...(record.settledAt ? { settledAt: record.settledAt } : {}),
214
222
  ...(record.exitCode === undefined ? {} : { exitCode: record.exitCode }),
215
223
  ...(record.failure ? { failure: record.failure } : {}),
224
+ ...(record.peakMemoryBytes === undefined ? {} : { peakMemoryBytes: record.peakMemoryBytes }),
225
+ ...(record.memorySampleCount === undefined ? {} : { memorySampleCount: record.memorySampleCount }),
226
+ ...(record.memorySource ? { memorySource: record.memorySource } : {}),
227
+ ...(record.memoryObservedAt ? { memoryObservedAt: record.memoryObservedAt } : {}),
216
228
  })),
217
229
  };
218
230
  }
231
+ providerMemoryTelemetry() {
232
+ const observations = this.records
233
+ .filter(record => record.name === "provider"
234
+ && record.peakMemoryBytes !== undefined
235
+ && record.peakMemoryBytes > 0
236
+ && record.memorySampleCount !== undefined
237
+ && record.memorySampleCount > 0
238
+ && record.memorySource
239
+ && record.memoryObservedAt);
240
+ if (observations.length === 0)
241
+ return null;
242
+ const peak = observations.reduce((current, candidate) => (candidate.peakMemoryBytes ?? 0) > (current.peakMemoryBytes ?? 0) ? candidate : current);
243
+ return {
244
+ peakMemoryBytes: peak.peakMemoryBytes,
245
+ sampleCount: observations.reduce((total, candidate) => total + (candidate.memorySampleCount ?? 0), 0),
246
+ source: peak.memorySource,
247
+ observedAt: peak.memoryObservedAt,
248
+ };
249
+ }
219
250
  async observeChildReady(record) {
220
251
  const deadline = Date.now() + this.metadataReadyTimeoutMs;
221
252
  while (Date.now() <= deadline) {
@@ -228,19 +259,32 @@ export class AttemptProcessSupervisor {
228
259
  }
229
260
  if (terminalChildStatuses.has(record.status)) {
230
261
  const detail = await childStartFailureDetail(record);
231
- throw new Error(`attempt_process_supervisor_start_failed: child '${record.childId}' exited before supervision metadata was produced${detail ? `: ${detail}` : "."}`);
262
+ throw new ForgeFailureError("attempt_process_supervisor_start_failed", `attempt_process_supervisor_start_failed: child '${record.childId}' exited before supervision metadata was produced${detail ? `: ${detail}` : "."}`, "bridge");
232
263
  }
233
264
  await wait(25);
234
265
  }
235
266
  record.controller.abort("Supervised child metadata was not produced before the readiness deadline.");
236
267
  const detail = await childStartFailureDetail(record);
237
- throw new Error(`attempt_process_supervisor_start_timeout: child '${record.childId}' did not become supervised${detail ? `: ${detail}` : "."}`);
268
+ throw new ForgeFailureError("attempt_process_supervisor_start_timeout", `attempt_process_supervisor_start_timeout: child '${record.childId}' did not become supervised${detail ? `: ${detail}` : "."}`, "bridge");
238
269
  }
239
- async settleChild(record, status, exitCode, failure) {
270
+ async settleChild(record, status, exitCode, failure, result) {
240
271
  record.status = status;
241
272
  record.settledAt = new Date().toISOString();
242
273
  record.exitCode = exitCode;
243
- record.failure = failure === undefined ? undefined : boundedFailure(failure);
274
+ record.failure = failure === undefined
275
+ ? undefined
276
+ : boundedFailure(failure, record.failureRedactionValues);
277
+ if (result?.peakMemoryBytes !== undefined
278
+ && result.peakMemoryBytes > 0
279
+ && result.memorySampleCount !== undefined
280
+ && result.memorySampleCount > 0
281
+ && result.memorySource
282
+ && result.memoryObservedAt) {
283
+ record.peakMemoryBytes = result.peakMemoryBytes;
284
+ record.memorySampleCount = result.memorySampleCount;
285
+ record.memorySource = result.memorySource;
286
+ record.memoryObservedAt = result.memoryObservedAt;
287
+ }
244
288
  await this.persist().catch(error => {
245
289
  this.persistenceFailure ??= error;
246
290
  this.status = "degraded";
@@ -310,29 +354,32 @@ function boundedInteger(value, minimum, maximum, name) {
310
354
  }
311
355
  return value;
312
356
  }
313
- function boundedFailure(error) {
314
- const message = error instanceof Error ? error.message : String(error);
357
+ function boundedFailure(error, exactSecrets = []) {
358
+ const message = sanitizeFailureError(error, exactSecrets).message;
315
359
  return message.replaceAll(/[\r\n\t]+/gu, " ").slice(0, 512);
316
360
  }
317
361
  async function childStartFailureDetail(record) {
318
362
  try {
319
363
  const result = await record.result;
320
- const stderr = boundedDiagnostic(result.stderr);
364
+ const stderr = boundedDiagnostic(result.stderr, record.failureRedactionValues);
321
365
  return stderr
322
366
  ? `exit ${result.exitCode}; stderr: ${stderr}`
323
367
  : `exit ${result.exitCode}`;
324
368
  }
325
369
  catch (error) {
326
- return boundedFailure(error);
370
+ return boundedFailure(error, record.failureRedactionValues);
327
371
  }
328
372
  }
329
- function boundedDiagnostic(value) {
330
- return value.replaceAll(/[\r\n\t]+/gu, " ").trim().slice(0, 512);
373
+ function boundedDiagnostic(value, exactSecrets = []) {
374
+ return sanitizeFailureError(value, exactSecrets).message
375
+ .replaceAll(/[\r\n\t]+/gu, " ")
376
+ .trim()
377
+ .slice(0, 512);
331
378
  }
332
379
  function isRetryableStartFailure(error) {
333
- return error instanceof Error
334
- && (error.message.startsWith("attempt_process_supervisor_start_failed:")
335
- || error.message.startsWith("attempt_process_supervisor_start_timeout:"));
380
+ return error instanceof ForgeFailureError
381
+ && (error.failureCode === "attempt_process_supervisor_start_failed"
382
+ || error.failureCode === "attempt_process_supervisor_start_timeout");
336
383
  }
337
384
  function cancellationError(signal) {
338
385
  return new Error(String(signal.reason ?? "Supervised child launch was cancelled."));
@@ -0,0 +1,3 @@
1
+ import type { PinnedDirectoryIdentity } from "./pinned-directory-process.js";
2
+ /** Keep the validated source inode open: reopening a checked pathname can follow a replacement. */
3
+ export declare function readCanonicalSourceFile(target: string, maximumBytes: number, expectedIdentity?: PinnedDirectoryIdentity): Promise<Buffer>;
@@ -0,0 +1,54 @@
1
+ import { constants } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ /** Keep the validated source inode open: reopening a checked pathname can follow a replacement. */
5
+ export async function readCanonicalSourceFile(target, maximumBytes, expectedIdentity) {
6
+ const resolved = path.resolve(target);
7
+ const pathBefore = await canonicalStat(resolved);
8
+ if (expectedIdentity && !sameFile(pathBefore, expectedIdentity)) {
9
+ throw new Error("host_runtime_source_changed: source identity changed before reading.");
10
+ }
11
+ const handle = await fs.open(resolved, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
12
+ | (constants.O_NONBLOCK ?? 0));
13
+ try {
14
+ const before = await handle.stat({ bigint: true });
15
+ if (!before.isFile() || before.nlink !== 1n || !sameFile(pathBefore, before) || before.size > BigInt(maximumBytes)) {
16
+ throw new Error("host_runtime_source_invalid: source changed or exceeds the byte bound.");
17
+ }
18
+ if (!sameFile(before, await canonicalStat(resolved))) {
19
+ throw new Error("host_runtime_source_changed: source changed before reading.");
20
+ }
21
+ const bytes = Buffer.alloc(Number(before.size));
22
+ let offset = 0;
23
+ while (offset < bytes.length) {
24
+ const read = await handle.read(bytes, offset, bytes.length - offset, offset);
25
+ if (!read.bytesRead)
26
+ throw new Error("host_runtime_source_changed: incomplete source read.");
27
+ offset += read.bytesRead;
28
+ }
29
+ const after = await handle.stat({ bigint: true });
30
+ const pathAfter = await canonicalStat(resolved);
31
+ if (after.nlink !== 1n || !sameFile(before, after) || !sameFile(after, pathAfter) || before.size !== after.size
32
+ || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs) {
33
+ throw new Error("host_runtime_source_changed: source changed while reading.");
34
+ }
35
+ return bytes;
36
+ }
37
+ finally {
38
+ await handle.close();
39
+ }
40
+ }
41
+ async function canonicalStat(target) {
42
+ const stat = await fs.lstat(target, { bigint: true });
43
+ const real = await fs.realpath(target);
44
+ const realStat = await fs.lstat(real, { bigint: true });
45
+ const samePath = process.platform === "win32" ? real.toLowerCase() === target.toLowerCase() : real === target;
46
+ if (!samePath || !stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || !realStat.isFile()
47
+ || realStat.isSymbolicLink() || realStat.nlink !== 1n || !sameFile(stat, realStat)) {
48
+ throw new Error("host_runtime_source_invalid: source must be a canonical regular file.");
49
+ }
50
+ return realStat;
51
+ }
52
+ function sameFile(left, right) {
53
+ return left.dev === right.dev && left.ino === right.ino;
54
+ }
@@ -0,0 +1,90 @@
1
+ import type { CommandRunner } from "../sandbox/sandbox.types.js";
2
+ export interface DockerEngineCapabilitySnapshot {
3
+ readonly available: boolean;
4
+ readonly sampledAt: string;
5
+ readonly runtimeVersion: string | null;
6
+ readonly operatingSystem: string | null;
7
+ readonly context: string;
8
+ readonly unavailableCode: string | null;
9
+ readonly unavailableReason: string | null;
10
+ }
11
+ export interface DockerEngineProbeOptions {
12
+ readonly dockerExecutable?: string;
13
+ readonly dockerContext?: string;
14
+ readonly recovery?: DockerEngineRecoveryPolicy;
15
+ readonly recoveryExecutor?: DockerEngineRecoveryExecutor;
16
+ readonly platform?: NodeJS.Platform;
17
+ readonly dockerHost?: string;
18
+ readonly now?: () => number;
19
+ readonly wait?: (delayMs: number) => Promise<void>;
20
+ readonly onStatus?: (message: string) => void;
21
+ }
22
+ export interface DockerEngineRecoveryPolicy {
23
+ readonly enabled: boolean;
24
+ readonly failureThreshold?: number;
25
+ readonly confirmationDelayMs?: number;
26
+ readonly cooldownMs?: number;
27
+ readonly startupTimeoutMs?: number;
28
+ readonly pollIntervalMs?: number;
29
+ readonly canRecover?: () => boolean;
30
+ }
31
+ export interface DockerEngineRecoveryResult {
32
+ readonly started: boolean;
33
+ readonly reason?: string;
34
+ }
35
+ export interface DockerEngineRecoveryExecutor {
36
+ restart(): Promise<DockerEngineRecoveryResult>;
37
+ }
38
+ /**
39
+ * Probes ordinary Docker-daemon access for host-run Testcontainers and project
40
+ * services. This is deliberately independent from the stricter Forge sandbox
41
+ * preflight: a usable Docker Desktop engine may satisfy docker_engine even
42
+ * when it is not approved to host an agent sandbox.
43
+ */
44
+ export declare class DockerEngineProbe {
45
+ private readonly commands;
46
+ private readonly cacheMs;
47
+ private cached;
48
+ private sampledAtMs;
49
+ private inFlight;
50
+ private requiredWorkRecoveryInFlight;
51
+ private consecutiveFailures;
52
+ private lastRecoveryAttemptAtMs;
53
+ private readonly docker;
54
+ private readonly context;
55
+ private readonly platform;
56
+ private readonly dockerHost;
57
+ private readonly now;
58
+ private readonly wait;
59
+ private readonly recovery;
60
+ private readonly recoveryExecutor;
61
+ private readonly onStatus;
62
+ constructor(options?: DockerEngineProbeOptions, commands?: CommandRunner, cacheMs?: number);
63
+ current(): DockerEngineCapabilitySnapshot | null;
64
+ refresh(force?: boolean): Promise<DockerEngineCapabilitySnapshot>;
65
+ /**
66
+ * Establishes a fresh daemon fence for work that declares Docker as a hard
67
+ * prerequisite. This remains bounded, cooldown protected and single-flight;
68
+ * it never contacts Docker unless a caller explicitly invokes this method.
69
+ */
70
+ ensureAvailableForRequiredWork(): Promise<DockerEngineCapabilitySnapshot>;
71
+ private inspectSafely;
72
+ private recoverIfEligible;
73
+ private isLocalDockerDesktopContext;
74
+ private inspect;
75
+ }
76
+ /**
77
+ * Restarts only the local Docker Desktop installation. It never targets a
78
+ * remote daemon and is used only behind the explicit bridge recovery policy.
79
+ */
80
+ export declare class WindowsDockerDesktopRecoveryExecutor implements DockerEngineRecoveryExecutor {
81
+ private readonly platform;
82
+ private readonly commands;
83
+ private readonly programFiles;
84
+ private readonly wait;
85
+ constructor(platform?: NodeJS.Platform, commands?: CommandRunner, programFiles?: string, wait?: (delayMs: number) => Promise<void>);
86
+ restart(): Promise<DockerEngineRecoveryResult>;
87
+ private readProcessState;
88
+ private cleanupOrphanedCompanions;
89
+ private startDesktop;
90
+ }