@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 0.1.3-dev.ba7d36726eda

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.
Files changed (118) hide show
  1. package/README.md +78 -0
  2. package/dist/bridge/bridge.base.d.ts +5 -1
  3. package/dist/bridge/bridge.service.d.ts +52 -3
  4. package/dist/bridge/bridge.service.js +776 -19
  5. package/dist/bridge/bridge.types.d.ts +131 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +46 -0
  14. package/dist/cli/cli.service.d.ts +42 -2
  15. package/dist/cli/cli.service.js +350 -23
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +39 -0
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +15 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +50 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +74 -0
  23. package/dist/runtime/attempt-process-supervisor.js +290 -0
  24. package/dist/runtime/host-listener-inspector.d.ts +28 -0
  25. package/dist/runtime/host-listener-inspector.js +185 -0
  26. package/dist/runtime/host-port-broker.d.ts +76 -0
  27. package/dist/runtime/host-port-broker.js +333 -0
  28. package/dist/runtime/host-runtime-contract.d.ts +39 -0
  29. package/dist/runtime/host-runtime-contract.js +295 -0
  30. package/dist/runtime/host-runtime-lease.d.ts +122 -0
  31. package/dist/runtime/host-runtime-lease.js +1241 -0
  32. package/dist/runtime/host-runtime-promotion.d.ts +21 -0
  33. package/dist/runtime/host-runtime-promotion.js +128 -0
  34. package/dist/runtime/node-host-runtime-adapter.d.ts +44 -0
  35. package/dist/runtime/node-host-runtime-adapter.js +205 -0
  36. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  37. package/dist/runtime/playwright-host-runtime-adapter.js +136 -0
  38. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  39. package/dist/runtime/runtime-isolation-lease.js +158 -0
  40. package/dist/sandbox/command-runner.d.ts +4 -0
  41. package/dist/sandbox/command-runner.js +47 -0
  42. package/dist/sandbox/contract.d.ts +5 -0
  43. package/dist/sandbox/contract.js +276 -0
  44. package/dist/sandbox/event-budget.d.ts +11 -0
  45. package/dist/sandbox/event-budget.js +27 -0
  46. package/dist/sandbox/image-policy.d.ts +35 -0
  47. package/dist/sandbox/image-policy.js +247 -0
  48. package/dist/sandbox/index.d.ts +5 -0
  49. package/dist/sandbox/index.js +5 -0
  50. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  51. package/dist/sandbox/lease-watchdog.js +67 -0
  52. package/dist/sandbox/preflight.d.ts +45 -0
  53. package/dist/sandbox/preflight.js +300 -0
  54. package/dist/sandbox/process.d.ts +20 -0
  55. package/dist/sandbox/process.js +209 -0
  56. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  57. package/dist/sandbox/provider-auth/codex.js +212 -0
  58. package/dist/sandbox/repository-lock.d.ts +5 -0
  59. package/dist/sandbox/repository-lock.js +119 -0
  60. package/dist/sandbox/runtime.d.ts +297 -0
  61. package/dist/sandbox/runtime.js +2338 -0
  62. package/dist/sandbox/sandbox.types.d.ts +86 -0
  63. package/dist/sandbox/sandbox.types.js +1 -0
  64. package/dist/sandbox/smoke.d.ts +76 -0
  65. package/dist/sandbox/smoke.js +380 -0
  66. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  67. package/dist/sandbox/worker-supervisor.js +36 -0
  68. package/dist/sandbox/worker.d.ts +1 -0
  69. package/dist/sandbox/worker.js +225 -0
  70. package/dist/sandbox/workspace.service.d.ts +13 -0
  71. package/dist/sandbox/workspace.service.js +67 -0
  72. package/dist/session/phase-completion.artifacts.js +23 -2
  73. package/dist/session/phase-completion.helpers.js +6 -1
  74. package/dist/session/phase-completion.runtime.js +3 -1
  75. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  76. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  77. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  78. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  79. package/dist/session/provider-cli/provider-cli.deterministic.js +20 -9
  80. package/dist/session/provider-cli/provider-cli.process.d.ts +3 -0
  81. package/dist/session/provider-cli/provider-cli.process.js +65 -14
  82. package/dist/session/provider-cli/provider-cli.prompts.d.ts +14 -2
  83. package/dist/session/provider-cli/provider-cli.prompts.js +99 -15
  84. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  85. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  86. package/dist/session/provider-cli/provider-cli.service.d.ts +4 -1
  87. package/dist/session/provider-cli/provider-cli.service.js +167 -28
  88. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  89. package/dist/session/session-redaction.d.ts +3 -0
  90. package/dist/session/session-redaction.js +23 -0
  91. package/dist/session/session.base.d.ts +4 -0
  92. package/dist/session/session.service.d.ts +6 -1
  93. package/dist/session/session.service.js +184 -38
  94. package/dist/session/session.types.d.ts +58 -1
  95. package/dist/session/session.types.js +172 -2
  96. package/dist/shared/path/path.js +3 -1
  97. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  98. package/dist/shared/process/host-process-recovery.js +78 -0
  99. package/dist/shared/process/host-process-supervisor.d.ts +40 -0
  100. package/dist/shared/process/host-process-supervisor.js +195 -0
  101. package/dist/shared/provider-capabilities.d.ts +1 -0
  102. package/dist/shared/provider-capabilities.js +3 -0
  103. package/dist/shared/redaction/redaction.js +6 -1
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/dist/worktree/index.d.ts +1 -0
  107. package/dist/worktree/index.js +1 -0
  108. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  109. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  110. package/dist/worktree/worktree-paths.d.ts +4 -0
  111. package/dist/worktree/worktree-paths.js +30 -0
  112. package/dist/worktree/worktree.base.d.ts +3 -0
  113. package/dist/worktree/worktree.service.d.ts +6 -2
  114. package/dist/worktree/worktree.service.js +60 -43
  115. package/dist/worktree/worktree.types.d.ts +3 -1
  116. package/package.json +6 -3
  117. package/runtime/windows-job-supervisor.ps1 +287 -0
  118. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,300 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { BoundedCommandRunner } from "./command-runner.js";
