@loucompanion/forge-bridge 0.1.3-dev.09aeeb7ccadf → 0.1.3-dev.1fe428dd78ab

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 (51) hide show
  1. package/README.md +44 -0
  2. package/dist/bridge/bridge.base.d.ts +5 -1
  3. package/dist/bridge/bridge.service.d.ts +36 -2
  4. package/dist/bridge/bridge.service.js +564 -14
  5. package/dist/bridge/bridge.types.d.ts +110 -1
  6. package/dist/cli/cli.help.js +12 -0
  7. package/dist/cli/cli.service.d.ts +12 -0
  8. package/dist/cli/cli.service.js +168 -1
  9. package/dist/sandbox/command-runner.d.ts +4 -0
  10. package/dist/sandbox/command-runner.js +47 -0
  11. package/dist/sandbox/contract.d.ts +5 -0
  12. package/dist/sandbox/contract.js +276 -0
  13. package/dist/sandbox/event-budget.d.ts +11 -0
  14. package/dist/sandbox/event-budget.js +27 -0
  15. package/dist/sandbox/image-policy.d.ts +33 -0
  16. package/dist/sandbox/image-policy.js +245 -0
  17. package/dist/sandbox/index.d.ts +5 -0
  18. package/dist/sandbox/index.js +5 -0
  19. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  20. package/dist/sandbox/lease-watchdog.js +67 -0
  21. package/dist/sandbox/preflight.d.ts +36 -0
  22. package/dist/sandbox/preflight.js +239 -0
  23. package/dist/sandbox/process.d.ts +20 -0
  24. package/dist/sandbox/process.js +124 -0
  25. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  26. package/dist/sandbox/provider-auth/codex.js +212 -0
  27. package/dist/sandbox/repository-lock.d.ts +5 -0
  28. package/dist/sandbox/repository-lock.js +83 -0
  29. package/dist/sandbox/runtime.d.ts +218 -0
  30. package/dist/sandbox/runtime.js +1672 -0
  31. package/dist/sandbox/sandbox.types.d.ts +83 -0
  32. package/dist/sandbox/sandbox.types.js +1 -0
  33. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  34. package/dist/sandbox/worker-supervisor.js +36 -0
  35. package/dist/sandbox/worker.d.ts +1 -0
  36. package/dist/sandbox/worker.js +228 -0
  37. package/dist/sandbox/workspace.service.d.ts +12 -0
  38. package/dist/sandbox/workspace.service.js +49 -0
  39. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  40. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  41. package/dist/session/provider-cli/provider-cli.deterministic.js +20 -9
  42. package/dist/session/provider-cli/provider-cli.prompts.d.ts +2 -0
  43. package/dist/session/provider-cli/provider-cli.prompts.js +31 -0
  44. package/dist/session/provider-cli/provider-cli.service.d.ts +3 -0
  45. package/dist/session/provider-cli/provider-cli.service.js +31 -13
  46. package/dist/session/session.service.js +14 -7
  47. package/dist/session/session.types.d.ts +41 -1
  48. package/dist/session/session.types.js +119 -2
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. 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;
@@ -54,6 +73,25 @@ export interface BridgeCapabilities {
54
73
  readonly repoMappings?: readonly BridgeRepoMapping[] | null;
55
74
  readonly os?: string | null;
56
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;
57
95
  readonly supportsApiSecretGrant?: boolean | null;
58
96
  readonly supportsManagedWorktrees?: boolean | null;
59
97
  readonly supportsManagedForgeArtifacts?: boolean | null;
@@ -67,6 +105,19 @@ export interface BridgeHelloRequest {
67
105
  readonly protocolVersion: number;
68
106
  readonly capabilities: BridgeCapabilities;
69
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;
70
121
  }
71
122
  export interface BridgeHeartbeatRequest {
72
123
  readonly status?: string | null;
@@ -82,17 +133,75 @@ export interface BridgeCallbackAck {
82
133
  export interface BridgeDispatchEventCallback {
83
134
  readonly leaseId: string;
84
135
  readonly fencingToken: string;
136
+ readonly dispatchAttemptId?: string | null;
137
+ readonly runnerInstanceId?: string | null;
85
138
  readonly event: RunnerDispatchEvent;
86
139
  }
87
140
  export interface BridgeDispatchResultCallback {
88
141
  readonly leaseId: string;
89
142
  readonly fencingToken: string;
143
+ readonly dispatchAttemptId?: string | null;
144
+ readonly runnerInstanceId?: string | null;
90
145
  readonly result: RunOnceResultPacket;
91
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
+ }
92
180
  export interface BridgeCancelRequest {
93
181
  readonly leaseId: string;
94
182
  readonly fencingToken: string;
95
183
  readonly reason?: string | null;
96
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
+ }
97
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>;
98
206
  export type BridgeCancelHandler = (request: BridgeCancelRequest) => Promise<void>;
207
+ export type BridgeReapHandler = (request: BridgeReapRequest) => Promise<void>;
@@ -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;
@@ -12,6 +12,9 @@ import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge
12
12
  import { forgeBridgeVersion } from "../version.js";
13
13
  import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
14
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";
15
18
  import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
16
19
  import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
17
20
  import { commandHelpText, helpText } from "./cli.help.js";
@@ -89,6 +92,7 @@ export class ForgeBridgeCliService {
89
92
  if (args.command === "pair") {
90
93
  try {
91
94
  const current = readLocalConfigSync();
95
+ io.stdout.write(`Pairing with server: ${args.serverUrl}\n`);
92
96
  const code = await resolvePairingCode(args.code);
93
97
  const repoRoot = await resolvePairRepoRoot(args.repoRoot, current.defaultRepoRoot);
94
98
  await fs.mkdir(repoRoot, { recursive: true });
@@ -125,6 +129,15 @@ export class ForgeBridgeCliService {
125
129
  if (args.command === "cleanup") {
126
130
  return await cleanupRepos(args.config, args.mode, args.force, io);
127
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
+ }
128
141
  if (args.command === "mcp") {
129
142
  const parsed = parseCompletionMcpArgs(args.argv);
130
143
  if (!parsed.ok) {
@@ -253,6 +266,9 @@ export function parseArgs(argv) {
253
266
  if (argv[0] === "mcp") {
254
267
  return { ok: true, command: "mcp", argv };
255
268
  }
269
+ if (argv[0] === "sandbox") {
270
+ return parseSandboxArgs(argv);
271
+ }
256
272
  if (argv[0] !== "run-once") {
257
273
  return {
258
274
  ok: false,
@@ -283,6 +299,151 @@ export function parseArgs(argv) {
283
299
  resultPath: path.resolve(resultPath)
284
300
  };
285
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
+ }
286
447
  function parseConnectArgs(argv) {
287
448
  let serverUrl = null;
288
449
  let runnerId = null;
@@ -471,7 +632,7 @@ async function claimBridgeRunner(args) {
471
632
  })
472
633
  });
473
634
  if (!response.ok) {
474
- throw new Error(`Pairing failed (${response.status}): ${await response.text()}`);
635
+ throw new Error(formatPairingFailure(response.status, args.serverUrl, await response.text()));
475
636
  }
476
637
  const value = await response.json();
477
638
  if (!value.runnerId || !value.deviceToken) {
@@ -546,6 +707,12 @@ async function resolvePairRepoRoot(repoRoot, configuredRoot) {
546
707
  function normalizePairingCode(code) {
547
708
  return code?.replace(/[\s-]/g, "").trim() ?? "";
548
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
+ }
549
716
  function normalizeServerUrl(serverUrl) {
550
717
  return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
551
718
  }
@@ -0,0 +1,4 @@
1
+ import type { CommandOptions, CommandResult, CommandRunner } from "./sandbox.types.js";
2
+ export declare class BoundedCommandRunner implements CommandRunner {
3
+ run(command: string, args: readonly string[], options?: CommandOptions): Promise<CommandResult>;
4
+ }
@@ -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[];