@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.e7b62d8fdd33

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 (145) hide show
  1. package/README.md +83 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +55 -3
  4. package/dist/bridge/bridge.service.js +834 -87
  5. package/dist/bridge/bridge.types.d.ts +89 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +37 -3
  16. package/dist/cli/cli.service.d.ts +33 -2
  17. package/dist/cli/cli.service.js +209 -27
  18. package/dist/cli/local-config.d.ts +5 -0
  19. package/dist/cli/local-config.js +45 -1
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/preflight.d.ts +45 -1
  53. package/dist/sandbox/preflight.js +299 -34
  54. package/dist/sandbox/process.js +125 -40
  55. package/dist/sandbox/repository-lock.js +41 -3
  56. package/dist/sandbox/runtime.d.ts +66 -3
  57. package/dist/sandbox/runtime.js +670 -105
  58. package/dist/sandbox/sandbox.types.d.ts +10 -0
  59. package/dist/sandbox/smoke.d.ts +76 -0
  60. package/dist/sandbox/smoke.js +380 -0
  61. package/dist/sandbox/worker-supervisor.js +1 -1
  62. package/dist/sandbox/worker.js +2 -5
  63. package/dist/sandbox/workspace.service.d.ts +1 -0
  64. package/dist/sandbox/workspace.service.js +28 -5
  65. package/dist/session/managed-attempt-tools.d.ts +21 -0
  66. package/dist/session/managed-attempt-tools.js +912 -0
  67. package/dist/session/managed-build-coordinator.d.ts +5 -0
  68. package/dist/session/managed-build-coordinator.js +105 -0
  69. package/dist/session/managed-build-receipt.d.ts +21 -0
  70. package/dist/session/managed-build-receipt.js +335 -0
  71. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  72. package/dist/session/phase-completion.artifacts.js +233 -24
  73. package/dist/session/phase-completion.d.ts +12 -2
  74. package/dist/session/phase-completion.helpers.d.ts +5 -0
  75. package/dist/session/phase-completion.helpers.js +51 -10
  76. package/dist/session/phase-completion.js +14 -3
  77. package/dist/session/phase-completion.prompts.d.ts +3 -3
  78. package/dist/session/phase-completion.prompts.js +24 -8
  79. package/dist/session/phase-completion.runtime.d.ts +13 -1
  80. package/dist/session/phase-completion.runtime.js +189 -8
  81. package/dist/session/phase-completion.schemas.js +117 -4
  82. package/dist/session/phase-completion.validation.d.ts +4 -1
  83. package/dist/session/phase-completion.validation.js +153 -22
  84. package/dist/session/pre-publication-validation.d.ts +46 -0
  85. package/dist/session/pre-publication-validation.js +208 -0
  86. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  87. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  88. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  89. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  90. package/dist/session/provider-cli/provider-cli.deterministic.js +49 -1
  91. package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
  92. package/dist/session/provider-cli/provider-cli.process.js +463 -34
  93. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  94. package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
  95. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  96. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  97. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  98. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  99. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  100. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
  101. package/dist/session/provider-cli/provider-cli.service.js +524 -100
  102. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  103. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  104. package/dist/session/provider-cli/provider-output-state.js +91 -16
  105. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  106. package/dist/session/provider-cli/provider-usage.js +28 -5
  107. package/dist/session/session-redaction.d.ts +3 -0
  108. package/dist/session/session-redaction.js +38 -0
  109. package/dist/session/session.base.d.ts +6 -1
  110. package/dist/session/session.service.d.ts +8 -2
  111. package/dist/session/session.service.js +522 -41
  112. package/dist/session/session.types.d.ts +72 -0
  113. package/dist/session/session.types.js +74 -3
  114. package/dist/shared/format/format-bytes.d.ts +1 -0
  115. package/dist/shared/format/format-bytes.js +9 -0
  116. package/dist/shared/path/path.d.ts +1 -0
  117. package/dist/shared/path/path.js +21 -1
  118. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  119. package/dist/shared/process/dotnet-toolchain.js +179 -0
  120. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  121. package/dist/shared/process/host-process-recovery.js +78 -0
  122. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  123. package/dist/shared/process/host-process-supervisor.js +330 -0
  124. package/dist/shared/process/process.d.ts +1 -0
  125. package/dist/shared/process/process.js +1 -0
  126. package/dist/shared/redaction/redaction.js +12 -2
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/worktree/artifact-injector.d.ts +3 -2
  130. package/dist/worktree/artifact-injector.js +43 -4
  131. package/dist/worktree/dependency-warmer.d.ts +28 -1
  132. package/dist/worktree/dependency-warmer.js +308 -9
  133. package/dist/worktree/index.d.ts +2 -1
  134. package/dist/worktree/index.js +1 -0
  135. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  136. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  137. package/dist/worktree/worktree-paths.d.ts +4 -0
  138. package/dist/worktree/worktree-paths.js +30 -0
  139. package/dist/worktree/worktree.base.d.ts +3 -0
  140. package/dist/worktree/worktree.service.d.ts +13 -2
  141. package/dist/worktree/worktree.service.js +238 -47
  142. package/dist/worktree/worktree.types.d.ts +5 -1
  143. package/package.json +5 -3
  144. package/runtime/windows-job-supervisor.ps1 +444 -0
  145. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,12 +1,18 @@
