@b4run/sandbox 0.8.28

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/docker/docker-cli.d.ts +23 -0
  4. package/dist/docker/docker-cli.d.ts.map +1 -0
  5. package/dist/docker/docker-cli.js +29 -0
  6. package/dist/docker/docker-exec.d.ts +12 -0
  7. package/dist/docker/docker-exec.d.ts.map +1 -0
  8. package/dist/docker/docker-exec.js +74 -0
  9. package/dist/docker/docker-filesystem.d.ts +11 -0
  10. package/dist/docker/docker-filesystem.d.ts.map +1 -0
  11. package/dist/docker/docker-filesystem.js +87 -0
  12. package/dist/docker/docker-pid-exhaustion.d.ts +8 -0
  13. package/dist/docker/docker-pid-exhaustion.d.ts.map +1 -0
  14. package/dist/docker/docker-pid-exhaustion.js +39 -0
  15. package/dist/docker/docker-sandbox.d.ts +20 -0
  16. package/dist/docker/docker-sandbox.d.ts.map +1 -0
  17. package/dist/docker/docker-sandbox.js +267 -0
  18. package/dist/docker/thread-lifecycle.d.ts +15 -0
  19. package/dist/docker/thread-lifecycle.d.ts.map +1 -0
  20. package/dist/docker/thread-lifecycle.js +85 -0
  21. package/dist/errors.d.ts +12 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +10 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +3 -0
  27. package/dist/kubernetes/default-kube-client.d.ts +6 -0
  28. package/dist/kubernetes/default-kube-client.d.ts.map +1 -0
  29. package/dist/kubernetes/default-kube-client.js +331 -0
  30. package/dist/kubernetes/kube-client.d.ts +138 -0
  31. package/dist/kubernetes/kube-client.d.ts.map +1 -0
  32. package/dist/kubernetes/kube-client.js +26 -0
  33. package/dist/kubernetes/kube-exec.d.ts +7 -0
  34. package/dist/kubernetes/kube-exec.d.ts.map +1 -0
  35. package/dist/kubernetes/kube-exec.js +38 -0
  36. package/dist/kubernetes/kube-filesystem.d.ts +5 -0
  37. package/dist/kubernetes/kube-filesystem.d.ts.map +1 -0
  38. package/dist/kubernetes/kube-filesystem.js +57 -0
  39. package/dist/kubernetes/kube-sandbox.d.ts +25 -0
  40. package/dist/kubernetes/kube-sandbox.d.ts.map +1 -0
  41. package/dist/kubernetes/kube-sandbox.js +278 -0
  42. package/dist/testing/conformance.d.ts +13 -0
  43. package/dist/testing/conformance.d.ts.map +1 -0
  44. package/dist/testing/conformance.js +75 -0
  45. package/dist/testing/fake-sandbox.d.ts +16 -0
  46. package/dist/testing/fake-sandbox.d.ts.map +1 -0
  47. package/dist/testing/fake-sandbox.js +61 -0
  48. package/dist/testing/index.d.ts +3 -0
  49. package/dist/testing/index.d.ts.map +1 -0
  50. package/dist/testing/index.js +2 -0
  51. package/dist/tsconfig.tsbuildinfo +1 -0
  52. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Brian Love
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @b4run/sandbox
2
+
3
+ Docker and Kubernetes sandbox providers for isolated B4.run workspace execution.
4
+
5
+ **Use this when:** You need to isolate workspace filesystem and shell execution from the B4.run host process.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @b4run/sandbox
11
+ ```
12
+
13
+ ## Example
14
+
15
+ ```ts
16
+ import { dockerSandbox } from "@b4run/sandbox"
17
+ import { fakeSandbox } from "@b4run/sandbox/testing"
18
+
19
+ const provider = dockerSandbox({ image: "node:24-slim" })
20
+ const testProvider = fakeSandbox()
21
+ ```
22
+
23
+ ## Runtime and stability
24
+
25
+ - `@b4run/sandbox` is a node-only, supported application surface.
26
+ - `@b4run/sandbox/testing` is a node-only, supported testing surface.
27
+
28
+ A sandbox narrows where workspace operations run; applications still own image hardening, credentials, network policy, and resource limits.
29
+
30
+ ## Related
31
+
32
+ - [Sandbox API reference](https://b4.run/docs/api/sandbox) — Docker, Kubernetes, and testing provider contracts.
33
+ - [Execution Sandbox guide](https://b4.run/docs/sandbox) — application setup and security boundaries.
34
+ - [`@b4run/workspace`](https://www.npmjs.com/package/@b4run/workspace) — the filesystem and shell contracts sandbox providers implement.
35
+
36
+ ## Maturity and support
37
+
38
+ B4.run is pre-1.0, and its public surface can change. All publishable B4.run packages release together as a fixed group; review the [`@b4run/sandbox` changelog](https://github.com/cacheplane/b4run/blob/main/packages/sandbox/CHANGELOG.md) and [upgrading guide](https://b4.run/docs/upgrading) before upgrading. For support, use [GitHub Discussions](https://github.com/cacheplane/b4run/discussions); report defects in [GitHub Issues](https://github.com/cacheplane/b4run/issues).
39
+
40
+ ## License
41
+
42
+ MIT. See the [repository license](https://github.com/cacheplane/b4run/blob/main/LICENSE).
@@ -0,0 +1,23 @@
1
+ export interface SpawnResult {
2
+ readonly stdout: string;
3
+ readonly stderr: string;
4
+ readonly exitCode: number;
5
+ }
6
+ export type Spawner = (args: readonly string[], opts?: {
7
+ readonly stdin?: string;
8
+ readonly signal?: AbortSignal;
9
+ }) => Promise<SpawnResult>;
10
+ export interface Docker {
11
+ run(args: readonly string[], opts?: {
12
+ readonly signal?: AbortSignal;
13
+ }): Promise<SpawnResult>;
14
+ exec(container: string, command: readonly string[], opts?: {
15
+ readonly stdin?: string;
16
+ readonly signal?: AbortSignal;
17
+ }): Promise<SpawnResult>;
18
+ }
19
+ /** Thin docker-CLI wrapper. `spawn` is injectable so unit tests need no daemon. */
20
+ export declare function createDocker(deps?: {
21
+ readonly spawn?: Spawner;
22
+ }): Docker;
23
+ //# sourceMappingURL=docker-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-cli.d.ts","sourceRoot":"","sources":["../../src/docker/docker-cli.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,MAAM,OAAO,GAAG,CACpB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAC9D,OAAO,CAAC,WAAW,CAAC,CAAA;AAsBzB,MAAM,WAAW,MAAM;IACrB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC5F,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAChE,OAAO,CAAC,WAAW,CAAC,CAAA;CACxB;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,IAAI,GAAE;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAM5E"}
@@ -0,0 +1,29 @@
1
+ import { spawn } from "node:child_process";
2
+ const defaultSpawn = (args, opts) => new Promise((resolve, reject) => {
3
+ const child = spawn("docker", [...args], {
4
+ stdio: ["pipe", "pipe", "pipe"],
5
+ ...(opts?.signal ? { signal: opts.signal } : {}),
6
+ });
7
+ let stdout = "";
8
+ let stderr = "";
9
+ child.stdout.on("data", (c) => {
10
+ stdout += String(c);
11
+ });
12
+ child.stderr.on("data", (c) => {
13
+ stderr += String(c);
14
+ });
15
+ child.on("error", reject);
16
+ child.on("close", (code) => resolve({ stdout, stderr, exitCode: code ?? 1 }));
17
+ if (opts?.stdin !== undefined)
18
+ child.stdin.end(opts.stdin);
19
+ else
20
+ child.stdin.end();
21
+ });
22
+ /** Thin docker-CLI wrapper. `spawn` is injectable so unit tests need no daemon. */
23
+ export function createDocker(deps = {}) {
24
+ const sp = deps.spawn ?? defaultSpawn;
25
+ return {
26
+ run: (args, opts) => sp(args, opts),
27
+ exec: (container, command, opts) => sp(["exec", "-i", container, ...command], opts),
28
+ };
29
+ }
@@ -0,0 +1,12 @@
1
+ import type { ExecBackend } from "@b4run/workspace";
2
+ import type { Docker } from "./docker-cli.js";
3
+ import { type DockerPidExhaustionRecovery } from "./docker-pid-exhaustion.js";
4
+ interface DockerExecOptions {
5
+ readonly timeoutMs?: number;
6
+ readonly runWithExecLease?: <T>(operation: () => Promise<T>) => Promise<T>;
7
+ readonly pidExhaustionRecovery?: DockerPidExhaustionRecovery;
8
+ }
9
+ /** ExecBackend that runs commands inside a docker container via `docker exec sh -c`. */
10
+ export declare function dockerExec(docker: Docker, container: string, opts?: DockerExecOptions): ExecBackend;
11
+ export {};
12
+ //# sourceMappingURL=docker-exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-exec.d.ts","sourceRoot":"","sources":["../../src/docker/docker-exec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EACL,KAAK,2BAA2B,EAEjC,MAAM,4BAA4B,CAAA;AAEnC,UAAU,iBAAiB;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAC1E,QAAQ,CAAC,qBAAqB,CAAC,EAAE,2BAA2B,CAAA;CAC7D;AAMD,wFAAwF;AACxF,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,iBAAsB,GAC3B,WAAW,CA0Eb"}
@@ -0,0 +1,74 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isDockerExecAdmissionPidExhaustion, } from "./docker-pid-exhaustion.js";
3
+ function shellQuote(s) {
4
+ return `'${s.replaceAll("'", `'\\''`)}'`;
5
+ }
6
+ /** ExecBackend that runs commands inside a docker container via `docker exec sh -c`. */
7
+ export function dockerExec(docker, container, opts = {}) {
8
+ return {
9
+ async runCommand(args, ctx) {
10
+ const envPrefix = args.env
11
+ ? Object.entries(args.env)
12
+ .map(([k, v]) => {
13
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) {
14
+ throw new Error(`Invalid environment variable name ${JSON.stringify(k)}: keys must match /^[A-Za-z_][A-Za-z0-9_]*$/`);
15
+ }
16
+ return `${k}=${shellQuote(v)} `;
17
+ })
18
+ .join("")
19
+ : "";
20
+ const cwd = args.cwd ?? ctx.workspaceRoot;
21
+ const cdPrefix = cwd ? `cd ${shellQuote(cwd)} && ` : "";
22
+ const full = `${envPrefix}${cdPrefix}${args.command}`;
23
+ const startedMarker = `__B4_EXEC_STARTED_${randomUUID()}__`;
24
+ const startedPrefix = `${startedMarker}\n`;
25
+ const shArgs = ["sh", "-c", `printf '%s\\n' ${shellQuote(startedMarker)}; ${full}`];
26
+ // `timeout` has second granularity, so round up to the enforced ceiling and
27
+ // report THAT (not the raw ms) — otherwise `timeoutMs: 500` reports "500ms"
28
+ // while the process actually gets a full 1s.
29
+ const timeoutSecs = opts.timeoutMs !== undefined ? Math.ceil(opts.timeoutMs / 1000) : undefined;
30
+ const argv = timeoutSecs !== undefined ? ["timeout", `${timeoutSecs}s`, ...shArgs] : shArgs;
31
+ const attempt = async () => {
32
+ const result = await docker.exec(container, argv, { signal: ctx.signal });
33
+ const started = result.stdout.startsWith(startedPrefix);
34
+ return {
35
+ result: started
36
+ ? { ...result, stdout: result.stdout.slice(startedPrefix.length) }
37
+ : result,
38
+ started,
39
+ };
40
+ };
41
+ const executeWithLease = (operation) => opts.runWithExecLease !== undefined ? opts.runWithExecLease(operation) : operation();
42
+ let recoveryToken;
43
+ const firstAttempt = await executeWithLease(async () => {
44
+ // Token capture and Docker admission share one lease. A queued command
45
+ // therefore records the generation of the keeper it actually enters,
46
+ // not the generation that existed before an earlier recovery.
47
+ recoveryToken = opts.pidExhaustionRecovery?.captureToken();
48
+ return attempt();
49
+ });
50
+ let r = firstAttempt.result;
51
+ const isConfiguredTimeout = timeoutSecs !== undefined && r.exitCode === 124;
52
+ if (opts.pidExhaustionRecovery !== undefined &&
53
+ recoveryToken !== undefined &&
54
+ !isConfiguredTimeout &&
55
+ !firstAttempt.started &&
56
+ isDockerExecAdmissionPidExhaustion(r)) {
57
+ const recovered = await opts.pidExhaustionRecovery.recoverAndRetry(recoveryToken,
58
+ // Recovery owns the exclusive lifecycle lease, so its retry must run
59
+ // directly rather than attempting to acquire a nested shared lease.
60
+ async () => (await attempt()).result);
61
+ if (recovered !== undefined)
62
+ r = recovered;
63
+ }
64
+ if (timeoutSecs !== undefined && r.exitCode === 124) {
65
+ return {
66
+ stdout: r.stdout,
67
+ stderr: `${r.stderr}${r.stderr ? "\n" : ""}Command timed out after ${timeoutSecs}s (resources.timeoutMs: ${opts.timeoutMs}ms).`,
68
+ exitCode: 124,
69
+ };
70
+ }
71
+ return { stdout: r.stdout, stderr: r.stderr, exitCode: r.exitCode };
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,11 @@
1
+ import type { FilesystemBackend } from "@b4run/workspace";
2
+ import type { Docker } from "./docker-cli.js";
3
+ import { type DockerPidExhaustionRecovery } from "./docker-pid-exhaustion.js";
4
+ interface DockerFilesystemOptions {
5
+ readonly runWithExecLease?: <T>(operation: () => Promise<T>) => Promise<T>;
6
+ readonly pidExhaustionRecovery?: DockerPidExhaustionRecovery;
7
+ }
8
+ /** FilesystemBackend whose ops run inside a docker container via `docker exec`. */
9
+ export declare function dockerFilesystem(docker: Docker, container: string, opts?: DockerFilesystemOptions): FilesystemBackend;
10
+ export {};
11
+ //# sourceMappingURL=docker-filesystem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-filesystem.d.ts","sourceRoot":"","sources":["../../src/docker/docker-filesystem.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAkB,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EACL,KAAK,2BAA2B,EAGjC,MAAM,4BAA4B,CAAA;AAEnC,UAAU,uBAAuB;IAC/B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAC1E,QAAQ,CAAC,qBAAqB,CAAC,EAAE,2BAA2B,CAAA;CAC7D;AAMD,mFAAmF;AACnF,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,uBAA4B,GACjC,iBAAiB,CAqFnB"}
@@ -0,0 +1,87 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isDockerExecAdmissionPidExhaustion, isStartedShellPidExhaustion, } from "./docker-pid-exhaustion.js";
3
+ function q(s) {
4
+ return `'${s.replaceAll("'", `'\\''`)}'`;
5
+ }
6
+ /** FilesystemBackend whose ops run inside a docker container via `docker exec`. */
7
+ export function dockerFilesystem(docker, container, opts = {}) {
8
+ const run = async (cmd, ctx, stdin) => {
9
+ const startedMarker = `__B4_FILESYSTEM_STARTED_${randomUUID()}__`;
10
+ const startedPrefix = `${startedMarker}\n`;
11
+ const attempt = async () => {
12
+ const result = await docker.exec(container, ["sh", "-c", `printf '%s\\n' ${q(startedMarker)}; ${cmd}`], {
13
+ ...(stdin !== undefined ? { stdin } : {}),
14
+ signal: ctx.signal,
15
+ });
16
+ const started = result.stdout.startsWith(startedPrefix);
17
+ return {
18
+ result: started ? { ...result, stdout: result.stdout.slice(startedPrefix.length) } : result,
19
+ started,
20
+ };
21
+ };
22
+ const executeWithLease = (operation) => opts.runWithExecLease !== undefined ? opts.runWithExecLease(operation) : operation();
23
+ let recoveryToken;
24
+ const firstAttempt = await executeWithLease(async () => {
25
+ recoveryToken = opts.pidExhaustionRecovery?.captureToken();
26
+ return attempt();
27
+ });
28
+ let result = firstAttempt.result;
29
+ if (opts.pidExhaustionRecovery !== undefined &&
30
+ recoveryToken !== undefined &&
31
+ (firstAttempt.started
32
+ ? isStartedShellPidExhaustion(result)
33
+ : isDockerExecAdmissionPidExhaustion(result))) {
34
+ const recovered = await opts.pidExhaustionRecovery.recoverAndRetry(recoveryToken, async () => (await attempt()).result);
35
+ if (recovered !== undefined)
36
+ result = recovered;
37
+ }
38
+ return result;
39
+ };
40
+ return {
41
+ async readFile(path, ctx, opts) {
42
+ const r = await run(`cat ${q(path)}`, ctx);
43
+ if (r.exitCode !== 0)
44
+ throw new Error(`readFile failed: ${r.stderr.trim()}`);
45
+ const max = opts?.maxBytes;
46
+ if (max !== undefined && Number.isFinite(max) && Buffer.byteLength(r.stdout) > max) {
47
+ throw new Error(`readFile ${path}: content exceeds maxBytes (${max}).`);
48
+ }
49
+ return r.stdout;
50
+ },
51
+ async writeFile(path, content, ctx) {
52
+ const r = await run(`mkdir -p "$(dirname ${q(path)})" && cat > ${q(path)}`, ctx, content);
53
+ if (r.exitCode !== 0)
54
+ throw new Error(`writeFile failed: ${r.stderr.trim()}`);
55
+ return { bytesWritten: Buffer.byteLength(content) };
56
+ },
57
+ async listDir(path, ctx) {
58
+ const r = await run(`ls -1 ${q(path)}`, ctx);
59
+ if (r.exitCode !== 0)
60
+ throw new Error(`listDir failed: ${r.stderr.trim()}`);
61
+ return r.stdout
62
+ .split("\n")
63
+ .map((l) => l.trim())
64
+ .filter(Boolean);
65
+ },
66
+ async realPath(path, ctx) {
67
+ const r = await run(`realpath -m ${q(path)}`, ctx);
68
+ return r.exitCode === 0 ? r.stdout.trim() : path;
69
+ },
70
+ async statFile(path, ctx) {
71
+ const r = await run(`stat -c '%s %Y' ${q(path)}`, ctx);
72
+ if (r.exitCode !== 0)
73
+ throw new Error(`statFile failed: ${r.stderr.trim()}`);
74
+ const [size, mtime] = r.stdout.trim().split(" ");
75
+ return { size: Number(size), mtimeMs: Number(mtime) * 1000 };
76
+ },
77
+ async removeFile(path, ctx) {
78
+ await run(`rm -f ${q(path)}`, ctx);
79
+ },
80
+ async touchFile(path, ctx) {
81
+ await run(`touch ${q(path)}`, ctx);
82
+ },
83
+ async mkdir(path, ctx) {
84
+ await run(`mkdir -p ${q(path)}`, ctx);
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,8 @@
1
+ import type { SpawnResult } from "./docker-cli.js";
2
+ export interface DockerPidExhaustionRecovery {
3
+ readonly captureToken: () => unknown;
4
+ readonly recoverAndRetry: (token: unknown, retry: () => Promise<SpawnResult>) => Promise<SpawnResult | undefined>;
5
+ }
6
+ export declare function isDockerExecAdmissionPidExhaustion(result: SpawnResult): boolean;
7
+ export declare function isStartedShellPidExhaustion(result: SpawnResult): boolean;
8
+ //# sourceMappingURL=docker-pid-exhaustion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-pid-exhaustion.d.ts","sourceRoot":"","sources":["../../src/docker/docker-pid-exhaustion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,OAAO,CAAA;IACpC,QAAQ,CAAC,eAAe,EAAE,CACxB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,KAC9B,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;CACtC;AAED,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAU/E;AA0BD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAOxE"}
@@ -0,0 +1,39 @@
1
+ export function isDockerExecAdmissionPidExhaustion(result) {
2
+ if (result.exitCode === 0)
3
+ return false;
4
+ const output = `${result.stdout}\n${result.stderr}`;
5
+ return (output.includes("OCI runtime exec failed") &&
6
+ (output.includes("Resource temporarily unavailable") ||
7
+ output.includes("read init-p: connection reset by peer") ||
8
+ (output.includes("unable to start container process") &&
9
+ output.includes("procReady not received"))));
10
+ }
11
+ function isShellForkFailure(line) {
12
+ const prefix = "sh: ";
13
+ if (!line.startsWith(prefix))
14
+ return false;
15
+ let detail = line.slice(prefix.length);
16
+ const separator = detail.indexOf(": ");
17
+ if (separator > 0) {
18
+ const possibleLineNumber = detail.slice(0, separator);
19
+ if ([...possibleLineNumber].every((character) => character >= "0" && character <= "9")) {
20
+ detail = detail.slice(separator + 2);
21
+ }
22
+ }
23
+ const normalized = detail.toLowerCase();
24
+ return (normalized === "cannot fork" ||
25
+ normalized === "can't fork" ||
26
+ normalized === "cannot fork: resource temporarily unavailable" ||
27
+ normalized === "can't fork: resource temporarily unavailable" ||
28
+ normalized === "fork: resource temporarily unavailable" ||
29
+ normalized === "fork: retry: resource temporarily unavailable");
30
+ }
31
+ export function isStartedShellPidExhaustion(result) {
32
+ if (result.exitCode === 0)
33
+ return false;
34
+ const diagnostics = result.stderr
35
+ .split(/\r?\n/)
36
+ .map((line) => line.trim())
37
+ .filter(Boolean);
38
+ return diagnostics.length > 0 && diagnostics.every(isShellForkFailure);
39
+ }
@@ -0,0 +1,20 @@
1
+ import type { SandboxProvider } from "@b4run/workspace";
2
+ import { type Docker } from "./docker-cli.js";
3
+ export interface DockerSandboxOptions {
4
+ /** Container image for the sandbox (must include a POSIX shell). */
5
+ readonly image: string;
6
+ /** Injected for tests; defaults to the real docker CLI. */
7
+ readonly docker?: Docker;
8
+ }
9
+ /**
10
+ * Docker reference SandboxProvider. Per thread: a persistent container
11
+ * `b4-sbx-<threadId>` (sleep infinity) with a named volume mounted at
12
+ * /workspace. acquire() reuses only a keeper owned by this provider lifecycle
13
+ * with a matching persisted identity; otherwise it replaces the keeper while
14
+ * preserving the volume. release() removes the container but KEEPS the volume;
15
+ * destroy() removes both. Network: deny → --network none (exact); allow →
16
+ * bridge (denylist is best-effort and NOT enforced here — see the spec's
17
+ * honest-scope note). Host env is never inherited; only policy.env is passed.
18
+ */
19
+ export declare function dockerSandbox(opts: DockerSandboxOptions): SandboxProvider;
20
+ //# sourceMappingURL=docker-sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-sandbox.d.ts","sourceRoot":"","sources":["../../src/docker/docker-sandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAErF,OAAO,EAAgB,KAAK,MAAM,EAAoB,MAAM,iBAAiB,CAAA;AAU7E,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAyED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,eAAe,CA6QzE"}