@loucompanion/forge-bridge 0.1.3-dev.8777774c67bc → 0.1.3-dev.88e93f53db5b

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 (137) hide show
  1. package/README.md +51 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +50 -3
  4. package/dist/bridge/bridge.service.js +730 -87
  5. package/dist/bridge/bridge.types.d.ts +63 -1
  6. package/dist/cleanup/cleanup.service.d.ts +14 -2
  7. package/dist/cleanup/cleanup.service.js +496 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +37 -3
  14. package/dist/cli/cli.service.d.ts +30 -2
  15. package/dist/cli/cli.service.js +191 -27
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +41 -1
  18. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  19. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  20. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  21. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  22. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  23. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  24. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  25. package/dist/runtime/attempt-process-supervisor.js +384 -0
  26. package/dist/runtime/generated-output-reset.d.ts +10 -0
  27. package/dist/runtime/generated-output-reset.js +103 -0
  28. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  29. package/dist/runtime/host-listener-inspector.js +349 -0
  30. package/dist/runtime/host-port-broker.d.ts +76 -0
  31. package/dist/runtime/host-port-broker.js +333 -0
  32. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  33. package/dist/runtime/host-runtime-contract.js +700 -0
  34. package/dist/runtime/host-runtime-lease.d.ts +194 -0
  35. package/dist/runtime/host-runtime-lease.js +2138 -0
  36. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  37. package/dist/runtime/host-runtime-promotion.js +159 -0
  38. package/dist/runtime/memory-capacity.d.ts +68 -0
  39. package/dist/runtime/memory-capacity.js +121 -0
  40. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  41. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  42. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  43. package/dist/runtime/playwright-host-runtime-adapter.js +225 -0
  44. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  45. package/dist/runtime/runtime-isolation-lease.js +158 -0
  46. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  47. package/dist/runtime/workspace-resource-estimator.js +174 -0
  48. package/dist/sandbox/image-policy.d.ts +3 -1
  49. package/dist/sandbox/image-policy.js +4 -2
  50. package/dist/sandbox/preflight.d.ts +49 -1
  51. package/dist/sandbox/preflight.js +313 -31
  52. package/dist/sandbox/process.js +125 -40
  53. package/dist/sandbox/repository-lock.js +41 -3
  54. package/dist/sandbox/runtime.d.ts +85 -4
  55. package/dist/sandbox/runtime.js +806 -126
  56. package/dist/sandbox/sandbox.types.d.ts +10 -0
  57. package/dist/sandbox/smoke.d.ts +76 -0
  58. package/dist/sandbox/smoke.js +380 -0
  59. package/dist/sandbox/worker-supervisor.js +1 -1
  60. package/dist/sandbox/worker.js +2 -5
  61. package/dist/sandbox/workspace.service.d.ts +1 -0
  62. package/dist/sandbox/workspace.service.js +28 -5
  63. package/dist/session/managed-attempt-tools.d.ts +18 -0
  64. package/dist/session/managed-attempt-tools.js +854 -0
  65. package/dist/session/managed-build-coordinator.d.ts +5 -0
  66. package/dist/session/managed-build-coordinator.js +105 -0
  67. package/dist/session/managed-build-receipt.d.ts +21 -0
  68. package/dist/session/managed-build-receipt.js +335 -0
  69. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  70. package/dist/session/phase-completion.artifacts.js +233 -24
  71. package/dist/session/phase-completion.d.ts +11 -2
  72. package/dist/session/phase-completion.helpers.d.ts +5 -0
  73. package/dist/session/phase-completion.helpers.js +51 -10
  74. package/dist/session/phase-completion.js +12 -3
  75. package/dist/session/phase-completion.prompts.d.ts +3 -3
  76. package/dist/session/phase-completion.prompts.js +21 -6
  77. package/dist/session/phase-completion.runtime.d.ts +13 -1
  78. package/dist/session/phase-completion.runtime.js +189 -8
  79. package/dist/session/phase-completion.schemas.js +108 -3
  80. package/dist/session/phase-completion.validation.d.ts +4 -1
  81. package/dist/session/phase-completion.validation.js +151 -21
  82. package/dist/session/provider-cli/ci-repair-readiness.d.ts +14 -0
  83. package/dist/session/provider-cli/ci-repair-readiness.js +105 -0
  84. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  85. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  86. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  87. package/dist/session/provider-cli/provider-cli.process.d.ts +23 -0
  88. package/dist/session/provider-cli/provider-cli.process.js +420 -32
  89. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  90. package/dist/session/provider-cli/provider-cli.prompts.js +99 -19
  91. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  92. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  93. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  94. package/dist/session/provider-cli/provider-cli.service.d.ts +20 -2
  95. package/dist/session/provider-cli/provider-cli.service.js +464 -102
  96. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  97. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  98. package/dist/session/provider-cli/provider-output-state.js +91 -16
  99. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  100. package/dist/session/provider-cli/provider-usage.js +28 -5
  101. package/dist/session/session-redaction.d.ts +3 -0
  102. package/dist/session/session-redaction.js +38 -0
  103. package/dist/session/session.base.d.ts +6 -1
  104. package/dist/session/session.service.d.ts +8 -2
  105. package/dist/session/session.service.js +522 -41
  106. package/dist/session/session.types.d.ts +70 -0
  107. package/dist/session/session.types.js +76 -4
  108. package/dist/shared/format/format-bytes.d.ts +1 -0
  109. package/dist/shared/format/format-bytes.js +9 -0
  110. package/dist/shared/path/path.d.ts +1 -0
  111. package/dist/shared/path/path.js +21 -1
  112. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  113. package/dist/shared/process/host-process-recovery.js +78 -0
  114. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  115. package/dist/shared/process/host-process-supervisor.js +330 -0
  116. package/dist/shared/process/process.d.ts +1 -0
  117. package/dist/shared/process/process.js +1 -0
  118. package/dist/shared/redaction/redaction.js +12 -2
  119. package/dist/version.d.ts +1 -1
  120. package/dist/version.js +1 -1
  121. package/dist/worktree/artifact-injector.d.ts +3 -2
  122. package/dist/worktree/artifact-injector.js +43 -4
  123. package/dist/worktree/dependency-warmer.d.ts +28 -1
  124. package/dist/worktree/dependency-warmer.js +308 -9
  125. package/dist/worktree/index.d.ts +2 -1
  126. package/dist/worktree/index.js +1 -0
  127. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  128. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  129. package/dist/worktree/worktree-paths.d.ts +4 -0
  130. package/dist/worktree/worktree-paths.js +30 -0
  131. package/dist/worktree/worktree.base.d.ts +3 -0
  132. package/dist/worktree/worktree.service.d.ts +9 -2
  133. package/dist/worktree/worktree.service.js +217 -48
  134. package/dist/worktree/worktree.types.d.ts +5 -1
  135. package/package.json +5 -3
  136. package/runtime/windows-job-supervisor.ps1 +444 -0
  137. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -16,10 +16,11 @@ export function helpText() {
16
16
  " connect * Start the runner bridge and wait for Forge dispatches",
17
17
  " pair * Pair this machine with Forge and save runner credentials",
18
18
  " status * Show config, runner identity, repos, provider CLIs, and updates",
19
+ " server * Show or change the saved Forge server URL",
19
20
  " add-repo * Map a Forge repo label/name to a local checkout path",
20
21
  " remove-repo * Remove a saved repo mapping",
21
22
  " cleanup * Prune managed bridge worktrees",
22
- " sandbox * Initialize, diagnose, or reap Docker sandboxes",
23
+ " sandbox * Initialize, diagnose, smoke-test, or reap Docker sandboxes",
23
24
  " version * Print the installed bridge package version",
24
25
  " help * Display help for command",
25
26
  " run-once * Internal: run one dispatch packet from JSON",
@@ -30,6 +31,7 @@ export function helpText() {
30
31
  " forge-bridge pair 1234-5678 --server https://srv1493013.tail66073d.ts.net:8443 --repo-root \"%USERPROFILE%\\ForgeRepos\"",
31
32
  " forge-bridge connect",
32
33
  " forge-bridge status --check-updates",
34
+ " forge-bridge server set http://localhost:5208",
33
35
  " forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
34
36
  " forge-bridge cleanup --stale",
35
37
  "",
@@ -100,6 +102,23 @@ export function commandHelpText(command) {
100
102
  " forge-bridge status --check-updates",
101
103
  "",
102
104
  ].join("\n");
105
+ case "server":
106
+ return [
107
+ "Forge Bridge — server",
108
+ "Show or change the saved Forge server URL.",
109
+ "",
110
+ "Usage:",
111
+ " forge-bridge server show",
112
+ " forge-bridge server set <url>",
113
+ "",
114
+ "The saved runner credentials are retained. Pair again when switching to a different Forge deployment.",
115
+ "FORGE_URL overrides the saved server when that environment variable is set.",
116
+ "",
117
+ "Examples:",
118
+ " forge-bridge server set http://localhost:5208",
119
+ " forge-bridge server set https://srv1493013.tail66073d.ts.net:8443",
120
+ "",
121
+ ].join("\n");
103
122
  case "add-repo":
104
123
  return [
105
124
  "Forge Bridge — add-repo",
@@ -137,13 +156,28 @@ export function commandHelpText(command) {
137
156
  case "sandbox":
138
157
  return [
139
158
  "Forge Bridge — sandbox",
140
- "Initialize or diagnose a repository-owned Forge sandbox contract.",
159
+ "Configure local Docker sandbox placement or inspect a repository-owned sandbox contract.",
141
160
  "",
142
161
  "Usage:",
162
+ " forge-bridge sandbox on [--allow-open-public]",
163
+ " forge-bridge sandbox off",
164
+ " forge-bridge sandbox status",
143
165
  " forge-bridge sandbox init [--repo <path>]",
144
166
  " forge-bridge sandbox doctor [--repo <path>] [--require-pinned-base]",
167
+ " forge-bridge sandbox smoke [--repo <path>] [--report <path>] [--require-pinned-base] [--probe-loopback-port]",
145
168
  " forge-bridge sandbox reap",
146
169
  "",
170
+ "`sandbox on` keeps unrestricted public networking denied unless the explicit",
171
+ "developer-only --allow-open-public option is supplied. The local policy is",
172
+ "saved in bridge.json and takes effect after the connected bridge restarts.",
173
+ "Environment variables remain higher-priority operator overrides.",
174
+ "",
175
+ "The smoke command builds the repository Dockerfile, runs a disposable hardened",
176
+ "container, verifies exact-label teardown, and writes a redacted JSON report.",
177
+ "The optional port probe publishes 127.0.0.1::8080 only for the disposable fixture",
178
+ "and verifies Docker-assigned host-port discovery; normal sandboxes publish no ports.",
179
+ "Stop the connected bridge first so the Docker ownership lock can be acquired.",
180
+ "",
147
181
  ].join("\n");
148
182
  case "run-once":
149
183
  return [
@@ -160,7 +194,7 @@ export function commandHelpText(command) {
160
194
  "Internal MCP server used by provider CLIs to report Forge phase artifacts.",
161
195
  "",
162
196
  "Usage:",
163
- " forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase>",
197
+ " forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase> # includes ci-repair",
164
198
  "",
165
199
  ].join("\n");
166
200
  case "version":
@@ -1,12 +1,15 @@
1
1
  import type { BridgeService } from "../bridge/bridge.base.js";
2
2
  import type { BridgeServiceConfig } from "../bridge/bridge.types.js";
3
+ import type { CleanupMode } from "../cleanup/cleanup.types.js";
4
+ import type { CleanupService } from "../cleanup/cleanup.base.js";
3
5
  import type { SessionService } from "../session/session.base.js";
4
6
  import type { CliIo, CliService } from "./cli.base.js";
5
7
  export declare class ForgeBridgeCliService implements CliService {
6
8
  private readonly sessions;
7
9
  private readonly bridgeFactory;
8
10
  private readonly pairRunner;
9
- constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient);
11
+ private readonly cleanupFactory;
12
+ constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient, cleanupFactory?: CleanupServiceFactory);
10
13
  run(argv: readonly string[], io?: CliIo): Promise<number>;
11
14
  }
12
15
  export declare function parseArgs(argv: readonly string[]): {
@@ -34,6 +37,14 @@ export declare function parseArgs(argv: readonly string[]): {
34
37
  readonly ok: true;
35
38
  readonly command: "remove-repo";
36
39
  readonly label: string;
40
+ } | {
41
+ readonly ok: true;
42
+ readonly command: "server-show";
43
+ readonly config: BridgeServiceConfig;
44
+ } | {
45
+ readonly ok: true;
46
+ readonly command: "server-set";
47
+ readonly serverUrl: string;
37
48
  } | {
38
49
  readonly ok: true;
39
50
  readonly command: "status";
@@ -54,9 +65,26 @@ export declare function parseArgs(argv: readonly string[]): {
54
65
  readonly command: "sandbox-doctor";
55
66
  readonly repoPath: string;
56
67
  readonly requirePinnedBase: boolean;
68
+ } | {
69
+ readonly ok: true;
70
+ readonly command: "sandbox-smoke";
71
+ readonly repoPath: string;
72
+ readonly reportPath?: string;
73
+ readonly requirePinnedBase: boolean;
74
+ readonly probeLoopbackPort: boolean;
57
75
  } | {
58
76
  readonly ok: true;
59
77
  readonly command: "sandbox-reap";
78
+ } | {
79
+ readonly ok: true;
80
+ readonly command: "sandbox-on";
81
+ readonly allowOpenPublic: boolean;
82
+ } | {
83
+ readonly ok: true;
84
+ readonly command: "sandbox-off";
85
+ } | {
86
+ readonly ok: true;
87
+ readonly command: "sandbox-status";
60
88
  } | {
61
89
  readonly ok: false;
62
90
  readonly error: string;
@@ -78,5 +106,5 @@ interface PairRunnerClaim {
78
106
  type PairRunnerClient = (args: PairArgs) => Promise<PairRunnerClaim>;
79
107
  export declare function parseCodexModelCatalog(raw: string): string[];
80
108
  export declare function parseCursorModelList(raw: string): string[];
81
- type CleanupMode = "stale" | "merged" | "all";
109
+ type CleanupServiceFactory = (stateRoot: string) => CleanupService;
82
110
  export {};
@@ -7,14 +7,17 @@ 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
9
  import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
10
- import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
10
+ import { redactText } from "../shared/redaction/redaction.js";
11
11
  import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
12
12
  import { forgeBridgeVersion } from "../version.js";
13
- import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
13
+ import { ManagedWorktreeCleanupService } from "../cleanup/cleanup.service.js";
14
+ import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, setSandboxPolicy, setServerUrl, writeLocalConfig } from "./local-config.js";
14
15
  import { BridgeSessionService } from "../session/session.service.js";
16
+ import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "../session/session-redaction.js";
15
17
  import { validateDockerfilePolicy, validateSandboxContract } from "../sandbox/contract.js";
16
18
  import { DockerPreflight } from "../sandbox/preflight.js";
17
19
  import { DockerSandboxRuntime } from "../sandbox/runtime.js";
20
+ import { DockerSandboxSmoke } from "../sandbox/smoke.js";
18
21
  import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
19
22
  import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
20
23
  import { commandHelpText, helpText } from "./cli.help.js";
@@ -24,10 +27,12 @@ export class ForgeBridgeCliService {
24
27
  sessions;
25
28
  bridgeFactory;
26
29
  pairRunner;
27
- constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner) {
30
+ cleanupFactory;
31
+ constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot)) {
28
32
  this.sessions = sessions;
29
33
  this.bridgeFactory = bridgeFactory;
30
34
  this.pairRunner = pairRunner;
35
+ this.cleanupFactory = cleanupFactory;
31
36
  }
32
37
  async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
33
38
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
@@ -67,6 +72,10 @@ export class ForgeBridgeCliService {
67
72
  process.once("SIGINT", stop);
68
73
  process.once("SIGTERM", stop);
69
74
  try {
75
+ if (config.cleanupWorktrees && config.repoMappings.some(mapping => mapping.localPath?.trim())) {
76
+ io.stdout.write("Cleaning stale managed worktrees before accepting dispatches.\n");
77
+ await cleanupRepos(config, "stale", true, this.cleanupFactory(config.sandboxStateRoot), io);
78
+ }
70
79
  io.stdout.write(connectStartupText(config));
71
80
  await this.bridgeFactory(config).run(controller.signal);
72
81
  return 0;
@@ -122,12 +131,38 @@ export class ForgeBridgeCliService {
122
131
  return 2;
123
132
  }
124
133
  }
134
+ if (args.command === "server-show") {
135
+ const saved = readLocalConfigSync().serverUrl ?? "not configured";
136
+ io.stdout.write(`Saved server: ${saved}\n`);
137
+ if (process.env.FORGE_URL?.trim()) {
138
+ io.stdout.write(`Effective server: ${process.env.FORGE_URL.trim()} (FORGE_URL override)\n`);
139
+ }
140
+ else {
141
+ io.stdout.write(`Effective server: ${args.config.serverUrl || "not configured"}\n`);
142
+ }
143
+ return 0;
144
+ }
145
+ if (args.command === "server-set") {
146
+ try {
147
+ const config = await setServerUrl(args.serverUrl);
148
+ io.stdout.write(`Saved Forge server: ${config.serverUrl}\n`);
149
+ io.stdout.write("Existing runner credentials were retained. Re-run `forge-bridge pair --server <url>` if this is a different Forge deployment.\n");
150
+ if (process.env.FORGE_URL?.trim()) {
151
+ io.stdout.write(`Warning: FORGE_URL is set to ${process.env.FORGE_URL.trim()} and overrides the saved server.\n`);
152
+ }
153
+ return 0;
154
+ }
155
+ catch (error) {
156
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
157
+ return 2;
158
+ }
159
+ }
125
160
  if (args.command === "status") {
126
161
  await writeStatus(args.config, args.checkUpdates, io);
127
162
  return 0;
128
163
  }
129
164
  if (args.command === "cleanup") {
130
- return await cleanupRepos(args.config, args.mode, args.force, io);
165
+ return await cleanupRepos(args.config, args.mode, args.force, this.cleanupFactory(args.config.sandboxStateRoot), io);
131
166
  }
132
167
  if (args.command === "sandbox-init") {
133
168
  return await initializeSandbox(args.repoPath, io);
@@ -135,9 +170,28 @@ export class ForgeBridgeCliService {
135
170
  if (args.command === "sandbox-doctor") {
136
171
  return await diagnoseSandbox(args.repoPath, args.requirePinnedBase, io);
137
172
  }
173
+ if (args.command === "sandbox-smoke") {
174
+ return await smokeSandbox(args.repoPath, args.reportPath, args.requirePinnedBase, args.probeLoopbackPort, io);
175
+ }
138
176
  if (args.command === "sandbox-reap") {
139
177
  return await reapSandboxes(io);
140
178
  }
179
+ if (args.command === "sandbox-on") {
180
+ const saved = await setSandboxPolicy(true, args.allowOpenPublic);
181
+ io.stdout.write("Saved local Docker sandbox policy. Restart the bridge to apply it.\n");
182
+ writeSandboxPolicyStatus(saved, io);
183
+ return 0;
184
+ }
185
+ if (args.command === "sandbox-off") {
186
+ const saved = await setSandboxPolicy(false, false);
187
+ io.stdout.write("Disabled local Docker sandbox placement. Restart the bridge to apply it.\n");
188
+ writeSandboxPolicyStatus(saved, io);
189
+ return 0;
190
+ }
191
+ if (args.command === "sandbox-status") {
192
+ writeSandboxPolicyStatus(readLocalConfigSync(), io);
193
+ return 0;
194
+ }
141
195
  if (args.command === "mcp") {
142
196
  const parsed = parseCompletionMcpArgs(args.argv);
143
197
  if (!parsed.ok) {
@@ -156,12 +210,12 @@ export class ForgeBridgeCliService {
156
210
  emit: async (event) => {
157
211
  const line = {
158
212
  type: "runner_event",
159
- event: redactSecrets(event, redactionSecrets)
213
+ event: redactRunnerDispatchEvent(event, redactionSecrets)
160
214
  };
161
215
  io.stdout.write(`${JSON.stringify(line)}\n`);
162
216
  }
163
217
  });
164
- await writeJsonAtomic(args.resultPath, redactSecrets(result, redactionSecrets));
218
+ await writeJsonAtomic(args.resultPath, redactRunOnceResultPacket(result, redactionSecrets));
165
219
  return result.exitCode;
166
220
  }
167
221
  catch (error) {
@@ -247,6 +301,15 @@ export function parseArgs(argv) {
247
301
  }
248
302
  return { ok: true, command: "remove-repo", label: argv[1] };
249
303
  }
304
+ if (argv[0] === "server") {
305
+ if (argv.length === 1 || (argv[1] === "show" && argv.length === 2)) {
306
+ return { ok: true, command: "server-show", config: configuredBridgeServiceConfig() };
307
+ }
308
+ if (argv[1] === "set" && argv[2] && argv.length === 3) {
309
+ return { ok: true, command: "server-set", serverUrl: argv[2] };
310
+ }
311
+ return { ok: false, error: "Usage: forge-bridge server <show|set <url>>" };
312
+ }
250
313
  if (argv[0] === "status") {
251
314
  let checkUpdates = false;
252
315
  for (let index = 1; index < argv.length; index += 1) {
@@ -301,8 +364,26 @@ export function parseArgs(argv) {
301
364
  }
302
365
  function parseSandboxArgs(argv) {
303
366
  const action = argv[1];
367
+ if (action === "on") {
368
+ let allowOpenPublic = false;
369
+ for (let index = 2; index < argv.length; index += 1) {
370
+ if (argv[index] === "--allow-open-public") {
371
+ allowOpenPublic = true;
372
+ }
373
+ else {
374
+ return { ok: false, error: `Unknown sandbox argument: ${argv[index]}` };
375
+ }
376
+ }
377
+ return { ok: true, command: "sandbox-on", allowOpenPublic };
378
+ }
379
+ if (action === "off" && argv.length === 2)
380
+ return { ok: true, command: "sandbox-off" };
381
+ if (action === "status" && argv.length === 2)
382
+ return { ok: true, command: "sandbox-status" };
304
383
  let repoPath = process.cwd();
384
+ let reportPath;
305
385
  let requirePinnedBase = false;
386
+ let probeLoopbackPort = false;
306
387
  for (let index = 2; index < argv.length; index += 1) {
307
388
  const arg = argv[index];
308
389
  if (arg === "--repo") {
@@ -310,9 +391,17 @@ function parseSandboxArgs(argv) {
310
391
  if (!repoPath)
311
392
  return { ok: false, error: "--repo requires a path." };
312
393
  }
313
- else if (arg === "--require-pinned-base" && action === "doctor") {
394
+ else if (arg === "--report" && action === "smoke") {
395
+ reportPath = argv[++index];
396
+ if (!reportPath)
397
+ return { ok: false, error: "--report requires a path." };
398
+ }
399
+ else if (arg === "--require-pinned-base" && (action === "doctor" || action === "smoke")) {
314
400
  requirePinnedBase = true;
315
401
  }
402
+ else if (arg === "--probe-loopback-port" && action === "smoke") {
403
+ probeLoopbackPort = true;
404
+ }
316
405
  else {
317
406
  return { ok: false, error: `Unknown sandbox argument: ${arg}` };
318
407
  }
@@ -321,9 +410,18 @@ function parseSandboxArgs(argv) {
321
410
  return { ok: true, command: "sandbox-init", repoPath: path.resolve(repoPath) };
322
411
  if (action === "doctor")
323
412
  return { ok: true, command: "sandbox-doctor", repoPath: path.resolve(repoPath), requirePinnedBase };
413
+ if (action === "smoke")
414
+ return {
415
+ ok: true,
416
+ command: "sandbox-smoke",
417
+ repoPath: path.resolve(repoPath),
418
+ reportPath: reportPath ? path.resolve(reportPath) : undefined,
419
+ requirePinnedBase,
420
+ probeLoopbackPort,
421
+ };
324
422
  if (action === "reap" && argv.length === 2)
325
423
  return { ok: true, command: "sandbox-reap" };
326
- return { ok: false, error: "Usage: forge-bridge sandbox <init|doctor|reap> [--repo <path>] [--require-pinned-base]" };
424
+ 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
425
  }
328
426
  const sandboxContractTemplate = `schemaVersion: 1
329
427
  workingDirectory: /workspace
@@ -389,6 +487,10 @@ async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
389
487
  diskHighWatermarkPercent: config.diskHighWatermarkPercent,
390
488
  diskLowWatermarkPercent: config.diskLowWatermarkPercent,
391
489
  minimumWorkspaceBytes: config.minimumWorkspaceBytes,
490
+ hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
491
+ defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
492
+ defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
493
+ runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
392
494
  requireRootless: config.requireRootlessDocker
393
495
  });
394
496
  try {
@@ -409,6 +511,42 @@ async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
409
511
  return 1;
410
512
  }
411
513
  }
514
+ async function smokeSandbox(repoPath, reportPath, requirePinnedBase, probeLoopbackPort, io) {
515
+ try {
516
+ const result = await new DockerSandboxSmoke(defaultBridgeServiceConfig()).run({
517
+ repoPath,
518
+ reportPath,
519
+ requirePinnedBase,
520
+ probeLoopbackPort,
521
+ onProgress: (stage) => io.stdout.write(`Smoke: ${stage}...\n`),
522
+ });
523
+ const { report } = result;
524
+ if (report.repository.contractDigest)
525
+ io.stdout.write(`Contract: valid (${report.repository.contractDigest})\n`);
526
+ for (const warning of report.repository.policyWarnings)
527
+ io.stdout.write(`Warning: ${warning}\n`);
528
+ if (report.docker.runtimeVersion) {
529
+ io.stdout.write(`Docker: healthy (${report.docker.runtimeVersion}; capacity ${report.docker.capacity})\n`);
530
+ }
531
+ io.stdout.write(`Attempt: ${report.dispatchAttemptId}\n`);
532
+ io.stdout.write(`Resource lease: ${report.resourceLeaseId}\n`);
533
+ if (report.publishedPortProbe) {
534
+ io.stdout.write(`Loopback port: ${report.publishedPortProbe.hostIp}:${report.publishedPortProbe.hostPort} -> ${report.publishedPortProbe.containerPort}/tcp\n`);
535
+ }
536
+ io.stdout.write(`Cleanup: ${report.cleanup.verified ? "verified (zero exact-label resources)" : "failed"}\n`);
537
+ io.stdout.write(`Report: ${result.reportPath}\n`);
538
+ if (report.status === "passed") {
539
+ io.stdout.write("Sandbox smoke: PASS\n");
540
+ return 0;
541
+ }
542
+ io.stderr.write(`Sandbox smoke: FAIL (${report.failure?.code ?? "sandbox_smoke_failed"} at ${report.failure?.stage ?? "unknown"}): ${report.failure?.message ?? "unknown failure"}\n`);
543
+ return 1;
544
+ }
545
+ catch (error) {
546
+ io.stderr.write(`Sandbox smoke could not write its report: ${error instanceof Error ? error.message : String(error)}\n`);
547
+ return 1;
548
+ }
549
+ }
412
550
  async function reapSandboxes(io) {
413
551
  const config = defaultBridgeServiceConfig();
414
552
  if (!/^[0-9a-f-]{32,36}$/iu.test(config.runnerId)) {
@@ -423,6 +561,10 @@ async function reapSandboxes(io) {
423
561
  diskHighWatermarkPercent: config.diskHighWatermarkPercent,
424
562
  diskLowWatermarkPercent: config.diskLowWatermarkPercent,
425
563
  minimumWorkspaceBytes: config.minimumWorkspaceBytes,
564
+ hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
565
+ defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
566
+ defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
567
+ runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
426
568
  requireRootless: config.requireRootlessDocker
427
569
  });
428
570
  try {
@@ -576,7 +718,7 @@ function parsePairArgs(argv) {
576
718
  name: name?.trim() || defaultName,
577
719
  fingerprint: fingerprint?.trim() || `local-bridge:${os.hostname()}:${user}`,
578
720
  repoRoot: repoRoot?.trim() ? path.resolve(repoRoot) : undefined,
579
- cleanupWorktrees: cleanupWorktrees ?? localConfig.cleanupWorktrees ?? false,
721
+ cleanupWorktrees: cleanupWorktrees ?? defaultConfig.cleanupWorktrees,
580
722
  cliProviders: cliProviders.length > 0 ? cliProviders : localConfig.cliProviders && localConfig.cliProviders.length > 0 ? [...localConfig.cliProviders] : ["codex"],
581
723
  projectLabels: projectLabels.length > 0 ? projectLabels : localConfig.projectLabels ? [...localConfig.projectLabels] : []
582
724
  };
@@ -731,11 +873,27 @@ function connectStartupText(config) {
731
873
  ` repo folder: ${config.defaultRepoRoot || "not configured"}`,
732
874
  ` providers: ${formatList(config.cliProviders)}`,
733
875
  ` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
876
+ ` open public sandbox networking: ${config.allowOpenPublicSandbox ? "enabled" : "disabled"}`,
734
877
  "",
735
878
  "Press Ctrl+C to stop.",
736
879
  "",
737
880
  ].join("\n");
738
881
  }
882
+ function writeSandboxPolicyStatus(local, io) {
883
+ const effective = configuredBridgeServiceConfig({}, local);
884
+ io.stdout.write(`Config: ${resolveLocalConfigPath()}\n`);
885
+ io.stdout.write(`Docker sandbox placement: ${effective.dockerEnabled ? "on" : "off"}\n`);
886
+ io.stdout.write(`Open public sandbox networking: ${effective.allowOpenPublicSandbox ? "allowed" : "denied"}\n`);
887
+ if (process.env.FORGE_DOCKER_SANDBOX !== undefined) {
888
+ io.stdout.write("Docker setting source: FORGE_DOCKER_SANDBOX override\n");
889
+ }
890
+ if (process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX !== undefined) {
891
+ io.stdout.write("Open public setting source: FORGE_ALLOW_OPEN_PUBLIC_SANDBOX override\n");
892
+ }
893
+ if (effective.allowOpenPublicSandbox) {
894
+ io.stdout.write("Warning: open_public permits unrestricted outbound network access from developer sandboxes.\n");
895
+ }
896
+ }
739
897
  function formatList(values) {
740
898
  return values.length > 0 ? values.join(", ") : "none";
741
899
  }
@@ -772,7 +930,7 @@ async function writeStatus(config, checkUpdates, io) {
772
930
  io.stdout.write("Update check: no update channel configured.\n");
773
931
  }
774
932
  }
775
- async function cleanupRepos(config, mode, force, io) {
933
+ async function cleanupRepos(config, mode, force, cleanup, io) {
776
934
  const repos = config.repoMappings.filter((mapping) => mapping.localPath?.trim());
777
935
  if (repos.length === 0) {
778
936
  io.stdout.write("No repos configured.\n");
@@ -781,20 +939,28 @@ async function cleanupRepos(config, mode, force, io) {
781
939
  let failures = 0;
782
940
  for (const repo of repos) {
783
941
  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
942
  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`);
943
+ const result = await cleanup.run({
944
+ repoRoot: repoPath,
945
+ allowedRepoRoot: config.defaultRepoRoot || undefined,
946
+ mode,
947
+ force,
948
+ });
949
+ if (!force) {
950
+ const action = mode === "all"
951
+ ? "remove"
952
+ : mode === "stale"
953
+ ? `remove ${result.candidateWorktrees} stale managed ${result.candidateWorktrees === 1 ? "worktree" : "worktrees"} from`
954
+ : "prune";
955
+ io.stdout.write(`[dry-run] Would ${action} ${result.worktreeRoot}\n`);
956
+ }
957
+ else if (mode === "all") {
958
+ io.stdout.write(`Removed ${result.worktreeRoot}\n`);
959
+ }
960
+ else if (mode === "stale") {
961
+ io.stdout.write(`Removed ${result.removedWorktrees} stale managed ${result.removedWorktrees === 1 ? "worktree" : "worktrees"} from ${result.worktreeRoot}\n`);
795
962
  }
796
963
  else {
797
- await runGit(repoPath, ["worktree", "prune"]);
798
964
  io.stdout.write(`Pruned stale worktree metadata for ${repoPath}\n`);
799
965
  }
800
966
  }
@@ -943,8 +1109,8 @@ function uniqueModels(models) {
943
1109
  function isRecord(value) {
944
1110
  return typeof value === "object" && value !== null;
945
1111
  }
946
- async function withDetectedCliProviders(config) {
947
- const detected = await detectCliProviders(configuredProviderCandidates(config.cliProviders));
1112
+ async function withDetectedCliProviders(config, candidates = config.cliProviders) {
1113
+ const detected = await detectCliProviders(uniqueProviders(candidates));
948
1114
  const found = detected.filter((result) => result.found);
949
1115
  if (found.length === 0) {
950
1116
  return config;
@@ -968,6 +1134,7 @@ async function withDetectedCliProviders(config) {
968
1134
  };
969
1135
  }
970
1136
  async function withDetectedPairCliProviders(args) {
1137
+ const configured = configuredProviderCandidates(args.cliProviders);
971
1138
  const detectedConfig = await withDetectedCliProviders({
972
1139
  ...defaultBridgeServiceConfig({
973
1140
  serverUrl: args.serverUrl,
@@ -977,7 +1144,7 @@ async function withDetectedPairCliProviders(args) {
977
1144
  cleanupWorktrees: args.cleanupWorktrees,
978
1145
  defaultRepoRoot: args.repoRoot ?? defaultBridgeServiceConfig().defaultRepoRoot
979
1146
  })
980
- });
1147
+ }, configured);
981
1148
  return {
982
1149
  ...args,
983
1150
  cliProviders: detectedConfig.cliProviders
@@ -992,6 +1159,3 @@ function uniqueProviders(providers) {
992
1159
  .filter((provider) => provider.length > 0)
993
1160
  .filter((provider, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === provider.toLowerCase()) === index);
994
1161
  }
995
- async function runGit(cwd, args) {
996
- await execFileAsync("git", args, { cwd, timeout: 10_000 });
997
- }
@@ -7,6 +7,8 @@ export interface BridgeLocalConfig {
7
7
  readonly deviceName?: string;
8
8
  readonly defaultRepoRoot?: string;
9
9
  readonly cleanupWorktrees?: boolean;
10
+ readonly dockerEnabled?: boolean;
11
+ readonly allowOpenPublicSandbox?: boolean;
10
12
  readonly cliProviders?: readonly string[];
11
13
  readonly providerStatuses?: Readonly<Record<string, BridgeProviderStatus>>;
12
14
  readonly projectLabels?: readonly string[];
@@ -34,4 +36,6 @@ export declare function removeRepoMapping(label: string): Promise<{
34
36
  readonly config: BridgeLocalConfig;
35
37
  readonly removed: boolean;
36
38
  }>;
39
+ export declare function setServerUrl(serverUrl: string): Promise<BridgeLocalConfig>;
40
+ export declare function setSandboxPolicy(dockerEnabled: boolean, allowOpenPublicSandbox: boolean): Promise<BridgeLocalConfig>;
37
41
  export declare function providerStatusesFor(providers: readonly string[]): Readonly<Record<string, BridgeProviderStatus>>;
@@ -45,6 +45,12 @@ 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
+ allowOpenPublicSandbox: process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX === undefined
52
+ ? local.allowOpenPublicSandbox ?? config.allowOpenPublicSandbox
53
+ : config.allowOpenPublicSandbox,
48
54
  cliProviders,
49
55
  providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
50
56
  projectLabels: local.projectLabels ?? config.projectLabels,
@@ -88,6 +94,19 @@ export async function removeRepoMapping(label) {
88
94
  await writeLocalConfig(next);
89
95
  return { config: next, removed: after.length !== before.length };
90
96
  }
97
+ export async function setServerUrl(serverUrl) {
98
+ const normalized = normalizeServerUrl(serverUrl);
99
+ const current = await readLocalConfig();
100
+ const next = normalizeLocalConfig({ ...current, serverUrl: normalized });
101
+ await writeLocalConfig(next);
102
+ return next;
103
+ }
104
+ export async function setSandboxPolicy(dockerEnabled, allowOpenPublicSandbox) {
105
+ const current = await readLocalConfig();
106
+ const next = normalizeLocalConfig({ ...current, dockerEnabled, allowOpenPublicSandbox });
107
+ await writeLocalConfig(next);
108
+ return next;
109
+ }
91
110
  export function providerStatusesFor(providers) {
92
111
  return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
93
112
  }
@@ -106,6 +125,8 @@ function normalizeLocalConfig(config) {
106
125
  deviceName: cleanString(config.deviceName),
107
126
  defaultRepoRoot: cleanString(config.defaultRepoRoot),
108
127
  cleanupWorktrees: config.cleanupWorktrees,
128
+ dockerEnabled: config.dockerEnabled,
129
+ allowOpenPublicSandbox: config.allowOpenPublicSandbox,
109
130
  cliProviders: cleanList(config.cliProviders),
110
131
  providerStatuses: normalizeProviderStatuses(config.providerStatuses),
111
132
  projectLabels: cleanList(config.projectLabels),
@@ -200,10 +221,11 @@ function normalizeQaAppLaunchTarget(value) {
200
221
  command: cleanString(value.command),
201
222
  working_directory: cleanString(value.working_directory),
202
223
  ready_url: cleanString(value.ready_url),
224
+ managed_runtime_port: cleanString(value.managed_runtime_port),
203
225
  routes,
204
226
  notes: cleanString(value.notes)
205
227
  };
206
- return normalized.url || normalized.command || normalized.ready_url || routes?.length
228
+ return normalized.url || normalized.command || normalized.ready_url || normalized.managed_runtime_port
207
229
  ? normalized
208
230
  : undefined;
209
231
  }
@@ -211,6 +233,24 @@ function cleanString(value) {
211
233
  const clean = value?.trim();
212
234
  return clean ? clean : undefined;
213
235
  }
236
+ function normalizeServerUrl(value) {
237
+ const clean = value.trim();
238
+ let parsed;
239
+ try {
240
+ parsed = new URL(clean);
241
+ }
242
+ catch {
243
+ throw new Error(`Invalid Forge server URL: ${value}`);
244
+ }
245
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
246
+ throw new Error("Forge server URL must use http or https.");
247
+ }
248
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
249
+ throw new Error("Forge server URL cannot include credentials, a query string, or a fragment.");
250
+ }
251
+ parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
252
+ return parsed.toString().replace(/\/$/u, "");
253
+ }
214
254
  function cleanList(values) {
215
255
  const clean = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
216
256
  return clean.length > 0 ? clean : undefined;