boxsh.js 0.1.1 → 1.0.0
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/README.md +28 -37
- package/package.json +1 -1
- package/src/client.mjs +11 -8
- package/src/index.d.ts +5 -8
- package/src/exec/boxsh +0 -0
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# boxsh.js
|
|
2
2
|
|
|
3
|
-
Node.js SDK for [boxsh](../../README.md) — a sandboxed POSIX shell with
|
|
3
|
+
Node.js SDK for [boxsh](../../README.md) — a sandboxed POSIX shell with OS-native isolation and copy-on-write overlay filesystem.
|
|
4
4
|
|
|
5
5
|
boxsh.js lets you drive a long-lived boxsh instance from Node.js: execute shell commands, read/write files, and perform search-and-replace edits — all inside an isolated sandbox.
|
|
6
6
|
|
|
7
|
-
**Requirements:** Node.js ≥ 18, Linux, `boxsh` binary on `$PATH` (or set `BOXSH` env var).
|
|
7
|
+
**Requirements:** Node.js ≥ 18, Linux or macOS, `boxsh` binary on `$PATH` (or set `BOXSH` env var).
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install
|
|
12
|
+
npm install boxsh.js
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -82,72 +82,64 @@ console.log(diff); // unified diff format
|
|
|
82
82
|
|
|
83
83
|
## Sandbox isolation
|
|
84
84
|
|
|
85
|
-
With `sandbox` enabled, commands run inside
|
|
85
|
+
With `sandbox` enabled, commands run inside an OS-native sandbox (Linux namespaces or macOS Seatbelt), separated from the host. You can further isolate the network:
|
|
86
86
|
|
|
87
87
|
```js
|
|
88
88
|
const client = new BoxshClient({
|
|
89
89
|
sandbox: true,
|
|
90
90
|
newNetNs: true, // Isolated network namespace (no external access)
|
|
91
|
-
newPidNs: true, // Isolated PID namespace
|
|
92
91
|
});
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
---
|
|
96
95
|
|
|
97
|
-
## Overlay
|
|
96
|
+
## COW Bind (Overlay Filesystem)
|
|
98
97
|
|
|
99
|
-
|
|
98
|
+
COW bind is the primary usage pattern for boxsh: mount a read-only source directory as a copy-on-write workspace. Commands can read and write freely, but all modifications land in the destination directory while the source remains untouched.
|
|
100
99
|
|
|
101
100
|
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
work Working directory required by overlayfs (must be on the same filesystem as upper)
|
|
106
|
-
dst Mount point (the path visible inside the sandbox)
|
|
101
|
+
Bind parameters:
|
|
102
|
+
src Read-only base directory (your project/repository)
|
|
103
|
+
dst Writable destination directory (all modifications go here)
|
|
107
104
|
```
|
|
108
105
|
|
|
109
106
|
```js
|
|
110
107
|
import { BoxshClient } from 'boxsh.js';
|
|
111
108
|
import fs from 'node:fs';
|
|
112
109
|
|
|
113
|
-
// Prepare
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const mnt = '/tmp/sandbox/mnt';
|
|
117
|
-
fs.mkdirSync(upper, { recursive: true });
|
|
118
|
-
fs.mkdirSync(work, { recursive: true });
|
|
119
|
-
fs.mkdirSync(mnt, { recursive: true });
|
|
110
|
+
// Prepare destination directory
|
|
111
|
+
const dst = '/tmp/sandbox/dst';
|
|
112
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
120
113
|
|
|
121
114
|
const client = new BoxshClient({
|
|
122
115
|
sandbox: true,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
},
|
|
116
|
+
binds: [{
|
|
117
|
+
mode: 'cow',
|
|
118
|
+
src: '/home/user/myproject', // read-only base
|
|
119
|
+
dst, // modifications land here
|
|
120
|
+
}],
|
|
129
121
|
});
|
|
130
122
|
|
|
131
|
-
// Inside the sandbox,
|
|
132
|
-
await client.exec('npm install',
|
|
123
|
+
// Inside the sandbox, dst is a COW copy of myproject
|
|
124
|
+
await client.exec('npm install', dst);
|
|
133
125
|
|
|
134
126
|
// Read/write files via built-in tools (RPC, no shell round-trip needed)
|
|
135
|
-
const pkg = await client.read(`${
|
|
136
|
-
await client.write(`${
|
|
127
|
+
const pkg = await client.read(`${dst}/package.json`);
|
|
128
|
+
await client.write(`${dst}/result.txt`, 'done\n');
|
|
137
129
|
|
|
138
130
|
await client.close();
|
|
139
131
|
|
|
140
|
-
// At this point
|
|
141
|
-
// You can commit, archive, or simply delete
|
|
132
|
+
// At this point dst/ contains all modifications; base is completely untouched.
|
|
133
|
+
// You can commit, archive, or simply delete dst/ to discard changes.
|
|
142
134
|
```
|
|
143
135
|
|
|
144
|
-
The
|
|
136
|
+
The destination directory persists across sessions. To resume a previous session, create a new BoxshClient pointing at the same `dst` directory.
|
|
145
137
|
|
|
146
138
|
---
|
|
147
139
|
|
|
148
140
|
## Inspecting changes
|
|
149
141
|
|
|
150
|
-
`getChanges` scans the
|
|
142
|
+
`getChanges` scans the COW destination directory against the base and returns all added, modified, and deleted files. `formatChanges` formats the result as human-readable text.
|
|
151
143
|
|
|
152
144
|
Both functions run on the host side (inside the Node.js process) and do not require a running boxsh instance.
|
|
153
145
|
|
|
@@ -155,7 +147,7 @@ Both functions run on the host side (inside the Node.js process) and do not requ
|
|
|
155
147
|
import { getChanges, formatChanges } from 'boxsh.js';
|
|
156
148
|
|
|
157
149
|
const changes = getChanges({
|
|
158
|
-
upper: '/tmp/sandbox/
|
|
150
|
+
upper: '/tmp/sandbox/dst',
|
|
159
151
|
base: '/home/user/myproject',
|
|
160
152
|
});
|
|
161
153
|
// [{ path: 'package-lock.json', type: 'modified' },
|
|
@@ -194,8 +186,7 @@ await client.exec(`echo ${shellQuote(userInput)}`);
|
|
|
194
186
|
| `workers` | `number` | `1` | Number of pre-forked workers |
|
|
195
187
|
| `sandbox` | `boolean` | `false` | Enable namespace sandbox |
|
|
196
188
|
| `newNetNs` | `boolean` | `false` | Isolate network |
|
|
197
|
-
| `
|
|
198
|
-
| `overlay` | `{ lower, upper, work, dst }` | — | Overlay mount configuration |
|
|
189
|
+
| `binds` | `BoxshBindOption[]` | — | Bind mount configuration (ro/wr/cow) |
|
|
199
190
|
|
|
200
191
|
### `client.exec(cmd, cwd?, timeout?) → Promise<{ exitCode, stdout, stderr }>`
|
|
201
192
|
|
|
@@ -223,7 +214,7 @@ Send SIGTERM immediately.
|
|
|
223
214
|
|
|
224
215
|
### `getChanges({ upper, base }) → Array<{ path, type }>`
|
|
225
216
|
|
|
226
|
-
Scan the
|
|
217
|
+
Scan the destination directory and return a list of changes relative to base. `type` is `'added'`, `'modified'`, or `'deleted'`.
|
|
227
218
|
|
|
228
219
|
### `formatChanges(changes) → string`
|
|
229
220
|
|
package/package.json
CHANGED
package/src/client.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BoxshClient — manages a long-lived boxsh RPC process.
|
|
3
3
|
*
|
|
4
|
-
* Spawns boxsh with --rpc and optional --sandbox/--
|
|
4
|
+
* Spawns boxsh with --rpc and optional --sandbox/--bind flags.
|
|
5
5
|
* All commands and tool calls are sent as JSON lines to stdin and
|
|
6
6
|
* responses are read back as JSON lines from stdout.
|
|
7
7
|
*
|
|
@@ -36,12 +36,11 @@ export class BoxshClient {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @param {object} [options]
|
|
39
|
-
* @param {string} [options.boxshPath] Path to boxsh binary (default: BOXSH env var → 'boxsh')
|
|
39
|
+
* @param {string} [options.boxshPath] Path to boxsh binary (default: BOXSH env var → 'boxsh' in PATH)
|
|
40
40
|
* @param {number} [options.workers] Worker count (default: 1)
|
|
41
41
|
* @param {boolean} [options.sandbox] Enable --sandbox flag
|
|
42
42
|
* @param {boolean} [options.newNetNs] Enable --new-net-ns flag
|
|
43
|
-
* @param {
|
|
44
|
-
* @param {{ lower: string, upper: string, work: string, dst: string }} [options.overlay]
|
|
43
|
+
* @param {Array<{ mode: 'ro'|'wr', path: string } | { mode: 'cow', src: string, dst: string }>} [options.binds]
|
|
45
44
|
*/
|
|
46
45
|
constructor(options = {}) {
|
|
47
46
|
const boxsh = options.boxshPath ?? process.env['BOXSH'] ?? 'boxsh';
|
|
@@ -49,10 +48,14 @@ export class BoxshClient {
|
|
|
49
48
|
|
|
50
49
|
if (options.sandbox) args.push('--sandbox');
|
|
51
50
|
if (options.newNetNs) args.push('--new-net-ns');
|
|
52
|
-
if (options.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
if (options.binds) {
|
|
52
|
+
for (const b of options.binds) {
|
|
53
|
+
if (b.mode === 'cow') {
|
|
54
|
+
args.push('--bind', `cow:${b.src}:${b.dst}`);
|
|
55
|
+
} else {
|
|
56
|
+
args.push('--bind', `${b.mode}:${b.path}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
this.#proc = spawn(boxsh, args, { stdio: ['pipe', 'pipe', 'inherit'] });
|
package/src/index.d.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
dst: string;
|
|
6
|
-
}
|
|
1
|
+
export type BoxshBindOption =
|
|
2
|
+
| { mode: 'ro'; path: string }
|
|
3
|
+
| { mode: 'wr'; path: string }
|
|
4
|
+
| { mode: 'cow'; src: string; dst: string };
|
|
7
5
|
|
|
8
6
|
export interface BoxshClientOptions {
|
|
9
7
|
boxshPath?: string;
|
|
10
8
|
workers?: number;
|
|
11
9
|
sandbox?: boolean;
|
|
12
10
|
newNetNs?: boolean;
|
|
13
|
-
|
|
14
|
-
overlay?: BoxshOverlayOptions;
|
|
11
|
+
binds?: BoxshBindOption[];
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
export interface ExecResult {
|
package/src/exec/boxsh
DELETED
|
Binary file
|