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

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 (165) 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 +16 -0
  38. package/dist/runtime/attempt-process-supervisor.js +94 -23
  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 +192 -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 +29 -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 +19 -9
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +137 -107
  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 +489 -34
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +106 -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-recovery.js +3 -2
  144. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  145. package/dist/shared/process/host-process-supervisor.js +60 -11
  146. package/dist/shared/process/process.d.ts +2 -0
  147. package/dist/shared/process/process.js +12 -7
  148. package/dist/shared/provider-capabilities.d.ts +3 -0
  149. package/dist/shared/provider-capabilities.js +85 -0
  150. package/dist/shared/redaction/redaction.d.ts +1 -0
  151. package/dist/shared/redaction/redaction.js +39 -3
  152. package/dist/shared/time/monotonic.d.ts +1 -0
  153. package/dist/shared/time/monotonic.js +8 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/worktree/dependency-warmer.d.ts +41 -9
  157. package/dist/worktree/dependency-warmer.js +497 -22
  158. package/dist/worktree/index.d.ts +1 -1
  159. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  160. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  161. package/dist/worktree/worktree.service.d.ts +7 -0
  162. package/dist/worktree/worktree.service.js +253 -21
  163. package/dist/worktree/worktree.types.d.ts +7 -1
  164. package/package.json +7 -5
  165. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { runAttemptPathOperation } from "./attempt-path-operation.js";
