@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 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 (223) hide show
  1. package/README.md +165 -1
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +9 -1
  5. package/dist/bridge/bridge.service.d.ts +129 -3
  6. package/dist/bridge/bridge.service.js +2427 -70
  7. package/dist/bridge/bridge.types.d.ts +218 -2
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +14 -2
  21. package/dist/cleanup/cleanup.service.js +496 -3
  22. package/dist/cleanup/cleanup.types.d.ts +10 -0
  23. package/dist/cleanup/index.d.ts +1 -1
  24. package/dist/cleanup/index.js +1 -1
  25. package/dist/cleanup/internals.d.ts +2 -2
  26. package/dist/cleanup/internals.js +1 -1
  27. package/dist/cli/cli.help.d.ts +2 -0
  28. package/dist/cli/cli.help.js +217 -0
  29. package/dist/cli/cli.service.d.ts +55 -7
  30. package/dist/cli/cli.service.js +429 -219
  31. package/dist/cli/local-config.d.ts +7 -0
  32. package/dist/cli/local-config.js +51 -3
  33. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  34. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  35. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  36. package/dist/runtime/attempt-database-broker.js +390 -0
  37. package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
  38. package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
  39. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  40. package/dist/runtime/attempt-path-operation.js +94 -0
  41. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  42. package/dist/runtime/attempt-private-directory.js +54 -0
  43. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  44. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  45. package/dist/runtime/attempt-process-supervisor.d.ts +102 -0
  46. package/dist/runtime/attempt-process-supervisor.js +421 -0
  47. package/dist/runtime/canonical-source-file.d.ts +3 -0
  48. package/dist/runtime/canonical-source-file.js +54 -0
  49. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  50. package/dist/runtime/docker-engine-probe.js +333 -0
  51. package/dist/runtime/generated-output-reset.d.ts +20 -0
  52. package/dist/runtime/generated-output-reset.js +157 -0
  53. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  54. package/dist/runtime/host-database-capability-boundary.js +62 -0
  55. package/dist/runtime/host-diagnostics.d.ts +11 -0
  56. package/dist/runtime/host-diagnostics.js +192 -0
  57. package/dist/runtime/host-execution-root.d.ts +4 -0
  58. package/dist/runtime/host-execution-root.js +39 -0
  59. package/dist/runtime/host-listener-inspector.d.ts +51 -0
  60. package/dist/runtime/host-listener-inspector.js +385 -0
  61. package/dist/runtime/host-port-broker.d.ts +76 -0
  62. package/dist/runtime/host-port-broker.js +333 -0
  63. package/dist/runtime/host-runtime-contract.d.ts +58 -0
  64. package/dist/runtime/host-runtime-contract.js +813 -0
  65. package/dist/runtime/host-runtime-lease.d.ts +217 -0
  66. package/dist/runtime/host-runtime-lease.js +2487 -0
  67. package/dist/runtime/host-runtime-promotion.d.ts +32 -0
  68. package/dist/runtime/host-runtime-promotion.js +206 -0
  69. package/dist/runtime/memory-capacity.d.ts +70 -0
  70. package/dist/runtime/memory-capacity.js +123 -0
  71. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  72. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  73. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  74. package/dist/runtime/node-host-runtime-adapter.js +286 -0
  75. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  76. package/dist/runtime/pinned-directory-process.js +61 -0
  77. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  78. package/dist/runtime/pinned-source-directory.js +94 -0
  79. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  80. package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
  81. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  82. package/dist/runtime/prepared-runtime-identity.js +218 -0
  83. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  84. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  85. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  86. package/dist/runtime/runtime-isolation-lease.js +161 -0
  87. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  88. package/dist/runtime/workspace-resource-estimator.js +174 -0
  89. package/dist/sandbox/command-runner.d.ts +4 -0
  90. package/dist/sandbox/command-runner.js +47 -0
  91. package/dist/sandbox/contract.d.ts +5 -0
  92. package/dist/sandbox/contract.js +276 -0
  93. package/dist/sandbox/event-budget.d.ts +11 -0
  94. package/dist/sandbox/event-budget.js +27 -0
  95. package/dist/sandbox/image-policy.d.ts +35 -0
  96. package/dist/sandbox/image-policy.js +247 -0
  97. package/dist/sandbox/index.d.ts +5 -0
  98. package/dist/sandbox/index.js +5 -0
  99. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  100. package/dist/sandbox/lease-watchdog.js +67 -0
  101. package/dist/sandbox/preflight.d.ts +84 -0
  102. package/dist/sandbox/preflight.js +521 -0
  103. package/dist/sandbox/process.d.ts +20 -0
  104. package/dist/sandbox/process.js +209 -0
  105. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  106. package/dist/sandbox/provider-auth/codex.js +212 -0
  107. package/dist/sandbox/repository-lock.d.ts +6 -0
  108. package/dist/sandbox/repository-lock.js +129 -0
  109. package/dist/sandbox/runtime.d.ts +302 -0
  110. package/dist/sandbox/runtime.js +2368 -0
  111. package/dist/sandbox/sandbox.types.d.ts +93 -0
  112. package/dist/sandbox/sandbox.types.js +1 -0
  113. package/dist/sandbox/smoke.d.ts +76 -0
  114. package/dist/sandbox/smoke.js +380 -0
  115. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  116. package/dist/sandbox/worker-supervisor.js +36 -0
  117. package/dist/sandbox/worker.d.ts +1 -0
  118. package/dist/sandbox/worker.js +225 -0
  119. package/dist/sandbox/workspace.service.d.ts +13 -0
  120. package/dist/sandbox/workspace.service.js +74 -0
  121. package/dist/session/managed-attempt-tools.d.ts +28 -0
  122. package/dist/session/managed-attempt-tools.js +1016 -0
  123. package/dist/session/managed-build-coordinator.d.ts +5 -0
  124. package/dist/session/managed-build-coordinator.js +106 -0
  125. package/dist/session/managed-build-receipt.d.ts +21 -0
  126. package/dist/session/managed-build-receipt.js +337 -0
  127. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  128. package/dist/session/managed-verification-timeouts.js +17 -0
  129. package/dist/session/phase-completion.artifacts.d.ts +27 -0
  130. package/dist/session/phase-completion.artifacts.js +829 -0
  131. package/dist/session/phase-completion.d.ts +53 -42
  132. package/dist/session/phase-completion.helpers.d.ts +117 -0
  133. package/dist/session/phase-completion.helpers.js +707 -0
  134. package/dist/session/phase-completion.js +125 -3007
  135. package/dist/session/phase-completion.prompts.d.ts +14 -0
  136. package/dist/session/phase-completion.prompts.js +233 -0
  137. package/dist/session/phase-completion.runtime.d.ts +49 -0
  138. package/dist/session/phase-completion.runtime.js +840 -0
  139. package/dist/session/phase-completion.schemas.d.ts +3 -0
  140. package/dist/session/phase-completion.schemas.js +859 -0
  141. package/dist/session/phase-completion.validation.d.ts +38 -0
  142. package/dist/session/phase-completion.validation.js +918 -0
  143. package/dist/session/pre-publication-validation.d.ts +75 -0
  144. package/dist/session/pre-publication-validation.js +661 -0
  145. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  146. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  147. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  148. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  149. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
  150. package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
  151. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  152. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  153. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  154. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  155. package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
  156. package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
  157. package/dist/session/provider-cli/provider-cli.process.js +874 -0
  158. package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
  159. package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
  160. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  161. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  162. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  163. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  164. package/dist/session/provider-cli/provider-cli.runtime.d.ts +22 -0
  165. package/dist/session/provider-cli/provider-cli.runtime.js +92 -0
  166. package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
  167. package/dist/session/provider-cli/provider-cli.service.js +1258 -2497
  168. package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
  169. package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
  170. package/dist/session/provider-cli/provider-output-state.js +817 -0
  171. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  172. package/dist/session/provider-cli/provider-usage.js +28 -5
  173. package/dist/session/session-redaction.d.ts +3 -0
  174. package/dist/session/session-redaction.js +448 -0
  175. package/dist/session/session.base.d.ts +6 -1
  176. package/dist/session/session.service.d.ts +8 -2
  177. package/dist/session/session.service.js +586 -59
  178. package/dist/session/session.types.d.ts +324 -3
  179. package/dist/session/session.types.js +939 -10
  180. package/dist/session/unreferenced-exports.d.ts +12 -0
  181. package/dist/session/unreferenced-exports.js +59 -0
  182. package/dist/shared/format/format-bytes.d.ts +1 -0
  183. package/dist/shared/format/format-bytes.js +9 -0
  184. package/dist/shared/git/git.js +2 -1
  185. package/dist/shared/path/path.d.ts +1 -0
  186. package/dist/shared/path/path.js +21 -1
  187. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  188. package/dist/shared/process/dotnet-toolchain.js +307 -0
  189. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  190. package/dist/shared/process/host-process-recovery.js +79 -0
  191. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  192. package/dist/shared/process/host-process-supervisor.js +331 -0
  193. package/dist/shared/process/process.d.ts +3 -0
  194. package/dist/shared/process/process.js +13 -7
  195. package/dist/shared/process/windows-launch.d.ts +8 -0
  196. package/dist/shared/process/windows-launch.js +41 -0
  197. package/dist/shared/provider-capabilities.d.ts +4 -0
  198. package/dist/shared/provider-capabilities.js +88 -0
  199. package/dist/shared/redaction/redaction.d.ts +1 -0
  200. package/dist/shared/redaction/redaction.js +47 -4
  201. package/dist/shared/time/monotonic.d.ts +1 -0
  202. package/dist/shared/time/monotonic.js +8 -0
  203. package/dist/version.d.ts +1 -1
  204. package/dist/version.js +1 -1
  205. package/dist/worktree/artifact-injector.d.ts +3 -2
  206. package/dist/worktree/artifact-injector.js +82 -10
  207. package/dist/worktree/dependency-warmer.d.ts +53 -1
  208. package/dist/worktree/dependency-warmer.js +619 -11
  209. package/dist/worktree/index.d.ts +2 -1
  210. package/dist/worktree/index.js +1 -0
  211. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  212. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  213. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  214. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  215. package/dist/worktree/worktree-paths.d.ts +4 -0
  216. package/dist/worktree/worktree-paths.js +30 -0
  217. package/dist/worktree/worktree.base.d.ts +3 -0
  218. package/dist/worktree/worktree.service.d.ts +16 -3
  219. package/dist/worktree/worktree.service.js +334 -54
  220. package/dist/worktree/worktree.types.d.ts +9 -1
  221. package/package.json +12 -7
  222. package/runtime/windows-job-supervisor.ps1 +722 -0
  223. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -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
