@coder/ai-sdk-sandbox 0.1.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.
@@ -0,0 +1,447 @@
1
+ import { HarnessV1SandboxProvider, HarnessV1NetworkSandboxSession } from '@ai-sdk/harness';
2
+ import { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
3
+
4
+ /**
5
+ * Transport abstraction over a Coder workspace. The sandbox session talks to a
6
+ * workspace exclusively through this interface, which keeps the harness-facing
7
+ * session decoupled from *how* we reach Coder (the default is the `coder` CLI;
8
+ * tests inject a mock, and a future implementation could use the Coder REST API
9
+ * or a persistent SSH/SFTP connection).
10
+ */
11
+ interface CoderTransport {
12
+ /** Run a command to completion, buffering stdout/stderr into strings. */
13
+ exec(options: TransportExecOptions): Promise<ExecResult>;
14
+ /** Start a long-running process and return streaming handles immediately. */
15
+ spawn(options: TransportExecOptions): SpawnedProcess;
16
+ /**
17
+ * Open a TCP port-forward from the host to a port inside the workspace and
18
+ * resolve once the local endpoint accepts connections.
19
+ */
20
+ forwardPort(options: ForwardPortOptions): Promise<PortForward>;
21
+ /** Ensure the workspace is started. Should be idempotent. */
22
+ start(workspace: string, options?: LifecycleOptions): Promise<void>;
23
+ /** Stop the workspace. Should be idempotent. */
24
+ stop(workspace: string, options?: LifecycleOptions): Promise<void>;
25
+ /** Delete the workspace. Must tolerate an already-stopped workspace. */
26
+ destroy(workspace: string, options?: LifecycleOptions): Promise<void>;
27
+ /**
28
+ * Look up a workspace's current status, or `null` if it does not exist. Used
29
+ * for get-or-create and for polling readiness after a create/start.
30
+ */
31
+ status(workspace: string, options?: LifecycleOptions): Promise<WorkspaceStatus | null>;
32
+ /**
33
+ * Create a workspace from a template. Resolves once the provisioner build
34
+ * completes (which is *not* the same as the agent being ready — poll
35
+ * {@link CoderTransport.status} for that).
36
+ */
37
+ create(options: CreateWorkspaceOptions): Promise<void>;
38
+ /**
39
+ * List the presets defined for a template (optionally a specific version).
40
+ * Used for preflight validation of a requested preset name.
41
+ */
42
+ listPresets(options: ListPresetsOptions): Promise<PresetInfo[]>;
43
+ }
44
+ /**
45
+ * Workspace-level build status (`latest_build.status` in the Coder API) — the
46
+ * computed state of the workspace, not the raw provisioner-job status. A fully
47
+ * started workspace reports `'running'`.
48
+ *
49
+ * The trailing `string & Record<never, never>` arm accepts statuses from newer
50
+ * Coder versions while preserving editor autocomplete for the known values (the
51
+ * same idiom is used by the other status unions below).
52
+ */
53
+ type WorkspaceBuildStatus = "pending" | "starting" | "running" | "stopping" | "stopped" | "failed" | "canceling" | "canceled" | "deleting" | "deleted" | (string & Record<never, never>);
54
+ /** Connectivity of a workspace agent (`agent.status`). */
55
+ type WorkspaceAgentStatus = "connecting" | "connected" | "disconnected" | "timeout" | (string & Record<never, never>);
56
+ /** Startup-script lifecycle of a workspace agent (`agent.lifecycle_state`). */
57
+ type WorkspaceAgentLifecycle = "created" | "starting" | "start_timeout" | "start_error" | "ready" | "shutting_down" | "shutdown_timeout" | "shutdown_error" | "off" | (string & Record<never, never>);
58
+ interface WorkspaceAgentInfo {
59
+ /** Agent name (e.g. `main`). */
60
+ name: string;
61
+ /** Connectivity to the control plane. */
62
+ status: WorkspaceAgentStatus;
63
+ /** Startup-script progress; `'ready'` means the startup script finished. */
64
+ lifecycleState: WorkspaceAgentLifecycle;
65
+ }
66
+ interface WorkspaceStatus {
67
+ /** Workspace name (without owner/agent qualifiers). */
68
+ name: string;
69
+ /** Workspace-level build status (`latest_build.status`). */
70
+ buildStatus: WorkspaceBuildStatus;
71
+ /** Direction of the latest build: `'start' | 'stop' | 'delete'`. */
72
+ transition: "start" | "stop" | "delete" | (string & Record<never, never>);
73
+ /** Agents across the latest build's resources. */
74
+ agents: WorkspaceAgentInfo[];
75
+ }
76
+ interface CreateWorkspaceOptions {
77
+ /** Workspace name to create, optionally `owner/name`. */
78
+ workspace: string;
79
+ /** Template name to create from. */
80
+ template: string;
81
+ /** Specific template version name; defaults to the template's active version. */
82
+ templateVersion?: string;
83
+ /** Named preset to apply (`--preset`); `'none'` forces no preset. */
84
+ preset?: string;
85
+ /** Rich parameter values, already stringified (`--parameter name=value`). */
86
+ parameters?: Record<string, string>;
87
+ /** Path to a YAML rich-parameter file (`--rich-parameter-file`). */
88
+ parameterFile?: string;
89
+ /** Accept template defaults for any parameter not otherwise provided. */
90
+ useParameterDefaults?: boolean;
91
+ /** Ephemeral (one-time build) parameter values, already stringified. */
92
+ ephemeralParameters?: Record<string, string>;
93
+ /** Auto-stop the workspace after this duration, e.g. `'8h'` (`--stop-after`). */
94
+ stopAfter?: string;
95
+ /** `--automatic-updates` setting. */
96
+ automaticUpdates?: "always" | "never";
97
+ /** Organization name or uuid for ambiguous template names (`--org`). */
98
+ org?: string;
99
+ abortSignal?: AbortSignal;
100
+ }
101
+ interface ListPresetsOptions {
102
+ template: string;
103
+ templateVersion?: string;
104
+ org?: string;
105
+ abortSignal?: AbortSignal;
106
+ }
107
+ interface PresetInfo {
108
+ name: string;
109
+ /** Whether the template author marked this as the default preset. */
110
+ default: boolean;
111
+ description?: string;
112
+ }
113
+ interface TransportExecOptions {
114
+ /** Workspace reference: `[owner/]workspace[.agent]`. */
115
+ workspace: string;
116
+ /** Command to run, as a shell string executed by bash inside the workspace. */
117
+ command: string;
118
+ /** Absolute working directory to run in. */
119
+ workingDirectory?: string;
120
+ /** Environment variables to set for the command (remote-side). */
121
+ env?: Record<string, string>;
122
+ /** Payload written to the command's stdin, then closed. */
123
+ stdin?: Uint8Array | string;
124
+ abortSignal?: AbortSignal;
125
+ }
126
+ interface ExecResult {
127
+ exitCode: number;
128
+ stdout: string;
129
+ stderr: string;
130
+ }
131
+ /**
132
+ * Handle to a spawned process. Structurally compatible with the AI SDK's
133
+ * `Experimental_SandboxProcess`, so the session can return it directly.
134
+ */
135
+ interface SpawnedProcess {
136
+ readonly pid?: number;
137
+ readonly stdout: ReadableStream<Uint8Array>;
138
+ readonly stderr: ReadableStream<Uint8Array>;
139
+ wait(): Promise<{
140
+ exitCode: number;
141
+ }>;
142
+ kill(): Promise<void>;
143
+ }
144
+ interface ForwardPortOptions {
145
+ workspace: string;
146
+ /** Port inside the workspace to forward to. */
147
+ remotePort: number;
148
+ abortSignal?: AbortSignal;
149
+ }
150
+ interface PortForward {
151
+ /** Host interface the forward listens on (typically `127.0.0.1`). */
152
+ readonly localHost: string;
153
+ /** Host port that tunnels to the workspace's `remotePort`. */
154
+ readonly localPort: number;
155
+ /**
156
+ * `true` once the underlying tunnel process has exited or errored. Consumers
157
+ * read this to detect a dead tunnel and re-establish the forward.
158
+ */
159
+ readonly closed: boolean;
160
+ /** Tear down the forward. Idempotent. */
161
+ close(): Promise<void>;
162
+ }
163
+ interface LifecycleOptions {
164
+ abortSignal?: AbortSignal;
165
+ }
166
+
167
+ interface CoderCliTransportOptions {
168
+ /** Path or name of the coder binary. Default: `coder`. */
169
+ coderBinary?: string;
170
+ /**
171
+ * Path or name of the OpenSSH client used for exec/spawn. Default: `ssh`.
172
+ * Exec goes through real OpenSSH (via a `coder ssh --stdio` ProxyCommand)
173
+ * rather than `coder ssh <ws> -- cmd`, because the latter allocates a PTY —
174
+ * which mangles output (CRLF), merges stdout/stderr, and breaks exit-code
175
+ * propagation. `coder ssh`'s own help recommends `coder config-ssh` for full
176
+ * SSH parity; this is the programmatic equivalent.
177
+ */
178
+ sshBinary?: string;
179
+ /** Coder deployment URL; sets `CODER_URL`. Falls back to ambient `coder login`. */
180
+ url?: string;
181
+ /** Coder session token; sets `CODER_SESSION_TOKEN`. Falls back to ambient login. */
182
+ token?: string;
183
+ /** Extra environment merged into every coder/ssh invocation. */
184
+ env?: Record<string, string>;
185
+ /**
186
+ * Run remote commands through a bash *login* shell (`bash -lc`) so PATH and
187
+ * profile-managed toolchains (nvm, asdf, mise, …) resolve. Default: `true`.
188
+ */
189
+ loginShell?: boolean;
190
+ /**
191
+ * Coder startup-script wait behavior for the proxied connection
192
+ * (`coder ssh --wait`). Default `'no'`: programmatic exec should not block on
193
+ * (or stream the logs of) startup scripts. Set `'auto'`/`'yes'` if your
194
+ * workspace provisions required tooling in a blocking startup script.
195
+ */
196
+ waitMode?: "yes" | "no" | "auto";
197
+ /**
198
+ * Redirect the ProxyCommand's own stderr to /dev/null so coder CLI chatter
199
+ * (version-mismatch warnings, startup logs) does not bleed into a command's
200
+ * stderr. Default: `true`. Disable to surface coder connection errors.
201
+ */
202
+ silenceProxyStderr?: boolean;
203
+ /** Timeout (ms) to wait for a port-forward's local endpoint. Default 30000. */
204
+ portForwardTimeoutMs?: number;
205
+ }
206
+ /**
207
+ * Default {@link CoderTransport}. Exec/spawn use OpenSSH via a
208
+ * `coder ssh --stdio` ProxyCommand; port-forward and lifecycle use the `coder`
209
+ * CLI directly.
210
+ */
211
+ declare class CoderCliTransport implements CoderTransport {
212
+ #private;
213
+ constructor(options?: CoderCliTransportOptions);
214
+ exec(options: TransportExecOptions): Promise<ExecResult>;
215
+ spawn(options: TransportExecOptions): SpawnedProcess;
216
+ forwardPort(options: ForwardPortOptions): Promise<PortForward>;
217
+ start(workspace: string, options?: LifecycleOptions): Promise<void>;
218
+ stop(workspace: string, options?: LifecycleOptions): Promise<void>;
219
+ destroy(workspace: string, options?: LifecycleOptions): Promise<void>;
220
+ status(workspace: string, options?: LifecycleOptions): Promise<WorkspaceStatus | null>;
221
+ create(options: CreateWorkspaceOptions): Promise<void>;
222
+ listPresets(options: ListPresetsOptions): Promise<PresetInfo[]>;
223
+ }
224
+
225
+ /** Stable provider id reported on the {@link HarnessV1SandboxProvider}. */
226
+ declare const CODER_WORKSPACE_PROVIDER_ID = "coder-workspace";
227
+ /**
228
+ * Settings for creating a workspace on demand from a template. Set this as the
229
+ * `create` field on {@link CoderWorkspaceSettings} to enable "create mode": the
230
+ * provider will get-or-create a workspace (rather than only wrapping an existing
231
+ * one) and wait for its agent to become ready before running the harness.
232
+ */
233
+ interface CoderCreateSettings {
234
+ /** Template name to create from. Required to enable creation. */
235
+ template: string;
236
+ /** Specific template version name; defaults to the template's active version. */
237
+ templateVersion?: string;
238
+ /**
239
+ * Named preset to apply (`coder create --preset`). Use `'none'` to force no
240
+ * preset. Note: a preset's parameter values take precedence over any
241
+ * overlapping {@link CoderCreateSettings.parameters} (Coder's behavior) — set
242
+ * a given value via the preset *or* `parameters`, not both.
243
+ */
244
+ preset?: string;
245
+ /**
246
+ * Rich parameter values by parameter name. Numbers and booleans are
247
+ * stringified. For `list(string)` parameters prefer {@link parameterFile}.
248
+ */
249
+ parameters?: Record<string, string | number | boolean>;
250
+ /** Path to a YAML rich-parameter file (`--rich-parameter-file`). */
251
+ parameterFile?: string;
252
+ /** Accept template defaults for any parameter not otherwise provided. */
253
+ useParameterDefaults?: boolean;
254
+ /** Ephemeral (one-time build) parameter values by name. */
255
+ ephemeralParameters?: Record<string, string | number | boolean>;
256
+ /** Auto-stop the workspace after this duration, e.g. `'8h'` (`--stop-after`). */
257
+ stopAfter?: string;
258
+ /** `--automatic-updates` setting (default: Coder's, `never`). */
259
+ automaticUpdates?: "always" | "never";
260
+ /** Organization name or uuid for ambiguous template names (`--org`). */
261
+ org?: string;
262
+ /**
263
+ * Owner for an auto-derived workspace name (`owner/name`). Only applied when
264
+ * the name is derived from the sessionId; when you pass an explicit
265
+ * `workspace` string, include the owner there. Defaults to the authenticated
266
+ * user.
267
+ */
268
+ owner?: string;
269
+ /**
270
+ * What to do if a workspace with the target name already exists:
271
+ * `'attach'` (default) reuses it; `'error'` fails.
272
+ */
273
+ ifExists?: "attach" | "error";
274
+ /**
275
+ * Prefix for the workspace name derived from the harness sessionId when no
276
+ * explicit `workspace` is set (fresh-per-session). Default: `'agent'`.
277
+ */
278
+ namePrefix?: string;
279
+ /**
280
+ * Preflight-validate the requested {@link preset} name against the template's
281
+ * presets before creating, failing fast with the available names. Best-effort
282
+ * (skipped silently if introspection fails). Default: `true`.
283
+ */
284
+ validate?: boolean;
285
+ }
286
+ /**
287
+ * A workspace reference: a fixed `[owner/]workspace[.agent]`, or a resolver from
288
+ * the harness `sessionId`.
289
+ */
290
+ type CoderWorkspaceRef = string | ((sessionId: string | undefined) => string);
291
+ /** Settings common to every {@link createCoderWorkspace} configuration. */
292
+ interface CoderWorkspaceBaseSettings {
293
+ /** Max time (ms) to wait for the agent to become ready after create/start. Default 300000. */
294
+ readyTimeoutMs?: number;
295
+ /**
296
+ * Ports the workspace exposes. The bridge-backed adapters resolve the bridge
297
+ * port from `createClaudeCode({ port })` or, failing that, `ports[0]`, so the
298
+ * default exposes a single port (4000) that the bridge will bind and that
299
+ * `getPortUrl` will forward. Default: `[4000]`.
300
+ */
301
+ ports?: number[];
302
+ /**
303
+ * Absolute default working directory. If omitted, it is resolved from `$HOME`
304
+ * in the workspace at session-create time, falling back to `/home/coder`.
305
+ */
306
+ defaultWorkingDirectory?: string;
307
+ /**
308
+ * Whether this provider owns the workspace lifecycle (`stop()`/`destroy()`
309
+ * actually stop/delete it). Default depends on mode:
310
+ * - **wrap mode** (no `create`): `false` — `stop()`/`destroy()` only release
311
+ * host-side resources and never touch the workspace.
312
+ * - **create mode**: `true` — but a workspace the provider only *attached* to
313
+ * (an explicitly-named, pre-existing one) is never deleted; only workspaces
314
+ * the provider actually created are. A per-session derived name is always
315
+ * treated as owned.
316
+ */
317
+ ownsLifecycle?: boolean;
318
+ /** Run `coder start` before attaching (useful for stopped workspaces in wrap mode). */
319
+ ensureStarted?: boolean;
320
+ /**
321
+ * Transport used to reach Coder. Defaults to a {@link CoderCliTransport} that
322
+ * shells out to an ambient `coder` login. To configure the CLI transport —
323
+ * binary paths, `url`/`token`, extra env, login shell, startup-wait behavior —
324
+ * construct one explicitly, e.g. `transport: new CoderCliTransport({ url, token })`.
325
+ * You can also supply a non-CLI transport (REST, tests, …).
326
+ */
327
+ transport?: CoderTransport;
328
+ }
329
+ /**
330
+ * Settings for {@link createCoderWorkspace}. **At least one of `workspace` or
331
+ * `create` is required** — and you may set both:
332
+ * - `workspace` only — wrap an existing workspace.
333
+ * - `create` only — create a fresh per-session workspace from a template (its
334
+ * name is derived from the harness `sessionId`), deleted on `destroy()`.
335
+ * - both — get-or-create the named `workspace` from the template.
336
+ */
337
+ type CoderWorkspaceSettings = CoderWorkspaceBaseSettings & ({
338
+ /**
339
+ * The workspace to use, as `[owner/]workspace[.agent]` — a fixed name or
340
+ * a resolver from the harness `sessionId`. With `create` it is
341
+ * get-or-created; otherwise it must already exist.
342
+ */
343
+ workspace: CoderWorkspaceRef;
344
+ /** Optionally create the workspace from a template if it doesn't exist. */
345
+ create?: CoderCreateSettings;
346
+ } | {
347
+ /**
348
+ * Optional explicit workspace name/resolver. Omit to derive a fresh
349
+ * per-session name from the harness `sessionId`.
350
+ */
351
+ workspace?: CoderWorkspaceRef;
352
+ /** Create the workspace on demand from a template. See {@link CoderCreateSettings}. */
353
+ create: CoderCreateSettings;
354
+ });
355
+ /**
356
+ * Create a {@link HarnessV1SandboxProvider} that runs harness sessions inside a
357
+ * Coder workspace. Either wraps an existing workspace, or — when a `create`
358
+ * block is supplied — creates one on demand from a template.
359
+ *
360
+ * @example Wrap an existing workspace
361
+ * ```ts
362
+ * createCoderWorkspace({ workspace: 'my-dev-workspace' })
363
+ * ```
364
+ *
365
+ * @example Create a fresh per-session workspace from a template
366
+ * ```ts
367
+ * createCoderWorkspace({
368
+ * create: { template: 'docker', preset: 'Large' },
369
+ * })
370
+ * ```
371
+ */
372
+ declare function createCoderWorkspace(settings: CoderWorkspaceSettings): HarnessV1SandboxProvider;
373
+
374
+ interface ReadFileOptions {
375
+ path: string;
376
+ abortSignal?: AbortSignal;
377
+ }
378
+ interface ReadTextFileOptions extends ReadFileOptions {
379
+ encoding?: string;
380
+ startLine?: number;
381
+ endLine?: number;
382
+ }
383
+ interface WriteFileOptions<CONTENT> {
384
+ path: string;
385
+ content: CONTENT;
386
+ abortSignal?: AbortSignal;
387
+ }
388
+ interface WriteTextFileOptions extends WriteFileOptions<string> {
389
+ encoding?: string;
390
+ }
391
+
392
+ type SandboxProcessOptions = Parameters<Experimental_SandboxSession["run"]>[0];
393
+ interface CoderWorkspaceSessionConfig {
394
+ transport: CoderTransport;
395
+ /** Workspace reference: `[owner/]workspace[.agent]`. */
396
+ workspace: string;
397
+ /** Stable id used by the harness for cross-process resume (the workspace name). */
398
+ id: string;
399
+ /** Absolute default working directory for `run`/`spawn` and relative file paths. */
400
+ defaultWorkingDirectory: string;
401
+ /** Ports the workspace exposes; `ports[0]` is what the adapter binds the bridge to. */
402
+ ports: number[];
403
+ /**
404
+ * When true, `stop()`/`destroy()` actually stop/delete the workspace. When
405
+ * false (wrapping a caller-owned workspace) they only release host-side
406
+ * resources (port-forwards) and leave the workspace running.
407
+ */
408
+ ownsLifecycle: boolean;
409
+ }
410
+ /**
411
+ * A {@link HarnessV1NetworkSandboxSession} backed by a Coder workspace.
412
+ *
413
+ * Exec maps to `coder ssh`, file I/O to base64-over-`coder ssh`, and
414
+ * `getPortUrl` to an OpenSSH `-L` local forward over a `coder ssh --stdio`
415
+ * ProxyCommand, exposed as a local `ws://127.0.0.1:<port>` URL — which is what
416
+ * bridge-backed harness adapters (Claude Code, Codex) open their WebSocket
417
+ * against.
418
+ */
419
+ declare class CoderWorkspaceSession implements HarnessV1NetworkSandboxSession {
420
+ #private;
421
+ readonly id: string;
422
+ readonly defaultWorkingDirectory: string;
423
+ readonly description: string;
424
+ constructor(config: CoderWorkspaceSessionConfig);
425
+ get ports(): ReadonlyArray<number>;
426
+ readonly run: (options: SandboxProcessOptions) => Promise<ExecResult>;
427
+ readonly spawn: (options: SandboxProcessOptions) => Promise<SpawnedProcess>;
428
+ readonly readFile: (options: ReadFileOptions) => Promise<ReadableStream<Uint8Array<ArrayBufferLike>> | null>;
429
+ readonly readBinaryFile: (options: ReadFileOptions) => Promise<Uint8Array<ArrayBufferLike> | null>;
430
+ readonly readTextFile: (options: ReadTextFileOptions) => Promise<string | null>;
431
+ readonly writeFile: (options: WriteFileOptions<ReadableStream<Uint8Array>>) => Promise<void>;
432
+ readonly writeBinaryFile: (options: WriteFileOptions<Uint8Array>) => Promise<void>;
433
+ readonly writeTextFile: (options: WriteTextFileOptions) => Promise<void>;
434
+ readonly getPortUrl: (options: {
435
+ port: number;
436
+ protocol?: "http" | "https" | "ws";
437
+ }) => Promise<string>;
438
+ readonly setPorts: (ports: ReadonlyArray<number>, _options?: {
439
+ abortSignal?: AbortSignal;
440
+ }) => Promise<void>;
441
+ readonly stop: () => Promise<void>;
442
+ readonly destroy: () => Promise<void>;
443
+ /** Reduced view exposing only the base file/exec surface (no infra controls). */
444
+ readonly restricted: () => Experimental_SandboxSession;
445
+ }
446
+
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 };