@loucompanion/forge-bridge 0.1.3-dev.f2b8822ce953 → 0.1.3-dev.fbe15023cdfe
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.
- package/README.md +78 -0
- package/dist/bridge/bridge.base.d.ts +6 -1
- package/dist/bridge/bridge.service.d.ts +60 -3
- package/dist/bridge/bridge.service.js +970 -24
- package/dist/bridge/bridge.types.d.ts +172 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.js +47 -1
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +351 -22
- package/dist/cli/local-config.d.ts +6 -0
- package/dist/cli/local-config.js +47 -3
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
- package/dist/runtime/attempt-process-supervisor.js +350 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +44 -0
- package/dist/runtime/host-listener-inspector.js +324 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +133 -0
- package/dist/runtime/host-runtime-lease.js +1460 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
- package/dist/runtime/node-host-runtime-adapter.js +268 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +54 -0
- package/dist/sandbox/preflight.js +335 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +119 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +86 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +72 -0
- package/dist/session/managed-attempt-tools.d.ts +12 -0
- package/dist/session/managed-attempt-tools.js +561 -0
- package/dist/session/phase-completion.artifacts.js +32 -4
- package/dist/session/phase-completion.d.ts +4 -2
- package/dist/session/phase-completion.helpers.js +27 -3
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +20 -6
- package/dist/session/phase-completion.runtime.d.ts +12 -1
- package/dist/session/phase-completion.runtime.js +129 -7
- package/dist/session/phase-completion.schemas.js +132 -5
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +106 -2
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +17 -0
- package/dist/session/provider-cli/provider-cli.process.js +223 -14
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +17 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +226 -18
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -3
- package/dist/session/provider-cli/provider-cli.service.js +503 -98
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +463 -58
- package/dist/session/session.types.d.ts +144 -1
- package/dist/session/session.types.js +279 -3
- package/dist/shared/path/path.js +3 -1
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +57 -0
- package/dist/shared/process/host-process-supervisor.js +282 -0
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/process/windows-launch.d.ts +1 -1
- package/dist/shared/process/windows-launch.js +6 -3
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +10 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +21 -1
- package/dist/worktree/dependency-warmer.js +138 -5
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +7 -2
- package/dist/worktree/worktree.service.js +72 -44
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +6 -3
- package/runtime/windows-job-supervisor.ps1 +356 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
package/dist/cli/cli.service.js
CHANGED
|
@@ -6,11 +6,18 @@ 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 {
|
|
9
|
+
import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
|
|
10
|
+
import { redactText } from "../shared/redaction/redaction.js";
|
|
10
11
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
11
12
|
import { forgeBridgeVersion } from "../version.js";
|
|
12
|
-
import {
|
|
13
|
+
import { ManagedWorktreeCleanupService } from "../cleanup/cleanup.service.js";
|
|
14
|
+
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, setSandboxPolicy, setServerUrl, writeLocalConfig } from "./local-config.js";
|
|
13
15
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
16
|
+
import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "../session/session-redaction.js";
|
|
17
|
+
import { validateDockerfilePolicy, validateSandboxContract } from "../sandbox/contract.js";
|
|
18
|
+
import { DockerPreflight } from "../sandbox/preflight.js";
|
|
19
|
+
import { DockerSandboxRuntime } from "../sandbox/runtime.js";
|
|
20
|
+
import { DockerSandboxSmoke } from "../sandbox/smoke.js";
|
|
14
21
|
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
15
22
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
16
23
|
import { commandHelpText, helpText } from "./cli.help.js";
|
|
@@ -20,10 +27,12 @@ export class ForgeBridgeCliService {
|
|
|
20
27
|
sessions;
|
|
21
28
|
bridgeFactory;
|
|
22
29
|
pairRunner;
|
|
23
|
-
|
|
30
|
+
cleanupFactory;
|
|
31
|
+
constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot)) {
|
|
24
32
|
this.sessions = sessions;
|
|
25
33
|
this.bridgeFactory = bridgeFactory;
|
|
26
34
|
this.pairRunner = pairRunner;
|
|
35
|
+
this.cleanupFactory = cleanupFactory;
|
|
27
36
|
}
|
|
28
37
|
async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
29
38
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
@@ -88,6 +97,7 @@ export class ForgeBridgeCliService {
|
|
|
88
97
|
if (args.command === "pair") {
|
|
89
98
|
try {
|
|
90
99
|
const current = readLocalConfigSync();
|
|
100
|
+
io.stdout.write(`Pairing with server: ${args.serverUrl}\n`);
|
|
91
101
|
const code = await resolvePairingCode(args.code);
|
|
92
102
|
const repoRoot = await resolvePairRepoRoot(args.repoRoot, current.defaultRepoRoot);
|
|
93
103
|
await fs.mkdir(repoRoot, { recursive: true });
|
|
@@ -117,12 +127,66 @@ export class ForgeBridgeCliService {
|
|
|
117
127
|
return 2;
|
|
118
128
|
}
|
|
119
129
|
}
|
|
130
|
+
if (args.command === "server-show") {
|
|
131
|
+
const saved = readLocalConfigSync().serverUrl ?? "not configured";
|
|
132
|
+
io.stdout.write(`Saved server: ${saved}\n`);
|
|
133
|
+
if (process.env.FORGE_URL?.trim()) {
|
|
134
|
+
io.stdout.write(`Effective server: ${process.env.FORGE_URL.trim()} (FORGE_URL override)\n`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
io.stdout.write(`Effective server: ${args.config.serverUrl || "not configured"}\n`);
|
|
138
|
+
}
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (args.command === "server-set") {
|
|
142
|
+
try {
|
|
143
|
+
const config = await setServerUrl(args.serverUrl);
|
|
144
|
+
io.stdout.write(`Saved Forge server: ${config.serverUrl}\n`);
|
|
145
|
+
io.stdout.write("Existing runner credentials were retained. Re-run `forge-bridge pair --server <url>` if this is a different Forge deployment.\n");
|
|
146
|
+
if (process.env.FORGE_URL?.trim()) {
|
|
147
|
+
io.stdout.write(`Warning: FORGE_URL is set to ${process.env.FORGE_URL.trim()} and overrides the saved server.\n`);
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
120
156
|
if (args.command === "status") {
|
|
121
157
|
await writeStatus(args.config, args.checkUpdates, io);
|
|
122
158
|
return 0;
|
|
123
159
|
}
|
|
124
160
|
if (args.command === "cleanup") {
|
|
125
|
-
return await cleanupRepos(args.config, args.mode, args.force, io);
|
|
161
|
+
return await cleanupRepos(args.config, args.mode, args.force, this.cleanupFactory(args.config.sandboxStateRoot), io);
|
|
162
|
+
}
|
|
163
|
+
if (args.command === "sandbox-init") {
|
|
164
|
+
return await initializeSandbox(args.repoPath, io);
|
|
165
|
+
}
|
|
166
|
+
if (args.command === "sandbox-doctor") {
|
|
167
|
+
return await diagnoseSandbox(args.repoPath, args.requirePinnedBase, io);
|
|
168
|
+
}
|
|
169
|
+
if (args.command === "sandbox-smoke") {
|
|
170
|
+
return await smokeSandbox(args.repoPath, args.reportPath, args.requirePinnedBase, args.probeLoopbackPort, io);
|
|
171
|
+
}
|
|
172
|
+
if (args.command === "sandbox-reap") {
|
|
173
|
+
return await reapSandboxes(io);
|
|
174
|
+
}
|
|
175
|
+
if (args.command === "sandbox-on") {
|
|
176
|
+
const saved = await setSandboxPolicy(true, args.allowOpenPublic);
|
|
177
|
+
io.stdout.write("Saved local Docker sandbox policy. Restart the bridge to apply it.\n");
|
|
178
|
+
writeSandboxPolicyStatus(saved, io);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
if (args.command === "sandbox-off") {
|
|
182
|
+
const saved = await setSandboxPolicy(false, false);
|
|
183
|
+
io.stdout.write("Disabled local Docker sandbox placement. Restart the bridge to apply it.\n");
|
|
184
|
+
writeSandboxPolicyStatus(saved, io);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
if (args.command === "sandbox-status") {
|
|
188
|
+
writeSandboxPolicyStatus(readLocalConfigSync(), io);
|
|
189
|
+
return 0;
|
|
126
190
|
}
|
|
127
191
|
if (args.command === "mcp") {
|
|
128
192
|
const parsed = parseCompletionMcpArgs(args.argv);
|
|
@@ -142,12 +206,12 @@ export class ForgeBridgeCliService {
|
|
|
142
206
|
emit: async (event) => {
|
|
143
207
|
const line = {
|
|
144
208
|
type: "runner_event",
|
|
145
|
-
event:
|
|
209
|
+
event: redactRunnerDispatchEvent(event, redactionSecrets)
|
|
146
210
|
};
|
|
147
211
|
io.stdout.write(`${JSON.stringify(line)}\n`);
|
|
148
212
|
}
|
|
149
213
|
});
|
|
150
|
-
await writeJsonAtomic(args.resultPath,
|
|
214
|
+
await writeJsonAtomic(args.resultPath, redactRunOnceResultPacket(result, redactionSecrets));
|
|
151
215
|
return result.exitCode;
|
|
152
216
|
}
|
|
153
217
|
catch (error) {
|
|
@@ -233,6 +297,15 @@ export function parseArgs(argv) {
|
|
|
233
297
|
}
|
|
234
298
|
return { ok: true, command: "remove-repo", label: argv[1] };
|
|
235
299
|
}
|
|
300
|
+
if (argv[0] === "server") {
|
|
301
|
+
if (argv.length === 1 || (argv[1] === "show" && argv.length === 2)) {
|
|
302
|
+
return { ok: true, command: "server-show", config: configuredBridgeServiceConfig() };
|
|
303
|
+
}
|
|
304
|
+
if (argv[1] === "set" && argv[2] && argv.length === 3) {
|
|
305
|
+
return { ok: true, command: "server-set", serverUrl: argv[2] };
|
|
306
|
+
}
|
|
307
|
+
return { ok: false, error: "Usage: forge-bridge server <show|set <url>>" };
|
|
308
|
+
}
|
|
236
309
|
if (argv[0] === "status") {
|
|
237
310
|
let checkUpdates = false;
|
|
238
311
|
for (let index = 1; index < argv.length; index += 1) {
|
|
@@ -252,6 +325,9 @@ export function parseArgs(argv) {
|
|
|
252
325
|
if (argv[0] === "mcp") {
|
|
253
326
|
return { ok: true, command: "mcp", argv };
|
|
254
327
|
}
|
|
328
|
+
if (argv[0] === "sandbox") {
|
|
329
|
+
return parseSandboxArgs(argv);
|
|
330
|
+
}
|
|
255
331
|
if (argv[0] !== "run-once") {
|
|
256
332
|
return {
|
|
257
333
|
ok: false,
|
|
@@ -282,6 +358,230 @@ export function parseArgs(argv) {
|
|
|
282
358
|
resultPath: path.resolve(resultPath)
|
|
283
359
|
};
|
|
284
360
|
}
|
|
361
|
+
function parseSandboxArgs(argv) {
|
|
362
|
+
const action = argv[1];
|
|
363
|
+
if (action === "on") {
|
|
364
|
+
let allowOpenPublic = false;
|
|
365
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
366
|
+
if (argv[index] === "--allow-open-public") {
|
|
367
|
+
allowOpenPublic = true;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
return { ok: false, error: `Unknown sandbox argument: ${argv[index]}` };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { ok: true, command: "sandbox-on", allowOpenPublic };
|
|
374
|
+
}
|
|
375
|
+
if (action === "off" && argv.length === 2)
|
|
376
|
+
return { ok: true, command: "sandbox-off" };
|
|
377
|
+
if (action === "status" && argv.length === 2)
|
|
378
|
+
return { ok: true, command: "sandbox-status" };
|
|
379
|
+
let repoPath = process.cwd();
|
|
380
|
+
let reportPath;
|
|
381
|
+
let requirePinnedBase = false;
|
|
382
|
+
let probeLoopbackPort = false;
|
|
383
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
384
|
+
const arg = argv[index];
|
|
385
|
+
if (arg === "--repo") {
|
|
386
|
+
repoPath = argv[++index] ?? "";
|
|
387
|
+
if (!repoPath)
|
|
388
|
+
return { ok: false, error: "--repo requires a path." };
|
|
389
|
+
}
|
|
390
|
+
else if (arg === "--report" && action === "smoke") {
|
|
391
|
+
reportPath = argv[++index];
|
|
392
|
+
if (!reportPath)
|
|
393
|
+
return { ok: false, error: "--report requires a path." };
|
|
394
|
+
}
|
|
395
|
+
else if (arg === "--require-pinned-base" && (action === "doctor" || action === "smoke")) {
|
|
396
|
+
requirePinnedBase = true;
|
|
397
|
+
}
|
|
398
|
+
else if (arg === "--probe-loopback-port" && action === "smoke") {
|
|
399
|
+
probeLoopbackPort = true;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
return { ok: false, error: `Unknown sandbox argument: ${arg}` };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (action === "init")
|
|
406
|
+
return { ok: true, command: "sandbox-init", repoPath: path.resolve(repoPath) };
|
|
407
|
+
if (action === "doctor")
|
|
408
|
+
return { ok: true, command: "sandbox-doctor", repoPath: path.resolve(repoPath), requirePinnedBase };
|
|
409
|
+
if (action === "smoke")
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
command: "sandbox-smoke",
|
|
413
|
+
repoPath: path.resolve(repoPath),
|
|
414
|
+
reportPath: reportPath ? path.resolve(reportPath) : undefined,
|
|
415
|
+
requirePinnedBase,
|
|
416
|
+
probeLoopbackPort,
|
|
417
|
+
};
|
|
418
|
+
if (action === "reap" && argv.length === 2)
|
|
419
|
+
return { ok: true, command: "sandbox-reap" };
|
|
420
|
+
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]" };
|
|
421
|
+
}
|
|
422
|
+
const sandboxContractTemplate = `schemaVersion: 1
|
|
423
|
+
workingDirectory: /workspace
|
|
424
|
+
commands:
|
|
425
|
+
install: npm ci
|
|
426
|
+
test: npm test
|
|
427
|
+
caches:
|
|
428
|
+
- name: npm
|
|
429
|
+
mode: readonly
|
|
430
|
+
services: []
|
|
431
|
+
environment: []
|
|
432
|
+
artifacts: []
|
|
433
|
+
logs: []
|
|
434
|
+
browser: false
|
|
435
|
+
`;
|
|
436
|
+
const sandboxDockerfileTemplate = `# Pin this base by digest before enabling sandbox_required.
|
|
437
|
+
FROM node:22.17.0-bookworm-slim
|
|
438
|
+
WORKDIR /workspace
|
|
439
|
+
`;
|
|
440
|
+
async function initializeSandbox(repoPath, io) {
|
|
441
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
442
|
+
const files = [
|
|
443
|
+
["forge.sandbox.yml", sandboxContractTemplate],
|
|
444
|
+
["Dockerfile", sandboxDockerfileTemplate],
|
|
445
|
+
["forge.env.example", "# List non-secret variable names in forge.sandbox.yml. Never commit secret values.\n"]
|
|
446
|
+
];
|
|
447
|
+
await fs.mkdir(forgePath, { recursive: true });
|
|
448
|
+
let created = 0;
|
|
449
|
+
for (const [name, contents] of files) {
|
|
450
|
+
const target = path.join(forgePath, name);
|
|
451
|
+
try {
|
|
452
|
+
const handle = await fs.open(target, "wx", 0o600);
|
|
453
|
+
await handle.writeFile(contents);
|
|
454
|
+
await handle.close();
|
|
455
|
+
io.stdout.write(`Created ${path.relative(repoPath, target)}\n`);
|
|
456
|
+
created += 1;
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
if (error.code !== "EEXIST")
|
|
460
|
+
throw error;
|
|
461
|
+
io.stdout.write(`Kept existing ${path.relative(repoPath, target)}\n`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
io.stdout.write(created > 0 ? "Sandbox scaffold is ready. Run `forge-bridge sandbox doctor`.\n" : "Sandbox scaffold already exists.\n");
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
|
|
468
|
+
try {
|
|
469
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
470
|
+
const contractSource = await fs.readFile(path.join(forgePath, "forge.sandbox.yml"), "utf8");
|
|
471
|
+
const dockerfile = await fs.readFile(path.join(forgePath, "Dockerfile"), "utf8");
|
|
472
|
+
const contract = validateSandboxContract(contractSource);
|
|
473
|
+
const warnings = validateDockerfilePolicy(dockerfile, requirePinnedBase);
|
|
474
|
+
io.stdout.write(`Contract: valid (${contract.digest})\n`);
|
|
475
|
+
for (const warning of warnings)
|
|
476
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
477
|
+
const config = defaultBridgeServiceConfig();
|
|
478
|
+
const preflight = new DockerPreflight({
|
|
479
|
+
dockerExecutable: config.dockerExecutable,
|
|
480
|
+
dockerContext: config.dockerContext,
|
|
481
|
+
stateRoot: config.sandboxStateRoot,
|
|
482
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
483
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
484
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
485
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
486
|
+
hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
|
|
487
|
+
defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
|
|
488
|
+
defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
|
|
489
|
+
runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
|
|
490
|
+
requireRootless: config.requireRootlessDocker
|
|
491
|
+
});
|
|
492
|
+
try {
|
|
493
|
+
const result = await preflight.refresh();
|
|
494
|
+
if (!result.healthy) {
|
|
495
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
496
|
+
return 1;
|
|
497
|
+
}
|
|
498
|
+
io.stdout.write(`Docker: healthy (${result.runtimeVersion}; capacity ${result.maxConcurrentSandboxes})\n`);
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
await preflight.release();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
507
|
+
return 1;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function smokeSandbox(repoPath, reportPath, requirePinnedBase, probeLoopbackPort, io) {
|
|
511
|
+
try {
|
|
512
|
+
const result = await new DockerSandboxSmoke(defaultBridgeServiceConfig()).run({
|
|
513
|
+
repoPath,
|
|
514
|
+
reportPath,
|
|
515
|
+
requirePinnedBase,
|
|
516
|
+
probeLoopbackPort,
|
|
517
|
+
onProgress: (stage) => io.stdout.write(`Smoke: ${stage}...\n`),
|
|
518
|
+
});
|
|
519
|
+
const { report } = result;
|
|
520
|
+
if (report.repository.contractDigest)
|
|
521
|
+
io.stdout.write(`Contract: valid (${report.repository.contractDigest})\n`);
|
|
522
|
+
for (const warning of report.repository.policyWarnings)
|
|
523
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
524
|
+
if (report.docker.runtimeVersion) {
|
|
525
|
+
io.stdout.write(`Docker: healthy (${report.docker.runtimeVersion}; capacity ${report.docker.capacity})\n`);
|
|
526
|
+
}
|
|
527
|
+
io.stdout.write(`Attempt: ${report.dispatchAttemptId}\n`);
|
|
528
|
+
io.stdout.write(`Resource lease: ${report.resourceLeaseId}\n`);
|
|
529
|
+
if (report.publishedPortProbe) {
|
|
530
|
+
io.stdout.write(`Loopback port: ${report.publishedPortProbe.hostIp}:${report.publishedPortProbe.hostPort} -> ${report.publishedPortProbe.containerPort}/tcp\n`);
|
|
531
|
+
}
|
|
532
|
+
io.stdout.write(`Cleanup: ${report.cleanup.verified ? "verified (zero exact-label resources)" : "failed"}\n`);
|
|
533
|
+
io.stdout.write(`Report: ${result.reportPath}\n`);
|
|
534
|
+
if (report.status === "passed") {
|
|
535
|
+
io.stdout.write("Sandbox smoke: PASS\n");
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
io.stderr.write(`Sandbox smoke: FAIL (${report.failure?.code ?? "sandbox_smoke_failed"} at ${report.failure?.stage ?? "unknown"}): ${report.failure?.message ?? "unknown failure"}\n`);
|
|
539
|
+
return 1;
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
io.stderr.write(`Sandbox smoke could not write its report: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
543
|
+
return 1;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function reapSandboxes(io) {
|
|
547
|
+
const config = defaultBridgeServiceConfig();
|
|
548
|
+
if (!/^[0-9a-f-]{32,36}$/iu.test(config.runnerId)) {
|
|
549
|
+
io.stderr.write("FORGE_RUNNER_ID must identify the runner whose exact-labeled resources may be reaped.\n");
|
|
550
|
+
return 2;
|
|
551
|
+
}
|
|
552
|
+
const preflight = new DockerPreflight({
|
|
553
|
+
dockerExecutable: config.dockerExecutable,
|
|
554
|
+
dockerContext: config.dockerContext,
|
|
555
|
+
stateRoot: config.sandboxStateRoot,
|
|
556
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
557
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
558
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
559
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
560
|
+
hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
|
|
561
|
+
defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
|
|
562
|
+
defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
|
|
563
|
+
runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
|
|
564
|
+
requireRootless: config.requireRootlessDocker
|
|
565
|
+
});
|
|
566
|
+
try {
|
|
567
|
+
const result = await preflight.refresh();
|
|
568
|
+
if (!result.healthy && result.unavailableCode !== "docker_disk_low") {
|
|
569
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
570
|
+
return 1;
|
|
571
|
+
}
|
|
572
|
+
const runtime = new DockerSandboxRuntime(config);
|
|
573
|
+
await runtime.reap({ diskLow: result.diskLow });
|
|
574
|
+
io.stdout.write(`Sandbox reaper completed for runner ${config.runnerId}.\n`);
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
579
|
+
return 1;
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
await preflight.release();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
285
585
|
function parseConnectArgs(argv) {
|
|
286
586
|
let serverUrl = null;
|
|
287
587
|
let runnerId = null;
|
|
@@ -470,7 +770,7 @@ async function claimBridgeRunner(args) {
|
|
|
470
770
|
})
|
|
471
771
|
});
|
|
472
772
|
if (!response.ok) {
|
|
473
|
-
throw new Error(
|
|
773
|
+
throw new Error(formatPairingFailure(response.status, args.serverUrl, await response.text()));
|
|
474
774
|
}
|
|
475
775
|
const value = await response.json();
|
|
476
776
|
if (!value.runnerId || !value.deviceToken) {
|
|
@@ -499,6 +799,8 @@ function pairedConfig(current, args, claim) {
|
|
|
499
799
|
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
500
800
|
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
501
801
|
supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
|
|
802
|
+
supportsDraftAttachments: current.supportsDraftAttachments ?? true,
|
|
803
|
+
supportsDraftImageAttachments: supportsDraftImageAttachmentsFor(args.cliProviders),
|
|
502
804
|
repoMappings: current.repoMappings ?? []
|
|
503
805
|
};
|
|
504
806
|
}
|
|
@@ -543,6 +845,12 @@ async function resolvePairRepoRoot(repoRoot, configuredRoot) {
|
|
|
543
845
|
function normalizePairingCode(code) {
|
|
544
846
|
return code?.replace(/[\s-]/g, "").trim() ?? "";
|
|
545
847
|
}
|
|
848
|
+
function formatPairingFailure(status, serverUrl, body) {
|
|
849
|
+
const hint = body.includes("runner.invalid_pairing_code")
|
|
850
|
+
? " Make sure the code was generated by the same Forge server shown above."
|
|
851
|
+
: "";
|
|
852
|
+
return `Pairing failed (${status}) against ${serverUrl}: ${body}${hint}`;
|
|
853
|
+
}
|
|
546
854
|
function normalizeServerUrl(serverUrl) {
|
|
547
855
|
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
548
856
|
}
|
|
@@ -561,11 +869,27 @@ function connectStartupText(config) {
|
|
|
561
869
|
` repo folder: ${config.defaultRepoRoot || "not configured"}`,
|
|
562
870
|
` providers: ${formatList(config.cliProviders)}`,
|
|
563
871
|
` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
|
|
872
|
+
` open public sandbox networking: ${config.allowOpenPublicSandbox ? "enabled" : "disabled"}`,
|
|
564
873
|
"",
|
|
565
874
|
"Press Ctrl+C to stop.",
|
|
566
875
|
"",
|
|
567
876
|
].join("\n");
|
|
568
877
|
}
|
|
878
|
+
function writeSandboxPolicyStatus(local, io) {
|
|
879
|
+
const effective = configuredBridgeServiceConfig({}, local);
|
|
880
|
+
io.stdout.write(`Config: ${resolveLocalConfigPath()}\n`);
|
|
881
|
+
io.stdout.write(`Docker sandbox placement: ${effective.dockerEnabled ? "on" : "off"}\n`);
|
|
882
|
+
io.stdout.write(`Open public sandbox networking: ${effective.allowOpenPublicSandbox ? "allowed" : "denied"}\n`);
|
|
883
|
+
if (process.env.FORGE_DOCKER_SANDBOX !== undefined) {
|
|
884
|
+
io.stdout.write("Docker setting source: FORGE_DOCKER_SANDBOX override\n");
|
|
885
|
+
}
|
|
886
|
+
if (process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX !== undefined) {
|
|
887
|
+
io.stdout.write("Open public setting source: FORGE_ALLOW_OPEN_PUBLIC_SANDBOX override\n");
|
|
888
|
+
}
|
|
889
|
+
if (effective.allowOpenPublicSandbox) {
|
|
890
|
+
io.stdout.write("Warning: open_public permits unrestricted outbound network access from developer sandboxes.\n");
|
|
891
|
+
}
|
|
892
|
+
}
|
|
569
893
|
function formatList(values) {
|
|
570
894
|
return values.length > 0 ? values.join(", ") : "none";
|
|
571
895
|
}
|
|
@@ -602,7 +926,7 @@ async function writeStatus(config, checkUpdates, io) {
|
|
|
602
926
|
io.stdout.write("Update check: no update channel configured.\n");
|
|
603
927
|
}
|
|
604
928
|
}
|
|
605
|
-
async function cleanupRepos(config, mode, force, io) {
|
|
929
|
+
async function cleanupRepos(config, mode, force, cleanup, io) {
|
|
606
930
|
const repos = config.repoMappings.filter((mapping) => mapping.localPath?.trim());
|
|
607
931
|
if (repos.length === 0) {
|
|
608
932
|
io.stdout.write("No repos configured.\n");
|
|
@@ -611,20 +935,28 @@ async function cleanupRepos(config, mode, force, io) {
|
|
|
611
935
|
let failures = 0;
|
|
612
936
|
for (const repo of repos) {
|
|
613
937
|
const repoPath = path.resolve(repo.localPath);
|
|
614
|
-
const worktreeRoot = path.join(path.dirname(repoPath), ".forge-worktrees", path.basename(repoPath));
|
|
615
|
-
if (!force) {
|
|
616
|
-
const action = mode === "all" ? "remove" : "prune";
|
|
617
|
-
io.stdout.write(`[dry-run] Would ${action} ${worktreeRoot}\n`);
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
938
|
try {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
939
|
+
const result = await cleanup.run({
|
|
940
|
+
repoRoot: repoPath,
|
|
941
|
+
allowedRepoRoot: config.defaultRepoRoot || undefined,
|
|
942
|
+
mode,
|
|
943
|
+
force,
|
|
944
|
+
});
|
|
945
|
+
if (!force) {
|
|
946
|
+
const action = mode === "all"
|
|
947
|
+
? "remove"
|
|
948
|
+
: mode === "stale"
|
|
949
|
+
? `remove ${result.candidateWorktrees} stale managed ${result.candidateWorktrees === 1 ? "worktree" : "worktrees"} from`
|
|
950
|
+
: "prune";
|
|
951
|
+
io.stdout.write(`[dry-run] Would ${action} ${result.worktreeRoot}\n`);
|
|
952
|
+
}
|
|
953
|
+
else if (mode === "all") {
|
|
954
|
+
io.stdout.write(`Removed ${result.worktreeRoot}\n`);
|
|
955
|
+
}
|
|
956
|
+
else if (mode === "stale") {
|
|
957
|
+
io.stdout.write(`Removed ${result.removedWorktrees} stale managed ${result.removedWorktrees === 1 ? "worktree" : "worktrees"} from ${result.worktreeRoot}\n`);
|
|
625
958
|
}
|
|
626
959
|
else {
|
|
627
|
-
await runGit(repoPath, ["worktree", "prune"]);
|
|
628
960
|
io.stdout.write(`Pruned stale worktree metadata for ${repoPath}\n`);
|
|
629
961
|
}
|
|
630
962
|
}
|
|
@@ -822,6 +1154,3 @@ function uniqueProviders(providers) {
|
|
|
822
1154
|
.filter((provider) => provider.length > 0)
|
|
823
1155
|
.filter((provider, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === provider.toLowerCase()) === index);
|
|
824
1156
|
}
|
|
825
|
-
async function runGit(cwd, args) {
|
|
826
|
-
await execFileAsync("git", args, { cwd, timeout: 10_000 });
|
|
827
|
-
}
|
|
@@ -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[];
|
|
@@ -17,6 +19,8 @@ export interface BridgeLocalConfig {
|
|
|
17
19
|
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
20
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
19
21
|
readonly supportsDraftRunOnce?: boolean;
|
|
22
|
+
readonly supportsDraftAttachments?: boolean;
|
|
23
|
+
readonly supportsDraftImageAttachments?: boolean;
|
|
20
24
|
}
|
|
21
25
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
22
26
|
readonly label: string;
|
|
@@ -32,4 +36,6 @@ export declare function removeRepoMapping(label: string): Promise<{
|
|
|
32
36
|
readonly config: BridgeLocalConfig;
|
|
33
37
|
readonly removed: boolean;
|
|
34
38
|
}>;
|
|
39
|
+
export declare function setServerUrl(serverUrl: string): Promise<BridgeLocalConfig>;
|
|
40
|
+
export declare function setSandboxPolicy(dockerEnabled: boolean, allowOpenPublicSandbox: boolean): Promise<BridgeLocalConfig>;
|
|
35
41
|
export declare function providerStatusesFor(providers: readonly string[]): Readonly<Record<string, BridgeProviderStatus>>;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -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,
|
|
@@ -54,7 +60,9 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
54
60
|
supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
|
|
55
61
|
supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
|
|
56
62
|
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
|
|
57
|
-
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce
|
|
63
|
+
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce,
|
|
64
|
+
supportsDraftAttachments: local.supportsDraftAttachments ?? config.supportsDraftAttachments,
|
|
65
|
+
supportsDraftImageAttachments: local.supportsDraftImageAttachments ?? config.supportsDraftImageAttachments
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
68
|
export async function addRepoMapping(label, repoPath) {
|
|
@@ -86,6 +94,19 @@ export async function removeRepoMapping(label) {
|
|
|
86
94
|
await writeLocalConfig(next);
|
|
87
95
|
return { config: next, removed: after.length !== before.length };
|
|
88
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
|
+
}
|
|
89
110
|
export function providerStatusesFor(providers) {
|
|
90
111
|
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
|
|
91
112
|
}
|
|
@@ -104,6 +125,8 @@ function normalizeLocalConfig(config) {
|
|
|
104
125
|
deviceName: cleanString(config.deviceName),
|
|
105
126
|
defaultRepoRoot: cleanString(config.defaultRepoRoot),
|
|
106
127
|
cleanupWorktrees: config.cleanupWorktrees,
|
|
128
|
+
dockerEnabled: config.dockerEnabled,
|
|
129
|
+
allowOpenPublicSandbox: config.allowOpenPublicSandbox,
|
|
107
130
|
cliProviders: cleanList(config.cliProviders),
|
|
108
131
|
providerStatuses: normalizeProviderStatuses(config.providerStatuses),
|
|
109
132
|
projectLabels: cleanList(config.projectLabels),
|
|
@@ -113,7 +136,9 @@ function normalizeLocalConfig(config) {
|
|
|
113
136
|
supportsManagedWorktrees: config.supportsManagedWorktrees,
|
|
114
137
|
supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
|
|
115
138
|
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
|
|
116
|
-
supportsDraftRunOnce: config.supportsDraftRunOnce
|
|
139
|
+
supportsDraftRunOnce: config.supportsDraftRunOnce,
|
|
140
|
+
supportsDraftAttachments: config.supportsDraftAttachments,
|
|
141
|
+
supportsDraftImageAttachments: config.supportsDraftImageAttachments
|
|
117
142
|
};
|
|
118
143
|
}
|
|
119
144
|
function emptyConfig() {
|
|
@@ -196,10 +221,11 @@ function normalizeQaAppLaunchTarget(value) {
|
|
|
196
221
|
command: cleanString(value.command),
|
|
197
222
|
working_directory: cleanString(value.working_directory),
|
|
198
223
|
ready_url: cleanString(value.ready_url),
|
|
224
|
+
managed_runtime_port: cleanString(value.managed_runtime_port),
|
|
199
225
|
routes,
|
|
200
226
|
notes: cleanString(value.notes)
|
|
201
227
|
};
|
|
202
|
-
return normalized.url || normalized.command || normalized.ready_url ||
|
|
228
|
+
return normalized.url || normalized.command || normalized.ready_url || normalized.managed_runtime_port
|
|
203
229
|
? normalized
|
|
204
230
|
: undefined;
|
|
205
231
|
}
|
|
@@ -207,6 +233,24 @@ function cleanString(value) {
|
|
|
207
233
|
const clean = value?.trim();
|
|
208
234
|
return clean ? clean : undefined;
|
|
209
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
|
+
}
|
|
210
254
|
function cleanList(values) {
|
|
211
255
|
const clean = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
|
|
212
256
|
return clean.length > 0 ? clean : undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AttemptProcessChildHandle, AttemptProcessSupervisor } from "./attempt-process-supervisor.js";
|
|
2
|
+
import type { HostPortLeaseOwner } from "./host-port-broker.js";
|
|
3
|
+
import { HostListenerInspector } from "./host-listener-inspector.js";
|
|
4
|
+
import type { HostListenerOwnership, NodeRuntimeCommandLauncher, NodeRuntimeLaunchRequest, NodeRuntimeReadinessDiagnostics } from "./node-host-runtime-adapter.js";
|
|
5
|
+
/** Launches one explicitly configured HTTP project beneath the attempt coordinator. */
|
|
6
|
+
export declare class AttemptNodeRuntimeLauncher implements NodeRuntimeCommandLauncher<AttemptProcessChildHandle> {
|
|
7
|
+
private readonly supervisor;
|
|
8
|
+
private readonly owner;
|
|
9
|
+
private readonly logDirectory;
|
|
10
|
+
private readonly listenerInspector;
|
|
11
|
+
private readonly diagnosticsByChild;
|
|
12
|
+
constructor(supervisor: AttemptProcessSupervisor, owner: HostPortLeaseOwner, logDirectory: string, listenerInspector?: HostListenerInspector);
|
|
13
|
+
launch(request: NodeRuntimeLaunchRequest, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
|
|
14
|
+
inspectListener(child: AttemptProcessChildHandle, request: NodeRuntimeLaunchRequest): Promise<HostListenerOwnership>;
|
|
15
|
+
stop(child: AttemptProcessChildHandle): Promise<void>;
|
|
16
|
+
readinessDiagnostics(child: AttemptProcessChildHandle): Promise<NodeRuntimeReadinessDiagnostics>;
|
|
17
|
+
}
|