@ggui-ai/sandbox 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +99 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/run.d.ts +8 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +428 -0
- package/dist/spawner.d.ts +19 -0
- package/dist/spawner.d.ts.map +1 -0
- package/dist/spawner.js +1 -0
- package/dist/types.d.ts +253 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Support. While redistributing the Work or
|
|
166
|
+
Derivative Works thereof, You may choose to offer, and charge a
|
|
167
|
+
fee for, acceptance of support, warranty, indemnity, or other
|
|
168
|
+
liability obligations and/or rights consistent with this License.
|
|
169
|
+
However, in accepting such obligations, You may act only on Your
|
|
170
|
+
own behalf and on Your sole responsibility, not on behalf of any
|
|
171
|
+
other Contributor, and only if You agree to indemnify, defend,
|
|
172
|
+
and hold each Contributor harmless for any liability incurred by,
|
|
173
|
+
or claims asserted against, such Contributor by reason of your
|
|
174
|
+
accepting any such warranty or support.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Loqu, Inc. (Guuey)
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
200
|
+
implied. See the License for the specific language governing
|
|
201
|
+
permissions and limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @ggui-ai/sandbox
|
|
2
|
+
|
|
3
|
+
Bounded process-isolation runner for untrusted Node subprocesses.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { runSandboxed } from "@ggui-ai/sandbox";
|
|
9
|
+
|
|
10
|
+
const result = await runSandboxed({
|
|
11
|
+
command: process.execPath, // node
|
|
12
|
+
args: ["./build-probe.js"],
|
|
13
|
+
timeoutMs: 15_000,
|
|
14
|
+
env: { NODE_ENV: "production" }, // parent process.env NEVER merged
|
|
15
|
+
maxStdoutBytes: 4 * 1024 * 1024, // 4 MiB cap
|
|
16
|
+
nodeHeapMb: 256,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (result.outcome === "exit" && result.exitCode === 0) {
|
|
20
|
+
// result.stdout + result.stderr captured, child gone, tmpdir cleaned.
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it actually enforces (MVP)
|
|
25
|
+
|
|
26
|
+
One subprocess, one bounded run, one pinned outcome. Portably, from pure Node user-space:
|
|
27
|
+
|
|
28
|
+
- **Process boundary.** `spawn` with `shell: false`, `detached: false`, `windowsHide: true`, `stdio: ['pipe','pipe','pipe']`. Child cannot attach to the parent's controlling terminal, cannot fork into a new process group, cannot inherit open fds.
|
|
29
|
+
- **Working-directory isolation.** Caller supplies an absolute `cwd`, or the sandbox mints an owned `mkdtempSync` dir and removes it at the end of the run. Relative `cwd` values are rejected at validation — no silent resolve against the parent's CWD.
|
|
30
|
+
- **Environment allowlist.** The parent's `process.env` is **never** merged in. Only the caller's `env` keys plus a minimal bootstrap (`PATH`, `HOME`, `TMPDIR` when present on the parent) reach the child. Callers that want to forward extra vars copy them in explicitly.
|
|
31
|
+
- **Wall-clock timeout.** `timeoutMs` is required. On overrun the sandbox sends `SIGTERM`, waits `gracePeriodMs`, then escalates to `SIGKILL`. Outcome is `'timeout'`.
|
|
32
|
+
- **Output byte caps.** `stdout` and `stderr` are captured up to `maxStdoutBytes` / `maxStderrBytes` (defaults: 8 MiB / 1 MiB). Exceeding the cap terminates the child with outcome `'overflow-stdout'` / `'overflow-stderr'` and the captured output is truncated to exactly the cap.
|
|
33
|
+
- **No stdin leakage.** Absent `stdin` → closed immediately. Present → written then closed. Parent stdin is never forwarded.
|
|
34
|
+
- **V8 heap cap (Node children only).** When `nodeHeapMb` is set AND the command basename is `node` / equals `process.execPath`, the sandbox prepends `--max-old-space-size=<mb>` to `NODE_OPTIONS`. Caps V8's old-generation heap only.
|
|
35
|
+
|
|
36
|
+
Every decision path funnels through a single `finish()` closure — no fd, timer, or tmpdir leaks regardless of which termination path fires first.
|
|
37
|
+
|
|
38
|
+
## What it does NOT enforce
|
|
39
|
+
|
|
40
|
+
All of the following need OS-level primitives (network namespaces, seccomp, cgroups, chroot/Landlock) that aren't portable from Node user-space. Consumers who need these run the sandbox under a stronger layer (Docker `network:none`, gVisor, firecracker, systemd with the right ambient config):
|
|
41
|
+
|
|
42
|
+
- **Network egress blocking.** Not enforced. The child has the same network access as the parent.
|
|
43
|
+
- **Filesystem read boundaries.** Not enforced. The child runs as the parent's UID/GID and can read anything the parent can. Relative paths resolve under `cwd`; absolute paths and `..` traversal remain reachable.
|
|
44
|
+
- **CPU share / scheduling cap.** Not enforced. Node has no portable rlimit surface for CPU time. Total RSS is not capped either — only V8's old-gen heap (and only when the child is Node).
|
|
45
|
+
- **Syscall filtering.** Not enforced. No seccomp, no LSM hooks. The child can make any syscall the parent could.
|
|
46
|
+
- **Fork-bomb / grandchild containment.** Not enforced. The sandbox kills only its direct child; descendants that reparent (the classic daemon / double-fork trick) survive.
|
|
47
|
+
|
|
48
|
+
If the threat model requires any of these guarantees, **do not rely on this package alone**. The sandbox is the portable MVP; stacking it under Docker / gVisor / firecracker is the production posture, not a "later" improvement.
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
runSandboxed(opts: SandboxOptions): Promise<SandboxResult>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `SandboxOptions`
|
|
57
|
+
|
|
58
|
+
| Field | Required | Default | Notes |
|
|
59
|
+
| ---------------- | -------- | ------------ | --------------------------------------------------------- |
|
|
60
|
+
| `command` | ✅ | — | Absolute path to the executable. No shell interpretation. |
|
|
61
|
+
| `args` | ✅ | — | Forwarded verbatim. Pass `[]` for no args. |
|
|
62
|
+
| `cwd` | ❌ | owned tmpdir | Must be absolute when supplied. |
|
|
63
|
+
| `env` | ❌ | `{}` | Allowlist. Parent's `process.env` is NEVER merged. |
|
|
64
|
+
| `timeoutMs` | ✅ | — | Positive finite integer. No "infinity". |
|
|
65
|
+
| `shutdownSignal` | ❌ | `'SIGTERM'` | Soft-kill signal; SIGKILL escalation is unconditional. |
|
|
66
|
+
| `gracePeriodMs` | ❌ | `2000` | Must be `< timeoutMs`. |
|
|
67
|
+
| `stdin` | ❌ | closed | `string \| Uint8Array`. Absent = immediate EOF. |
|
|
68
|
+
| `maxStdoutBytes` | ❌ | 8 MiB | Positive integer. Overflow → `'overflow-stdout'`. |
|
|
69
|
+
| `maxStderrBytes` | ❌ | 1 MiB | Positive integer. Overflow → `'overflow-stderr'`. |
|
|
70
|
+
| `nodeHeapMb` | ❌ | — | Node children only. Sets `--max-old-space-size`. |
|
|
71
|
+
| `signal` | ❌ | — | External `AbortSignal` → outcome `'canceled'`. |
|
|
72
|
+
| `spawner` | ❌ | real `spawn` | Test seam; production leaves unset. |
|
|
73
|
+
|
|
74
|
+
### `SandboxResult`
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
interface SandboxResult {
|
|
78
|
+
outcome: "exit" | "timeout" | "canceled" | "overflow-stdout" | "overflow-stderr" | "spawn-error";
|
|
79
|
+
exitCode: number | null; // present only on 'exit'
|
|
80
|
+
signal: NodeJS.Signals | null; // present only on 'exit'
|
|
81
|
+
stdout: string; // UTF-8, truncated to maxStdoutBytes
|
|
82
|
+
stderr: string; // UTF-8, truncated to maxStderrBytes
|
|
83
|
+
durationMs: number;
|
|
84
|
+
stdoutTruncated: boolean;
|
|
85
|
+
stderrTruncated: boolean;
|
|
86
|
+
cwd: string; // absolute
|
|
87
|
+
cwdOwnedBySandbox: boolean; // true → already cleaned up
|
|
88
|
+
nodeHeapMbApplied: boolean;
|
|
89
|
+
errorMessage: string; // non-empty only on 'spawn-error'
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Typical use
|
|
94
|
+
|
|
95
|
+
The UI-gen render-probe path uses this package so LLM-generated TSX never executes in the parent Node process — each render spawns a subprocess through `runSandboxed` with a short timeout, a bounded stdout cap, a V8 heap cap, a sandbox-owned tmpdir cwd, and an env allowlist forwarding only `NODE_ENV`.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
Apache 2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/sandbox` — bounded process-isolation runner for
|
|
3
|
+
* OSS UI-gen workloads.
|
|
4
|
+
*
|
|
5
|
+
* Public surface:
|
|
6
|
+
*
|
|
7
|
+
* - {@link runSandboxed} — one-shot runner. Spawns a subprocess,
|
|
8
|
+
* enforces timeout + output caps + env allowlist + cwd
|
|
9
|
+
* isolation, returns a {@link SandboxResult} with captured
|
|
10
|
+
* output + terminal outcome.
|
|
11
|
+
*
|
|
12
|
+
* - {@link SandboxOptions} / {@link SandboxResult} /
|
|
13
|
+
* {@link SandboxOutcome} — pinned types.
|
|
14
|
+
*
|
|
15
|
+
* For the honest security boundary + what this MVP does NOT enforce,
|
|
16
|
+
* see the JSDoc header on `./types.ts` and the package README.
|
|
17
|
+
*/
|
|
18
|
+
export { runSandboxed } from './run.js';
|
|
19
|
+
export type { SandboxOptions, SandboxOutcome, SandboxResult, } from './types.js';
|
|
20
|
+
export type { Spawner, SpawnerOptions } from './spawner.js';
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/sandbox` — bounded process-isolation runner for
|
|
3
|
+
* OSS UI-gen workloads.
|
|
4
|
+
*
|
|
5
|
+
* Public surface:
|
|
6
|
+
*
|
|
7
|
+
* - {@link runSandboxed} — one-shot runner. Spawns a subprocess,
|
|
8
|
+
* enforces timeout + output caps + env allowlist + cwd
|
|
9
|
+
* isolation, returns a {@link SandboxResult} with captured
|
|
10
|
+
* output + terminal outcome.
|
|
11
|
+
*
|
|
12
|
+
* - {@link SandboxOptions} / {@link SandboxResult} /
|
|
13
|
+
* {@link SandboxOutcome} — pinned types.
|
|
14
|
+
*
|
|
15
|
+
* For the honest security boundary + what this MVP does NOT enforce,
|
|
16
|
+
* see the JSDoc header on `./types.ts` and the package README.
|
|
17
|
+
*/
|
|
18
|
+
export { runSandboxed } from './run.js';
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SandboxOptions, SandboxResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run a command in a bounded subprocess. See {@link SandboxOptions}
|
|
4
|
+
* and the `./types.ts` header for the full semantics + honest-boundary
|
|
5
|
+
* lock.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runSandboxed(opts: SandboxOptions): Promise<SandboxResult>;
|
|
8
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACV,cAAc,EAEd,aAAa,EACd,MAAM,YAAY,CAAC;AAepB;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,aAAa,CAAC,CA2PxB"}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runSandboxed` — bounded subprocess runner.
|
|
3
|
+
*
|
|
4
|
+
* Single code path per outcome. The state machine is intentionally
|
|
5
|
+
* small:
|
|
6
|
+
*
|
|
7
|
+
* starting → (spawn error) → 'spawn-error'
|
|
8
|
+
* → (child exits) → 'exit'
|
|
9
|
+
* → (timeout fires) → kill → 'timeout'
|
|
10
|
+
* → (signal aborts) → kill → 'canceled'
|
|
11
|
+
* → (stdout overflow) → kill → 'overflow-stdout'
|
|
12
|
+
* → (stderr overflow) → kill → 'overflow-stderr'
|
|
13
|
+
*
|
|
14
|
+
* Once an outcome is decided, the runner:
|
|
15
|
+
*
|
|
16
|
+
* 1. stops accepting further outcome transitions (`outcomeDecided`),
|
|
17
|
+
* 2. if the child is still alive, sends `shutdownSignal`,
|
|
18
|
+
* 3. after `gracePeriodMs` escalates to `SIGKILL`,
|
|
19
|
+
* 4. waits for the 'exit' event to flush remaining stdio,
|
|
20
|
+
* 5. decodes captured buffers as UTF-8, truncated to their caps,
|
|
21
|
+
* 6. cleans up the owned tmpdir if one was created,
|
|
22
|
+
* 7. resolves the promise with a full `SandboxResult`.
|
|
23
|
+
*
|
|
24
|
+
* The runner resolves exactly once. All cleanup paths funnel through
|
|
25
|
+
* a single `finish()` closure so we can't leak a file descriptor,
|
|
26
|
+
* timer, or tmpdir no matter which path fired first.
|
|
27
|
+
*/
|
|
28
|
+
import { spawn } from 'node:child_process';
|
|
29
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
import { basename, isAbsolute, join } from 'node:path';
|
|
32
|
+
/**
|
|
33
|
+
* Default wall-clock grace between `shutdownSignal` and `SIGKILL`.
|
|
34
|
+
* 2 seconds covers typical Node child shutdown; a hostile / stuck
|
|
35
|
+
* child hits the SIGKILL escalation deterministically.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_GRACE_PERIOD_MS = 2_000;
|
|
38
|
+
/** Default stdout budget — 8 MiB. */
|
|
39
|
+
const DEFAULT_MAX_STDOUT_BYTES = 8 * 1024 * 1024;
|
|
40
|
+
/** Default stderr budget — 1 MiB. */
|
|
41
|
+
const DEFAULT_MAX_STDERR_BYTES = 1 * 1024 * 1024;
|
|
42
|
+
/**
|
|
43
|
+
* Run a command in a bounded subprocess. See {@link SandboxOptions}
|
|
44
|
+
* and the `./types.ts` header for the full semantics + honest-boundary
|
|
45
|
+
* lock.
|
|
46
|
+
*/
|
|
47
|
+
export async function runSandboxed(opts) {
|
|
48
|
+
// ── 1. Validate inputs ───────────────────────────────────────────
|
|
49
|
+
validateOptions(opts);
|
|
50
|
+
const gracePeriodMs = opts.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
|
|
51
|
+
const maxStdoutBytes = opts.maxStdoutBytes ?? DEFAULT_MAX_STDOUT_BYTES;
|
|
52
|
+
const maxStderrBytes = opts.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES;
|
|
53
|
+
const shutdownSignal = opts.shutdownSignal ?? 'SIGTERM';
|
|
54
|
+
if (gracePeriodMs >= opts.timeoutMs) {
|
|
55
|
+
throw new RangeError(`runSandboxed: gracePeriodMs (${gracePeriodMs}) must be < timeoutMs (${opts.timeoutMs}) so SIGTERM has time to take effect before the sandbox considers the child stuck.`);
|
|
56
|
+
}
|
|
57
|
+
const start = Date.now();
|
|
58
|
+
// ── 2. Resolve cwd (owned tmpdir or caller-supplied absolute) ────
|
|
59
|
+
let cwd;
|
|
60
|
+
let cwdOwnedBySandbox = false;
|
|
61
|
+
if (opts.cwd !== undefined) {
|
|
62
|
+
cwd = opts.cwd;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
cwd = mkdtempSync(join(tmpdir(), 'ggui-sandbox-'));
|
|
66
|
+
cwdOwnedBySandbox = true;
|
|
67
|
+
}
|
|
68
|
+
// ── 3. Resolve env + detect Node child for heap cap ──────────────
|
|
69
|
+
const { env, nodeHeapMbApplied } = resolveEnv(opts);
|
|
70
|
+
// ── 4. Pre-spawn guard: caller already aborted ───────────────────
|
|
71
|
+
if (opts.signal?.aborted) {
|
|
72
|
+
cleanupCwd(cwd, cwdOwnedBySandbox);
|
|
73
|
+
return {
|
|
74
|
+
outcome: 'canceled',
|
|
75
|
+
exitCode: null,
|
|
76
|
+
signal: null,
|
|
77
|
+
stdout: '',
|
|
78
|
+
stderr: '',
|
|
79
|
+
durationMs: 0,
|
|
80
|
+
stdoutTruncated: false,
|
|
81
|
+
stderrTruncated: false,
|
|
82
|
+
cwd,
|
|
83
|
+
cwdOwnedBySandbox,
|
|
84
|
+
nodeHeapMbApplied,
|
|
85
|
+
errorMessage: '',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ── 5. Spawn the child ──────────────────────────────────────────
|
|
89
|
+
const spawner = opts.spawner ??
|
|
90
|
+
((cmd, args, spawnOpts) => spawn(cmd, args, {
|
|
91
|
+
cwd: spawnOpts.cwd,
|
|
92
|
+
env: spawnOpts.env,
|
|
93
|
+
stdio: spawnOpts.stdio,
|
|
94
|
+
shell: spawnOpts.shell,
|
|
95
|
+
detached: spawnOpts.detached,
|
|
96
|
+
windowsHide: spawnOpts.windowsHide,
|
|
97
|
+
}));
|
|
98
|
+
let child;
|
|
99
|
+
try {
|
|
100
|
+
child = spawner(opts.command, opts.args, {
|
|
101
|
+
cwd,
|
|
102
|
+
env,
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
+
shell: false,
|
|
105
|
+
detached: false,
|
|
106
|
+
windowsHide: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
cleanupCwd(cwd, cwdOwnedBySandbox);
|
|
111
|
+
return {
|
|
112
|
+
outcome: 'spawn-error',
|
|
113
|
+
exitCode: null,
|
|
114
|
+
signal: null,
|
|
115
|
+
stdout: '',
|
|
116
|
+
stderr: '',
|
|
117
|
+
durationMs: Date.now() - start,
|
|
118
|
+
stdoutTruncated: false,
|
|
119
|
+
stderrTruncated: false,
|
|
120
|
+
cwd,
|
|
121
|
+
cwdOwnedBySandbox,
|
|
122
|
+
nodeHeapMbApplied,
|
|
123
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// ── 6. Supervise the child ──────────────────────────────────────
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const stdoutChunks = [];
|
|
129
|
+
const stderrChunks = [];
|
|
130
|
+
let stdoutBytes = 0;
|
|
131
|
+
let stderrBytes = 0;
|
|
132
|
+
let stdoutTruncated = false;
|
|
133
|
+
let stderrTruncated = false;
|
|
134
|
+
let outcome = 'exit'; // Overwritten on every
|
|
135
|
+
let outcomeDecided = false; // decision path
|
|
136
|
+
let errorMessage = '';
|
|
137
|
+
let killTimer = null;
|
|
138
|
+
let onAbort = null;
|
|
139
|
+
// Wall-clock timer — runs from spawn. Firing decides 'timeout'.
|
|
140
|
+
const timeoutTimer = setTimeout(() => decide('timeout', ''), opts.timeoutMs);
|
|
141
|
+
timeoutTimer.unref?.();
|
|
142
|
+
// External abort signal — fires 'canceled' when parent wants to
|
|
143
|
+
// cancel mid-run.
|
|
144
|
+
if (opts.signal) {
|
|
145
|
+
onAbort = () => decide('canceled', '');
|
|
146
|
+
opts.signal.addEventListener('abort', onAbort, { once: true });
|
|
147
|
+
}
|
|
148
|
+
// stdin — close immediately when absent, else write-then-close.
|
|
149
|
+
// Errors here are non-fatal — the child may close its stdin
|
|
150
|
+
// before we finish writing, which is fine.
|
|
151
|
+
if (opts.stdin !== undefined && child.stdin) {
|
|
152
|
+
try {
|
|
153
|
+
child.stdin.end(opts.stdin);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
/* child stdin already closed — not our problem */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
child.stdin?.end();
|
|
161
|
+
}
|
|
162
|
+
const onStdout = (chunk) => {
|
|
163
|
+
if (outcomeDecided)
|
|
164
|
+
return;
|
|
165
|
+
const remaining = maxStdoutBytes - stdoutBytes;
|
|
166
|
+
if (chunk.length <= remaining) {
|
|
167
|
+
stdoutChunks.push(chunk);
|
|
168
|
+
stdoutBytes += chunk.length;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Overflow — accept only the remaining bytes, then decide.
|
|
172
|
+
if (remaining > 0) {
|
|
173
|
+
stdoutChunks.push(chunk.subarray(0, remaining));
|
|
174
|
+
stdoutBytes += remaining;
|
|
175
|
+
}
|
|
176
|
+
stdoutTruncated = true;
|
|
177
|
+
decide('overflow-stdout', '');
|
|
178
|
+
};
|
|
179
|
+
const onStderr = (chunk) => {
|
|
180
|
+
if (outcomeDecided)
|
|
181
|
+
return;
|
|
182
|
+
const remaining = maxStderrBytes - stderrBytes;
|
|
183
|
+
if (chunk.length <= remaining) {
|
|
184
|
+
stderrChunks.push(chunk);
|
|
185
|
+
stderrBytes += chunk.length;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (remaining > 0) {
|
|
189
|
+
stderrChunks.push(chunk.subarray(0, remaining));
|
|
190
|
+
stderrBytes += remaining;
|
|
191
|
+
}
|
|
192
|
+
stderrTruncated = true;
|
|
193
|
+
decide('overflow-stderr', '');
|
|
194
|
+
};
|
|
195
|
+
child.stdout?.on('data', onStdout);
|
|
196
|
+
child.stderr?.on('data', onStderr);
|
|
197
|
+
child.on('error', (err) => {
|
|
198
|
+
// Post-spawn error. When the child never gets a pid (ENOENT on
|
|
199
|
+
// Linux surfaces async via 'error' with no 'exit' follow-up),
|
|
200
|
+
// 'exit' will not fire — we must resolve from here. Per Node's
|
|
201
|
+
// docs, 'exit' MAY fire after 'error' but isn't guaranteed.
|
|
202
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
203
|
+
if (child.pid === undefined && !outcomeDecided) {
|
|
204
|
+
outcomeDecided = true;
|
|
205
|
+
clearTimeout(timeoutTimer);
|
|
206
|
+
if (onAbort && opts.signal) {
|
|
207
|
+
opts.signal.removeEventListener('abort', onAbort);
|
|
208
|
+
onAbort = null;
|
|
209
|
+
}
|
|
210
|
+
cleanupCwd(cwd, cwdOwnedBySandbox);
|
|
211
|
+
resolve({
|
|
212
|
+
outcome: 'spawn-error',
|
|
213
|
+
exitCode: null,
|
|
214
|
+
signal: null,
|
|
215
|
+
stdout: decodeUpTo(stdoutChunks, maxStdoutBytes),
|
|
216
|
+
stderr: decodeUpTo(stderrChunks, maxStderrBytes),
|
|
217
|
+
durationMs: Date.now() - start,
|
|
218
|
+
stdoutTruncated,
|
|
219
|
+
stderrTruncated,
|
|
220
|
+
cwd,
|
|
221
|
+
cwdOwnedBySandbox,
|
|
222
|
+
nodeHeapMbApplied,
|
|
223
|
+
errorMessage: message,
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
decide('spawn-error', message);
|
|
228
|
+
});
|
|
229
|
+
const decide = (next, message) => {
|
|
230
|
+
if (outcomeDecided)
|
|
231
|
+
return;
|
|
232
|
+
outcomeDecided = true;
|
|
233
|
+
outcome = next;
|
|
234
|
+
errorMessage = message;
|
|
235
|
+
// If the child is still alive, request graceful shutdown. The
|
|
236
|
+
// real terminal state ('stopped') is observed on the 'exit'
|
|
237
|
+
// event below.
|
|
238
|
+
killChild(child, shutdownSignal, gracePeriodMs, (timer) => {
|
|
239
|
+
killTimer = timer;
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
child.on('exit', (code, signal) => {
|
|
243
|
+
// Flush any buffered output (the 'data' listener handles it
|
|
244
|
+
// live, but the kernel can deliver final chunks after exit on
|
|
245
|
+
// some platforms). Node's ChildProcess already emits all
|
|
246
|
+
// 'data' before 'exit', so we just decode.
|
|
247
|
+
if (killTimer) {
|
|
248
|
+
clearTimeout(killTimer);
|
|
249
|
+
killTimer = null;
|
|
250
|
+
}
|
|
251
|
+
clearTimeout(timeoutTimer);
|
|
252
|
+
if (onAbort && opts.signal) {
|
|
253
|
+
opts.signal.removeEventListener('abort', onAbort);
|
|
254
|
+
onAbort = null;
|
|
255
|
+
}
|
|
256
|
+
// If no one has decided yet, the child exited on its own.
|
|
257
|
+
if (!outcomeDecided) {
|
|
258
|
+
outcomeDecided = true;
|
|
259
|
+
outcome = 'exit';
|
|
260
|
+
}
|
|
261
|
+
const stdout = decodeUpTo(stdoutChunks, maxStdoutBytes);
|
|
262
|
+
const stderr = decodeUpTo(stderrChunks, maxStderrBytes);
|
|
263
|
+
cleanupCwd(cwd, cwdOwnedBySandbox);
|
|
264
|
+
resolve({
|
|
265
|
+
outcome,
|
|
266
|
+
exitCode: outcome === 'exit' ? code : null,
|
|
267
|
+
signal: outcome === 'exit' ? signal : null,
|
|
268
|
+
stdout,
|
|
269
|
+
stderr,
|
|
270
|
+
durationMs: Date.now() - start,
|
|
271
|
+
stdoutTruncated,
|
|
272
|
+
stderrTruncated,
|
|
273
|
+
cwd,
|
|
274
|
+
cwdOwnedBySandbox,
|
|
275
|
+
nodeHeapMbApplied,
|
|
276
|
+
errorMessage,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Input validation — throws synchronously on invalid configuration
|
|
283
|
+
* so callers find bugs at the call site, not as a confusing 'exit'
|
|
284
|
+
* outcome with an empty stderr.
|
|
285
|
+
*/
|
|
286
|
+
function validateOptions(opts) {
|
|
287
|
+
if (!opts.command || typeof opts.command !== 'string') {
|
|
288
|
+
throw new TypeError('runSandboxed: `command` must be a non-empty string');
|
|
289
|
+
}
|
|
290
|
+
if (!Array.isArray(opts.args)) {
|
|
291
|
+
throw new TypeError('runSandboxed: `args` must be an array');
|
|
292
|
+
}
|
|
293
|
+
if (typeof opts.timeoutMs !== 'number' ||
|
|
294
|
+
!Number.isFinite(opts.timeoutMs) ||
|
|
295
|
+
opts.timeoutMs <= 0 ||
|
|
296
|
+
!Number.isInteger(opts.timeoutMs)) {
|
|
297
|
+
throw new RangeError('runSandboxed: `timeoutMs` must be a positive finite integer');
|
|
298
|
+
}
|
|
299
|
+
if (opts.cwd !== undefined && !isAbsolute(opts.cwd)) {
|
|
300
|
+
throw new TypeError(`runSandboxed: \`cwd\` must be an absolute path, got ${JSON.stringify(opts.cwd)}`);
|
|
301
|
+
}
|
|
302
|
+
if (opts.maxStdoutBytes !== undefined &&
|
|
303
|
+
(opts.maxStdoutBytes <= 0 || !Number.isInteger(opts.maxStdoutBytes))) {
|
|
304
|
+
throw new RangeError('runSandboxed: `maxStdoutBytes` must be a positive integer');
|
|
305
|
+
}
|
|
306
|
+
if (opts.maxStderrBytes !== undefined &&
|
|
307
|
+
(opts.maxStderrBytes <= 0 || !Number.isInteger(opts.maxStderrBytes))) {
|
|
308
|
+
throw new RangeError('runSandboxed: `maxStderrBytes` must be a positive integer');
|
|
309
|
+
}
|
|
310
|
+
if (opts.gracePeriodMs !== undefined && opts.gracePeriodMs < 0) {
|
|
311
|
+
throw new RangeError('runSandboxed: `gracePeriodMs` must be >= 0');
|
|
312
|
+
}
|
|
313
|
+
if (opts.nodeHeapMb !== undefined &&
|
|
314
|
+
(opts.nodeHeapMb <= 0 || !Number.isInteger(opts.nodeHeapMb))) {
|
|
315
|
+
throw new RangeError('runSandboxed: `nodeHeapMb` must be a positive integer');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Resolve the child's environment.
|
|
320
|
+
*
|
|
321
|
+
* The parent's `process.env` is NEVER merged in. Only three things
|
|
322
|
+
* reach the child:
|
|
323
|
+
*
|
|
324
|
+
* 1. A minimal bootstrap (`PATH`, `HOME`, and `TMPDIR` when
|
|
325
|
+
* present on the parent). `PATH` is required or the kernel
|
|
326
|
+
* cannot locate the command; `HOME` is required for most Node
|
|
327
|
+
* shutdown paths (e.g. npm config); `TMPDIR` lets child code
|
|
328
|
+
* that writes scratch files respect the parent's temp location.
|
|
329
|
+
* Callers can override any of these by declaring the key
|
|
330
|
+
* explicitly in `opts.env`.
|
|
331
|
+
*
|
|
332
|
+
* 2. Every key from `opts.env` (verbatim — parent values NEVER
|
|
333
|
+
* leak).
|
|
334
|
+
*
|
|
335
|
+
* 3. `NODE_OPTIONS=--max-old-space-size=<nodeHeapMb>` when the
|
|
336
|
+
* child is a Node process AND `opts.nodeHeapMb` is set.
|
|
337
|
+
* Merges with any `NODE_OPTIONS` the caller supplied.
|
|
338
|
+
*/
|
|
339
|
+
function resolveEnv(opts) {
|
|
340
|
+
const env = {};
|
|
341
|
+
// Bootstrap — only the three keys that matter for the child to
|
|
342
|
+
// locate its binary, have a home dir, and know where to scratch.
|
|
343
|
+
const bootstrapKeys = ['PATH', 'HOME', 'TMPDIR'];
|
|
344
|
+
for (const key of bootstrapKeys) {
|
|
345
|
+
const parentValue = process.env[key];
|
|
346
|
+
if (parentValue !== undefined)
|
|
347
|
+
env[key] = parentValue;
|
|
348
|
+
}
|
|
349
|
+
// Explicit allowlist — callers OVERRIDE bootstrap values if they
|
|
350
|
+
// declare the key themselves. Shallow merge; no special handling.
|
|
351
|
+
if (opts.env) {
|
|
352
|
+
for (const [key, value] of Object.entries(opts.env)) {
|
|
353
|
+
env[key] = value;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Node heap cap — only applied for Node children.
|
|
357
|
+
let nodeHeapMbApplied = false;
|
|
358
|
+
if (opts.nodeHeapMb !== undefined && isNodeCommand(opts.command)) {
|
|
359
|
+
const flag = `--max-old-space-size=${opts.nodeHeapMb}`;
|
|
360
|
+
const existing = env.NODE_OPTIONS;
|
|
361
|
+
env.NODE_OPTIONS = existing ? `${existing} ${flag}` : flag;
|
|
362
|
+
nodeHeapMbApplied = true;
|
|
363
|
+
}
|
|
364
|
+
return { env, nodeHeapMbApplied };
|
|
365
|
+
}
|
|
366
|
+
function isNodeCommand(command) {
|
|
367
|
+
if (command === process.execPath)
|
|
368
|
+
return true;
|
|
369
|
+
const base = basename(command);
|
|
370
|
+
return base === 'node' || base === 'node.exe';
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Kill the child. Sends `shutdownSignal` first; after `gracePeriodMs`
|
|
374
|
+
* escalates to SIGKILL. Both calls tolerate "child already exited" —
|
|
375
|
+
* `kill()` returns false but doesn't throw on a reaped child.
|
|
376
|
+
*
|
|
377
|
+
* `onTimer` lets the caller hold a reference to the escalation timer
|
|
378
|
+
* so it can cancel on early exit without a second kill attempt.
|
|
379
|
+
*/
|
|
380
|
+
function killChild(child, signal, gracePeriodMs, onTimer) {
|
|
381
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
382
|
+
return;
|
|
383
|
+
try {
|
|
384
|
+
child.kill(signal);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Already gone.
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const timer = setTimeout(() => {
|
|
391
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
392
|
+
return;
|
|
393
|
+
try {
|
|
394
|
+
child.kill('SIGKILL');
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
/* already gone */
|
|
398
|
+
}
|
|
399
|
+
}, gracePeriodMs);
|
|
400
|
+
timer.unref?.();
|
|
401
|
+
onTimer(timer);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Concatenate captured buffer chunks and decode as UTF-8, truncated
|
|
405
|
+
* to `max` bytes. UTF-8 decode tolerates a truncation mid-codepoint
|
|
406
|
+
* by replacing the partial bytes with the replacement character.
|
|
407
|
+
*/
|
|
408
|
+
function decodeUpTo(chunks, max) {
|
|
409
|
+
const joined = Buffer.concat(chunks);
|
|
410
|
+
const sliced = joined.length > max ? joined.subarray(0, max) : joined;
|
|
411
|
+
return sliced.toString('utf-8');
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Remove the tmpdir the sandbox created. Best-effort — a stray lock
|
|
415
|
+
* file or open handle in the child should not propagate as a caller-
|
|
416
|
+
* visible error. Real failures surface via logs, not exceptions.
|
|
417
|
+
*/
|
|
418
|
+
function cleanupCwd(cwd, owned) {
|
|
419
|
+
if (!owned)
|
|
420
|
+
return;
|
|
421
|
+
try {
|
|
422
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// Best-effort. Tests that need a clean tmpdir use mkdtempSync
|
|
426
|
+
// under os.tmpdir() so the OS eventually reclaims it.
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawner seam. Broken out from the runner so tests can substitute
|
|
3
|
+
* a fake child process without touching the filesystem or launching
|
|
4
|
+
* a real OS process.
|
|
5
|
+
*
|
|
6
|
+
* Kept minimal — only the shape the sandbox actually uses. Real
|
|
7
|
+
* production wiring imports `spawn` from `node:child_process`.
|
|
8
|
+
*/
|
|
9
|
+
import type { ChildProcess } from 'node:child_process';
|
|
10
|
+
export type Spawner = (command: string, args: readonly string[], options: SpawnerOptions) => ChildProcess;
|
|
11
|
+
export interface SpawnerOptions {
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
readonly env: NodeJS.ProcessEnv;
|
|
14
|
+
readonly stdio: ['pipe', 'pipe', 'pipe'];
|
|
15
|
+
readonly shell: false;
|
|
16
|
+
readonly detached: false;
|
|
17
|
+
readonly windowsHide: true;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=spawner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawner.d.ts","sourceRoot":"","sources":["../src/spawner.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,OAAO,GAAG,CACpB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,KACpB,YAAY,CAAC;AAElB,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;CAC5B"}
|
package/dist/spawner.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/sandbox` — public API types.
|
|
3
|
+
*
|
|
4
|
+
* One high-level entry point (`runSandboxed`) with a small, pinned
|
|
5
|
+
* surface. The discriminated `SandboxOutcome` lets callers branch on
|
|
6
|
+
* terminal state without re-deriving it from exit codes / signals.
|
|
7
|
+
*
|
|
8
|
+
* ## Honest security boundary (MVP)
|
|
9
|
+
*
|
|
10
|
+
* What this package DOES enforce, portably, from pure Node:
|
|
11
|
+
*
|
|
12
|
+
* - **Process boundary.** Work runs in a fresh `child_process.spawn`
|
|
13
|
+
* subprocess with `shell: false` + `detached: false` + `stdio:
|
|
14
|
+
* ['pipe', 'pipe', 'pipe']`. The child cannot attach to the
|
|
15
|
+
* parent's controlling terminal, cannot inherit open fds, and
|
|
16
|
+
* cannot fork into a new process group. A crash in the child
|
|
17
|
+
* cannot corrupt parent state.
|
|
18
|
+
*
|
|
19
|
+
* - **Working-directory isolation.** Callers either bring their own
|
|
20
|
+
* absolute `cwd` or let the sandbox mint an owned tmpdir that it
|
|
21
|
+
* cleans up on finish. Relative paths inside the child resolve
|
|
22
|
+
* against the declared `cwd` only. (Not an FS boundary — the
|
|
23
|
+
* child still has the parent user's read permissions on absolute
|
|
24
|
+
* paths; see "Does NOT enforce" below.)
|
|
25
|
+
*
|
|
26
|
+
* - **Environment allowlist.** The child receives the `env` object
|
|
27
|
+
* verbatim. The parent's `process.env` is NEVER merged.
|
|
28
|
+
* Consumers that want to expose specific variables write them
|
|
29
|
+
* explicitly. A minimal bootstrap (`PATH`, `HOME`, `TMPDIR` if
|
|
30
|
+
* present on the parent) is injected only so the child can
|
|
31
|
+
* resolve the command and write scratch data — and is documented
|
|
32
|
+
* in {@link SandboxOptions.env}.
|
|
33
|
+
*
|
|
34
|
+
* - **Wall-clock timeout.** `timeoutMs` is required — no
|
|
35
|
+
* "infinity." On overrun the sandbox sends `SIGTERM`, waits
|
|
36
|
+
* `gracePeriodMs`, then escalates to `SIGKILL`. Outcome is
|
|
37
|
+
* `'timeout'`.
|
|
38
|
+
*
|
|
39
|
+
* - **Output byte caps.** stdout + stderr are accumulated up to
|
|
40
|
+
* `maxStdoutBytes` / `maxStderrBytes`. When either is exceeded
|
|
41
|
+
* the sandbox terminates the child and returns outcome
|
|
42
|
+
* `'overflow-stdout'` / `'overflow-stderr'`. Captured output is
|
|
43
|
+
* truncated to exactly the cap.
|
|
44
|
+
*
|
|
45
|
+
* - **V8 heap cap (Node children only).** When `nodeHeapMb` is
|
|
46
|
+
* set AND the command is `process.execPath` (or has the basename
|
|
47
|
+
* `node`), the sandbox prepends `--max-old-space-size=<mb>` to
|
|
48
|
+
* `args` via the `NODE_OPTIONS` env var. This caps V8's old-
|
|
49
|
+
* generation heap. It does NOT cap total RSS (native buffers,
|
|
50
|
+
* ArrayBuffers, child-of-child memory).
|
|
51
|
+
*
|
|
52
|
+
* - **No stdin leakage.** When `stdin` is absent the child's stdin
|
|
53
|
+
* is closed immediately. When present, the sandbox writes exactly
|
|
54
|
+
* the supplied bytes then closes. The parent never forwards its
|
|
55
|
+
* own stdin.
|
|
56
|
+
*
|
|
57
|
+
* What this package does NOT enforce (portably from Node):
|
|
58
|
+
*
|
|
59
|
+
* - **Network egress blocking.** Impossible portably from Node
|
|
60
|
+
* user-space. Real enforcement needs OS primitives: Linux
|
|
61
|
+
* network namespaces + iptables, macOS pf, Windows WFP, or a
|
|
62
|
+
* sidecar (Docker network:none, gVisor, firecracker). Consumers
|
|
63
|
+
* who need no-egress MUST layer those themselves; the sandbox
|
|
64
|
+
* does not pretend.
|
|
65
|
+
*
|
|
66
|
+
* - **Filesystem read boundaries.** The child shares the parent
|
|
67
|
+
* process's UID/GID + filesystem visibility. Relative paths
|
|
68
|
+
* resolve under `cwd`, but absolute paths and `..` traversals
|
|
69
|
+
* remain reachable. A true FS sandbox needs chroot / pivot_root /
|
|
70
|
+
* user namespaces / Landlock (Linux) / sandbox-exec (macOS).
|
|
71
|
+
*
|
|
72
|
+
* - **CPU share / scheduling cap.** Node has no portable rlimit
|
|
73
|
+
* surface for CPU time. Consumers who need CPU caps run the
|
|
74
|
+
* sandbox under a cgroup / `ulimit` / container.
|
|
75
|
+
*
|
|
76
|
+
* - **Syscall filtering.** No seccomp, no LSM hooks. The child can
|
|
77
|
+
* make any syscall the parent could.
|
|
78
|
+
*
|
|
79
|
+
* - **Fork-bomb containment.** No `RLIMIT_NPROC`. A malicious
|
|
80
|
+
* child could spawn descendants that the sandbox does NOT track
|
|
81
|
+
* or kill. (The sandbox kills only its direct child; grandchildren
|
|
82
|
+
* survive if they reparent.)
|
|
83
|
+
*
|
|
84
|
+
* Every "does not" above is a deliberate omission to keep the MVP
|
|
85
|
+
* portable + honest. Consumers that require any of those guarantees
|
|
86
|
+
* run the sandbox under a stronger layer (Docker, gVisor, firecracker,
|
|
87
|
+
* etc.) — the sandbox does not lie about what it gives them.
|
|
88
|
+
*/
|
|
89
|
+
import type { Spawner } from './spawner.js';
|
|
90
|
+
/**
|
|
91
|
+
* Options for a single {@link runSandboxed} invocation. Immutable —
|
|
92
|
+
* callers construct a fresh object per run.
|
|
93
|
+
*/
|
|
94
|
+
export interface SandboxOptions {
|
|
95
|
+
/**
|
|
96
|
+
* Absolute path to the executable. Required. No shell interpretation
|
|
97
|
+
* (`shell: false` is the only supported mode); to run a Node script
|
|
98
|
+
* pass `process.execPath` or a resolved `node` path and put the
|
|
99
|
+
* script path in `args`.
|
|
100
|
+
*/
|
|
101
|
+
readonly command: string;
|
|
102
|
+
/**
|
|
103
|
+
* Args forwarded verbatim to the child. Pass `[]` for no args.
|
|
104
|
+
*/
|
|
105
|
+
readonly args: readonly string[];
|
|
106
|
+
/**
|
|
107
|
+
* Working directory. Absolute paths only. Absent = sandbox creates
|
|
108
|
+
* an owned tmpdir under `os.tmpdir()` and removes it at the end
|
|
109
|
+
* of the run (result.cwdOwnedBySandbox === true). Relative paths
|
|
110
|
+
* are rejected — the sandbox does not silently resolve against the
|
|
111
|
+
* parent's CWD.
|
|
112
|
+
*/
|
|
113
|
+
readonly cwd?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Environment variables exposed to the child. **The parent's
|
|
116
|
+
* `process.env` is NEVER merged in.** Only the keys in this map
|
|
117
|
+
* plus a minimal bootstrap (`PATH`, `HOME`, and `TMPDIR` when
|
|
118
|
+
* present on the parent — required so the kernel + Node can find
|
|
119
|
+
* the command and write scratch data) reach the child. Callers
|
|
120
|
+
* that want to forward extra variables do so explicitly by reading
|
|
121
|
+
* `process.env` and copying the keys they want.
|
|
122
|
+
*
|
|
123
|
+
* Absent = empty allowlist (child gets only the bootstrap).
|
|
124
|
+
*/
|
|
125
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
126
|
+
/**
|
|
127
|
+
* Wall-clock timeout in milliseconds. Required — the sandbox does
|
|
128
|
+
* not support "run forever." Must be a positive finite integer.
|
|
129
|
+
*
|
|
130
|
+
* On overrun:
|
|
131
|
+
* 1. sandbox sends {@link shutdownSignal} to the child,
|
|
132
|
+
* 2. waits up to {@link gracePeriodMs},
|
|
133
|
+
* 3. escalates to `SIGKILL` if the child is still alive.
|
|
134
|
+
*
|
|
135
|
+
* Outcome is `'timeout'`.
|
|
136
|
+
*/
|
|
137
|
+
readonly timeoutMs: number;
|
|
138
|
+
/**
|
|
139
|
+
* Signal to send when terminating. Defaults to `'SIGTERM'`.
|
|
140
|
+
* The escalation to `'SIGKILL'` after {@link gracePeriodMs} is
|
|
141
|
+
* unconditional and not configurable.
|
|
142
|
+
*/
|
|
143
|
+
readonly shutdownSignal?: NodeJS.Signals;
|
|
144
|
+
/**
|
|
145
|
+
* Grace period in ms between the soft-kill signal and the hard
|
|
146
|
+
* `SIGKILL`. Defaults to `2000`. Must be >= 0 and < {@link
|
|
147
|
+
* timeoutMs} to leave headroom for a child that ignores SIGTERM.
|
|
148
|
+
*/
|
|
149
|
+
readonly gracePeriodMs?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Optional data written to the child's stdin. When omitted, stdin
|
|
152
|
+
* is closed immediately and the child reads EOF. Strings are
|
|
153
|
+
* encoded as UTF-8; `Uint8Array` is written verbatim.
|
|
154
|
+
*/
|
|
155
|
+
readonly stdin?: string | Uint8Array;
|
|
156
|
+
/**
|
|
157
|
+
* Max bytes to capture from stdout before terminating the child.
|
|
158
|
+
* Defaults to `8 * 1024 * 1024` (8 MiB). Must be > 0.
|
|
159
|
+
* Exceeding the cap terminates the child with outcome
|
|
160
|
+
* `'overflow-stdout'`; captured stdout is truncated to the cap.
|
|
161
|
+
*/
|
|
162
|
+
readonly maxStdoutBytes?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Max bytes to capture from stderr before terminating the child.
|
|
165
|
+
* Defaults to `1 * 1024 * 1024` (1 MiB). Must be > 0.
|
|
166
|
+
* Exceeding the cap terminates the child with outcome
|
|
167
|
+
* `'overflow-stderr'`; captured stderr is truncated to the cap.
|
|
168
|
+
*/
|
|
169
|
+
readonly maxStderrBytes?: number;
|
|
170
|
+
/**
|
|
171
|
+
* V8 old-generation heap cap in MiB. Applied only when the
|
|
172
|
+
* resolved `command` basename is `node` (or equals
|
|
173
|
+
* `process.execPath`). For non-Node children the field is ignored
|
|
174
|
+
* and {@link SandboxResult.nodeHeapMbApplied} is `false`.
|
|
175
|
+
*
|
|
176
|
+
* Applied via `NODE_OPTIONS=--max-old-space-size=<mb>`. Caps V8's
|
|
177
|
+
* old-gen only — not total RSS.
|
|
178
|
+
*/
|
|
179
|
+
readonly nodeHeapMb?: number;
|
|
180
|
+
/**
|
|
181
|
+
* External cancellation. When the signal fires, the sandbox
|
|
182
|
+
* terminates the child (SIGTERM → grace → SIGKILL) and returns
|
|
183
|
+
* outcome `'canceled'`.
|
|
184
|
+
*/
|
|
185
|
+
readonly signal?: AbortSignal;
|
|
186
|
+
/**
|
|
187
|
+
* Test seam — substitute a spawner to exercise the runner without
|
|
188
|
+
* launching real processes. Production code leaves this unset.
|
|
189
|
+
*/
|
|
190
|
+
readonly spawner?: Spawner;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Terminal state of a sandbox run. Exhaustive; every possible
|
|
194
|
+
* shutdown path maps to exactly one of these.
|
|
195
|
+
*/
|
|
196
|
+
export type SandboxOutcome =
|
|
197
|
+
/** Child exited on its own within the budget. `exitCode`/`signal`
|
|
198
|
+
* record why. */
|
|
199
|
+
'exit'
|
|
200
|
+
/** `timeoutMs` elapsed; sandbox killed the child. */
|
|
201
|
+
| 'timeout'
|
|
202
|
+
/** External `signal` aborted the run; sandbox killed the child. */
|
|
203
|
+
| 'canceled'
|
|
204
|
+
/** stdout exceeded `maxStdoutBytes`; sandbox killed the child. */
|
|
205
|
+
| 'overflow-stdout'
|
|
206
|
+
/** stderr exceeded `maxStderrBytes`; sandbox killed the child. */
|
|
207
|
+
| 'overflow-stderr'
|
|
208
|
+
/** Spawn failed or an internal error occurred before the child
|
|
209
|
+
* could be supervised. `errorMessage` carries detail. */
|
|
210
|
+
| 'spawn-error';
|
|
211
|
+
/**
|
|
212
|
+
* Result of a {@link runSandboxed} invocation. All fields are
|
|
213
|
+
* non-optional — consumers get a consistent shape whatever the
|
|
214
|
+
* outcome. Fields that are only meaningful for some outcomes carry
|
|
215
|
+
* documented sentinels (e.g. `exitCode: null` for signal-induced
|
|
216
|
+
* termination).
|
|
217
|
+
*/
|
|
218
|
+
export interface SandboxResult {
|
|
219
|
+
/** Terminal state. */
|
|
220
|
+
readonly outcome: SandboxOutcome;
|
|
221
|
+
/** Exit code when the child exited on its own; `null` when the
|
|
222
|
+
* sandbox killed it or spawn failed. */
|
|
223
|
+
readonly exitCode: number | null;
|
|
224
|
+
/** Signal that terminated the child; `null` when the child exited
|
|
225
|
+
* with an explicit code or spawn failed. */
|
|
226
|
+
readonly signal: NodeJS.Signals | null;
|
|
227
|
+
/** Captured stdout, decoded as UTF-8. Truncated to
|
|
228
|
+
* `maxStdoutBytes`. */
|
|
229
|
+
readonly stdout: string;
|
|
230
|
+
/** Captured stderr, decoded as UTF-8. Truncated to
|
|
231
|
+
* `maxStderrBytes`. */
|
|
232
|
+
readonly stderr: string;
|
|
233
|
+
/** Wall-clock duration in milliseconds. */
|
|
234
|
+
readonly durationMs: number;
|
|
235
|
+
/** True iff stdout output was truncated (actual stream was larger
|
|
236
|
+
* than `maxStdoutBytes`). */
|
|
237
|
+
readonly stdoutTruncated: boolean;
|
|
238
|
+
/** True iff stderr output was truncated. */
|
|
239
|
+
readonly stderrTruncated: boolean;
|
|
240
|
+
/** Absolute `cwd` the child ran in. When the caller brought their
|
|
241
|
+
* own it is echoed back; when the sandbox minted one this points
|
|
242
|
+
* at the owned tmpdir (already removed by the time the result
|
|
243
|
+
* resolves, per `cwdOwnedBySandbox`). */
|
|
244
|
+
readonly cwd: string;
|
|
245
|
+
/** True iff the sandbox created (and cleaned up) `cwd`. */
|
|
246
|
+
readonly cwdOwnedBySandbox: boolean;
|
|
247
|
+
/** True iff the sandbox applied the V8 heap cap (Node child only). */
|
|
248
|
+
readonly nodeHeapMbApplied: boolean;
|
|
249
|
+
/** Human-readable reason when `outcome === 'spawn-error'`.
|
|
250
|
+
* Empty string for every other outcome. */
|
|
251
|
+
readonly errorMessage: string;
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAEjC;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAEhD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAEzC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAErC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAEjC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAEjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc;AACxB;iBACiB;AACf,MAAM;AACR,qDAAqD;GACnD,SAAS;AACX,mEAAmE;GACjE,UAAU;AACZ,kEAAkE;GAChE,iBAAiB;AACnB,kEAAkE;GAChE,iBAAiB;AACnB;yDACyD;GACvD,aAAa,CAAC;AAElB;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAEjC;4CACwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAEjC;gDAC4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAEvC;2BACuB;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;2BACuB;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,2CAA2C;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B;iCAC6B;IAC7B,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAElC,4CAA4C;IAC5C,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAElC;;;6CAGyC;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,2DAA2D;IAC3D,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IAEpC,sEAAsE;IACtE,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IAEpC;+CAC2C;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ggui-ai/sandbox",
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"description": "Bounded process-isolation runner for untrusted Node subprocesses. Spawns a child with explicit cwd, env allowlist, wall-clock timeout, output caps, and (for Node children) a V8 heap cap. Honest scope — does NOT attempt network egress blocking, filesystem read boundaries, CPU limits, or syscall filtering (those need OS primitives: namespaces / seccomp / cgroups).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ggui",
|
|
7
|
+
"sandbox",
|
|
8
|
+
"process-isolation",
|
|
9
|
+
"subprocess",
|
|
10
|
+
"spawn"
|
|
11
|
+
],
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"homepage": "https://github.com/ggui-ai/ggui/tree/main/packages/sandbox",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/ggui-ai/ggui.git",
|
|
17
|
+
"directory": "packages/sandbox"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.0.0",
|
|
37
|
+
"typescript": "^5.0.0",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/ggui-ai/ggui/issues"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20.0.0"
|
|
45
|
+
},
|
|
46
|
+
"author": "ggui contributors <hello@ggui.ai>",
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.build.json",
|
|
49
|
+
"dev": "tsc --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest"
|
|
53
|
+
}
|
|
54
|
+
}
|