@agentforge-ai/sandbox 0.6.0 → 0.7.1
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/dist/index.d.ts +70 -1
- package/dist/index.js +279 -7
- package/dist/index.js.map +1 -1
- package/dist/sandbox-manager.js +275 -8
- package/dist/sandbox-manager.js.map +1 -1
- package/dist/types.d.ts +36 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
export { DockerSandbox } from './docker-sandbox.js';
|
|
2
2
|
export { ContainerPool } from './container-pool.js';
|
|
3
3
|
export { SandboxManager, isDockerAvailable } from './sandbox-manager.js';
|
|
4
|
+
import { SandboxProfile, SandboxProvider, NativeSandboxConfig, ExecOptions, ExecResult } from './types.js';
|
|
5
|
+
export { DockerSandboxConfig, PoolConfig, PoolEntry, ResourceLimits, SandboxManagerConfig } from './types.js';
|
|
4
6
|
export { BLOCKED_BIND_PREFIXES, DEFAULT_CAP_DROP, SecurityError, validateBind, validateBinds, validateCommand, validateImageName } from './security.js';
|
|
5
|
-
export { DockerSandboxConfig, ExecOptions, ExecResult, PoolConfig, PoolEntry, ResourceLimits, SandboxManagerConfig, SandboxProvider } from './types.js';
|
|
6
7
|
import 'dockerode';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @module native-sandbox
|
|
11
|
+
*
|
|
12
|
+
* NativeSandbox — a lightweight process-level sandbox using platform-native
|
|
13
|
+
* isolation mechanisms as an alternative to Docker.
|
|
14
|
+
*
|
|
15
|
+
* Platform support:
|
|
16
|
+
* - macOS: `sandbox-exec` with Apple Seatbelt profiles (.sb)
|
|
17
|
+
* - Linux: `bwrap` (Bubblewrap) if available
|
|
18
|
+
* - Fallback: plain child_process with timeout + env filtering (no isolation)
|
|
19
|
+
*
|
|
20
|
+
* Security note: spawn() is used intentionally here because this module is the
|
|
21
|
+
* sandbox itself. Commands are passed through isolation wrappers (sandbox-exec /
|
|
22
|
+
* bwrap) that enforce the security policy. The env is sanitized before use.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Conservative default profile for native sandboxes.
|
|
27
|
+
* Network access is disabled; only /tmp is accessible.
|
|
28
|
+
*/
|
|
29
|
+
declare const DEFAULT_SANDBOX_PROFILE: SandboxProfile;
|
|
30
|
+
/**
|
|
31
|
+
* Detects which native isolation method is available on the current platform.
|
|
32
|
+
*
|
|
33
|
+
* @returns An object indicating availability and the method found.
|
|
34
|
+
*/
|
|
35
|
+
declare function isNativeSandboxAvailable(): Promise<{
|
|
36
|
+
available: boolean;
|
|
37
|
+
method: 'sandbox-exec' | 'bwrap' | 'none';
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* A sandbox provider that uses platform-native process isolation instead of Docker.
|
|
41
|
+
*
|
|
42
|
+
* Isolation strategy (in priority order):
|
|
43
|
+
* 1. macOS: `sandbox-exec` with Apple Seatbelt profiles
|
|
44
|
+
* 2. Linux: `bwrap` (Bubblewrap)
|
|
45
|
+
* 3. Fallback: plain child_process with timeout + env filtering (no isolation)
|
|
46
|
+
*/
|
|
47
|
+
declare class NativeSandbox implements SandboxProvider {
|
|
48
|
+
private readonly config;
|
|
49
|
+
private readonly profile;
|
|
50
|
+
private readonly id;
|
|
51
|
+
private running;
|
|
52
|
+
private isolationMethod;
|
|
53
|
+
constructor(config: NativeSandboxConfig);
|
|
54
|
+
start(): Promise<void>;
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
destroy(): Promise<void>;
|
|
57
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
58
|
+
readFile(filePath: string): Promise<string>;
|
|
59
|
+
writeFile(filePath: string, content: string): Promise<void>;
|
|
60
|
+
isRunning(): Promise<boolean>;
|
|
61
|
+
getContainerId(): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Validates that a file path is within the allowed filesystem paths defined
|
|
64
|
+
* in the sandbox profile. Throws if the path is not allowed.
|
|
65
|
+
*/
|
|
66
|
+
private _validatePath;
|
|
67
|
+
/**
|
|
68
|
+
* Builds a sanitized environment for child processes.
|
|
69
|
+
* Removes sensitive variables (secrets, tokens, credentials) and merges
|
|
70
|
+
* any additional environment variables provided by the caller.
|
|
71
|
+
*/
|
|
72
|
+
private _buildSafeEnv;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { DEFAULT_SANDBOX_PROFILE, ExecOptions, ExecResult, NativeSandbox, NativeSandboxConfig, SandboxProfile, SandboxProvider, isNativeSandboxAvailable };
|
package/dist/index.js
CHANGED
|
@@ -280,11 +280,11 @@ var DockerSandbox = class {
|
|
|
280
280
|
*
|
|
281
281
|
* @param path - Absolute path inside the container.
|
|
282
282
|
*/
|
|
283
|
-
async readFile(
|
|
284
|
-
const result = await this.exec(`cat "${
|
|
283
|
+
async readFile(path2) {
|
|
284
|
+
const result = await this.exec(`cat "${path2.replace(/"/g, '\\"')}"`);
|
|
285
285
|
if (result.exitCode !== 0) {
|
|
286
286
|
throw new Error(
|
|
287
|
-
`DockerSandbox.readFile: failed to read "${
|
|
287
|
+
`DockerSandbox.readFile: failed to read "${path2}" (exit ${result.exitCode}): ${result.stderr}`
|
|
288
288
|
);
|
|
289
289
|
}
|
|
290
290
|
return result.stdout;
|
|
@@ -296,13 +296,13 @@ var DockerSandbox = class {
|
|
|
296
296
|
* @param path - Absolute path inside the container.
|
|
297
297
|
* @param content - UTF-8 string content.
|
|
298
298
|
*/
|
|
299
|
-
async writeFile(
|
|
299
|
+
async writeFile(path2, content) {
|
|
300
300
|
const b64 = Buffer.from(content, "utf8").toString("base64");
|
|
301
|
-
const cmd = `printf '%s' "${b64}" | base64 -d > "${
|
|
301
|
+
const cmd = `printf '%s' "${b64}" | base64 -d > "${path2.replace(/"/g, '\\"')}"`;
|
|
302
302
|
const result = await this.exec(cmd);
|
|
303
303
|
if (result.exitCode !== 0) {
|
|
304
304
|
throw new Error(
|
|
305
|
-
`DockerSandbox.writeFile: failed to write "${
|
|
305
|
+
`DockerSandbox.writeFile: failed to write "${path2}" (exit ${result.exitCode}): ${result.stderr}`
|
|
306
306
|
);
|
|
307
307
|
}
|
|
308
308
|
}
|
|
@@ -536,7 +536,254 @@ var ContainerPool = class {
|
|
|
536
536
|
|
|
537
537
|
// src/sandbox-manager.ts
|
|
538
538
|
import Dockerode2 from "dockerode";
|
|
539
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
540
|
+
|
|
541
|
+
// src/native-sandbox.ts
|
|
542
|
+
import { execFile, spawn } from "child_process";
|
|
543
|
+
import { promisify } from "util";
|
|
544
|
+
import fs from "fs/promises";
|
|
545
|
+
import path from "path";
|
|
546
|
+
import os from "os";
|
|
539
547
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
548
|
+
var execFileAsync = promisify(execFile);
|
|
549
|
+
var DEFAULT_SANDBOX_PROFILE = {
|
|
550
|
+
allowNetwork: false,
|
|
551
|
+
allowFS: ["/tmp"],
|
|
552
|
+
timeout: 30,
|
|
553
|
+
memory: 512
|
|
554
|
+
};
|
|
555
|
+
async function isNativeSandboxAvailable() {
|
|
556
|
+
if (process.platform === "darwin") {
|
|
557
|
+
try {
|
|
558
|
+
await execFileAsync("which", ["sandbox-exec"], { timeout: 5e3 });
|
|
559
|
+
return { available: true, method: "sandbox-exec" };
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (process.platform === "linux") {
|
|
564
|
+
try {
|
|
565
|
+
await execFileAsync("which", ["bwrap"], { timeout: 5e3 });
|
|
566
|
+
return { available: true, method: "bwrap" };
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { available: false, method: "none" };
|
|
571
|
+
}
|
|
572
|
+
function generateSeatbeltProfile(profile) {
|
|
573
|
+
const lines = [
|
|
574
|
+
"(version 1)",
|
|
575
|
+
"(deny default)",
|
|
576
|
+
// Always allow process execution so /bin/sh -c works
|
|
577
|
+
"(allow process-exec)",
|
|
578
|
+
"(allow process-fork)",
|
|
579
|
+
// Allow reading system libraries and frameworks
|
|
580
|
+
'(allow file-read* (subpath "/usr/lib"))',
|
|
581
|
+
'(allow file-read* (subpath "/usr/libexec"))',
|
|
582
|
+
'(allow file-read* (subpath "/System/Library"))',
|
|
583
|
+
'(allow file-read* (subpath "/Library/Preferences"))',
|
|
584
|
+
'(allow file-read* (literal "/dev/null"))',
|
|
585
|
+
'(allow file-read* (literal "/dev/urandom"))',
|
|
586
|
+
'(allow file-read* (literal "/dev/random"))',
|
|
587
|
+
// Allow sysctl reads (needed by many programs)
|
|
588
|
+
"(allow sysctl-read)",
|
|
589
|
+
// Allow signal sending to self
|
|
590
|
+
"(allow signal (target self))",
|
|
591
|
+
// Allow mach operations needed for basic process functionality
|
|
592
|
+
"(allow mach-lookup)"
|
|
593
|
+
];
|
|
594
|
+
for (const allowedPath of profile.allowFS) {
|
|
595
|
+
lines.push(`(allow file-read* (subpath "${allowedPath}"))`);
|
|
596
|
+
lines.push(`(allow file-write* (subpath "${allowedPath}"))`);
|
|
597
|
+
}
|
|
598
|
+
if (profile.allowNetwork) {
|
|
599
|
+
lines.push("(allow network*)");
|
|
600
|
+
}
|
|
601
|
+
return lines.join("\n");
|
|
602
|
+
}
|
|
603
|
+
function buildBwrapArgs(profile, command, cwd) {
|
|
604
|
+
const args = [
|
|
605
|
+
"--ro-bind",
|
|
606
|
+
"/",
|
|
607
|
+
"/",
|
|
608
|
+
"--dev",
|
|
609
|
+
"/dev",
|
|
610
|
+
"--proc",
|
|
611
|
+
"/proc",
|
|
612
|
+
"--tmpfs",
|
|
613
|
+
"/tmp"
|
|
614
|
+
];
|
|
615
|
+
for (const allowedPath of profile.allowFS) {
|
|
616
|
+
if (allowedPath === "/tmp") continue;
|
|
617
|
+
args.push("--bind", allowedPath, allowedPath);
|
|
618
|
+
}
|
|
619
|
+
if (!profile.allowNetwork) {
|
|
620
|
+
args.push("--unshare-net");
|
|
621
|
+
}
|
|
622
|
+
args.push("--unshare-user", "--unshare-pid", "--unshare-ipc", "--unshare-uts");
|
|
623
|
+
if (cwd) {
|
|
624
|
+
args.push("--chdir", cwd);
|
|
625
|
+
}
|
|
626
|
+
args.push("--", "/bin/sh", "-c", command);
|
|
627
|
+
return args;
|
|
628
|
+
}
|
|
629
|
+
var NativeSandbox = class {
|
|
630
|
+
config;
|
|
631
|
+
profile;
|
|
632
|
+
id;
|
|
633
|
+
running = false;
|
|
634
|
+
isolationMethod = "none";
|
|
635
|
+
constructor(config) {
|
|
636
|
+
this.config = config;
|
|
637
|
+
this.profile = config.profile ?? { ...DEFAULT_SANDBOX_PROFILE };
|
|
638
|
+
this.id = `native-${randomUUID3()}`;
|
|
639
|
+
}
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// Lifecycle
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
async start() {
|
|
644
|
+
const { method } = await isNativeSandboxAvailable();
|
|
645
|
+
this.isolationMethod = method;
|
|
646
|
+
if (method === "none") {
|
|
647
|
+
console.warn(
|
|
648
|
+
"[NativeSandbox] No native isolation available (sandbox-exec / bwrap not found). Running with limited isolation (timeout + env filtering only). For stronger isolation, install Docker or use bwrap on Linux."
|
|
649
|
+
);
|
|
650
|
+
} else {
|
|
651
|
+
console.info(`[NativeSandbox] Using isolation method: ${method}`);
|
|
652
|
+
}
|
|
653
|
+
if (this.config.workingDirectory) {
|
|
654
|
+
await fs.mkdir(this.config.workingDirectory, { recursive: true });
|
|
655
|
+
}
|
|
656
|
+
this.running = true;
|
|
657
|
+
}
|
|
658
|
+
async stop() {
|
|
659
|
+
this.running = false;
|
|
660
|
+
}
|
|
661
|
+
async destroy() {
|
|
662
|
+
this.running = false;
|
|
663
|
+
}
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// Execution
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
async exec(command, options) {
|
|
668
|
+
const timeoutMs = options?.timeout ?? this.profile.timeout * 1e3;
|
|
669
|
+
const cwd = options?.cwd ?? this.config.workingDirectory ?? os.tmpdir();
|
|
670
|
+
const extraEnv = options?.env ?? {};
|
|
671
|
+
return new Promise((resolve, reject) => {
|
|
672
|
+
let stdout = "";
|
|
673
|
+
let stderr = "";
|
|
674
|
+
const env = this._buildSafeEnv(extraEnv);
|
|
675
|
+
let proc;
|
|
676
|
+
if (this.isolationMethod === "sandbox-exec") {
|
|
677
|
+
const sbProfile = generateSeatbeltProfile(this.profile);
|
|
678
|
+
proc = spawn("sandbox-exec", ["-p", sbProfile, "/bin/sh", "-c", command], {
|
|
679
|
+
cwd,
|
|
680
|
+
env,
|
|
681
|
+
timeout: timeoutMs
|
|
682
|
+
});
|
|
683
|
+
} else if (this.isolationMethod === "bwrap") {
|
|
684
|
+
const bwrapArgs = buildBwrapArgs(this.profile, command, cwd);
|
|
685
|
+
proc = spawn("bwrap", bwrapArgs, {
|
|
686
|
+
env,
|
|
687
|
+
timeout: timeoutMs
|
|
688
|
+
});
|
|
689
|
+
} else {
|
|
690
|
+
proc = spawn("/bin/sh", ["-c", command], {
|
|
691
|
+
cwd,
|
|
692
|
+
env,
|
|
693
|
+
timeout: timeoutMs
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
proc.stdout?.on("data", (chunk) => {
|
|
697
|
+
stdout += chunk.toString();
|
|
698
|
+
});
|
|
699
|
+
proc.stderr?.on("data", (chunk) => {
|
|
700
|
+
stderr += chunk.toString();
|
|
701
|
+
});
|
|
702
|
+
proc.on("error", (err) => {
|
|
703
|
+
reject(new Error(`[NativeSandbox] exec error: ${err.message}`));
|
|
704
|
+
});
|
|
705
|
+
proc.on("close", (code) => {
|
|
706
|
+
resolve({
|
|
707
|
+
stdout,
|
|
708
|
+
stderr,
|
|
709
|
+
exitCode: code ?? 1
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
// ---------------------------------------------------------------------------
|
|
715
|
+
// File I/O (with path validation)
|
|
716
|
+
// ---------------------------------------------------------------------------
|
|
717
|
+
async readFile(filePath) {
|
|
718
|
+
this._validatePath(filePath);
|
|
719
|
+
return fs.readFile(filePath, "utf-8");
|
|
720
|
+
}
|
|
721
|
+
async writeFile(filePath, content) {
|
|
722
|
+
this._validatePath(filePath);
|
|
723
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
724
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
725
|
+
}
|
|
726
|
+
// ---------------------------------------------------------------------------
|
|
727
|
+
// Health
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
async isRunning() {
|
|
730
|
+
return this.running;
|
|
731
|
+
}
|
|
732
|
+
getContainerId() {
|
|
733
|
+
return this.running ? this.id : null;
|
|
734
|
+
}
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// Private helpers
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
/**
|
|
739
|
+
* Validates that a file path is within the allowed filesystem paths defined
|
|
740
|
+
* in the sandbox profile. Throws if the path is not allowed.
|
|
741
|
+
*/
|
|
742
|
+
_validatePath(filePath) {
|
|
743
|
+
const resolved = path.resolve(filePath);
|
|
744
|
+
const allowed = this.profile.allowFS.some((allowedPath) => {
|
|
745
|
+
const resolvedAllowed = path.resolve(allowedPath);
|
|
746
|
+
return resolved.startsWith(resolvedAllowed + path.sep) || resolved === resolvedAllowed;
|
|
747
|
+
});
|
|
748
|
+
if (!allowed) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
`[NativeSandbox] Access denied: path "${filePath}" is not within allowed filesystem paths: ` + JSON.stringify(this.profile.allowFS)
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Builds a sanitized environment for child processes.
|
|
756
|
+
* Removes sensitive variables (secrets, tokens, credentials) and merges
|
|
757
|
+
* any additional environment variables provided by the caller.
|
|
758
|
+
*/
|
|
759
|
+
_buildSafeEnv(extra) {
|
|
760
|
+
const SENSITIVE_PATTERNS = [
|
|
761
|
+
/SECRET/i,
|
|
762
|
+
/TOKEN/i,
|
|
763
|
+
/PASSWORD/i,
|
|
764
|
+
/PASSWD/i,
|
|
765
|
+
/API_KEY/i,
|
|
766
|
+
/PRIVATE_KEY/i,
|
|
767
|
+
/CREDENTIAL/i,
|
|
768
|
+
/AUTH/i,
|
|
769
|
+
/AWS_/i,
|
|
770
|
+
/GCP_/i,
|
|
771
|
+
/AZURE_/i
|
|
772
|
+
];
|
|
773
|
+
const safeEnv = {};
|
|
774
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
775
|
+
if (value === void 0) continue;
|
|
776
|
+
const isSensitive = SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
777
|
+
if (!isSensitive) {
|
|
778
|
+
safeEnv[key] = value;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
Object.assign(safeEnv, extra);
|
|
782
|
+
return safeEnv;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
// src/sandbox-manager.ts
|
|
540
787
|
var E2BProviderStub = class {
|
|
541
788
|
async start() {
|
|
542
789
|
throw new Error(
|
|
@@ -609,6 +856,16 @@ var SandboxManager = class {
|
|
|
609
856
|
);
|
|
610
857
|
}
|
|
611
858
|
}
|
|
859
|
+
if (this.config.provider === "native") {
|
|
860
|
+
const { available, method } = await isNativeSandboxAvailable();
|
|
861
|
+
if (!available) {
|
|
862
|
+
console.warn(
|
|
863
|
+
"[SandboxManager] No native isolation method found (sandbox-exec / bwrap). Sandboxes will run with limited isolation (timeout + env filtering only)."
|
|
864
|
+
);
|
|
865
|
+
} else {
|
|
866
|
+
console.info(`[SandboxManager] Native isolation available: ${method}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
612
869
|
this._registerShutdownHandlers();
|
|
613
870
|
}
|
|
614
871
|
/**
|
|
@@ -626,6 +883,18 @@ var SandboxManager = class {
|
|
|
626
883
|
this.active.set(id2, stub);
|
|
627
884
|
return stub;
|
|
628
885
|
}
|
|
886
|
+
if (this.config.provider === "native") {
|
|
887
|
+
const nativeConfig = {
|
|
888
|
+
scope: overrides.scope,
|
|
889
|
+
profile: this.config.nativeConfig?.profile,
|
|
890
|
+
workingDirectory: this.config.nativeConfig?.workingDirectory
|
|
891
|
+
};
|
|
892
|
+
const sandbox2 = new NativeSandbox(nativeConfig);
|
|
893
|
+
await sandbox2.start();
|
|
894
|
+
const id2 = this._generateId(overrides.scope);
|
|
895
|
+
this.active.set(id2, sandbox2);
|
|
896
|
+
return sandbox2;
|
|
897
|
+
}
|
|
629
898
|
const mergedConfig = {
|
|
630
899
|
image: "node:22-slim",
|
|
631
900
|
...this.config.dockerConfig,
|
|
@@ -672,7 +941,7 @@ var SandboxManager = class {
|
|
|
672
941
|
// Private helpers
|
|
673
942
|
// ---------------------------------------------------------------------------
|
|
674
943
|
_generateId(scope) {
|
|
675
|
-
return `agentforge-${scope}-${
|
|
944
|
+
return `agentforge-${scope}-${randomUUID4().slice(0, 8)}`;
|
|
676
945
|
}
|
|
677
946
|
_registerShutdownHandlers() {
|
|
678
947
|
if (this.shutdownRegistered) return;
|
|
@@ -689,10 +958,13 @@ export {
|
|
|
689
958
|
BLOCKED_BIND_PREFIXES,
|
|
690
959
|
ContainerPool,
|
|
691
960
|
DEFAULT_CAP_DROP,
|
|
961
|
+
DEFAULT_SANDBOX_PROFILE,
|
|
692
962
|
DockerSandbox,
|
|
963
|
+
NativeSandbox,
|
|
693
964
|
SandboxManager,
|
|
694
965
|
SecurityError,
|
|
695
966
|
isDockerAvailable,
|
|
967
|
+
isNativeSandboxAvailable,
|
|
696
968
|
validateBind,
|
|
697
969
|
validateBinds,
|
|
698
970
|
validateCommand,
|