6
+ const minimumWorkspaceBytes = 10 * 1024 * 1024 * 1024;
7
+ const defaultHostMemoryReserveBytes = 2 * 1024 ** 3;
8
+ export class DockerPreflight {
9
+ commands;
10
+ cacheMs;
11
+ generation = 0;
12
+ cached = null;
13
+ cacheAt = 0;
14
+ options;
15
+ docker;
16
+ ownership = new DockerContextOwnershipLock();
17
+ diskPressureLatched = false;
18
+ constructor(options = {}, commands = new BoundedCommandRunner(), cacheMs = 30_000) {
19
+ this.commands = commands;
20
+ this.cacheMs = cacheMs;
21
+ this.options = typeof options === "string" ? { dockerExecutable: options } : options;
22
+ this.docker = this.options.dockerExecutable ?? process.env.FORGE_DOCKER_EXECUTABLE ?? "docker";
23
+ }
24
+ current() {
25
+ return this.cached;
26
+ }
27
+ async refresh() {
28
+ await fs.mkdir(this.options.stateRoot ?? path.join(os.homedir(), ".forge", "bridge", "sandboxes"), { recursive: true, mode: 0o700 });
29
+ const result = await this.run(true);
30
+ if (canRunDockerMaintenance(result)) {
31
+ await this.ownership.acquire(this.options.dockerContext);
32
+ }
33
+ return result;
34
+ }
35
+ async release() {
36
+ await this.ownership.release();
37
+ }
38
+ async run(force = false) {
39
+ if (!force && this.cached && Date.now() - this.cacheAt < this.cacheMs)
40
+ return this.cached;
41
+ this.generation += 1;
42
+ const result = await this.inspect().catch((error) => unavailable(this.generation, "docker_unreachable", error instanceof Error ? error.message : String(error)));
43
+ this.cached = result;
44
+ this.cacheAt = Date.now();
45
+ return result;
46
+ }
47
+ async inspect() {
48
+ if (!new Set(["linux", "darwin", "win32"]).has(process.platform)) {
49
+ return unavailable(this.generation, "platform_unsupported", `Docker sandboxes are not supported on ${process.platform}.`);
50
+ }
51
+ const dockerArgs = this.options.dockerContext ? ["--context", this.options.dockerContext] : [];
52
+ const infoResult = await this.commands.run(this.docker, [...dockerArgs, "info", "--format", "{{json .}}"], { timeoutMs: 10_000, maxOutputBytes: 512 * 1024 });
53
+ if (infoResult.exitCode !== 0)
54
+ return unavailable(this.generation, "docker_unreachable", singleLine(infoResult.stderr || infoResult.stdout || "Docker daemon is unreachable."));
55
+ const info = parseInfo(infoResult.stdout);
56
+ if (String(info.OSType ?? "").toLowerCase() !== "linux")
57
+ return unavailable(this.generation, "docker_linux_required", "Forge sandbox images require a Linux Docker daemon.");
58
+ const securityOptions = strings(info.SecurityOptions).map((value) => value.toLowerCase());
59
+ const rootless = securityOptions.some((value) => value.includes("rootless"));
60
+ const userNamespaces = rootless || securityOptions.some((value) => value.includes("userns"));
61
+ const cgroupV2 = String(info.CgroupVersion ?? "") === "2";
62
+ const seccomp = securityOptions.some((value) => value.includes("seccomp"));
63
+ const lsm = securityOptions.some((value) => value.includes("apparmor") || value.includes("selinux"));
64
+ const storageDriver = String(info.Driver ?? "").toLowerCase();
65
+ const storageQuota = storageDriver === "overlay2" && Boolean(info.DriverStatus?.some?.((entry) => JSON.stringify(entry).toLowerCase().includes("project quota")));
66
+ const desktopDeveloperMode = process.platform !== "linux" && process.env.FORGE_ALLOW_DOCKER_DESKTOP_SANDBOX === "true";
67
+ const requireRootless = this.options.requireRootless ?? process.env.FORGE_SANDBOX_REQUIRE_ROOTLESS !== "false";
68
+ if (!rootless && !desktopDeveloperMode && requireRootless) {
69
+ return unavailable(this.generation, "docker_rootless_required", "Docker daemon is reachable but is not running rootless.", {
70
+ rootless, userNamespaces, cgroupV2, seccomp, lsm, storageQuota, storagePolicy: false, compose: false, internalNetwork: true, buildkit: false, imagePolicy: false, securityProfile: false, egressProxy: false
71
+ });
72
+ }
73
+ const composeResult = await this.commands.run(this.docker, [...dockerArgs, "compose", "version", "--short"], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => null);
74
+ const compose = composeResult?.exitCode === 0;
75
+ const buildxResult = await this.commands.run(this.docker, [...dockerArgs, "buildx", "version"], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => null);
76
+ const buildxHelp = await this.commands.run(this.docker, [...dockerArgs, "buildx", "build", "--help"], { timeoutMs: 5_000, maxOutputBytes: 256 * 1024 }).catch(() => null);
77
+ const buildxPruneHelp = await this.commands.run(this.docker, [...dockerArgs, "buildx", "prune", "--help"], { timeoutMs: 5_000, maxOutputBytes: 256 * 1024 }).catch(() => null);
78
+ const containerUpdateHelp = await this.commands.run(this.docker, [...dockerArgs, "container", "update", "--help"], { timeoutMs: 5_000, maxOutputBytes: 256 * 1024 }).catch(() => null);
79
+ const buildkit = buildxResult?.exitCode === 0
80
+ && buildxHelp?.exitCode === 0
81
+ && ["--ulimit", "--shm-size"].every(flag => buildxHelp.stdout.includes(flag))
82
+ && buildxPruneHelp?.exitCode === 0
83
+ && ["--max-used-space", "--min-free-space", "--reserved-space"].every(flag => buildxPruneHelp.stdout.includes(flag))
84
+ && containerUpdateHelp?.exitCode === 0
85
+ && ["--pids-limit", "--memory", "--memory-swap"].every(flag => containerUpdateHelp.stdout.includes(flag));
86
+ const imagePolicy = process.env.FORGE_SANDBOX_IMAGE_POLICY_ENFORCED === "true"
87
+ && Boolean(process.env.FORGE_SANDBOX_SBOM_EXECUTABLE?.trim())
88
+ && Boolean(process.env.FORGE_SANDBOX_VULNERABILITY_EXECUTABLE?.trim())
89
+ && Boolean(process.env.FORGE_SANDBOX_PROVENANCE_SIGNING_KEY?.trim())
90
+ && Boolean(process.env.FORGE_SANDBOX_SIGNATURE_EXECUTABLE?.trim())
91
+ && Boolean(process.env.FORGE_SANDBOX_SIGNATURE_PUBLIC_KEY?.trim())
92
+ && process.env.FORGE_SANDBOX_BUILD_EGRESS_ENFORCED === "true";
93
+ const securityProfile = Boolean(process.env.FORGE_SANDBOX_SECCOMP_PROFILE?.trim())
94
+ && Boolean(process.env.FORGE_SANDBOX_LSM_SECURITY_OPT?.trim())
95
+ && seccomp && lsm;
96
+ const egressProxy = process.env.FORGE_SANDBOX_EGRESS_ENFORCED === "true"
97
+ && /@sha256:[0-9a-f]{64}$/iu.test(process.env.FORGE_SANDBOX_EGRESS_PROXY_IMAGE?.trim() ?? "");
98
+ const storagePolicy = storagePolicyAvailable(storageQuota);
99
+ const configuredStateRoot = this.options.stateRoot ?? path.join(os.homedir(), ".forge", "bridge", "sandboxes");
100
+ const dockerRoot = process.platform === "linux" && typeof info.DockerRootDir === "string" && path.isAbsolute(info.DockerRootDir)
101
+ ? info.DockerRootDir
102
+ : configuredStateRoot;
103
+ const disk = await diskSpace(dockerRoot);
104
+ const requiredWorkspaceBytes = this.options.minimumWorkspaceBytes ?? minimumWorkspaceBytes;
105
+ const highWatermark = Math.min(99, Math.max(1, this.options.diskHighWatermarkPercent ?? 90));
106
+ const lowWatermark = Math.min(highWatermark - 1, Math.max(0, this.options.diskLowWatermarkPercent ?? highWatermark - 10));
107
+ const diskLow = evaluateDiskPressure(disk.available, disk.total, requiredWorkspaceBytes, highWatermark, lowWatermark, this.diskPressureLatched);
108
+ this.diskPressureLatched = diskLow;
109
+ const hostAvailableMemoryBytes = os.freemem();
110
+ let totalMemoryBytes = os.totalmem();
111
+ let availableMemoryBytes = hostAvailableMemoryBytes;
112
+ const dockerDesktop = process.platform !== "linux"
113
+ && String(info.OperatingSystem ?? "").toLowerCase().includes("docker desktop");
114
+ const dockerMemoryBytes = Number(info.MemTotal);
115
+ if (dockerDesktop && Number.isFinite(dockerMemoryBytes) && dockerMemoryBytes > 0) {
116
+ totalMemoryBytes = dockerMemoryBytes;
117
+ const stats = await this.commands.run(this.docker, [...dockerArgs, "stats", "--no-stream", "--format", "{{.MemUsage}}"], { timeoutMs: 10_000, maxOutputBytes: 512 * 1024 }).catch(() => null);
118
+ if (stats?.exitCode === 0) {
119
+ const dockerUsedMemoryBytes = stats.stdout
120
+ .split(/\r?\n/gu)
121
+ .map(line => parseDockerMemoryUsage(line))
122
+ .reduce((total, value) => total + value, 0);
123
+ availableMemoryBytes = Math.max(0, dockerMemoryBytes - dockerUsedMemoryBytes);
124
+ }
125
+ }
126
+ const cpuCount = Math.max(1, os.availableParallelism?.() ?? os.cpus().length);
127
+ const availableCpuMillis = Math.max(0, (cpuCount - 1) * 1000);
128
+ const memoryReserveBytes = positiveInteger(this.options.hostMemoryReserveBytes, defaultHostMemoryReserveBytes);
129
+ const workerMemoryLimitBytes = positiveInteger(this.options.defaultWorkerMemoryLimitBytes, 2 * 1024 ** 3);
130
+ const serviceMemoryLimitBytes = nonNegativeInteger(this.options.defaultServiceMemoryLimitBytes, 0);
131
+ const runtimeOverheadBytes = Math.max(128 * 1024 ** 2, nonNegativeInteger(this.options.runtimeOverheadBytes, 128 * 1024 ** 2));
132
+ const sandboxMemoryBytes = workerMemoryLimitBytes + serviceMemoryLimitBytes + runtimeOverheadBytes;
133
+ const minimumAdmissionMemoryBytes = 128 * 1024 ** 2 + Math.max(128 * 1024 ** 2, runtimeOverheadBytes);
134
+ const sandboxCpuMillis = 2_000;
135
+ const cpuSlots = Math.max(0, Math.floor(availableCpuMillis / sandboxCpuMillis));
136
+ const configuredMax = positiveInteger(this.options.maxConcurrentSandboxes, positiveInteger(process.env.FORGE_MAX_CONCURRENT_SANDBOXES, 3));
137
+ const availableForAdmissionBytes = Math.max(0, availableMemoryBytes - memoryReserveBytes);
138
+ const capacityLowCode = !diskLow && availableForAdmissionBytes < minimumAdmissionMemoryBytes
139
+ ? "docker_memory_low"
140
+ : !diskLow && cpuSlots <= 0
141
+ ? "docker_cpu_low"
142
+ : null;
143
+ const maxConcurrentSandboxes = diskLow || capacityLowCode ? 0 : Math.min(3, configuredMax, cpuSlots);
144
+ const unavailableCode = diskLow ? "docker_disk_low" : capacityLowCode;
145
+ const unavailableReason = diskLow
146
+ ? `Docker workspace has only ${disk.available} bytes available.`
147
+ : unavailableCode === "docker_memory_low"
148
+ ? `Docker reports ${availableMemoryBytes} bytes available and Forge reserves ${memoryReserveBytes} host bytes, leaving ${availableForAdmissionBytes} bytes for admission; the smallest supported sandbox requires ${minimumAdmissionMemoryBytes} bytes. Exact attempt memory is calculated dynamically from its worker profile, sidecars, and runtime helpers.`
149
+ : unavailableCode === "docker_cpu_low"
150
+ ? `Docker host has ${availableCpuMillis} available CPU millis; at least ${sandboxCpuMillis} are needed for one sandbox.`
151
+ : null;
152
+ const features = { rootless, userNamespaces, cgroupV2, seccomp, lsm, storageQuota, storagePolicy, compose, internalNetwork: true, buildkit, imagePolicy, securityProfile, egressProxy };
153
+ const capacityAvailable = unavailableCode === null;
154
+ return {
155
+ available: capacityAvailable,
156
+ healthy: capacityAvailable,
157
+ generation: this.generation,
158
+ runtimeVersion: `docker/${String(info.ServerVersion ?? "unknown")}${rootless ? "-rootless" : "-desktop"}`,
159
+ maxConcurrentSandboxes,
160
+ totalMemoryBytes,
161
+ availableMemoryBytes,
162
+ hostReserveBytes: memoryReserveBytes,
163
+ admissionMemoryBytes: sandboxMemoryBytes,
164
+ availableCpuMillis,
165
+ availableWorkspaceBytes: disk.available,
166
+ diskLow,
167
+ unavailableCode,
168
+ unavailableReason,
169
+ features,
170
+ };
171
+ }
172
+ }
173
+ const dockerMaintenanceUnavailableCodes = new Set(["docker_disk_low", "docker_memory_low", "docker_cpu_low"]);
174
+ export function canRunDockerMaintenance(preflight) {
175
+ return preflight.healthy || dockerMaintenanceUnavailableCodes.has(preflight.unavailableCode ?? "");
176
+ }
177
+ export function evaluateDiskPressure(availableBytes, totalBytes, minimumAvailableBytes, highWatermarkPercent, lowWatermarkPercent, pressureLatched) {
178
+ if (availableBytes < minimumAvailableBytes)
179
+ return true;
180
+ if (!(totalBytes > 0))
181
+ return true;
182
+ const usedPercent = (1 - availableBytes / totalBytes) * 100;
183
+ if (usedPercent >= highWatermarkPercent)
184
+ return true;
185
+ return pressureLatched && usedPercent > lowWatermarkPercent;
186
+ }
187
+ export function storagePolicyAvailable(storageQuota, environment = process.env) {
188
+ return storageQuota || environment.FORGE_SANDBOX_DEDICATED_DISK === "true";
189
+ }
190
+ export class DockerContextOwnershipLock {
191
+ lockPath = null;
192
+ async acquire(contextName) {
193
+ if (this.lockPath)
194
+ return;
195
+ contextName = contextName?.trim() || process.env.DOCKER_CONTEXT?.trim() || process.env.DOCKER_HOST?.trim() || "default";
196
+ const key = createHash("sha256").update(contextName).digest("hex").slice(0, 24);
197
+ const lockPath = path.join(os.tmpdir(), `forge-docker-context-${key}.lock`);
198
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
199
+ try {
200
+ const handle = await fs.open(lockPath, "wx", 0o600);
201
+ await handle.writeFile(JSON.stringify({ pid: process.pid, contextName, acquiredAt: new Date().toISOString() }));
202
+ await handle.sync();
203
+ await handle.close();
204
+ }
205
+ catch (error) {
206
+ const owner = await readLock(lockPath);
207
+ if (owner?.pid && !processExists(owner.pid)) {
208
+ await fs.rm(lockPath, { force: true });
209
+ return await this.acquire(contextName);
210
+ }
211
+ throw new Error(`docker_context_owned: Docker context '${contextName}' is already owned by Forge runner pid ${owner?.pid ?? "unknown"}.`, { cause: error });
212
+ }
213
+ this.lockPath = lockPath;
214
+ }
215
+ async release() {
216
+ if (!this.lockPath)
217
+ return;
218
+ await fs.rm(this.lockPath, { force: true });
219
+ this.lockPath = null;
220
+ }
221
+ }
222
+ function parseInfo(stdout) {
223
+ try {
224
+ const value = JSON.parse(stdout.trim());
225
+ if (!value || typeof value !== "object" || Array.isArray(value))
226
+ throw new Error("not an object");
227
+ return value;
228
+ }
229
+ catch (error) {
230
+ throw new Error("Docker returned an invalid info document.", { cause: error });
231
+ }
232
+ }
233
+ function unavailable(generation, code, reason, features = {
234
+ 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
235
+ }) {
236
+ return {
237
+ available: false,
238
+ healthy: false,
239
+ generation,
240
+ runtimeVersion: null,
241
+ maxConcurrentSandboxes: 0,
242
+ totalMemoryBytes: os.totalmem(),
243
+ availableMemoryBytes: os.freemem(),
244
+ hostReserveBytes: defaultHostMemoryReserveBytes,
245
+ admissionMemoryBytes: 2 * 1024 ** 3 + 128 * 1024 ** 2,
246
+ availableCpuMillis: Math.max(0, (os.availableParallelism?.() ?? os.cpus().length) * 1000),
247
+ availableWorkspaceBytes: 0,
248
+ diskLow: code === "docker_disk_low",
249
+ unavailableCode: code,
250
+ unavailableReason: singleLine(reason),
251
+ features,
252
+ };
253
+ }
254
+ async function diskSpace(inputPath) {
255
+ const stats = await fs.statfs(inputPath, { bigint: true }).catch(() => null);
256
+ if (!stats)
257
+ return { available: 0, total: 0 };
258
+ return {
259
+ available: safeNumber(stats.bavail * stats.bsize),
260
+ total: safeNumber(stats.blocks * stats.bsize),
261
+ };
262
+ }
263
+ function safeNumber(value) {
264
+ return Number(value > BigInt(Number.MAX_SAFE_INTEGER) ? BigInt(Number.MAX_SAFE_INTEGER) : value);
265
+ }
266
+ function strings(value) {
267
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
268
+ }
269
+ function positiveInteger(value, fallback) {
270
+ const parsed = Number(value);
271
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
272
+ }
273
+ function singleLine(value) {
274
+ return value.replace(/[\r\n\t]+/gu, " ").trim().slice(0, 1000);
275
+ }
276
+ function nonNegativeInteger(value, fallback) {
277
+ const parsed = Number(value);
278
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
279
+ }
280
+ export function parseDockerMemoryUsage(value) {
281
+ const usage = value.split("/", 1)[0]?.trim().replaceAll(" ", "") ?? "";
282
+ const match = /^(\d+(?:\.\d+)?)(b|kib|mib|gib|tib)$/iu.exec(usage);
283
+ if (!match)
284
+ return 0;
285
+ const amount = Number(match[1]);
286
+ const multiplier = { b: 1, kib: 1024, mib: 1024 ** 2, gib: 1024 ** 3, tib: 1024 ** 4 }[match[2].toLowerCase()];
287
+ return Number.isFinite(amount) ? Math.round(amount * multiplier) : 0;
288
+ }
289
+ async function readLock(lockPath) {
290
+ return await fs.readFile(lockPath, "utf8").then((value) => JSON.parse(value)).catch(() => null);
291
+ }
292
+ function processExists(pid) {
293
+ try {
294
+ process.kill(pid, 0);
295
+ return true;
296
+ }
297
+ catch {
298
+ return false;
299
+ }
300
+ }
@@ -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>;
@@ -0,0 +1,209 @@
1
+ import { spawn } from "node:child_process";
2
+ import { StringDecoder } from "node:string_decoder";
3
+ const terminationSigkillDelayMs = 5_000;
4
+ const forcedCloseSettleDelayMs = 5_000;
5
+ const expectedStdinCloseErrorCodes = new Set(["EPIPE", "ECONNRESET", "ERR_STREAM_DESTROYED"]);
6
+ function isExpectedStdinCloseError(error) {
7
+ return "code" in error
8
+ && typeof error.code === "string"
9
+ && expectedStdinCloseErrorCodes.has(error.code);
10
+ }
11
+ export async function runSandboxProcess(request) {
12
+ const limit = Math.max(1024, request.maxOutputBytes ?? 1024 * 1024);
13
+ return await new Promise((resolve, reject) => {
14
+ const child = spawn(request.command, [...request.args], {
15
+ cwd: request.cwd,
16
+ env: request.env,
17
+ shell: false,
18
+ windowsHide: true,
19
+ stdio: [request.input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
20
+ });
21
+ let stdout = Buffer.alloc(0);
22
+ let stderr = Buffer.alloc(0);
23
+ let truncated = false;
24
+ let settled = false;
25
+ let terminating = false;
26
+ let discardStdoutLineCallbacks = false;
27
+ let stdoutText = "";
28
+ const stdoutDecoder = new StringDecoder("utf8");
29
+ let stdoutLineChain = Promise.resolve();
30
+ let stdoutLineError = null;
31
+ let timeout = null;
32
+ let sigkillTimer = null;
33
+ let forcedSettleTimer = null;
34
+ const append = (current, chunk) => {
35
+ if (current.length >= limit) {
36
+ truncated = true;
37
+ return current;
38
+ }
39
+ const remaining = limit - current.length;
40
+ if (chunk.length > remaining) {
41
+ truncated = true;
42
+ if (request.terminateOnOutputLimit)
43
+ terminate(`sandbox_log_limit: process output exceeded ${limit} bytes`);
44
+ }
45
+ return Buffer.concat([current, chunk.subarray(0, remaining)]);
46
+ };
47
+ const appendStderrLine = (line) => {
48
+ stderr = append(stderr, Buffer.from(`\n${line}\n`));
49
+ };
50
+ const cleanup = () => {
51
+ if (timeout)
52
+ clearTimeout(timeout);
53
+ if (sigkillTimer)
54
+ clearTimeout(sigkillTimer);
55
+ if (forcedSettleTimer)
56
+ clearTimeout(forcedSettleTimer);
57
+ child.stdout.removeListener("data", onStdoutData);
58
+ child.stderr.removeListener("data", onStderrData);
59
+ child.removeListener("error", onProcessError);
60
+ child.removeListener("close", onProcessClose);
61
+ request.signal?.removeEventListener("abort", abort);
62
+ };
63
+ const settle = async (code, force) => {
64
+ if (settled)
65
+ return;
66
+ settled = true;
67
+ discardStdoutLineCallbacks = force;
68
+ cleanup();
69
+ if (stdoutLineError) {
70
+ reject(stdoutLineError);
71
+ return;
72
+ }
73
+ if (!force) {
74
+ if (request.onStdoutLine)
75
+ stdoutText += stdoutDecoder.end();
76
+ if (request.onStdoutLine && stdoutText) {
77
+ stdoutLineChain = stdoutLineChain.then(() => request.onStdoutLine(stdoutText));
78
+ }
79
+ await stdoutLineChain.catch(error => { stdoutLineError ??= error; });
80
+ if (stdoutLineError) {
81
+ reject(stdoutLineError);
82
+ return;
83
+ }
84
+ }
85
+ resolve({
86
+ exitCode: code ?? 1,
87
+ stdout: stdout.toString("utf8"),
88
+ stderr: stderr.toString("utf8"),
89
+ truncated
90
+ });
91
+ };
92
+ const forceSettle = (reason) => {
93
+ if (settled)
94
+ return;
95
+ appendStderrLine(reason);
96
+ void settle(1, true);
97
+ };
98
+ const killChild = (signal) => {
99
+ if (child.exitCode !== null || child.signalCode !== null)
100
+ return;
101
+ try {
102
+ child.kill(signal);
103
+ }
104
+ catch {
105
+ // The process may have exited between timeout/abort and close. Still let the
106
+ // supervisor timers settle if close never arrives.
107
+ }
108
+ };
109
+ const terminate = (reason) => {
110
+ if (settled || terminating)
111
+ return;
112
+ terminating = true;
113
+ appendStderrLine(reason);
114
+ killChild("SIGTERM");
115
+ sigkillTimer = setTimeout(() => {
116
+ killChild("SIGKILL");
117
+ forcedSettleTimer = setTimeout(() => forceSettle("process did not close after SIGKILL; forcing supervisor settlement"), forcedCloseSettleDelayMs);
118
+ forcedSettleTimer.unref?.();
119
+ }, terminationSigkillDelayMs);
120
+ sigkillTimer.unref?.();
121
+ };
122
+ const abort = () => terminate(String(request.signal?.reason ?? "process aborted"));
123
+ const onStdoutData = (chunk) => {
124
+ if (settled)
125
+ return;
126
+ stdout = append(stdout, chunk);
127
+ if (!request.onStdoutLine || settled)
128
+ return;
129
+ stdoutText += stdoutDecoder.write(chunk);
130
+ if (Buffer.byteLength(stdoutText, "utf8") > 1024 * 1024 && !stdoutText.includes("\n")) {
131
+ stdoutLineError = new Error("sandbox process emitted an oversized stdout line");
132
+ terminate("oversized stdout line");
133
+ return;
134
+ }
135
+ const lines = stdoutText.split(/\r?\n/u);
136
+ stdoutText = lines.pop() ?? "";
137
+ if (lines.length === 0)
138
+ return;
139
+ child.stdout.pause();
140
+ for (const line of lines) {
141
+ stdoutLineChain = stdoutLineChain.then(async () => {
142
+ if (discardStdoutLineCallbacks)
143
+ return;
144
+ if (Buffer.byteLength(line, "utf8") > 1024 * 1024)
145
+ throw new Error("sandbox process emitted an oversized stdout line");
146
+ await request.onStdoutLine(line);
147
+ });
148
+ }
149
+ stdoutLineChain = stdoutLineChain
150
+ .catch(error => {
151
+ stdoutLineError = error;
152
+ terminate("stdout event handler failed");
153
+ })
154
+ .finally(() => {
155
+ if (!settled)
156
+ child.stdout.resume();
157
+ });
158
+ };
159
+ const onStderrData = (chunk) => {
160
+ if (!settled)
161
+ stderr = append(stderr, chunk);
162
+ };
163
+ const onProcessError = (error) => {
164
+ if (!settled) {
165
+ settled = true;
166
+ cleanup();
167
+ reject(error);
168
+ }
169
+ };
170
+ const onStdinError = (error) => {
171
+ if (isExpectedStdinCloseError(error))
172
+ return;
173
+ onProcessError(error);
174
+ };
175
+ const onProcessClose = (code) => {
176
+ void settle(code, false);
177
+ };
178
+ child.stdout.on("data", onStdoutData);
179
+ child.stderr.on("data", onStderrData);
180
+ child.on("error", onProcessError);
181
+ child.on("close", onProcessClose);
182
+ // Keep this listener for the lifetime of stdin. A pipe error can be emitted
183
+ // after the child close event has settled the supervisor promise.
184
+ child.stdin?.on("error", onStdinError);
185
+ timeout = request.timeoutMs
186
+ ? setTimeout(() => terminate(`process timeout after ${request.timeoutMs}ms`), request.timeoutMs)
187
+ : null;
188
+ timeout?.unref?.();
189
+ request.signal?.addEventListener("abort", abort, { once: true });
190
+ if (request.signal?.aborted) {
191
+ abort();
192
+ }
193
+ if (request.input !== undefined) {
194
+ try {
195
+ child.stdin.end(request.input);
196
+ }
197
+ catch (error) {
198
+ onStdinError(error instanceof Error ? error : new Error(String(error)));
199
+ }
200
+ }
201
+ });
202
+ }
203
+ export async function requireSandboxProcess(request) {
204
+ const result = await runSandboxProcess(request);
205
+ if (result.exitCode !== 0) {
206
+ throw new Error(`${request.command} ${request.args[0] ?? ""} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`}`);
207
+ }
208
+ return result;
209
+ }
@@ -0,0 +1,26 @@
1
+ export interface SandboxAuthDoctorResult {
2
+ readonly available: boolean;
3
+ readonly code: "ok" | "sandbox_auth_unavailable";
4
+ readonly message: string;
5
+ readonly authPath?: string;
6
+ }
7
+ export interface SandboxProviderCredentialFile {
8
+ readonly name: string;
9
+ readonly contentBase64: string;
10
+ }
11
+ export interface SandboxPreparedProviderAuth {
12
+ readonly files: readonly SandboxProviderCredentialFile[];
13
+ readonly containerMountPath: string;
14
+ readonly writable: boolean;
15
+ }
16
+ export interface SandboxProviderAuthAdapter {
17
+ readonly provider: "codex";
18
+ readonly authKind: "bridge_cli";
19
+ readonly containerMountPath: string;
20
+ readonly writable: boolean;
21
+ doctor(environment?: NodeJS.ProcessEnv): SandboxAuthDoctorResult;
22
+ prepareAttemptAuth(environment?: NodeJS.ProcessEnv): Promise<SandboxPreparedProviderAuth>;
23
+ }
24
+ export declare const codexSandboxProviderAuthAdapter: SandboxProviderAuthAdapter;
25
+ export declare function sandboxProviderAuthAdapterFor(provider: string, authKind: string): SandboxProviderAuthAdapter | null;
26
+ export declare function sandboxProviderAuthUnavailableMessage(provider: string, authKind: string): string | null;