@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.1ff37038366e
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 +165 -1
- 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 +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2427 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- 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 +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- 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 +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- 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-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +397 -0
- 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 +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- 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 +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +58 -0
- package/dist/runtime/host-runtime-contract.js +813 -0
- package/dist/runtime/host-runtime-lease.d.ts +217 -0
- package/dist/runtime/host-runtime-lease.js +2487 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- 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 +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +285 -0
- 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 +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- 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.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +6 -0
- package/dist/sandbox/repository-lock.js +128 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2367 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1015 -0
- 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 +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +840 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- 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 +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- 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 +16 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- 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.d.ts +3 -0
- package/dist/session/session-redaction.js +448 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- 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 +21 -1
- 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.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- 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-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { formatBytes } from "../shared/format/format-bytes.js";
|
|
6
|
+
import { captureHostProcessIdentity } from "../shared/process/host-process-supervisor.js";
|
|
7
|
+
import { RecoveryStabilizedMemory, resolveAdaptiveMemoryReserveBytes } from "../runtime/memory-capacity.js";
|
|
8
|
+
import { BoundedCommandRunner } from "./command-runner.js";
|
|
9
|
+
const minimumWorkspaceBytes = 10 * 1024 * 1024 * 1024;
|
|
10
|
+
export class DockerPreflight {
|
|
11
|
+
commands;
|
|
12
|
+
cacheMs;
|
|
13
|
+
generation = 0;
|
|
14
|
+
cached = null;
|
|
15
|
+
fullPreflightAt = 0;
|
|
16
|
+
staticPreflight = null;
|
|
17
|
+
inspectionInFlight = null;
|
|
18
|
+
lastCapacitySample = null;
|
|
19
|
+
options;
|
|
20
|
+
docker;
|
|
21
|
+
ownership = new DockerContextOwnershipLock();
|
|
22
|
+
diskPressureLatched = false;
|
|
23
|
+
memoryAvailability = new RecoveryStabilizedMemory();
|
|
24
|
+
constructor(options = {}, commands = new BoundedCommandRunner(), cacheMs = 30_000) {
|
|
25
|
+
this.commands = commands;
|
|
26
|
+
this.cacheMs = cacheMs;
|
|
27
|
+
this.options = typeof options === "string" ? { dockerExecutable: options } : options;
|
|
28
|
+
this.docker = this.options.dockerExecutable ?? process.env.FORGE_DOCKER_EXECUTABLE ?? "docker";
|
|
29
|
+
}
|
|
30
|
+
unavailable(code, reason, features) {
|
|
31
|
+
const totalMemoryBytes = os.totalmem();
|
|
32
|
+
this.memoryAvailability.observe(0);
|
|
33
|
+
return unavailable(this.generation, code, reason, this.memoryReserveBytes(totalMemoryBytes), this.defaultAdmissionMemoryBytes(), totalMemoryBytes, features);
|
|
34
|
+
}
|
|
35
|
+
memoryReserveBytes(totalMemoryBytes) {
|
|
36
|
+
return resolveSandboxHostMemoryReserveBytes(totalMemoryBytes, this.options.hostMemoryReserveBytes);
|
|
37
|
+
}
|
|
38
|
+
defaultAdmissionMemoryBytes() {
|
|
39
|
+
const workerMemoryLimitBytes = positiveInteger(this.options.defaultWorkerMemoryLimitBytes, 2 * 1024 ** 3);
|
|
40
|
+
const serviceMemoryLimitBytes = nonNegativeInteger(this.options.defaultServiceMemoryLimitBytes, 0);
|
|
41
|
+
const runtimeOverheadBytes = Math.max(128 * 1024 ** 2, nonNegativeInteger(this.options.runtimeOverheadBytes, 128 * 1024 ** 2));
|
|
42
|
+
return workerMemoryLimitBytes + serviceMemoryLimitBytes + runtimeOverheadBytes;
|
|
43
|
+
}
|
|
44
|
+
current() {
|
|
45
|
+
return this.cached;
|
|
46
|
+
}
|
|
47
|
+
async refresh() {
|
|
48
|
+
await fs.mkdir(this.options.stateRoot ?? path.join(os.homedir(), ".forge", "bridge", "sandboxes"), { recursive: true, mode: 0o700 });
|
|
49
|
+
const result = await this.run(true);
|
|
50
|
+
if (canRunDockerMaintenance(result)) {
|
|
51
|
+
await this.ownership.acquire(this.options.dockerContext);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
async release() {
|
|
56
|
+
await this.ownership.release();
|
|
57
|
+
}
|
|
58
|
+
async run(force = false) {
|
|
59
|
+
const inFlight = this.inspectionInFlight;
|
|
60
|
+
if (inFlight) {
|
|
61
|
+
const result = await inFlight.promise;
|
|
62
|
+
if (inFlight.kind === "full" || (!force && Date.now() - this.fullPreflightAt < this.cacheMs)) {
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
return await this.run(force);
|
|
66
|
+
}
|
|
67
|
+
if (!force && this.cached && Date.now() - this.fullPreflightAt < this.cacheMs)
|
|
68
|
+
return this.cached;
|
|
69
|
+
return await this.startInspection("full", () => this.inspect().catch((error) => this.unavailable("docker_unreachable", error instanceof Error ? error.message : String(error))), true);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Refreshes only the admission inputs that change while the daemon's static
|
|
73
|
+
* capability snapshot remains valid. Heartbeats use this path so periodic
|
|
74
|
+
* liveness reporting does not wake Docker/WSL to rerun feature probes.
|
|
75
|
+
*/
|
|
76
|
+
async refreshCapacity() {
|
|
77
|
+
if (this.inspectionInFlight) {
|
|
78
|
+
return await this.inspectionInFlight.promise;
|
|
79
|
+
}
|
|
80
|
+
const staticPreflight = this.staticPreflight;
|
|
81
|
+
if (!staticPreflight || !this.cached || !canRunDockerMaintenance(this.cached)) {
|
|
82
|
+
return this.cached ?? this.unavailable("docker_preflight_required", "Docker capability preflight has not completed.");
|
|
83
|
+
}
|
|
84
|
+
return await this.startInspection("capacity", () => this.inspectCapacity(staticPreflight).catch((error) => this.unavailable("docker_capacity_unavailable", error instanceof Error ? error.message : String(error), staticPreflight.features)), false);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* When the cached static preflight remains usable, produces a capacity sample
|
|
88
|
+
* that began after this admission request. An older inspection is awaited,
|
|
89
|
+
* not preempted or reused. Later admission callers may share the next sample
|
|
90
|
+
* only when it began after they entered this method. Without a usable static
|
|
91
|
+
* snapshot, the existing fail-closed result is returned without probing.
|
|
92
|
+
*/
|
|
93
|
+
async refreshCapacityForAdmission() {
|
|
94
|
+
const generationAtRequest = this.generation;
|
|
95
|
+
while (true) {
|
|
96
|
+
const inFlight = this.inspectionInFlight;
|
|
97
|
+
if (inFlight) {
|
|
98
|
+
if (inFlight.kind === "capacity" && inFlight.generation > generationAtRequest) {
|
|
99
|
+
return await inFlight.promise;
|
|
100
|
+
}
|
|
101
|
+
await inFlight.promise;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const lastCapacitySample = this.lastCapacitySample;
|
|
105
|
+
if (lastCapacitySample
|
|
106
|
+
&& lastCapacitySample.generation > generationAtRequest
|
|
107
|
+
&& lastCapacitySample.generation === this.generation
|
|
108
|
+
&& lastCapacitySample.result === this.cached) {
|
|
109
|
+
return lastCapacitySample.result;
|
|
110
|
+
}
|
|
111
|
+
const staticPreflight = this.staticPreflight;
|
|
112
|
+
if (!staticPreflight || !this.cached || !canRunDockerMaintenance(this.cached)) {
|
|
113
|
+
return this.cached ?? this.unavailable("docker_preflight_required", "Docker capability preflight has not completed.");
|
|
114
|
+
}
|
|
115
|
+
return await this.startInspection("capacity", () => this.inspectCapacity(staticPreflight).catch((error) => this.unavailable("docker_capacity_unavailable", error instanceof Error ? error.message : String(error), staticPreflight.features)), false);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
startInspection(kind, inspect, full) {
|
|
119
|
+
this.generation += 1;
|
|
120
|
+
const generation = this.generation;
|
|
121
|
+
const operation = Promise.resolve()
|
|
122
|
+
.then(inspect)
|
|
123
|
+
.then((result) => {
|
|
124
|
+
this.cached = result;
|
|
125
|
+
if (kind === "capacity")
|
|
126
|
+
this.lastCapacitySample = { generation, result };
|
|
127
|
+
if (full)
|
|
128
|
+
this.fullPreflightAt = Date.now();
|
|
129
|
+
return result;
|
|
130
|
+
})
|
|
131
|
+
.finally(() => {
|
|
132
|
+
if (this.inspectionInFlight?.promise === operation) {
|
|
133
|
+
this.inspectionInFlight = null;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
this.inspectionInFlight = { kind, generation, promise: operation };
|
|
137
|
+
return operation;
|
|
138
|
+
}
|
|
139
|
+
async inspect() {
|
|
140
|
+
if (!new Set(["linux", "darwin", "win32"]).has(process.platform)) {
|
|
141
|
+
return this.unavailable("platform_unsupported", `Docker sandboxes are not supported on ${process.platform}.`);
|
|
142
|
+
}
|
|
143
|
+
const dockerArgs = this.options.dockerContext ? ["--context", this.options.dockerContext] : [];
|
|
144
|
+
const infoResult = await this.commands.run(this.docker, [...dockerArgs, "info", "--format", "{{json .}}"], { timeoutMs: 10_000, maxOutputBytes: 512 * 1024 });
|
|
145
|
+
if (infoResult.exitCode !== 0)
|
|
146
|
+
return this.unavailable("docker_unreachable", singleLine(infoResult.stderr || infoResult.stdout || "Docker daemon is unreachable."));
|
|
147
|
+
const info = parseInfo(infoResult.stdout);
|
|
148
|
+
if (String(info.OSType ?? "").toLowerCase() !== "linux")
|
|
149
|
+
return this.unavailable("docker_linux_required", "Forge sandbox images require a Linux Docker daemon.");
|
|
150
|
+
const securityOptions = strings(info.SecurityOptions).map((value) => value.toLowerCase());
|
|
151
|
+
const rootless = securityOptions.some((value) => value.includes("rootless"));
|
|
152
|
+
const userNamespaces = rootless || securityOptions.some((value) => value.includes("userns"));
|
|
153
|
+
const cgroupV2 = String(info.CgroupVersion ?? "") === "2";
|
|
154
|
+
const seccomp = securityOptions.some((value) => value.includes("seccomp"));
|
|
155
|
+
const lsm = securityOptions.some((value) => value.includes("apparmor") || value.includes("selinux"));
|
|
156
|
+
const storageDriver = String(info.Driver ?? "").toLowerCase();
|
|
157
|
+
const storageQuota = storageDriver === "overlay2" && Boolean(info.DriverStatus?.some?.((entry) => JSON.stringify(entry).toLowerCase().includes("project quota")));
|
|
158
|
+
const desktopDeveloperMode = process.platform !== "linux" && process.env.FORGE_ALLOW_DOCKER_DESKTOP_SANDBOX === "true";
|
|
159
|
+
const requireRootless = this.options.requireRootless ?? process.env.FORGE_SANDBOX_REQUIRE_ROOTLESS !== "false";
|
|
160
|
+
if (!rootless && !desktopDeveloperMode && requireRootless) {
|
|
161
|
+
return this.unavailable("docker_rootless_required", "Docker daemon is reachable but is not running rootless.", {
|
|
162
|
+
rootless, userNamespaces, cgroupV2, seccomp, lsm, storageQuota, storagePolicy: false, compose: false, internalNetwork: true, buildkit: false, imagePolicy: false, securityProfile: false, egressProxy: false
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const composeResult = await this.commands.run(this.docker, [...dockerArgs, "compose", "version", "--short"], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => null);
|
|
166
|
+
const compose = composeResult?.exitCode === 0;
|
|
167
|
+
const buildxResult = await this.commands.run(this.docker, [...dockerArgs, "buildx", "version"], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => null);
|
|
168
|
+
const buildxHelp = await this.commands.run(this.docker, [...dockerArgs, "buildx", "build", "--help"], { timeoutMs: 5_000, maxOutputBytes: 256 * 1024 }).catch(() => null);
|
|
169
|
+
const buildxPruneHelp = await this.commands.run(this.docker, [...dockerArgs, "buildx", "prune", "--help"], { timeoutMs: 5_000, maxOutputBytes: 256 * 1024 }).catch(() => null);
|
|
170
|
+
const containerUpdateHelp = await this.commands.run(this.docker, [...dockerArgs, "container", "update", "--help"], { timeoutMs: 5_000, maxOutputBytes: 256 * 1024 }).catch(() => null);
|
|
171
|
+
const buildkit = buildxResult?.exitCode === 0
|
|
172
|
+
&& buildxHelp?.exitCode === 0
|
|
173
|
+
&& ["--ulimit", "--shm-size"].every(flag => buildxHelp.stdout.includes(flag))
|
|
174
|
+
&& buildxPruneHelp?.exitCode === 0
|
|
175
|
+
&& ["--max-used-space", "--min-free-space", "--reserved-space"].every(flag => buildxPruneHelp.stdout.includes(flag))
|
|
176
|
+
&& containerUpdateHelp?.exitCode === 0
|
|
177
|
+
&& ["--pids-limit", "--memory", "--memory-swap"].every(flag => containerUpdateHelp.stdout.includes(flag));
|
|
178
|
+
const imagePolicy = process.env.FORGE_SANDBOX_IMAGE_POLICY_ENFORCED === "true"
|
|
179
|
+
&& Boolean(process.env.FORGE_SANDBOX_SBOM_EXECUTABLE?.trim())
|
|
180
|
+
&& Boolean(process.env.FORGE_SANDBOX_VULNERABILITY_EXECUTABLE?.trim())
|
|
181
|
+
&& Boolean(process.env.FORGE_SANDBOX_PROVENANCE_SIGNING_KEY?.trim())
|
|
182
|
+
&& Boolean(process.env.FORGE_SANDBOX_SIGNATURE_EXECUTABLE?.trim())
|
|
183
|
+
&& Boolean(process.env.FORGE_SANDBOX_SIGNATURE_PUBLIC_KEY?.trim())
|
|
184
|
+
&& process.env.FORGE_SANDBOX_BUILD_EGRESS_ENFORCED === "true";
|
|
185
|
+
const securityProfile = Boolean(process.env.FORGE_SANDBOX_SECCOMP_PROFILE?.trim())
|
|
186
|
+
&& Boolean(process.env.FORGE_SANDBOX_LSM_SECURITY_OPT?.trim())
|
|
187
|
+
&& seccomp && lsm;
|
|
188
|
+
const egressProxy = process.env.FORGE_SANDBOX_EGRESS_ENFORCED === "true"
|
|
189
|
+
&& /@sha256:[0-9a-f]{64}$/iu.test(process.env.FORGE_SANDBOX_EGRESS_PROXY_IMAGE?.trim() ?? "");
|
|
190
|
+
const storagePolicy = storagePolicyAvailable(storageQuota);
|
|
191
|
+
const configuredStateRoot = this.options.stateRoot ?? path.join(os.homedir(), ".forge", "bridge", "sandboxes");
|
|
192
|
+
const dockerRoot = process.platform === "linux" && typeof info.DockerRootDir === "string" && path.isAbsolute(info.DockerRootDir)
|
|
193
|
+
? info.DockerRootDir
|
|
194
|
+
: configuredStateRoot;
|
|
195
|
+
const dockerDesktop = process.platform !== "linux"
|
|
196
|
+
&& String(info.OperatingSystem ?? "").toLowerCase().includes("docker desktop");
|
|
197
|
+
const reportedDockerMemoryBytes = Number(info.MemTotal);
|
|
198
|
+
const features = { rootless, userNamespaces, cgroupV2, seccomp, lsm, storageQuota, storagePolicy, compose, internalNetwork: true, buildkit, imagePolicy, securityProfile, egressProxy };
|
|
199
|
+
const staticPreflight = {
|
|
200
|
+
dockerArgs,
|
|
201
|
+
dockerRoot,
|
|
202
|
+
dockerDesktop,
|
|
203
|
+
dockerMemoryBytes: Number.isFinite(reportedDockerMemoryBytes) && reportedDockerMemoryBytes > 0
|
|
204
|
+
? reportedDockerMemoryBytes
|
|
205
|
+
: null,
|
|
206
|
+
runtimeVersion: `docker/${String(info.ServerVersion ?? "unknown")}${rootless ? "-rootless" : "-desktop"}`,
|
|
207
|
+
features,
|
|
208
|
+
};
|
|
209
|
+
this.staticPreflight = staticPreflight;
|
|
210
|
+
return await this.inspectCapacity(staticPreflight);
|
|
211
|
+
}
|
|
212
|
+
async inspectCapacity(staticPreflight) {
|
|
213
|
+
const disk = await diskSpace(staticPreflight.dockerRoot);
|
|
214
|
+
const requiredWorkspaceBytes = this.options.minimumWorkspaceBytes ?? minimumWorkspaceBytes;
|
|
215
|
+
const highWatermark = Math.min(99, Math.max(1, this.options.diskHighWatermarkPercent ?? 90));
|
|
216
|
+
const lowWatermark = Math.min(highWatermark - 1, Math.max(0, this.options.diskLowWatermarkPercent ?? highWatermark - 10));
|
|
217
|
+
const diskLow = evaluateDiskPressure(disk.available, disk.total, requiredWorkspaceBytes, highWatermark, lowWatermark, this.diskPressureLatched);
|
|
218
|
+
this.diskPressureLatched = diskLow;
|
|
219
|
+
const requiredAvailableBytes = requiredAvailableBytesToClearDiskPressure(disk.total, requiredWorkspaceBytes, lowWatermark);
|
|
220
|
+
const rawHostAvailableMemoryBytesBefore = Math.max(0, os.freemem());
|
|
221
|
+
const hostTotalMemoryBytes = os.totalmem();
|
|
222
|
+
let totalMemoryBytes = hostTotalMemoryBytes;
|
|
223
|
+
// Native Docker shares the host capacity domain. Docker Desktop has a
|
|
224
|
+
// second VM/daemon domain, so neither domain may be treated as a substitute
|
|
225
|
+
// for the other when deciding whether new committed memory is safe.
|
|
226
|
+
let rawDaemonAvailableMemoryBytes = rawHostAvailableMemoryBytesBefore;
|
|
227
|
+
let daemonMemorySampleFailure = null;
|
|
228
|
+
if (staticPreflight.dockerDesktop) {
|
|
229
|
+
rawDaemonAvailableMemoryBytes = null;
|
|
230
|
+
if (staticPreflight.dockerMemoryBytes) {
|
|
231
|
+
totalMemoryBytes = staticPreflight.dockerMemoryBytes;
|
|
232
|
+
const stats = await this.commands.run(this.docker, [...staticPreflight.dockerArgs, "stats", "--no-stream", "--format", "{{.MemUsage}}"], { timeoutMs: 10_000, maxOutputBytes: 512 * 1024 }).catch(() => null);
|
|
233
|
+
if (stats?.exitCode === 0) {
|
|
234
|
+
let dockerUsedMemoryBytes = 0;
|
|
235
|
+
for (const line of stats.stdout.split(/\r?\n/gu).map(value => value.trim()).filter(Boolean)) {
|
|
236
|
+
const usage = tryParseDockerMemoryUsage(line);
|
|
237
|
+
if (usage === null) {
|
|
238
|
+
daemonMemorySampleFailure = "Docker returned an invalid memory-usage sample.";
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
dockerUsedMemoryBytes += usage;
|
|
242
|
+
}
|
|
243
|
+
if (daemonMemorySampleFailure === null) {
|
|
244
|
+
rawDaemonAvailableMemoryBytes = Math.max(0, staticPreflight.dockerMemoryBytes - dockerUsedMemoryBytes);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
daemonMemorySampleFailure = singleLine(stats?.stderr || stats?.stdout || "Docker Desktop memory capacity could not be sampled.");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
daemonMemorySampleFailure = "Docker Desktop did not report a valid memory-capacity limit.";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const rawHostAvailableMemoryBytesAfter = Math.max(0, os.freemem());
|
|
256
|
+
const rawHostAvailableMemoryBytes = Math.min(rawHostAvailableMemoryBytesBefore, rawHostAvailableMemoryBytesAfter);
|
|
257
|
+
if (!staticPreflight.dockerDesktop) {
|
|
258
|
+
rawDaemonAvailableMemoryBytes = rawHostAvailableMemoryBytes;
|
|
259
|
+
}
|
|
260
|
+
const sampledAt = new Date().toISOString();
|
|
261
|
+
const rawAvailableMemoryBytes = rawDaemonAvailableMemoryBytes === null
|
|
262
|
+
? 0
|
|
263
|
+
: Math.min(rawHostAvailableMemoryBytes, rawDaemonAvailableMemoryBytes);
|
|
264
|
+
const availableMemoryBytes = this.memoryAvailability.observe(rawAvailableMemoryBytes);
|
|
265
|
+
const cpuCount = Math.max(1, os.availableParallelism?.() ?? os.cpus().length);
|
|
266
|
+
const availableCpuMillis = Math.max(0, (cpuCount - 1) * 1000);
|
|
267
|
+
// The reserve protects the physical host and its control plane. Docker
|
|
268
|
+
// Desktop's VM total remains the independent daemon commitment ceiling,
|
|
269
|
+
// but must not shrink the reserve derived from the Windows host.
|
|
270
|
+
const memoryReserveBytes = this.memoryReserveBytes(hostTotalMemoryBytes);
|
|
271
|
+
const workerMemoryLimitBytes = positiveInteger(this.options.defaultWorkerMemoryLimitBytes, 2 * 1024 ** 3);
|
|
272
|
+
const serviceMemoryLimitBytes = nonNegativeInteger(this.options.defaultServiceMemoryLimitBytes, 0);
|
|
273
|
+
const runtimeOverheadBytes = Math.max(128 * 1024 ** 2, nonNegativeInteger(this.options.runtimeOverheadBytes, 128 * 1024 ** 2));
|
|
274
|
+
const sandboxMemoryBytes = workerMemoryLimitBytes + serviceMemoryLimitBytes + runtimeOverheadBytes;
|
|
275
|
+
const minimumAdmissionMemoryBytes = 128 * 1024 ** 2 + Math.max(128 * 1024 ** 2, runtimeOverheadBytes);
|
|
276
|
+
const sandboxCpuMillis = 2_000;
|
|
277
|
+
const cpuSlots = Math.max(0, Math.floor(availableCpuMillis / sandboxCpuMillis));
|
|
278
|
+
const configuredMax = positiveInteger(this.options.maxConcurrentSandboxes, positiveInteger(process.env.FORGE_MAX_CONCURRENT_SANDBOXES, 3));
|
|
279
|
+
const availableForAdmissionBytes = Math.max(0, availableMemoryBytes - memoryReserveBytes);
|
|
280
|
+
const capacityLowCode = !diskLow && rawDaemonAvailableMemoryBytes === null
|
|
281
|
+
? "docker_memory_sample_unavailable"
|
|
282
|
+
: !diskLow && availableForAdmissionBytes < minimumAdmissionMemoryBytes
|
|
283
|
+
? "docker_memory_low"
|
|
284
|
+
: !diskLow && cpuSlots <= 0
|
|
285
|
+
? "docker_cpu_low"
|
|
286
|
+
: null;
|
|
287
|
+
const maxConcurrentSandboxes = diskLow || capacityLowCode ? 0 : Math.min(3, configuredMax, cpuSlots);
|
|
288
|
+
const unavailableCode = diskLow ? "docker_disk_low" : capacityLowCode;
|
|
289
|
+
const unavailableReason = diskLow
|
|
290
|
+
? `Docker workspace has ${formatBytes(disk.available)} available · ${formatBytes(requiredAvailableBytes)} required to clear disk pressure.`
|
|
291
|
+
: unavailableCode === "docker_memory_sample_unavailable"
|
|
292
|
+
? `Docker Desktop memory capacity is unavailable (${daemonMemorySampleFailure ?? "unknown sampler failure"}); the host reports ${formatBytes(rawHostAvailableMemoryBytes)} free, but Forge will not admit a sandbox until both memory domains are known.`
|
|
293
|
+
: unavailableCode === "docker_memory_low"
|
|
294
|
+
? `${staticPreflight.dockerDesktop
|
|
295
|
+
? `The host reports ${formatBytes(rawHostAvailableMemoryBytes)} free and the Docker domain reports ${formatBytes(rawDaemonAvailableMemoryBytes ?? 0)} free; Forge uses the conservative ${formatBytes(rawAvailableMemoryBytes)} bound`
|
|
296
|
+
: `Docker reports ${formatBytes(rawAvailableMemoryBytes)} free`} and reserves ${formatBytes(memoryReserveBytes)} for the host, leaving ${formatBytes(availableForAdmissionBytes)} for admission; the smallest supported sandbox requires ${formatBytes(minimumAdmissionMemoryBytes)}. Exact attempt memory is calculated dynamically from its worker profile, sidecars, and runtime helpers.`
|
|
297
|
+
: unavailableCode === "docker_cpu_low"
|
|
298
|
+
? `Docker host has ${availableCpuMillis} available CPU millis; at least ${sandboxCpuMillis} are needed for one sandbox.`
|
|
299
|
+
: null;
|
|
300
|
+
const capacityAvailable = unavailableCode === null;
|
|
301
|
+
return {
|
|
302
|
+
available: capacityAvailable,
|
|
303
|
+
healthy: capacityAvailable,
|
|
304
|
+
generation: this.generation,
|
|
305
|
+
sampledAt,
|
|
306
|
+
runtimeVersion: staticPreflight.runtimeVersion,
|
|
307
|
+
maxConcurrentSandboxes,
|
|
308
|
+
totalMemoryBytes,
|
|
309
|
+
rawHostAvailableMemoryBytes,
|
|
310
|
+
rawDaemonAvailableMemoryBytes,
|
|
311
|
+
rawAvailableMemoryBytes,
|
|
312
|
+
availableMemoryBytes,
|
|
313
|
+
hostReserveBytes: memoryReserveBytes,
|
|
314
|
+
admissionMemoryBytes: sandboxMemoryBytes,
|
|
315
|
+
availableCpuMillis,
|
|
316
|
+
availableWorkspaceBytes: disk.available,
|
|
317
|
+
diskLow,
|
|
318
|
+
unavailableCode,
|
|
319
|
+
unavailableReason,
|
|
320
|
+
features: staticPreflight.features,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Uses the same reviewed reserve vector as host-runtime admission. The input
|
|
326
|
+
* is always the physical host total; Docker Desktop's VM total is tracked as
|
|
327
|
+
* a separate daemon commitment domain.
|
|
328
|
+
*/
|
|
329
|
+
export function resolveSandboxHostMemoryReserveBytes(totalMemoryBytes, explicitBytes, environment = process.env) {
|
|
330
|
+
return resolveAdaptiveMemoryReserveBytes(totalMemoryBytes, {
|
|
331
|
+
explicitBytes,
|
|
332
|
+
floorBytes: positiveInteger(environment.FORGE_SANDBOX_HOST_MEMORY_RESERVE_FLOOR_BYTES, 1024 ** 3),
|
|
333
|
+
ceilingBytes: positiveInteger(environment.FORGE_SANDBOX_HOST_MEMORY_RESERVE_CEILING_BYTES, 4 * 1024 ** 3),
|
|
334
|
+
percent: positiveNumber(environment.FORGE_SANDBOX_HOST_MEMORY_RESERVE_PERCENT, 12.5),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const dockerMaintenanceUnavailableCodes = new Set([
|
|
338
|
+
"docker_disk_low",
|
|
339
|
+
"docker_memory_low",
|
|
340
|
+
"docker_memory_sample_unavailable",
|
|
341
|
+
"docker_cpu_low",
|
|
342
|
+
]);
|
|
343
|
+
export function canRunDockerMaintenance(preflight) {
|
|
344
|
+
return preflight.healthy || dockerMaintenanceUnavailableCodes.has(preflight.unavailableCode ?? "");
|
|
345
|
+
}
|
|
346
|
+
export function evaluateDiskPressure(availableBytes, totalBytes, minimumAvailableBytes, highWatermarkPercent, lowWatermarkPercent, pressureLatched) {
|
|
347
|
+
if (availableBytes < minimumAvailableBytes)
|
|
348
|
+
return true;
|
|
349
|
+
if (!(totalBytes > 0))
|
|
350
|
+
return true;
|
|
351
|
+
const usedPercent = (1 - availableBytes / totalBytes) * 100;
|
|
352
|
+
if (usedPercent >= highWatermarkPercent)
|
|
353
|
+
return true;
|
|
354
|
+
return pressureLatched && usedPercent > lowWatermarkPercent;
|
|
355
|
+
}
|
|
356
|
+
export function requiredAvailableBytesToClearDiskPressure(totalBytes, minimumAvailableBytes, lowWatermarkPercent) {
|
|
357
|
+
const watermarkBytes = totalBytes > 0
|
|
358
|
+
? Math.ceil(totalBytes * (1 - Math.min(100, Math.max(0, lowWatermarkPercent)) / 100))
|
|
359
|
+
: 0;
|
|
360
|
+
return Math.max(0, minimumAvailableBytes, watermarkBytes);
|
|
361
|
+
}
|
|
362
|
+
export function storagePolicyAvailable(storageQuota, environment = process.env) {
|
|
363
|
+
return storageQuota || environment.FORGE_SANDBOX_DEDICATED_DISK === "true";
|
|
364
|
+
}
|
|
365
|
+
export class DockerContextOwnershipLock {
|
|
366
|
+
lockPath = null;
|
|
367
|
+
lockToken = null;
|
|
368
|
+
async acquire(contextName) {
|
|
369
|
+
if (this.lockPath)
|
|
370
|
+
return;
|
|
371
|
+
contextName = contextName?.trim() || process.env.DOCKER_CONTEXT?.trim() || process.env.DOCKER_HOST?.trim() || "default";
|
|
372
|
+
const key = createHash("sha256").update(contextName).digest("hex").slice(0, 24);
|
|
373
|
+
const lockPath = path.join(os.tmpdir(), `forge-docker-context-${key}.lock`);
|
|
374
|
+
const token = randomUUID();
|
|
375
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
376
|
+
try {
|
|
377
|
+
const handle = await fs.open(lockPath, "wx", 0o600);
|
|
378
|
+
await handle.writeFile(JSON.stringify({
|
|
379
|
+
schemaVersion: 2,
|
|
380
|
+
token,
|
|
381
|
+
pid: process.pid,
|
|
382
|
+
processIdentity: captureHostProcessIdentity(process.pid),
|
|
383
|
+
contextName,
|
|
384
|
+
acquiredAt: new Date().toISOString(),
|
|
385
|
+
}));
|
|
386
|
+
await handle.sync();
|
|
387
|
+
await handle.close();
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
const owner = await readLock(lockPath);
|
|
391
|
+
if (!dockerContextOwnerIsActive(owner)) {
|
|
392
|
+
await fs.rm(lockPath, { force: true });
|
|
393
|
+
return await this.acquire(contextName);
|
|
394
|
+
}
|
|
395
|
+
throw new Error(`docker_context_owned: Docker context '${contextName}' is already owned by Forge runner pid ${owner?.pid ?? "unknown"}.`, { cause: error });
|
|
396
|
+
}
|
|
397
|
+
this.lockPath = lockPath;
|
|
398
|
+
this.lockToken = token;
|
|
399
|
+
}
|
|
400
|
+
async release() {
|
|
401
|
+
if (!this.lockPath)
|
|
402
|
+
return;
|
|
403
|
+
const owner = await readLock(this.lockPath);
|
|
404
|
+
if (owner?.token === this.lockToken) {
|
|
405
|
+
await fs.rm(this.lockPath, { force: true });
|
|
406
|
+
}
|
|
407
|
+
this.lockPath = null;
|
|
408
|
+
this.lockToken = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function parseInfo(stdout) {
|
|
412
|
+
try {
|
|
413
|
+
const value = JSON.parse(stdout.trim());
|
|
414
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
415
|
+
throw new Error("not an object");
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
throw new Error("Docker returned an invalid info document.", { cause: error });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function unavailable(generation, code, reason, hostReserveBytes, admissionMemoryBytes, totalMemoryBytes, features = {
|
|
423
|
+
rootless: false, userNamespaces: false, cgroupV2: false, seccomp: false, lsm: false, storageQuota: false, storagePolicy: false, compose: false, internalNetwork: false, buildkit: false, imagePolicy: false, securityProfile: false, egressProxy: false
|
|
424
|
+
}) {
|
|
425
|
+
const rawHostAvailableMemoryBytes = Math.max(0, os.freemem());
|
|
426
|
+
return {
|
|
427
|
+
available: false,
|
|
428
|
+
healthy: false,
|
|
429
|
+
generation,
|
|
430
|
+
sampledAt: new Date().toISOString(),
|
|
431
|
+
runtimeVersion: null,
|
|
432
|
+
maxConcurrentSandboxes: 0,
|
|
433
|
+
totalMemoryBytes,
|
|
434
|
+
rawHostAvailableMemoryBytes,
|
|
435
|
+
rawDaemonAvailableMemoryBytes: null,
|
|
436
|
+
rawAvailableMemoryBytes: rawHostAvailableMemoryBytes,
|
|
437
|
+
availableMemoryBytes: rawHostAvailableMemoryBytes,
|
|
438
|
+
hostReserveBytes,
|
|
439
|
+
admissionMemoryBytes,
|
|
440
|
+
availableCpuMillis: Math.max(0, (os.availableParallelism?.() ?? os.cpus().length) * 1000),
|
|
441
|
+
availableWorkspaceBytes: 0,
|
|
442
|
+
diskLow: code === "docker_disk_low",
|
|
443
|
+
unavailableCode: code,
|
|
444
|
+
unavailableReason: singleLine(reason),
|
|
445
|
+
features,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function diskSpace(inputPath) {
|
|
449
|
+
const stats = await fs.statfs(inputPath, { bigint: true }).catch(() => null);
|
|
450
|
+
if (!stats)
|
|
451
|
+
return { available: 0, total: 0 };
|
|
452
|
+
return {
|
|
453
|
+
available: safeNumber(stats.bavail * stats.bsize),
|
|
454
|
+
total: safeNumber(stats.blocks * stats.bsize),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function safeNumber(value) {
|
|
458
|
+
return Number(value > BigInt(Number.MAX_SAFE_INTEGER) ? BigInt(Number.MAX_SAFE_INTEGER) : value);
|
|
459
|
+
}
|
|
460
|
+
function strings(value) {
|
|
461
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
462
|
+
}
|
|
463
|
+
function positiveInteger(value, fallback) {
|
|
464
|
+
const parsed = Number(value);
|
|
465
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
466
|
+
}
|
|
467
|
+
function singleLine(value) {
|
|
468
|
+
return value.replace(/[\r\n\t]+/gu, " ").trim().slice(0, 1000);
|
|
469
|
+
}
|
|
470
|
+
function nonNegativeInteger(value, fallback) {
|
|
471
|
+
const parsed = Number(value);
|
|
472
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
473
|
+
}
|
|
474
|
+
function positiveNumber(value, fallback) {
|
|
475
|
+
const parsed = Number(value);
|
|
476
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
477
|
+
}
|
|
478
|
+
function tryParseDockerMemoryUsage(value) {
|
|
479
|
+
const usage = value.split("/", 1)[0]?.trim().replaceAll(" ", "") ?? "";
|
|
480
|
+
const match = /^(\d+(?:\.\d+)?)(b|kib|mib|gib|tib)$/iu.exec(usage);
|
|
481
|
+
if (!match)
|
|
482
|
+
return null;
|
|
483
|
+
const amount = Number(match[1]);
|
|
484
|
+
const multiplier = { b: 1, kib: 1024, mib: 1024 ** 2, gib: 1024 ** 3, tib: 1024 ** 4 }[match[2].toLowerCase()];
|
|
485
|
+
return Number.isFinite(amount) ? Math.round(amount * multiplier) : null;
|
|
486
|
+
}
|
|
487
|
+
export function parseDockerMemoryUsage(value) {
|
|
488
|
+
return tryParseDockerMemoryUsage(value) ?? 0;
|
|
489
|
+
}
|
|
490
|
+
async function readLock(lockPath) {
|
|
491
|
+
return await fs.readFile(lockPath, "utf8").then((value) => JSON.parse(value)).catch(() => null);
|
|
492
|
+
}
|
|
493
|
+
export function dockerContextOwnerIsActive(owner) {
|
|
494
|
+
if (!owner || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0 || !processExists(owner.pid)) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const liveIdentity = captureHostProcessIdentity(owner.pid);
|
|
498
|
+
if (owner.processIdentity) {
|
|
499
|
+
// A live but unverifiable process must remain protected. Windows and Linux
|
|
500
|
+
// normally provide a birth marker, so a mismatch is positive PID-reuse proof.
|
|
501
|
+
return !liveIdentity || liveIdentity === owner.processIdentity;
|
|
502
|
+
}
|
|
503
|
+
if (process.platform === "win32" && liveIdentity?.startsWith("win32:") && owner.acquiredAt) {
|
|
504
|
+
const ticks = Number(liveIdentity.slice("win32:".length));
|
|
505
|
+
const acquiredAt = Date.parse(owner.acquiredAt);
|
|
506
|
+
const processStartedAt = ticks / 10_000 - 62_135_596_800_000;
|
|
507
|
+
if (Number.isFinite(processStartedAt) && Number.isFinite(acquiredAt) && processStartedAt > acquiredAt) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
function processExists(pid) {
|
|
514
|
+
try {
|
|
515
|
+
process.kill(pid, 0);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SandboxProcessRequest {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
readonly cwd?: string;
|
|
5
|
+
readonly input?: string | Buffer;
|
|
6
|
+
readonly timeoutMs?: number;
|
|
7
|
+
readonly maxOutputBytes?: number;
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
9
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
10
|
+
readonly onStdoutLine?: (line: string) => void | Promise<void>;
|
|
11
|
+
readonly terminateOnOutputLimit?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SandboxProcessResult {
|
|
14
|
+
readonly exitCode: number;
|
|
15
|
+
readonly stdout: string;
|
|
16
|
+
readonly stderr: string;
|
|
17
|
+
readonly truncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function runSandboxProcess(request: SandboxProcessRequest): Promise<SandboxProcessResult>;
|
|
20
|
+
export declare function requireSandboxProcess(request: SandboxProcessRequest): Promise<SandboxProcessResult>;
|