@loucompanion/forge-bridge 0.1.3-dev.bf78fdcaa623 → 0.1.3-dev.c28767e94230
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/dist/bridge/bridge.service.d.ts +4 -0
- package/dist/bridge/bridge.service.js +4 -4
- package/dist/sandbox/image-policy.d.ts +3 -1
- package/dist/sandbox/image-policy.js +4 -2
- package/dist/sandbox/preflight.d.ts +4 -0
- package/dist/sandbox/preflight.js +26 -9
- package/dist/sandbox/runtime.d.ts +19 -1
- package/dist/sandbox/runtime.js +132 -23
- package/dist/session/session.types.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -76,5 +76,9 @@ export declare class SignalRBridgeConnection implements BridgeConnection {
|
|
|
76
76
|
}
|
|
77
77
|
export declare function defaultBridgeServiceConfig(overrides?: Partial<BridgeServiceConfig>): BridgeServiceConfig;
|
|
78
78
|
export declare function resolveBridgeRunnerHubUrl(serverUrl: string): URL;
|
|
79
|
+
export declare function canReapManagedSandboxes(preflight: {
|
|
80
|
+
readonly healthy: boolean;
|
|
81
|
+
readonly unavailableCode?: string | null;
|
|
82
|
+
}): boolean;
|
|
79
83
|
export type SandboxResourceVector = DispatchSandboxResourceRequest;
|
|
80
84
|
export declare function canReserveSandboxResources(used: SandboxResourceVector, requested: SandboxResourceVector, capacity: SandboxResourceVector): boolean;
|
|
@@ -9,7 +9,7 @@ import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
|
9
9
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
10
10
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
11
11
|
import { forgeBridgeVersion } from "../version.js";
|
|
12
|
-
import { DockerPreflight } from "../sandbox/preflight.js";
|
|
12
|
+
import { canRunDockerMaintenance, DockerPreflight } from "../sandbox/preflight.js";
|
|
13
13
|
import { DockerSandboxRuntime } from "../sandbox/runtime.js";
|
|
14
14
|
import { codexSandboxProviderAuthAdapter, sandboxProviderAuthAdapterFor, sandboxProviderAuthUnavailableMessage } from "../sandbox/provider-auth/codex.js";
|
|
15
15
|
export class ForgeBridgeService {
|
|
@@ -1001,8 +1001,8 @@ function abortPromise(signal) {
|
|
|
1001
1001
|
}
|
|
1002
1002
|
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
1003
1003
|
}
|
|
1004
|
-
function canReapManagedSandboxes(preflight) {
|
|
1005
|
-
return preflight
|
|
1004
|
+
export function canReapManagedSandboxes(preflight) {
|
|
1005
|
+
return canRunDockerMaintenance(preflight);
|
|
1006
1006
|
}
|
|
1007
1007
|
function sandboxNetworkPolicyFailure(dispatch) {
|
|
1008
1008
|
const sandbox = dispatch.sandbox;
|
|
@@ -1063,7 +1063,7 @@ function defaultSandboxResources() {
|
|
|
1063
1063
|
return {
|
|
1064
1064
|
sandboxSlots: 1,
|
|
1065
1065
|
cpuMillis: 2000,
|
|
1066
|
-
memoryBytes:
|
|
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<
|
|
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: [
|
|
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",
|
|
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,
|
|
@@ -27,6 +27,10 @@ export declare class DockerPreflight {
|
|
|
27
27
|
run(force?: boolean): Promise<SandboxPreflightResult>;
|
|
28
28
|
private inspect;
|
|
29
29
|
}
|
|
30
|
+
export declare function canRunDockerMaintenance(preflight: {
|
|
31
|
+
readonly healthy: boolean;
|
|
32
|
+
readonly unavailableCode?: string | null;
|
|
33
|
+
}): boolean;
|
|
30
34
|
export declare function evaluateDiskPressure(availableBytes: number, totalBytes: number, minimumAvailableBytes: number, highWatermarkPercent: number, lowWatermarkPercent: number, pressureLatched: boolean): boolean;
|
|
31
35
|
export declare function storagePolicyAvailable(storageQuota: boolean, environment?: NodeJS.ProcessEnv): boolean;
|
|
32
36
|
export declare class DockerContextOwnershipLock {
|
|
@@ -26,7 +26,7 @@ export class DockerPreflight {
|
|
|
26
26
|
async refresh() {
|
|
27
27
|
await fs.mkdir(this.options.stateRoot ?? path.join(os.homedir(), ".forge", "bridge", "sandboxes"), { recursive: true, mode: 0o700 });
|
|
28
28
|
const result = await this.run(true);
|
|
29
|
-
if (result
|
|
29
|
+
if (canRunDockerMaintenance(result)) {
|
|
30
30
|
await this.ownership.acquire(this.options.dockerContext);
|
|
31
31
|
}
|
|
32
32
|
return result;
|
|
@@ -108,14 +108,27 @@ 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
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
111
|
+
const memoryReserveBytes = 2 * 1024 ** 3;
|
|
112
|
+
const sandboxMemoryBytes = 2 * 1024 ** 3;
|
|
113
|
+
const sandboxCpuMillis = 2_000;
|
|
114
|
+
const memorySlots = Math.max(0, Math.floor((availableMemoryBytes - memoryReserveBytes) / sandboxMemoryBytes));
|
|
115
|
+
const cpuSlots = Math.max(0, Math.floor(availableCpuMillis / sandboxCpuMillis));
|
|
116
|
+
const configuredMax = positiveInteger(this.options.maxConcurrentSandboxes, positiveInteger(process.env.FORGE_MAX_CONCURRENT_SANDBOXES, 3));
|
|
117
|
+
const capacityLowCode = !diskLow && memorySlots <= 0 ? "docker_memory_low" : !diskLow && cpuSlots <= 0 ? "docker_cpu_low" : null;
|
|
118
|
+
const maxConcurrentSandboxes = diskLow || capacityLowCode ? 0 : Math.min(3, configuredMax, cpuSlots, memorySlots);
|
|
119
|
+
const unavailableCode = diskLow ? "docker_disk_low" : capacityLowCode;
|
|
120
|
+
const unavailableReason = diskLow
|
|
121
|
+
? `Docker workspace has only ${disk.available} bytes available.`
|
|
122
|
+
: unavailableCode === "docker_memory_low"
|
|
123
|
+
? `Docker host has ${availableMemoryBytes} bytes free; at least ${memoryReserveBytes + sandboxMemoryBytes} bytes are needed for one sandbox plus reserve.`
|
|
124
|
+
: unavailableCode === "docker_cpu_low"
|
|
125
|
+
? `Docker host has ${availableCpuMillis} available CPU millis; at least ${sandboxCpuMillis} are needed for one sandbox.`
|
|
126
|
+
: null;
|
|
115
127
|
const features = { rootless, userNamespaces, cgroupV2, seccomp, lsm, storageQuota, storagePolicy, compose, internalNetwork: true, buildkit, imagePolicy, securityProfile, egressProxy };
|
|
128
|
+
const capacityAvailable = unavailableCode === null;
|
|
116
129
|
return {
|
|
117
|
-
available:
|
|
118
|
-
healthy:
|
|
130
|
+
available: capacityAvailable,
|
|
131
|
+
healthy: capacityAvailable,
|
|
119
132
|
generation: this.generation,
|
|
120
133
|
runtimeVersion: `docker/${String(info.ServerVersion ?? "unknown")}${rootless ? "-rootless" : "-desktop"}`,
|
|
121
134
|
maxConcurrentSandboxes,
|
|
@@ -123,12 +136,16 @@ export class DockerPreflight {
|
|
|
123
136
|
availableCpuMillis,
|
|
124
137
|
availableWorkspaceBytes: disk.available,
|
|
125
138
|
diskLow,
|
|
126
|
-
unavailableCode
|
|
127
|
-
unavailableReason
|
|
139
|
+
unavailableCode,
|
|
140
|
+
unavailableReason,
|
|
128
141
|
features,
|
|
129
142
|
};
|
|
130
143
|
}
|
|
131
144
|
}
|
|
145
|
+
const dockerMaintenanceUnavailableCodes = new Set(["docker_disk_low", "docker_memory_low", "docker_cpu_low"]);
|
|
146
|
+
export function canRunDockerMaintenance(preflight) {
|
|
147
|
+
return preflight.healthy || dockerMaintenanceUnavailableCodes.has(preflight.unavailableCode ?? "");
|
|
148
|
+
}
|
|
132
149
|
export function evaluateDiskPressure(availableBytes, totalBytes, minimumAvailableBytes, highWatermarkPercent, lowWatermarkPercent, pressureLatched) {
|
|
133
150
|
if (availableBytes < minimumAvailableBytes)
|
|
134
151
|
return true;
|
|
@@ -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
|
|
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;
|
package/dist/sandbox/runtime.js
CHANGED
|
@@ -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
|
|
544
|
-
await fs.mkdir(
|
|
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
|
|
548
|
-
|
|
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"), [
|
|
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
|
|
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
|
-
|
|
1421
|
-
|
|
1422
|
-
"--
|
|
1423
|
-
|
|
1424
|
-
|
|
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 {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.3-dev.c28767e94230";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.3-dev.c28767e94230";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loucompanion/forge-bridge",
|
|
3
|
-
"version": "0.1.3-dev.
|
|
3
|
+
"version": "0.1.3-dev.c28767e94230",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|