@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.bfa11ebe98c6
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 +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1562 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
- package/dist/runtime/attempt-process-supervisor.js +67 -20
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +107 -53
- package/dist/runtime/host-runtime-lease.d.ts +90 -8
- package/dist/runtime/host-runtime-lease.js +1097 -108
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +28 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +17 -8
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +132 -103
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +481 -31
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +118 -16
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +657 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +8 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +771 -68
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +57 -9
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
package/dist/sandbox/runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { failureResult } from "../session/session.types.js";
|
|
5
|
+
import { createFailureEnvelope, ForgeFailureError, failureFactsForCode, failurePolicyForCode, failureResult, parseRunOnceResultPacket, } from "../session/session.types.js";
|
|
6
6
|
import { dockerfileUpstreamImages, validateDockerfilePolicy, validateSandboxContract } from "./contract.js";
|
|
7
7
|
import { requireSandboxProcess, runSandboxProcess } from "./process.js";
|
|
8
8
|
import { createLeaseUpdate } from "./lease-watchdog.js";
|
|
@@ -85,7 +85,7 @@ export class DockerSandboxRuntime {
|
|
|
85
85
|
await fs.mkdir(path.join(attemptRoot, "output"), { recursive: true, mode: 0o700 });
|
|
86
86
|
const repoPath = dispatch.repo.serverLocalPath ? path.resolve(dispatch.repo.serverLocalPath) : null;
|
|
87
87
|
if (!repoPath)
|
|
88
|
-
throw
|
|
88
|
+
throw trustedSandboxFailure("sandbox_input_failed", "bridge repository mapping is required for bundle handoff.");
|
|
89
89
|
await sink.emit(event("sandbox_preparing", `Preparing Docker sandbox attempt ${dispatch.sandbox.dispatchAttemptId}.`));
|
|
90
90
|
const git = new HostGit(this.config, repoPath);
|
|
91
91
|
const prepared = await git.prepare(dispatch, attemptRoot, runtimeSignal);
|
|
@@ -100,7 +100,7 @@ export class DockerSandboxRuntime {
|
|
|
100
100
|
const providerAuthAdapter = sandboxProviderAuthAdapterFor(runtimeDispatch.execution.provider, runtimeDispatch.execution.authKind);
|
|
101
101
|
const providerAuthUnavailable = sandboxProviderAuthUnavailableMessage(runtimeDispatch.execution.provider, runtimeDispatch.execution.authKind);
|
|
102
102
|
if (providerAuthUnavailable) {
|
|
103
|
-
throw
|
|
103
|
+
throw trustedSandboxFailure("sandbox_auth_unavailable", providerAuthUnavailable);
|
|
104
104
|
}
|
|
105
105
|
await sink.emit(event("sandbox_metadata", "Resolved immutable sandbox inputs.", {
|
|
106
106
|
inputCommitSha: prepared.inputCommitSha,
|
|
@@ -125,6 +125,7 @@ export class DockerSandboxRuntime {
|
|
|
125
125
|
await this.writeLedger(active.ledger);
|
|
126
126
|
let teardownComplete = false;
|
|
127
127
|
let memoryTelemetryEvent = null;
|
|
128
|
+
let trustedHostFailure = null;
|
|
128
129
|
try {
|
|
129
130
|
const internalNetworkId = dockerObjectId((await this.docker(["network", "create", ...networkArgs, ...labelArgs(labels), resources.network], 30_000, runtimeSignal)).stdout, "network");
|
|
130
131
|
await this.docker(["volume", "create", ...labelArgs(labels), resources.workspaceVolume], 30_000, runtimeSignal);
|
|
@@ -217,19 +218,18 @@ export class DockerSandboxRuntime {
|
|
|
217
218
|
await sink.emit(memoryTelemetryEvent).catch(() => undefined);
|
|
218
219
|
});
|
|
219
220
|
if (start.truncated)
|
|
220
|
-
throw
|
|
221
|
+
throw trustedSandboxFailure("sandbox_log_limit", "sandbox output exceeded the 8 MiB supervisor capture budget.");
|
|
221
222
|
const containerState = await this.inspectContainerState(containerId);
|
|
222
223
|
if (containerState.oomKilled) {
|
|
223
|
-
|
|
224
|
+
trustedHostFailure = {
|
|
225
|
+
code: "sandbox_oom_killed",
|
|
226
|
+
message: "Docker terminated the agent after it exceeded the memory limit.",
|
|
227
|
+
};
|
|
224
228
|
}
|
|
225
|
-
if (start.exitCode !== 0) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
start.stdout
|
|
230
|
-
].filter(Boolean).join("\n"));
|
|
231
|
-
if (resourceFailure)
|
|
232
|
-
throw new Error(`${resourceFailure.code}: ${resourceFailure.message}`);
|
|
229
|
+
if (start.exitCode !== 0 && !trustedHostFailure) {
|
|
230
|
+
// Docker's inspect state is trusted host telemetry. Child stdout/stderr is
|
|
231
|
+
// evidence only and must not be able to choose a machine failure code.
|
|
232
|
+
trustedHostFailure = classifySandboxResourceFailure(containerState.error ?? "");
|
|
233
233
|
}
|
|
234
234
|
active.phase = "active";
|
|
235
235
|
await sink.emit(event("sandbox_agent_exited", `Sandbox worker exited with code ${start.exitCode}.`, {
|
|
@@ -250,7 +250,7 @@ export class DockerSandboxRuntime {
|
|
|
250
250
|
if (runtimeDispatch.sandbox.publicationMode === "bridge_push") {
|
|
251
251
|
const handle = runtimeDispatch.sandbox.publishGrantHandle;
|
|
252
252
|
if (!handle || !output.manifest.resultCommitSha || !this.publicationGrantRedeemer) {
|
|
253
|
-
throw
|
|
253
|
+
throw trustedSandboxFailure("sandbox_output_publish_forbidden", "bridge publication grant redemption is unavailable.");
|
|
254
254
|
}
|
|
255
255
|
const credential = await this.publicationGrantRedeemer({
|
|
256
256
|
dispatchAttemptId: runtimeDispatch.sandbox.dispatchAttemptId,
|
|
@@ -269,29 +269,26 @@ export class DockerSandboxRuntime {
|
|
|
269
269
|
resultCommitSha: output.manifest.resultCommitSha
|
|
270
270
|
}));
|
|
271
271
|
}
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
summary: resourceFailure.message,
|
|
282
|
-
events: [...result.events, event(resourceFailure.code, resourceFailure.message, { phase: "agent" })]
|
|
283
|
-
}, memoryTelemetryEvent);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
272
|
+
const rawResult = parseRunOnceResultPacket(JSON.parse(await fs.readFile(output.resultPath, "utf8")));
|
|
273
|
+
const result = {
|
|
274
|
+
...rawResult,
|
|
275
|
+
resultCommitSha: output.manifest.resultCommitSha ?? rawResult.resultCommitSha ?? null,
|
|
276
|
+
publishedCommitSha: runtimeDispatch.sandbox.publicationMode === "bridge_push"
|
|
277
|
+
? output.manifest.resultCommitSha
|
|
278
|
+
: rawResult.publishedCommitSha ?? null,
|
|
279
|
+
};
|
|
280
|
+
const canonicalResult = applyTrustedHostFailure(result, trustedHostFailure);
|
|
286
281
|
await sink.emit(event("sandbox_output_imported", "Validated and imported sandbox output.", {
|
|
287
282
|
resultCommitSha: output.manifest.resultCommitSha,
|
|
288
283
|
}));
|
|
289
|
-
return appendRunOnceEvent(
|
|
284
|
+
return appendRunOnceEvent(canonicalResult, memoryTelemetryEvent);
|
|
290
285
|
}
|
|
291
286
|
catch (error) {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
287
|
+
const effectiveError = trustedHostFailure
|
|
288
|
+
? new ForgeFailureError(trustedHostFailure.code, trustedHostFailure.message, "sandbox", { cause: error })
|
|
289
|
+
: error;
|
|
290
|
+
const classified = classifySandboxRuntimeFailure(effectiveError);
|
|
291
|
+
return appendRunOnceEvent(failureResult(classified.message, [event(classified.code, classified.message, { phase: classified.phase })], failureFactsForCode(classified.code, "sandbox")), memoryTelemetryEvent);
|
|
295
292
|
}
|
|
296
293
|
finally {
|
|
297
294
|
try {
|
|
@@ -525,7 +522,7 @@ export class DockerSandboxRuntime {
|
|
|
525
522
|
ledgerPath(dispatchAttemptId) {
|
|
526
523
|
const attemptHex = dispatchAttemptId.replace(/-/g, "").toLowerCase();
|
|
527
524
|
if (!/^[0-9a-f]{32}$/.test(attemptHex))
|
|
528
|
-
throw
|
|
525
|
+
throw trustedSandboxFailure("sandbox_identity_invalid", "dispatch attempt id must be a full UUID.");
|
|
529
526
|
return path.join(this.config.sandboxStateRoot, "ledger", `${attemptHex}.json`);
|
|
530
527
|
}
|
|
531
528
|
async removeLedger(dispatchAttemptId) {
|
|
@@ -534,11 +531,11 @@ export class DockerSandboxRuntime {
|
|
|
534
531
|
async buildImage(contextRoot, contextDigest, project, contract, upstreamImages, policy, sink, signal) {
|
|
535
532
|
const toolingImage = process.env.FORGE_AGENT_TOOLING_IMAGE ?? "node:22.17.0-bookworm-slim";
|
|
536
533
|
if (policy === "sandbox_required" && !/@sha256:[0-9a-f]{64}$/i.test(toolingImage)) {
|
|
537
|
-
throw
|
|
534
|
+
throw trustedSandboxFailure("sandbox_build_failed", "FORGE_AGENT_TOOLING_IMAGE must be digest-pinned for sandbox_required.");
|
|
538
535
|
}
|
|
539
536
|
const codexVersion = process.env.FORGE_CODEX_PACKAGE_VERSION ?? "0.144.1";
|
|
540
537
|
if (!/^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/i.test(codexVersion))
|
|
541
|
-
throw
|
|
538
|
+
throw trustedSandboxFailure("sandbox_build_failed", "invalid pinned Codex package version.");
|
|
542
539
|
const policyConfigurationDigest = await sandboxImagePolicyConfigurationDigest();
|
|
543
540
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
544
541
|
const bridgeRuntimeDigest = await bridgeRuntimeContentDigest(packageRoot);
|
|
@@ -631,7 +628,7 @@ export class DockerSandboxRuntime {
|
|
|
631
628
|
}
|
|
632
629
|
await sink.emit(event("sandbox_image_cache_miss", `Sandbox image ${key.slice(0, 12)} requires a build.`, { cacheKey: key }));
|
|
633
630
|
if (policy === "sandbox_required" && process.env.FORGE_SANDBOX_BUILD_EGRESS_ENFORCED !== "true") {
|
|
634
|
-
throw
|
|
631
|
+
throw trustedSandboxFailure("sandbox_build_policy_failed", "sandbox_required builds need an operator-enforced builder egress policy.");
|
|
635
632
|
}
|
|
636
633
|
const buildStartedAt = Date.now();
|
|
637
634
|
await sink.emit(event("sandbox_build_started", `Building sandbox image ${key.slice(0, 12)}.`, { cacheKey: key }));
|
|
@@ -658,7 +655,7 @@ export class DockerSandboxRuntime {
|
|
|
658
655
|
const installMode = await copyBridgePackageManifests(packageRoot, stagedBridgeRoot);
|
|
659
656
|
const stagedBridgeRuntimeDigest = await bridgeRuntimeContentDigest(stagedBridgeRoot);
|
|
660
657
|
if (stagedBridgeRuntimeDigest !== bridgeRuntimeDigest) {
|
|
661
|
-
throw
|
|
658
|
+
throw trustedSandboxFailure("sandbox_build_failed", "bridge runtime payload changed while preparing the sandbox image; retry the dispatch.");
|
|
662
659
|
}
|
|
663
660
|
const dockerfile = agentOverlayDockerfile(toolingImage, projectContextName, installMode, codexVersion);
|
|
664
661
|
await fs.writeFile(path.join(overlayRoot, "Dockerfile"), dockerfile);
|
|
@@ -666,7 +663,7 @@ export class DockerSandboxRuntime {
|
|
|
666
663
|
await this.runBuildStage("tooling", key, buildxBuildArgs(builder, path.join(metadataRoot, "tooling.json"), ["--pull=false", "--target", "tooling", ...imageLabelArgs(project, key, "tooling", bridgeRuntimeDigest), "--tag", helperTag, overlayRoot]), sink, signal);
|
|
667
664
|
const helperImageDigest = (await this.docker(["image", "inspect", helperTag, "--format", "{{.Id}}"], 15_000)).stdout.trim();
|
|
668
665
|
if (!/^sha256:[0-9a-f]{64}$/i.test(helperImageDigest))
|
|
669
|
-
throw
|
|
666
|
+
throw trustedSandboxFailure("sandbox_build_failed", "helper image did not resolve to an immutable digest.");
|
|
670
667
|
const projectContextRef = ociLayoutContextRef(path.relative(overlayRoot, projectOciRoot));
|
|
671
668
|
await this.runBuildStage("agent", key, buildxBuildArgs(builder, path.join(metadataRoot, "agent.json"), [
|
|
672
669
|
"--pull=false",
|
|
@@ -677,7 +674,7 @@ export class DockerSandboxRuntime {
|
|
|
677
674
|
]), sink, signal, { cwd: overlayRoot });
|
|
678
675
|
const digest = (await this.docker(["image", "inspect", overlayTag, "--format", "{{.Id}}"], 15_000)).stdout.trim();
|
|
679
676
|
if (!/^sha256:[0-9a-f]{64}$/i.test(digest))
|
|
680
|
-
throw
|
|
677
|
+
throw trustedSandboxFailure("sandbox_build_failed", "overlay image did not resolve to an immutable digest.");
|
|
681
678
|
const imagePolicy = await evaluateSandboxImagePolicy({
|
|
682
679
|
key,
|
|
683
680
|
project: dockerLabelValue(project),
|
|
@@ -752,7 +749,7 @@ export class DockerSandboxRuntime {
|
|
|
752
749
|
byteCount: artifact.byteCount,
|
|
753
750
|
truncated: artifact.truncated || result.truncated
|
|
754
751
|
})).catch(() => undefined);
|
|
755
|
-
throw
|
|
752
|
+
throw trustedSandboxFailure("sandbox_build_failed", `${stage} image build failed; restricted log digest ${artifact.digest}.`);
|
|
756
753
|
}
|
|
757
754
|
async ensureBuildxBuilder(signal) {
|
|
758
755
|
const name = dedicatedBuildxBuilderName(this.config.runnerId, this.config.dockerContext);
|
|
@@ -775,7 +772,7 @@ export class DockerSandboxRuntime {
|
|
|
775
772
|
if (Number(hostConfig.PidsLimit) !== policy.builderPidsLimit
|
|
776
773
|
|| Number(hostConfig.Memory) !== policy.builderMemoryBytes
|
|
777
774
|
|| Number(hostConfig.MemorySwap) !== policy.builderMemoryBytes) {
|
|
778
|
-
throw
|
|
775
|
+
throw trustedSandboxFailure("sandbox_build_policy_failed", "dedicated BuildKit builder resource limits were not applied.");
|
|
779
776
|
}
|
|
780
777
|
return name;
|
|
781
778
|
}
|
|
@@ -848,10 +845,10 @@ export class DockerSandboxRuntime {
|
|
|
848
845
|
return null;
|
|
849
846
|
const image = process.env.FORGE_SANDBOX_EGRESS_PROXY_IMAGE?.trim();
|
|
850
847
|
if (!image || !/@sha256:[0-9a-f]{64}$/iu.test(image)) {
|
|
851
|
-
throw
|
|
848
|
+
throw trustedSandboxFailure("sandbox_egress_proxy_unavailable", "public_allowlist needs a digest-pinned Squid-compatible proxy image.");
|
|
852
849
|
}
|
|
853
850
|
if (process.env.FORGE_SANDBOX_EGRESS_ENFORCED !== "true") {
|
|
854
|
-
throw
|
|
851
|
+
throw trustedSandboxFailure("sandbox_egress_proxy_unavailable", "runner egress enforcement is not enabled.");
|
|
855
852
|
}
|
|
856
853
|
await this.verifyCatalogImage(image, dispatch.sandbox.executionPolicy, sink, signal);
|
|
857
854
|
const name = `${resources.prefix}-egress`;
|
|
@@ -1162,7 +1159,7 @@ class HostGit {
|
|
|
1162
1159
|
|| !resultCommitSha
|
|
1163
1160
|
|| handle.branch !== dispatch.lease.worktreeKey
|
|
1164
1161
|
|| !/^[0-9a-f-]{32,36}$/iu.test(handle.grantId)) {
|
|
1165
|
-
throw
|
|
1162
|
+
throw trustedSandboxFailure("sandbox_output_publish_forbidden", "remote publication grant is missing or outside its attempt scope.");
|
|
1166
1163
|
}
|
|
1167
1164
|
assertCurrent();
|
|
1168
1165
|
const credentialEnvironment = publicationCredentialEnvironment(credential);
|
|
@@ -1202,15 +1199,15 @@ class HostGit {
|
|
|
1202
1199
|
terminateOnOutputLimit: true
|
|
1203
1200
|
});
|
|
1204
1201
|
if (objectInventory.exitCode !== 0 || objectInventory.truncated) {
|
|
1205
|
-
throw
|
|
1202
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "Git bundle exceeds object validation limits.");
|
|
1206
1203
|
}
|
|
1207
1204
|
validateGitObjectBudget(objectInventory.stdout);
|
|
1208
1205
|
const imported = (await quarantineGit(quarantine, ["rev-parse", `${importRef}^{commit}`], 30_000)).stdout.trim();
|
|
1209
1206
|
if (imported !== output.manifest.resultCommitSha)
|
|
1210
|
-
throw
|
|
1207
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "imported result SHA differs from manifest.");
|
|
1211
1208
|
const ancestry = await runSandboxProcess({ command: "git", args: ["-C", quarantine, "merge-base", "--is-ancestor", output.manifest.inputCommitSha, imported], timeoutMs: 30_000 });
|
|
1212
1209
|
if (ancestry.exitCode !== 0)
|
|
1213
|
-
throw
|
|
1210
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "result commit is not descended from input commit.");
|
|
1214
1211
|
const commitDepth = await quarantineGit(quarantine, ["rev-list", "--count", `${output.manifest.inputCommitSha}..${imported}`], 2 * 60_000);
|
|
1215
1212
|
validateGitCommitDepth(commitDepth.stdout);
|
|
1216
1213
|
await this.git(["fetch", "--no-write-fetch-head", quarantine, `${importRef}:${importRef}`], 5 * 60_000);
|
|
@@ -1238,7 +1235,7 @@ class HostGit {
|
|
|
1238
1235
|
const head = (await requireSandboxProcess({ command: "git", args: ["-C", checkedOut.path, "rev-parse", "HEAD"], timeoutMs: 30_000 })).stdout.trim();
|
|
1239
1236
|
const status = (await requireSandboxProcess({ command: "git", args: ["-C", checkedOut.path, "status", "--porcelain=v1", "--untracked-files=no"], timeoutMs: 30_000, maxOutputBytes: 4 * 1024 * 1024 })).stdout;
|
|
1240
1237
|
if (head !== expected || status.trim())
|
|
1241
|
-
throw
|
|
1238
|
+
throw trustedSandboxFailure("sandbox_output_conflict", "managed worktree changed after sandbox input was captured.");
|
|
1242
1239
|
await this.git(["update-ref", branchRef, imported, expected], 30_000);
|
|
1243
1240
|
await requireSandboxProcess({ command: "git", args: ["-C", checkedOut.path, "reset", "--hard"], timeoutMs: 2 * 60_000, maxOutputBytes: 4 * 1024 * 1024 });
|
|
1244
1241
|
}
|
|
@@ -1249,7 +1246,7 @@ class HostGit {
|
|
|
1249
1246
|
if (result.exitCode === 0 && /^[0-9a-f]{40,64}$/i.test(sha))
|
|
1250
1247
|
return sha;
|
|
1251
1248
|
}
|
|
1252
|
-
throw
|
|
1249
|
+
throw trustedSandboxFailure("sandbox_input_failed", "no approved input Git ref could be resolved.");
|
|
1253
1250
|
}
|
|
1254
1251
|
git(args, timeoutMs, signal, env) { return requireSandboxProcess({ command: "git", args: ["-C", this.repoPath, ...args], timeoutMs, maxOutputBytes: 4 * 1024 * 1024, signal, env }); }
|
|
1255
1252
|
}
|
|
@@ -1259,19 +1256,19 @@ export function managedBranchUpdateExpectedValue(current, expectedInput, importe
|
|
|
1259
1256
|
return "0".repeat(imported.length);
|
|
1260
1257
|
}
|
|
1261
1258
|
if (normalized !== expectedInput) {
|
|
1262
|
-
throw
|
|
1259
|
+
throw trustedSandboxFailure("sandbox_output_conflict", "managed branch changed after sandbox input was captured.");
|
|
1263
1260
|
}
|
|
1264
1261
|
return normalized;
|
|
1265
1262
|
}
|
|
1266
1263
|
export function publicationPushArgs(branch, resultCommitSha, inputCommitSha, remoteCommitSha, remote = "origin") {
|
|
1267
1264
|
const isCommit = (value) => /^[0-9a-f]{40,64}$/iu.test(value);
|
|
1268
1265
|
if (!branch || branch.length > 240 || !isCommit(resultCommitSha) || !isCommit(inputCommitSha) || (remoteCommitSha && !isCommit(remoteCommitSha))) {
|
|
1269
|
-
throw
|
|
1266
|
+
throw trustedSandboxFailure("sandbox_output_publish_rejected", "publication inputs contain an invalid branch or object id.");
|
|
1270
1267
|
}
|
|
1271
1268
|
if (remoteCommitSha === resultCommitSha)
|
|
1272
1269
|
return null;
|
|
1273
1270
|
if (remoteCommitSha && remoteCommitSha !== inputCommitSha) {
|
|
1274
|
-
throw
|
|
1271
|
+
throw trustedSandboxFailure("sandbox_output_publish_stale", "remote branch changed after the sandbox input was captured.");
|
|
1275
1272
|
}
|
|
1276
1273
|
const remoteRef = `refs/heads/${branch}`;
|
|
1277
1274
|
return [
|
|
@@ -1303,7 +1300,7 @@ export function validatePublicationCredential(credential, nowMs = Date.now()) {
|
|
|
1303
1300
|
|| credential.secret.length > 16 * 1024
|
|
1304
1301
|
|| /[\r\n\0]/u.test(credential.secret)
|
|
1305
1302
|
|| Date.parse(credential.expiresAt) <= nowMs) {
|
|
1306
|
-
throw
|
|
1303
|
+
throw trustedSandboxFailure("sandbox_output_publish_forbidden", "redeemed publication credential is invalid or expired.");
|
|
1307
1304
|
}
|
|
1308
1305
|
}
|
|
1309
1306
|
export function publicationCredentialEnvironment(credential, environment = process.env) {
|
|
@@ -1333,7 +1330,7 @@ export function restrictedBuildLog(source, exactSecrets = [], maximumBytes = 4 *
|
|
|
1333
1330
|
export function validateGitObjectBudget(inventory, maximumObjectCount = 500_000, maximumExpandedBytes = 2 * 1024 * 1024 * 1024) {
|
|
1334
1331
|
const lines = inventory.split(/\r?\n/u).filter(Boolean);
|
|
1335
1332
|
if (lines.length === 0 || lines.length > maximumObjectCount) {
|
|
1336
|
-
throw
|
|
1333
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "Git bundle exceeds the object-count budget.");
|
|
1337
1334
|
}
|
|
1338
1335
|
const maximum = BigInt(maximumExpandedBytes);
|
|
1339
1336
|
let expanded = 0n;
|
|
@@ -1341,13 +1338,13 @@ export function validateGitObjectBudget(inventory, maximumObjectCount = 500_000,
|
|
|
1341
1338
|
for (const line of lines) {
|
|
1342
1339
|
const match = /^([0-9a-f]{40}|[0-9a-f]{64}) (blob|tree|commit|tag) ([0-9]+)$/iu.exec(line);
|
|
1343
1340
|
if (!match || objects.has(match[1].toLowerCase())) {
|
|
1344
|
-
throw
|
|
1341
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "Git object inventory is malformed or duplicated.");
|
|
1345
1342
|
}
|
|
1346
1343
|
objects.add(match[1].toLowerCase());
|
|
1347
1344
|
const size = BigInt(match[3]);
|
|
1348
1345
|
expanded += size;
|
|
1349
1346
|
if (size > maximum || expanded > maximum) {
|
|
1350
|
-
throw
|
|
1347
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "Git bundle exceeds the expanded-object byte budget.");
|
|
1351
1348
|
}
|
|
1352
1349
|
}
|
|
1353
1350
|
return { objectCount: lines.length, expandedBytes: Number(expanded) };
|
|
@@ -1355,11 +1352,11 @@ export function validateGitObjectBudget(inventory, maximumObjectCount = 500_000,
|
|
|
1355
1352
|
export function validateGitCommitDepth(value, maximumCommitDepth = 50_000) {
|
|
1356
1353
|
const normalized = value.trim();
|
|
1357
1354
|
if (!/^[0-9]+$/u.test(normalized)) {
|
|
1358
|
-
throw
|
|
1355
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "Git commit-depth result is malformed.");
|
|
1359
1356
|
}
|
|
1360
1357
|
const depth = BigInt(normalized);
|
|
1361
1358
|
if (depth > BigInt(maximumCommitDepth)) {
|
|
1362
|
-
throw
|
|
1359
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "Git result exceeds the commit-depth budget.");
|
|
1363
1360
|
}
|
|
1364
1361
|
return Number(depth);
|
|
1365
1362
|
}
|
|
@@ -1375,10 +1372,10 @@ export function selectAuditArtifactGcCandidates(records, retentionCount, retenti
|
|
|
1375
1372
|
export function selectSandboxConfigRefs(executionPolicy, configuredSha, defaultBranch) {
|
|
1376
1373
|
const approvedSha = configuredSha?.trim().toLowerCase() || null;
|
|
1377
1374
|
if (executionPolicy === "sandbox_required" && !approvedSha) {
|
|
1378
|
-
throw
|
|
1375
|
+
throw trustedSandboxFailure("sandbox_config_commit_unapproved", "sandbox_required dispatches need a server-approved config commit SHA.");
|
|
1379
1376
|
}
|
|
1380
1377
|
if (approvedSha && !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(approvedSha)) {
|
|
1381
|
-
throw
|
|
1378
|
+
throw trustedSandboxFailure("sandbox_config_commit_invalid", "approved sandbox config commit is not a full Git object id.");
|
|
1382
1379
|
}
|
|
1383
1380
|
return approvedSha
|
|
1384
1381
|
? { refs: [approvedSha], approvedSha }
|
|
@@ -1423,12 +1420,12 @@ async function validateOutput(attemptRoot, dispatch) {
|
|
|
1423
1420
|
const manifestPath = path.join(outputRoot, "manifest.json");
|
|
1424
1421
|
const stat = await fs.lstat(manifestPath).catch(error => {
|
|
1425
1422
|
if (isNodeErrorWithCode(error, "ENOENT")) {
|
|
1426
|
-
throw
|
|
1423
|
+
throw trustedSandboxFailure("sandbox_output_missing", "sandbox worker did not produce manifest.json.");
|
|
1427
1424
|
}
|
|
1428
1425
|
throw error;
|
|
1429
1426
|
});
|
|
1430
1427
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
|
|
1431
|
-
throw
|
|
1428
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "manifest must be a bounded regular file.");
|
|
1432
1429
|
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
1433
1430
|
if (!dispatch.sandbox
|
|
1434
1431
|
|| manifest.dispatchAttemptId !== dispatch.sandbox.dispatchAttemptId
|
|
@@ -1439,10 +1436,10 @@ async function validateOutput(attemptRoot, dispatch) {
|
|
|
1439
1436
|
|| manifest.branch !== dispatch.lease.worktreeKey
|
|
1440
1437
|
|| manifest.contractDigest !== dispatch.sandbox.contractDigest
|
|
1441
1438
|
|| manifest.imageDigest !== dispatch.sandbox.imageDigest)
|
|
1442
|
-
throw
|
|
1439
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "manifest identity or fence mismatch.");
|
|
1443
1440
|
if ((manifest.resultCommitSha === null) !== (manifest.bundle === null)
|
|
1444
1441
|
|| (manifest.resultCommitSha !== null && !/^[0-9a-f]{40,64}$/i.test(manifest.resultCommitSha))) {
|
|
1445
|
-
throw
|
|
1442
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "result commit and bundle must be present together.");
|
|
1446
1443
|
}
|
|
1447
1444
|
const resultPath = await validateManifestFile(outputRoot, manifest.result, 16 * 1024 * 1024);
|
|
1448
1445
|
const bundlePath = manifest.bundle ? await validateManifestFile(outputRoot, manifest.bundle, 512 * 1024 * 1024) : null;
|
|
@@ -1450,14 +1447,14 @@ async function validateOutput(attemptRoot, dispatch) {
|
|
|
1450
1447
|
}
|
|
1451
1448
|
async function validateManifestFile(root, file, max) {
|
|
1452
1449
|
if (!/^[a-z0-9_.-]+$/i.test(file.path))
|
|
1453
|
-
throw
|
|
1450
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "output path is invalid.");
|
|
1454
1451
|
const target = path.join(root, file.path);
|
|
1455
1452
|
const stat = await fs.lstat(target);
|
|
1456
1453
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== file.bytes || stat.size > max)
|
|
1457
|
-
throw
|
|
1454
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "output file bounds mismatch.");
|
|
1458
1455
|
const digest = createHash("sha256").update(await fs.readFile(target)).digest("hex");
|
|
1459
1456
|
if (digest !== file.sha256)
|
|
1460
|
-
throw
|
|
1457
|
+
throw trustedSandboxFailure("sandbox_output_invalid", "output hash mismatch.");
|
|
1461
1458
|
return target;
|
|
1462
1459
|
}
|
|
1463
1460
|
function isMissingSandboxManifest(error) {
|
|
@@ -1479,7 +1476,7 @@ export function sandboxWorkerFailureMessage(exitCode, stderr, containerError) {
|
|
|
1479
1476
|
function identityFor(dispatch) {
|
|
1480
1477
|
const value = dispatch.sandbox?.dispatchAttemptId.replace(/-/g, "").toLowerCase() ?? "";
|
|
1481
1478
|
if (!/^[0-9a-f]{32}$/.test(value))
|
|
1482
|
-
throw
|
|
1479
|
+
throw trustedSandboxFailure("sandbox_identity_invalid", "dispatch attempt id must be a full UUID.");
|
|
1483
1480
|
return { attemptHex: value };
|
|
1484
1481
|
}
|
|
1485
1482
|
class SandboxTeardownError extends Error {
|
|
@@ -1688,7 +1685,7 @@ export function assertSandboxImportCurrent(expected, current, signal, nowMs = Da
|
|
|
1688
1685
|
|| current.dispatchAttemptId !== expected.dispatchAttemptId
|
|
1689
1686
|
|| current.fencingToken !== expected.fencingToken
|
|
1690
1687
|
|| Date.parse(expected.expiresAt) <= nowMs) {
|
|
1691
|
-
throw
|
|
1688
|
+
throw trustedSandboxFailure("sandbox_output_stale", "output cannot move the managed ref because its attempt, fence, or lease is no longer current.");
|
|
1692
1689
|
}
|
|
1693
1690
|
}
|
|
1694
1691
|
export function validSandboxLedger(value) {
|
|
@@ -1830,7 +1827,7 @@ export function sandboxResourceLabels(dispatch, image, contract) {
|
|
|
1830
1827
|
];
|
|
1831
1828
|
const invalid = ownershipIds.find(([, value]) => !isFullUuid(value));
|
|
1832
1829
|
if (invalid)
|
|
1833
|
-
throw
|
|
1830
|
+
throw trustedSandboxFailure("sandbox_identity_invalid", `${invalid[0]} id must be a full UUID.`);
|
|
1834
1831
|
return {
|
|
1835
1832
|
"dev.forge.managed": "true", "dev.forge.session-id": dispatch.session.id, "dev.forge.resource-lease-id": resourceLeaseId, "dev.forge.execution-lease-id": resourceLeaseId,
|
|
1836
1833
|
"dev.forge.dispatch-attempt-id": dispatch.sandbox.dispatchAttemptId, "dev.forge.fencing-token": dispatch.lease.fencingToken,
|
|
@@ -1919,13 +1916,13 @@ export async function extractOciLayoutArchive(archivePath, destination, signal)
|
|
|
1919
1916
|
signal
|
|
1920
1917
|
});
|
|
1921
1918
|
if (result.exitCode !== 0 || result.truncated) {
|
|
1922
|
-
throw
|
|
1919
|
+
throw trustedSandboxFailure("sandbox_build_failed", `project OCI layout extraction failed with exit code ${result.exitCode}; ${formatOciExtractionFailure(result)}. Archive retained at ${archivePath}.`);
|
|
1923
1920
|
}
|
|
1924
1921
|
await fs.rm(archivePath, { force: true }).catch(() => undefined);
|
|
1925
1922
|
}
|
|
1926
1923
|
export function ociLayoutContextRef(layoutPath) {
|
|
1927
1924
|
if (/^[A-Za-z]:[\\/]/u.test(layoutPath)) {
|
|
1928
|
-
throw
|
|
1925
|
+
throw trustedSandboxFailure("sandbox_build_failed", "Windows absolute OCI layout paths are not BuildKit-safe; use a relative path from the build working directory.");
|
|
1929
1926
|
}
|
|
1930
1927
|
const normalized = layoutPath.replace(/\\/gu, "/").replace(/^\.\//u, "");
|
|
1931
1928
|
// BuildKit's oci-layout resolver expects a raw filesystem path here; it does not percent-decode
|
|
@@ -1945,7 +1942,7 @@ export async function readOciLayoutImageDigest(layoutRoot) {
|
|
|
1945
1942
|
&& /^sha256:[0-9a-f]{64}$/iu.test(entry.digest)
|
|
1946
1943
|
&& entry.annotations?.["vnd.docker.reference.type"] !== "attestation-manifest");
|
|
1947
1944
|
if (!manifest || typeof manifest.digest !== "string") {
|
|
1948
|
-
throw
|
|
1945
|
+
throw trustedSandboxFailure("sandbox_build_failed", "project OCI layout did not expose an immutable image digest.");
|
|
1949
1946
|
}
|
|
1950
1947
|
return manifest.digest;
|
|
1951
1948
|
}
|
|
@@ -1980,11 +1977,11 @@ export async function bridgeRuntimeContentDigest(packageRoot) {
|
|
|
1980
1977
|
const distRoot = path.join(root, "dist");
|
|
1981
1978
|
const distStat = await fs.lstat(distRoot).catch(() => null);
|
|
1982
1979
|
if (!distStat?.isDirectory() || distStat.isSymbolicLink()) {
|
|
1983
|
-
throw
|
|
1980
|
+
throw trustedSandboxFailure("sandbox_build_failed", "bridge runtime dist must be a real directory.");
|
|
1984
1981
|
}
|
|
1985
1982
|
const distFiles = await collectBridgeRuntimeFiles(root, "dist");
|
|
1986
1983
|
if (distFiles.length === 0) {
|
|
1987
|
-
throw
|
|
1984
|
+
throw trustedSandboxFailure("sandbox_build_failed", "bridge runtime dist is empty.");
|
|
1988
1985
|
}
|
|
1989
1986
|
const files = [packageManifest, ...(lockManifest ? [lockManifest] : []), ...distFiles]
|
|
1990
1987
|
.sort((left, right) => compareBridgeRuntimePaths(left.relativePath, right.relativePath));
|
|
@@ -2003,7 +2000,7 @@ export async function bridgeRuntimeContentDigest(packageRoot) {
|
|
|
2003
2000
|
async function requireBridgeRuntimeFile(root, relativePath) {
|
|
2004
2001
|
const file = await optionalBridgeRuntimeFile(root, relativePath);
|
|
2005
2002
|
if (!file)
|
|
2006
|
-
throw
|
|
2003
|
+
throw trustedSandboxFailure("sandbox_build_failed", `bridge runtime ${relativePath} is missing.`);
|
|
2007
2004
|
return file;
|
|
2008
2005
|
}
|
|
2009
2006
|
async function optionalBridgeRuntimeFile(root, relativePath) {
|
|
@@ -2012,7 +2009,7 @@ async function optionalBridgeRuntimeFile(root, relativePath) {
|
|
|
2012
2009
|
if (!stat)
|
|
2013
2010
|
return null;
|
|
2014
2011
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
2015
|
-
throw
|
|
2012
|
+
throw trustedSandboxFailure("sandbox_build_failed", `bridge runtime ${relativePath} must be a regular file.`);
|
|
2016
2013
|
}
|
|
2017
2014
|
return { absolutePath, relativePath: relativePath.replace(/\\/gu, "/") };
|
|
2018
2015
|
}
|
|
@@ -2024,14 +2021,14 @@ async function collectBridgeRuntimeFiles(root, relativeDirectory) {
|
|
|
2024
2021
|
for (const entry of entries) {
|
|
2025
2022
|
const relativePath = path.join(relativeDirectory, entry.name);
|
|
2026
2023
|
if (entry.isSymbolicLink()) {
|
|
2027
|
-
throw
|
|
2024
|
+
throw trustedSandboxFailure("sandbox_build_failed", `bridge runtime ${relativePath.replace(/\\/gu, "/")} cannot be a symbolic link.`);
|
|
2028
2025
|
}
|
|
2029
2026
|
if (entry.isDirectory()) {
|
|
2030
2027
|
files.push(...await collectBridgeRuntimeFiles(root, relativePath));
|
|
2031
2028
|
continue;
|
|
2032
2029
|
}
|
|
2033
2030
|
if (!entry.isFile()) {
|
|
2034
|
-
throw
|
|
2031
|
+
throw trustedSandboxFailure("sandbox_build_failed", `bridge runtime ${relativePath.replace(/\\/gu, "/")} must be a regular file.`);
|
|
2035
2032
|
}
|
|
2036
2033
|
files.push({
|
|
2037
2034
|
absolutePath: path.join(root, relativePath),
|
|
@@ -2086,7 +2083,7 @@ async function copyOptionalBridgeManifest(packageRoot, bridgeRoot, fileName) {
|
|
|
2086
2083
|
}
|
|
2087
2084
|
export function buildxBuildArgs(builder, metadataFile, tail, environment = process.env, options = {}) {
|
|
2088
2085
|
if (!/^forge-[0-9a-f]{20}$/u.test(builder))
|
|
2089
|
-
throw
|
|
2086
|
+
throw trustedSandboxFailure("sandbox_build_failed", "invalid dedicated builder name.");
|
|
2090
2087
|
const resources = buildResourcePolicy(environment);
|
|
2091
2088
|
const args = ["buildx", "build", "--builder", builder];
|
|
2092
2089
|
if (options.load !== false)
|
|
@@ -2108,7 +2105,7 @@ export function dedicatedBuildxBuilderName(runnerId, dockerContext) {
|
|
|
2108
2105
|
}
|
|
2109
2106
|
export function dedicatedBuildxBuilderContainerName(builder) {
|
|
2110
2107
|
if (!/^forge-[0-9a-f]{20}$/u.test(builder))
|
|
2111
|
-
throw
|
|
2108
|
+
throw trustedSandboxFailure("sandbox_build_failed", "invalid dedicated builder name.");
|
|
2112
2109
|
return `buildx_buildkit_${builder}0`;
|
|
2113
2110
|
}
|
|
2114
2111
|
function boundedBuildCacheSize(value, fallback) {
|
|
@@ -2132,7 +2129,7 @@ export function selectImageGcCandidates(records, keepPerProject, ttlMs, nowMs) {
|
|
|
2132
2129
|
async function sandboxProviderAuthForDispatch(dispatch) {
|
|
2133
2130
|
const unavailable = sandboxProviderAuthUnavailableMessage(dispatch.execution.provider, dispatch.execution.authKind);
|
|
2134
2131
|
if (unavailable) {
|
|
2135
|
-
throw
|
|
2132
|
+
throw trustedSandboxFailure("sandbox_auth_unavailable", unavailable);
|
|
2136
2133
|
}
|
|
2137
2134
|
const adapter = sandboxProviderAuthAdapterFor(dispatch.execution.provider, dispatch.execution.authKind);
|
|
2138
2135
|
return adapter ? adapter.prepareAttemptAuth() : null;
|
|
@@ -2176,7 +2173,7 @@ export function proxyEnvironmentArgs(dispatch, contract, resources) {
|
|
|
2176
2173
|
export function egressProxyConfig(allowedHosts) {
|
|
2177
2174
|
const domains = allowedHosts.map(host => host.startsWith("*.") ? `.${host.slice(2)}` : host).join(" ");
|
|
2178
2175
|
if (!domains)
|
|
2179
|
-
throw
|
|
2176
|
+
throw trustedSandboxFailure("sandbox_egress_policy_invalid", "public_allowlist requires at least one approved host.");
|
|
2180
2177
|
return [
|
|
2181
2178
|
"http_port 3128",
|
|
2182
2179
|
"access_log none",
|
|
@@ -2208,7 +2205,7 @@ export function serviceContainerArgs(service, image, resources, labels, security
|
|
|
2208
2205
|
else {
|
|
2209
2206
|
const healthCommand = environment.FORGE_SERVICE_BROWSER_HEALTH_CMD?.trim();
|
|
2210
2207
|
if (!healthCommand || /[\r\n\0]/u.test(healthCommand) || healthCommand.length > 1000) {
|
|
2211
|
-
throw
|
|
2208
|
+
throw trustedSandboxFailure("sandbox_service_health_policy_invalid", "browser service needs an operator-configured bounded health command.");
|
|
2212
2209
|
}
|
|
2213
2210
|
args.push("--env", "FORGE_BROWSER_TOKEN_FILE=/run/secrets/browser-token", "--volume", `${resources.secretsVolume}:/run/secrets:ro`, "--health-cmd", healthCommand, "--health-interval", "1s", "--health-timeout", "5s", "--health-retries", "60");
|
|
2214
2211
|
}
|
|
@@ -2221,13 +2218,13 @@ export function resolveSandboxSecurityOptions(executionPolicy, environment = pro
|
|
|
2221
2218
|
const seccomp = environment.FORGE_SANDBOX_SECCOMP_PROFILE?.trim();
|
|
2222
2219
|
const lsm = environment.FORGE_SANDBOX_LSM_SECURITY_OPT?.trim();
|
|
2223
2220
|
if (seccomp && (!path.isAbsolute(seccomp) || /[\r\n\0]/u.test(seccomp))) {
|
|
2224
|
-
throw
|
|
2221
|
+
throw trustedSandboxFailure("sandbox_security_profile_invalid", "seccomp profile must be an absolute host path.");
|
|
2225
2222
|
}
|
|
2226
2223
|
if (lsm && !/^(?:apparmor=[a-zA-Z0-9_.-]{1,128}|label=[a-zA-Z0-9_:,.-]{1,256})$/u.test(lsm)) {
|
|
2227
|
-
throw
|
|
2224
|
+
throw trustedSandboxFailure("sandbox_security_profile_invalid", "LSM security option is invalid.");
|
|
2228
2225
|
}
|
|
2229
2226
|
if (executionPolicy === "sandbox_required" && (!seccomp || !lsm)) {
|
|
2230
|
-
throw
|
|
2227
|
+
throw trustedSandboxFailure("sandbox_security_profile_unavailable", "sandbox_required needs explicit seccomp and LSM profiles.");
|
|
2231
2228
|
}
|
|
2232
2229
|
return [
|
|
2233
2230
|
...(seccomp ? ["--security-opt", `seccomp=${seccomp}`] : []),
|
|
@@ -2263,17 +2260,46 @@ function event(eventType, content, data) {
|
|
|
2263
2260
|
};
|
|
2264
2261
|
}
|
|
2265
2262
|
export function classifySandboxRuntimeFailure(value) {
|
|
2266
|
-
const message = value.
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
:
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2263
|
+
const message = (value instanceof Error ? value.message : String(value))
|
|
2264
|
+
.replace(/[\r\n\t]+/gu, " ")
|
|
2265
|
+
.trim()
|
|
2266
|
+
.slice(0, 2000) || "Sandbox execution failed.";
|
|
2267
|
+
const code = value instanceof ForgeFailureError
|
|
2268
|
+
? value.failureCode
|
|
2269
|
+
: "sandbox_runtime_failed";
|
|
2270
|
+
return { code, phase: failurePolicyForCode(code).phase, message };
|
|
2271
|
+
}
|
|
2272
|
+
export function applyTrustedHostFailure(result, trustedFailure) {
|
|
2273
|
+
if (!trustedFailure)
|
|
2274
|
+
return result;
|
|
2275
|
+
const originalFailure = result.failure
|
|
2276
|
+
? [{
|
|
2277
|
+
code: result.failure.code,
|
|
2278
|
+
message: result.failure.message,
|
|
2279
|
+
source: result.failure.source,
|
|
2280
|
+
}, ...result.failure.causalChain]
|
|
2281
|
+
: result.exitCode === 0
|
|
2282
|
+
? []
|
|
2283
|
+
: [{
|
|
2284
|
+
code: "sandbox_child_result_unknown",
|
|
2285
|
+
message: result.summary,
|
|
2286
|
+
source: "sandbox",
|
|
2287
|
+
}];
|
|
2288
|
+
const facts = {
|
|
2289
|
+
...failureFactsForCode(trustedFailure.code, "sandbox"),
|
|
2290
|
+
causalChain: originalFailure,
|
|
2291
|
+
};
|
|
2292
|
+
return {
|
|
2293
|
+
...result,
|
|
2294
|
+
schemaVersion: 2,
|
|
2295
|
+
exitCode: result.exitCode === 0 ? 1 : result.exitCode,
|
|
2296
|
+
summary: trustedFailure.message,
|
|
2297
|
+
failure: createFailureEnvelope(trustedFailure.message, facts),
|
|
2298
|
+
events: [
|
|
2299
|
+
...result.events,
|
|
2300
|
+
event(trustedFailure.code, trustedFailure.message, { phase: facts.phase }),
|
|
2301
|
+
],
|
|
2302
|
+
};
|
|
2277
2303
|
}
|
|
2278
2304
|
export function classifySandboxResourceFailure(value) {
|
|
2279
2305
|
const diagnostic = value.replace(/[\r\n\t]+/gu, " ").trim().slice(0, 2000);
|
|
@@ -2293,6 +2319,9 @@ export function classifySandboxResourceFailure(value) {
|
|
|
2293
2319
|
}
|
|
2294
2320
|
return null;
|
|
2295
2321
|
}
|
|
2322
|
+
function trustedSandboxFailure(code, detail, options) {
|
|
2323
|
+
return new ForgeFailureError(code, `${code}: ${detail}`, "sandbox", options);
|
|
2324
|
+
}
|
|
2296
2325
|
function parseSandboxEventLine(line) {
|
|
2297
2326
|
if (!line.trim().startsWith("{"))
|
|
2298
2327
|
return null;
|
|
@@ -2307,25 +2336,25 @@ function parseSandboxEventLine(line) {
|
|
|
2307
2336
|
return null;
|
|
2308
2337
|
const candidate = value.event;
|
|
2309
2338
|
if (!candidate || typeof candidate !== "object")
|
|
2310
|
-
throw
|
|
2339
|
+
throw trustedSandboxFailure("sandbox_event_invalid", "runner event envelope is missing event data.");
|
|
2311
2340
|
const eventValue = candidate;
|
|
2312
2341
|
for (const field of ["role", "kind", "content", "eventType"]) {
|
|
2313
2342
|
if (typeof eventValue[field] !== "string")
|
|
2314
|
-
throw
|
|
2343
|
+
throw trustedSandboxFailure("sandbox_event_invalid", `${field} must be a string.`);
|
|
2315
2344
|
}
|
|
2316
2345
|
if (eventValue.role.length > 64
|
|
2317
2346
|
|| eventValue.kind.length > 128
|
|
2318
2347
|
|| eventValue.eventType.length > 128
|
|
2319
2348
|
|| Buffer.byteLength(eventValue.content, "utf8") > 256 * 1024) {
|
|
2320
|
-
throw
|
|
2349
|
+
throw trustedSandboxFailure("sandbox_event_invalid", "runner event exceeds field bounds.");
|
|
2321
2350
|
}
|
|
2322
2351
|
if (eventValue.data !== undefined && eventValue.data !== null
|
|
2323
2352
|
&& (typeof eventValue.data !== "string" || Buffer.byteLength(eventValue.data, "utf8") > 256 * 1024)) {
|
|
2324
|
-
throw
|
|
2353
|
+
throw trustedSandboxFailure("sandbox_event_invalid", "runner event data exceeds field bounds.");
|
|
2325
2354
|
}
|
|
2326
2355
|
if (eventValue.occurredAt !== undefined && eventValue.occurredAt !== null
|
|
2327
2356
|
&& (typeof eventValue.occurredAt !== "string" || !Number.isFinite(Date.parse(eventValue.occurredAt)))) {
|
|
2328
|
-
throw
|
|
2357
|
+
throw trustedSandboxFailure("sandbox_event_invalid", "runner event occurredAt is invalid.");
|
|
2329
2358
|
}
|
|
2330
2359
|
return {
|
|
2331
2360
|
role: eventValue.role,
|