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