@ai-sdk/sandbox-just-bash 0.0.0 → 1.0.0-beta.15

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/CHANGELOG.md ADDED
@@ -0,0 +1,115 @@
1
+ # @ai-sdk/sandbox-just-bash
2
+
3
+ ## 1.0.0-beta.15
4
+
5
+ ### Patch Changes
6
+
7
+ - @ai-sdk/harness@1.0.0-beta.15
8
+
9
+ ## 1.0.0-beta.14
10
+
11
+ ### Patch Changes
12
+
13
+ - b8396f0: trigger initial beta release
14
+ - Updated dependencies [b8396f0]
15
+ - @ai-sdk/harness@1.0.0-beta.14
16
+ - @ai-sdk/provider-utils@5.0.0-beta.49
17
+
18
+ ## 1.0.0-canary.13
19
+
20
+ ### Patch Changes
21
+
22
+ - @ai-sdk/harness@1.0.0-canary.13
23
+
24
+ ## 1.0.0-canary.12
25
+
26
+ ### Patch Changes
27
+
28
+ - @ai-sdk/harness@1.0.0-canary.12
29
+
30
+ ## 1.0.0-canary.11
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [be83911]
35
+ - @ai-sdk/harness@1.0.0-canary.11
36
+
37
+ ## 1.0.0-canary.10
38
+
39
+ ### Patch Changes
40
+
41
+ - @ai-sdk/harness@1.0.0-canary.10
42
+
43
+ ## 1.0.0-canary.9
44
+
45
+ ### Patch Changes
46
+
47
+ - @ai-sdk/harness@1.0.0-canary.9
48
+
49
+ ## 1.0.0-canary.8
50
+
51
+ ### Patch Changes
52
+
53
+ - Updated dependencies [aae0138]
54
+ - @ai-sdk/harness@1.0.0-canary.8
55
+
56
+ ## 1.0.0-canary.7
57
+
58
+ ### Patch Changes
59
+
60
+ - e551763: fix(harness): avoid using peer dependencies for underlying harness and sandbox SDKs
61
+ - Updated dependencies [3d87086]
62
+ - Updated dependencies [aeda373]
63
+ - Updated dependencies [1ea15a3]
64
+ - Updated dependencies [375fdd7]
65
+ - Updated dependencies [b4507d5]
66
+ - @ai-sdk/harness@1.0.0-canary.7
67
+ - @ai-sdk/provider-utils@5.0.0-canary.48
68
+
69
+ ## 1.0.0-canary.6
70
+
71
+ ### Patch Changes
72
+
73
+ - @ai-sdk/harness@1.0.0-canary.6
74
+
75
+ ## 1.0.0-canary.5
76
+
77
+ ### Patch Changes
78
+
79
+ - Updated dependencies [d77bed4]
80
+ - Updated dependencies [bae5e2b]
81
+ - @ai-sdk/harness@1.0.0-canary.5
82
+ - @ai-sdk/provider-utils@5.0.0-canary.47
83
+
84
+ ## 1.0.0-canary.4
85
+
86
+ ### Patch Changes
87
+
88
+ - Updated dependencies [3d9a50c]
89
+ - @ai-sdk/harness@1.0.0-canary.4
90
+
91
+ ## 1.0.0-canary.3
92
+
93
+ ### Patch Changes
94
+
95
+ - Updated dependencies [21d3d60]
96
+ - @ai-sdk/harness@1.0.0-canary.3
97
+
98
+ ## 1.0.0-canary.2
99
+
100
+ ### Patch Changes
101
+
102
+ - 6c7a3e5: Start the `1.0.0` canary release line for the experimental harness and sandbox packages. They were unintentionally published as `0.0.0-canary.*` because they were scaffolded with a `0.0.0-canary.0` premajor version, which semver could not advance past on a major bump.
103
+ - Updated dependencies [6c7a3e5]
104
+ - @ai-sdk/harness@1.0.0-canary.2
105
+
106
+ ## 0.0.0-canary.1
107
+
108
+ ### Major Changes
109
+
110
+ - 9d6dbe0: feat(harness): add sandbox specific expansion for harness abstraction, add `sandbox-just-bash` and `sandbox-vercel`
111
+
112
+ ### Patch Changes
113
+
114
+ - Updated dependencies [9d6dbe0]
115
+ - @ai-sdk/harness@0.0.0-canary.1
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2023 Vercel, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # AI SDK - just-bash Sandbox
2
+
3
+ _This package is **experimental**._
4
+
5
+ `HarnessV1SandboxProvider` implementation for [`just-bash`](https://github.com/vercel-labs/just-bash), an in-process JavaScript bash environment with a virtual filesystem. File operations and spawned processes share the same in-memory state.
6
+
7
+ This provider does not expose ports, so it cannot be used with features that require actual network sandboxes. It is primarily useful for handing a local `Experimental_SandboxSession` to AI SDK tools that accept `experimental_sandbox`.
8
+
9
+ ## Setup
10
+
11
+ ```bash
12
+ npm i @ai-sdk/sandbox-just-bash
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ The factory is synchronous. The returned provider is stable; the actual `just-bash` `Sandbox` is created on demand inside `provider.createSession()`.
18
+
19
+ ```ts
20
+ import { createJustBashSandbox } from '@ai-sdk/sandbox-just-bash';
21
+
22
+ const justBashSandbox = createJustBashSandbox({ cwd: '/work' });
23
+
24
+ const networkSandboxSession = await justBashSandbox.createSession();
25
+ const sandboxSession = networkSandboxSession.restricted();
26
+
27
+ await sandboxSession.writeTextFile({ path: '/work/hello.txt', content: 'hi' });
28
+
29
+ const { stdout } = await sandboxSession.run({
30
+ command: 'cat /work/hello.txt',
31
+ });
32
+ console.log(stdout); // "hi"
33
+ ```
34
+
35
+ `networkSandboxSession.restricted()` is typed as `Experimental_SandboxSession` and is the surface to hand to user tools. The network sandbox session's `getPortUrl` throws (just-bash has no port story) and `setNetworkPolicy` is omitted (no local enforcement primitive).
36
+
37
+ To wrap an already-created `just-bash` `Sandbox` (e.g. with a custom `fs`), pass it via `sandbox`. Install `just-bash` directly if your application imports `Sandbox`, `OverlayFs`, or other `just-bash` exports:
38
+
39
+ ```ts
40
+ import { createJustBashSandbox } from '@ai-sdk/sandbox-just-bash';
41
+ import { OverlayFs, Sandbox } from 'just-bash';
42
+
43
+ const overlay = new OverlayFs({ root: process.cwd() });
44
+ const sandbox = createJustBashSandbox({
45
+ sandbox: await Sandbox.create({ fs: overlay, cwd: overlay.getMountPoint() }),
46
+ });
47
+ ```
@@ -0,0 +1,53 @@
1
+ import { HarnessV1SandboxProvider, HarnessV1NetworkSandboxSession } from '@ai-sdk/harness';
2
+ import { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
3
+ import { Sandbox } from 'just-bash';
4
+
5
+ /**
6
+ * Parameters forwarded to `just-bash`'s `Sandbox.create` when creating a
7
+ * sandbox from scratch. Aliased directly from the underlying SDK so the full
8
+ * surface is available without us re-declaring it.
9
+ */
10
+ type JustBashSandboxCreateParams = NonNullable<Parameters<typeof Sandbox.create>[0]>;
11
+ /**
12
+ * Settings for {@link createJustBashSandbox}. Two mutually-exclusive shapes:
13
+ *
14
+ * - `{ sandbox }` — wrap an already-created `just-bash` `Sandbox`. The caller
15
+ * owns its lifecycle.
16
+ * - {@link JustBashSandboxCreateParams} fields — provider calls
17
+ * `Sandbox.create(settings)` on every `createSession()`.
18
+ *
19
+ * just-bash has no port exposure and no snapshot mechanism, so image
20
+ * management is a per-call no-op: if an adapter declares a bootstrap recipe
21
+ * the provider runs it once on the freshly-created sandbox before returning.
22
+ */
23
+ type JustBashSandboxSettings = {
24
+ sandbox: Sandbox;
25
+ } | (JustBashSandboxCreateParams & {
26
+ sandbox?: never;
27
+ });
28
+ declare function createJustBashSandbox(settings?: JustBashSandboxSettings): HarnessV1SandboxProvider;
29
+ /**
30
+ * `HarnessV1SandboxProvider` implementation backed by `just-bash`. Useful for
31
+ * non-bridge harness flows and for handing a local `Experimental_SandboxSession`
32
+ * to AI SDK tools — use `provider.createSession()` then
33
+ * `sandboxSession.restricted()` to get the latter.
34
+ *
35
+ * Note: just-bash cannot expose ports, so bridge-backed harness adapters
36
+ * (claude-code, codex) will reject this provider at start.
37
+ */
38
+ declare class JustBashSandboxProvider implements HarnessV1SandboxProvider {
39
+ private readonly settings;
40
+ readonly specificationVersion: "harness-sandbox-v1";
41
+ readonly providerId = "just-bash-sandbox";
42
+ constructor(settings: JustBashSandboxSettings);
43
+ createSession: (options?: {
44
+ sessionId?: string;
45
+ abortSignal?: AbortSignal;
46
+ identity?: string;
47
+ onFirstCreate?: (session: Experimental_SandboxSession, opts: {
48
+ abortSignal?: AbortSignal;
49
+ }) => Promise<void>;
50
+ }) => Promise<HarnessV1NetworkSandboxSession>;
51
+ }
52
+
53
+ export { JustBashSandboxProvider, type JustBashSandboxSettings, createJustBashSandbox };
package/dist/index.js ADDED
@@ -0,0 +1,284 @@
1
+ // src/just-bash-sandbox.ts
2
+ import { Sandbox } from "just-bash";
3
+
4
+ // src/just-bash-network-sandbox-session.ts
5
+ import {
6
+ HarnessCapabilityUnsupportedError
7
+ } from "@ai-sdk/harness";
8
+ import { randomUUID } from "crypto";
9
+
10
+ // src/just-bash-sandbox-session.ts
11
+ import { isAbsolute, posix } from "path";
12
+ import {
13
+ extractLines
14
+ } from "@ai-sdk/provider-utils";
15
+ var JustBashSandboxSession = class {
16
+ constructor(sandbox) {
17
+ this.sandbox = sandbox;
18
+ }
19
+ get description() {
20
+ return [
21
+ "just-bash JavaScript bash environment with a virtual filesystem.",
22
+ "Filesystem changes and spawned processes share the same in-memory state.",
23
+ `Current working directory: ${this.sandbox.bashEnvInstance.getCwd()}`
24
+ ].join("\n");
25
+ }
26
+ resolvePath(path) {
27
+ return isAbsolute(path) ? path : posix.join(this.sandbox.bashEnvInstance.getCwd(), path);
28
+ }
29
+ async run({
30
+ command,
31
+ workingDirectory,
32
+ env,
33
+ abortSignal
34
+ }) {
35
+ abortSignal == null ? void 0 : abortSignal.throwIfAborted();
36
+ const finished = await this.sandbox.runCommand({
37
+ cmd: "bash",
38
+ args: ["-c", command],
39
+ ...workingDirectory !== void 0 ? { cwd: workingDirectory } : {},
40
+ ...env !== void 0 ? { env } : {},
41
+ ...abortSignal !== void 0 ? { signal: abortSignal } : {}
42
+ });
43
+ const [stdout, stderr] = await Promise.all([
44
+ finished.stdout(),
45
+ finished.stderr()
46
+ ]);
47
+ return {
48
+ exitCode: finished.exitCode,
49
+ stdout,
50
+ stderr
51
+ };
52
+ }
53
+ async spawn({
54
+ command,
55
+ workingDirectory,
56
+ env,
57
+ abortSignal
58
+ }) {
59
+ abortSignal == null ? void 0 : abortSignal.throwIfAborted();
60
+ const live = await this.sandbox.runCommand({
61
+ cmd: "bash",
62
+ args: ["-c", command],
63
+ detached: true,
64
+ ...workingDirectory !== void 0 ? { cwd: workingDirectory } : {},
65
+ ...env !== void 0 ? { env } : {},
66
+ ...abortSignal !== void 0 ? { signal: abortSignal } : {}
67
+ });
68
+ return createSandboxProcess(live, abortSignal);
69
+ }
70
+ async readFile({
71
+ path,
72
+ abortSignal
73
+ }) {
74
+ const bytes = await this.readBinaryFile({ path, abortSignal });
75
+ if (bytes == null) return null;
76
+ return bytesToStream(bytes);
77
+ }
78
+ async readBinaryFile({
79
+ path,
80
+ abortSignal
81
+ }) {
82
+ abortSignal == null ? void 0 : abortSignal.throwIfAborted();
83
+ const resolved = this.resolvePath(path);
84
+ try {
85
+ const bytes = await this.sandbox.bashEnvInstance.fs.readFileBuffer(resolved);
86
+ return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
87
+ } catch (error) {
88
+ if (isFileNotFoundError(error)) return null;
89
+ throw error;
90
+ }
91
+ }
92
+ async readTextFile({
93
+ path,
94
+ encoding = "utf-8",
95
+ startLine,
96
+ endLine,
97
+ abortSignal
98
+ }) {
99
+ const bytes = await this.readBinaryFile({ path, abortSignal });
100
+ if (bytes == null) return null;
101
+ const text = Buffer.from(bytes).toString(encoding);
102
+ return extractLines({ text, startLine, endLine });
103
+ }
104
+ async writeFile({
105
+ path,
106
+ content,
107
+ abortSignal
108
+ }) {
109
+ const bytes = await collectStream(content);
110
+ await this.writeBinaryFile({ path, content: bytes, abortSignal });
111
+ }
112
+ async writeBinaryFile({
113
+ path,
114
+ content,
115
+ abortSignal
116
+ }) {
117
+ abortSignal == null ? void 0 : abortSignal.throwIfAborted();
118
+ const resolved = this.resolvePath(path);
119
+ await this.sandbox.bashEnvInstance.fs.writeFile(resolved, content);
120
+ }
121
+ async writeTextFile({
122
+ path,
123
+ content,
124
+ encoding = "utf-8",
125
+ abortSignal
126
+ }) {
127
+ const buffer = Buffer.from(content, encoding);
128
+ await this.writeBinaryFile({
129
+ path,
130
+ content: new Uint8Array(
131
+ buffer.buffer,
132
+ buffer.byteOffset,
133
+ buffer.byteLength
134
+ ),
135
+ abortSignal
136
+ });
137
+ }
138
+ };
139
+ function createSandboxProcess(command, abortSignal) {
140
+ const encoder = new TextEncoder();
141
+ const controllers = {};
142
+ const stdout = new ReadableStream({
143
+ start(controller) {
144
+ controllers.stdout = controller;
145
+ }
146
+ });
147
+ const stderr = new ReadableStream({
148
+ start(controller) {
149
+ controllers.stderr = controller;
150
+ }
151
+ });
152
+ const drained = (async () => {
153
+ var _a, _b, _c, _d;
154
+ try {
155
+ for await (const message of command.logs()) {
156
+ const target = message.type === "stdout" ? controllers.stdout : controllers.stderr;
157
+ target == null ? void 0 : target.enqueue(encoder.encode(message.data));
158
+ }
159
+ (_a = controllers.stdout) == null ? void 0 : _a.close();
160
+ (_b = controllers.stderr) == null ? void 0 : _b.close();
161
+ } catch (error) {
162
+ (_c = controllers.stdout) == null ? void 0 : _c.error(error);
163
+ (_d = controllers.stderr) == null ? void 0 : _d.error(error);
164
+ }
165
+ })();
166
+ return {
167
+ stdout,
168
+ stderr,
169
+ async wait() {
170
+ var _a;
171
+ const finished = await command.wait();
172
+ await drained;
173
+ if (abortSignal == null ? void 0 : abortSignal.aborted) {
174
+ throw (_a = abortSignal.reason) != null ? _a : new DOMException("Aborted", "AbortError");
175
+ }
176
+ return { exitCode: finished.exitCode };
177
+ },
178
+ async kill() {
179
+ await command.kill();
180
+ }
181
+ };
182
+ }
183
+ function bytesToStream(bytes) {
184
+ return new ReadableStream({
185
+ start(controller) {
186
+ controller.enqueue(bytes);
187
+ controller.close();
188
+ }
189
+ });
190
+ }
191
+ async function collectStream(stream) {
192
+ const reader = stream.getReader();
193
+ const chunks = [];
194
+ let total = 0;
195
+ while (true) {
196
+ const { value, done } = await reader.read();
197
+ if (done) break;
198
+ if (value) {
199
+ chunks.push(value);
200
+ total += value.byteLength;
201
+ }
202
+ }
203
+ const out = new Uint8Array(total);
204
+ let offset = 0;
205
+ for (const chunk of chunks) {
206
+ out.set(chunk, offset);
207
+ offset += chunk.byteLength;
208
+ }
209
+ return out;
210
+ }
211
+ function isFileNotFoundError(error) {
212
+ if (error == null || typeof error !== "object") return false;
213
+ const code = error.code;
214
+ if (code === "ENOENT") return true;
215
+ const message = error.message;
216
+ return typeof message === "string" && /no such file|not found|does not exist|ENOENT/i.test(message);
217
+ }
218
+
219
+ // src/just-bash-network-sandbox-session.ts
220
+ var JUST_BASH_PROVIDER_ID = "just-bash-sandbox";
221
+ var JustBashNetworkSandboxSession = class extends JustBashSandboxSession {
222
+ constructor(input) {
223
+ super(input.sandbox);
224
+ this.ports = [];
225
+ this.getPortUrl = async (_options) => {
226
+ throw new HarnessCapabilityUnsupportedError({
227
+ harnessId: JUST_BASH_PROVIDER_ID,
228
+ message: "just-bash sandboxes run in-process and cannot expose a port URL. Use a hosted sandbox (e.g. @ai-sdk/sandbox-vercel) for bridge-backed harness adapters."
229
+ });
230
+ };
231
+ this.stop = async () => {
232
+ if (!this.ownsLifecycle) return;
233
+ };
234
+ this.destroy = async () => {
235
+ await this.stop();
236
+ };
237
+ this.ownsLifecycle = input.ownsLifecycle;
238
+ this.id = randomUUID();
239
+ this.defaultWorkingDirectory = input.sandbox.bashEnvInstance.getCwd();
240
+ }
241
+ restricted() {
242
+ return new JustBashSandboxSession(this.sandbox);
243
+ }
244
+ };
245
+
246
+ // src/just-bash-sandbox.ts
247
+ var JUST_BASH_PROVIDER_ID2 = "just-bash-sandbox";
248
+ function createJustBashSandbox(settings = {}) {
249
+ return new JustBashSandboxProvider(settings);
250
+ }
251
+ var JustBashSandboxProvider = class {
252
+ constructor(settings) {
253
+ this.settings = settings;
254
+ this.specificationVersion = "harness-sandbox-v1";
255
+ this.providerId = JUST_BASH_PROVIDER_ID2;
256
+ this.createSession = async (options) => {
257
+ var _a;
258
+ (_a = options == null ? void 0 : options.abortSignal) == null ? void 0 : _a.throwIfAborted();
259
+ if ("sandbox" in this.settings && this.settings.sandbox) {
260
+ return new JustBashNetworkSandboxSession({
261
+ sandbox: this.settings.sandbox,
262
+ ownsLifecycle: false
263
+ });
264
+ }
265
+ const createParams = this.settings;
266
+ const sandbox = await Sandbox.create(createParams);
267
+ const sandboxSession = new JustBashNetworkSandboxSession({
268
+ sandbox,
269
+ ownsLifecycle: true
270
+ });
271
+ if ((options == null ? void 0 : options.onFirstCreate) != null) {
272
+ await options.onFirstCreate(sandboxSession.restricted(), {
273
+ abortSignal: options == null ? void 0 : options.abortSignal
274
+ });
275
+ }
276
+ return sandboxSession;
277
+ };
278
+ }
279
+ };
280
+ export {
281
+ JustBashSandboxProvider,
282
+ createJustBashSandbox
283
+ };
284
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/just-bash-sandbox.ts","../src/just-bash-network-sandbox-session.ts","../src/just-bash-sandbox-session.ts"],"sourcesContent":["import type {\n HarnessV1NetworkSandboxSession,\n HarnessV1SandboxProvider,\n} from '@ai-sdk/harness';\nimport type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';\nimport { Sandbox } from 'just-bash';\nimport { JustBashNetworkSandboxSession } from './just-bash-network-sandbox-session';\n\n/**\n * Parameters forwarded to `just-bash`'s `Sandbox.create` when creating a\n * sandbox from scratch. Aliased directly from the underlying SDK so the full\n * surface is available without us re-declaring it.\n */\ntype JustBashSandboxCreateParams = NonNullable<\n Parameters<typeof Sandbox.create>[0]\n>;\n\n/**\n * Settings for {@link createJustBashSandbox}. Two mutually-exclusive shapes:\n *\n * - `{ sandbox }` — wrap an already-created `just-bash` `Sandbox`. The caller\n * owns its lifecycle.\n * - {@link JustBashSandboxCreateParams} fields — provider calls\n * `Sandbox.create(settings)` on every `createSession()`.\n *\n * just-bash has no port exposure and no snapshot mechanism, so image\n * management is a per-call no-op: if an adapter declares a bootstrap recipe\n * the provider runs it once on the freshly-created sandbox before returning.\n */\nexport type JustBashSandboxSettings =\n | { sandbox: Sandbox }\n | (JustBashSandboxCreateParams & { sandbox?: never });\n\nconst JUST_BASH_PROVIDER_ID = 'just-bash-sandbox';\n\nexport function createJustBashSandbox(\n settings: JustBashSandboxSettings = {} as JustBashSandboxSettings,\n): HarnessV1SandboxProvider {\n return new JustBashSandboxProvider(settings);\n}\n\n/**\n * `HarnessV1SandboxProvider` implementation backed by `just-bash`. Useful for\n * non-bridge harness flows and for handing a local `Experimental_SandboxSession`\n * to AI SDK tools — use `provider.createSession()` then\n * `sandboxSession.restricted()` to get the latter.\n *\n * Note: just-bash cannot expose ports, so bridge-backed harness adapters\n * (claude-code, codex) will reject this provider at start.\n */\nexport class JustBashSandboxProvider implements HarnessV1SandboxProvider {\n readonly specificationVersion = 'harness-sandbox-v1' as const;\n readonly providerId = JUST_BASH_PROVIDER_ID;\n\n constructor(private readonly settings: JustBashSandboxSettings) {}\n\n createSession = async (options?: {\n sessionId?: string;\n abortSignal?: AbortSignal;\n identity?: string;\n onFirstCreate?: (\n session: SandboxSession,\n opts: { abortSignal?: AbortSignal },\n ) => Promise<void>;\n }): Promise<HarnessV1NetworkSandboxSession> => {\n options?.abortSignal?.throwIfAborted();\n\n if ('sandbox' in this.settings && this.settings.sandbox) {\n return new JustBashNetworkSandboxSession({\n sandbox: this.settings.sandbox,\n ownsLifecycle: false,\n });\n }\n\n const createParams = this.settings as JustBashSandboxCreateParams;\n\n const sandbox = await Sandbox.create(createParams);\n const sandboxSession = new JustBashNetworkSandboxSession({\n sandbox,\n ownsLifecycle: true,\n });\n\n if (options?.onFirstCreate != null) {\n await options.onFirstCreate(sandboxSession.restricted(), {\n abortSignal: options?.abortSignal,\n });\n }\n\n return sandboxSession;\n };\n}\n","import {\n HarnessCapabilityUnsupportedError,\n type HarnessV1NetworkSandboxSession,\n} from '@ai-sdk/harness';\nimport type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';\nimport { randomUUID } from 'node:crypto';\nimport type { Sandbox } from 'just-bash';\nimport { JustBashSandboxSession } from './just-bash-sandbox-session';\n\nconst JUST_BASH_PROVIDER_ID = 'just-bash-sandbox';\n\n/**\n * `HarnessV1NetworkSandboxSession` backed by a `just-bash` `Sandbox`. It extends\n * {@link JustBashSandboxSession} with the infra surface. Exposes no ports —\n * there is no network namespace and no way to publish an HTTP endpoint\n * reachable from outside the host process. Bridge-backed harness adapters that\n * need `getPortUrl` will fail with `HarnessCapabilityUnsupportedError` at start.\n *\n * Network policy is not implementable locally either — `setNetworkPolicy` is\n * omitted.\n */\nexport class JustBashNetworkSandboxSession\n extends JustBashSandboxSession\n implements HarnessV1NetworkSandboxSession\n{\n /**\n * Minted at construct time. just-bash has no native identifier and no\n * cross-process reattach surface; the value exists only to satisfy the\n * `HarnessV1NetworkSandboxSession.id` contract.\n */\n readonly id: string;\n readonly defaultWorkingDirectory: string;\n private readonly ownsLifecycle: boolean;\n\n constructor(input: { sandbox: Sandbox; ownsLifecycle: boolean }) {\n super(input.sandbox);\n this.ownsLifecycle = input.ownsLifecycle;\n this.id = randomUUID();\n this.defaultWorkingDirectory = input.sandbox.bashEnvInstance.getCwd();\n }\n\n readonly ports: ReadonlyArray<number> = [];\n\n restricted(): SandboxSession {\n return new JustBashSandboxSession(this.sandbox);\n }\n\n getPortUrl = async (_options: {\n port: number;\n protocol?: 'http' | 'https' | 'ws';\n }): Promise<string> => {\n throw new HarnessCapabilityUnsupportedError({\n harnessId: JUST_BASH_PROVIDER_ID,\n message:\n 'just-bash sandboxes run in-process and cannot expose a port URL. ' +\n 'Use a hosted sandbox (e.g. @ai-sdk/sandbox-vercel) for bridge-backed harness adapters.',\n });\n };\n\n stop = async (): Promise<void> => {\n if (!this.ownsLifecycle) return;\n // just-bash has no explicit shutdown; the sandbox is garbage-collected\n // along with its in-memory filesystem once references drop.\n };\n\n destroy = async (): Promise<void> => {\n await this.stop();\n };\n}\n","import { isAbsolute, posix } from 'node:path';\nimport {\n extractLines,\n type Experimental_SandboxSession,\n type Experimental_SandboxProcess,\n} from '@ai-sdk/provider-utils';\nimport type { Sandbox, SandboxCommand } from 'just-bash';\n\n/**\n * `Experimental_SandboxSession` implementation backed by a `just-bash`\n * `Sandbox`. File operations and spawned processes share the same in-memory\n * filesystem, so a bridge process spawned via `spawn` can read files the host\n * wrote via `writeTextFile` (and vice versa).\n *\n * This is the tool-safe surface returned by\n * `JustBashNetworkSandboxSession.restricted()` — not constructed directly by\n * consumers.\n */\nexport class JustBashSandboxSession implements Experimental_SandboxSession {\n constructor(protected readonly sandbox: Sandbox) {}\n\n get description(): string {\n return [\n 'just-bash JavaScript bash environment with a virtual filesystem.',\n 'Filesystem changes and spawned processes share the same in-memory state.',\n `Current working directory: ${this.sandbox.bashEnvInstance.getCwd()}`,\n ].join('\\n');\n }\n\n private resolvePath(path: string): string {\n return isAbsolute(path)\n ? path\n : posix.join(this.sandbox.bashEnvInstance.getCwd(), path);\n }\n\n async run({\n command,\n workingDirectory,\n env,\n abortSignal,\n }: {\n command: string;\n workingDirectory?: string;\n env?: Record<string, string>;\n abortSignal?: AbortSignal;\n }): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n abortSignal?.throwIfAborted();\n\n const finished = await this.sandbox.runCommand({\n cmd: 'bash',\n args: ['-c', command],\n ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),\n ...(env !== undefined ? { env } : {}),\n ...(abortSignal !== undefined ? { signal: abortSignal } : {}),\n });\n\n const [stdout, stderr] = await Promise.all([\n finished.stdout(),\n finished.stderr(),\n ]);\n\n return {\n exitCode: finished.exitCode,\n stdout,\n stderr,\n };\n }\n\n async spawn({\n command,\n workingDirectory,\n env,\n abortSignal,\n }: {\n command: string;\n workingDirectory?: string;\n env?: Record<string, string>;\n abortSignal?: AbortSignal;\n }): Promise<Experimental_SandboxProcess> {\n abortSignal?.throwIfAborted();\n\n const live = await this.sandbox.runCommand({\n cmd: 'bash',\n args: ['-c', command],\n detached: true,\n ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),\n ...(env !== undefined ? { env } : {}),\n ...(abortSignal !== undefined ? { signal: abortSignal } : {}),\n });\n\n return createSandboxProcess(live, abortSignal);\n }\n\n async readFile({\n path,\n abortSignal,\n }: {\n path: string;\n abortSignal?: AbortSignal;\n }): Promise<ReadableStream<Uint8Array> | null> {\n const bytes = await this.readBinaryFile({ path, abortSignal });\n if (bytes == null) return null;\n return bytesToStream(bytes);\n }\n\n async readBinaryFile({\n path,\n abortSignal,\n }: {\n path: string;\n abortSignal?: AbortSignal;\n }): Promise<Uint8Array | null> {\n abortSignal?.throwIfAborted();\n const resolved = this.resolvePath(path);\n try {\n const bytes =\n await this.sandbox.bashEnvInstance.fs.readFileBuffer(resolved);\n return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n } catch (error) {\n if (isFileNotFoundError(error)) return null;\n throw error;\n }\n }\n\n async readTextFile({\n path,\n encoding = 'utf-8',\n startLine,\n endLine,\n abortSignal,\n }: {\n path: string;\n encoding?: string;\n startLine?: number;\n endLine?: number;\n abortSignal?: AbortSignal;\n }): Promise<string | null> {\n const bytes = await this.readBinaryFile({ path, abortSignal });\n if (bytes == null) return null;\n const text = Buffer.from(bytes).toString(encoding as BufferEncoding);\n return extractLines({ text, startLine, endLine });\n }\n\n async writeFile({\n path,\n content,\n abortSignal,\n }: {\n path: string;\n content: ReadableStream<Uint8Array>;\n abortSignal?: AbortSignal;\n }): Promise<void> {\n const bytes = await collectStream(content);\n await this.writeBinaryFile({ path, content: bytes, abortSignal });\n }\n\n async writeBinaryFile({\n path,\n content,\n abortSignal,\n }: {\n path: string;\n content: Uint8Array;\n abortSignal?: AbortSignal;\n }): Promise<void> {\n abortSignal?.throwIfAborted();\n const resolved = this.resolvePath(path);\n await this.sandbox.bashEnvInstance.fs.writeFile(resolved, content);\n }\n\n async writeTextFile({\n path,\n content,\n encoding = 'utf-8',\n abortSignal,\n }: {\n path: string;\n content: string;\n encoding?: string;\n abortSignal?: AbortSignal;\n }): Promise<void> {\n const buffer = Buffer.from(content, encoding as BufferEncoding);\n await this.writeBinaryFile({\n path,\n content: new Uint8Array(\n buffer.buffer,\n buffer.byteOffset,\n buffer.byteLength,\n ),\n abortSignal,\n });\n }\n}\n\nfunction createSandboxProcess(\n command: SandboxCommand,\n abortSignal: AbortSignal | undefined,\n): Experimental_SandboxProcess {\n const encoder = new TextEncoder();\n const controllers: {\n stdout?: ReadableStreamDefaultController<Uint8Array>;\n stderr?: ReadableStreamDefaultController<Uint8Array>;\n } = {};\n\n const stdout = new ReadableStream<Uint8Array>({\n start(controller) {\n controllers.stdout = controller;\n },\n });\n\n const stderr = new ReadableStream<Uint8Array>({\n start(controller) {\n controllers.stderr = controller;\n },\n });\n\n const drained = (async () => {\n try {\n for await (const message of command.logs()) {\n const target =\n message.type === 'stdout' ? controllers.stdout : controllers.stderr;\n target?.enqueue(encoder.encode(message.data));\n }\n controllers.stdout?.close();\n controllers.stderr?.close();\n } catch (error) {\n controllers.stdout?.error(error);\n controllers.stderr?.error(error);\n }\n })();\n\n return {\n stdout,\n stderr,\n async wait(): Promise<{ exitCode: number }> {\n const finished = await command.wait();\n await drained;\n if (abortSignal?.aborted) {\n throw abortSignal.reason ?? new DOMException('Aborted', 'AbortError');\n }\n return { exitCode: finished.exitCode };\n },\n async kill(): Promise<void> {\n await command.kill();\n },\n };\n}\n\nfunction bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n}\n\nasync function collectStream(\n stream: ReadableStream<Uint8Array>,\n): Promise<Uint8Array> {\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n if (value) {\n chunks.push(value);\n total += value.byteLength;\n }\n }\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n}\n\nfunction isFileNotFoundError(error: unknown): boolean {\n if (error == null || typeof error !== 'object') return false;\n const code = (error as { code?: unknown }).code;\n if (code === 'ENOENT') return true;\n const message = (error as { message?: unknown }).message;\n return (\n typeof message === 'string' &&\n /no such file|not found|does not exist|ENOENT/i.test(message)\n );\n}\n"],"mappings":"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","names":["JUST_BASH_PROVIDER_ID"]}
package/package.json CHANGED
@@ -1 +1,69 @@
1
- {"name":"@ai-sdk/sandbox-just-bash","version":"0.0.0"}
1
+ {
2
+ "name": "@ai-sdk/sandbox-just-bash",
3
+ "version": "1.0.0-beta.15",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "source": "./src/index.ts",
10
+ "files": [
11
+ "dist/**/*",
12
+ "src",
13
+ "!src/**/*.test.ts",
14
+ "!src/**/*.test-d.ts",
15
+ "!src/**/__snapshots__",
16
+ "!src/**/__fixtures__",
17
+ "CHANGELOG.md",
18
+ "README.md"
19
+ ],
20
+ "exports": {
21
+ "./package.json": "./package.json",
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "just-bash": "^2.14.5",
30
+ "@ai-sdk/harness": "1.0.0-beta.15",
31
+ "@ai-sdk/provider-utils": "5.0.0-beta.49"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "22.19.19",
35
+ "tsup": "^8.5.1",
36
+ "typescript": "5.8.3",
37
+ "@vercel/ai-tsconfig": "0.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "provenance": true
45
+ },
46
+ "homepage": "https://ai-sdk.dev/docs",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/vercel/ai",
50
+ "directory": "packages/sandbox-just-bash"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/vercel/ai/issues"
54
+ },
55
+ "keywords": [
56
+ "ai",
57
+ "sandbox",
58
+ "just-bash"
59
+ ],
60
+ "scripts": {
61
+ "build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
62
+ "build:watch": "pnpm clean && tsup --watch",
63
+ "clean": "del-cli dist *.tsbuildinfo",
64
+ "type-check": "tsc --build",
65
+ "test": "pnpm test:node",
66
+ "test:watch": "vitest --config vitest.node.config.js",
67
+ "test:node": "vitest --config vitest.node.config.js --run"
68
+ }
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export {
2
+ createJustBashSandbox,
3
+ JustBashSandboxProvider,
4
+ type JustBashSandboxSettings,
5
+ } from './just-bash-sandbox';
@@ -0,0 +1,69 @@
1
+ import {
2
+ HarnessCapabilityUnsupportedError,
3
+ type HarnessV1NetworkSandboxSession,
4
+ } from '@ai-sdk/harness';
5
+ import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
6
+ import { randomUUID } from 'node:crypto';
7
+ import type { Sandbox } from 'just-bash';
8
+ import { JustBashSandboxSession } from './just-bash-sandbox-session';
9
+
10
+ const JUST_BASH_PROVIDER_ID = 'just-bash-sandbox';
11
+
12
+ /**
13
+ * `HarnessV1NetworkSandboxSession` backed by a `just-bash` `Sandbox`. It extends
14
+ * {@link JustBashSandboxSession} with the infra surface. Exposes no ports —
15
+ * there is no network namespace and no way to publish an HTTP endpoint
16
+ * reachable from outside the host process. Bridge-backed harness adapters that
17
+ * need `getPortUrl` will fail with `HarnessCapabilityUnsupportedError` at start.
18
+ *
19
+ * Network policy is not implementable locally either — `setNetworkPolicy` is
20
+ * omitted.
21
+ */
22
+ export class JustBashNetworkSandboxSession
23
+ extends JustBashSandboxSession
24
+ implements HarnessV1NetworkSandboxSession
25
+ {
26
+ /**
27
+ * Minted at construct time. just-bash has no native identifier and no
28
+ * cross-process reattach surface; the value exists only to satisfy the
29
+ * `HarnessV1NetworkSandboxSession.id` contract.
30
+ */
31
+ readonly id: string;
32
+ readonly defaultWorkingDirectory: string;
33
+ private readonly ownsLifecycle: boolean;
34
+
35
+ constructor(input: { sandbox: Sandbox; ownsLifecycle: boolean }) {
36
+ super(input.sandbox);
37
+ this.ownsLifecycle = input.ownsLifecycle;
38
+ this.id = randomUUID();
39
+ this.defaultWorkingDirectory = input.sandbox.bashEnvInstance.getCwd();
40
+ }
41
+
42
+ readonly ports: ReadonlyArray<number> = [];
43
+
44
+ restricted(): SandboxSession {
45
+ return new JustBashSandboxSession(this.sandbox);
46
+ }
47
+
48
+ getPortUrl = async (_options: {
49
+ port: number;
50
+ protocol?: 'http' | 'https' | 'ws';
51
+ }): Promise<string> => {
52
+ throw new HarnessCapabilityUnsupportedError({
53
+ harnessId: JUST_BASH_PROVIDER_ID,
54
+ message:
55
+ 'just-bash sandboxes run in-process and cannot expose a port URL. ' +
56
+ 'Use a hosted sandbox (e.g. @ai-sdk/sandbox-vercel) for bridge-backed harness adapters.',
57
+ });
58
+ };
59
+
60
+ stop = async (): Promise<void> => {
61
+ if (!this.ownsLifecycle) return;
62
+ // just-bash has no explicit shutdown; the sandbox is garbage-collected
63
+ // along with its in-memory filesystem once references drop.
64
+ };
65
+
66
+ destroy = async (): Promise<void> => {
67
+ await this.stop();
68
+ };
69
+ }
@@ -0,0 +1,290 @@
1
+ import { isAbsolute, posix } from 'node:path';
2
+ import {
3
+ extractLines,
4
+ type Experimental_SandboxSession,
5
+ type Experimental_SandboxProcess,
6
+ } from '@ai-sdk/provider-utils';
7
+ import type { Sandbox, SandboxCommand } from 'just-bash';
8
+
9
+ /**
10
+ * `Experimental_SandboxSession` implementation backed by a `just-bash`
11
+ * `Sandbox`. File operations and spawned processes share the same in-memory
12
+ * filesystem, so a bridge process spawned via `spawn` can read files the host
13
+ * wrote via `writeTextFile` (and vice versa).
14
+ *
15
+ * This is the tool-safe surface returned by
16
+ * `JustBashNetworkSandboxSession.restricted()` — not constructed directly by
17
+ * consumers.
18
+ */
19
+ export class JustBashSandboxSession implements Experimental_SandboxSession {
20
+ constructor(protected readonly sandbox: Sandbox) {}
21
+
22
+ get description(): string {
23
+ return [
24
+ 'just-bash JavaScript bash environment with a virtual filesystem.',
25
+ 'Filesystem changes and spawned processes share the same in-memory state.',
26
+ `Current working directory: ${this.sandbox.bashEnvInstance.getCwd()}`,
27
+ ].join('\n');
28
+ }
29
+
30
+ private resolvePath(path: string): string {
31
+ return isAbsolute(path)
32
+ ? path
33
+ : posix.join(this.sandbox.bashEnvInstance.getCwd(), path);
34
+ }
35
+
36
+ async run({
37
+ command,
38
+ workingDirectory,
39
+ env,
40
+ abortSignal,
41
+ }: {
42
+ command: string;
43
+ workingDirectory?: string;
44
+ env?: Record<string, string>;
45
+ abortSignal?: AbortSignal;
46
+ }): Promise<{ exitCode: number; stdout: string; stderr: string }> {
47
+ abortSignal?.throwIfAborted();
48
+
49
+ const finished = await this.sandbox.runCommand({
50
+ cmd: 'bash',
51
+ args: ['-c', command],
52
+ ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),
53
+ ...(env !== undefined ? { env } : {}),
54
+ ...(abortSignal !== undefined ? { signal: abortSignal } : {}),
55
+ });
56
+
57
+ const [stdout, stderr] = await Promise.all([
58
+ finished.stdout(),
59
+ finished.stderr(),
60
+ ]);
61
+
62
+ return {
63
+ exitCode: finished.exitCode,
64
+ stdout,
65
+ stderr,
66
+ };
67
+ }
68
+
69
+ async spawn({
70
+ command,
71
+ workingDirectory,
72
+ env,
73
+ abortSignal,
74
+ }: {
75
+ command: string;
76
+ workingDirectory?: string;
77
+ env?: Record<string, string>;
78
+ abortSignal?: AbortSignal;
79
+ }): Promise<Experimental_SandboxProcess> {
80
+ abortSignal?.throwIfAborted();
81
+
82
+ const live = await this.sandbox.runCommand({
83
+ cmd: 'bash',
84
+ args: ['-c', command],
85
+ detached: true,
86
+ ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),
87
+ ...(env !== undefined ? { env } : {}),
88
+ ...(abortSignal !== undefined ? { signal: abortSignal } : {}),
89
+ });
90
+
91
+ return createSandboxProcess(live, abortSignal);
92
+ }
93
+
94
+ async readFile({
95
+ path,
96
+ abortSignal,
97
+ }: {
98
+ path: string;
99
+ abortSignal?: AbortSignal;
100
+ }): Promise<ReadableStream<Uint8Array> | null> {
101
+ const bytes = await this.readBinaryFile({ path, abortSignal });
102
+ if (bytes == null) return null;
103
+ return bytesToStream(bytes);
104
+ }
105
+
106
+ async readBinaryFile({
107
+ path,
108
+ abortSignal,
109
+ }: {
110
+ path: string;
111
+ abortSignal?: AbortSignal;
112
+ }): Promise<Uint8Array | null> {
113
+ abortSignal?.throwIfAborted();
114
+ const resolved = this.resolvePath(path);
115
+ try {
116
+ const bytes =
117
+ await this.sandbox.bashEnvInstance.fs.readFileBuffer(resolved);
118
+ return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
119
+ } catch (error) {
120
+ if (isFileNotFoundError(error)) return null;
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ async readTextFile({
126
+ path,
127
+ encoding = 'utf-8',
128
+ startLine,
129
+ endLine,
130
+ abortSignal,
131
+ }: {
132
+ path: string;
133
+ encoding?: string;
134
+ startLine?: number;
135
+ endLine?: number;
136
+ abortSignal?: AbortSignal;
137
+ }): Promise<string | null> {
138
+ const bytes = await this.readBinaryFile({ path, abortSignal });
139
+ if (bytes == null) return null;
140
+ const text = Buffer.from(bytes).toString(encoding as BufferEncoding);
141
+ return extractLines({ text, startLine, endLine });
142
+ }
143
+
144
+ async writeFile({
145
+ path,
146
+ content,
147
+ abortSignal,
148
+ }: {
149
+ path: string;
150
+ content: ReadableStream<Uint8Array>;
151
+ abortSignal?: AbortSignal;
152
+ }): Promise<void> {
153
+ const bytes = await collectStream(content);
154
+ await this.writeBinaryFile({ path, content: bytes, abortSignal });
155
+ }
156
+
157
+ async writeBinaryFile({
158
+ path,
159
+ content,
160
+ abortSignal,
161
+ }: {
162
+ path: string;
163
+ content: Uint8Array;
164
+ abortSignal?: AbortSignal;
165
+ }): Promise<void> {
166
+ abortSignal?.throwIfAborted();
167
+ const resolved = this.resolvePath(path);
168
+ await this.sandbox.bashEnvInstance.fs.writeFile(resolved, content);
169
+ }
170
+
171
+ async writeTextFile({
172
+ path,
173
+ content,
174
+ encoding = 'utf-8',
175
+ abortSignal,
176
+ }: {
177
+ path: string;
178
+ content: string;
179
+ encoding?: string;
180
+ abortSignal?: AbortSignal;
181
+ }): Promise<void> {
182
+ const buffer = Buffer.from(content, encoding as BufferEncoding);
183
+ await this.writeBinaryFile({
184
+ path,
185
+ content: new Uint8Array(
186
+ buffer.buffer,
187
+ buffer.byteOffset,
188
+ buffer.byteLength,
189
+ ),
190
+ abortSignal,
191
+ });
192
+ }
193
+ }
194
+
195
+ function createSandboxProcess(
196
+ command: SandboxCommand,
197
+ abortSignal: AbortSignal | undefined,
198
+ ): Experimental_SandboxProcess {
199
+ const encoder = new TextEncoder();
200
+ const controllers: {
201
+ stdout?: ReadableStreamDefaultController<Uint8Array>;
202
+ stderr?: ReadableStreamDefaultController<Uint8Array>;
203
+ } = {};
204
+
205
+ const stdout = new ReadableStream<Uint8Array>({
206
+ start(controller) {
207
+ controllers.stdout = controller;
208
+ },
209
+ });
210
+
211
+ const stderr = new ReadableStream<Uint8Array>({
212
+ start(controller) {
213
+ controllers.stderr = controller;
214
+ },
215
+ });
216
+
217
+ const drained = (async () => {
218
+ try {
219
+ for await (const message of command.logs()) {
220
+ const target =
221
+ message.type === 'stdout' ? controllers.stdout : controllers.stderr;
222
+ target?.enqueue(encoder.encode(message.data));
223
+ }
224
+ controllers.stdout?.close();
225
+ controllers.stderr?.close();
226
+ } catch (error) {
227
+ controllers.stdout?.error(error);
228
+ controllers.stderr?.error(error);
229
+ }
230
+ })();
231
+
232
+ return {
233
+ stdout,
234
+ stderr,
235
+ async wait(): Promise<{ exitCode: number }> {
236
+ const finished = await command.wait();
237
+ await drained;
238
+ if (abortSignal?.aborted) {
239
+ throw abortSignal.reason ?? new DOMException('Aborted', 'AbortError');
240
+ }
241
+ return { exitCode: finished.exitCode };
242
+ },
243
+ async kill(): Promise<void> {
244
+ await command.kill();
245
+ },
246
+ };
247
+ }
248
+
249
+ function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
250
+ return new ReadableStream<Uint8Array>({
251
+ start(controller) {
252
+ controller.enqueue(bytes);
253
+ controller.close();
254
+ },
255
+ });
256
+ }
257
+
258
+ async function collectStream(
259
+ stream: ReadableStream<Uint8Array>,
260
+ ): Promise<Uint8Array> {
261
+ const reader = stream.getReader();
262
+ const chunks: Uint8Array[] = [];
263
+ let total = 0;
264
+ while (true) {
265
+ const { value, done } = await reader.read();
266
+ if (done) break;
267
+ if (value) {
268
+ chunks.push(value);
269
+ total += value.byteLength;
270
+ }
271
+ }
272
+ const out = new Uint8Array(total);
273
+ let offset = 0;
274
+ for (const chunk of chunks) {
275
+ out.set(chunk, offset);
276
+ offset += chunk.byteLength;
277
+ }
278
+ return out;
279
+ }
280
+
281
+ function isFileNotFoundError(error: unknown): boolean {
282
+ if (error == null || typeof error !== 'object') return false;
283
+ const code = (error as { code?: unknown }).code;
284
+ if (code === 'ENOENT') return true;
285
+ const message = (error as { message?: unknown }).message;
286
+ return (
287
+ typeof message === 'string' &&
288
+ /no such file|not found|does not exist|ENOENT/i.test(message)
289
+ );
290
+ }
@@ -0,0 +1,91 @@
1
+ import type {
2
+ HarnessV1NetworkSandboxSession,
3
+ HarnessV1SandboxProvider,
4
+ } from '@ai-sdk/harness';
5
+ import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
6
+ import { Sandbox } from 'just-bash';
7
+ import { JustBashNetworkSandboxSession } from './just-bash-network-sandbox-session';
8
+
9
+ /**
10
+ * Parameters forwarded to `just-bash`'s `Sandbox.create` when creating a
11
+ * sandbox from scratch. Aliased directly from the underlying SDK so the full
12
+ * surface is available without us re-declaring it.
13
+ */
14
+ type JustBashSandboxCreateParams = NonNullable<
15
+ Parameters<typeof Sandbox.create>[0]
16
+ >;
17
+
18
+ /**
19
+ * Settings for {@link createJustBashSandbox}. Two mutually-exclusive shapes:
20
+ *
21
+ * - `{ sandbox }` — wrap an already-created `just-bash` `Sandbox`. The caller
22
+ * owns its lifecycle.
23
+ * - {@link JustBashSandboxCreateParams} fields — provider calls
24
+ * `Sandbox.create(settings)` on every `createSession()`.
25
+ *
26
+ * just-bash has no port exposure and no snapshot mechanism, so image
27
+ * management is a per-call no-op: if an adapter declares a bootstrap recipe
28
+ * the provider runs it once on the freshly-created sandbox before returning.
29
+ */
30
+ export type JustBashSandboxSettings =
31
+ | { sandbox: Sandbox }
32
+ | (JustBashSandboxCreateParams & { sandbox?: never });
33
+
34
+ const JUST_BASH_PROVIDER_ID = 'just-bash-sandbox';
35
+
36
+ export function createJustBashSandbox(
37
+ settings: JustBashSandboxSettings = {} as JustBashSandboxSettings,
38
+ ): HarnessV1SandboxProvider {
39
+ return new JustBashSandboxProvider(settings);
40
+ }
41
+
42
+ /**
43
+ * `HarnessV1SandboxProvider` implementation backed by `just-bash`. Useful for
44
+ * non-bridge harness flows and for handing a local `Experimental_SandboxSession`
45
+ * to AI SDK tools — use `provider.createSession()` then
46
+ * `sandboxSession.restricted()` to get the latter.
47
+ *
48
+ * Note: just-bash cannot expose ports, so bridge-backed harness adapters
49
+ * (claude-code, codex) will reject this provider at start.
50
+ */
51
+ export class JustBashSandboxProvider implements HarnessV1SandboxProvider {
52
+ readonly specificationVersion = 'harness-sandbox-v1' as const;
53
+ readonly providerId = JUST_BASH_PROVIDER_ID;
54
+
55
+ constructor(private readonly settings: JustBashSandboxSettings) {}
56
+
57
+ createSession = async (options?: {
58
+ sessionId?: string;
59
+ abortSignal?: AbortSignal;
60
+ identity?: string;
61
+ onFirstCreate?: (
62
+ session: SandboxSession,
63
+ opts: { abortSignal?: AbortSignal },
64
+ ) => Promise<void>;
65
+ }): Promise<HarnessV1NetworkSandboxSession> => {
66
+ options?.abortSignal?.throwIfAborted();
67
+
68
+ if ('sandbox' in this.settings && this.settings.sandbox) {
69
+ return new JustBashNetworkSandboxSession({
70
+ sandbox: this.settings.sandbox,
71
+ ownsLifecycle: false,
72
+ });
73
+ }
74
+
75
+ const createParams = this.settings as JustBashSandboxCreateParams;
76
+
77
+ const sandbox = await Sandbox.create(createParams);
78
+ const sandboxSession = new JustBashNetworkSandboxSession({
79
+ sandbox,
80
+ ownsLifecycle: true,
81
+ });
82
+
83
+ if (options?.onFirstCreate != null) {
84
+ await options.onFirstCreate(sandboxSession.restricted(), {
85
+ abortSignal: options?.abortSignal,
86
+ });
87
+ }
88
+
89
+ return sandboxSession;
90
+ };
91
+ }