@coder/ai-sdk-sandbox 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -6
- package/dist/index.d.ts +65 -1
- package/dist/index.js +48 -20
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -10,14 +10,13 @@ It implements the `HarnessV1SandboxProvider` contract from `@ai-sdk/harness`, so
|
|
|
10
10
|
you pass it as the `sandbox` to a `HarnessAgent` exactly like
|
|
11
11
|
`@ai-sdk/sandbox-vercel`.
|
|
12
12
|
|
|
13
|
-
> **Status:** experimental.
|
|
14
|
-
> `@
|
|
15
|
-
> `@ai-sdk/harness@1.0.0-canary.11`.
|
|
13
|
+
> **Status:** experimental. This provider tracks the stable AI SDK v7 harness
|
|
14
|
+
> packages (`@ai-sdk/harness@^1.0.23`).
|
|
16
15
|
|
|
17
16
|
## Install
|
|
18
17
|
|
|
19
18
|
```bash
|
|
20
|
-
npm add @coder/ai-sdk-sandbox @ai-sdk/harness
|
|
19
|
+
npm add @coder/ai-sdk-sandbox @ai-sdk/harness @ai-sdk/harness-claude-code @ai-sdk/provider-utils
|
|
21
20
|
```
|
|
22
21
|
|
|
23
22
|
On the host you also need:
|
|
@@ -128,13 +127,51 @@ createCoderWorkspace({
|
|
|
128
127
|
});
|
|
129
128
|
```
|
|
130
129
|
|
|
130
|
+
## Provisioning a workspace without a session
|
|
131
|
+
|
|
132
|
+
`ensureCoderWorkspace(settings)` runs the same get-or-create → start-if-stopped
|
|
133
|
+
→ wait-until-ready pipeline as create mode, but without creating a harness
|
|
134
|
+
sandbox session — use it to provision a workspace for **other tools** to bind
|
|
135
|
+
to. It takes an explicit `workspace` name (`[owner/]workspace`; there is no
|
|
136
|
+
`sessionId` to derive one from), an optional `create` block (same shape as
|
|
137
|
+
above; `namePrefix`/`owner` are unused), plus `readyTimeoutMs`, `transport`, and
|
|
138
|
+
`abortSignal`. A stopped workspace is always started. Without `create`, the
|
|
139
|
+
workspace must already exist.
|
|
140
|
+
|
|
141
|
+
It returns an `EnsuredCoderWorkspace`: the workspace's final (ready) status
|
|
142
|
+
snapshot plus `created` (whether this call created it) and — when the transport
|
|
143
|
+
reports one — `id`, the workspace UUID. That id is the handle other Coder
|
|
144
|
+
packages bind to. `@coder/ai-sdk-agent` is intentionally **not** a dependency of
|
|
145
|
+
this package; the two compose by a plain string handoff:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { ensureCoderWorkspace } from "@coder/ai-sdk-sandbox";
|
|
149
|
+
import { CoderAgent } from "@coder/ai-sdk-agent";
|
|
150
|
+
|
|
151
|
+
const ws = await ensureCoderWorkspace({
|
|
152
|
+
workspace: "agent-ws",
|
|
153
|
+
create: { template: "docker" },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const agent = new CoderAgent({
|
|
157
|
+
baseUrl: process.env.CODER_URL!,
|
|
158
|
+
token: process.env.CODER_SESSION_TOKEN!,
|
|
159
|
+
organizationId: "<org-uuid>",
|
|
160
|
+
workspaceId: ws.id!, // binds the chat's workspace-scoped tools
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The non-null assertion is safe on `coder` CLIs that emit `id` in
|
|
165
|
+
`coder list -o json`; old CLIs omit it, so guard
|
|
166
|
+
(`if (ws.id === undefined) throw …`) when you can't pin the CLI version.
|
|
167
|
+
|
|
131
168
|
## Terminal UI
|
|
132
169
|
|
|
133
170
|
For an interactive chat in your terminal instead of one-shot `generate()` calls,
|
|
134
171
|
wrap the same agent with the AI SDK terminal UI ([`@ai-sdk/tui`](https://ai-sdk.dev/v7/docs/ai-sdk-harnesses/terminal-ui)):
|
|
135
172
|
|
|
136
173
|
```bash
|
|
137
|
-
npm add @ai-sdk/tui
|
|
174
|
+
npm add @ai-sdk/tui
|
|
138
175
|
```
|
|
139
176
|
|
|
140
177
|
The TUI drives a session-less agent, so adapt the `HarnessAgent` (whose
|
|
@@ -299,7 +336,7 @@ and a full Claude Code turn with tool use (`scripts/e2e-claude.ts`).
|
|
|
299
336
|
|
|
300
337
|
```bash
|
|
301
338
|
npm install
|
|
302
|
-
npm run typecheck # tsc against the real
|
|
339
|
+
npm run typecheck # tsc against the real harness types
|
|
303
340
|
npm test # vitest: unit + local integration (fake `coder` + `ssh`)
|
|
304
341
|
npm run build # tsup → dist/ (ESM + d.ts)
|
|
305
342
|
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,11 @@ interface WorkspaceAgentInfo {
|
|
|
64
64
|
lifecycleState: WorkspaceAgentLifecycle;
|
|
65
65
|
}
|
|
66
66
|
interface WorkspaceStatus {
|
|
67
|
+
/**
|
|
68
|
+
* Workspace UUID (top-level `id` in the Coder API). Optional: old `coder`
|
|
69
|
+
* CLIs or non-CLI transports may not report it, so never require it.
|
|
70
|
+
*/
|
|
71
|
+
id?: string;
|
|
67
72
|
/** Workspace name (without owner/agent qualifiers). */
|
|
68
73
|
name: string;
|
|
69
74
|
/** Workspace-level build status (`latest_build.status`). */
|
|
@@ -370,6 +375,65 @@ type CoderWorkspaceSettings = CoderWorkspaceBaseSettings & ({
|
|
|
370
375
|
* ```
|
|
371
376
|
*/
|
|
372
377
|
declare function createCoderWorkspace(settings: CoderWorkspaceSettings): HarnessV1SandboxProvider;
|
|
378
|
+
/**
|
|
379
|
+
* Settings for {@link ensureCoderWorkspace} — the provisioning subset of
|
|
380
|
+
* {@link createCoderWorkspace}'s settings. There is no harness session here, so
|
|
381
|
+
* the workspace name must be explicit (no `sessionId` to derive one from) and a
|
|
382
|
+
* stopped workspace is always started.
|
|
383
|
+
*/
|
|
384
|
+
interface EnsureCoderWorkspaceSettings extends Pick<CoderWorkspaceBaseSettings, "readyTimeoutMs" | "transport"> {
|
|
385
|
+
/** The workspace to ensure, as `[owner/]workspace`. Required. */
|
|
386
|
+
workspace: string;
|
|
387
|
+
/**
|
|
388
|
+
* Create the workspace from a template when it doesn't exist; without it an
|
|
389
|
+
* existing workspace is required. `create.namePrefix` and `create.owner` are
|
|
390
|
+
* unused here (they only shape provider-derived names).
|
|
391
|
+
*/
|
|
392
|
+
create?: CoderCreateSettings;
|
|
393
|
+
/** Aborts pending create/start/status calls and the readiness polling. */
|
|
394
|
+
abortSignal?: AbortSignal;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Result of {@link ensureCoderWorkspace}: the workspace's final (ready) status
|
|
398
|
+
* snapshot plus whether this call created it.
|
|
399
|
+
*/
|
|
400
|
+
interface EnsuredCoderWorkspace extends WorkspaceStatus {
|
|
401
|
+
/** `true` when this call created the workspace (vs. reusing an existing one). */
|
|
402
|
+
created: boolean;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Ensure a Coder workspace exists, is started, and has a ready agent — without
|
|
406
|
+
* creating a harness sandbox session. Get-or-creates (when `create` is set),
|
|
407
|
+
* runs `coder start` if stopped, then polls until an agent is connected and its
|
|
408
|
+
* startup script has finished. Shares its internals with
|
|
409
|
+
* {@link createCoderWorkspace}'s create mode.
|
|
410
|
+
*
|
|
411
|
+
* The result includes the workspace UUID (`id`) when the transport reports one
|
|
412
|
+
* (a new-enough `coder` CLI; older CLIs may omit it) — the handle that other
|
|
413
|
+
* Coder packages bind to. `@coder/ai-sdk-agent` is intentionally *not* a
|
|
414
|
+
* dependency of this package; compose the two by passing the id:
|
|
415
|
+
*
|
|
416
|
+
* @example Provision a workspace, then bind a `CoderAgent` chat to it
|
|
417
|
+
* ```ts
|
|
418
|
+
* import { ensureCoderWorkspace } from '@coder/ai-sdk-sandbox';
|
|
419
|
+
* import { CoderAgent } from '@coder/ai-sdk-agent';
|
|
420
|
+
*
|
|
421
|
+
* const ws = await ensureCoderWorkspace({
|
|
422
|
+
* workspace: 'agent-ws',
|
|
423
|
+
* create: { template: 'docker' },
|
|
424
|
+
* });
|
|
425
|
+
* if (ws.id === undefined) {
|
|
426
|
+
* throw new Error('this coder CLI does not report workspace ids; upgrade it');
|
|
427
|
+
* }
|
|
428
|
+
* const agent = new CoderAgent({
|
|
429
|
+
* baseUrl: 'https://coder.example.com',
|
|
430
|
+
* token: process.env.CODER_SESSION_TOKEN!,
|
|
431
|
+
* organizationId: '<org-uuid>',
|
|
432
|
+
* workspaceId: ws.id, // binds the chat's workspace-scoped tools
|
|
433
|
+
* });
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
declare function ensureCoderWorkspace(settings: EnsureCoderWorkspaceSettings): Promise<EnsuredCoderWorkspace>;
|
|
373
437
|
|
|
374
438
|
interface ReadFileOptions {
|
|
375
439
|
path: string;
|
|
@@ -444,4 +508,4 @@ declare class CoderWorkspaceSession implements HarnessV1NetworkSandboxSession {
|
|
|
444
508
|
readonly restricted: () => Experimental_SandboxSession;
|
|
445
509
|
}
|
|
446
510
|
|
|
447
|
-
export { CODER_WORKSPACE_PROVIDER_ID, CoderCliTransport, type CoderCliTransportOptions, type CoderCreateSettings, type CoderTransport, type CoderWorkspaceBaseSettings, type CoderWorkspaceRef, CoderWorkspaceSession, type CoderWorkspaceSessionConfig, type CoderWorkspaceSettings, type CreateWorkspaceOptions, type ExecResult, type ForwardPortOptions, type LifecycleOptions, type ListPresetsOptions, type PortForward, type PresetInfo, type SpawnedProcess, type TransportExecOptions, type WorkspaceAgentInfo, type WorkspaceAgentLifecycle, type WorkspaceAgentStatus, type WorkspaceBuildStatus, type WorkspaceStatus, createCoderWorkspace };
|
|
511
|
+
export { CODER_WORKSPACE_PROVIDER_ID, CoderCliTransport, type CoderCliTransportOptions, type CoderCreateSettings, type CoderTransport, type CoderWorkspaceBaseSettings, type CoderWorkspaceRef, CoderWorkspaceSession, type CoderWorkspaceSessionConfig, type CoderWorkspaceSettings, type CreateWorkspaceOptions, type EnsureCoderWorkspaceSettings, type EnsuredCoderWorkspace, type ExecResult, type ForwardPortOptions, type LifecycleOptions, type ListPresetsOptions, type PortForward, type PresetInfo, type SpawnedProcess, type TransportExecOptions, type WorkspaceAgentInfo, type WorkspaceAgentLifecycle, type WorkspaceAgentStatus, type WorkspaceBuildStatus, type WorkspaceStatus, createCoderWorkspace, ensureCoderWorkspace };
|
package/dist/index.js
CHANGED
|
@@ -125,6 +125,8 @@ function parseWorkspaceStatus(workspace) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
return {
|
|
128
|
+
// The workspace UUID; omitted (not required) so old CLI output stays accepted.
|
|
129
|
+
...typeof ws.id === "string" && ws.id !== "" ? { id: ws.id } : {},
|
|
128
130
|
name: typeof ws.name === "string" ? ws.name : "",
|
|
129
131
|
buildStatus: typeof build.status === "string" ? build.status : "pending",
|
|
130
132
|
transition: typeof build.transition === "string" ? build.transition : "start",
|
|
@@ -833,34 +835,61 @@ function createCoderWorkspace(settings) {
|
|
|
833
835
|
}
|
|
834
836
|
};
|
|
835
837
|
}
|
|
838
|
+
async function ensureCoderWorkspace(settings) {
|
|
839
|
+
const transport = settings.transport ?? new CoderCliTransport();
|
|
840
|
+
const { created, status } = await ensureWorkspaceReady(
|
|
841
|
+
transport,
|
|
842
|
+
settings.workspace,
|
|
843
|
+
settings.create,
|
|
844
|
+
settings.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
|
|
845
|
+
"ensureCoderWorkspace",
|
|
846
|
+
settings.abortSignal
|
|
847
|
+
);
|
|
848
|
+
return { ...status, created };
|
|
849
|
+
}
|
|
836
850
|
async function ensureWorkspace(transport, workspace, settings, readyTimeoutMs, abortSignal) {
|
|
837
|
-
|
|
838
|
-
if (create === void 0) {
|
|
851
|
+
if (settings.create === void 0) {
|
|
839
852
|
if (settings.ensureStarted) {
|
|
840
853
|
await transport.start(workspace, { abortSignal });
|
|
841
854
|
}
|
|
842
855
|
return { createdByProvider: false };
|
|
843
856
|
}
|
|
857
|
+
const { created } = await ensureWorkspaceReady(
|
|
858
|
+
transport,
|
|
859
|
+
workspace,
|
|
860
|
+
settings.create,
|
|
861
|
+
readyTimeoutMs,
|
|
862
|
+
"createCoderWorkspace",
|
|
863
|
+
abortSignal
|
|
864
|
+
);
|
|
865
|
+
return { createdByProvider: created };
|
|
866
|
+
}
|
|
867
|
+
async function ensureWorkspaceReady(transport, workspace, create, readyTimeoutMs, caller, abortSignal) {
|
|
844
868
|
const existing = await transport.status(workspace, { abortSignal });
|
|
845
|
-
let
|
|
869
|
+
let created = false;
|
|
846
870
|
if (existing === null) {
|
|
871
|
+
if (create === void 0) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
`${caller}: workspace "${workspace}" does not exist; set \`create\` to create it from a template.`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
847
876
|
if (create.validate ?? true) {
|
|
848
|
-
await validatePreset(transport, create, abortSignal);
|
|
877
|
+
await validatePreset(transport, create, caller, abortSignal);
|
|
849
878
|
}
|
|
850
879
|
await transport.create(toCreateOptions(workspace, create, abortSignal));
|
|
851
|
-
|
|
880
|
+
created = true;
|
|
852
881
|
} else {
|
|
853
|
-
if (create
|
|
882
|
+
if (create?.ifExists === "error") {
|
|
854
883
|
throw new Error(
|
|
855
|
-
|
|
884
|
+
`${caller}: workspace "${workspace}" already exists (create.ifExists: 'error').`
|
|
856
885
|
);
|
|
857
886
|
}
|
|
858
887
|
if (isStopped(existing)) {
|
|
859
888
|
await transport.start(workspace, { abortSignal });
|
|
860
889
|
}
|
|
861
890
|
}
|
|
862
|
-
await waitForReady(transport, workspace, readyTimeoutMs, abortSignal);
|
|
863
|
-
return {
|
|
891
|
+
const status = await waitForReady(transport, workspace, readyTimeoutMs, caller, abortSignal);
|
|
892
|
+
return { created, status };
|
|
864
893
|
}
|
|
865
894
|
function isStopped(status) {
|
|
866
895
|
return status.buildStatus === "stopped" || status.buildStatus === "stopping" || status.transition === "stop";
|
|
@@ -889,7 +918,7 @@ function stringifyParams(params) {
|
|
|
889
918
|
}
|
|
890
919
|
return out;
|
|
891
920
|
}
|
|
892
|
-
async function validatePreset(transport, create, abortSignal) {
|
|
921
|
+
async function validatePreset(transport, create, caller, abortSignal) {
|
|
893
922
|
if (create.preset === void 0 || create.preset.toLowerCase() === "none") return;
|
|
894
923
|
let presets;
|
|
895
924
|
try {
|
|
@@ -906,11 +935,11 @@ async function validatePreset(transport, create, abortSignal) {
|
|
|
906
935
|
if (!presets.some((preset) => preset.name === create.preset)) {
|
|
907
936
|
const available = presets.map((preset) => `"${preset.name}"`).join(", ");
|
|
908
937
|
throw new Error(
|
|
909
|
-
|
|
938
|
+
`${caller}: preset "${create.preset}" not found for template "${create.template}". Available presets: ${available || "(none)"}.`
|
|
910
939
|
);
|
|
911
940
|
}
|
|
912
941
|
}
|
|
913
|
-
async function waitForReady(transport, workspace, timeoutMs, abortSignal) {
|
|
942
|
+
async function waitForReady(transport, workspace, timeoutMs, caller, abortSignal) {
|
|
914
943
|
const deadline = Date.now() + timeoutMs;
|
|
915
944
|
let last = "unknown";
|
|
916
945
|
for (; ; ) {
|
|
@@ -919,28 +948,26 @@ async function waitForReady(transport, workspace, timeoutMs, abortSignal) {
|
|
|
919
948
|
if (status !== null) {
|
|
920
949
|
last = `build=${status.buildStatus} agents=[` + status.agents.map((a) => `${a.name || "?"}:${a.status}/${a.lifecycleState}`).join(", ") + "]";
|
|
921
950
|
if (status.buildStatus === "failed") {
|
|
922
|
-
throw new Error(
|
|
951
|
+
throw new Error(`${caller}: workspace "${workspace}" build failed (${last}).`);
|
|
923
952
|
}
|
|
924
953
|
if (status.buildStatus === "canceled" || status.buildStatus === "deleted") {
|
|
925
|
-
throw new Error(
|
|
926
|
-
`createCoderWorkspace: workspace "${workspace}" is ${status.buildStatus} (${last}).`
|
|
927
|
-
);
|
|
954
|
+
throw new Error(`${caller}: workspace "${workspace}" is ${status.buildStatus} (${last}).`);
|
|
928
955
|
}
|
|
929
956
|
const errored = status.agents.find(
|
|
930
957
|
(a) => a.lifecycleState === "start_error" || a.lifecycleState === "start_timeout"
|
|
931
958
|
);
|
|
932
959
|
if (errored) {
|
|
933
960
|
throw new Error(
|
|
934
|
-
|
|
961
|
+
`${caller}: workspace "${workspace}" agent "${errored.name || "?"}" failed to start (lifecycle: ${errored.lifecycleState}).`
|
|
935
962
|
);
|
|
936
963
|
}
|
|
937
964
|
if (status.buildStatus === "running" && status.agents.some((a) => a.status === "connected" && a.lifecycleState === "ready")) {
|
|
938
|
-
return;
|
|
965
|
+
return status;
|
|
939
966
|
}
|
|
940
967
|
}
|
|
941
968
|
if (Date.now() >= deadline) {
|
|
942
969
|
throw new Error(
|
|
943
|
-
|
|
970
|
+
`${caller}: timed out after ${timeoutMs}ms waiting for workspace "${workspace}" to become ready (last status: ${last}).`
|
|
944
971
|
);
|
|
945
972
|
}
|
|
946
973
|
await delay(READY_POLL_INTERVAL_MS, void 0, { signal: abortSignal });
|
|
@@ -978,5 +1005,6 @@ export {
|
|
|
978
1005
|
CODER_WORKSPACE_PROVIDER_ID,
|
|
979
1006
|
CoderCliTransport,
|
|
980
1007
|
CoderWorkspaceSession,
|
|
981
|
-
createCoderWorkspace
|
|
1008
|
+
createCoderWorkspace,
|
|
1009
|
+
ensureCoderWorkspace
|
|
982
1010
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coder/ai-sdk-sandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Coder workspace sandbox provider for the Vercel AI SDK v7 HarnessAgent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"provenance": true
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@ai-sdk/harness": "1.0.
|
|
45
|
-
"@ai-sdk/harness-claude-code": "1.0.
|
|
46
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
47
|
-
"@ai-sdk/tui": "1.0.
|
|
44
|
+
"@ai-sdk/harness": "^1.0.23",
|
|
45
|
+
"@ai-sdk/harness-claude-code": "^1.0.23",
|
|
46
|
+
"@ai-sdk/provider-utils": "^5.0.7",
|
|
47
|
+
"@ai-sdk/tui": "^1.0.19",
|
|
48
48
|
"@arethetypeswrong/cli": "^0.18.0",
|
|
49
49
|
"@types/node": "^22",
|
|
50
50
|
"@vitest/coverage-v8": "^4.1.9",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"zod": "4.4.3"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@ai-sdk/harness": "^1.0.0
|
|
60
|
-
"@ai-sdk/provider-utils": "^5.0.0
|
|
59
|
+
"@ai-sdk/harness": "^1.0.0",
|
|
60
|
+
"@ai-sdk/provider-utils": "^5.0.0"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": ">=22"
|