@ai-sdk/sandbox-just-bash 1.0.123 → 1.0.126
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 +27 -0
- package/README.md +12 -19
- package/dist/index.d.ts +38 -12
- package/dist/index.js +182 -92
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +10 -1
- package/src/just-bash-legacy-sandbox-provider.ts +100 -0
- package/src/just-bash-network-sandbox-session.ts +5 -5
- package/src/just-bash-sandbox-session.ts +10 -5
- package/src/just-bash-sandbox.ts +69 -133
- package/src/utils.ts +118 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @ai-sdk/sandbox-just-bash
|
|
2
2
|
|
|
3
|
+
## 1.0.126
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 31742b9: feat(harness): refactor sandbox APIs for cleaner separation of concerns from harness layer, more intuitive DX regardless of how your sandbox is configured, and streamlined handling of sandbox templates/snapshots
|
|
8
|
+
- Updated dependencies [af9597b]
|
|
9
|
+
- Updated dependencies [31742b9]
|
|
10
|
+
- Updated dependencies [bc49f78]
|
|
11
|
+
- @ai-sdk/harness@1.0.126
|
|
12
|
+
- @ai-sdk/provider-utils@5.0.49
|
|
13
|
+
|
|
14
|
+
## 1.0.125
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 5482b08: fix(sandbox-just-bash): preserve command resolution when providing realpath on non-synchronous filesystems
|
|
19
|
+
- Updated dependencies [be877ff]
|
|
20
|
+
- @ai-sdk/provider-utils@5.0.48
|
|
21
|
+
- @ai-sdk/harness@1.0.125
|
|
22
|
+
|
|
23
|
+
## 1.0.124
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [40231b6]
|
|
28
|
+
- @ai-sdk/harness@1.0.124
|
|
29
|
+
|
|
3
30
|
## 1.0.123
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
_This package is **experimental**._
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Sandbox session implementation for [`just-bash`](https://github.com/vercel-labs/just-bash), an in-process JavaScript bash environment with a virtual filesystem.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`just-bash` sandboxes do not expose ports, so they cannot be used with features that require actual network sandboxes.
|
|
8
8
|
|
|
9
9
|
## Setup
|
|
10
10
|
|
|
@@ -14,14 +14,14 @@ npm i @ai-sdk/sandbox-just-bash
|
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Each creation produces a fresh in-memory sandbox and applies an optional harness template.
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import {
|
|
20
|
+
import { createJustBashNetworkSandboxSession } from '@ai-sdk/sandbox-just-bash';
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const networkSandboxSession = await createJustBashNetworkSandboxSession({
|
|
23
|
+
cwd: '/work',
|
|
24
|
+
});
|
|
25
25
|
const sandboxSession = networkSandboxSession.restricted();
|
|
26
26
|
|
|
27
27
|
await sandboxSession.writeTextFile({ path: '/work/hello.txt', content: 'hi' });
|
|
@@ -30,18 +30,11 @@ const { stdout } = await sandboxSession.run({
|
|
|
30
30
|
command: 'cat /work/hello.txt',
|
|
31
31
|
});
|
|
32
32
|
console.log(stdout); // "hi"
|
|
33
|
+
await networkSandboxSession.destroy();
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
### Use cases
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
```
|
|
38
|
+
- Create a fresh in-memory session with `createJustBashNetworkSandboxSession()` and pass it to `HarnessAgent.createSession({ sandboxSession })`. An optional `sandboxId` only labels its wrapper and cannot enforce uniqueness.
|
|
39
|
+
- Take a network sandbox session's process-restricted subset with `networkSandboxSession.restricted()` and use it with AI SDK tools that accept `experimental_sandbox`.
|
|
40
|
+
- Adapt an existing native `Sandbox` with `createJustBashSandboxSessionFromNativeSandbox(nativeSandbox)` for AI SDK tools. `resumeJustBashNetworkSandboxSession()` reports that cross-process resume is unsupported.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { HarnessV1SandboxProvider, HarnessV1NetworkSandboxSession } from '@ai-sdk/harness';
|
|
1
|
+
import { HarnessV1SandboxProvider, HarnessV1NetworkSandboxSession, HarnessV1SandboxSessionCreateOptions, HarnessV1SandboxSessionResumeOptions } from '@ai-sdk/harness';
|
|
2
2
|
import { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
3
|
import { Sandbox } from 'just-bash';
|
|
4
4
|
|
|
5
|
+
type JustBashSandboxCreateParams$1 = NonNullable<Parameters<typeof Sandbox.create>[0]>;
|
|
6
|
+
type JustBashCreationSettings$1 = JustBashSandboxCreateParams$1 & {
|
|
7
|
+
sandbox?: never;
|
|
8
|
+
};
|
|
5
9
|
/**
|
|
6
|
-
*
|
|
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:
|
|
10
|
+
* Settings for `createJustBashSandbox`. Two mutually-exclusive shapes:
|
|
13
11
|
*
|
|
14
12
|
* - `{ sandbox }` — wrap an already-created `just-bash` `Sandbox`. The caller
|
|
15
13
|
* owns its lifecycle.
|
|
@@ -19,12 +17,13 @@ type JustBashSandboxCreateParams = NonNullable<Parameters<typeof Sandbox.create>
|
|
|
19
17
|
* just-bash has no port exposure and no snapshot mechanism, so image
|
|
20
18
|
* management is a per-call no-op: if an adapter declares a bootstrap recipe
|
|
21
19
|
* the provider runs it once on the freshly-created sandbox before returning.
|
|
20
|
+
*
|
|
21
|
+
* @deprecated Use `JustBashNetworkSandboxSessionCreateOptions` for new sessions.
|
|
22
22
|
*/
|
|
23
23
|
type JustBashSandboxSettings = {
|
|
24
24
|
sandbox: Sandbox;
|
|
25
|
-
} |
|
|
26
|
-
|
|
27
|
-
});
|
|
25
|
+
} | JustBashCreationSettings$1;
|
|
26
|
+
/** @deprecated Use `createJustBashNetworkSandboxSession` instead. */
|
|
28
27
|
declare function createJustBashSandbox(settings?: JustBashSandboxSettings): HarnessV1SandboxProvider;
|
|
29
28
|
/**
|
|
30
29
|
* `HarnessV1SandboxProvider` implementation backed by `just-bash`. Useful for
|
|
@@ -35,6 +34,7 @@ declare function createJustBashSandbox(settings?: JustBashSandboxSettings): Harn
|
|
|
35
34
|
* Note: just-bash cannot expose ports, so bridge-backed harness adapters
|
|
36
35
|
* (claude-code, codex) will reject this provider at start.
|
|
37
36
|
*/
|
|
37
|
+
/** @deprecated Use `createJustBashNetworkSandboxSession` instead. */
|
|
38
38
|
declare class JustBashSandboxProvider implements HarnessV1SandboxProvider {
|
|
39
39
|
private readonly settings;
|
|
40
40
|
readonly specificationVersion: "harness-sandbox-v1";
|
|
@@ -50,4 +50,30 @@ declare class JustBashSandboxProvider implements HarnessV1SandboxProvider {
|
|
|
50
50
|
}) => Promise<HarnessV1NetworkSandboxSession>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Flattens an intersection of object types into a single object type so the
|
|
55
|
+
* resolved shape displays as its named properties rather than a chain of
|
|
56
|
+
* `A & B & C`.
|
|
57
|
+
*/
|
|
58
|
+
type Prettify<T> = {
|
|
59
|
+
[K in keyof T]: T[K];
|
|
60
|
+
} & {};
|
|
61
|
+
type JustBashNativeSandboxSession = Sandbox;
|
|
62
|
+
/**
|
|
63
|
+
* Parameters forwarded to `just-bash`'s `Sandbox.create` when creating a
|
|
64
|
+
* sandbox from scratch. Aliased directly from the underlying SDK so the full
|
|
65
|
+
* surface is available without us re-declaring it.
|
|
66
|
+
*/
|
|
67
|
+
type JustBashSandboxCreateParams = NonNullable<Parameters<typeof Sandbox.create>[0]>;
|
|
68
|
+
type JustBashCreationSettings = Omit<JustBashSandboxCreateParams, 'sandbox'>;
|
|
69
|
+
type JustBashNetworkSandboxSessionCreateOptions = Prettify<HarnessV1SandboxSessionCreateOptions<JustBashCreationSettings> & {
|
|
70
|
+
sandbox?: never;
|
|
71
|
+
}>;
|
|
72
|
+
type JustBashLookupSettings = Record<never, never>;
|
|
73
|
+
type JustBashNetworkSandboxSessionResumeOptions = HarnessV1SandboxSessionResumeOptions<JustBashLookupSettings>;
|
|
74
|
+
declare function createJustBashNetworkSandboxSession(options?: JustBashNetworkSandboxSessionCreateOptions): Promise<HarnessV1NetworkSandboxSession>;
|
|
75
|
+
declare function resumeJustBashNetworkSandboxSession(options: JustBashNetworkSandboxSessionResumeOptions): Promise<HarnessV1NetworkSandboxSession>;
|
|
76
|
+
declare function createJustBashSandboxSessionFromNativeSandbox(nativeSandbox: JustBashNativeSandboxSession): Experimental_SandboxSession;
|
|
77
|
+
declare function createJustBashNetworkSandboxSessionFromNativeSandbox(nativeSandbox: JustBashNativeSandboxSession): HarnessV1NetworkSandboxSession;
|
|
78
|
+
|
|
79
|
+
export { type JustBashNativeSandboxSession, type JustBashNetworkSandboxSessionCreateOptions, type JustBashNetworkSandboxSessionResumeOptions, JustBashSandboxProvider, type JustBashSandboxSettings, createJustBashNetworkSandboxSession, createJustBashNetworkSandboxSessionFromNativeSandbox, createJustBashSandbox, createJustBashSandboxSessionFromNativeSandbox, resumeJustBashNetworkSandboxSession };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/just-bash-sandbox.ts
|
|
1
|
+
// src/just-bash-legacy-sandbox-provider.ts
|
|
2
2
|
import { Sandbox } from "just-bash";
|
|
3
3
|
|
|
4
4
|
// src/just-bash-network-sandbox-session.ts
|
|
@@ -12,6 +12,103 @@ import { isAbsolute, posix } from "path";
|
|
|
12
12
|
import {
|
|
13
13
|
extractLines
|
|
14
14
|
} from "@ai-sdk/provider-utils";
|
|
15
|
+
|
|
16
|
+
// src/utils.ts
|
|
17
|
+
import { defineCommand } from "just-bash";
|
|
18
|
+
var preparations = /* @__PURE__ */ new WeakMap();
|
|
19
|
+
async function ensureRealpath(sandbox) {
|
|
20
|
+
let preparation = preparations.get(sandbox);
|
|
21
|
+
if (preparation == null) {
|
|
22
|
+
preparation = prepareRealpath(sandbox);
|
|
23
|
+
preparations.set(sandbox, preparation);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await preparation;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (preparations.get(sandbox) === preparation) {
|
|
29
|
+
preparations.delete(sandbox);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function prepareRealpath(sandbox) {
|
|
35
|
+
const realpathType = await sandbox.bashEnvInstance.exec("type realpath");
|
|
36
|
+
if (realpathType.exitCode === 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
sandbox.bashEnvInstance.registerCommand(
|
|
40
|
+
defineCommand("realpath", executeRealpath)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
async function executeRealpath(args, context) {
|
|
44
|
+
if (args.length === 0 || args[0] === "") {
|
|
45
|
+
return {
|
|
46
|
+
stdout: "",
|
|
47
|
+
stderr: "realpath: missing operand\n",
|
|
48
|
+
exitCode: 1
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let pending = args[0].startsWith("/") ? args[0] : `${context.cwd}/${args[0]}`;
|
|
52
|
+
let resolved = "";
|
|
53
|
+
let linkCount = 0;
|
|
54
|
+
while (pending.length > 0) {
|
|
55
|
+
pending = pending.replace(/^\//, "");
|
|
56
|
+
if (pending.length === 0) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const separatorIndex = pending.indexOf("/");
|
|
60
|
+
const component = separatorIndex === -1 ? pending : pending.slice(0, separatorIndex);
|
|
61
|
+
pending = separatorIndex === -1 ? "" : pending.slice(separatorIndex + 1);
|
|
62
|
+
if (component === "" || component === ".") {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (component === "..") {
|
|
66
|
+
resolved = resolved.slice(0, resolved.lastIndexOf("/"));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const candidate = `${resolved}/${component}`;
|
|
70
|
+
let isSymbolicLink = false;
|
|
71
|
+
try {
|
|
72
|
+
isSymbolicLink = (await context.fs.lstat(candidate)).isSymbolicLink;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!isFileNotFoundError(error)) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!isSymbolicLink) {
|
|
79
|
+
resolved = candidate;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
linkCount += 1;
|
|
83
|
+
if (linkCount > 64) {
|
|
84
|
+
return { stdout: "", stderr: "", exitCode: 1 };
|
|
85
|
+
}
|
|
86
|
+
let target;
|
|
87
|
+
try {
|
|
88
|
+
target = await context.fs.readlink(candidate);
|
|
89
|
+
} catch {
|
|
90
|
+
return { stdout: "", stderr: "", exitCode: 1 };
|
|
91
|
+
}
|
|
92
|
+
const remainder = pending.length > 0 ? `/${pending}` : "";
|
|
93
|
+
pending = target.startsWith("/") ? `${target}${remainder}` : `${candidate.slice(0, candidate.lastIndexOf("/"))}/${target}${remainder}`;
|
|
94
|
+
resolved = "";
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
stdout: `${resolved || "/"}
|
|
98
|
+
`,
|
|
99
|
+
stderr: "",
|
|
100
|
+
exitCode: 0
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function isFileNotFoundError(error) {
|
|
104
|
+
if (error == null || typeof error !== "object") return false;
|
|
105
|
+
const code = error.code;
|
|
106
|
+
if (code === "ENOENT") return true;
|
|
107
|
+
const message = error.message;
|
|
108
|
+
return typeof message === "string" && /no such file|not found|ENOENT/i.test(message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/just-bash-sandbox-session.ts
|
|
15
112
|
var JustBashSandboxSession = class {
|
|
16
113
|
constructor(sandbox) {
|
|
17
114
|
this.sandbox = sandbox;
|
|
@@ -32,7 +129,9 @@ var JustBashSandboxSession = class {
|
|
|
32
129
|
env,
|
|
33
130
|
abortSignal
|
|
34
131
|
}) {
|
|
35
|
-
abortSignal
|
|
132
|
+
abortSignal?.throwIfAborted();
|
|
133
|
+
await ensureRealpath(this.sandbox);
|
|
134
|
+
abortSignal?.throwIfAborted();
|
|
36
135
|
const finished = await this.sandbox.runCommand({
|
|
37
136
|
cmd: "bash",
|
|
38
137
|
args: createBashArgs(command, env),
|
|
@@ -55,7 +154,9 @@ var JustBashSandboxSession = class {
|
|
|
55
154
|
env,
|
|
56
155
|
abortSignal
|
|
57
156
|
}) {
|
|
58
|
-
abortSignal
|
|
157
|
+
abortSignal?.throwIfAborted();
|
|
158
|
+
await ensureRealpath(this.sandbox);
|
|
159
|
+
abortSignal?.throwIfAborted();
|
|
59
160
|
const live = await this.sandbox.runCommand({
|
|
60
161
|
cmd: "bash",
|
|
61
162
|
args: createBashArgs(command, env),
|
|
@@ -77,13 +178,13 @@ var JustBashSandboxSession = class {
|
|
|
77
178
|
path,
|
|
78
179
|
abortSignal
|
|
79
180
|
}) {
|
|
80
|
-
abortSignal
|
|
181
|
+
abortSignal?.throwIfAborted();
|
|
81
182
|
const resolved = this.resolvePath(path);
|
|
82
183
|
try {
|
|
83
184
|
const bytes = await this.sandbox.bashEnvInstance.fs.readFileBuffer(resolved);
|
|
84
185
|
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
85
186
|
} catch (error) {
|
|
86
|
-
if (
|
|
187
|
+
if (isFileNotFoundError2(error)) return null;
|
|
87
188
|
throw error;
|
|
88
189
|
}
|
|
89
190
|
}
|
|
@@ -112,7 +213,7 @@ var JustBashSandboxSession = class {
|
|
|
112
213
|
content,
|
|
113
214
|
abortSignal
|
|
114
215
|
}) {
|
|
115
|
-
abortSignal
|
|
216
|
+
abortSignal?.throwIfAborted();
|
|
116
217
|
const resolved = this.resolvePath(path);
|
|
117
218
|
await this.sandbox.bashEnvInstance.fs.writeFile(resolved, content);
|
|
118
219
|
}
|
|
@@ -161,28 +262,26 @@ function createSandboxProcess(command, abortSignal) {
|
|
|
161
262
|
}
|
|
162
263
|
});
|
|
163
264
|
const drained = (async () => {
|
|
164
|
-
var _a, _b, _c, _d;
|
|
165
265
|
try {
|
|
166
266
|
for await (const message of command.logs()) {
|
|
167
267
|
const target = message.type === "stdout" ? controllers.stdout : controllers.stderr;
|
|
168
|
-
target
|
|
268
|
+
target?.enqueue(encoder.encode(message.data));
|
|
169
269
|
}
|
|
170
|
-
|
|
171
|
-
|
|
270
|
+
controllers.stdout?.close();
|
|
271
|
+
controllers.stderr?.close();
|
|
172
272
|
} catch (error) {
|
|
173
|
-
|
|
174
|
-
|
|
273
|
+
controllers.stdout?.error(error);
|
|
274
|
+
controllers.stderr?.error(error);
|
|
175
275
|
}
|
|
176
276
|
})();
|
|
177
277
|
return {
|
|
178
278
|
stdout,
|
|
179
279
|
stderr,
|
|
180
280
|
async wait() {
|
|
181
|
-
var _a;
|
|
182
281
|
const finished = await command.wait();
|
|
183
282
|
await drained;
|
|
184
|
-
if (abortSignal
|
|
185
|
-
throw
|
|
283
|
+
if (abortSignal?.aborted) {
|
|
284
|
+
throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
|
|
186
285
|
}
|
|
187
286
|
return { exitCode: finished.exitCode };
|
|
188
287
|
},
|
|
@@ -219,7 +318,7 @@ async function collectStream(stream) {
|
|
|
219
318
|
}
|
|
220
319
|
return out;
|
|
221
320
|
}
|
|
222
|
-
function
|
|
321
|
+
function isFileNotFoundError2(error) {
|
|
223
322
|
if (error == null || typeof error !== "object") return false;
|
|
224
323
|
const code = error.code;
|
|
225
324
|
if (code === "ENOENT") return true;
|
|
@@ -246,11 +345,12 @@ var JustBashNetworkSandboxSession = class extends JustBashSandboxSession {
|
|
|
246
345
|
return (await this.getPortEndpoint(options)).url;
|
|
247
346
|
};
|
|
248
347
|
this.stop = async () => {
|
|
348
|
+
await this.sandbox.stop();
|
|
249
349
|
};
|
|
250
350
|
this.destroy = async () => {
|
|
251
351
|
await this.stop();
|
|
252
352
|
};
|
|
253
|
-
this.id = randomUUID();
|
|
353
|
+
this.id = input.sandboxId ?? randomUUID();
|
|
254
354
|
this.defaultWorkingDirectory = input.sandbox.bashEnvInstance.getCwd();
|
|
255
355
|
}
|
|
256
356
|
restricted() {
|
|
@@ -258,77 +358,12 @@ var JustBashNetworkSandboxSession = class extends JustBashSandboxSession {
|
|
|
258
358
|
}
|
|
259
359
|
};
|
|
260
360
|
|
|
261
|
-
// src/just-bash-sandbox.ts
|
|
361
|
+
// src/just-bash-legacy-sandbox-provider.ts
|
|
262
362
|
var JUST_BASH_PROVIDER_ID2 = "just-bash-sandbox";
|
|
263
|
-
var REALPATH_PATH = "/usr/bin/realpath";
|
|
264
|
-
var REALPATH_SCRIPT = `#!/usr/bin/env bash
|
|
265
|
-
pending=\${1:?}
|
|
266
|
-
resolved=
|
|
267
|
-
link_count=0
|
|
268
|
-
link_marker=__AI_SDK_REALPATH_LINK_END__
|
|
269
|
-
case "$pending" in
|
|
270
|
-
/*) ;;
|
|
271
|
-
*) pending=$PWD/$pending ;;
|
|
272
|
-
esac
|
|
273
|
-
while [ -n "$pending" ]; do
|
|
274
|
-
pending=\${pending#/}
|
|
275
|
-
[ -n "$pending" ] || break
|
|
276
|
-
component=\${pending%%/*}
|
|
277
|
-
if [ "$pending" = "$component" ]; then
|
|
278
|
-
pending=
|
|
279
|
-
else
|
|
280
|
-
pending=\${pending#*/}
|
|
281
|
-
fi
|
|
282
|
-
case "$component" in
|
|
283
|
-
""|.) continue ;;
|
|
284
|
-
..)
|
|
285
|
-
resolved=\${resolved%/*}
|
|
286
|
-
continue
|
|
287
|
-
;;
|
|
288
|
-
esac
|
|
289
|
-
candidate=$resolved/$component
|
|
290
|
-
if [ -L "$candidate" ]; then
|
|
291
|
-
link_count=$((link_count + 1))
|
|
292
|
-
[ "$link_count" -le 64 ] || exit 1
|
|
293
|
-
link_target_framed=$(readlink "$candidate"; readlink_status=$?; printf '%s' "$link_marker"; exit "$readlink_status")
|
|
294
|
-
readlink_status=$?
|
|
295
|
-
[ "$readlink_status" -eq 0 ] || exit 1
|
|
296
|
-
target=\${link_target_framed%$link_marker}
|
|
297
|
-
target=\${target%$'
|
|
298
|
-
'}
|
|
299
|
-
case "$target" in
|
|
300
|
-
/*) pending=$target\${pending:+/$pending} ;;
|
|
301
|
-
*) pending=\${candidate%/*}/$target\${pending:+/$pending} ;;
|
|
302
|
-
esac
|
|
303
|
-
resolved=
|
|
304
|
-
else
|
|
305
|
-
resolved=$candidate
|
|
306
|
-
fi
|
|
307
|
-
done
|
|
308
|
-
printf '%s
|
|
309
|
-
' "\${resolved:-/}"
|
|
310
|
-
`;
|
|
311
|
-
async function ensureRealpath(sandbox) {
|
|
312
|
-
const fs = sandbox.bashEnvInstance.fs;
|
|
313
|
-
try {
|
|
314
|
-
await fs.lstat(REALPATH_PATH);
|
|
315
|
-
return;
|
|
316
|
-
} catch (error) {
|
|
317
|
-
if (!isFileNotFoundError2(error)) {
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
await sandbox.writeFiles({ [REALPATH_PATH]: REALPATH_SCRIPT });
|
|
322
|
-
await fs.chmod(REALPATH_PATH, 493);
|
|
323
|
-
}
|
|
324
|
-
function isFileNotFoundError2(error) {
|
|
325
|
-
if (error == null || typeof error !== "object") return false;
|
|
326
|
-
const code = error.code;
|
|
327
|
-
if (code === "ENOENT") return true;
|
|
328
|
-
const message = error.message;
|
|
329
|
-
return typeof message === "string" && /no such file|not found|ENOENT/i.test(message);
|
|
330
|
-
}
|
|
331
363
|
function createJustBashSandbox(settings = {}) {
|
|
364
|
+
console.warn(
|
|
365
|
+
"createJustBashSandbox is deprecated. Use createJustBashNetworkSandboxSession and HarnessAgent.createSession({ sandboxSession }) instead."
|
|
366
|
+
);
|
|
332
367
|
return new JustBashSandboxProvider(settings);
|
|
333
368
|
}
|
|
334
369
|
var JustBashSandboxProvider = class {
|
|
@@ -337,25 +372,80 @@ var JustBashSandboxProvider = class {
|
|
|
337
372
|
this.specificationVersion = "harness-sandbox-v1";
|
|
338
373
|
this.providerId = JUST_BASH_PROVIDER_ID2;
|
|
339
374
|
this.createSession = async (options) => {
|
|
340
|
-
|
|
341
|
-
(_a = options == null ? void 0 : options.abortSignal) == null ? void 0 : _a.throwIfAborted();
|
|
375
|
+
options?.abortSignal?.throwIfAborted();
|
|
342
376
|
const ownsLifecycle = !("sandbox" in this.settings && this.settings.sandbox);
|
|
343
|
-
const sandbox = ownsLifecycle ? await Sandbox.create(
|
|
377
|
+
const sandbox = ownsLifecycle ? await Sandbox.create(
|
|
378
|
+
this.settings
|
|
379
|
+
) : this.settings.sandbox;
|
|
344
380
|
await ensureRealpath(sandbox);
|
|
345
381
|
const sandboxSession = new JustBashNetworkSandboxSession({
|
|
346
382
|
sandbox
|
|
347
383
|
});
|
|
348
|
-
if (
|
|
384
|
+
if (options?.onFirstCreate != null) {
|
|
349
385
|
await options.onFirstCreate(sandboxSession.restricted(), {
|
|
350
|
-
abortSignal: options
|
|
386
|
+
abortSignal: options?.abortSignal
|
|
351
387
|
});
|
|
352
388
|
}
|
|
353
389
|
return sandboxSession;
|
|
354
390
|
};
|
|
391
|
+
console.warn(
|
|
392
|
+
"JustBashSandboxProvider is deprecated. Use createJustBashNetworkSandboxSession instead."
|
|
393
|
+
);
|
|
355
394
|
}
|
|
356
395
|
};
|
|
396
|
+
|
|
397
|
+
// src/just-bash-sandbox.ts
|
|
398
|
+
import {
|
|
399
|
+
HarnessCapabilityUnsupportedError as HarnessCapabilityUnsupportedError2
|
|
400
|
+
} from "@ai-sdk/harness";
|
|
401
|
+
import { Sandbox as Sandbox2 } from "just-bash";
|
|
402
|
+
async function createJustBashNetworkSandboxSession(options = {}) {
|
|
403
|
+
if ("sandbox" in options) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
"createJustBashNetworkSandboxSession: use createJustBashNetworkSandboxSessionFromNativeSandbox for an existing sandbox."
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const {
|
|
409
|
+
template,
|
|
410
|
+
abortSignal,
|
|
411
|
+
sandboxId,
|
|
412
|
+
sandbox: _sandbox,
|
|
413
|
+
...nativeOptions
|
|
414
|
+
} = options;
|
|
415
|
+
abortSignal?.throwIfAborted();
|
|
416
|
+
const nativeSandbox = await Sandbox2.create(nativeOptions);
|
|
417
|
+
try {
|
|
418
|
+
await ensureRealpath(nativeSandbox);
|
|
419
|
+
const session = new JustBashNetworkSandboxSession({
|
|
420
|
+
sandbox: nativeSandbox,
|
|
421
|
+
sandboxId
|
|
422
|
+
});
|
|
423
|
+
await template?.prepare({ session: session.restricted(), abortSignal });
|
|
424
|
+
return session;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
await nativeSandbox.stop().catch(() => {
|
|
427
|
+
});
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function resumeJustBashNetworkSandboxSession(options) {
|
|
432
|
+
options.abortSignal?.throwIfAborted();
|
|
433
|
+
throw new HarnessCapabilityUnsupportedError2({
|
|
434
|
+
message: "just-bash sandboxes run in-process and cannot be resumed by sandboxId."
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function createJustBashSandboxSessionFromNativeSandbox(nativeSandbox) {
|
|
438
|
+
return new JustBashSandboxSession(nativeSandbox);
|
|
439
|
+
}
|
|
440
|
+
function createJustBashNetworkSandboxSessionFromNativeSandbox(nativeSandbox) {
|
|
441
|
+
return new JustBashNetworkSandboxSession({ sandbox: nativeSandbox });
|
|
442
|
+
}
|
|
357
443
|
export {
|
|
358
444
|
JustBashSandboxProvider,
|
|
359
|
-
|
|
445
|
+
createJustBashNetworkSandboxSession,
|
|
446
|
+
createJustBashNetworkSandboxSessionFromNativeSandbox,
|
|
447
|
+
createJustBashSandbox,
|
|
448
|
+
createJustBashSandboxSessionFromNativeSandbox,
|
|
449
|
+
resumeJustBashNetworkSandboxSession
|
|
360
450
|
};
|
|
361
451
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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';\nconst REALPATH_PATH = '/usr/bin/realpath';\nconst REALPATH_SCRIPT = `#!/usr/bin/env bash\npending=\\${1:?}\nresolved=\nlink_count=0\nlink_marker=__AI_SDK_REALPATH_LINK_END__\ncase \"$pending\" in\n /*) ;;\n *) pending=$PWD/$pending ;;\nesac\nwhile [ -n \"$pending\" ]; do\n pending=\\${pending#/}\n [ -n \"$pending\" ] || break\n component=\\${pending%%/*}\n if [ \"$pending\" = \"$component\" ]; then\n pending=\n else\n pending=\\${pending#*/}\n fi\n case \"$component\" in\n \"\"|.) continue ;;\n ..)\n resolved=\\${resolved%/*}\n continue\n ;;\n esac\n candidate=$resolved/$component\n if [ -L \"$candidate\" ]; then\n link_count=$((link_count + 1))\n [ \"$link_count\" -le 64 ] || exit 1\n link_target_framed=$(readlink \"$candidate\"; readlink_status=$?; printf '%s' \"$link_marker\"; exit \"$readlink_status\")\n readlink_status=$?\n [ \"$readlink_status\" -eq 0 ] || exit 1\n target=\\${link_target_framed%$link_marker}\n target=\\${target%$'\\n'}\n case \"$target\" in\n /*) pending=$target\\${pending:+/$pending} ;;\n *) pending=\\${candidate%/*}/$target\\${pending:+/$pending} ;;\n esac\n resolved=\n else\n resolved=$candidate\n fi\ndone\nprintf '%s\\n' \"\\${resolved:-/}\"\n`;\n\nasync function ensureRealpath(sandbox: Sandbox): Promise<void> {\n const fs = sandbox.bashEnvInstance.fs;\n try {\n await fs.lstat(REALPATH_PATH);\n return;\n } catch (error) {\n if (!isFileNotFoundError(error)) {\n throw error;\n }\n }\n\n await sandbox.writeFiles({ [REALPATH_PATH]: REALPATH_SCRIPT });\n await fs.chmod(REALPATH_PATH, 0o755);\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|ENOENT/i.test(message)\n );\n}\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 const ownsLifecycle = !(\n 'sandbox' in this.settings && this.settings.sandbox\n );\n const sandbox = ownsLifecycle\n ? await Sandbox.create(this.settings as JustBashSandboxCreateParams)\n : this.settings.sandbox;\n await ensureRealpath(sandbox);\n const sandboxSession = new JustBashNetworkSandboxSession({\n sandbox,\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 type HarnessV1PortEndpoint,\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 `getPortEndpoint` will fail with `HarnessCapabilityUnsupportedError` at\n * 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\n constructor(input: { sandbox: Sandbox }) {\n super(input.sandbox);\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 getPortEndpoint = async (_options: {\n port: number;\n protocol?: 'http' | 'https' | 'ws';\n }): Promise<HarnessV1PortEndpoint> => {\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 /**\n * @deprecated Use `getPortEndpoint` instead.\n */\n getPortUrl = async (options: {\n port: number;\n protocol?: 'http' | 'https' | 'ws';\n }): Promise<string> => {\n return (await this.getPortEndpoint(options)).url;\n };\n\n stop = async (): Promise<void> => {\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: createBashArgs(command, env),\n ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),\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: createBashArgs(command, env),\n detached: true,\n ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),\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 createBashArgs(\n command: string,\n env: Record<string, string> | undefined,\n): string[] {\n // just-bash applies runCommand's env to the outer execution, but its `bash`\n // command only inherits exported variables. Export assignments passed as\n // positional arguments so values are not interpolated into the script.\n const assignments =\n env == null\n ? []\n : Object.entries(env).map(([name, value]) => `${name}=${value}`);\n\n if (assignments.length === 0) {\n return ['-c', command];\n }\n\n return [\n '-c',\n `${assignments.map(() => 'export -- \"$1\"; shift').join('\\n')}\\n${command}`,\n 'bash',\n ...assignments,\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","isFileNotFoundError"]}
|
|
1
|
+
{"version":3,"sources":["../src/just-bash-legacy-sandbox-provider.ts","../src/just-bash-network-sandbox-session.ts","../src/just-bash-sandbox-session.ts","../src/utils.ts","../src/just-bash-sandbox.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';\nimport { ensureRealpath } from './utils';\n\ntype JustBashSandboxCreateParams = NonNullable<\n Parameters<typeof Sandbox.create>[0]\n>;\n\ntype JustBashCreationSettings = JustBashSandboxCreateParams & {\n sandbox?: never;\n};\n\n/**\n * Settings for `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 *\n * @deprecated Use `JustBashNetworkSandboxSessionCreateOptions` for new sessions.\n */\nexport type JustBashSandboxSettings =\n | { sandbox: Sandbox }\n | JustBashCreationSettings;\n\nconst JUST_BASH_PROVIDER_ID = 'just-bash-sandbox';\n\n/** @deprecated Use `createJustBashNetworkSandboxSession` instead. */\nexport function createJustBashSandbox(\n settings: JustBashSandboxSettings = {} as JustBashSandboxSettings,\n): HarnessV1SandboxProvider {\n console.warn(\n 'createJustBashSandbox is deprecated. Use createJustBashNetworkSandboxSession and HarnessAgent.createSession({ sandboxSession }) instead.',\n );\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 */\n/** @deprecated Use `createJustBashNetworkSandboxSession` instead. */\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 console.warn(\n 'JustBashSandboxProvider is deprecated. Use createJustBashNetworkSandboxSession instead.',\n );\n }\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 const ownsLifecycle = !(\n 'sandbox' in this.settings && this.settings.sandbox\n );\n const sandbox = ownsLifecycle\n ? await Sandbox.create(\n this.settings as NonNullable<Parameters<typeof Sandbox.create>[0]>,\n )\n : this.settings.sandbox;\n await ensureRealpath(sandbox);\n const sandboxSession = new JustBashNetworkSandboxSession({\n sandbox,\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 type HarnessV1PortEndpoint,\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 `getPortEndpoint` will fail with `HarnessCapabilityUnsupportedError` at\n * 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 * Caller-supplied label or a UUID minted at construct time. just-bash has\n * no native identifier and cannot reattach across processes.\n */\n readonly id: string;\n readonly defaultWorkingDirectory: string;\n\n constructor(input: { sandbox: Sandbox; sandboxId?: string }) {\n super(input.sandbox);\n this.id = input.sandboxId ?? 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 getPortEndpoint = async (_options: {\n port: number;\n protocol?: 'http' | 'https' | 'ws';\n }): Promise<HarnessV1PortEndpoint> => {\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 /**\n * @deprecated Use `getPortEndpoint` instead.\n */\n getPortUrl = async (options: {\n port: number;\n protocol?: 'http' | 'https' | 'ws';\n }): Promise<string> => {\n return (await this.getPortEndpoint(options)).url;\n };\n\n stop = async (): Promise<void> => {\n // just-bash has no explicit shutdown; the sandbox is garbage-collected\n // along with its in-memory filesystem once references drop.\n await this.sandbox.stop();\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 as SandboxSession,\n type Experimental_SandboxProcess as SandboxProcess,\n} from '@ai-sdk/provider-utils';\nimport type { Sandbox, SandboxCommand } from 'just-bash';\nimport { ensureRealpath } from './utils';\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 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 await ensureRealpath(this.sandbox);\n abortSignal?.throwIfAborted();\n\n const finished = await this.sandbox.runCommand({\n cmd: 'bash',\n args: createBashArgs(command, env),\n ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),\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<SandboxProcess> {\n abortSignal?.throwIfAborted();\n await ensureRealpath(this.sandbox);\n abortSignal?.throwIfAborted();\n\n const live = await this.sandbox.runCommand({\n cmd: 'bash',\n args: createBashArgs(command, env),\n detached: true,\n ...(workingDirectory !== undefined ? { cwd: workingDirectory } : {}),\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 createBashArgs(\n command: string,\n env: Record<string, string> | undefined,\n): string[] {\n // just-bash applies runCommand's env to the outer execution, but its `bash`\n // command only inherits exported variables. Export assignments passed as\n // positional arguments so values are not interpolated into the script.\n const assignments =\n env == null\n ? []\n : Object.entries(env).map(([name, value]) => `${name}=${value}`);\n\n if (assignments.length === 0) {\n return ['-c', command];\n }\n\n return [\n '-c',\n `${assignments.map(() => 'export -- \"$1\"; shift').join('\\n')}\\n${command}`,\n 'bash',\n ...assignments,\n ];\n}\n\nfunction createSandboxProcess(\n command: SandboxCommand,\n abortSignal: AbortSignal | undefined,\n): 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","import { defineCommand, type CommandContext, type Sandbox } from 'just-bash';\n\nconst preparations = new WeakMap<Sandbox, Promise<void>>();\n\nexport async function ensureRealpath(sandbox: Sandbox): Promise<void> {\n let preparation = preparations.get(sandbox);\n if (preparation == null) {\n preparation = prepareRealpath(sandbox);\n preparations.set(sandbox, preparation);\n }\n\n try {\n await preparation;\n } catch (error) {\n if (preparations.get(sandbox) === preparation) {\n preparations.delete(sandbox);\n }\n throw error;\n }\n}\n\nasync function prepareRealpath(sandbox: Sandbox): Promise<void> {\n const realpathType = await sandbox.bashEnvInstance.exec('type realpath');\n if (realpathType.exitCode === 0) {\n return;\n }\n\n sandbox.bashEnvInstance.registerCommand(\n defineCommand('realpath', executeRealpath),\n );\n}\n\nasync function executeRealpath(\n args: string[],\n context: CommandContext,\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n if (args.length === 0 || args[0] === '') {\n return {\n stdout: '',\n stderr: 'realpath: missing operand\\n',\n exitCode: 1,\n };\n }\n\n let pending = args[0].startsWith('/') ? args[0] : `${context.cwd}/${args[0]}`;\n let resolved = '';\n let linkCount = 0;\n\n while (pending.length > 0) {\n pending = pending.replace(/^\\//, '');\n if (pending.length === 0) {\n break;\n }\n\n const separatorIndex = pending.indexOf('/');\n const component =\n separatorIndex === -1 ? pending : pending.slice(0, separatorIndex);\n pending = separatorIndex === -1 ? '' : pending.slice(separatorIndex + 1);\n\n if (component === '' || component === '.') {\n continue;\n }\n if (component === '..') {\n resolved = resolved.slice(0, resolved.lastIndexOf('/'));\n continue;\n }\n\n const candidate = `${resolved}/${component}`;\n let isSymbolicLink = false;\n try {\n isSymbolicLink = (await context.fs.lstat(candidate)).isSymbolicLink;\n } catch (error) {\n if (!isFileNotFoundError(error)) {\n throw error;\n }\n }\n\n if (!isSymbolicLink) {\n resolved = candidate;\n continue;\n }\n\n linkCount += 1;\n if (linkCount > 64) {\n return { stdout: '', stderr: '', exitCode: 1 };\n }\n\n let target: string;\n try {\n target = await context.fs.readlink(candidate);\n } catch {\n return { stdout: '', stderr: '', exitCode: 1 };\n }\n\n const remainder = pending.length > 0 ? `/${pending}` : '';\n pending = target.startsWith('/')\n ? `${target}${remainder}`\n : `${candidate.slice(0, candidate.lastIndexOf('/'))}/${target}${remainder}`;\n resolved = '';\n }\n\n return {\n stdout: `${resolved || '/'}\\n`,\n stderr: '',\n exitCode: 0,\n };\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|ENOENT/i.test(message)\n );\n}\n","import {\n HarnessCapabilityUnsupportedError,\n type HarnessV1NetworkSandboxSession,\n type HarnessV1SandboxSessionCreateOptions,\n type HarnessV1SandboxSessionResumeOptions,\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';\nimport { JustBashSandboxSession } from './just-bash-sandbox-session';\nimport { ensureRealpath } from './utils';\n\n/**\n * Flattens an intersection of object types into a single object type so the\n * resolved shape displays as its named properties rather than a chain of\n * `A & B & C`.\n */\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\nexport type JustBashNativeSandboxSession = Sandbox;\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\ntype JustBashCreationSettings = Omit<JustBashSandboxCreateParams, 'sandbox'>;\n\nexport type JustBashNetworkSandboxSessionCreateOptions = Prettify<\n HarnessV1SandboxSessionCreateOptions<JustBashCreationSettings> & {\n sandbox?: never;\n }\n>;\n\ntype JustBashLookupSettings = Record<never, never>;\n\nexport type JustBashNetworkSandboxSessionResumeOptions =\n HarnessV1SandboxSessionResumeOptions<JustBashLookupSettings>;\n\nexport async function createJustBashNetworkSandboxSession(\n options: JustBashNetworkSandboxSessionCreateOptions = {},\n): Promise<HarnessV1NetworkSandboxSession> {\n if ('sandbox' in options) {\n throw new Error(\n 'createJustBashNetworkSandboxSession: use createJustBashNetworkSandboxSessionFromNativeSandbox for an existing sandbox.',\n );\n }\n const {\n template,\n abortSignal,\n sandboxId,\n sandbox: _sandbox,\n ...nativeOptions\n } = options;\n abortSignal?.throwIfAborted();\n const nativeSandbox = await Sandbox.create(nativeOptions);\n try {\n await ensureRealpath(nativeSandbox);\n const session = new JustBashNetworkSandboxSession({\n sandbox: nativeSandbox,\n sandboxId,\n });\n await template?.prepare({ session: session.restricted(), abortSignal });\n return session;\n } catch (error) {\n await nativeSandbox.stop().catch(() => {});\n throw error;\n }\n}\n\nexport async function resumeJustBashNetworkSandboxSession(\n options: JustBashNetworkSandboxSessionResumeOptions,\n): Promise<HarnessV1NetworkSandboxSession> {\n options.abortSignal?.throwIfAborted();\n throw new HarnessCapabilityUnsupportedError({\n message:\n 'just-bash sandboxes run in-process and cannot be resumed by sandboxId.',\n });\n}\n\nexport function createJustBashSandboxSessionFromNativeSandbox(\n nativeSandbox: JustBashNativeSandboxSession,\n): SandboxSession {\n return new JustBashSandboxSession(nativeSandbox);\n}\n\nexport function createJustBashNetworkSandboxSessionFromNativeSandbox(\n nativeSandbox: JustBashNativeSandboxSession,\n): HarnessV1NetworkSandboxSession {\n return new JustBashNetworkSandboxSession({ sandbox: nativeSandbox });\n}\n"],"mappings":"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","names":["isFileNotFoundError","JUST_BASH_PROVIDER_ID","HarnessCapabilityUnsupportedError","Sandbox","Sandbox","HarnessCapabilityUnsupportedError"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/sandbox-just-bash",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.126",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.126",
|
|
30
|
+
"@ai-sdk/provider-utils": "5.0.49",
|
|
31
31
|
"just-bash": "^2.14.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export {
|
|
2
|
+
type JustBashSandboxSettings,
|
|
2
3
|
createJustBashSandbox,
|
|
3
4
|
JustBashSandboxProvider,
|
|
4
|
-
|
|
5
|
+
} from './just-bash-legacy-sandbox-provider';
|
|
6
|
+
export {
|
|
7
|
+
createJustBashNetworkSandboxSession,
|
|
8
|
+
resumeJustBashNetworkSandboxSession,
|
|
9
|
+
createJustBashSandboxSessionFromNativeSandbox,
|
|
10
|
+
createJustBashNetworkSandboxSessionFromNativeSandbox,
|
|
11
|
+
type JustBashNativeSandboxSession,
|
|
12
|
+
type JustBashNetworkSandboxSessionCreateOptions,
|
|
13
|
+
type JustBashNetworkSandboxSessionResumeOptions,
|
|
5
14
|
} from './just-bash-sandbox';
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
import { ensureRealpath } from './utils';
|
|
9
|
+
|
|
10
|
+
type JustBashSandboxCreateParams = NonNullable<
|
|
11
|
+
Parameters<typeof Sandbox.create>[0]
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
type JustBashCreationSettings = JustBashSandboxCreateParams & {
|
|
15
|
+
sandbox?: never;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Settings for `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
|
+
* @deprecated Use `JustBashNetworkSandboxSessionCreateOptions` for new sessions.
|
|
31
|
+
*/
|
|
32
|
+
export type JustBashSandboxSettings =
|
|
33
|
+
| { sandbox: Sandbox }
|
|
34
|
+
| JustBashCreationSettings;
|
|
35
|
+
|
|
36
|
+
const JUST_BASH_PROVIDER_ID = 'just-bash-sandbox';
|
|
37
|
+
|
|
38
|
+
/** @deprecated Use `createJustBashNetworkSandboxSession` instead. */
|
|
39
|
+
export function createJustBashSandbox(
|
|
40
|
+
settings: JustBashSandboxSettings = {} as JustBashSandboxSettings,
|
|
41
|
+
): HarnessV1SandboxProvider {
|
|
42
|
+
console.warn(
|
|
43
|
+
'createJustBashSandbox is deprecated. Use createJustBashNetworkSandboxSession and HarnessAgent.createSession({ sandboxSession }) instead.',
|
|
44
|
+
);
|
|
45
|
+
return new JustBashSandboxProvider(settings);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `HarnessV1SandboxProvider` implementation backed by `just-bash`. Useful for
|
|
50
|
+
* non-bridge harness flows and for handing a local `Experimental_SandboxSession`
|
|
51
|
+
* to AI SDK tools — use `provider.createSession()` then
|
|
52
|
+
* `sandboxSession.restricted()` to get the latter.
|
|
53
|
+
*
|
|
54
|
+
* Note: just-bash cannot expose ports, so bridge-backed harness adapters
|
|
55
|
+
* (claude-code, codex) will reject this provider at start.
|
|
56
|
+
*/
|
|
57
|
+
/** @deprecated Use `createJustBashNetworkSandboxSession` instead. */
|
|
58
|
+
export class JustBashSandboxProvider implements HarnessV1SandboxProvider {
|
|
59
|
+
readonly specificationVersion = 'harness-sandbox-v1' as const;
|
|
60
|
+
readonly providerId = JUST_BASH_PROVIDER_ID;
|
|
61
|
+
|
|
62
|
+
constructor(private readonly settings: JustBashSandboxSettings) {
|
|
63
|
+
console.warn(
|
|
64
|
+
'JustBashSandboxProvider is deprecated. Use createJustBashNetworkSandboxSession instead.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createSession = async (options?: {
|
|
69
|
+
sessionId?: string;
|
|
70
|
+
abortSignal?: AbortSignal;
|
|
71
|
+
identity?: string;
|
|
72
|
+
onFirstCreate?: (
|
|
73
|
+
session: SandboxSession,
|
|
74
|
+
opts: { abortSignal?: AbortSignal },
|
|
75
|
+
) => Promise<void>;
|
|
76
|
+
}): Promise<HarnessV1NetworkSandboxSession> => {
|
|
77
|
+
options?.abortSignal?.throwIfAborted();
|
|
78
|
+
|
|
79
|
+
const ownsLifecycle = !(
|
|
80
|
+
'sandbox' in this.settings && this.settings.sandbox
|
|
81
|
+
);
|
|
82
|
+
const sandbox = ownsLifecycle
|
|
83
|
+
? await Sandbox.create(
|
|
84
|
+
this.settings as NonNullable<Parameters<typeof Sandbox.create>[0]>,
|
|
85
|
+
)
|
|
86
|
+
: this.settings.sandbox;
|
|
87
|
+
await ensureRealpath(sandbox);
|
|
88
|
+
const sandboxSession = new JustBashNetworkSandboxSession({
|
|
89
|
+
sandbox,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (options?.onFirstCreate != null) {
|
|
93
|
+
await options.onFirstCreate(sandboxSession.restricted(), {
|
|
94
|
+
abortSignal: options?.abortSignal,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return sandboxSession;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -26,16 +26,15 @@ export class JustBashNetworkSandboxSession
|
|
|
26
26
|
implements HarnessV1NetworkSandboxSession
|
|
27
27
|
{
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `HarnessV1NetworkSandboxSession.id` contract.
|
|
29
|
+
* Caller-supplied label or a UUID minted at construct time. just-bash has
|
|
30
|
+
* no native identifier and cannot reattach across processes.
|
|
32
31
|
*/
|
|
33
32
|
readonly id: string;
|
|
34
33
|
readonly defaultWorkingDirectory: string;
|
|
35
34
|
|
|
36
|
-
constructor(input: { sandbox: Sandbox }) {
|
|
35
|
+
constructor(input: { sandbox: Sandbox; sandboxId?: string }) {
|
|
37
36
|
super(input.sandbox);
|
|
38
|
-
this.id = randomUUID();
|
|
37
|
+
this.id = input.sandboxId ?? randomUUID();
|
|
39
38
|
this.defaultWorkingDirectory = input.sandbox.bashEnvInstance.getCwd();
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -70,6 +69,7 @@ export class JustBashNetworkSandboxSession
|
|
|
70
69
|
stop = async (): Promise<void> => {
|
|
71
70
|
// just-bash has no explicit shutdown; the sandbox is garbage-collected
|
|
72
71
|
// along with its in-memory filesystem once references drop.
|
|
72
|
+
await this.sandbox.stop();
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
destroy = async (): Promise<void> => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { isAbsolute, posix } from 'node:path';
|
|
2
2
|
import {
|
|
3
3
|
extractLines,
|
|
4
|
-
type Experimental_SandboxSession,
|
|
5
|
-
type Experimental_SandboxProcess,
|
|
4
|
+
type Experimental_SandboxSession as SandboxSession,
|
|
5
|
+
type Experimental_SandboxProcess as SandboxProcess,
|
|
6
6
|
} from '@ai-sdk/provider-utils';
|
|
7
7
|
import type { Sandbox, SandboxCommand } from 'just-bash';
|
|
8
|
+
import { ensureRealpath } from './utils';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* `Experimental_SandboxSession` implementation backed by a `just-bash`
|
|
@@ -16,7 +17,7 @@ import type { Sandbox, SandboxCommand } from 'just-bash';
|
|
|
16
17
|
* `JustBashNetworkSandboxSession.restricted()` — not constructed directly by
|
|
17
18
|
* consumers.
|
|
18
19
|
*/
|
|
19
|
-
export class JustBashSandboxSession implements
|
|
20
|
+
export class JustBashSandboxSession implements SandboxSession {
|
|
20
21
|
constructor(protected readonly sandbox: Sandbox) {}
|
|
21
22
|
|
|
22
23
|
get description(): string {
|
|
@@ -45,6 +46,8 @@ export class JustBashSandboxSession implements Experimental_SandboxSession {
|
|
|
45
46
|
abortSignal?: AbortSignal;
|
|
46
47
|
}): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
47
48
|
abortSignal?.throwIfAborted();
|
|
49
|
+
await ensureRealpath(this.sandbox);
|
|
50
|
+
abortSignal?.throwIfAborted();
|
|
48
51
|
|
|
49
52
|
const finished = await this.sandbox.runCommand({
|
|
50
53
|
cmd: 'bash',
|
|
@@ -75,7 +78,9 @@ export class JustBashSandboxSession implements Experimental_SandboxSession {
|
|
|
75
78
|
workingDirectory?: string;
|
|
76
79
|
env?: Record<string, string>;
|
|
77
80
|
abortSignal?: AbortSignal;
|
|
78
|
-
}): Promise<
|
|
81
|
+
}): Promise<SandboxProcess> {
|
|
82
|
+
abortSignal?.throwIfAborted();
|
|
83
|
+
await ensureRealpath(this.sandbox);
|
|
79
84
|
abortSignal?.throwIfAborted();
|
|
80
85
|
|
|
81
86
|
const live = await this.sandbox.runCommand({
|
|
@@ -217,7 +222,7 @@ function createBashArgs(
|
|
|
217
222
|
function createSandboxProcess(
|
|
218
223
|
command: SandboxCommand,
|
|
219
224
|
abortSignal: AbortSignal | undefined,
|
|
220
|
-
):
|
|
225
|
+
): SandboxProcess {
|
|
221
226
|
const encoder = new TextEncoder();
|
|
222
227
|
const controllers: {
|
|
223
228
|
stdout?: ReadableStreamDefaultController<Uint8Array>;
|
package/src/just-bash-sandbox.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
HarnessCapabilityUnsupportedError,
|
|
3
|
+
type HarnessV1NetworkSandboxSession,
|
|
4
|
+
type HarnessV1SandboxSessionCreateOptions,
|
|
5
|
+
type HarnessV1SandboxSessionResumeOptions,
|
|
4
6
|
} from '@ai-sdk/harness';
|
|
5
7
|
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
6
8
|
import { Sandbox } from 'just-bash';
|
|
7
9
|
import { JustBashNetworkSandboxSession } from './just-bash-network-sandbox-session';
|
|
10
|
+
import { JustBashSandboxSession } from './just-bash-sandbox-session';
|
|
11
|
+
import { ensureRealpath } from './utils';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Flattens an intersection of object types into a single object type so the
|
|
15
|
+
* resolved shape displays as its named properties rather than a chain of
|
|
16
|
+
* `A & B & C`.
|
|
17
|
+
*/
|
|
18
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
19
|
+
|
|
20
|
+
export type JustBashNativeSandboxSession = Sandbox;
|
|
8
21
|
|
|
9
22
|
/**
|
|
10
23
|
* Parameters forwarded to `just-bash`'s `Sandbox.create` when creating a
|
|
@@ -15,145 +28,68 @@ type JustBashSandboxCreateParams = NonNullable<
|
|
|
15
28
|
Parameters<typeof Sandbox.create>[0]
|
|
16
29
|
>;
|
|
17
30
|
|
|
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 });
|
|
31
|
+
type JustBashCreationSettings = Omit<JustBashSandboxCreateParams, 'sandbox'>;
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
case "$pending" in
|
|
42
|
-
/*) ;;
|
|
43
|
-
*) pending=$PWD/$pending ;;
|
|
44
|
-
esac
|
|
45
|
-
while [ -n "$pending" ]; do
|
|
46
|
-
pending=\${pending#/}
|
|
47
|
-
[ -n "$pending" ] || break
|
|
48
|
-
component=\${pending%%/*}
|
|
49
|
-
if [ "$pending" = "$component" ]; then
|
|
50
|
-
pending=
|
|
51
|
-
else
|
|
52
|
-
pending=\${pending#*/}
|
|
53
|
-
fi
|
|
54
|
-
case "$component" in
|
|
55
|
-
""|.) continue ;;
|
|
56
|
-
..)
|
|
57
|
-
resolved=\${resolved%/*}
|
|
58
|
-
continue
|
|
59
|
-
;;
|
|
60
|
-
esac
|
|
61
|
-
candidate=$resolved/$component
|
|
62
|
-
if [ -L "$candidate" ]; then
|
|
63
|
-
link_count=$((link_count + 1))
|
|
64
|
-
[ "$link_count" -le 64 ] || exit 1
|
|
65
|
-
link_target_framed=$(readlink "$candidate"; readlink_status=$?; printf '%s' "$link_marker"; exit "$readlink_status")
|
|
66
|
-
readlink_status=$?
|
|
67
|
-
[ "$readlink_status" -eq 0 ] || exit 1
|
|
68
|
-
target=\${link_target_framed%$link_marker}
|
|
69
|
-
target=\${target%$'\n'}
|
|
70
|
-
case "$target" in
|
|
71
|
-
/*) pending=$target\${pending:+/$pending} ;;
|
|
72
|
-
*) pending=\${candidate%/*}/$target\${pending:+/$pending} ;;
|
|
73
|
-
esac
|
|
74
|
-
resolved=
|
|
75
|
-
else
|
|
76
|
-
resolved=$candidate
|
|
77
|
-
fi
|
|
78
|
-
done
|
|
79
|
-
printf '%s\n' "\${resolved:-/}"
|
|
80
|
-
`;
|
|
33
|
+
export type JustBashNetworkSandboxSessionCreateOptions = Prettify<
|
|
34
|
+
HarnessV1SandboxSessionCreateOptions<JustBashCreationSettings> & {
|
|
35
|
+
sandbox?: never;
|
|
36
|
+
}
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
type JustBashLookupSettings = Record<never, never>;
|
|
81
40
|
|
|
82
|
-
|
|
83
|
-
|
|
41
|
+
export type JustBashNetworkSandboxSessionResumeOptions =
|
|
42
|
+
HarnessV1SandboxSessionResumeOptions<JustBashLookupSettings>;
|
|
43
|
+
|
|
44
|
+
export async function createJustBashNetworkSandboxSession(
|
|
45
|
+
options: JustBashNetworkSandboxSessionCreateOptions = {},
|
|
46
|
+
): Promise<HarnessV1NetworkSandboxSession> {
|
|
47
|
+
if ('sandbox' in options) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'createJustBashNetworkSandboxSession: use createJustBashNetworkSandboxSessionFromNativeSandbox for an existing sandbox.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const {
|
|
53
|
+
template,
|
|
54
|
+
abortSignal,
|
|
55
|
+
sandboxId,
|
|
56
|
+
sandbox: _sandbox,
|
|
57
|
+
...nativeOptions
|
|
58
|
+
} = options;
|
|
59
|
+
abortSignal?.throwIfAborted();
|
|
60
|
+
const nativeSandbox = await Sandbox.create(nativeOptions);
|
|
84
61
|
try {
|
|
85
|
-
await
|
|
86
|
-
|
|
62
|
+
await ensureRealpath(nativeSandbox);
|
|
63
|
+
const session = new JustBashNetworkSandboxSession({
|
|
64
|
+
sandbox: nativeSandbox,
|
|
65
|
+
sandboxId,
|
|
66
|
+
});
|
|
67
|
+
await template?.prepare({ session: session.restricted(), abortSignal });
|
|
68
|
+
return session;
|
|
87
69
|
} catch (error) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
70
|
+
await nativeSandbox.stop().catch(() => {});
|
|
71
|
+
throw error;
|
|
91
72
|
}
|
|
92
|
-
|
|
93
|
-
await sandbox.writeFiles({ [REALPATH_PATH]: REALPATH_SCRIPT });
|
|
94
|
-
await fs.chmod(REALPATH_PATH, 0o755);
|
|
95
73
|
}
|
|
96
74
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
75
|
+
export async function resumeJustBashNetworkSandboxSession(
|
|
76
|
+
options: JustBashNetworkSandboxSessionResumeOptions,
|
|
77
|
+
): Promise<HarnessV1NetworkSandboxSession> {
|
|
78
|
+
options.abortSignal?.throwIfAborted();
|
|
79
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
80
|
+
message:
|
|
81
|
+
'just-bash sandboxes run in-process and cannot be resumed by sandboxId.',
|
|
82
|
+
});
|
|
106
83
|
}
|
|
107
84
|
|
|
108
|
-
export function
|
|
109
|
-
|
|
110
|
-
):
|
|
111
|
-
return new
|
|
85
|
+
export function createJustBashSandboxSessionFromNativeSandbox(
|
|
86
|
+
nativeSandbox: JustBashNativeSandboxSession,
|
|
87
|
+
): SandboxSession {
|
|
88
|
+
return new JustBashSandboxSession(nativeSandbox);
|
|
112
89
|
}
|
|
113
90
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
* `sandboxSession.restricted()` to get the latter.
|
|
119
|
-
*
|
|
120
|
-
* Note: just-bash cannot expose ports, so bridge-backed harness adapters
|
|
121
|
-
* (claude-code, codex) will reject this provider at start.
|
|
122
|
-
*/
|
|
123
|
-
export class JustBashSandboxProvider implements HarnessV1SandboxProvider {
|
|
124
|
-
readonly specificationVersion = 'harness-sandbox-v1' as const;
|
|
125
|
-
readonly providerId = JUST_BASH_PROVIDER_ID;
|
|
126
|
-
|
|
127
|
-
constructor(private readonly settings: JustBashSandboxSettings) {}
|
|
128
|
-
|
|
129
|
-
createSession = async (options?: {
|
|
130
|
-
sessionId?: string;
|
|
131
|
-
abortSignal?: AbortSignal;
|
|
132
|
-
identity?: string;
|
|
133
|
-
onFirstCreate?: (
|
|
134
|
-
session: SandboxSession,
|
|
135
|
-
opts: { abortSignal?: AbortSignal },
|
|
136
|
-
) => Promise<void>;
|
|
137
|
-
}): Promise<HarnessV1NetworkSandboxSession> => {
|
|
138
|
-
options?.abortSignal?.throwIfAborted();
|
|
139
|
-
|
|
140
|
-
const ownsLifecycle = !(
|
|
141
|
-
'sandbox' in this.settings && this.settings.sandbox
|
|
142
|
-
);
|
|
143
|
-
const sandbox = ownsLifecycle
|
|
144
|
-
? await Sandbox.create(this.settings as JustBashSandboxCreateParams)
|
|
145
|
-
: this.settings.sandbox;
|
|
146
|
-
await ensureRealpath(sandbox);
|
|
147
|
-
const sandboxSession = new JustBashNetworkSandboxSession({
|
|
148
|
-
sandbox,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (options?.onFirstCreate != null) {
|
|
152
|
-
await options.onFirstCreate(sandboxSession.restricted(), {
|
|
153
|
-
abortSignal: options?.abortSignal,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return sandboxSession;
|
|
158
|
-
};
|
|
91
|
+
export function createJustBashNetworkSandboxSessionFromNativeSandbox(
|
|
92
|
+
nativeSandbox: JustBashNativeSandboxSession,
|
|
93
|
+
): HarnessV1NetworkSandboxSession {
|
|
94
|
+
return new JustBashNetworkSandboxSession({ sandbox: nativeSandbox });
|
|
159
95
|
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { defineCommand, type CommandContext, type Sandbox } from 'just-bash';
|
|
2
|
+
|
|
3
|
+
const preparations = new WeakMap<Sandbox, Promise<void>>();
|
|
4
|
+
|
|
5
|
+
export async function ensureRealpath(sandbox: Sandbox): Promise<void> {
|
|
6
|
+
let preparation = preparations.get(sandbox);
|
|
7
|
+
if (preparation == null) {
|
|
8
|
+
preparation = prepareRealpath(sandbox);
|
|
9
|
+
preparations.set(sandbox, preparation);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await preparation;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (preparations.get(sandbox) === preparation) {
|
|
16
|
+
preparations.delete(sandbox);
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function prepareRealpath(sandbox: Sandbox): Promise<void> {
|
|
23
|
+
const realpathType = await sandbox.bashEnvInstance.exec('type realpath');
|
|
24
|
+
if (realpathType.exitCode === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sandbox.bashEnvInstance.registerCommand(
|
|
29
|
+
defineCommand('realpath', executeRealpath),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function executeRealpath(
|
|
34
|
+
args: string[],
|
|
35
|
+
context: CommandContext,
|
|
36
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
37
|
+
if (args.length === 0 || args[0] === '') {
|
|
38
|
+
return {
|
|
39
|
+
stdout: '',
|
|
40
|
+
stderr: 'realpath: missing operand\n',
|
|
41
|
+
exitCode: 1,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let pending = args[0].startsWith('/') ? args[0] : `${context.cwd}/${args[0]}`;
|
|
46
|
+
let resolved = '';
|
|
47
|
+
let linkCount = 0;
|
|
48
|
+
|
|
49
|
+
while (pending.length > 0) {
|
|
50
|
+
pending = pending.replace(/^\//, '');
|
|
51
|
+
if (pending.length === 0) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const separatorIndex = pending.indexOf('/');
|
|
56
|
+
const component =
|
|
57
|
+
separatorIndex === -1 ? pending : pending.slice(0, separatorIndex);
|
|
58
|
+
pending = separatorIndex === -1 ? '' : pending.slice(separatorIndex + 1);
|
|
59
|
+
|
|
60
|
+
if (component === '' || component === '.') {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (component === '..') {
|
|
64
|
+
resolved = resolved.slice(0, resolved.lastIndexOf('/'));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const candidate = `${resolved}/${component}`;
|
|
69
|
+
let isSymbolicLink = false;
|
|
70
|
+
try {
|
|
71
|
+
isSymbolicLink = (await context.fs.lstat(candidate)).isSymbolicLink;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!isFileNotFoundError(error)) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isSymbolicLink) {
|
|
79
|
+
resolved = candidate;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
linkCount += 1;
|
|
84
|
+
if (linkCount > 64) {
|
|
85
|
+
return { stdout: '', stderr: '', exitCode: 1 };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let target: string;
|
|
89
|
+
try {
|
|
90
|
+
target = await context.fs.readlink(candidate);
|
|
91
|
+
} catch {
|
|
92
|
+
return { stdout: '', stderr: '', exitCode: 1 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const remainder = pending.length > 0 ? `/${pending}` : '';
|
|
96
|
+
pending = target.startsWith('/')
|
|
97
|
+
? `${target}${remainder}`
|
|
98
|
+
: `${candidate.slice(0, candidate.lastIndexOf('/'))}/${target}${remainder}`;
|
|
99
|
+
resolved = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
stdout: `${resolved || '/'}\n`,
|
|
104
|
+
stderr: '',
|
|
105
|
+
exitCode: 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isFileNotFoundError(error: unknown): boolean {
|
|
110
|
+
if (error == null || typeof error !== 'object') return false;
|
|
111
|
+
const code = (error as { code?: unknown }).code;
|
|
112
|
+
if (code === 'ENOENT') return true;
|
|
113
|
+
const message = (error as { message?: unknown }).message;
|
|
114
|
+
return (
|
|
115
|
+
typeof message === 'string' &&
|
|
116
|
+
/no such file|not found|ENOENT/i.test(message)
|
|
117
|
+
);
|
|
118
|
+
}
|