4
+ /** Validate every existing ancestor before creating anything through a pathname. */
5
+ export async function assertCanonicalDirectory(target, allowMissing = false) {
6
+ const resolved = path.resolve(target);
7
+ const parent = path.dirname(resolved);
8
+ if (parent !== resolved)
9
+ await assertCanonicalDirectory(parent, allowMissing);
10
+ try {
11
+ const stat = await fs.lstat(resolved, { bigint: true });
12
+ const real = await fs.realpath(resolved);
13
+ const realStat = await fs.lstat(real, { bigint: true });
14
+ if (stat.isSymbolicLink() || !stat.isDirectory() || !samePath(real, resolved)
15
+ || stat.dev !== realStat.dev || stat.ino !== realStat.ino) {
16
+ throw new Error("playwright_runtime_adapter_invalid: writable paths must be canonical, link-free directories.");
17
+ }
18
+ return realStat;
19
+ }
20
+ catch (error) {
21
+ if (allowMissing && error.code === "ENOENT")
22
+ return;
23
+ throw error;
24
+ }
25
+ }
26
+ export async function createCanonicalDirectory(target) {
27
+ if (await assertCanonicalDirectory(target, true))
28
+ return;
29
+ const resolved = path.resolve(target);
30
+ let parent = path.dirname(resolved);
31
+ let identity = await assertCanonicalDirectory(parent, true);
32
+ while (!identity) {
33
+ parent = path.dirname(parent);
34
+ identity = await assertCanonicalDirectory(parent, true);
35
+ }
36
+ await runAttemptPathOperation(parent, identity, { kind: "mkdir", segments: path.relative(parent, resolved).split(path.sep) });
37
+ await assertCanonicalDirectory(resolved);
38
+ }
39
+ export async function createPrivateCanonicalDirectory(target) {
40
+ await createCanonicalDirectory(target);
41
+ await restorePrivateDirectoryMode(target);
42
+ }
43
+ export async function restorePrivateDirectoryMode(target) {
44
+ if (process.platform === "win32")
45
+ return;
46
+ const resolved = path.resolve(target);
47
+ const parent = path.dirname(resolved);
48
+ const identity = await assertCanonicalDirectory(parent);
49
+ await runAttemptPathOperation(parent, identity, { kind: "private-directory-mode", segments: [path.basename(resolved)] });
50
+ await assertCanonicalDirectory(resolved);
51
+ }
52
+ function samePath(left, right) {
53
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
54
+ }
@@ -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;
@@ -42,6 +46,8 @@ export interface AttemptProcessChildHandle {
42
46
  export interface AttemptProcessSupervisorOptions {
43
47
  readonly metadataReadyTimeoutMs?: number;
44
48
  readonly signal?: AbortSignal;
49
+ readonly replaceLedgerFile?: (temporaryPath: string, ledgerPath: string) => Promise<void>;
50
+ readonly ledgerReplaceRetryMs?: number;
45
51
  }
46
52
  export interface AttemptProcessChildReadyOptions {
47
53
  readonly maxStartAttempts?: number;
@@ -58,6 +64,8 @@ export declare class AttemptProcessSupervisor {
58
64
  readonly ledgerPath: string;
59
65
  private readonly identity;
60
66
  private readonly metadataReadyTimeoutMs;
67
+ private readonly replaceLedgerFile;
68
+ private readonly ledgerReplaceRetryMs;
61
69
  private readonly records;
62
70
  private readonly currentByName;
63
71
  private readonly attemptController;
@@ -79,8 +87,16 @@ export declare class AttemptProcessSupervisor {
79
87
  stopAll(fencingToken: string, reason?: unknown): Promise<void>;
80
88
  private performStopAll;
81
89
  snapshot(): AttemptProcessSupervisorLedger;
90
+ providerMemoryTelemetry(): AttemptProcessTreeMemoryTelemetry | null;
82
91
  private observeChildReady;
83
92
  private settleChild;
93
+ private replaceLedger;
84
94
  private assertFence;
85
95
  private persist;
86
96
  }
97
+ export interface AttemptProcessTreeMemoryTelemetry {
98
+ readonly peakMemoryBytes: number;
99
+ readonly sampleCount: number;
100
+ readonly source: string;
101
+ readonly observedAt: string;
102
+ }
@@ -1,8 +1,15 @@
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";
5
+ import { monotonicNowMs } from "../shared/time/monotonic.js";
4
6
  const terminalChildStatuses = new Set(["exited", "failed"]);
5
7
  const defaultMetadataReadyTimeoutMs = process.platform === "win32" ? 60_000 : 10_000;
8
+ // Windows refuses to replace a file another process (a scanner or indexer) briefly holds
9
+ // open. One such refusal must not degrade the attempt, so the ledger replace is retried.
10
+ const ledgerReplaceAttempts = 40;
11
+ const defaultLedgerReplaceRetryMs = 25;
12
+ const transientLedgerReplaceCodes = new Set(["EPERM", "EACCES", "EBUSY"]);
6
13
  /**
7
14
  * Owns the independently supervised process trees that belong to one host
8
15
  * dispatch attempt. Each named child gets a distinct Windows Job Object or
@@ -14,6 +21,8 @@ export class AttemptProcessSupervisor {
14
21
  ledgerPath;
15
22
  identity;
16
23
  metadataReadyTimeoutMs;
24
+ replaceLedgerFile;
25
+ ledgerReplaceRetryMs;
17
26
  records = [];
18
27
  currentByName = new Map();
19
28
  attemptController = new AbortController();
@@ -25,6 +34,8 @@ export class AttemptProcessSupervisor {
25
34
  this.runner = runner;
26
35
  this.identity = validateIdentity(identity);
27
36
  this.metadataReadyTimeoutMs = boundedInteger(options.metadataReadyTimeoutMs ?? defaultMetadataReadyTimeoutMs, 100, 120_000, "metadataReadyTimeoutMs");
37
+ this.replaceLedgerFile = options.replaceLedgerFile ?? ((temporaryPath, ledgerPath) => fs.rename(temporaryPath, ledgerPath));
38
+ this.ledgerReplaceRetryMs = options.ledgerReplaceRetryMs ?? defaultLedgerReplaceRetryMs;
28
39
  this.ledgerPath = path.join(this.identity.logDirectory, "attempt-process-supervisor.json");
29
40
  if (options.signal?.aborted) {
30
41
  this.status = "stopping";
@@ -32,9 +43,11 @@ export class AttemptProcessSupervisor {
32
43
  }
33
44
  else {
34
45
  options.signal?.addEventListener("abort", () => {
35
- void this.stopAll(this.identity.fencingToken, options.signal?.reason).catch(error => {
36
- this.persistenceFailure ??= error;
37
- });
46
+ // stopAll already persists a degraded terminal snapshot when a child
47
+ // rejects during cancellation. Do not relabel that process failure as
48
+ // a persistence failure: doing so poisons every later cleanup retry,
49
+ // even after all supervised processes have exited.
50
+ void this.stopAll(this.identity.fencingToken, options.signal?.reason).catch(() => undefined);
38
51
  }, { once: true });
39
52
  }
40
53
  }
@@ -57,8 +70,10 @@ export class AttemptProcessSupervisor {
57
70
  const combinedSignal = signal
58
71
  ? AbortSignal.any([this.attemptController.signal, controller.signal, signal])
59
72
  : AbortSignal.any([this.attemptController.signal, controller.signal]);
73
+ const failureRedactionValues = [...new Set((request.failureRedactionValues ?? []).filter(value => value.length > 0))];
74
+ const { failureRedactionValues: _failureRedactionValues, ...executableRequest } = request;
60
75
  const childRequest = {
61
- ...request,
76
+ ...executableRequest,
62
77
  env: {
63
78
  ...request.env,
64
79
  FORGE_RUNTIME_MODE: "host",
@@ -82,6 +97,7 @@ export class AttemptProcessSupervisor {
82
97
  processSupervisorId,
83
98
  metadataPath,
84
99
  controller,
100
+ failureRedactionValues,
85
101
  startedAt,
86
102
  status: "starting",
87
103
  result: Promise.resolve({ exitCode: 1, stdout: "", stderr: "not started" }),
@@ -90,7 +106,7 @@ export class AttemptProcessSupervisor {
90
106
  this.currentByName.set(name, record);
91
107
  const result = this.runner.run(childRequest, handlers, combinedSignal);
92
108
  record.result = result;
93
- void result.then(value => this.settleChild(record, "exited", value.exitCode), error => this.settleChild(record, "failed", undefined, error));
109
+ void result.then(value => this.settleChild(record, "exited", value.exitCode, undefined, value), error => this.settleChild(record, "failed", undefined, error));
94
110
  try {
95
111
  await this.persist();
96
112
  }
@@ -180,7 +196,9 @@ export class AttemptProcessSupervisor {
180
196
  record.status = "stopping";
181
197
  await this.persist();
182
198
  const results = await Promise.allSettled(active.map(record => record.result));
183
- const rejected = results.filter((result) => result.status === "rejected");
199
+ const rejected = results.flatMap((result, index) => result.status === "rejected"
200
+ ? [sanitizeFailureError(result.reason, active[index].failureRedactionValues)]
201
+ : []);
184
202
  const terminalStatus = rejected.length > 0 || this.persistenceFailure !== undefined ? "degraded" : "stopped";
185
203
  await this.persist({
186
204
  ...this.snapshot(),
@@ -189,7 +207,7 @@ export class AttemptProcessSupervisor {
189
207
  });
190
208
  this.status = terminalStatus;
191
209
  if (rejected.length > 0) {
192
- throw new AggregateError(rejected.map(result => result.reason), "attempt_process_supervisor_cleanup_degraded");
210
+ throw new AggregateError(rejected, "attempt_process_supervisor_cleanup_degraded");
193
211
  }
194
212
  if (this.persistenceFailure !== undefined)
195
213
  throw this.persistenceFailure;
@@ -213,12 +231,35 @@ export class AttemptProcessSupervisor {
213
231
  ...(record.settledAt ? { settledAt: record.settledAt } : {}),
214
232
  ...(record.exitCode === undefined ? {} : { exitCode: record.exitCode }),
215
233
  ...(record.failure ? { failure: record.failure } : {}),
234
+ ...(record.peakMemoryBytes === undefined ? {} : { peakMemoryBytes: record.peakMemoryBytes }),
235
+ ...(record.memorySampleCount === undefined ? {} : { memorySampleCount: record.memorySampleCount }),
236
+ ...(record.memorySource ? { memorySource: record.memorySource } : {}),
237
+ ...(record.memoryObservedAt ? { memoryObservedAt: record.memoryObservedAt } : {}),
216
238
  })),
217
239
  };
218
240
  }
241
+ providerMemoryTelemetry() {
242
+ const observations = this.records
243
+ .filter(record => record.name === "provider"
244
+ && record.peakMemoryBytes !== undefined
245
+ && record.peakMemoryBytes > 0
246
+ && record.memorySampleCount !== undefined
247
+ && record.memorySampleCount > 0
248
+ && record.memorySource
249
+ && record.memoryObservedAt);
250
+ if (observations.length === 0)
251
+ return null;
252
+ const peak = observations.reduce((current, candidate) => (candidate.peakMemoryBytes ?? 0) > (current.peakMemoryBytes ?? 0) ? candidate : current);
253
+ return {
254
+ peakMemoryBytes: peak.peakMemoryBytes,
255
+ sampleCount: observations.reduce((total, candidate) => total + (candidate.memorySampleCount ?? 0), 0),
256
+ source: peak.memorySource,
257
+ observedAt: peak.memoryObservedAt,
258
+ };
259
+ }
219
260
  async observeChildReady(record) {
220
- const deadline = Date.now() + this.metadataReadyTimeoutMs;
221
- while (Date.now() <= deadline) {
261
+ const deadline = monotonicNowMs() + this.metadataReadyTimeoutMs;
262
+ while (monotonicNowMs() <= deadline) {
222
263
  if (await fileExists(record.metadataPath)) {
223
264
  if (record.status === "starting") {
224
265
  record.status = "running";
@@ -228,24 +269,51 @@ export class AttemptProcessSupervisor {
228
269
  }
229
270
  if (terminalChildStatuses.has(record.status)) {
230
271
  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}` : "."}`);
272
+ 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
273
  }
233
274
  await wait(25);
234
275
  }
235
276
  record.controller.abort("Supervised child metadata was not produced before the readiness deadline.");
236
277
  const detail = await childStartFailureDetail(record);
237
- throw new Error(`attempt_process_supervisor_start_timeout: child '${record.childId}' did not become supervised${detail ? `: ${detail}` : "."}`);
278
+ throw new ForgeFailureError("attempt_process_supervisor_start_timeout", `attempt_process_supervisor_start_timeout: child '${record.childId}' did not become supervised${detail ? `: ${detail}` : "."}`, "bridge");
238
279
  }
239
- async settleChild(record, status, exitCode, failure) {
280
+ async settleChild(record, status, exitCode, failure, result) {
240
281
  record.status = status;
241
282
  record.settledAt = new Date().toISOString();
242
283
  record.exitCode = exitCode;
243
- record.failure = failure === undefined ? undefined : boundedFailure(failure);
284
+ record.failure = failure === undefined
285
+ ? undefined
286
+ : boundedFailure(failure, record.failureRedactionValues);
287
+ if (result?.peakMemoryBytes !== undefined
288
+ && result.peakMemoryBytes > 0
289
+ && result.memorySampleCount !== undefined
290
+ && result.memorySampleCount > 0
291
+ && result.memorySource
292
+ && result.memoryObservedAt) {
293
+ record.peakMemoryBytes = result.peakMemoryBytes;
294
+ record.memorySampleCount = result.memorySampleCount;
295
+ record.memorySource = result.memorySource;
296
+ record.memoryObservedAt = result.memoryObservedAt;
297
+ }
244
298
  await this.persist().catch(error => {
245
299
  this.persistenceFailure ??= error;
246
300
  this.status = "degraded";
247
301
  });
248
302
  }
303
+ async replaceLedger(temporary) {
304
+ for (let attempt = 1;; attempt += 1) {
305
+ try {
306
+ await this.replaceLedgerFile(temporary, this.ledgerPath);
307
+ return;
308
+ }
309
+ catch (error) {
310
+ const code = error.code;
311
+ if (attempt >= ledgerReplaceAttempts || !code || !transientLedgerReplaceCodes.has(code))
312
+ throw error;
313
+ await wait(this.ledgerReplaceRetryMs);
314
+ }
315
+ }
316
+ }
249
317
  assertFence(fencingToken) {
250
318
  if (fencingToken !== this.identity.fencingToken) {
251
319
  throw new Error("attempt_process_supervisor_fenced: fencing token does not own this attempt.");
@@ -264,7 +332,7 @@ export class AttemptProcessSupervisor {
264
332
  finally {
265
333
  await handle.close();
266
334
  }
267
- await fs.rename(temporary, this.ledgerPath);
335
+ await this.replaceLedger(temporary);
268
336
  }
269
337
  catch (error) {
270
338
  await fs.rm(temporary, { force: true }).catch(() => undefined);
@@ -310,29 +378,32 @@ function boundedInteger(value, minimum, maximum, name) {
310
378
  }
311
379
  return value;
312
380
  }
313
- function boundedFailure(error) {
314
- const message = error instanceof Error ? error.message : String(error);
381
+ function boundedFailure(error, exactSecrets = []) {
382
+ const message = sanitizeFailureError(error, exactSecrets).message;
315
383
  return message.replaceAll(/[\r\n\t]+/gu, " ").slice(0, 512);
316
384
  }
317
385
  async function childStartFailureDetail(record) {
318
386
  try {
319
387
  const result = await record.result;
320
- const stderr = boundedDiagnostic(result.stderr);
388
+ const stderr = boundedDiagnostic(result.stderr, record.failureRedactionValues);
321
389
  return stderr
322
390
  ? `exit ${result.exitCode}; stderr: ${stderr}`
323
391
  : `exit ${result.exitCode}`;
324
392
  }
325
393
  catch (error) {
326
- return boundedFailure(error);
394
+ return boundedFailure(error, record.failureRedactionValues);
327
395
  }
328
396
  }
329
- function boundedDiagnostic(value) {
330
- return value.replaceAll(/[\r\n\t]+/gu, " ").trim().slice(0, 512);
397
+ function boundedDiagnostic(value, exactSecrets = []) {
398
+ return sanitizeFailureError(value, exactSecrets).message
399
+ .replaceAll(/[\r\n\t]+/gu, " ")
400
+ .trim()
401
+ .slice(0, 512);
331
402
  }
332
403
  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:"));
404
+ return error instanceof ForgeFailureError
405
+ && (error.failureCode === "attempt_process_supervisor_start_failed"
406
+ || error.failureCode === "attempt_process_supervisor_start_timeout");
336
407
  }
337
408
  function cancellationError(signal) {
338
409
  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
+ }