@vinhnt-sdk/sandbox 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nguyen Thanh Vinh
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,36 @@
1
+ # @vinhnt-sdk/sandbox
2
+
3
+ Sandbox primitives + fail-closed factory for isolated tool/command execution.
4
+
5
+ ```typescript
6
+ import { createSandbox, SandboxUnavailableError } from "@vinhnt-sdk/sandbox";
7
+ import { createHostSandbox } from "@vinhnt-sdk/sandbox-host";
8
+ import { createProcessSandbox } from "@vinhnt-sdk/sandbox-process";
9
+
10
+ const sandbox = createSandbox({ defaultTimeoutMs: 30_000, scope: "process" }, {
11
+ host: createHostSandbox,
12
+ process: createProcessSandbox,
13
+ });
14
+
15
+ const result = await sandbox.execute({
16
+ command: "node ./script.js",
17
+ cwd: "/workspace",
18
+ timeoutMs: 30_000,
19
+ });
20
+ ```
21
+
22
+ ## Fail-closed by default
23
+
24
+ Requesting a sandbox scope that has no registered backend throws
25
+ `SandboxUnavailableError` — it never silently downgrades to a weaker sandbox.
26
+ Wire only the backends your application actually supports.
27
+
28
+ ## Contents
29
+
30
+ - `ProcessSandbox` interface — `execute()` / `destroy()` contract.
31
+ - `SandboxConfig`, `SandboxResult`, `SandboxScope`, `KNOWN_SANDBOX_SCOPES`.
32
+ - `SandboxUnavailableError` — fail-closed error carrying the missing scope.
33
+ - `createSandbox(config, backends)` — fail-closed factory.
34
+ - `parseCommand` — shell command tokenizer.
35
+ - `killProcessTree` / `isPidAlive` / `treeKillSpawnOptions` — tree-scoped termination.
36
+ - Backend packages: `@vinhnt-sdk/sandbox-host`, `@vinhnt-sdk/sandbox-process`, `@vinhnt-sdk/sandbox-container`.
@@ -0,0 +1,4 @@
1
+ import type { SandboxConfig, ProcessSandbox } from "../types.js";
2
+ /** Create a host (no-isolation) sandbox backend. */
3
+ export declare function createHostSandbox(_config?: SandboxConfig): ProcessSandbox;
4
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/backends/host.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAiB,cAAc,EAAgC,MAAM,aAAa,CAAC;AA8D9G,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,cAAc,CAEzE"}
@@ -0,0 +1,57 @@
1
+ import { execFile } from "node:child_process";
2
+ import { parseCommand } from "../shell-parser.js";
3
+ import { treeKillSpawnOptions } from "../kill-tree.js";
4
+ import { withTimeoutAndAbort } from "../timeout.js";
5
+ import { sanitizeEnv } from "@vinhnt-sdk/security";
6
+ class HostSandbox {
7
+ scope = "host";
8
+ async execute(options) {
9
+ const start = Date.now();
10
+ return new Promise((resolve) => {
11
+ const parsed = parseCommand(options.command);
12
+ const child = execFile(parsed.file, parsed.args, {
13
+ cwd: options.cwd,
14
+ maxBuffer: 10 * 1024 * 1024,
15
+ encoding: "utf-8",
16
+ windowsHide: true,
17
+ ...treeKillSpawnOptions(),
18
+ env: options.env && Object.keys(options.env).length > 0
19
+ ? { ...sanitizeEnv(), ...options.env }
20
+ : sanitizeEnv(),
21
+ }, (err, stdout, stderr) => {
22
+ const durationMs = Date.now() - start;
23
+ if (err) {
24
+ const exitCode = typeof err.code === "number" ? err.code : 1;
25
+ resolve({
26
+ result: { stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), exitCode },
27
+ exitCode,
28
+ durationMs,
29
+ timedOut: handle.timedOut,
30
+ });
31
+ }
32
+ else {
33
+ resolve({
34
+ result: { stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), exitCode: 0 },
35
+ exitCode: 0,
36
+ durationMs,
37
+ timedOut: false,
38
+ });
39
+ }
40
+ });
41
+ const handle = withTimeoutAndAbort(child, options.timeoutMs, options.signal, (reason) => {
42
+ resolve({
43
+ result: { stdout: "", stderr: reason, exitCode: 1 },
44
+ exitCode: 1,
45
+ durationMs: Date.now() - start,
46
+ timedOut: handle.timedOut,
47
+ });
48
+ });
49
+ });
50
+ }
51
+ async destroy() { }
52
+ }
53
+ /** Create a host (no-isolation) sandbox backend. */
54
+ export function createHostSandbox(_config) {
55
+ return new HostSandbox();
56
+ }
57
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/backends/host.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { SandboxConfig, ProcessSandbox } from "../types.js";
2
+ /** Create a process-isolation sandbox backend. */
3
+ export declare function createProcessSandbox(config: SandboxConfig): ProcessSandbox;
4
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/backends/process.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAiB,cAAc,EAAgC,MAAM,aAAa,CAAC;AAyJ9G,kDAAkD;AAClD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAE1E"}
@@ -0,0 +1,150 @@
1
+ import { execFile } from "node:child_process";
2
+ import { realpath } from "node:fs/promises";
3
+ import { relative, isAbsolute } from "node:path";
4
+ import { parseCommand } from "../shell-parser.js";
5
+ import { treeKillSpawnOptions } from "../kill-tree.js";
6
+ import { withTimeoutAndAbort } from "../timeout.js";
7
+ const DEFAULT_ALLOWED_COMMANDS = new Set([
8
+ "node", "npm", "npx", "pnpm", "git",
9
+ "ls", "cat", "grep", "find", "head", "tail", "wc", "sort", "uniq", "diff",
10
+ "echo", "pwd", "whoami", "date",
11
+ ]);
12
+ const BLOCKED_COMMAND_PATTERNS = [
13
+ /\brm\s+-rf\s+\/(?:[^a-z]|$)/i,
14
+ /\bmkfs\b/i,
15
+ /\bdd\b.*of=\/dev/i,
16
+ /\bformat\b/i,
17
+ /\bshutdown\b/i,
18
+ /\breboot\b/i,
19
+ /\bkill\s+-9\s+1\b/i,
20
+ /\bcurl\b.*\|\s*sh/i,
21
+ /\bwget\b.*\|\s*sh/i,
22
+ ];
23
+ function extractBaseName(file) {
24
+ const name = file.split("/").pop()?.split("\\").pop() ?? file;
25
+ return process.platform === "win32" ? name.toLowerCase().replace(/\.exe$/, "") : name;
26
+ }
27
+ function isCommandAllowed(command, allowedCommands) {
28
+ const parsed = parseCommand(command);
29
+ const baseCommand = extractBaseName(parsed.file);
30
+ for (const pattern of BLOCKED_COMMAND_PATTERNS) {
31
+ if (pattern.test(command))
32
+ return false;
33
+ }
34
+ const allowlist = allowedCommands ?? DEFAULT_ALLOWED_COMMANDS;
35
+ return allowlist.has(baseCommand);
36
+ }
37
+ function isPathWithin(target, roots) {
38
+ for (const root of roots) {
39
+ const rel = relative(root, target);
40
+ if (rel === "")
41
+ return true;
42
+ if (rel.startsWith(".."))
43
+ continue;
44
+ if (isAbsolute(rel))
45
+ continue;
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+ function createEmptyEnv(allowedVars) {
51
+ const env = {};
52
+ if (allowedVars) {
53
+ for (const key of allowedVars) {
54
+ const value = process.env[key];
55
+ if (value !== undefined)
56
+ env[key] = value;
57
+ }
58
+ }
59
+ return env;
60
+ }
61
+ class ProcessSandboxImpl {
62
+ scope = "process";
63
+ config;
64
+ constructor(config) {
65
+ this.config = config;
66
+ }
67
+ async execute(options) {
68
+ const start = Date.now();
69
+ if (!isCommandAllowed(options.command, this.config.allowedCommands)) {
70
+ return {
71
+ result: { stdout: "", stderr: `Command not allowed: ${options.command.split(" ")[0]}`, exitCode: 126 },
72
+ exitCode: 126, durationMs: Date.now() - start, timedOut: false,
73
+ };
74
+ }
75
+ if (this.config.allowedPaths && this.config.allowedPaths.length > 0) {
76
+ let cwdReal;
77
+ const allowedReal = [];
78
+ try {
79
+ cwdReal = await realpath(options.cwd);
80
+ }
81
+ catch {
82
+ cwdReal = options.cwd;
83
+ }
84
+ for (const p of this.config.allowedPaths) {
85
+ let rootReal = p;
86
+ try {
87
+ rootReal = await realpath(p);
88
+ }
89
+ catch { /* fall back to lexical path */ }
90
+ allowedReal.push(rootReal);
91
+ }
92
+ if (!isPathWithin(cwdReal, allowedReal)) {
93
+ return {
94
+ result: { stdout: "", stderr: `Working directory outside allowed paths: ${options.cwd}`, exitCode: 126 },
95
+ exitCode: 126, durationMs: Date.now() - start, timedOut: false,
96
+ };
97
+ }
98
+ }
99
+ const secureEnv = this.config.allowedEnvVars
100
+ ? createEmptyEnv(this.config.allowedEnvVars)
101
+ : createEmptyEnv();
102
+ const parsed = parseCommand(options.command);
103
+ const execArgs = [];
104
+ if (this.config.enablePermissionModel && extractBaseName(parsed.file) === "node") {
105
+ execArgs.push("--permission");
106
+ execArgs.push(`--allow-fs-read=${options.cwd}`);
107
+ execArgs.push("--allow-fs-read=/tmp");
108
+ execArgs.push("--allow-fs-write=/tmp");
109
+ }
110
+ const maxBuffer = 10 * 1024 * 1024;
111
+ return new Promise((resolve) => {
112
+ const child = execFile(parsed.file, [...execArgs, ...parsed.args], {
113
+ cwd: options.cwd,
114
+ maxBuffer,
115
+ encoding: "utf-8",
116
+ windowsHide: true,
117
+ ...treeKillSpawnOptions(),
118
+ env: secureEnv,
119
+ ...(process.platform !== "win32" ? { uid: process.getuid?.(), gid: process.getgid?.() } : {}),
120
+ }, (err, stdout, stderr) => {
121
+ const durationMs = Date.now() - start;
122
+ if (err) {
123
+ const exitCode = typeof err.code === "number" ? err.code : 1;
124
+ resolve({
125
+ result: { stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), exitCode },
126
+ exitCode, durationMs, timedOut: handle.timedOut,
127
+ });
128
+ }
129
+ else {
130
+ resolve({
131
+ result: { stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), exitCode: 0 },
132
+ exitCode: 0, durationMs, timedOut: false,
133
+ });
134
+ }
135
+ });
136
+ const handle = withTimeoutAndAbort(child, options.timeoutMs, options.signal, (reason) => {
137
+ resolve({
138
+ result: { stdout: "", stderr: reason, exitCode: 1 },
139
+ exitCode: 1, durationMs: Date.now() - start, timedOut: handle.timedOut,
140
+ });
141
+ });
142
+ });
143
+ }
144
+ async destroy() { }
145
+ }
146
+ /** Create a process-isolation sandbox backend. */
147
+ export function createProcessSandbox(config) {
148
+ return new ProcessSandboxImpl(config);
149
+ }
150
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../../src/backends/process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { VntError } from "@vinhnt-sdk/schema";
2
+ /**
3
+ * Thrown when a sandbox scope is requested but no backend is available for it.
4
+ *
5
+ * Sandboxes are **fail-closed**: requesting an unsupported/unavailable scope
6
+ * never silently downgrades to a weaker sandbox. Instead this error makes the
7
+ * gap explicit, listing the scopes that ARE wired.
8
+ */
9
+ export declare class SandboxUnavailableError extends VntError {
10
+ readonly scope: string;
11
+ readonly availableScopes: readonly string[];
12
+ readonly code: "ERR_SANDBOX_UNAVAILABLE";
13
+ readonly retryable = false;
14
+ constructor(scope: string, availableScopes?: readonly string[]);
15
+ }
16
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C;;;;;;GAMG;AACH,qBAAa,uBAAwB,SAAQ,QAAQ;aAKjC,KAAK,EAAE,MAAM;aACb,eAAe,EAAE,SAAS,MAAM,EAAE;IALpD,SAAyB,IAAI,EAAG,yBAAyB,CAAU;IACnE,SAAyB,SAAS,SAAS;gBAGzB,KAAK,EAAE,MAAM,EACb,eAAe,GAAE,SAAS,MAAM,EAAO;CAU1D"}
package/dist/error.js ADDED
@@ -0,0 +1,24 @@
1
+ import { VntError } from "@vinhnt-sdk/schema";
2
+ /**
3
+ * Thrown when a sandbox scope is requested but no backend is available for it.
4
+ *
5
+ * Sandboxes are **fail-closed**: requesting an unsupported/unavailable scope
6
+ * never silently downgrades to a weaker sandbox. Instead this error makes the
7
+ * gap explicit, listing the scopes that ARE wired.
8
+ */
9
+ export class SandboxUnavailableError extends VntError {
10
+ scope;
11
+ availableScopes;
12
+ code = "ERR_SANDBOX_UNAVAILABLE";
13
+ retryable = false;
14
+ constructor(scope, availableScopes = []) {
15
+ super(`Sandbox scope "${scope}" is not available` +
16
+ (availableScopes.length > 0
17
+ ? ` (available: ${availableScopes.join(", ")})`
18
+ : " — no sandbox backends registered; wire a backend package like @vinhnt-sdk/sandbox-host or @vinhnt-sdk/sandbox-process"));
19
+ this.scope = scope;
20
+ this.availableScopes = availableScopes;
21
+ this.name = "SandboxUnavailableError";
22
+ }
23
+ }
24
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,OAAO,uBAAwB,SAAQ,QAAQ;IAKjC;IACA;IALO,IAAI,GAAG,yBAAkC,CAAC;IAC1C,SAAS,GAAG,KAAK,CAAC;IAE3C,YACkB,KAAa,EACb,kBAAqC,EAAE;QAEvD,KAAK,CACH,kBAAkB,KAAK,oBAAoB;YAC3C,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC;gBACzB,CAAC,CAAC,gBAAgB,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAC/C,CAAC,CAAC,wHAAwH,CAAC,CAC9H,CAAC;QARc,UAAK,GAAL,KAAK,CAAQ;QACb,oBAAe,GAAf,eAAe,CAAwB;QAQvD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF"}
@@ -0,0 +1,20 @@
1
+ import type { SandboxConfig, ProcessSandbox, SandboxScope } from "./types.js";
2
+ /** Factory that builds a `ProcessSandbox` for a scope from a config. */
3
+ export type SandboxBackendFactory = (config: SandboxConfig) => ProcessSandbox;
4
+ /**
5
+ * Backend registry passed to `createSandbox`. Keys are sandbox scopes; values
6
+ * are factories (or undefined if the scope is not wired).
7
+ */
8
+ export type SandboxBackends = Record<SandboxScope, SandboxBackendFactory | undefined>;
9
+ /**
10
+ * Build a sandbox for the configured scope.
11
+ *
12
+ * **Fail-closed**: if the requested scope has no registered backend this throws
13
+ * `SandboxUnavailableError` — it never silently downgrades to a weaker sandbox.
14
+ *
15
+ * @param config - Sandbox configuration.
16
+ * @param backends - Map of scope → backend factory. Wire only what you support.
17
+ * @returns ProcessSandbox instance for the requested scope.
18
+ */
19
+ export declare function createSandbox(config: SandboxConfig, backends?: SandboxBackends): ProcessSandbox;
20
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG9E,wEAAwE;AACxE,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,aAAa,KAAK,cAAc,CAAC;AAE9E;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,YAAY,EAAE,qBAAqB,GAAG,SAAS,CAAC,CAAC;AAEtF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,GAAE,eAAoB,GAAG,cAAc,CAQnG"}
@@ -0,0 +1,21 @@
1
+ import { SandboxUnavailableError } from "./error.js";
2
+ /**
3
+ * Build a sandbox for the configured scope.
4
+ *
5
+ * **Fail-closed**: if the requested scope has no registered backend this throws
6
+ * `SandboxUnavailableError` — it never silently downgrades to a weaker sandbox.
7
+ *
8
+ * @param config - Sandbox configuration.
9
+ * @param backends - Map of scope → backend factory. Wire only what you support.
10
+ * @returns ProcessSandbox instance for the requested scope.
11
+ */
12
+ export function createSandbox(config, backends = {}) {
13
+ const scope = config.scope ?? "host";
14
+ const factory = backends[scope];
15
+ if (!factory) {
16
+ const availableScopes = Object.keys(backends).filter((s) => backends[s] !== undefined);
17
+ throw new SandboxUnavailableError(scope, availableScopes);
18
+ }
19
+ return factory(config);
20
+ }
21
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAWrD;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,MAAqB,EAAE,WAA4B,EAAE;IACjF,MAAM,KAAK,GAAiB,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC;IACnD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;QACvF,MAAM,IAAI,uBAAuB,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,11 @@
1
+ export type { SandboxScope, SandboxConfig, SandboxResult, ProcessSandbox, ProcessSandboxExecuteOptions } from "./types.js";
2
+ export { KNOWN_SANDBOX_SCOPES } from "./types.js";
3
+ export { SandboxUnavailableError } from "./error.js";
4
+ export { createSandbox } from "./factory.js";
5
+ export type { SandboxBackendFactory, SandboxBackends } from "./factory.js";
6
+ export { parseCommand } from "./shell-parser.js";
7
+ export { killProcessTree, killProcessTreeAndWait, killTreeTrackedPids, isPidAlive, pruneKillTreeState, resetKillTreeState, treeKillSpawnOptions } from "./kill-tree.js";
8
+ export { withTimeoutAndAbort } from "./timeout.js";
9
+ export { createHostSandbox } from "./backends/host.js";
10
+ export { createProcessSandbox } from "./backends/process.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAC3H,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,UAAU,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACxK,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAInD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { KNOWN_SANDBOX_SCOPES } from "./types.js";
2
+ export { SandboxUnavailableError } from "./error.js";
3
+ export { createSandbox } from "./factory.js";
4
+ export { parseCommand } from "./shell-parser.js";
5
+ export { killProcessTree, killProcessTreeAndWait, killTreeTrackedPids, isPidAlive, pruneKillTreeState, resetKillTreeState, treeKillSpawnOptions } from "./kill-tree.js";
6
+ export { withTimeoutAndAbort } from "./timeout.js";
7
+ // Re-export backend factories for backward compatibility
8
+ // These were previously in separate packages (sandbox-host, sandbox-process)
9
+ export { createHostSandbox } from "./backends/host.js";
10
+ export { createProcessSandbox } from "./backends/process.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,UAAU,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACxK,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,yDAAyD;AACzD,6EAA6E;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tree-scoped process termination.
3
+ *
4
+ * `child.kill()` only terminates the direct child; grandchild processes keep
5
+ * running (orphaned). This module kills the whole process tree:
6
+ * - POSIX: child is spawned `detached` (own process group) → `kill(-pid)`
7
+ * - Windows: `taskkill /pid <pid> /T /F`
8
+ *
9
+ * Termination is idempotent: repeated kills for the same pid are no-ops while
10
+ * the kill is still in-flight. The registry is pruned once the tracked process
11
+ * exits (or its TTL elapses) so an OS pid reuse is not silently swallowed.
12
+ *
13
+ * @module sandbox/kill-tree
14
+ * @packageDocumentation
15
+ */
16
+ import { type ChildProcess } from "node:child_process";
17
+ /** Clear the idempotency registry (used by tests). */
18
+ export declare function resetKillTreeState(): void;
19
+ /**
20
+ * Pids currently tracked as "kill already issued".
21
+ *
22
+ * Exposed for observability/tests so a caller can confirm that a terminated
23
+ * pid is no longer held (i.e. it is safe to kill again after reuse).
24
+ */
25
+ export declare function killTreeTrackedPids(): number[];
26
+ /**
27
+ * Prune the idempotency registry: drop pids whose process has exited or whose
28
+ * kill is older than the TTL. Without pruning the registry grows forever and a
29
+ * reused pid is treated as "already killed" — a silent no-op (RV-11).
30
+ *
31
+ * @param now - Clock value to compare the TTL against (injectable for tests).
32
+ */
33
+ export declare function pruneKillTreeState(now?: number): void;
34
+ /** True when the process (or its process group) is still alive. */
35
+ export declare function isPidAlive(pid: number): boolean;
36
+ /**
37
+ * Kill a child process and its whole subtree. Idempotent per pid while the
38
+ * kill is in-flight (the registry is pruned once the process exits).
39
+ *
40
+ * @param child - The spawned child process.
41
+ * @param signal - POSIX signal for the direct kill (default "SIGTERM").
42
+ * Windows always uses `taskkill /T /F`.
43
+ * @returns `true` when the kill was issued (or already issued), `false` when
44
+ * there is no pid yet (child not spawned).
45
+ */
46
+ export declare function killProcessTree(child: ChildProcess, signal?: NodeJS.Signals): boolean;
47
+ /**
48
+ * Kill a child process and its whole subtree, then await its exit.
49
+ *
50
+ * This is the awaited form of {@link killProcessTree} (RV-11): on Windows the
51
+ * `taskkill /T` spawn is no longer fire-and-forget — the returned promise
52
+ * resolves only after the child has actually exited, or the given timeout
53
+ * elapses.
54
+ *
55
+ * @param child - The spawned child process.
56
+ * @param signal - POSIX signal for the direct kill (default "SIGTERM").
57
+ * @param timeoutMs - How long to wait for the child to exit (default 5000).
58
+ * @returns `true` when the kill was issued (or already issued), `false` when
59
+ * there is no pid yet (child not spawned).
60
+ */
61
+ export declare function killProcessTreeAndWait(child: ChildProcess, signal?: NodeJS.Signals, timeoutMs?: number): Promise<boolean>;
62
+ /**
63
+ * Cross-platform spawn option that makes `killProcessTree` reach the whole
64
+ * tree. On Windows no extra flag is needed (taskkill walks the tree).
65
+ */
66
+ export declare function treeKillSpawnOptions(): {
67
+ detached: boolean;
68
+ };
69
+ //# sourceMappingURL=kill-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kill-tree.d.ts","sourceRoot":"","sources":["../src/kill-tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAK9D,sDAAsD;AACtD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAmB,GAAG,IAAI,CAMjE;AAED,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,GAAE,MAAM,CAAC,OAAmB,GAAG,OAAO,CA8BhG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,EACnB,MAAM,GAAE,MAAM,CAAC,OAAmB,EAClC,SAAS,SAAQ,GAChB,OAAO,CAAC,OAAO,CAAC,CAclB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAE5D"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Tree-scoped process termination.
3
+ *
4
+ * `child.kill()` only terminates the direct child; grandchild processes keep
5
+ * running (orphaned). This module kills the whole process tree:
6
+ * - POSIX: child is spawned `detached` (own process group) → `kill(-pid)`
7
+ * - Windows: `taskkill /pid <pid> /T /F`
8
+ *
9
+ * Termination is idempotent: repeated kills for the same pid are no-ops while
10
+ * the kill is still in-flight. The registry is pruned once the tracked process
11
+ * exits (or its TTL elapses) so an OS pid reuse is not silently swallowed.
12
+ *
13
+ * @module sandbox/kill-tree
14
+ * @packageDocumentation
15
+ */
16
+ import { spawn } from "node:child_process";
17
+ const terminatedPids = new Map();
18
+ const KILL_TTL_MS = 30_000;
19
+ /** Clear the idempotency registry (used by tests). */
20
+ export function resetKillTreeState() {
21
+ terminatedPids.clear();
22
+ }
23
+ /**
24
+ * Pids currently tracked as "kill already issued".
25
+ *
26
+ * Exposed for observability/tests so a caller can confirm that a terminated
27
+ * pid is no longer held (i.e. it is safe to kill again after reuse).
28
+ */
29
+ export function killTreeTrackedPids() {
30
+ return [...terminatedPids.keys()];
31
+ }
32
+ /**
33
+ * Prune the idempotency registry: drop pids whose process has exited or whose
34
+ * kill is older than the TTL. Without pruning the registry grows forever and a
35
+ * reused pid is treated as "already killed" — a silent no-op (RV-11).
36
+ *
37
+ * @param now - Clock value to compare the TTL against (injectable for tests).
38
+ */
39
+ export function pruneKillTreeState(now = Date.now()) {
40
+ for (const [pid, killedAt] of terminatedPids) {
41
+ if (!isPidAlive(pid) || now - killedAt > KILL_TTL_MS) {
42
+ terminatedPids.delete(pid);
43
+ }
44
+ }
45
+ }
46
+ /** True when the process (or its process group) is still alive. */
47
+ export function isPidAlive(pid) {
48
+ if (!pid || pid <= 0)
49
+ return false;
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ }
54
+ catch (err) {
55
+ return err.code !== "ESRCH";
56
+ }
57
+ }
58
+ /**
59
+ * Kill a child process and its whole subtree. Idempotent per pid while the
60
+ * kill is in-flight (the registry is pruned once the process exits).
61
+ *
62
+ * @param child - The spawned child process.
63
+ * @param signal - POSIX signal for the direct kill (default "SIGTERM").
64
+ * Windows always uses `taskkill /T /F`.
65
+ * @returns `true` when the kill was issued (or already issued), `false` when
66
+ * there is no pid yet (child not spawned).
67
+ */
68
+ export function killProcessTree(child, signal = "SIGTERM") {
69
+ const pid = child.pid;
70
+ if (!pid || pid <= 0)
71
+ return false;
72
+ pruneKillTreeState();
73
+ if (terminatedPids.has(pid))
74
+ return true;
75
+ terminatedPids.set(pid, Date.now());
76
+ if (process.platform === "win32") {
77
+ spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
78
+ windowsHide: true,
79
+ stdio: "ignore",
80
+ }).unref();
81
+ return true;
82
+ }
83
+ // POSIX: the child was spawned detached → kill the whole process group.
84
+ try {
85
+ process.kill(-pid, signal);
86
+ }
87
+ catch (err) {
88
+ if (err.code === "ESRCH") {
89
+ // Already gone — group kill is best-effort.
90
+ return true;
91
+ }
92
+ try {
93
+ process.kill(pid, signal);
94
+ }
95
+ catch {
96
+ // Already gone.
97
+ }
98
+ }
99
+ return true;
100
+ }
101
+ /**
102
+ * Kill a child process and its whole subtree, then await its exit.
103
+ *
104
+ * This is the awaited form of {@link killProcessTree} (RV-11): on Windows the
105
+ * `taskkill /T` spawn is no longer fire-and-forget — the returned promise
106
+ * resolves only after the child has actually exited, or the given timeout
107
+ * elapses.
108
+ *
109
+ * @param child - The spawned child process.
110
+ * @param signal - POSIX signal for the direct kill (default "SIGTERM").
111
+ * @param timeoutMs - How long to wait for the child to exit (default 5000).
112
+ * @returns `true` when the kill was issued (or already issued), `false` when
113
+ * there is no pid yet (child not spawned).
114
+ */
115
+ export function killProcessTreeAndWait(child, signal = "SIGTERM", timeoutMs = 5_000) {
116
+ const issued = killProcessTree(child, signal);
117
+ if (!issued)
118
+ return Promise.resolve(false);
119
+ if (child.exitCode !== null || child.signalCode !== null)
120
+ return Promise.resolve(true);
121
+ return new Promise((resolve) => {
122
+ const timer = setTimeout(() => resolve(true), timeoutMs);
123
+ const done = () => {
124
+ clearTimeout(timer);
125
+ resolve(true);
126
+ };
127
+ child.once("exit", done);
128
+ child.once("error", done);
129
+ });
130
+ }
131
+ /**
132
+ * Cross-platform spawn option that makes `killProcessTree` reach the whole
133
+ * tree. On Windows no extra flag is needed (taskkill walks the tree).
134
+ */
135
+ export function treeKillSpawnOptions() {
136
+ return { detached: process.platform !== "win32" };
137
+ }
138
+ //# sourceMappingURL=kill-tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kill-tree.js","sourceRoot":"","sources":["../src/kill-tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAE9D,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AACjD,MAAM,WAAW,GAAG,MAAM,CAAC;AAE3B,sDAAsD;AACtD,MAAM,UAAU,kBAAkB;IAChC,cAAc,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,CAAC,GAAG,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACzD,KAAK,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,cAAc,EAAE,CAAC;QAC7C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,QAAQ,GAAG,WAAW,EAAE,CAAC;YACrD,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAQ,GAA6B,CAAC,IAAI,KAAK,OAAO,CAAC;IACzD,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,KAAmB,EAAE,SAAyB,SAAS;IACrF,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;IACtB,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,kBAAkB,EAAE,CAAC;IACrB,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAEpC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,KAAK,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;YACnD,WAAW,EAAE,IAAI;YACjB,KAAK,EAAE,QAAQ;SAChB,CAAC,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,wEAAwE;IACxE,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACpD,4CAA4C;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,gBAAgB;QAClB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CACpC,KAAmB,EACnB,SAAyB,SAAS,EAClC,SAAS,GAAG,KAAK;IAEjB,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3C,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,GAAS,EAAE;YACtB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;AACpD,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shell command parsing utility.
3
+ * Extracts file and args from a command string, handling quotes and escapes.
4
+ */
5
+ export declare function parseCommand(cmd: string): {
6
+ file: string;
7
+ args: string[];
8
+ };
9
+ //# sourceMappingURL=shell-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-parser.d.ts","sourceRoot":"","sources":["../src/shell-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAiC1E"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shell command parsing utility.
3
+ * Extracts file and args from a command string, handling quotes and escapes.
4
+ */
5
+ export function parseCommand(cmd) {
6
+ const trimmed = cmd.trim();
7
+ if (!trimmed)
8
+ return { file: "", args: [] };
9
+ const tokens = [];
10
+ let i = 0;
11
+ let current = "";
12
+ let inSingle = false;
13
+ let inDouble = false;
14
+ while (i < trimmed.length) {
15
+ const ch = trimmed[i] ?? "";
16
+ if (inSingle) {
17
+ if (ch === "'") {
18
+ inSingle = false;
19
+ }
20
+ else {
21
+ current += ch;
22
+ }
23
+ i++;
24
+ }
25
+ else if (inDouble) {
26
+ if (ch === '"') {
27
+ inDouble = false;
28
+ i++;
29
+ }
30
+ else if (ch === "\\" && i + 1 < trimmed.length) {
31
+ current += trimmed[i + 1];
32
+ i += 2;
33
+ }
34
+ else {
35
+ current += ch;
36
+ i++;
37
+ }
38
+ }
39
+ else if (ch === "'") {
40
+ inSingle = true;
41
+ i++;
42
+ }
43
+ else if (ch === '"') {
44
+ inDouble = true;
45
+ i++;
46
+ }
47
+ else if (ch === "\\" && i + 1 < trimmed.length) {
48
+ current += trimmed[i + 1];
49
+ i += 2;
50
+ }
51
+ else if (/\s/.test(ch)) {
52
+ if (current) {
53
+ tokens.push(current);
54
+ current = "";
55
+ }
56
+ i++;
57
+ }
58
+ else {
59
+ current += ch;
60
+ i++;
61
+ }
62
+ }
63
+ if (current)
64
+ tokens.push(current);
65
+ if (tokens.length === 0)
66
+ return { file: "", args: [] };
67
+ return { file: tokens[0], args: tokens.slice(1) };
68
+ }
69
+ //# sourceMappingURL=shell-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-parser.js","sourceRoot":"","sources":["../src/shell-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAE5C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBAAC,QAAQ,GAAG,KAAK,CAAC;YAAC,CAAC;iBAChC,CAAC;gBAAC,OAAO,IAAI,EAAE,CAAC;YAAC,CAAC;YACvB,CAAC,EAAE,CAAC;QACN,CAAC;aAAM,IAAI,QAAQ,EAAE,CAAC;YACpB,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBAAC,QAAQ,GAAG,KAAK,CAAC;gBAAC,CAAC,EAAE,CAAC;YAAC,CAAC;iBACrC,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;gBAAC,OAAO,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAC,CAAC,IAAI,CAAC,CAAC;YAAC,CAAC;iBACjF,CAAC;gBAAC,OAAO,IAAI,EAAE,CAAC;gBAAC,CAAC,EAAE,CAAC;YAAC,CAAC;QAC9B,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YAAC,QAAQ,GAAG,IAAI,CAAC;YAAC,CAAC,EAAE,CAAC;QAAC,CAAC;aAC3C,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YAAC,QAAQ,GAAG,IAAI,CAAC;YAAC,CAAC,EAAE,CAAC;QAAC,CAAC;aACzC,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAAC,OAAO,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAC,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;aACjF,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACvB,IAAI,OAAO,EAAE,CAAC;gBAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;YAAC,CAAC;YACpD,CAAC,EAAE,CAAC;QACN,CAAC;aACI,CAAC;YAAC,OAAO,IAAI,EAAE,CAAC;YAAC,CAAC,EAAE,CAAC;QAAC,CAAC;IAC9B,CAAC;IACD,IAAI,OAAO;QAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAElC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACvD,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAE,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AACrD,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ /**
3
+ * Attach timeout + abort handling to a child process.
4
+ *
5
+ * This is the shared pattern used by all sandbox backends (host, process,
6
+ * container) to enforce execution timeouts and abort signals. It replaces
7
+ * the duplicated `setTimeout` + `killProcessTree` + abort listener logic
8
+ * that was previously copy-pasted across backends.
9
+ *
10
+ * RV-11: the timeout must kill the whole tree, not just the direct child.
11
+ * `execFile`'s built-in `timeout` option only SIGTERMs the spawned pid,
12
+ * orphaning grandchildren — so the timeout is handled here instead.
13
+ *
14
+ * @param child - The spawned child process
15
+ * @param timeoutMs - Maximum execution time in milliseconds
16
+ * @param signal - Optional abort signal
17
+ * @param onAbort - Callback when timeout or abort fires (resolves the promise)
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const child = execFile(file, args, options, callback);
22
+ * withTimeoutAndAbort(child, 30_000, signal, () => {
23
+ * resolve({ result: { stdout: "", stderr: "Aborted", exitCode: 1 }, exitCode: 1, durationMs: Date.now() - start, timedOut: false });
24
+ * });
25
+ * ```
26
+ */
27
+ export declare function withTimeoutAndAbort(child: ChildProcess, timeoutMs: number, signal: AbortSignal | undefined, onAbort: (reason: string) => void): {
28
+ timedOut: boolean;
29
+ clearTimer: () => void;
30
+ };
31
+ //# sourceMappingURL=timeout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../src/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAChC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,IAAI,CAAA;CAAE,CAgC/C"}
@@ -0,0 +1,57 @@
1
+ import { killProcessTree } from "./kill-tree.js";
2
+ /**
3
+ * Attach timeout + abort handling to a child process.
4
+ *
5
+ * This is the shared pattern used by all sandbox backends (host, process,
6
+ * container) to enforce execution timeouts and abort signals. It replaces
7
+ * the duplicated `setTimeout` + `killProcessTree` + abort listener logic
8
+ * that was previously copy-pasted across backends.
9
+ *
10
+ * RV-11: the timeout must kill the whole tree, not just the direct child.
11
+ * `execFile`'s built-in `timeout` option only SIGTERMs the spawned pid,
12
+ * orphaning grandchildren — so the timeout is handled here instead.
13
+ *
14
+ * @param child - The spawned child process
15
+ * @param timeoutMs - Maximum execution time in milliseconds
16
+ * @param signal - Optional abort signal
17
+ * @param onAbort - Callback when timeout or abort fires (resolves the promise)
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const child = execFile(file, args, options, callback);
22
+ * withTimeoutAndAbort(child, 30_000, signal, () => {
23
+ * resolve({ result: { stdout: "", stderr: "Aborted", exitCode: 1 }, exitCode: 1, durationMs: Date.now() - start, timedOut: false });
24
+ * });
25
+ * ```
26
+ */
27
+ export function withTimeoutAndAbort(child, timeoutMs, signal, onAbort) {
28
+ let timedOut = false;
29
+ // RV-11: kill the whole tree, not just the direct child
30
+ const timer = setTimeout(() => {
31
+ timedOut = true;
32
+ killProcessTree(child);
33
+ onAbort("Aborted");
34
+ }, timeoutMs);
35
+ const clearTimer = () => clearTimeout(timer);
36
+ child.once("exit", clearTimer);
37
+ child.once("error", clearTimer);
38
+ if (signal) {
39
+ if (signal.aborted) {
40
+ clearTimeout(timer);
41
+ killProcessTree(child);
42
+ onAbort(signal.reason ?? "Aborted");
43
+ }
44
+ else {
45
+ signal.addEventListener("abort", () => {
46
+ clearTimeout(timer);
47
+ killProcessTree(child);
48
+ onAbort(signal.reason ?? "Aborted");
49
+ }, { once: true });
50
+ }
51
+ }
52
+ return {
53
+ get timedOut() { return timedOut; },
54
+ clearTimer,
55
+ };
56
+ }
57
+ //# sourceMappingURL=timeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout.js","sourceRoot":"","sources":["../src/timeout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAmB,EACnB,SAAiB,EACjB,MAA+B,EAC/B,OAAiC;IAEjC,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,wDAAwD;IACxD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,QAAQ,GAAG,IAAI,CAAC;QAChB,eAAe,CAAC,KAAK,CAAC,CAAC;QACvB,OAAO,CAAC,SAAS,CAAC,CAAC;IACrB,CAAC,EAAE,SAAS,CAAC,CAAC;IAEd,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC/B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAEhC,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,eAAe,CAAC,KAAK,CAAC,CAAC;YACvB,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBACpC,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,eAAe,CAAC,KAAK,CAAC,CAAC;gBACvB,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC;YACtC,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,QAAQ,KAAK,OAAO,QAAQ,CAAC,CAAC,CAAC;QACnC,UAAU;KACsC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,68 @@
1
+ /** Sandbox execution scope — string type, NOT closed union */
2
+ export type SandboxScope = string;
3
+ /**
4
+ * Default sandbox scopes — exported for convenience.
5
+ * Backends register custom scopes via the `createSandbox` backends map.
6
+ */
7
+ export declare const KNOWN_SANDBOX_SCOPES: readonly ["host", "process", "container"];
8
+ /** Sandbox configuration */
9
+ export interface SandboxConfig {
10
+ defaultTimeoutMs: number;
11
+ /** Execution scope (default: "host" — no isolation) */
12
+ scope?: SandboxScope;
13
+ /** Allowed filesystem paths (for "process" scope) */
14
+ allowedPaths?: string[];
15
+ /** Blocked filesystem paths */
16
+ blockedPaths?: string[];
17
+ /** Allow network access (default: true) */
18
+ allowNetwork?: boolean;
19
+ /** Allowed environment variables */
20
+ allowedEnvVars?: string[];
21
+ /** Allowed commands (for "process" scope) */
22
+ allowedCommands?: Set<string>;
23
+ /** Enable Node.js Permission Model (Node 22+) */
24
+ enablePermissionModel?: boolean;
25
+ readonly metadata?: Record<string, unknown>;
26
+ }
27
+ /** Result of a sandboxed execution */
28
+ export interface SandboxResult<T = unknown> {
29
+ result: T;
30
+ exitCode: number;
31
+ durationMs: number;
32
+ timedOut: boolean;
33
+ /** Resources consumed during execution */
34
+ resources?: {
35
+ cpuTimeMs?: number;
36
+ memoryBytes?: number;
37
+ };
38
+ readonly metadata?: Record<string, unknown>;
39
+ }
40
+ /** Options for a sandboxed command execution */
41
+ export interface ProcessSandboxExecuteOptions {
42
+ command: string;
43
+ cwd: string;
44
+ timeoutMs: number;
45
+ env?: Record<string, string>;
46
+ signal?: AbortSignal;
47
+ }
48
+ /**
49
+ * Interface for sandbox execution adapters.
50
+ *
51
+ * Implementations can provide different levels of isolation:
52
+ * - `host`: No isolation
53
+ * - `process`: Node.js Permission Model + empty env + command allowlist
54
+ * - `container`: Docker/Firecracker microVM
55
+ */
56
+ export interface ProcessSandbox {
57
+ /** The scope this sandbox provides */
58
+ readonly scope: SandboxScope;
59
+ /** Execute a command in the sandbox */
60
+ execute(options: ProcessSandboxExecuteOptions): Promise<SandboxResult<{
61
+ stdout: string;
62
+ stderr: string;
63
+ exitCode: number;
64
+ }>>;
65
+ /** Clean up resources */
66
+ destroy(): Promise<void>;
67
+ }
68
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC;;;GAGG;AACH,eAAO,MAAM,oBAAoB,2CAA4C,CAAC;AAE9E,4BAA4B;AAC5B,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,uDAAuD;IACvD,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oCAAoC;IACpC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,iDAAiD;IACjD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,MAAM,EAAE,CAAC,CAAC;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,SAAS,CAAC,EAAE;QACV,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C;AAED,gDAAgD;AAChD,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAE7B,uCAAuC;IACvC,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAE7H,yBAAyB;IACzB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Default sandbox scopes — exported for convenience.
3
+ * Backends register custom scopes via the `createSandbox` backends map.
4
+ */
5
+ export const KNOWN_SANDBOX_SCOPES = ["host", "process", "container"];
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAU,CAAC"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@vinhnt-sdk/sandbox",
3
+ "version": "0.1.3",
4
+ "description": "Sandbox primitives for VNT Agent — ProcessSandbox interface, config/result types, fail-closed SandboxUnavailableError, command parsing and tree-scoped process termination",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "sideEffects": false,
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/vinhnt-develop/vinhnt-sdk.git",
29
+ "directory": "packages/sandbox"
30
+ },
31
+ "homepage": "https://github.com/vinhnt-develop/vinhnt-sdk/tree/main/packages/sandbox",
32
+ "bugs": {
33
+ "url": "https://github.com/vinhnt-develop/vinhnt-sdk/issues"
34
+ },
35
+ "keywords": [
36
+ "vnt",
37
+ "sandbox",
38
+ "process-isolation",
39
+ "security",
40
+ "shell"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@vinhnt-sdk/schema": "0.1.3",
47
+ "@vinhnt-sdk/security": "0.1.3"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -b",
51
+ "typecheck": "tsc --noEmit",
52
+ "clean": "node -e \"try{require('fs').rmSync('dist',{recursive:true,force:true})}catch(e){}\" && node -e \"try{require('fs').rmSync('tsconfig.tsbuildinfo',{force:true})}catch(e){}\"",
53
+ "test": "vitest run",
54
+ "lint": "eslint ."
55
+ }
56
+ }