@coder/ai-sdk-sandbox 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -13
- package/dist/index.js +0 -2
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -246,17 +246,181 @@ Esc or Ctrl+C).
|
|
|
246
246
|
|
|
247
247
|
Because the bridge runs inside the workspace, the workspace image must have:
|
|
248
248
|
|
|
249
|
-
- **Node.js**
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
- **
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
- **Node.js ≥ 22** on the login-shell PATH. The bridge is `node bridge.mjs`, the
|
|
250
|
+
native transport's relay is `node -e`, and the pinned
|
|
251
|
+
`@anthropic-ai/claude-code` bridge dependency requires Node 22+.
|
|
252
|
+
- **pnpm** on the login-shell PATH (e.g. `npm install -g pnpm`) — the adapter's
|
|
253
|
+
bootstrap runs `pnpm install --frozen-lockfile` to install the bridge's
|
|
254
|
+
dependencies.
|
|
255
|
+
- **Outbound network access** to the npm registry (the bootstrap downloads the
|
|
256
|
+
bridge's dependencies — including the Claude Code CLI's ~250 MB platform
|
|
257
|
+
binary, which ships as an npm package — on first use) and to the model API
|
|
258
|
+
(`api.anthropic.com` for Claude Code, `api.openai.com` for Codex). Pre-bake
|
|
259
|
+
the bootstrap into the image to skip the registry entirely — see
|
|
260
|
+
[Template authoring](#template-authoring-zero-install-sessions).
|
|
256
261
|
- **The model API key** available to the bridge — `ANTHROPIC_API_KEY` /
|
|
257
262
|
`OPENAI_API_KEY`. Configure it through the adapter's `auth` option or ensure it
|
|
258
263
|
is present in the workspace environment.
|
|
259
|
-
- `bash` and `base64` (
|
|
264
|
+
- `bash` and `base64` (remote commands run through `bash -lc`; file I/O is
|
|
265
|
+
base64-encoded), plus `stty` for the native transport's relay bootstrap. All
|
|
266
|
+
standard on any Linux dev image.
|
|
267
|
+
|
|
268
|
+
## Template authoring: zero-install sessions
|
|
269
|
+
|
|
270
|
+
How to build a Coder template whose workspaces start harness sessions with
|
|
271
|
+
**zero runtime install latency**. Generic template authoring is covered by the
|
|
272
|
+
[Coder template docs](https://coder.com/docs/admin/templates) and
|
|
273
|
+
[Coder's example Docker template](https://github.com/coder/coder/tree/main/examples/templates/docker);
|
|
274
|
+
this section covers only the ai-sdk-specific delta. A validated
|
|
275
|
+
Dockerfile + `main.tf` pair lives in [`examples/template/`](./examples/template/).
|
|
276
|
+
|
|
277
|
+
### What the first session installs
|
|
278
|
+
|
|
279
|
+
On the first session in a fresh workspace, the Claude Code adapter applies its
|
|
280
|
+
bootstrap recipe relative to the session's default working directory (`$HOME`
|
|
281
|
+
for this provider):
|
|
282
|
+
|
|
283
|
+
1. It writes `package.json`, `pnpm-lock.yaml`, and `bridge.mjs` (assets shipped
|
|
284
|
+
inside `@ai-sdk/harness-claude-code`) to `~/.harness-bootstrap/claude-code/`.
|
|
285
|
+
2. It runs `pnpm install --frozen-lockfile --store-dir .pnpm-store` there. This
|
|
286
|
+
is the expensive step: ~300 MB from the npm registry — the bridge
|
|
287
|
+
dependencies include the `@anthropic-ai/claude-code` platform binary — taking
|
|
288
|
+
tens of seconds to minutes depending on the network.
|
|
289
|
+
3. It runs the CLI's `install.cjs` (links the platform binary npm delivered; no
|
|
290
|
+
extra download) and `./node_modules/.bin/claude --version`.
|
|
291
|
+
4. It writes a `.bootstrap-<recipe-hash>.ok` marker next to them. Later sessions
|
|
292
|
+
see the marker and skip all of the above — a single file read.
|
|
293
|
+
|
|
294
|
+
Zero-install means baking the results of steps 1–3 into the image **at that
|
|
295
|
+
exact path**. The first session in each workspace then replays the recipe as a
|
|
296
|
+
fast offline no-op (~4 s measured; pnpm prints `Already up to date` and
|
|
297
|
+
downloads nothing) and writes the marker; every session after that is a single
|
|
298
|
+
file read.
|
|
299
|
+
|
|
300
|
+
### Step 1 — Dockerfile: pre-bake the bootstrap
|
|
301
|
+
|
|
302
|
+
[`examples/template/Dockerfile`](./examples/template/Dockerfile) installs the
|
|
303
|
+
[workspace requirements](#workspace-requirements) (Node 24 + pnpm on PATH,
|
|
304
|
+
bash/coreutils) and replays the recipe at build time — same files, same
|
|
305
|
+
commands, same absolute path:
|
|
306
|
+
|
|
307
|
+
```dockerfile
|
|
308
|
+
ARG HARNESS_CLAUDE_CODE_VERSION=1.0.90
|
|
309
|
+
RUN mkdir -p /home/${USER}/.harness-bootstrap/claude-code \
|
|
310
|
+
&& cd /tmp \
|
|
311
|
+
&& npm pack @ai-sdk/harness-claude-code@${HARNESS_CLAUDE_CODE_VERSION} \
|
|
312
|
+
&& tar -xzf ai-sdk-harness-claude-code-${HARNESS_CLAUDE_CODE_VERSION}.tgz \
|
|
313
|
+
&& cp package/dist/bridge/package.json package/dist/bridge/pnpm-lock.yaml \
|
|
314
|
+
/home/${USER}/.harness-bootstrap/claude-code/ \
|
|
315
|
+
&& cp package/dist/bridge/index.mjs \
|
|
316
|
+
/home/${USER}/.harness-bootstrap/claude-code/bridge.mjs \
|
|
317
|
+
&& rm -rf package ai-sdk-harness-claude-code-${HARNESS_CLAUDE_CODE_VERSION}.tgz \
|
|
318
|
+
&& cd /home/${USER}/.harness-bootstrap/claude-code \
|
|
319
|
+
&& pnpm install --frozen-lockfile --store-dir .pnpm-store \
|
|
320
|
+
&& node node_modules/@anthropic-ai/claude-code/install.cjs \
|
|
321
|
+
&& ./node_modules/.bin/claude --version
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Rules that make or break the pre-bake:
|
|
325
|
+
|
|
326
|
+
- **Bake at the final absolute path.** pnpm records the store location as an
|
|
327
|
+
absolute path (in `node_modules/.modules.yaml`), so a cache baked at, say,
|
|
328
|
+
`/opt/...` and copied into `$HOME` later makes the session's `pnpm install`
|
|
329
|
+
abort (`ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY` — pnpm wants to purge and
|
|
330
|
+
reinstall, and a session has no TTY to confirm).
|
|
331
|
+
- **Pin the adapter version your application resolves** (find it with
|
|
332
|
+
`pnpm why @ai-sdk/harness-claude-code`, or in your lockfile) and pass it as
|
|
333
|
+
`--build-arg HARNESS_CLAUDE_CODE_VERSION=...`. The recipe and its marker hash
|
|
334
|
+
derive from the adapter's shipped assets. A mismatch is not fatal — the
|
|
335
|
+
recipe re-runs in the same directory and downloads only the delta — but it
|
|
336
|
+
reintroduces registry traffic on first sessions.
|
|
337
|
+
- **Build for the workspace CPU architecture** — the pre-bake fetches the
|
|
338
|
+
builder platform's `claude` binary.
|
|
339
|
+
|
|
340
|
+
### Step 2 — main.tf: get the cache into the home volume
|
|
341
|
+
|
|
342
|
+
[`examples/template/main.tf`](./examples/template/main.tf) is Coder's example
|
|
343
|
+
Docker template minus the IDE modules, with three ai-sdk-specific choices:
|
|
344
|
+
|
|
345
|
+
- **The workspace image is the pre-baked one** (`variable "image"`); push it to
|
|
346
|
+
a registry your Coder provisioners can pull from.
|
|
347
|
+
- **`startup_script_behavior = "blocking"`** on `coder_agent`: this provider's
|
|
348
|
+
create mode waits for `lifecycle_state: ready` before the harness runs, and
|
|
349
|
+
blocking makes ready mean "startup script finished" (the attribute defaults to
|
|
350
|
+
`non-blocking`, in which case ready does not wait for the script).
|
|
351
|
+
- **The home volume seeds itself from the image.** Docker populates a fresh
|
|
352
|
+
(empty) named volume with the image's content at the mount path, so
|
|
353
|
+
`/home/coder/.harness-bootstrap` lands in every new workspace's volume with no
|
|
354
|
+
startup-script copying. No port configuration is needed either: the provider
|
|
355
|
+
forwards the bridge port itself (OpenSSH `-L` or the native relay), not
|
|
356
|
+
through template port shares.
|
|
357
|
+
|
|
358
|
+
For volume types that do _not_ copy image content (a Kubernetes PVC mounted at
|
|
359
|
+
`/home/coder` shadows it with an empty filesystem), stash the bake outside the
|
|
360
|
+
mount and restore it — at the same absolute path — on first start:
|
|
361
|
+
|
|
362
|
+
```dockerfile
|
|
363
|
+
# Dockerfile, after the pre-bake step:
|
|
364
|
+
RUN sudo cp -a /home/${USER}/.harness-bootstrap /opt/harness-bootstrap-stash
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```hcl
|
|
368
|
+
startup_script = <<-EOT
|
|
369
|
+
set -e
|
|
370
|
+
if [ ! -d ~/.harness-bootstrap ]; then
|
|
371
|
+
cp -a /opt/harness-bootstrap-stash ~/.harness-bootstrap
|
|
372
|
+
fi
|
|
373
|
+
EOT
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
(`cp -a` preserves the hardlinks between the pnpm store and `node_modules`, so
|
|
377
|
+
the stash and the restore each stay ~300 MB rather than doubling.)
|
|
378
|
+
|
|
379
|
+
Build, push, and point the provider at the template
|
|
380
|
+
([creating templates](https://coder.com/docs/admin/templates/creating-templates)):
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
cd examples/template
|
|
384
|
+
docker build -t <registry>/ai-sdk-sandbox-workspace:v1 .
|
|
385
|
+
docker push <registry>/ai-sdk-sandbox-workspace:v1
|
|
386
|
+
coder templates push ai-sdk-sandbox --variable image=<registry>/ai-sdk-sandbox-workspace:v1
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
createCoderWorkspace({ create: { template: "ai-sdk-sandbox" } });
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Sizing
|
|
394
|
+
|
|
395
|
+
Measured on the example image (linux/amd64): ~730 MB total — ~430 MB for
|
|
396
|
+
Ubuntu 24.04 + Node 24 + pnpm, ~300 MB for the bootstrap cache (dominated by
|
|
397
|
+
the pnpm store; `node_modules` hardlinks into it). Each workspace's home volume
|
|
398
|
+
receives the ~300 MB seeded cache; leave headroom for per-session work
|
|
399
|
+
directories (`claude-code-<sessionId>/`) and whatever your agents check out.
|
|
400
|
+
|
|
401
|
+
### Verifying zero installs
|
|
402
|
+
|
|
403
|
+
**Image-level (no Coder deployment needed).** Replay the bootstrap on a fresh
|
|
404
|
+
volume with networking disabled — it must succeed offline:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
docker volume rm -f probe-home && docker volume create probe-home
|
|
408
|
+
docker run --rm --network none -v probe-home:/home/coder \
|
|
409
|
+
<image> bash -lc 'cd ~/.harness-bootstrap/claude-code \
|
|
410
|
+
&& pnpm install --frozen-lockfile --store-dir .pnpm-store \
|
|
411
|
+
&& node node_modules/@anthropic-ai/claude-code/install.cjs \
|
|
412
|
+
&& ./node_modules/.bin/claude --version'
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Expect `Already up to date` from pnpm and the CLI version banner in a few
|
|
416
|
+
seconds — proof that a session's bootstrap needs no registry access.
|
|
417
|
+
|
|
418
|
+
**Session-level.** After the first session against a workspace from the
|
|
419
|
+
template, `~/.harness-bootstrap/claude-code/` must contain a `.bootstrap-*.ok`
|
|
420
|
+
marker (later sessions skip the recipe entirely). If session creation instead
|
|
421
|
+
stalls for minutes with pnpm download progress in the bootstrap, the image's
|
|
422
|
+
pinned adapter version doesn't match the application's (see the pinning rule
|
|
423
|
+
above).
|
|
260
424
|
|
|
261
425
|
## Settings
|
|
262
426
|
|
|
@@ -372,8 +536,6 @@ and a full Claude Code turn with tool use (`scripts/e2e-claude.ts`).
|
|
|
372
536
|
|
|
373
537
|
- `setNetworkPolicy` is not implemented (omitted) — egress is governed by your
|
|
374
538
|
Coder template/deployment, not this provider.
|
|
375
|
-
- `bridgePorts` is intentionally left undefined: this provider binds one
|
|
376
|
-
workspace per session rather than leasing ports from a shared sandbox.
|
|
377
539
|
- File reads buffer the whole file (binary content moves as base64). Fine for
|
|
378
540
|
bootstrap-sized files; not intended for streaming very large files.
|
|
379
541
|
- `CoderNativeTransport` currently targets POSIX workspaces with `bash`, `stty`,
|
|
@@ -383,9 +545,11 @@ and a full Claude Code turn with tool use (`scripts/e2e-claude.ts`).
|
|
|
383
545
|
native transport refuses to guess.
|
|
384
546
|
- `@ai-sdk/sandbox-just-bash` cannot expose ports and is rejected by bridge-backed
|
|
385
547
|
adapters — this provider exists precisely to provide that port.
|
|
386
|
-
- To run Claude Code / Codex, the **workspace** image needs Node.js (the
|
|
387
|
-
installs the bridge + CLI on first use) and egress to the npm registry
|
|
388
|
-
model API
|
|
548
|
+
- To run Claude Code / Codex, the **workspace** image needs Node.js ≥ 22 (the
|
|
549
|
+
adapter installs the bridge + CLI on first use) and egress to the npm registry
|
|
550
|
+
and the model API — unless the image pre-bakes the bootstrap (see
|
|
551
|
+
[Template authoring](#template-authoring-zero-install-sessions)). Provide the
|
|
552
|
+
API key via the adapter's `auth` option.
|
|
389
553
|
|
|
390
554
|
## Development
|
|
391
555
|
|
package/dist/index.js
CHANGED
|
@@ -2903,8 +2903,6 @@ function createCoderWorkspace(settings) {
|
|
|
2903
2903
|
return {
|
|
2904
2904
|
specificationVersion: "harness-sandbox-v1",
|
|
2905
2905
|
providerId: CODER_WORKSPACE_PROVIDER_ID,
|
|
2906
|
-
// `bridgePorts` intentionally left undefined: this provider binds one
|
|
2907
|
-
// workspace per session rather than leasing ports from a shared sandbox.
|
|
2908
2906
|
createSession: async (options) => {
|
|
2909
2907
|
const workspace = resolveWorkspace(options?.sessionId);
|
|
2910
2908
|
const { session, createdByProvider } = await buildSession(workspace, options?.abortSignal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coder/ai-sdk-sandbox",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Coder workspace sandbox provider for the Vercel AI SDK v7 HarnessAgent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
@@ -45,19 +45,19 @@
|
|
|
45
45
|
"yaml": "^2.9.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@ai-sdk/harness": "^1.0.
|
|
49
|
-
"@ai-sdk/harness-claude-code": "^1.0.
|
|
50
|
-
"@ai-sdk/provider-utils": "^5.0.
|
|
51
|
-
"@ai-sdk/tui": "^1.0.
|
|
48
|
+
"@ai-sdk/harness": "^1.0.87",
|
|
49
|
+
"@ai-sdk/harness-claude-code": "^1.0.90",
|
|
50
|
+
"@ai-sdk/provider-utils": "^5.0.30",
|
|
51
|
+
"@ai-sdk/tui": "^1.0.80",
|
|
52
52
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
53
53
|
"@types/node": "^26.2.0",
|
|
54
54
|
"@types/ws": "^8.18.1",
|
|
55
|
-
"@vitest/coverage-v8": "^4.1.
|
|
56
|
-
"publint": "^0.3.
|
|
55
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
56
|
+
"publint": "^0.3.24",
|
|
57
57
|
"tsup": "^8.5.1",
|
|
58
58
|
"tsx": "^4.23.12",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
|
-
"vitest": "^4.1.
|
|
60
|
+
"vitest": "^4.1.11",
|
|
61
61
|
"zod": "4.4.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|