+ }
@@ -0,0 +1,20 @@
1
+ import type { ProviderProcessHandlers, ProviderProcessRunRequest, ProviderProcessRunner, ProviderProcessRunResult } from "../session/provider-cli/provider-cli.process.js";
2
+ import { AttemptProcessSupervisor, type AttemptProcessSupervisorIdentity, type AttemptProcessSupervisorOptions } from "./attempt-process-supervisor.js";
3
+ /** Runner-local authority for active host attempt process coordinators. */
4
+ export declare class AttemptProcessSupervisorRegistry {
5
+ private readonly active;
6
+ register(identity: AttemptProcessSupervisorIdentity, runner: ProviderProcessRunner, options?: AttemptProcessSupervisorOptions): AttemptProcessSupervisor;
7
+ require(dispatchAttemptId: string, fencingToken: string): AttemptProcessSupervisor;
8
+ unregister(dispatchAttemptId: string, fencingToken: string): boolean;
9
+ has(dispatchAttemptId: string): boolean;
10
+ }
11
+ /**
12
+ * Routes managed-host provider commands through the attempt's named provider
13
+ * child while retaining the raw runner for Docker and unmanaged execution.
14
+ */
15
+ export declare class AttemptCoordinatedProviderProcessRunner implements ProviderProcessRunner {
16
+ private readonly registry;
17
+ private readonly rawRunner;
18
+ constructor(registry: AttemptProcessSupervisorRegistry, rawRunner: ProviderProcessRunner);
19
+ run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
20
+ }
@@ -0,0 +1,64 @@
1
+ import { AttemptProcessSupervisor, } from "./attempt-process-supervisor.js";
2
+ /** Runner-local authority for active host attempt process coordinators. */
3
+ export class AttemptProcessSupervisorRegistry {
4
+ active = new Map();
5
+ register(identity, runner, options = {}) {
6
+ const existing = this.active.get(identity.dispatchAttemptId);
7
+ if (existing) {
8
+ if (existing.fencingToken !== identity.fencingToken) {
9
+ throw new Error("attempt_process_registry_conflict: dispatch attempt is registered under a different fence.");
10
+ }
11
+ return existing.supervisor;
12
+ }
13
+ const supervisor = new AttemptProcessSupervisor(identity, runner, options);
14
+ this.active.set(identity.dispatchAttemptId, { fencingToken: identity.fencingToken, supervisor });
15
+ return supervisor;
16
+ }
17
+ require(dispatchAttemptId, fencingToken) {
18
+ const registered = this.active.get(dispatchAttemptId);
19
+ if (!registered) {
20
+ throw new Error("attempt_process_registry_missing: no process coordinator owns this dispatch attempt.");
21
+ }
22
+ if (registered.fencingToken !== fencingToken) {
23
+ throw new Error("attempt_process_registry_fenced: process coordinator fence does not match the dispatch.");
24
+ }
25
+ return registered.supervisor;
26
+ }
27
+ unregister(dispatchAttemptId, fencingToken) {
28
+ const registered = this.active.get(dispatchAttemptId);
29
+ if (!registered || registered.fencingToken !== fencingToken)
30
+ return false;
31
+ return this.active.delete(dispatchAttemptId);
32
+ }
33
+ has(dispatchAttemptId) {
34
+ return this.active.has(dispatchAttemptId);
35
+ }
36
+ }
37
+ /**
38
+ * Routes managed-host provider commands through the attempt's named provider
39
+ * child while retaining the raw runner for Docker and unmanaged execution.
40
+ */
41
+ export class AttemptCoordinatedProviderProcessRunner {
42
+ registry;
43
+ rawRunner;
44
+ constructor(registry, rawRunner) {
45
+ this.registry = registry;
46
+ this.rawRunner = rawRunner;
47
+ }
48
+ async run(request, handlers, signal) {
49
+ if (request.env?.FORGE_RUNTIME_MODE !== "host") {
50
+ return await this.rawRunner.run(request, handlers, signal);
51
+ }
52
+ const dispatchAttemptId = requiredEnvironment(request, "FORGE_DISPATCH_ATTEMPT_ID");
53
+ const fencingToken = requiredEnvironment(request, "FORGE_FENCING_TOKEN");
54
+ const supervisor = this.registry.require(dispatchAttemptId, fencingToken);
55
+ const child = await supervisor.startChildReady("provider", request, handlers, signal, { maxStartAttempts: request.stdin === undefined ? 1 : 3, retryDelayMs: 100 });
56
+ return await child.result;
57
+ }
58
+ }
59
+ function requiredEnvironment(request, name) {
60
+ const value = request.env?.[name]?.trim();
61
+ if (!value)
62
+ throw new Error(`attempt_process_registry_invalid: ${name} is required for managed-host execution.`);
63
+ return value;
64
+ }
@@ -0,0 +1,102 @@
1
+ import type { ProviderProcessHandlers, ProviderProcessRunRequest, ProviderProcessRunner, ProviderProcessRunResult } from "../session/provider-cli/provider-cli.process.js";
2
+ export type AttemptProcessChildStatus = "starting" | "running" | "stopping" | "exited" | "failed";
3
+ export type AttemptProcessSupervisorStatus = "active" | "stopping" | "stopped" | "degraded";
4
+ export interface AttemptProcessSupervisorIdentity {
5
+ readonly processSupervisorId: string;
6
+ readonly dispatchAttemptId: string;
7
+ readonly executionLeaseId: string;
8
+ readonly fencingToken: string;
9
+ readonly logDirectory: string;
10
+ }
11
+ export interface AttemptProcessChildLedgerEntry {
12
+ readonly name: string;
13
+ readonly generation: number;
14
+ readonly childId: string;
15
+ readonly processSupervisorId: string;
16
+ readonly metadataPath: string;
17
+ readonly status: AttemptProcessChildStatus;
18
+ readonly startedAt: string;
19
+ readonly settledAt?: string;
20
+ readonly exitCode?: number;
21
+ readonly failure?: string;
22
+ readonly peakMemoryBytes?: number;
23
+ readonly memorySampleCount?: number;
24
+ readonly memorySource?: string;
25
+ readonly memoryObservedAt?: string;
26
+ }
27
+ export interface AttemptProcessSupervisorLedger {
28
+ readonly schemaVersion: 1;
29
+ readonly attemptSupervisorId: string;
30
+ readonly dispatchAttemptId: string;
31
+ readonly executionLeaseId: string;
32
+ readonly status: AttemptProcessSupervisorStatus;
33
+ readonly updatedAt: string;
34
+ readonly children: readonly AttemptProcessChildLedgerEntry[];
35
+ }
36
+ export interface AttemptProcessChildHandle {
37
+ readonly name: string;
38
+ readonly generation: number;
39
+ readonly childId: string;
40
+ readonly processSupervisorId: string;
41
+ readonly metadataPath: string;
42
+ readonly ready: Promise<void>;
43
+ readonly result: Promise<ProviderProcessRunResult>;
44
+ stop(fencingToken: string): Promise<ProviderProcessRunResult>;
45
+ }
46
+ export interface AttemptProcessSupervisorOptions {
47
+ readonly metadataReadyTimeoutMs?: number;
48
+ readonly signal?: AbortSignal;
49
+ readonly replaceLedgerFile?: (temporaryPath: string, ledgerPath: string) => Promise<void>;
50
+ readonly ledgerReplaceRetryMs?: number;
51
+ }
52
+ export interface AttemptProcessChildReadyOptions {
53
+ readonly maxStartAttempts?: number;
54
+ readonly retryDelayMs?: number;
55
+ }
56
+ /**
57
+ * Owns the independently supervised process trees that belong to one host
58
+ * dispatch attempt. Each named child gets a distinct Windows Job Object or
59
+ * Unix process group through the existing process runner, while this
60
+ * coordinator supplies shared fencing, cancellation, and a durable ledger.
61
+ */
62
+ export declare class AttemptProcessSupervisor {
63
+ private readonly runner;
64
+ readonly ledgerPath: string;
65
+ private readonly identity;
66
+ private readonly metadataReadyTimeoutMs;
67
+ private readonly replaceLedgerFile;
68
+ private readonly ledgerReplaceRetryMs;
69
+ private readonly records;
70
+ private readonly currentByName;
71
+ private readonly attemptController;
72
+ private status;
73
+ private persistTail;
74
+ private persistenceFailure;
75
+ private stopOperation;
76
+ constructor(identity: AttemptProcessSupervisorIdentity, runner: ProviderProcessRunner, options?: AttemptProcessSupervisorOptions);
77
+ startChild(name: string, request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
78
+ /**
79
+ * Starts a child and proves that the OS process tree is supervised before
80
+ * returning it to the caller. A launch that settles before producing its
81
+ * provider-ready supervision record is safe to retry: the raw runner only
82
+ * persists that record after the actual provider child has started, before
83
+ * Forge announces the provider as active.
84
+ */
85
+ startChildReady(name: string, request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal, options?: AttemptProcessChildReadyOptions): Promise<AttemptProcessChildHandle>;
86
+ stopChild(name: string, generation: number, fencingToken: string, reason?: unknown): Promise<ProviderProcessRunResult>;
87
+ stopAll(fencingToken: string, reason?: unknown): Promise<void>;
88
+ private performStopAll;
89
+ snapshot(): AttemptProcessSupervisorLedger;
90
+ providerMemoryTelemetry(): AttemptProcessTreeMemoryTelemetry | null;
91
+ private observeChildReady;
92
+ private settleChild;
93
+ private replaceLedger;
94
+ private assertFence;
95
+ private persist;
96
+ }
97
+ export interface AttemptProcessTreeMemoryTelemetry {
98
+ readonly peakMemoryBytes: number;
99
+ readonly sampleCount: number;
100
+ readonly source: string;
101
+ readonly observedAt: string;
102
+ }
@@ -0,0 +1,421 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { ForgeFailureError, sanitizeFailureError } from "../session/session.types.js";
5
+ import { monotonicNowMs } from "../shared/time/monotonic.js";
6
+ const terminalChildStatuses = new Set(["exited", "failed"]);
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"]);
13
+ /**
14
+ * Owns the independently supervised process trees that belong to one host
15
+ * dispatch attempt. Each named child gets a distinct Windows Job Object or
16
+ * Unix process group through the existing process runner, while this
17
+ * coordinator supplies shared fencing, cancellation, and a durable ledger.
18
+ */
19
+ export class AttemptProcessSupervisor {
20
+ runner;
21
+ ledgerPath;
22
+ identity;
23
+ metadataReadyTimeoutMs;
24
+ replaceLedgerFile;
25
+ ledgerReplaceRetryMs;
26
+ records = [];
27
+ currentByName = new Map();
28
+ attemptController = new AbortController();
29
+ status = "active";
30
+ persistTail = Promise.resolve();
31
+ persistenceFailure;
32
+ stopOperation = null;
33
+ constructor(identity, runner, options = {}) {
34
+ this.runner = runner;
35
+ this.identity = validateIdentity(identity);
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;
39
+ this.ledgerPath = path.join(this.identity.logDirectory, "attempt-process-supervisor.json");
40
+ if (options.signal?.aborted) {
41
+ this.status = "stopping";
42
+ this.attemptController.abort(options.signal.reason);
43
+ }
44
+ else {
45
+ options.signal?.addEventListener("abort", () => {
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);
51
+ }, { once: true });
52
+ }
53
+ }
54
+ async startChild(name, request, handlers = {}, signal) {
55
+ assertChildName(name);
56
+ if (this.status !== "active" || this.attemptController.signal.aborted) {
57
+ throw new Error("attempt_process_supervisor_closed: the attempt no longer accepts child processes.");
58
+ }
59
+ if (signal?.aborted)
60
+ throw cancellationError(signal);
61
+ const existing = this.currentByName.get(name);
62
+ if (existing && !terminalChildStatuses.has(existing.status)) {
63
+ throw new Error(`attempt_process_supervisor_conflict: child '${name}' is already active.`);
64
+ }
65
+ const generation = (existing?.generation ?? 0) + 1;
66
+ const childId = `${name}:${generation}`;
67
+ const processSupervisorId = childProcessSupervisorId(this.identity.processSupervisorId, childId);
68
+ const metadataPath = path.join(this.identity.logDirectory, `process-supervisor.${name}.${generation}.json`);
69
+ const controller = new AbortController();
70
+ const combinedSignal = signal
71
+ ? AbortSignal.any([this.attemptController.signal, controller.signal, signal])
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;
75
+ const childRequest = {
76
+ ...executableRequest,
77
+ env: {
78
+ ...request.env,
79
+ FORGE_RUNTIME_MODE: "host",
80
+ FORGE_RESOURCE_LEASE_ID: this.identity.executionLeaseId,
81
+ FORGE_EXECUTION_LEASE_ID: this.identity.executionLeaseId,
82
+ FORGE_DISPATCH_ATTEMPT_ID: this.identity.dispatchAttemptId,
83
+ FORGE_FENCING_TOKEN: this.identity.fencingToken,
84
+ FORGE_LOG_DIR: this.identity.logDirectory,
85
+ FORGE_PROCESS_SUPERVISOR_ID: processSupervisorId,
86
+ FORGE_PROCESS_SUPERVISOR_METADATA_PATH: metadataPath,
87
+ FORGE_PROCESS_CHILD_ID: childId,
88
+ FORGE_PROCESS_CHILD_NAME: name,
89
+ FORGE_PROCESS_CHILD_GENERATION: String(generation),
90
+ },
91
+ };
92
+ const startedAt = new Date().toISOString();
93
+ const record = {
94
+ name,
95
+ generation,
96
+ childId,
97
+ processSupervisorId,
98
+ metadataPath,
99
+ controller,
100
+ failureRedactionValues,
101
+ startedAt,
102
+ status: "starting",
103
+ result: Promise.resolve({ exitCode: 1, stdout: "", stderr: "not started" }),
104
+ };
105
+ this.records.push(record);
106
+ this.currentByName.set(name, record);
107
+ const result = this.runner.run(childRequest, handlers, combinedSignal);
108
+ record.result = result;
109
+ void result.then(value => this.settleChild(record, "exited", value.exitCode, undefined, value), error => this.settleChild(record, "failed", undefined, error));
110
+ try {
111
+ await this.persist();
112
+ }
113
+ catch (error) {
114
+ controller.abort("Attempt process ledger could not be persisted.");
115
+ await result.catch(() => undefined);
116
+ throw error;
117
+ }
118
+ const ready = this.observeChildReady(record);
119
+ void ready.catch(() => undefined);
120
+ return {
121
+ name,
122
+ generation,
123
+ childId,
124
+ processSupervisorId,
125
+ metadataPath,
126
+ ready,
127
+ result,
128
+ stop: async (fencingToken) => await this.stopChild(name, generation, fencingToken),
129
+ };
130
+ }
131
+ /**
132
+ * Starts a child and proves that the OS process tree is supervised before
133
+ * returning it to the caller. A launch that settles before producing its
134
+ * provider-ready supervision record is safe to retry: the raw runner only
135
+ * persists that record after the actual provider child has started, before
136
+ * Forge announces the provider as active.
137
+ */
138
+ async startChildReady(name, request, handlers = {}, signal, options = {}) {
139
+ const maxStartAttempts = boundedInteger(options.maxStartAttempts ?? 1, 1, 5, "maxStartAttempts");
140
+ const retryDelayMs = boundedInteger(options.retryDelayMs ?? 100, 0, 5_000, "retryDelayMs");
141
+ let lastError;
142
+ for (let attempt = 1; attempt <= maxStartAttempts; attempt += 1) {
143
+ const child = await this.startChild(name, request, handlers, signal);
144
+ try {
145
+ await child.ready;
146
+ return child;
147
+ }
148
+ catch (error) {
149
+ lastError = error;
150
+ await child.result.catch(() => undefined);
151
+ if (!isRetryableStartFailure(error) || attempt === maxStartAttempts)
152
+ throw error;
153
+ if (signal?.aborted)
154
+ throw cancellationError(signal);
155
+ if (retryDelayMs > 0)
156
+ await wait(retryDelayMs * attempt);
157
+ }
158
+ }
159
+ throw lastError;
160
+ }
161
+ async stopChild(name, generation, fencingToken, reason = "Forge stopped the supervised child.") {
162
+ this.assertFence(fencingToken);
163
+ const record = this.currentByName.get(name);
164
+ if (!record || record.generation !== generation) {
165
+ throw new Error(`attempt_process_supervisor_stale: child '${name}' generation ${generation} is no longer current.`);
166
+ }
167
+ if (!terminalChildStatuses.has(record.status)) {
168
+ record.status = "stopping";
169
+ record.controller.abort(reason);
170
+ await this.persist();
171
+ }
172
+ return await record.result;
173
+ }
174
+ async stopAll(fencingToken, reason = "Forge stopped the supervised attempt.") {
175
+ this.assertFence(fencingToken);
176
+ // The durable terminal ledger is part of the stop operation's contract.
177
+ // Join an in-flight stop before inspecting terminal state so lease cleanup
178
+ // cannot remove the log directory underneath its final persistence.
179
+ if (this.stopOperation)
180
+ return await this.stopOperation;
181
+ if (this.status === "stopped")
182
+ return;
183
+ const operation = this.performStopAll(reason);
184
+ this.stopOperation = operation;
185
+ void operation.catch(() => undefined).finally(() => {
186
+ if (this.stopOperation === operation && this.status !== "stopped")
187
+ this.stopOperation = null;
188
+ });
189
+ return await this.stopOperation;
190
+ }
191
+ async performStopAll(reason) {
192
+ this.status = "stopping";
193
+ this.attemptController.abort(reason);
194
+ const active = [...this.currentByName.values()].filter(record => !terminalChildStatuses.has(record.status));
195
+ for (const record of active)
196
+ record.status = "stopping";
197
+ await this.persist();
198
+ const results = await Promise.allSettled(active.map(record => record.result));
199
+ const rejected = results.flatMap((result, index) => result.status === "rejected"
200
+ ? [sanitizeFailureError(result.reason, active[index].failureRedactionValues)]
201
+ : []);
202
+ const terminalStatus = rejected.length > 0 || this.persistenceFailure !== undefined ? "degraded" : "stopped";
203
+ await this.persist({
204
+ ...this.snapshot(),
205
+ status: terminalStatus,
206
+ updatedAt: new Date().toISOString(),
207
+ });
208
+ this.status = terminalStatus;
209
+ if (rejected.length > 0) {
210
+ throw new AggregateError(rejected, "attempt_process_supervisor_cleanup_degraded");
211
+ }
212
+ if (this.persistenceFailure !== undefined)
213
+ throw this.persistenceFailure;
214
+ }
215
+ snapshot() {
216
+ return {
217
+ schemaVersion: 1,
218
+ attemptSupervisorId: this.identity.processSupervisorId,
219
+ dispatchAttemptId: this.identity.dispatchAttemptId,
220
+ executionLeaseId: this.identity.executionLeaseId,
221
+ status: this.status,
222
+ updatedAt: new Date().toISOString(),
223
+ children: this.records.map(record => ({
224
+ name: record.name,
225
+ generation: record.generation,
226
+ childId: record.childId,
227
+ processSupervisorId: record.processSupervisorId,
228
+ metadataPath: record.metadataPath,
229
+ status: record.status,
230
+ startedAt: record.startedAt,
231
+ ...(record.settledAt ? { settledAt: record.settledAt } : {}),
232
+ ...(record.exitCode === undefined ? {} : { exitCode: record.exitCode }),
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 } : {}),
238
+ })),
239
+ };
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
+ }
260
+ async observeChildReady(record) {
261
+ const deadline = monotonicNowMs() + this.metadataReadyTimeoutMs;
262
+ while (monotonicNowMs() <= deadline) {
263
+ if (await fileExists(record.metadataPath)) {
264
+ if (record.status === "starting") {
265
+ record.status = "running";
266
+ await this.persist();
267
+ }
268
+ return;
269
+ }
270
+ if (terminalChildStatuses.has(record.status)) {
271
+ const detail = await childStartFailureDetail(record);
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");
273
+ }
274
+ await wait(25);
275
+ }
276
+ record.controller.abort("Supervised child metadata was not produced before the readiness deadline.");
277
+ const detail = await childStartFailureDetail(record);
278
+ throw new ForgeFailureError("attempt_process_supervisor_start_timeout", `attempt_process_supervisor_start_timeout: child '${record.childId}' did not become supervised${detail ? `: ${detail}` : "."}`, "bridge");
279
+ }
280
+ async settleChild(record, status, exitCode, failure, result) {
281
+ record.status = status;
282
+ record.settledAt = new Date().toISOString();
283
+ record.exitCode = exitCode;
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
+ }
298
+ await this.persist().catch(error => {
299
+ this.persistenceFailure ??= error;
300
+ this.status = "degraded";
301
+ });
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
+ }
317
+ assertFence(fencingToken) {
318
+ if (fencingToken !== this.identity.fencingToken) {
319
+ throw new Error("attempt_process_supervisor_fenced: fencing token does not own this attempt.");
320
+ }
321
+ }
322
+ persist(snapshot = this.snapshot()) {
323
+ const operation = this.persistTail.then(async () => {
324
+ await ensurePrivateDirectory(this.identity.logDirectory);
325
+ const temporary = `${this.ledgerPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
326
+ try {
327
+ const handle = await fs.open(temporary, "wx", 0o600);
328
+ try {
329
+ await handle.writeFile(`${JSON.stringify(snapshot)}\n`);
330
+ await handle.sync();
331
+ }
332
+ finally {
333
+ await handle.close();
334
+ }
335
+ await this.replaceLedger(temporary);
336
+ }
337
+ catch (error) {
338
+ await fs.rm(temporary, { force: true }).catch(() => undefined);
339
+ throw error;
340
+ }
341
+ });
342
+ this.persistTail = operation.catch(() => undefined);
343
+ return operation;
344
+ }
345
+ }
346
+ function validateIdentity(identity) {
347
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u.test(identity.processSupervisorId)) {
348
+ throw new Error("attempt_process_supervisor_invalid: process supervisor id is invalid.");
349
+ }
350
+ assertUuid(identity.dispatchAttemptId, "dispatch attempt");
351
+ assertUuid(identity.executionLeaseId, "execution lease");
352
+ assertUuid(identity.fencingToken, "fencing token");
353
+ if (!path.isAbsolute(identity.logDirectory)) {
354
+ throw new Error("attempt_process_supervisor_invalid: log directory must be absolute.");
355
+ }
356
+ return { ...identity, logDirectory: path.resolve(identity.logDirectory) };
357
+ }
358
+ function assertChildName(name) {
359
+ if (!/^[a-z][a-z0-9_-]{0,31}$/u.test(name)) {
360
+ throw new Error("attempt_process_supervisor_invalid: child name must be a safe lowercase identifier.");
361
+ }
362
+ }
363
+ function childProcessSupervisorId(attemptSupervisorId, childId) {
364
+ const digest = createHash("sha256")
365
+ .update(JSON.stringify([attemptSupervisorId, childId]))
366
+ .digest("hex")
367
+ .slice(0, 32);
368
+ return `host-${digest}`;
369
+ }
370
+ function assertUuid(value, label) {
371
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value)) {
372
+ throw new Error(`attempt_process_supervisor_invalid: ${label} id must be a UUID.`);
373
+ }
374
+ }
375
+ function boundedInteger(value, minimum, maximum, name) {
376
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
377
+ throw new Error(`attempt_process_supervisor_invalid: ${name} must be between ${minimum} and ${maximum}.`);
378
+ }
379
+ return value;
380
+ }
381
+ function boundedFailure(error, exactSecrets = []) {
382
+ const message = sanitizeFailureError(error, exactSecrets).message;
383
+ return message.replaceAll(/[\r\n\t]+/gu, " ").slice(0, 512);
384
+ }
385
+ async function childStartFailureDetail(record) {
386
+ try {
387
+ const result = await record.result;
388
+ const stderr = boundedDiagnostic(result.stderr, record.failureRedactionValues);
389
+ return stderr
390
+ ? `exit ${result.exitCode}; stderr: ${stderr}`
391
+ : `exit ${result.exitCode}`;
392
+ }
393
+ catch (error) {
394
+ return boundedFailure(error, record.failureRedactionValues);
395
+ }
396
+ }
397
+ function boundedDiagnostic(value, exactSecrets = []) {
398
+ return sanitizeFailureError(value, exactSecrets).message
399
+ .replaceAll(/[\r\n\t]+/gu, " ")
400
+ .trim()
401
+ .slice(0, 512);
402
+ }
403
+ function isRetryableStartFailure(error) {
404
+ return error instanceof ForgeFailureError
405
+ && (error.failureCode === "attempt_process_supervisor_start_failed"
406
+ || error.failureCode === "attempt_process_supervisor_start_timeout");
407
+ }
408
+ function cancellationError(signal) {
409
+ return new Error(String(signal.reason ?? "Supervised child launch was cancelled."));
410
+ }
411
+ async function ensurePrivateDirectory(directory) {
412
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
413
+ if (process.platform !== "win32")
414
+ await fs.chmod(directory, 0o700);
415
+ }
416
+ async function fileExists(target) {
417
+ return await fs.stat(target).then(() => true, () => false);
418
+ }
419
+ function wait(milliseconds) {
420
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
421
+ }
@@ -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>;