@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,62 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ export const hostDatabaseCapabilityEnvironmentNames = [
4
+ "FORGE_TEST_POSTGRES_ADMIN_CONNECTION_STRING",
5
+ "FORGE_TEST_POSTGRES_CONNECTION_STRING",
6
+ ];
7
+ const boundaryAttestationEnvironment = "FORGE_HOST_DATABASE_CAPABILITY_BOUNDARY";
8
+ const boundaryAttestationValue = "user_store_clear";
9
+ /**
10
+ * Fails before untrusted host work can start when a PostgreSQL administrator
11
+ * capability is reachable through either the process or Windows User scope.
12
+ * The default Windows probe returns only presence; it never reads the value
13
+ * into JavaScript or emits it in diagnostics.
14
+ */
15
+ export function assertHostDatabaseCapabilityBoundary(options = {}) {
16
+ const processEnvironment = options.processEnvironment ?? process.env;
17
+ const reserved = new Set(hostDatabaseCapabilityEnvironmentNames.map(name => name.toLowerCase()));
18
+ const processCapabilityPresent = Object.entries(processEnvironment).some(([name, value]) => reserved.has(name.toLowerCase()) && typeof value === "string" && value.trim().length > 0);
19
+ if (processCapabilityPresent) {
20
+ throw boundaryError("process");
21
+ }
22
+ if ((options.platform ?? process.platform) !== "win32")
23
+ return;
24
+ const inspectWindowsUserEnvironment = options.inspectWindowsUserEnvironment
25
+ ?? processEnvironment[boundaryAttestationEnvironment] === boundaryAttestationValue;
26
+ if (!inspectWindowsUserEnvironment)
27
+ return;
28
+ const windowsUserCapabilityPresent = options.windowsUserCapabilityPresent
29
+ ?? inspectWindowsUserCapabilityPresence;
30
+ if (windowsUserCapabilityPresent()) {
31
+ throw boundaryError("Windows User");
32
+ }
33
+ }
34
+ /** Establishes the production Bridge boundary and enables per-launch rechecks. */
35
+ export function establishHostDatabaseCapabilityBoundary() {
36
+ assertHostDatabaseCapabilityBoundary({ inspectWindowsUserEnvironment: true });
37
+ process.env[boundaryAttestationEnvironment] = boundaryAttestationValue;
38
+ }
39
+ function inspectWindowsUserCapabilityPresence() {
40
+ const systemRoot = process.env.SystemRoot?.trim() || process.env.WINDIR?.trim();
41
+ const executable = systemRoot
42
+ ? path.win32.join(systemRoot, "System32", "reg.exe")
43
+ : "reg.exe";
44
+ for (const name of hostDatabaseCapabilityEnvironmentNames) {
45
+ const result = spawnSync(executable, ["query", "HKCU\\Environment", "/v", name], {
46
+ stdio: "ignore",
47
+ timeout: 10_000,
48
+ windowsHide: true,
49
+ });
50
+ if (result.status === 0)
51
+ return true;
52
+ // reg.exe uses exit code 1 for a missing value. Any other result is an
53
+ // unverifiable boundary and must fail closed without exposing registry data.
54
+ if (result.status !== 1 || result.signal || result.error) {
55
+ throw new Error("host_database_capability_boundary_unverified: the Windows User environment could not be inspected safely.", { cause: result.error });
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ function boundaryError(scope) {
61
+ return new Error(`host_database_capability_forbidden: a PostgreSQL administrative capability is present in ${scope} scope; Forge-managed host execution is container-only until an isolated provisioning broker is available.`);
62
+ }
@@ -0,0 +1,11 @@
1
+ import type { PinnedDirectoryIdentity } from "./pinned-directory-process.js";
2
+ export interface HostDiagnosticsRetentionOptions {
3
+ /** Test-only shortening; production callers cannot extend the safety deadline. */
4
+ readonly deadlineMs?: number;
5
+ }
6
+ export declare class HostDiagnosticsBudgetError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ /** Runner-local evidence, never web-served; browser-profile, cache and temporary directories are excluded. */
10
+ export declare function retainHostDiagnostics(stateRoot: string, attemptRoot: string, attemptId: string, expectedAttemptRootIdentity: PinnedDirectoryIdentity | null, attemptOwnerRoot?: string, options?: HostDiagnosticsRetentionOptions): Promise<string>;
11
+ export declare function assertHostAttemptRootIdentity(attemptRoot: string, expected: PinnedDirectoryIdentity): Promise<void>;
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { readCanonicalSourceFile } from "./canonical-source-file.js";
5
+ import { assertCanonicalDirectory } from "./attempt-private-directory.js";
6
+ import { runAttemptArchiveOperation, runAttemptPathOperation } from "./attempt-path-operation.js";
7
+ import { readPinnedSourceDirectory } from "./pinned-source-directory.js";
8
+ import { compactHostAttemptId } from "./host-execution-root.js";
9
+ import { monotonicNowMs } from "../shared/time/monotonic.js";
10
+ const maximumEvidenceBytes = 200 * 1024 * 1024;
11
+ const diagnosticsRetentionDeadlineMs = 30_000;
12
+ const diagnosticsHelperTerminationConfirmationMs = 2_000;
13
+ export class HostDiagnosticsBudgetError extends Error {
14
+ constructor(message) {
15
+ super(`host_diagnostics_budget: ${message}`);
16
+ this.name = "HostDiagnosticsBudgetError";
17
+ }
18
+ }
19
+ /** Runner-local evidence, never web-served; browser-profile, cache and temporary directories are excluded. */
20
+ export async function retainHostDiagnostics(stateRoot, attemptRoot, attemptId, expectedAttemptRootIdentity, attemptOwnerRoot = stateRoot, options = {}) {
21
+ const requestedDeadlineMs = options.deadlineMs;
22
+ const configuredDeadlineMs = Math.max(1, Math.min(diagnosticsRetentionDeadlineMs, Number.isFinite(requestedDeadlineMs) && (requestedDeadlineMs ?? 0) > 0
23
+ ? Math.floor(requestedDeadlineMs)
24
+ : diagnosticsRetentionDeadlineMs));
25
+ const retentionDeadline = monotonicNowMs() + configuredDeadlineMs;
26
+ const identity = attemptId.replaceAll("-", "").toLowerCase();
27
+ const compactAttemptRoot = path.resolve(attemptOwnerRoot, "a", compactHostAttemptId(attemptId));
28
+ const legacyAttemptRoot = path.resolve(stateRoot, "host-attempts", identity);
29
+ if (!/^[0-9a-f]{32}$/u.test(identity)
30
+ || (path.resolve(attemptRoot) !== compactAttemptRoot
31
+ && path.resolve(attemptRoot) !== legacyAttemptRoot)) {
32
+ throw new Error("host_diagnostics_invalid_owner: refusing an unowned attempt path.");
33
+ }
34
+ const attemptStat = await withinRetentionDeadline(async () => await fs.lstat(attemptRoot).catch(error => {
35
+ if (error.code === "ENOENT")
36
+ return null;
37
+ throw error;
38
+ }), retentionDeadline, configuredDeadlineMs);
39
+ if (attemptStat) {
40
+ if (!expectedAttemptRootIdentity) {
41
+ throw new Error("host_diagnostics_invalid_owner: original attempt-root identity is unavailable.");
42
+ }
43
+ await withinRetentionDeadline(() => assertHostAttemptRootIdentity(attemptRoot, expectedAttemptRootIdentity), retentionDeadline, configuredDeadlineMs);
44
+ }
45
+ const target = path.join(stateRoot, "diagnostics", "host", identity);
46
+ const manifestPath = path.join(target, "manifest.json");
47
+ await withinRetentionDeadline(() => assertCanonicalDirectory(stateRoot), retentionDeadline, configuredDeadlineMs);
48
+ await createArchiveDirectory(target, retentionDeadline, configuredDeadlineMs);
49
+ const priorManifest = await withinRetentionDeadline(async () => await fs.lstat(manifestPath).catch(error => {
50
+ if (error.code === "ENOENT")
51
+ return null;
52
+ throw error;
53
+ }), retentionDeadline, configuredDeadlineMs);
54
+ if (priorManifest && (!priorManifest.isFile() || priorManifest.isSymbolicLink()))
55
+ throw new Error("host_diagnostics_unsafe_path: manifest is not an ordinary file.");
56
+ if (priorManifest && !attemptStat) {
57
+ const saved = JSON.parse((await withinRetentionDeadline(() => readCanonicalSourceFile(manifestPath, maximumEvidenceBytes), retentionDeadline, configuredDeadlineMs)).toString("utf8"));
58
+ if (saved.attemptId !== attemptId)
59
+ throw new Error("host_diagnostics_invalid_owner: archive identity mismatch.");
60
+ return target;
61
+ }
62
+ const files = [];
63
+ let bytes = 0;
64
+ let entries = 0;
65
+ let excludedGeneratedFiles = 0;
66
+ let excludedGeneratedBytes = 0n;
67
+ const readDirectory = async (directory, identity) => {
68
+ const timing = remainingHelperTiming(retentionDeadline, configuredDeadlineMs);
69
+ return await runWithinRetentionHelperDeadline(() => readPinnedSourceDirectory(directory, identity, timing.operationTimeoutMs, timing.terminationConfirmationMs), retentionDeadline, configuredDeadlineMs);
70
+ };
71
+ const walk = async (relative, entry) => {
72
+ if (++entries > 10000)
73
+ throw new HostDiagnosticsBudgetError("evidence inventory exceeds retention budget.");
74
+ const source = path.join(attemptRoot, relative);
75
+ if (entry.kind === "symlink")
76
+ throw new Error("host_diagnostics_unsafe_path: evidence contains a symlink.");
77
+ if (entry.kind === "directory") {
78
+ await createArchiveDirectory(path.join(target, relative), retentionDeadline, configuredDeadlineMs);
79
+ for (const child of await readDirectory(source, entry.identity)) {
80
+ await walk(path.join(relative, child.name), child);
81
+ }
82
+ return;
83
+ }
84
+ if (entry.kind !== "file")
85
+ throw new Error("host_diagnostics_unsafe_path: evidence is not an ordinary file.");
86
+ // .NET test result directories contain copied assemblies and symbols, often
87
+ // many times over. They are reproducible build products, not diagnostics.
88
+ if (relative.startsWith(`test-output${path.sep}`) && /\.(?:dll|pdb)$/iu.test(relative)) {
89
+ excludedGeneratedFiles += 1;
90
+ excludedGeneratedBytes += entry.size;
91
+ return;
92
+ }
93
+ if (files.length >= 5000 || entry.size > BigInt(maximumEvidenceBytes - bytes))
94
+ throw new HostDiagnosticsBudgetError("evidence exceeds retention budget; original attempt retained for recovery.");
95
+ const destination = path.join(target, relative);
96
+ const contents = await withinRetentionDeadline(() => readCanonicalSourceFile(source, maximumEvidenceBytes - bytes, entry.identity), retentionDeadline, configuredDeadlineMs);
97
+ bytes += contents.length;
98
+ const sha256 = createHash("sha256").update(contents).digest("hex");
99
+ remainingRetentionMs(retentionDeadline, configuredDeadlineMs);
100
+ await writeArchiveFile(destination, contents, sha256, retentionDeadline, configuredDeadlineMs);
101
+ files.push({ path: relative, bytes: contents.length, sha256 });
102
+ };
103
+ if (attemptStat) {
104
+ await withinRetentionDeadline(() => assertHostAttemptRootIdentity(attemptRoot, expectedAttemptRootIdentity), retentionDeadline, configuredDeadlineMs);
105
+ const rootEntries = await readDirectory(attemptRoot, expectedAttemptRootIdentity);
106
+ for (const directory of ["logs", "test-output"]) {
107
+ const entry = rootEntries.find(candidate => candidate.name === directory);
108
+ if (entry)
109
+ await walk(directory, entry);
110
+ }
111
+ }
112
+ // Only owned relative evidence paths and hashes go into the public-facing manifest.
113
+ const manifest = Buffer.from(JSON.stringify({
114
+ schemaVersion: 1,
115
+ attemptId,
116
+ retainedAt: new Date().toISOString(),
117
+ files,
118
+ excludedGeneratedFiles,
119
+ excludedGeneratedBytes: excludedGeneratedBytes.toString(),
120
+ }, null, 2));
121
+ await writeArchiveFile(manifestPath, manifest, createHash("sha256").update(manifest).digest("hex"), retentionDeadline, configuredDeadlineMs);
122
+ return target;
123
+ }
124
+ export async function assertHostAttemptRootIdentity(attemptRoot, expected) {
125
+ const current = await assertCanonicalDirectory(attemptRoot);
126
+ if (!current || current.dev !== expected.dev || current.ino !== expected.ino) {
127
+ throw new Error("host_diagnostics_invalid_owner: attempt-root identity changed.");
128
+ }
129
+ }
130
+ async function writeArchiveFile(target, bytes, sha256, retentionDeadline, configuredDeadlineMs) {
131
+ const parent = path.dirname(path.resolve(target));
132
+ const identity = (await withinRetentionDeadline(() => assertCanonicalDirectory(parent), retentionDeadline, configuredDeadlineMs));
133
+ const timing = remainingHelperTiming(retentionDeadline, configuredDeadlineMs);
134
+ await runWithinRetentionHelperDeadline(() => runAttemptArchiveOperation(parent, identity, { name: path.basename(target), bytes, sha256 }, timing.operationTimeoutMs, timing.terminationConfirmationMs), retentionDeadline, configuredDeadlineMs);
135
+ }
136
+ async function createArchiveDirectory(target, retentionDeadline, configuredDeadlineMs) {
137
+ const resolved = path.resolve(target);
138
+ if (await withinRetentionDeadline(() => assertCanonicalDirectory(resolved, true), retentionDeadline, configuredDeadlineMs))
139
+ return;
140
+ let parent = path.dirname(resolved);
141
+ let identity = await withinRetentionDeadline(() => assertCanonicalDirectory(parent, true), retentionDeadline, configuredDeadlineMs);
142
+ while (!identity) {
143
+ parent = path.dirname(parent);
144
+ identity = await withinRetentionDeadline(() => assertCanonicalDirectory(parent, true), retentionDeadline, configuredDeadlineMs);
145
+ }
146
+ const timing = remainingHelperTiming(retentionDeadline, configuredDeadlineMs);
147
+ await runWithinRetentionHelperDeadline(() => runAttemptPathOperation(parent, identity, { kind: "mkdir", segments: path.relative(parent, resolved).split(path.sep) }, timing.operationTimeoutMs, timing.terminationConfirmationMs), retentionDeadline, configuredDeadlineMs);
148
+ await withinRetentionDeadline(() => assertCanonicalDirectory(resolved), retentionDeadline, configuredDeadlineMs);
149
+ }
150
+ function remainingHelperTiming(retentionDeadline, configuredDeadlineMs) {
151
+ const remainingMs = remainingRetentionMs(retentionDeadline, configuredDeadlineMs);
152
+ const terminationConfirmationMs = Math.min(diagnosticsHelperTerminationConfirmationMs, Math.max(1, Math.floor(remainingMs / 4)));
153
+ const operationTimeoutMs = remainingMs - terminationConfirmationMs;
154
+ if (operationTimeoutMs <= 0)
155
+ throw diagnosticsDeadlineError(configuredDeadlineMs);
156
+ return { operationTimeoutMs, terminationConfirmationMs };
157
+ }
158
+ function remainingRetentionMs(retentionDeadline, configuredDeadlineMs) {
159
+ const remainingMs = retentionDeadline - monotonicNowMs();
160
+ if (remainingMs <= 0)
161
+ throw diagnosticsDeadlineError(configuredDeadlineMs);
162
+ return remainingMs;
163
+ }
164
+ async function withinRetentionDeadline(operation, retentionDeadline, configuredDeadlineMs) {
165
+ const remainingMs = remainingRetentionMs(retentionDeadline, configuredDeadlineMs);
166
+ let timer;
167
+ try {
168
+ return await Promise.race([
169
+ operation(),
170
+ new Promise((_resolve, reject) => {
171
+ timer = setTimeout(() => reject(diagnosticsDeadlineError(configuredDeadlineMs)), remainingMs);
172
+ }),
173
+ ]);
174
+ }
175
+ finally {
176
+ if (timer)
177
+ clearTimeout(timer);
178
+ }
179
+ }
180
+ async function runWithinRetentionHelperDeadline(operation, retentionDeadline, configuredDeadlineMs) {
181
+ try {
182
+ return await operation();
183
+ }
184
+ catch (error) {
185
+ if (monotonicNowMs() >= retentionDeadline)
186
+ throw diagnosticsDeadlineError(configuredDeadlineMs);
187
+ throw error;
188
+ }
189
+ }
190
+ function diagnosticsDeadlineError(configuredDeadlineMs) {
191
+ return new Error(`host_diagnostics_temporary_unavailable: diagnostics retention exceeded ${configuredDeadlineMs} ms.`);
192
+ }
@@ -0,0 +1,4 @@
1
+ /** Keep ephemeral host-attempt trees short and per-user. */
2
+ export declare function defaultHostExecutionRoot(configuredRoot?: string | undefined, platform?: NodeJS.Platform, homeDirectory?: string): string;
3
+ export declare function assertHostExecutionRoot(executionRoot: string, platform?: NodeJS.Platform): void;
4
+ export declare function compactHostAttemptId(dispatchAttemptId: string): string;
@@ -0,0 +1,39 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ const windowsPathBudget = 248;
4
+ /** Keep ephemeral host-attempt trees short and per-user. */
5
+ export function defaultHostExecutionRoot(configuredRoot = process.env.FORGE_SANDBOX_EXECUTION_ROOT, platform = process.platform, homeDirectory = os.homedir()) {
6
+ const platformPath = platform === "win32" ? path.win32 : path.posix;
7
+ const configured = configuredRoot?.trim();
8
+ const selected = configured
9
+ ? platformPath.resolve(configured)
10
+ : platformPath.resolve(homeDirectory, ".fgx");
11
+ assertHostExecutionRoot(selected, platform);
12
+ return selected;
13
+ }
14
+ export function assertHostExecutionRoot(executionRoot, platform = process.platform) {
15
+ const platformPath = platform === "win32" ? path.win32 : path.posix;
16
+ if (!platformPath.isAbsolute(executionRoot)) {
17
+ throw new Error("host_execution_root_invalid: FORGE_SANDBOX_EXECUTION_ROOT must be absolute.");
18
+ }
19
+ const resolved = platformPath.resolve(executionRoot);
20
+ if (resolved === platformPath.parse(resolved).root) {
21
+ throw new Error("host_execution_root_invalid: the execution root cannot be a filesystem root.");
22
+ }
23
+ if (platform !== "win32")
24
+ return;
25
+ // Model the deepest Forge-owned test-world plus Git's own bookkeeping. This
26
+ // rejects unsafe layouts before an agent starts instead of failing midway.
27
+ const projected = path.win32.join(resolved, "a", "f".repeat(20), "t", "p-i-0", "f".repeat(32), "forge-test-worlds", "f".repeat(32), `activation-commit-${"f".repeat(40)}`, ".git", "worktrees", "f".repeat(20), "locked");
28
+ if (projected.length > windowsPathBudget) {
29
+ throw new Error(`host_execution_path_budget_exceeded: '${resolved}' exceeds the safe Windows path budget by ${projected.length - windowsPathBudget} characters. `
30
+ + "Set FORGE_SANDBOX_EXECUTION_ROOT to a shorter per-user directory before starting the Bridge.");
31
+ }
32
+ }
33
+ export function compactHostAttemptId(dispatchAttemptId) {
34
+ const normalized = dispatchAttemptId.replaceAll("-", "").toLowerCase();
35
+ if (!/^[0-9a-f]{32}$/u.test(normalized)) {
36
+ throw new Error("host_execution_attempt_invalid: dispatch attempt must be a UUID.");
37
+ }
38
+ return normalized.slice(0, 20);
39
+ }
@@ -0,0 +1,51 @@
1
+ import type { AttemptProcessChildHandle } from "./attempt-process-supervisor.js";
2
+ import type { HostPortLeaseOwner, HostPortReservation } from "./host-port-broker.js";
3
+ import type { HostListenerOwnership } from "./node-host-runtime-adapter.js";
4
+ export interface HostInspectionCommandResult {
5
+ readonly exitCode: number;
6
+ readonly stdout: string;
7
+ readonly stderr: string;
8
+ readonly errorCode?: string;
9
+ readonly timedOut?: boolean;
10
+ readonly signal?: string;
11
+ }
12
+ export interface HostInspectionCommandRunner {
13
+ run(command: string, args: readonly string[]): Promise<HostInspectionCommandResult>;
14
+ }
15
+ /** Proves that a loopback listener belongs to one exact supervised child. */
16
+ export declare class HostListenerInspector {
17
+ private readonly commands;
18
+ private readonly platform;
19
+ private readonly retryDelayMs;
20
+ constructor(commands?: HostInspectionCommandRunner, platform?: NodeJS.Platform, retryDelayMs?: number);
21
+ inspect(child: AttemptProcessChildHandle, reservation: HostPortReservation, owner: HostPortLeaseOwner): Promise<HostListenerOwnership>;
22
+ /** Returns loopback TCP ports opened by one exact supervised child process tree. */
23
+ inspectOwnedLoopbackListeners(child: AttemptProcessChildHandle, owner: HostPortLeaseOwner): Promise<readonly number[]>;
24
+ /** Snapshots loopback TCP ports that predate a supervised child launch. */
25
+ inspectLoopbackListeners(): Promise<readonly number[]>;
26
+ private inspectWindows;
27
+ private inspectAllWindows;
28
+ private runWindowsAncestry;
29
+ private runWindowsNetstat;
30
+ private inspectUnix;
31
+ private inspectAllUnix;
32
+ }
33
+ export declare function parseWindowsLoopbackListeners(output: string): readonly {
34
+ pid: number;
35
+ port: number;
36
+ }[];
37
+ export declare function parseLsofLoopbackListeners(output: string): readonly {
38
+ pid: number;
39
+ port: number;
40
+ }[];
41
+ export declare function parseSsLoopbackListeners(output: string): readonly {
42
+ pid: number;
43
+ port: number;
44
+ }[];
45
+ export declare class ExecFileInspectionCommandRunner implements HostInspectionCommandRunner {
46
+ run(command: string, args: readonly string[]): Promise<HostInspectionCommandResult>;
47
+ }
48
+ export declare function parseWindowsListenerPids(output: string, port: number): readonly number[];
49
+ export declare function parseLsofPids(output: string): readonly number[];
50
+ export declare function parseSsPids(output: string, port: number): readonly number[];
51
+ export declare function parseProcessGroups(output: string): ReadonlyMap<number, number>;