1
+ import { type DotNetToolchainSelection } from "../shared/process/dotnet-toolchain.js";
1
2
  import type { BridgeService } from "../bridge/bridge.base.js";
2
3
  import type { BridgeServiceConfig } from "../bridge/bridge.types.js";
4
+ import type { CleanupMode } from "../cleanup/cleanup.types.js";
5
+ import type { CleanupService } from "../cleanup/cleanup.base.js";
3
6
  import type { SessionService } from "../session/session.base.js";
4
7
  import type { CliIo, CliService } from "./cli.base.js";
8
+ type DotNetToolchainConfigurator = (repoRoots: readonly string[]) => Promise<DotNetToolchainSelection | null>;
5
9
  export declare class ForgeBridgeCliService implements CliService {
6
10
  private readonly sessions;
7
11
  private readonly bridgeFactory;
8
12
  private readonly pairRunner;
9
- constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient);
13
+ private readonly cleanupFactory;
14
+ private readonly dotNetToolchainConfigurator;
15
+ constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient, cleanupFactory?: CleanupServiceFactory, dotNetToolchainConfigurator?: DotNetToolchainConfigurator);
10
16
  run(argv: readonly string[], io?: CliIo): Promise<number>;
11
17
  }
12
18
  export declare function parseArgs(argv: readonly string[]): {
@@ -34,6 +40,14 @@ export declare function parseArgs(argv: readonly string[]): {
34
40
  readonly ok: true;
35
41
  readonly command: "remove-repo";
36
42
  readonly label: string;
43
+ } | {
44
+ readonly ok: true;
45
+ readonly command: "server-show";
46
+ readonly config: BridgeServiceConfig;
47
+ } | {
48
+ readonly ok: true;
49
+ readonly command: "server-set";
50
+ readonly serverUrl: string;
37
51
  } | {
38
52
  readonly ok: true;
39
53
  readonly command: "status";
@@ -54,9 +68,26 @@ export declare function parseArgs(argv: readonly string[]): {
54
68
  readonly command: "sandbox-doctor";
55
69
  readonly repoPath: string;
56
70
  readonly requirePinnedBase: boolean;
71
+ } | {
72
+ readonly ok: true;
73
+ readonly command: "sandbox-smoke";
74
+ readonly repoPath: string;
75
+ readonly reportPath?: string;
76
+ readonly requirePinnedBase: boolean;
77
+ readonly probeLoopbackPort: boolean;
57
78
  } | {
58
79
  readonly ok: true;
59
80
  readonly command: "sandbox-reap";
81
+ } | {
82
+ readonly ok: true;
83
+ readonly command: "sandbox-on";
84
+ readonly allowOpenPublic: boolean;
85
+ } | {
86
+ readonly ok: true;
87
+ readonly command: "sandbox-off";
88
+ } | {
89
+ readonly ok: true;
90
+ readonly command: "sandbox-status";
60
91
  } | {
61
92
  readonly ok: false;
62
93
  readonly error: string;
@@ -78,5 +109,5 @@ interface PairRunnerClaim {
78
109
  type PairRunnerClient = (args: PairArgs) => Promise<PairRunnerClaim>;
79
110
  export declare function parseCodexModelCatalog(raw: string): string[];
80
111
  export declare function parseCursorModelList(raw: string): string[];
81
- type CleanupMode = "stale" | "merged" | "all";
112
+ type CleanupServiceFactory = (stateRoot: string) => CleanupService;
82
113
  export {};
@@ -6,15 +6,19 @@ import { createInterface } from "node:readline/promises";
6
6
  import { promisify } from "node:util";
7
7
  import { resolveCursorAgentDirectLaunch } from "../shared/cursor-agent/cursor-agent.launch.js";
8
8
  import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
9
+ import { configureDotNetToolchain } from "../shared/process/dotnet-toolchain.js";
9
10
  import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
10
- import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
11
+ import { redactText } from "../shared/redaction/redaction.js";
11
12
  import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
12
13
  import { forgeBridgeVersion } from "../version.js";
13
- import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
14
+ import { ManagedWorktreeCleanupService } from "../cleanup/cleanup.service.js";
15
+ import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, setSandboxPolicy, setServerUrl, writeLocalConfig } from "./local-config.js";
14
16
  import { BridgeSessionService } from "../session/session.service.js";
17
+ import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "../session/session-redaction.js";
15
18
  import { validateDockerfilePolicy, validateSandboxContract } from "../sandbox/contract.js";
16
19
  import { DockerPreflight } from "../sandbox/preflight.js";
17
20
  import { DockerSandboxRuntime } from "../sandbox/runtime.js";
21
+ import { DockerSandboxSmoke } from "../sandbox/smoke.js";
18
22
  import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
19
23
  import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
20
24
  import { commandHelpText, helpText } from "./cli.help.js";
@@ -24,10 +28,14 @@ export class ForgeBridgeCliService {
24
28
  sessions;
25
29
  bridgeFactory;
26
30
  pairRunner;
27
- constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner) {
31
+ cleanupFactory;
32
+ dotNetToolchainConfigurator;
33
+ constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot), dotNetToolchainConfigurator = configureDotNetToolchain) {
28
34
  this.sessions = sessions;
29
35
  this.bridgeFactory = bridgeFactory;
30
36
  this.pairRunner = pairRunner;
37
+ this.cleanupFactory = cleanupFactory;
38
+ this.dotNetToolchainConfigurator = dotNetToolchainConfigurator;
31
39
  }
32
40
  async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
33
41
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
@@ -62,11 +70,29 @@ export class ForgeBridgeCliService {
62
70
  io.stderr.write(`forge-bridge connect requires ${missing[0]}.\n`);
63
71
  return 2;
64
72
  }
73
+ let dotNetToolchain;
74
+ try {
75
+ dotNetToolchain = await this.dotNetToolchainConfigurator([
76
+ ...config.repoMappings.map((mapping) => mapping.localPath?.trim()),
77
+ config.defaultRepoRoot.trim(),
78
+ ].filter((repoRoot) => Boolean(repoRoot)));
79
+ }
80
+ catch (error) {
81
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
82
+ return 1;
83
+ }
84
+ if (dotNetToolchain) {
85
+ io.stdout.write(`Using .NET SDK ${dotNetToolchain.sdkVersion}: ${dotNetToolchain.executablePath}\n`);
86
+ }
65
87
  const controller = new AbortController();
66
88
  const stop = () => controller.abort("signal received");
67
89
  process.once("SIGINT", stop);
68
90
  process.once("SIGTERM", stop);
69
91
  try {
92
+ if (config.cleanupWorktrees && config.repoMappings.some(mapping => mapping.localPath?.trim())) {
93
+ io.stdout.write("Cleaning stale managed worktrees before accepting dispatches.\n");
94
+ await cleanupRepos(config, "stale", true, this.cleanupFactory(config.sandboxStateRoot), io);
95
+ }
70
96
  io.stdout.write(connectStartupText(config));
71
97
  await this.bridgeFactory(config).run(controller.signal);
72
98
  return 0;
@@ -122,12 +148,38 @@ export class ForgeBridgeCliService {
122
148
  return 2;
123
149
  }
124
150
  }
151
+ if (args.command === "server-show") {
152
+ const saved = readLocalConfigSync().serverUrl ?? "not configured";
153
+ io.stdout.write(`Saved server: ${saved}\n`);
154
+ if (process.env.FORGE_URL?.trim()) {
155
+ io.stdout.write(`Effective server: ${process.env.FORGE_URL.trim()} (FORGE_URL override)\n`);
156
+ }
157
+ else {
158
+ io.stdout.write(`Effective server: ${args.config.serverUrl || "not configured"}\n`);
159
+ }
160
+ return 0;
161
+ }
162
+ if (args.command === "server-set") {
163
+ try {
164
+ const config = await setServerUrl(args.serverUrl);
165
+ io.stdout.write(`Saved Forge server: ${config.serverUrl}\n`);
166
+ io.stdout.write("Existing runner credentials were retained. Re-run `forge-bridge pair --server <url>` if this is a different Forge deployment.\n");
167
+ if (process.env.FORGE_URL?.trim()) {
168
+ io.stdout.write(`Warning: FORGE_URL is set to ${process.env.FORGE_URL.trim()} and overrides the saved server.\n`);
169
+ }
170
+ return 0;
171
+ }
172
+ catch (error) {
173
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
174
+ return 2;
175
+ }
176
+ }
125
177
  if (args.command === "status") {
126
178
  await writeStatus(args.config, args.checkUpdates, io);
127
179
  return 0;
128
180
  }
129
181
  if (args.command === "cleanup") {
130
- return await cleanupRepos(args.config, args.mode, args.force, io);
182
+ return await cleanupRepos(args.config, args.mode, args.force, this.cleanupFactory(args.config.sandboxStateRoot), io);
131
183
  }
132
184
  if (args.command === "sandbox-init") {
133
185
  return await initializeSandbox(args.repoPath, io);
@@ -135,9 +187,28 @@ export class ForgeBridgeCliService {
135
187
  if (args.command === "sandbox-doctor") {
136
188
  return await diagnoseSandbox(args.repoPath, args.requirePinnedBase, io);
137
189
  }
190
+ if (args.command === "sandbox-smoke") {
191
+ return await smokeSandbox(args.repoPath, args.reportPath, args.requirePinnedBase, args.probeLoopbackPort, io);
192
+ }
138
193
  if (args.command === "sandbox-reap") {
139
194
  return await reapSandboxes(io);
140
195
  }
196
+ if (args.command === "sandbox-on") {
197
+ const saved = await setSandboxPolicy(true, args.allowOpenPublic);
198
+ io.stdout.write("Saved local Docker sandbox policy. Restart the bridge to apply it.\n");
199
+ writeSandboxPolicyStatus(saved, io);
200
+ return 0;
201
+ }
202
+ if (args.command === "sandbox-off") {
203
+ const saved = await setSandboxPolicy(false, false);
204
+ io.stdout.write("Disabled local Docker sandbox placement. Restart the bridge to apply it.\n");
205
+ writeSandboxPolicyStatus(saved, io);
206
+ return 0;
207
+ }
208
+ if (args.command === "sandbox-status") {
209
+ writeSandboxPolicyStatus(readLocalConfigSync(), io);
210
+ return 0;
211
+ }
141
212
  if (args.command === "mcp") {
142
213
  const parsed = parseCompletionMcpArgs(args.argv);
143
214
  if (!parsed.ok) {
@@ -156,12 +227,12 @@ export class ForgeBridgeCliService {
156
227
  emit: async (event) => {
157
228
  const line = {
158
229
  type: "runner_event",
159
- event: redactSecrets(event, redactionSecrets)
230
+ event: redactRunnerDispatchEvent(event, redactionSecrets)
160
231
  };
161
232
  io.stdout.write(`${JSON.stringify(line)}\n`);
162
233
  }
163
234
  });
164
- await writeJsonAtomic(args.resultPath, redactSecrets(result, redactionSecrets));
235
+ await writeJsonAtomic(args.resultPath, redactRunOnceResultPacket(result, redactionSecrets));
165
236
  return result.exitCode;
166
237
  }
167
238
  catch (error) {
@@ -247,6 +318,15 @@ export function parseArgs(argv) {
247
318
  }
248
319
  return { ok: true, command: "remove-repo", label: argv[1] };
249
320
  }
321
+ if (argv[0] === "server") {
322
+ if (argv.length === 1 || (argv[1] === "show" && argv.length === 2)) {
323
+ return { ok: true, command: "server-show", config: configuredBridgeServiceConfig() };
324
+ }
325
+ if (argv[1] === "set" && argv[2] && argv.length === 3) {
326
+ return { ok: true, command: "server-set", serverUrl: argv[2] };
327
+ }
328
+ return { ok: false, error: "Usage: forge-bridge server <show|set <url>>" };
329
+ }
250
330
  if (argv[0] === "status") {
251
331
  let checkUpdates = false;
252
332
  for (let index = 1; index < argv.length; index += 1) {
@@ -301,8 +381,26 @@ export function parseArgs(argv) {
301
381
  }
302
382
  function parseSandboxArgs(argv) {
303
383
  const action = argv[1];
384
+ if (action === "on") {
385
+ let allowOpenPublic = false;
386
+ for (let index = 2; index < argv.length; index += 1) {
387
+ if (argv[index] === "--allow-open-public") {
388
+ allowOpenPublic = true;
389
+ }
390
+ else {
391
+ return { ok: false, error: `Unknown sandbox argument: ${argv[index]}` };
392
+ }
393
+ }
394
+ return { ok: true, command: "sandbox-on", allowOpenPublic };
395
+ }
396
+ if (action === "off" && argv.length === 2)
397
+ return { ok: true, command: "sandbox-off" };
398
+ if (action === "status" && argv.length === 2)
399
+ return { ok: true, command: "sandbox-status" };
304
400
  let repoPath = process.cwd();
401
+ let reportPath;
305
402
  let requirePinnedBase = false;
403
+ let probeLoopbackPort = false;
306
404
  for (let index = 2; index < argv.length; index += 1) {
307
405
  const arg = argv[index];
308
406
  if (arg === "--repo") {
@@ -310,9 +408,17 @@ function parseSandboxArgs(argv) {
310
408
  if (!repoPath)
311
409
  return { ok: false, error: "--repo requires a path." };
312
410
  }
313
- else if (arg === "--require-pinned-base" && action === "doctor") {
411
+ else if (arg === "--report" && action === "smoke") {
412
+ reportPath = argv[++index];
413
+ if (!reportPath)
414
+ return { ok: false, error: "--report requires a path." };
415
+ }
416
+ else if (arg === "--require-pinned-base" && (action === "doctor" || action === "smoke")) {
314
417
  requirePinnedBase = true;
315
418
  }
419
+ else if (arg === "--probe-loopback-port" && action === "smoke") {
420
+ probeLoopbackPort = true;
421
+ }
316
422
  else {
317
423
  return { ok: false, error: `Unknown sandbox argument: ${arg}` };
318
424
  }
@@ -321,9 +427,18 @@ function parseSandboxArgs(argv) {
321
427
  return { ok: true, command: "sandbox-init", repoPath: path.resolve(repoPath) };
322
428
  if (action === "doctor")
323
429
  return { ok: true, command: "sandbox-doctor", repoPath: path.resolve(repoPath), requirePinnedBase };
430
+ if (action === "smoke")
431
+ return {
432
+ ok: true,
433
+ command: "sandbox-smoke",
434
+ repoPath: path.resolve(repoPath),
435
+ reportPath: reportPath ? path.resolve(reportPath) : undefined,
436
+ requirePinnedBase,
437
+ probeLoopbackPort,
438
+ };
324
439
  if (action === "reap" && argv.length === 2)
325
440
  return { ok: true, command: "sandbox-reap" };
326
- return { ok: false, error: "Usage: forge-bridge sandbox <init|doctor|reap> [--repo <path>] [--require-pinned-base]" };
441
+ return { ok: false, error: "Usage: forge-bridge sandbox <on|off|status|init|doctor|smoke|reap> [--allow-open-public] [--repo <path>] [--report <path>] [--require-pinned-base] [--probe-loopback-port]" };
327
442
  }
328
443
  const sandboxContractTemplate = `schemaVersion: 1
329
444
  workingDirectory: /workspace
@@ -389,6 +504,10 @@ async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
389
504
  diskHighWatermarkPercent: config.diskHighWatermarkPercent,
390
505
  diskLowWatermarkPercent: config.diskLowWatermarkPercent,
391
506
  minimumWorkspaceBytes: config.minimumWorkspaceBytes,
507
+ hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
508
+ defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
509
+ defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
510
+ runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
392
511
  requireRootless: config.requireRootlessDocker
393
512
  });
394
513
  try {
@@ -409,6 +528,42 @@ async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
409
528
  return 1;
410
529
  }
411
530
  }
531
+ async function smokeSandbox(repoPath, reportPath, requirePinnedBase, probeLoopbackPort, io) {
532
+ try {
533
+ const result = await new DockerSandboxSmoke(defaultBridgeServiceConfig()).run({
534
+ repoPath,
535
+ reportPath,
536
+ requirePinnedBase,
537
+ probeLoopbackPort,
538
+ onProgress: (stage) => io.stdout.write(`Smoke: ${stage}...\n`),
539
+ });
540
+ const { report } = result;
541
+ if (report.repository.contractDigest)
542
+ io.stdout.write(`Contract: valid (${report.repository.contractDigest})\n`);
543
+ for (const warning of report.repository.policyWarnings)
544
+ io.stdout.write(`Warning: ${warning}\n`);
545
+ if (report.docker.runtimeVersion) {
546
+ io.stdout.write(`Docker: healthy (${report.docker.runtimeVersion}; capacity ${report.docker.capacity})\n`);
547
+ }
548
+ io.stdout.write(`Attempt: ${report.dispatchAttemptId}\n`);
549
+ io.stdout.write(`Resource lease: ${report.resourceLeaseId}\n`);
550
+ if (report.publishedPortProbe) {
551
+ io.stdout.write(`Loopback port: ${report.publishedPortProbe.hostIp}:${report.publishedPortProbe.hostPort} -> ${report.publishedPortProbe.containerPort}/tcp\n`);
552
+ }
553
+ io.stdout.write(`Cleanup: ${report.cleanup.verified ? "verified (zero exact-label resources)" : "failed"}\n`);
554
+ io.stdout.write(`Report: ${result.reportPath}\n`);
555
+ if (report.status === "passed") {
556
+ io.stdout.write("Sandbox smoke: PASS\n");
557
+ return 0;
558
+ }
559
+ io.stderr.write(`Sandbox smoke: FAIL (${report.failure?.code ?? "sandbox_smoke_failed"} at ${report.failure?.stage ?? "unknown"}): ${report.failure?.message ?? "unknown failure"}\n`);
560
+ return 1;
561
+ }
562
+ catch (error) {
563
+ io.stderr.write(`Sandbox smoke could not write its report: ${error instanceof Error ? error.message : String(error)}\n`);
564
+ return 1;
565
+ }
566
+ }
412
567
  async function reapSandboxes(io) {
413
568
  const config = defaultBridgeServiceConfig();
414
569
  if (!/^[0-9a-f-]{32,36}$/iu.test(config.runnerId)) {
@@ -423,6 +578,10 @@ async function reapSandboxes(io) {
423
578
  diskHighWatermarkPercent: config.diskHighWatermarkPercent,
424
579
  diskLowWatermarkPercent: config.diskLowWatermarkPercent,
425
580
  minimumWorkspaceBytes: config.minimumWorkspaceBytes,
581
+ hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
582
+ defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
583
+ defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
584
+ runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
426
585
  requireRootless: config.requireRootlessDocker
427
586
  });
428
587
  try {
@@ -576,7 +735,7 @@ function parsePairArgs(argv) {
576
735
  name: name?.trim() || defaultName,
577
736
  fingerprint: fingerprint?.trim() || `local-bridge:${os.hostname()}:${user}`,
578
737
  repoRoot: repoRoot?.trim() ? path.resolve(repoRoot) : undefined,
579
- cleanupWorktrees: cleanupWorktrees ?? localConfig.cleanupWorktrees ?? false,
738
+ cleanupWorktrees: cleanupWorktrees ?? defaultConfig.cleanupWorktrees,
580
739
  cliProviders: cliProviders.length > 0 ? cliProviders : localConfig.cliProviders && localConfig.cliProviders.length > 0 ? [...localConfig.cliProviders] : ["codex"],
581
740
  projectLabels: projectLabels.length > 0 ? projectLabels : localConfig.projectLabels ? [...localConfig.projectLabels] : []
582
741
  };
@@ -731,11 +890,28 @@ function connectStartupText(config) {
731
890
  ` repo folder: ${config.defaultRepoRoot || "not configured"}`,
732
891
  ` providers: ${formatList(config.cliProviders)}`,
733
892
  ` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
893
+ ` Docker Desktop auto-recovery: ${config.dockerDesktopRecoveryEnabled ? "enabled" : "disabled"}`,
894
+ ` open public sandbox networking: ${config.allowOpenPublicSandbox ? "enabled" : "disabled"}`,
734
895
  "",
735
896
  "Press Ctrl+C to stop.",
736
897
  "",
737
898
  ].join("\n");
738
899
  }
900
+ function writeSandboxPolicyStatus(local, io) {
901
+ const effective = configuredBridgeServiceConfig({}, local);
902
+ io.stdout.write(`Config: ${resolveLocalConfigPath()}\n`);
903
+ io.stdout.write(`Docker sandbox placement: ${effective.dockerEnabled ? "on" : "off"}\n`);
904
+ io.stdout.write(`Open public sandbox networking: ${effective.allowOpenPublicSandbox ? "allowed" : "denied"}\n`);
905
+ if (process.env.FORGE_DOCKER_SANDBOX !== undefined) {
906
+ io.stdout.write("Docker setting source: FORGE_DOCKER_SANDBOX override\n");
907
+ }
908
+ if (process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX !== undefined) {
909
+ io.stdout.write("Open public setting source: FORGE_ALLOW_OPEN_PUBLIC_SANDBOX override\n");
910
+ }
911
+ if (effective.allowOpenPublicSandbox) {
912
+ io.stdout.write("Warning: open_public permits unrestricted outbound network access from developer sandboxes.\n");
913
+ }
914
+ }
739
915
  function formatList(values) {
740
916
  return values.length > 0 ? values.join(", ") : "none";
741
917
  }
@@ -772,7 +948,7 @@ async function writeStatus(config, checkUpdates, io) {
772
948
  io.stdout.write("Update check: no update channel configured.\n");
773
949
  }
774
950
  }
775
- async function cleanupRepos(config, mode, force, io) {
951
+ async function cleanupRepos(config, mode, force, cleanup, io) {
776
952
  const repos = config.repoMappings.filter((mapping) => mapping.localPath?.trim());
777
953
  if (repos.length === 0) {
778
954
  io.stdout.write("No repos configured.\n");
@@ -781,20 +957,28 @@ async function cleanupRepos(config, mode, force, io) {
781
957
  let failures = 0;
782
958
  for (const repo of repos) {
783
959
  const repoPath = path.resolve(repo.localPath);
784
- const worktreeRoot = path.join(path.dirname(repoPath), ".forge-worktrees", path.basename(repoPath));
785
- if (!force) {
786
- const action = mode === "all" ? "remove" : "prune";
787
- io.stdout.write(`[dry-run] Would ${action} ${worktreeRoot}\n`);
788
- continue;
789
- }
790
960
  try {
791
- if (mode === "all") {
792
- await fs.rm(worktreeRoot, { recursive: true, force: true });
793
- await runGit(repoPath, ["worktree", "prune"]);
794
- io.stdout.write(`Removed ${worktreeRoot}\n`);
961
+ const result = await cleanup.run({
962
+ repoRoot: repoPath,
963
+ allowedRepoRoot: config.defaultRepoRoot || undefined,
964
+ mode,
965
+ force,
966
+ });
967
+ if (!force) {
968
+ const action = mode === "all"
969
+ ? "remove"
970
+ : mode === "stale"
971
+ ? `remove ${result.candidateWorktrees} stale managed ${result.candidateWorktrees === 1 ? "worktree" : "worktrees"} from`
972
+ : "prune";
973
+ io.stdout.write(`[dry-run] Would ${action} ${result.worktreeRoot}\n`);
974
+ }
975
+ else if (mode === "all") {
976
+ io.stdout.write(`Removed ${result.worktreeRoot}\n`);
977
+ }
978
+ else if (mode === "stale") {
979
+ io.stdout.write(`Removed ${result.removedWorktrees} stale managed ${result.removedWorktrees === 1 ? "worktree" : "worktrees"} from ${result.worktreeRoot}\n`);
795
980
  }
796
981
  else {
797
- await runGit(repoPath, ["worktree", "prune"]);
798
982
  io.stdout.write(`Pruned stale worktree metadata for ${repoPath}\n`);
799
983
  }
800
984
  }
@@ -943,8 +1127,8 @@ function uniqueModels(models) {
943
1127
  function isRecord(value) {
944
1128
  return typeof value === "object" && value !== null;
945
1129
  }
946
- async function withDetectedCliProviders(config) {
947
- const detected = await detectCliProviders(configuredProviderCandidates(config.cliProviders));
1130
+ async function withDetectedCliProviders(config, candidates = config.cliProviders) {
1131
+ const detected = await detectCliProviders(uniqueProviders(candidates));
948
1132
  const found = detected.filter((result) => result.found);
949
1133
  if (found.length === 0) {
950
1134
  return config;
@@ -968,6 +1152,7 @@ async function withDetectedCliProviders(config) {
968
1152
  };
969
1153
  }
970
1154
  async function withDetectedPairCliProviders(args) {
1155
+ const configured = configuredProviderCandidates(args.cliProviders);
971
1156
  const detectedConfig = await withDetectedCliProviders({
972
1157
  ...defaultBridgeServiceConfig({
973
1158
  serverUrl: args.serverUrl,
@@ -977,7 +1162,7 @@ async function withDetectedPairCliProviders(args) {
977
1162
  cleanupWorktrees: args.cleanupWorktrees,
978
1163
  defaultRepoRoot: args.repoRoot ?? defaultBridgeServiceConfig().defaultRepoRoot
979
1164
  })
980
- });
1165
+ }, configured);
981
1166
  return {
982
1167
  ...args,
983
1168
  cliProviders: detectedConfig.cliProviders
@@ -992,6 +1177,3 @@ function uniqueProviders(providers) {
992
1177
  .filter((provider) => provider.length > 0)
993
1178
  .filter((provider, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === provider.toLowerCase()) === index);
994
1179
  }
995
- async function runGit(cwd, args) {
996
- await execFileAsync("git", args, { cwd, timeout: 10_000 });
997
- }
@@ -7,6 +7,9 @@ export interface BridgeLocalConfig {
7
7
  readonly deviceName?: string;
8
8
  readonly defaultRepoRoot?: string;
9
9
  readonly cleanupWorktrees?: boolean;
10
+ readonly dockerEnabled?: boolean;
11
+ readonly dockerDesktopRecoveryEnabled?: boolean;
12
+ readonly allowOpenPublicSandbox?: boolean;
10
13
  readonly cliProviders?: readonly string[];
11
14
  readonly providerStatuses?: Readonly<Record<string, BridgeProviderStatus>>;
12
15
  readonly projectLabels?: readonly string[];
@@ -34,4 +37,6 @@ export declare function removeRepoMapping(label: string): Promise<{
34
37
  readonly config: BridgeLocalConfig;
35
38
  readonly removed: boolean;
36
39
  }>;
40
+ export declare function setServerUrl(serverUrl: string): Promise<BridgeLocalConfig>;
41
+ export declare function setSandboxPolicy(dockerEnabled: boolean, allowOpenPublicSandbox: boolean): Promise<BridgeLocalConfig>;
37
42
  export declare function providerStatusesFor(providers: readonly string[]): Readonly<Record<string, BridgeProviderStatus>>;
@@ -45,6 +45,15 @@ export function applyLocalConfigDefaults(config, local) {
45
45
  deviceName: process.env.FORGE_DEVICE_NAME ?? local.deviceName ?? config.deviceName,
46
46
  defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? local.defaultRepoRoot ?? config.defaultRepoRoot,
47
47
  cleanupWorktrees: local.cleanupWorktrees ?? config.cleanupWorktrees,
48
+ dockerEnabled: process.env.FORGE_DOCKER_SANDBOX === undefined
49
+ ? local.dockerEnabled ?? config.dockerEnabled
50
+ : config.dockerEnabled,
51
+ dockerDesktopRecoveryEnabled: process.env.FORGE_DOCKER_DESKTOP_AUTO_RECOVERY === undefined
52
+ ? local.dockerDesktopRecoveryEnabled ?? config.dockerDesktopRecoveryEnabled
53
+ : config.dockerDesktopRecoveryEnabled,
54
+ allowOpenPublicSandbox: process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX === undefined
55
+ ? local.allowOpenPublicSandbox ?? config.allowOpenPublicSandbox
56
+ : config.allowOpenPublicSandbox,
48
57
  cliProviders,
49
58
  providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
50
59
  projectLabels: local.projectLabels ?? config.projectLabels,
@@ -88,6 +97,19 @@ export async function removeRepoMapping(label) {
88
97
  await writeLocalConfig(next);
89
98
  return { config: next, removed: after.length !== before.length };
90
99
  }
100
+ export async function setServerUrl(serverUrl) {
101
+ const normalized = normalizeServerUrl(serverUrl);
102
+ const current = await readLocalConfig();
103
+ const next = normalizeLocalConfig({ ...current, serverUrl: normalized });
104
+ await writeLocalConfig(next);
105
+ return next;
106
+ }
107
+ export async function setSandboxPolicy(dockerEnabled, allowOpenPublicSandbox) {
108
+ const current = await readLocalConfig();
109
+ const next = normalizeLocalConfig({ ...current, dockerEnabled, allowOpenPublicSandbox });
110
+ await writeLocalConfig(next);
111
+ return next;
112
+ }
91
113
  export function providerStatusesFor(providers) {
92
114
  return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
93
115
  }
@@ -106,6 +128,9 @@ function normalizeLocalConfig(config) {
106
128
  deviceName: cleanString(config.deviceName),
107
129
  defaultRepoRoot: cleanString(config.defaultRepoRoot),
108
130
  cleanupWorktrees: config.cleanupWorktrees,
131
+ dockerEnabled: config.dockerEnabled,
132
+ dockerDesktopRecoveryEnabled: config.dockerDesktopRecoveryEnabled,
133
+ allowOpenPublicSandbox: config.allowOpenPublicSandbox,
109
134
  cliProviders: cleanList(config.cliProviders),
110
135
  providerStatuses: normalizeProviderStatuses(config.providerStatuses),
111
136
  projectLabels: cleanList(config.projectLabels),
@@ -200,10 +225,11 @@ function normalizeQaAppLaunchTarget(value) {
200
225
  command: cleanString(value.command),
201
226
  working_directory: cleanString(value.working_directory),
202
227
  ready_url: cleanString(value.ready_url),
228
+ managed_runtime_port: cleanString(value.managed_runtime_port),
203
229
  routes,
204
230
  notes: cleanString(value.notes)
205
231
  };
206
- return normalized.url || normalized.command || normalized.ready_url || routes?.length
232
+ return normalized.url || normalized.command || normalized.ready_url || normalized.managed_runtime_port
207
233
  ? normalized
208
234
  : undefined;
209
235
  }
@@ -211,6 +237,24 @@ function cleanString(value) {
211
237
  const clean = value?.trim();
212
238
  return clean ? clean : undefined;
213
239
  }
240
+ function normalizeServerUrl(value) {
241
+ const clean = value.trim();
242
+ let parsed;
243
+ try {
244
+ parsed = new URL(clean);
245
+ }
246
+ catch {
247
+ throw new Error(`Invalid Forge server URL: ${value}`);
248
+ }
249
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
250
+ throw new Error("Forge server URL must use http or https.");
251
+ }
252
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
253
+ throw new Error("Forge server URL cannot include credentials, a query string, or a fragment.");
254
+ }
255
+ parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
256
+ return parsed.toString().replace(/\/$/u, "");
257
+ }
214
258
  function cleanList(values) {
215
259
  const clean = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
216
260
  return clean.length > 0 ? clean : undefined;
@@ -0,0 +1,36 @@
1
+ export interface AspNetRuntimePrebuildPlan {
2
+ readonly fingerprint: string;
3
+ readonly receiptPath: string;
4
+ readonly command: string;
5
+ readonly args: readonly string[];
6
+ readonly cwd: string;
7
+ readonly environment: Readonly<Record<string, string>>;
8
+ readonly projectPath: string;
9
+ readonly worktreePath: string;
10
+ readonly inputCommitSha: string;
11
+ }
12
+ export interface AspNetRuntimePrebuildResult {
13
+ readonly cacheHit: boolean;
14
+ readonly receiptPath: string;
15
+ readonly fingerprint: string;
16
+ }
17
+ export interface AspNetRuntimeBuildExecutionResult {
18
+ readonly exitCode: number;
19
+ readonly stdoutTail: string;
20
+ readonly stderrTail: string;
21
+ readonly stdoutLogPath: string;
22
+ readonly stderrLogPath: string;
23
+ }
24
+ export type AspNetRuntimeBuildExecutor = (plan: AspNetRuntimePrebuildPlan, signal?: AbortSignal) => Promise<AspNetRuntimeBuildExecutionResult>;
25
+ export declare function createAspNetRuntimePrebuildPlan(options: {
26
+ readonly command: string;
27
+ readonly args: readonly string[];
28
+ readonly cwd: string;
29
+ readonly worktreePath: string;
30
+ readonly environment?: Readonly<Record<string, string>>;
31
+ readonly inputCommitSha: string;
32
+ readonly runtimeContractFingerprint: string;
33
+ }): AspNetRuntimePrebuildPlan | null;
34
+ export declare function ensureAspNetRuntimePrebuilt(plan: AspNetRuntimePrebuildPlan, execute: AspNetRuntimeBuildExecutor, signal?: AbortSignal): Promise<AspNetRuntimePrebuildResult>;
35
+ export declare function withAspNetPrebuiltRunArguments(command: string, args: readonly string[]): readonly string[];
36
+ export declare function resolveWorktreeCommitSha(worktreePath: string, supplied?: string | null): Promise<string>;