@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 0.1.3-dev.9d8f2e7e5241
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 +44 -0
- package/dist/bridge/bridge.base.d.ts +5 -1
- package/dist/bridge/bridge.service.d.ts +40 -2
- package/dist/bridge/bridge.service.js +580 -16
- package/dist/bridge/bridge.types.d.ts +114 -1
- package/dist/cli/cli.help.js +12 -0
- package/dist/cli/cli.service.d.ts +12 -0
- package/dist/cli/cli.service.js +171 -1
- package/dist/cli/local-config.d.ts +2 -0
- package/dist/cli/local-config.js +6 -2
- 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 +40 -0
- package/dist/sandbox/preflight.js +256 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +190 -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 +83 -0
- package/dist/sandbox/runtime.d.ts +234 -0
- package/dist/sandbox/runtime.js +1773 -0
- package/dist/sandbox/sandbox.types.d.ts +83 -0
- package/dist/sandbox/sandbox.types.js +1 -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 +228 -0
- package/dist/sandbox/workspace.service.d.ts +12 -0
- package/dist/sandbox/workspace.service.js +49 -0
- package/dist/session/phase-completion.artifacts.js +23 -2
- 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 +20 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.process.js +23 -1
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +12 -1
- package/dist/session/provider-cli/provider-cli.prompts.js +119 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.service.js +151 -24
- package/dist/session/session.service.js +14 -7
- package/dist/session/session.types.d.ts +52 -1
- package/dist/session/session.types.js +159 -3
- 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 +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +8 -1
- package/dist/worktree/dependency-warmer.js +40 -3
- package/package.json +4 -2
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import type { RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
|
|
1
|
+
import type { ApiKeySecretGrant, RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
|
|
2
|
+
import type { SandboxFeatureMatrix } from "../sandbox/sandbox.types.js";
|
|
2
3
|
export interface BridgeServiceConfig {
|
|
3
4
|
readonly serverUrl: string;
|
|
4
5
|
readonly runnerId: string;
|
|
6
|
+
readonly runnerInstanceId: string;
|
|
7
|
+
readonly trustDomainId: string;
|
|
8
|
+
readonly maxConcurrentSandboxes: number;
|
|
9
|
+
readonly maxConcurrentSandboxBuilds: number;
|
|
10
|
+
readonly sandboxPidCapacity: number;
|
|
11
|
+
readonly dockerEnabled: boolean;
|
|
12
|
+
readonly sandboxPublicationMode?: "bridge_push" | "shared_filesystem";
|
|
13
|
+
readonly dockerExecutable: string;
|
|
14
|
+
readonly dockerContext: string;
|
|
15
|
+
readonly sandboxStateRoot: string;
|
|
16
|
+
readonly requireRootlessDocker: boolean;
|
|
17
|
+
readonly diskHighWatermarkPercent: number;
|
|
18
|
+
readonly diskLowWatermarkPercent: number;
|
|
19
|
+
readonly minimumWorkspaceBytes: number;
|
|
20
|
+
readonly imageRetentionCount: number;
|
|
21
|
+
readonly imageRetentionTtlMs: number;
|
|
22
|
+
readonly auditRetentionCount: number;
|
|
23
|
+
readonly auditRetentionTtlMs: number;
|
|
5
24
|
readonly deviceToken: string;
|
|
6
25
|
readonly deviceName: string;
|
|
7
26
|
readonly defaultRepoRoot: string;
|
|
@@ -17,6 +36,8 @@ export interface BridgeServiceConfig {
|
|
|
17
36
|
readonly supportsManagedForgeArtifacts: boolean;
|
|
18
37
|
readonly supportsRunnerAuthoredPhaseArtifacts: boolean;
|
|
19
38
|
readonly supportsDraftRunOnce: boolean;
|
|
39
|
+
readonly supportsDraftAttachments: boolean;
|
|
40
|
+
readonly supportsDraftImageAttachments: boolean;
|
|
20
41
|
readonly version: string;
|
|
21
42
|
readonly protocolVersion: number;
|
|
22
43
|
readonly minServerVersion?: string | null;
|
|
@@ -52,17 +73,51 @@ export interface BridgeCapabilities {
|
|
|
52
73
|
readonly repoMappings?: readonly BridgeRepoMapping[] | null;
|
|
53
74
|
readonly os?: string | null;
|
|
54
75
|
readonly sandboxProfiles?: readonly string[] | null;
|
|
76
|
+
readonly runnerInstanceId?: string | null;
|
|
77
|
+
readonly trustDomainId?: string | null;
|
|
78
|
+
readonly sandboxRuntimes?: readonly string[] | null;
|
|
79
|
+
readonly sandboxRuntimeVersion?: string | null;
|
|
80
|
+
readonly sandboxFeatures?: SandboxFeatureMatrix | null;
|
|
81
|
+
readonly sandboxPublicationModes?: readonly string[] | null;
|
|
82
|
+
readonly sandboxAuthModes?: Readonly<Record<string, readonly string[]>> | null;
|
|
83
|
+
readonly maxConcurrentSandboxes?: number | null;
|
|
84
|
+
readonly reservedSandboxCount?: number | null;
|
|
85
|
+
readonly activeSandboxCount?: number | null;
|
|
86
|
+
readonly startingSandboxCount?: number | null;
|
|
87
|
+
readonly degradedReservationCount?: number | null;
|
|
88
|
+
readonly availableMemoryBytes?: number | null;
|
|
89
|
+
readonly availableCpuMillis?: number | null;
|
|
90
|
+
readonly availableWorkspaceBytes?: number | null;
|
|
91
|
+
readonly diskLow?: boolean | null;
|
|
92
|
+
readonly preflightGeneration?: number | null;
|
|
93
|
+
readonly sandboxUnavailableCode?: string | null;
|
|
94
|
+
readonly sandboxUnavailableReason?: string | null;
|
|
55
95
|
readonly supportsApiSecretGrant?: boolean | null;
|
|
56
96
|
readonly supportsManagedWorktrees?: boolean | null;
|
|
57
97
|
readonly supportsManagedForgeArtifacts?: boolean | null;
|
|
58
98
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean | null;
|
|
59
99
|
readonly supportsDraftRunOnce?: boolean | null;
|
|
100
|
+
readonly supportsDraftAttachments?: boolean | null;
|
|
101
|
+
readonly supportsDraftImageAttachments?: boolean | null;
|
|
60
102
|
}
|
|
61
103
|
export interface BridgeHelloRequest {
|
|
62
104
|
readonly version: string;
|
|
63
105
|
readonly protocolVersion: number;
|
|
64
106
|
readonly capabilities: BridgeCapabilities;
|
|
65
107
|
readonly minServerVersion?: string | null;
|
|
108
|
+
readonly runnerInstanceId: string;
|
|
109
|
+
}
|
|
110
|
+
export interface BridgePublishCredential {
|
|
111
|
+
readonly username: string;
|
|
112
|
+
readonly secret: string;
|
|
113
|
+
readonly remoteUrl: string;
|
|
114
|
+
readonly expiresAt: string;
|
|
115
|
+
}
|
|
116
|
+
export interface BridgePublishGrantRedemptionResponse {
|
|
117
|
+
readonly accepted: boolean;
|
|
118
|
+
readonly code?: string | null;
|
|
119
|
+
readonly message?: string | null;
|
|
120
|
+
readonly credential?: BridgePublishCredential | null;
|
|
66
121
|
}
|
|
67
122
|
export interface BridgeHeartbeatRequest {
|
|
68
123
|
readonly status?: string | null;
|
|
@@ -78,17 +133,75 @@ export interface BridgeCallbackAck {
|
|
|
78
133
|
export interface BridgeDispatchEventCallback {
|
|
79
134
|
readonly leaseId: string;
|
|
80
135
|
readonly fencingToken: string;
|
|
136
|
+
readonly dispatchAttemptId?: string | null;
|
|
137
|
+
readonly runnerInstanceId?: string | null;
|
|
81
138
|
readonly event: RunnerDispatchEvent;
|
|
82
139
|
}
|
|
83
140
|
export interface BridgeDispatchResultCallback {
|
|
84
141
|
readonly leaseId: string;
|
|
85
142
|
readonly fencingToken: string;
|
|
143
|
+
readonly dispatchAttemptId?: string | null;
|
|
144
|
+
readonly runnerInstanceId?: string | null;
|
|
86
145
|
readonly result: RunOnceResultPacket;
|
|
87
146
|
}
|
|
147
|
+
export interface BridgeOfferDecisionCallback {
|
|
148
|
+
readonly dispatchAttemptId: string;
|
|
149
|
+
readonly leaseId: string;
|
|
150
|
+
readonly fencingToken: string;
|
|
151
|
+
readonly runnerInstanceId: string;
|
|
152
|
+
readonly accepted: boolean;
|
|
153
|
+
readonly code?: string | null;
|
|
154
|
+
readonly message?: string | null;
|
|
155
|
+
readonly reservedUntil?: string | null;
|
|
156
|
+
}
|
|
157
|
+
export interface BridgeDispatchStartedCallback {
|
|
158
|
+
readonly dispatchAttemptId: string;
|
|
159
|
+
readonly leaseId: string;
|
|
160
|
+
readonly fencingToken: string;
|
|
161
|
+
readonly runnerInstanceId: string;
|
|
162
|
+
}
|
|
163
|
+
export interface BridgeSecretGrantRedemptionRequest {
|
|
164
|
+
readonly dispatchAttemptId: string;
|
|
165
|
+
readonly leaseId: string;
|
|
166
|
+
readonly fencingToken: string;
|
|
167
|
+
readonly runnerInstanceId: string;
|
|
168
|
+
readonly grantId: string;
|
|
169
|
+
}
|
|
170
|
+
export interface BridgeSecretGrantRedemptionResponse extends BridgeCallbackAck {
|
|
171
|
+
readonly grant?: ApiKeySecretGrant | null;
|
|
172
|
+
}
|
|
173
|
+
export interface BridgeLeaseUpdateRequest {
|
|
174
|
+
readonly dispatchAttemptId: string;
|
|
175
|
+
readonly leaseId: string;
|
|
176
|
+
readonly fencingToken: string;
|
|
177
|
+
readonly runnerInstanceId: string;
|
|
178
|
+
readonly expiresAt: string;
|
|
179
|
+
}
|
|
88
180
|
export interface BridgeCancelRequest {
|
|
89
181
|
readonly leaseId: string;
|
|
90
182
|
readonly fencingToken: string;
|
|
91
183
|
readonly reason?: string | null;
|
|
92
184
|
}
|
|
185
|
+
export interface BridgeReapRequest {
|
|
186
|
+
readonly runnerId: string;
|
|
187
|
+
readonly runnerInstanceId: string;
|
|
188
|
+
readonly requestedAt: string;
|
|
189
|
+
readonly reason?: string | null;
|
|
190
|
+
}
|
|
191
|
+
export interface BridgeAttemptSnapshotItem {
|
|
192
|
+
readonly dispatchAttemptId: string;
|
|
193
|
+
readonly fencingToken: string;
|
|
194
|
+
readonly runnerInstanceId: string;
|
|
195
|
+
readonly status: string;
|
|
196
|
+
readonly expiresAt: string;
|
|
197
|
+
}
|
|
198
|
+
export interface BridgeAttemptSnapshotResponse extends BridgeCallbackAck {
|
|
199
|
+
readonly attempts?: readonly BridgeAttemptSnapshotItem[] | null;
|
|
200
|
+
readonly generatedAt?: string | null;
|
|
201
|
+
}
|
|
93
202
|
export type BridgeDispatchHandler = (dispatch: unknown) => Promise<void>;
|
|
203
|
+
export type BridgeOfferHandler = (dispatch: unknown) => Promise<void>;
|
|
204
|
+
export type BridgeStartHandler = (dispatch: unknown) => Promise<void>;
|
|
205
|
+
export type BridgeLeaseUpdateHandler = (request: BridgeLeaseUpdateRequest) => Promise<void>;
|
|
94
206
|
export type BridgeCancelHandler = (request: BridgeCancelRequest) => Promise<void>;
|
|
207
|
+
export type BridgeReapHandler = (request: BridgeReapRequest) => Promise<void>;
|
package/dist/cli/cli.help.js
CHANGED
|
@@ -19,6 +19,7 @@ export function helpText() {
|
|
|
19
19
|
" add-repo * Map a Forge repo label/name to a local checkout path",
|
|
20
20
|
" remove-repo * Remove a saved repo mapping",
|
|
21
21
|
" cleanup * Prune managed bridge worktrees",
|
|
22
|
+
" sandbox * Initialize, diagnose, or reap Docker sandboxes",
|
|
22
23
|
" version * Print the installed bridge package version",
|
|
23
24
|
" help * Display help for command",
|
|
24
25
|
" run-once * Internal: run one dispatch packet from JSON",
|
|
@@ -133,6 +134,17 @@ export function commandHelpText(command) {
|
|
|
133
134
|
" forge-bridge cleanup --all --force",
|
|
134
135
|
"",
|
|
135
136
|
].join("\n");
|
|
137
|
+
case "sandbox":
|
|
138
|
+
return [
|
|
139
|
+
"Forge Bridge — sandbox",
|
|
140
|
+
"Initialize or diagnose a repository-owned Forge sandbox contract.",
|
|
141
|
+
"",
|
|
142
|
+
"Usage:",
|
|
143
|
+
" forge-bridge sandbox init [--repo <path>]",
|
|
144
|
+
" forge-bridge sandbox doctor [--repo <path>] [--require-pinned-base]",
|
|
145
|
+
" forge-bridge sandbox reap",
|
|
146
|
+
"",
|
|
147
|
+
].join("\n");
|
|
136
148
|
case "run-once":
|
|
137
149
|
return [
|
|
138
150
|
"Forge Bridge — run-once",
|
|
@@ -45,6 +45,18 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
45
45
|
readonly config: BridgeServiceConfig;
|
|
46
46
|
readonly mode: CleanupMode;
|
|
47
47
|
readonly force: boolean;
|
|
48
|
+
} | {
|
|
49
|
+
readonly ok: true;
|
|
50
|
+
readonly command: "sandbox-init";
|
|
51
|
+
readonly repoPath: string;
|
|
52
|
+
} | {
|
|
53
|
+
readonly ok: true;
|
|
54
|
+
readonly command: "sandbox-doctor";
|
|
55
|
+
readonly repoPath: string;
|
|
56
|
+
readonly requirePinnedBase: boolean;
|
|
57
|
+
} | {
|
|
58
|
+
readonly ok: true;
|
|
59
|
+
readonly command: "sandbox-reap";
|
|
48
60
|
} | {
|
|
49
61
|
readonly ok: false;
|
|
50
62
|
readonly error: string;
|
package/dist/cli/cli.service.js
CHANGED
|
@@ -6,11 +6,15 @@ 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 { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
|
|
9
10
|
import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
10
11
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
11
12
|
import { forgeBridgeVersion } from "../version.js";
|
|
12
13
|
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
|
|
13
14
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
15
|
+
import { validateDockerfilePolicy, validateSandboxContract } from "../sandbox/contract.js";
|
|
16
|
+
import { DockerPreflight } from "../sandbox/preflight.js";
|
|
17
|
+
import { DockerSandboxRuntime } from "../sandbox/runtime.js";
|
|
14
18
|
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
15
19
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
16
20
|
import { commandHelpText, helpText } from "./cli.help.js";
|
|
@@ -88,6 +92,7 @@ export class ForgeBridgeCliService {
|
|
|
88
92
|
if (args.command === "pair") {
|
|
89
93
|
try {
|
|
90
94
|
const current = readLocalConfigSync();
|
|
95
|
+
io.stdout.write(`Pairing with server: ${args.serverUrl}\n`);
|
|
91
96
|
const code = await resolvePairingCode(args.code);
|
|
92
97
|
const repoRoot = await resolvePairRepoRoot(args.repoRoot, current.defaultRepoRoot);
|
|
93
98
|
await fs.mkdir(repoRoot, { recursive: true });
|
|
@@ -124,6 +129,15 @@ export class ForgeBridgeCliService {
|
|
|
124
129
|
if (args.command === "cleanup") {
|
|
125
130
|
return await cleanupRepos(args.config, args.mode, args.force, io);
|
|
126
131
|
}
|
|
132
|
+
if (args.command === "sandbox-init") {
|
|
133
|
+
return await initializeSandbox(args.repoPath, io);
|
|
134
|
+
}
|
|
135
|
+
if (args.command === "sandbox-doctor") {
|
|
136
|
+
return await diagnoseSandbox(args.repoPath, args.requirePinnedBase, io);
|
|
137
|
+
}
|
|
138
|
+
if (args.command === "sandbox-reap") {
|
|
139
|
+
return await reapSandboxes(io);
|
|
140
|
+
}
|
|
127
141
|
if (args.command === "mcp") {
|
|
128
142
|
const parsed = parseCompletionMcpArgs(args.argv);
|
|
129
143
|
if (!parsed.ok) {
|
|
@@ -252,6 +266,9 @@ export function parseArgs(argv) {
|
|
|
252
266
|
if (argv[0] === "mcp") {
|
|
253
267
|
return { ok: true, command: "mcp", argv };
|
|
254
268
|
}
|
|
269
|
+
if (argv[0] === "sandbox") {
|
|
270
|
+
return parseSandboxArgs(argv);
|
|
271
|
+
}
|
|
255
272
|
if (argv[0] !== "run-once") {
|
|
256
273
|
return {
|
|
257
274
|
ok: false,
|
|
@@ -282,6 +299,151 @@ export function parseArgs(argv) {
|
|
|
282
299
|
resultPath: path.resolve(resultPath)
|
|
283
300
|
};
|
|
284
301
|
}
|
|
302
|
+
function parseSandboxArgs(argv) {
|
|
303
|
+
const action = argv[1];
|
|
304
|
+
let repoPath = process.cwd();
|
|
305
|
+
let requirePinnedBase = false;
|
|
306
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
307
|
+
const arg = argv[index];
|
|
308
|
+
if (arg === "--repo") {
|
|
309
|
+
repoPath = argv[++index] ?? "";
|
|
310
|
+
if (!repoPath)
|
|
311
|
+
return { ok: false, error: "--repo requires a path." };
|
|
312
|
+
}
|
|
313
|
+
else if (arg === "--require-pinned-base" && action === "doctor") {
|
|
314
|
+
requirePinnedBase = true;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
return { ok: false, error: `Unknown sandbox argument: ${arg}` };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (action === "init")
|
|
321
|
+
return { ok: true, command: "sandbox-init", repoPath: path.resolve(repoPath) };
|
|
322
|
+
if (action === "doctor")
|
|
323
|
+
return { ok: true, command: "sandbox-doctor", repoPath: path.resolve(repoPath), requirePinnedBase };
|
|
324
|
+
if (action === "reap" && argv.length === 2)
|
|
325
|
+
return { ok: true, command: "sandbox-reap" };
|
|
326
|
+
return { ok: false, error: "Usage: forge-bridge sandbox <init|doctor|reap> [--repo <path>] [--require-pinned-base]" };
|
|
327
|
+
}
|
|
328
|
+
const sandboxContractTemplate = `schemaVersion: 1
|
|
329
|
+
workingDirectory: /workspace
|
|
330
|
+
commands:
|
|
331
|
+
install: npm ci
|
|
332
|
+
test: npm test
|
|
333
|
+
caches:
|
|
334
|
+
- name: npm
|
|
335
|
+
mode: readonly
|
|
336
|
+
services: []
|
|
337
|
+
environment: []
|
|
338
|
+
artifacts: []
|
|
339
|
+
logs: []
|
|
340
|
+
browser: false
|
|
341
|
+
`;
|
|
342
|
+
const sandboxDockerfileTemplate = `# Pin this base by digest before enabling sandbox_required.
|
|
343
|
+
FROM node:22.17.0-bookworm-slim
|
|
344
|
+
WORKDIR /workspace
|
|
345
|
+
`;
|
|
346
|
+
async function initializeSandbox(repoPath, io) {
|
|
347
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
348
|
+
const files = [
|
|
349
|
+
["forge.sandbox.yml", sandboxContractTemplate],
|
|
350
|
+
["Dockerfile", sandboxDockerfileTemplate],
|
|
351
|
+
["forge.env.example", "# List non-secret variable names in forge.sandbox.yml. Never commit secret values.\n"]
|
|
352
|
+
];
|
|
353
|
+
await fs.mkdir(forgePath, { recursive: true });
|
|
354
|
+
let created = 0;
|
|
355
|
+
for (const [name, contents] of files) {
|
|
356
|
+
const target = path.join(forgePath, name);
|
|
357
|
+
try {
|
|
358
|
+
const handle = await fs.open(target, "wx", 0o600);
|
|
359
|
+
await handle.writeFile(contents);
|
|
360
|
+
await handle.close();
|
|
361
|
+
io.stdout.write(`Created ${path.relative(repoPath, target)}\n`);
|
|
362
|
+
created += 1;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (error.code !== "EEXIST")
|
|
366
|
+
throw error;
|
|
367
|
+
io.stdout.write(`Kept existing ${path.relative(repoPath, target)}\n`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
io.stdout.write(created > 0 ? "Sandbox scaffold is ready. Run `forge-bridge sandbox doctor`.\n" : "Sandbox scaffold already exists.\n");
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
|
|
374
|
+
try {
|
|
375
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
376
|
+
const contractSource = await fs.readFile(path.join(forgePath, "forge.sandbox.yml"), "utf8");
|
|
377
|
+
const dockerfile = await fs.readFile(path.join(forgePath, "Dockerfile"), "utf8");
|
|
378
|
+
const contract = validateSandboxContract(contractSource);
|
|
379
|
+
const warnings = validateDockerfilePolicy(dockerfile, requirePinnedBase);
|
|
380
|
+
io.stdout.write(`Contract: valid (${contract.digest})\n`);
|
|
381
|
+
for (const warning of warnings)
|
|
382
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
383
|
+
const config = defaultBridgeServiceConfig();
|
|
384
|
+
const preflight = new DockerPreflight({
|
|
385
|
+
dockerExecutable: config.dockerExecutable,
|
|
386
|
+
dockerContext: config.dockerContext,
|
|
387
|
+
stateRoot: config.sandboxStateRoot,
|
|
388
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
389
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
390
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
391
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
392
|
+
requireRootless: config.requireRootlessDocker
|
|
393
|
+
});
|
|
394
|
+
try {
|
|
395
|
+
const result = await preflight.refresh();
|
|
396
|
+
if (!result.healthy) {
|
|
397
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
398
|
+
return 1;
|
|
399
|
+
}
|
|
400
|
+
io.stdout.write(`Docker: healthy (${result.runtimeVersion}; capacity ${result.maxConcurrentSandboxes})\n`);
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
await preflight.release();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function reapSandboxes(io) {
|
|
413
|
+
const config = defaultBridgeServiceConfig();
|
|
414
|
+
if (!/^[0-9a-f-]{32,36}$/iu.test(config.runnerId)) {
|
|
415
|
+
io.stderr.write("FORGE_RUNNER_ID must identify the runner whose exact-labeled resources may be reaped.\n");
|
|
416
|
+
return 2;
|
|
417
|
+
}
|
|
418
|
+
const preflight = new DockerPreflight({
|
|
419
|
+
dockerExecutable: config.dockerExecutable,
|
|
420
|
+
dockerContext: config.dockerContext,
|
|
421
|
+
stateRoot: config.sandboxStateRoot,
|
|
422
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
423
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
424
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
425
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
426
|
+
requireRootless: config.requireRootlessDocker
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
const result = await preflight.refresh();
|
|
430
|
+
if (!result.healthy && result.unavailableCode !== "docker_disk_low") {
|
|
431
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
432
|
+
return 1;
|
|
433
|
+
}
|
|
434
|
+
const runtime = new DockerSandboxRuntime(config);
|
|
435
|
+
await runtime.reap({ diskLow: result.diskLow });
|
|
436
|
+
io.stdout.write(`Sandbox reaper completed for runner ${config.runnerId}.\n`);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
441
|
+
return 1;
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
await preflight.release();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
285
447
|
function parseConnectArgs(argv) {
|
|
286
448
|
let serverUrl = null;
|
|
287
449
|
let runnerId = null;
|
|
@@ -470,7 +632,7 @@ async function claimBridgeRunner(args) {
|
|
|
470
632
|
})
|
|
471
633
|
});
|
|
472
634
|
if (!response.ok) {
|
|
473
|
-
throw new Error(
|
|
635
|
+
throw new Error(formatPairingFailure(response.status, args.serverUrl, await response.text()));
|
|
474
636
|
}
|
|
475
637
|
const value = await response.json();
|
|
476
638
|
if (!value.runnerId || !value.deviceToken) {
|
|
@@ -499,6 +661,8 @@ function pairedConfig(current, args, claim) {
|
|
|
499
661
|
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
500
662
|
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
501
663
|
supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
|
|
664
|
+
supportsDraftAttachments: current.supportsDraftAttachments ?? true,
|
|
665
|
+
supportsDraftImageAttachments: supportsDraftImageAttachmentsFor(args.cliProviders),
|
|
502
666
|
repoMappings: current.repoMappings ?? []
|
|
503
667
|
};
|
|
504
668
|
}
|
|
@@ -543,6 +707,12 @@ async function resolvePairRepoRoot(repoRoot, configuredRoot) {
|
|
|
543
707
|
function normalizePairingCode(code) {
|
|
544
708
|
return code?.replace(/[\s-]/g, "").trim() ?? "";
|
|
545
709
|
}
|
|
710
|
+
function formatPairingFailure(status, serverUrl, body) {
|
|
711
|
+
const hint = body.includes("runner.invalid_pairing_code")
|
|
712
|
+
? " Make sure the code was generated by the same Forge server shown above."
|
|
713
|
+
: "";
|
|
714
|
+
return `Pairing failed (${status}) against ${serverUrl}: ${body}${hint}`;
|
|
715
|
+
}
|
|
546
716
|
function normalizeServerUrl(serverUrl) {
|
|
547
717
|
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
548
718
|
}
|
|
@@ -17,6 +17,8 @@ export interface BridgeLocalConfig {
|
|
|
17
17
|
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
18
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
19
19
|
readonly supportsDraftRunOnce?: boolean;
|
|
20
|
+
readonly supportsDraftAttachments?: boolean;
|
|
21
|
+
readonly supportsDraftImageAttachments?: boolean;
|
|
20
22
|
}
|
|
21
23
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
22
24
|
readonly label: string;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -54,7 +54,9 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
54
54
|
supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
|
|
55
55
|
supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
|
|
56
56
|
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
|
|
57
|
-
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce
|
|
57
|
+
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce,
|
|
58
|
+
supportsDraftAttachments: local.supportsDraftAttachments ?? config.supportsDraftAttachments,
|
|
59
|
+
supportsDraftImageAttachments: local.supportsDraftImageAttachments ?? config.supportsDraftImageAttachments
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
export async function addRepoMapping(label, repoPath) {
|
|
@@ -113,7 +115,9 @@ function normalizeLocalConfig(config) {
|
|
|
113
115
|
supportsManagedWorktrees: config.supportsManagedWorktrees,
|
|
114
116
|
supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
|
|
115
117
|
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
|
|
116
|
-
supportsDraftRunOnce: config.supportsDraftRunOnce
|
|
118
|
+
supportsDraftRunOnce: config.supportsDraftRunOnce,
|
|
119
|
+
supportsDraftAttachments: config.supportsDraftAttachments,
|
|
120
|
+
supportsDraftImageAttachments: config.supportsDraftImageAttachments
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
function emptyConfig() {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export class BoundedCommandRunner {
|
|
3
|
+
async run(command, args, options = {}) {
|
|
4
|
+
const maxOutputBytes = options.maxOutputBytes ?? 1024 * 1024;
|
|
5
|
+
return await new Promise((resolve, reject) => {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const abort = () => controller.abort(options.signal?.reason ?? "command cancelled");
|
|
8
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
9
|
+
const timeout = options.timeoutMs
|
|
10
|
+
? setTimeout(() => controller.abort(`command exceeded ${options.timeoutMs}ms`), options.timeoutMs)
|
|
11
|
+
: null;
|
|
12
|
+
timeout?.unref?.();
|
|
13
|
+
const child = spawn(command, [...args], {
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
env: options.env ? { ...options.env } : undefined,
|
|
16
|
+
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
});
|
|
20
|
+
let stdout = Buffer.alloc(0);
|
|
21
|
+
let stderr = Buffer.alloc(0);
|
|
22
|
+
const append = (current, chunk) => {
|
|
23
|
+
if (current.length >= maxOutputBytes)
|
|
24
|
+
return current;
|
|
25
|
+
return Buffer.concat([current, chunk.subarray(0, maxOutputBytes - current.length)]);
|
|
26
|
+
};
|
|
27
|
+
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
28
|
+
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
29
|
+
child.on("error", (error) => {
|
|
30
|
+
cleanup();
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
child.on("close", (code) => {
|
|
34
|
+
cleanup();
|
|
35
|
+
resolve({ exitCode: code ?? 1, stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8") });
|
|
36
|
+
});
|
|
37
|
+
if (options.stdin !== undefined) {
|
|
38
|
+
child.stdin.end(options.stdin);
|
|
39
|
+
}
|
|
40
|
+
function cleanup() {
|
|
41
|
+
if (timeout)
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
options.signal?.removeEventListener("abort", abort);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ValidatedSandboxContract } from "./sandbox.types.js";
|
|
2
|
+
export declare function validateSandboxContract(source: string): ValidatedSandboxContract;
|
|
3
|
+
export declare function validateDockerfilePolicy(source: string, requirePinnedBase: boolean): readonly string[];
|
|
4
|
+
export declare function dockerfileBaseImages(source: string): readonly string[];
|
|
5
|
+
export declare function dockerfileUpstreamImages(source: string): readonly string[];
|