@loucompanion/forge-bridge 0.1.3-dev.bf78fdcaa623 → 0.1.3-dev.c24058d57ab3

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.
@@ -1063,7 +1063,7 @@ function defaultSandboxResources() {
1063
1063
  return {
1064
1064
  sandboxSlots: 1,
1065
1065
  cpuMillis: 2000,
1066
- memoryBytes: 4 * 1024 ** 3,
1066
+ memoryBytes: 2 * 1024 ** 3,
1067
1067
  workspaceBytes: 10 * 1024 ** 3,
1068
1068
  pids: 512,
1069
1069
  buildSlots: 0
@@ -1,4 +1,5 @@
1
1
  import { type SandboxProcessRequest, type SandboxProcessResult } from "./process.js";
2
+ export type SandboxImageLayer = "project" | "tooling" | "agent";
2
3
  export interface SandboxImagePolicyRequest {
3
4
  readonly key: string;
4
5
  readonly project: string;
@@ -10,7 +11,8 @@ export interface SandboxImagePolicyRequest {
10
11
  readonly toolingImage: string;
11
12
  readonly upstreamImages: readonly string[];
12
13
  readonly codexVersion: string;
13
- readonly images: Readonly<Record<"project" | "tooling" | "agent", string>>;
14
+ readonly images: Readonly<Record<SandboxImageLayer, string>>;
15
+ readonly imageScanTargets?: Readonly<Partial<Record<SandboxImageLayer, string>>>;
14
16
  readonly environment?: NodeJS.ProcessEnv;
15
17
  readonly signal?: AbortSignal;
16
18
  readonly runCommand?: (command: SandboxProcessRequest) => Promise<SandboxProcessResult>;
@@ -34,6 +34,7 @@ export async function evaluateSandboxImagePolicy(request) {
34
34
  status = "warning";
35
35
  }
36
36
  for (const [layer, imageDigest] of Object.entries(request.images)) {
37
+ const scanTarget = request.imageScanTargets?.[layer] ?? imageDigest;
37
38
  const provenance = {
38
39
  schemaVersion: 1,
39
40
  predicateType: "https://slsa.dev/provenance/v1",
@@ -67,7 +68,7 @@ export async function evaluateSandboxImagePolicy(request) {
67
68
  if (sbomExecutable) {
68
69
  const sbom = await runCommand({
69
70
  command: sbomExecutable,
70
- args: [imageDigest, "-o", "spdx-json"],
71
+ args: [scanTarget, "-o", "spdx-json"],
71
72
  timeoutMs: 10 * 60_000,
72
73
  maxOutputBytes: 32 * 1024 * 1024,
73
74
  signal: request.signal
@@ -86,7 +87,7 @@ export async function evaluateSandboxImagePolicy(request) {
86
87
  if (vulnerabilityExecutable) {
87
88
  const scan = await runCommand({
88
89
  command: vulnerabilityExecutable,
89
- args: ["image", "--scanners", "vuln", "--format", "json", "--severity", "HIGH,CRITICAL", "--exit-code", "1", "--no-progress", imageDigest],
90
+ args: ["image", "--scanners", "vuln", "--format", "json", "--severity", "HIGH,CRITICAL", "--exit-code", "1", "--no-progress", scanTarget],
90
91
  timeoutMs: 15 * 60_000,
91
92
  maxOutputBytes: 32 * 1024 * 1024,
92
93
  signal: request.signal
@@ -103,6 +104,7 @@ export async function evaluateSandboxImagePolicy(request) {
103
104
  }
104
105
  artifacts[layer] = {
105
106
  imageDigest,
107
+ scanTarget,
106
108
  provenanceSha256: sha256(provenanceBytes),
107
109
  provenanceSignatureSha256: signatureDigest,
108
110
  sbomSha256: sbomDigest,
@@ -108,7 +108,7 @@ export class DockerPreflight {
108
108
  const availableMemoryBytes = os.freemem();
109
109
  const cpuCount = Math.max(1, os.availableParallelism?.() ?? os.cpus().length);
110
110
  const availableCpuMillis = Math.max(0, (cpuCount - 1) * 1000);
111
- const memorySlots = Math.max(0, Math.floor((availableMemoryBytes - 2 * 1024 ** 3) / (4 * 1024 ** 3)));
111
+ const memorySlots = Math.max(0, Math.floor((availableMemoryBytes - 2 * 1024 ** 3) / (2 * 1024 ** 3)));
112
112
  const cpuSlots = Math.max(0, Math.floor((cpuCount - 1) / 2));
113
113
  const configuredMax = this.options.maxConcurrentSandboxes ?? positiveInteger(process.env.FORGE_MAX_CONCURRENT_SANDBOXES, 3);
114
114
  const maxConcurrentSandboxes = diskLow ? 0 : Math.max(1, Math.min(3, configuredMax, Math.max(1, cpuSlots), Math.max(1, memorySlots)));
@@ -188,7 +188,25 @@ export declare function sandboxRuntimeVolumes(resources: ResourceNames, includeE
188
188
  export declare function sandboxRuntimeCleanupVolumeNames(resources: ResourceNames, includeCredentials?: boolean): string[];
189
189
  export declare function mayReapManagedResource(labels: Readonly<Record<string, string>>, runnerId: string, liveAttempts: ReadonlySet<string>): boolean;
190
190
  export declare function shouldReapSandboxLedger(ledger: Readonly<Pick<SandboxLedger, "dispatchAttemptId" | "fencingToken" | "runnerInstanceId" | "expiresAt">>, currentRunnerInstanceId: string, serverAttempt: SandboxAttemptSnapshot | undefined, snapshotAvailable: boolean, nowMs?: number): boolean;
191
- export declare function buildxBuildArgs(builder: string, metadataFile: string, tail: readonly string[], environment?: NodeJS.ProcessEnv): string[];
191
+ export declare function prepareOverlayBuildRoot(overlayRoot: string): Promise<void>;
192
+ export declare function extractOciLayoutArchive(archivePath: string, destination: string, signal?: AbortSignal): Promise<void>;
193
+ export declare function ociLayoutContextRef(layoutPath: string): string;
194
+ export declare function formatOciExtractionFailure(result: {
195
+ readonly stdout: string;
196
+ readonly stderr: string;
197
+ readonly truncated: boolean;
198
+ }): string;
199
+ export declare function readOciLayoutImageDigest(layoutRoot: string): Promise<string>;
200
+ export declare function removeExtractedOciLayout(layoutRoot: string): Promise<void>;
201
+ export declare function ociLayoutScanTarget(layoutPath: string): string;
202
+ export declare function agentOverlayDockerignore(): string;
203
+ export declare function agentOverlayDockerfile(toolingImage: string, projectContextName: string, installMode: BridgePackageInstallMode, codexVersion: string): string;
204
+ export type BridgePackageInstallMode = "ci" | "install";
205
+ export declare function copyBridgePackageManifests(packageRoot: string, bridgeRoot: string): Promise<BridgePackageInstallMode>;
206
+ export declare function bridgePackageInstallCommand(mode: BridgePackageInstallMode, codexVersion: string): string;
207
+ export declare function buildxBuildArgs(builder: string, metadataFile: string, tail: readonly string[], environment?: NodeJS.ProcessEnv, options?: {
208
+ readonly load?: boolean;
209
+ }): string[];
192
210
  export declare function buildResourcePolicy(environment?: NodeJS.ProcessEnv): {
193
211
  readonly nofile: number;
194
212
  readonly sharedMemory: string;
@@ -530,29 +530,39 @@ export class DockerSandboxRuntime {
530
530
  await sink.emit(event("sandbox_build_started", `Building sandbox image ${key.slice(0, 12)}.`, { cacheKey: key }));
531
531
  const builder = await this.ensureBuildxBuilder(signal);
532
532
  const metadataRoot = path.join(this.config.sandboxStateRoot, "attestations", key, "buildkit");
533
- await fs.mkdir(metadataRoot, { recursive: true, mode: 0o700 });
534
- const cached = await this.docker(["image", "inspect", projectTag, "--format", "{{.Id}}"], 15_000).catch(() => null);
535
- let projectImage = cached?.stdout.trim();
536
- if (!projectImage) {
537
- await this.runBuildStage("project", key, buildxBuildArgs(builder, path.join(metadataRoot, "project.json"), ["--pull=false", ...imageLabelArgs(project, key, "project"), "--tag", projectTag, "--file", path.join(contextRoot, ".forge", "Dockerfile"), contextRoot]), sink, signal);
538
- projectImage = (await this.docker(["image", "inspect", projectTag, "--format", "{{.Id}}"], 15_000)).stdout.trim();
539
- }
540
- if (!/^sha256:[0-9a-f]{64}$/i.test(projectImage))
541
- throw new Error("sandbox_build_failed: project image did not resolve to an immutable digest.");
542
533
  const overlayRoot = path.join(this.config.sandboxStateRoot, "build", `overlay-${key}`);
543
- await fs.rm(overlayRoot, { recursive: true, force: true });
544
- await fs.mkdir(path.join(overlayRoot, "bridge"), { recursive: true });
534
+ await prepareOverlayBuildRoot(overlayRoot);
535
+ await fs.mkdir(metadataRoot, { recursive: true, mode: 0o700 });
536
+ const projectOciArchive = path.join(overlayRoot, "project.oci.tar");
537
+ const projectOciRoot = path.join(overlayRoot, "project-oci");
538
+ await this.runBuildStage("project", key, buildxBuildArgs(builder, path.join(metadataRoot, "project.json"), [
539
+ "--pull=false",
540
+ ...imageLabelArgs(project, key, "project"),
541
+ "--tag", projectTag,
542
+ "--output", `type=oci,dest=${projectOciArchive}`,
543
+ "--file", path.join(contextRoot, ".forge", "Dockerfile"),
544
+ contextRoot
545
+ ], process.env, { load: false }), sink, signal);
546
+ await extractOciLayoutArchive(projectOciArchive, projectOciRoot, signal);
547
+ const projectImage = await readOciLayoutImageDigest(projectOciRoot);
548
+ const projectContextName = "forge_project";
545
549
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
546
550
  await fs.cp(path.join(packageRoot, "dist"), path.join(overlayRoot, "bridge", "dist"), { recursive: true });
547
- await fs.copyFile(path.join(packageRoot, "package.json"), path.join(overlayRoot, "bridge", "package.json"));
548
- await fs.copyFile(path.join(packageRoot, "package-lock.json"), path.join(overlayRoot, "bridge", "package-lock.json"));
549
- const dockerfile = `FROM ${toolingImage} AS tooling\nWORKDIR /opt/forge-bridge\nCOPY bridge/package*.json ./\nRUN npm ci --omit=dev --ignore-scripts && npm install --global @openai/codex@${codexVersion} && npm cache clean --force\nCOPY bridge/dist ./dist\nFROM ${projectImage}\nUSER 0\nCOPY --from=tooling /usr/local/bin/node /usr/local/bin/node\nCOPY --from=tooling /usr/local/bin/codex /usr/local/bin/codex\nCOPY --from=tooling /usr/local/lib/node_modules/@openai/codex /usr/local/lib/node_modules/@openai/codex\nCOPY --from=tooling /opt/forge-bridge /opt/forge-bridge\nRUN (getent group 10001 || groupadd -g 10001 forge) && (getent passwd 10001 || useradd -u 10001 -g 10001 -d /home/forge -m forge)\nENV HOME=/home/forge TMPDIR=/tmp\nENTRYPOINT []\nCMD []\nUSER 10001:10001\n`;
551
+ const installMode = await copyBridgePackageManifests(packageRoot, path.join(overlayRoot, "bridge"));
552
+ const dockerfile = agentOverlayDockerfile(toolingImage, projectContextName, installMode, codexVersion);
550
553
  await fs.writeFile(path.join(overlayRoot, "Dockerfile"), dockerfile);
554
+ await fs.writeFile(path.join(overlayRoot, ".dockerignore"), agentOverlayDockerignore());
551
555
  await this.runBuildStage("tooling", key, buildxBuildArgs(builder, path.join(metadataRoot, "tooling.json"), ["--pull=false", "--target", "tooling", ...imageLabelArgs(project, key, "tooling"), "--tag", helperTag, overlayRoot]), sink, signal);
552
556
  const helperImageDigest = (await this.docker(["image", "inspect", helperTag, "--format", "{{.Id}}"], 15_000)).stdout.trim();
553
557
  if (!/^sha256:[0-9a-f]{64}$/i.test(helperImageDigest))
554
558
  throw new Error("sandbox_build_failed: helper image did not resolve to an immutable digest.");
555
- await this.runBuildStage("agent", key, buildxBuildArgs(builder, path.join(metadataRoot, "agent.json"), ["--pull=false", ...imageLabelArgs(project, key, "agent"), "--tag", overlayTag, overlayRoot]), sink, signal);
559
+ await this.runBuildStage("agent", key, buildxBuildArgs(builder, path.join(metadataRoot, "agent.json"), [
560
+ "--pull=false",
561
+ "--build-context", `${projectContextName}=${ociLayoutContextRef(projectOciRoot)}`,
562
+ ...imageLabelArgs(project, key, "agent"),
563
+ "--tag", overlayTag,
564
+ overlayRoot
565
+ ]), sink, signal);
556
566
  const digest = (await this.docker(["image", "inspect", overlayTag, "--format", "{{.Id}}"], 15_000)).stdout.trim();
557
567
  if (!/^sha256:[0-9a-f]{64}$/i.test(digest))
558
568
  throw new Error("sandbox_build_failed: overlay image did not resolve to an immutable digest.");
@@ -568,9 +578,11 @@ export class DockerSandboxRuntime {
568
578
  upstreamImages,
569
579
  codexVersion,
570
580
  images: { project: projectImage, tooling: helperImageDigest, agent: digest },
581
+ imageScanTargets: { project: ociLayoutScanTarget(projectOciRoot) },
571
582
  signal
572
583
  });
573
584
  await sink.emit(event(imagePolicy.status === "passed" ? "sandbox_image_policy_passed" : "sandbox_image_policy_warning", imagePolicy.status === "passed" ? `Image policy passed for ${digest}.` : `Image attestations are incomplete for adoption-mode image ${digest}.`, { policyDigest: imagePolicy.policyDigest }));
585
+ await removeExtractedOciLayout(projectOciRoot);
574
586
  await this.writeImageCacheRecord({
575
587
  schemaVersion: 1,
576
588
  key,
@@ -1413,17 +1425,114 @@ function validImageCacheRecord(value) {
1413
1425
  && /^sha256:[0-9a-f]{64}$/i.test(value.helperImageDigest)
1414
1426
  && /^[0-9a-f]{64}$/i.test(value.policyDigest);
1415
1427
  }
1416
- export function buildxBuildArgs(builder, metadataFile, tail, environment = process.env) {
1428
+ export async function prepareOverlayBuildRoot(overlayRoot) {
1429
+ const buildRoot = path.dirname(overlayRoot);
1430
+ await fs.mkdir(buildRoot, { recursive: true, mode: 0o700 });
1431
+ await fs.chmod(buildRoot, 0o700);
1432
+ await fs.rm(overlayRoot, { recursive: true, force: true });
1433
+ await fs.mkdir(overlayRoot, { recursive: true, mode: 0o700 });
1434
+ await fs.chmod(overlayRoot, 0o700);
1435
+ const bridgeRoot = path.join(overlayRoot, "bridge");
1436
+ await fs.mkdir(bridgeRoot, { recursive: true, mode: 0o700 });
1437
+ await fs.chmod(bridgeRoot, 0o700);
1438
+ }
1439
+ export async function extractOciLayoutArchive(archivePath, destination, signal) {
1440
+ await fs.rm(destination, { recursive: true, force: true });
1441
+ await fs.mkdir(destination, { recursive: true, mode: 0o700 });
1442
+ await fs.chmod(destination, 0o700);
1443
+ const result = await runSandboxProcess({
1444
+ command: process.platform === "win32" ? "tar.exe" : "tar",
1445
+ args: ["-xf", archivePath, "-C", destination],
1446
+ timeoutMs: 5 * 60_000,
1447
+ maxOutputBytes: 1024 * 1024,
1448
+ signal
1449
+ });
1450
+ if (result.exitCode !== 0 || result.truncated) {
1451
+ throw new Error(`sandbox_build_failed: project OCI layout extraction failed with exit code ${result.exitCode}; ${formatOciExtractionFailure(result)}. Archive retained at ${archivePath}.`);
1452
+ }
1453
+ await fs.rm(archivePath, { force: true }).catch(() => undefined);
1454
+ }
1455
+ export function ociLayoutContextRef(layoutPath) {
1456
+ const absolute = /^[A-Za-z]:[\\/]/u.test(layoutPath) ? layoutPath : path.resolve(layoutPath);
1457
+ const normalized = absolute.replace(/\\/gu, "/");
1458
+ // BuildKit's oci-layout resolver expects a raw filesystem path here; it does not percent-decode
1459
+ // URI path escapes such as %20 before opening index.json.
1460
+ return normalized.startsWith("/") ? `oci-layout://${normalized}` : `oci-layout:///${normalized}`;
1461
+ }
1462
+ export function formatOciExtractionFailure(result) {
1463
+ const output = result.stderr.trim() || result.stdout.trim() || "no tar output";
1464
+ return `tar output: ${output.slice(0, 2048)}${result.truncated ? " (output truncated)" : ""}`;
1465
+ }
1466
+ export async function readOciLayoutImageDigest(layoutRoot) {
1467
+ const indexPath = path.join(layoutRoot, "index.json");
1468
+ const index = JSON.parse(await fs.readFile(indexPath, "utf8"));
1469
+ const manifest = index.manifests?.find(entry => typeof entry.digest === "string"
1470
+ && /^sha256:[0-9a-f]{64}$/iu.test(entry.digest)
1471
+ && entry.annotations?.["vnd.docker.reference.type"] !== "attestation-manifest");
1472
+ if (!manifest || typeof manifest.digest !== "string") {
1473
+ throw new Error("sandbox_build_failed: project OCI layout did not expose an immutable image digest.");
1474
+ }
1475
+ return manifest.digest;
1476
+ }
1477
+ export async function removeExtractedOciLayout(layoutRoot) {
1478
+ await fs.rm(layoutRoot, { recursive: true, force: true });
1479
+ }
1480
+ export function ociLayoutScanTarget(layoutPath) {
1481
+ const absolute = /^[A-Za-z]:[\\/]/u.test(layoutPath) ? layoutPath : path.resolve(layoutPath);
1482
+ return `oci-dir:${absolute.replace(/\\/gu, "/")}`;
1483
+ }
1484
+ export function agentOverlayDockerignore() {
1485
+ return "project.oci.tar\nproject-oci\nproject-oci/**\n";
1486
+ }
1487
+ export function agentOverlayDockerfile(toolingImage, projectContextName, installMode, codexVersion) {
1488
+ return `FROM ${toolingImage} AS tooling
1489
+ WORKDIR /opt/forge-bridge
1490
+ COPY bridge/package*.json ./
1491
+ RUN ${bridgePackageInstallCommand(installMode, codexVersion)}
1492
+ COPY bridge/dist ./dist
1493
+ FROM ${projectContextName}
1494
+ USER 0
1495
+ COPY --from=tooling /usr/local/bin/node /usr/local/bin/node
1496
+ COPY --from=tooling /usr/local/bin/codex /usr/local/bin/codex
1497
+ COPY --from=tooling /usr/local/lib/node_modules/@openai/codex /usr/local/lib/node_modules/@openai/codex
1498
+ COPY --from=tooling /opt/forge-bridge /opt/forge-bridge
1499
+ RUN (getent group 10001 || groupadd -g 10001 forge) && (getent passwd 10001 || useradd -u 10001 -g 10001 -d /home/forge -m forge)
1500
+ ENV HOME=/home/forge TMPDIR=/tmp
1501
+ ENTRYPOINT []
1502
+ CMD []
1503
+ USER 10001:10001
1504
+ `;
1505
+ }
1506
+ export async function copyBridgePackageManifests(packageRoot, bridgeRoot) {
1507
+ await fs.copyFile(path.join(packageRoot, "package.json"), path.join(bridgeRoot, "package.json"));
1508
+ return await copyOptionalBridgeManifest(packageRoot, bridgeRoot, "package-lock.json") ? "ci" : "install";
1509
+ }
1510
+ export function bridgePackageInstallCommand(mode, codexVersion) {
1511
+ const bridgeInstall = mode === "ci"
1512
+ ? "npm ci --omit=dev --ignore-scripts"
1513
+ : "npm install --omit=dev --ignore-scripts";
1514
+ return `${bridgeInstall} && npm install --global @openai/codex@${codexVersion} && npm cache clean --force`;
1515
+ }
1516
+ async function copyOptionalBridgeManifest(packageRoot, bridgeRoot, fileName) {
1517
+ try {
1518
+ await fs.copyFile(path.join(packageRoot, fileName), path.join(bridgeRoot, fileName));
1519
+ return true;
1520
+ }
1521
+ catch (error) {
1522
+ if (error.code === "ENOENT")
1523
+ return false;
1524
+ throw error;
1525
+ }
1526
+ }
1527
+ export function buildxBuildArgs(builder, metadataFile, tail, environment = process.env, options = {}) {
1417
1528
  if (!/^forge-[0-9a-f]{20}$/u.test(builder))
1418
1529
  throw new Error("sandbox_build_failed: invalid dedicated builder name.");
1419
1530
  const resources = buildResourcePolicy(environment);
1420
- return [
1421
- "buildx", "build", "--builder", builder, "--load",
1422
- "--ulimit", `nofile=${resources.nofile}:${resources.nofile}`,
1423
- "--shm-size", resources.sharedMemory,
1424
- "--provenance=mode=max", "--sbom=true", "--metadata-file", metadataFile,
1425
- ...tail
1426
- ];
1531
+ const args = ["buildx", "build", "--builder", builder];
1532
+ if (options.load !== false)
1533
+ args.push("--load");
1534
+ args.push("--ulimit", `nofile=${resources.nofile}:${resources.nofile}`, "--shm-size", resources.sharedMemory, "--provenance=mode=max", "--sbom=true", "--metadata-file", metadataFile, ...tail);
1535
+ return args;
1427
1536
  }
1428
1537
  export function buildResourcePolicy(environment = process.env) {
1429
1538
  return {
@@ -93,7 +93,7 @@ function parseSandboxResourceRequest(value) {
93
93
  return {
94
94
  sandboxSlots: 1,
95
95
  cpuMillis: 2000,
96
- memoryBytes: 4 * 1024 ** 3,
96
+ memoryBytes: 2 * 1024 ** 3,
97
97
  workspaceBytes: 10 * 1024 ** 3,
98
98
  pids: 512,
99
99
  buildSlots: 1
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.3-dev.bf78fdcaa623";
1
+ export declare const forgeBridgeVersion = "0.1.3-dev.c24058d57ab3";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.3-dev.bf78fdcaa623";
1
+ export const forgeBridgeVersion = "0.1.3-dev.c24058d57ab3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.3-dev.bf78fdcaa623",
3
+ "version": "0.1.3-dev.c24058d57ab3",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",