@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.
package/dist/index.js ADDED
@@ -0,0 +1,982 @@
1
+ // src/cli-transport.ts
2
+ import { spawn as nodeSpawn } from "child_process";
3
+ import net from "net";
4
+ import { Readable } from "stream";
5
+
6
+ // src/shell.ts
7
+ function shellQuote(value) {
8
+ return `'${value.replace(/'/g, `'\\''`)}'`;
9
+ }
10
+ function buildRemoteScript(options) {
11
+ const segments = [];
12
+ if (options.workingDirectory !== void 0 && options.workingDirectory !== "") {
13
+ segments.push(`cd ${shellQuote(options.workingDirectory)} &&`);
14
+ }
15
+ const envEntries = Object.entries(options.env ?? {});
16
+ if (envEntries.length > 0) {
17
+ const assignments = envEntries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ");
18
+ segments.push(`env ${assignments} bash -c ${shellQuote(options.command)}`);
19
+ } else {
20
+ segments.push(options.command);
21
+ }
22
+ return segments.join(" ");
23
+ }
24
+
25
+ // src/cli-transport.ts
26
+ var DEFAULT_PORT_FORWARD_TIMEOUT_MS = 3e4;
27
+ function buildSshArgs(workspace, remoteScript, options) {
28
+ const shell = options.loginShell ? "bash -lc" : "bash -c";
29
+ const remoteCommand = `${shell} ${shellQuote(remoteScript)}`;
30
+ const proxy = `${shellQuote(options.coderBinary)} ssh --stdio --wait=${options.waitMode} ${shellQuote(workspace)}` + (options.silenceProxyStderr ? " 2>/dev/null" : "");
31
+ return [
32
+ "-o",
33
+ `ProxyCommand=${proxy}`,
34
+ "-o",
35
+ "StrictHostKeyChecking=no",
36
+ "-o",
37
+ "UserKnownHostsFile=/dev/null",
38
+ "-o",
39
+ "LogLevel=ERROR",
40
+ "-T",
41
+ sshHostAlias(workspace),
42
+ remoteCommand
43
+ ];
44
+ }
45
+ function sshHostAlias(workspace) {
46
+ return `coder.${workspace.replace(/[^A-Za-z0-9_.-]/g, "-")}`;
47
+ }
48
+ function buildLocalForwardArgs(workspace, localPort, remotePort, options) {
49
+ const proxy = `${shellQuote(options.coderBinary)} ssh --stdio --wait=${options.waitMode} ${shellQuote(workspace)}` + (options.silenceProxyStderr ? " 2>/dev/null" : "");
50
+ return [
51
+ "-o",
52
+ `ProxyCommand=${proxy}`,
53
+ "-o",
54
+ "StrictHostKeyChecking=no",
55
+ "-o",
56
+ "UserKnownHostsFile=/dev/null",
57
+ "-o",
58
+ "LogLevel=ERROR",
59
+ "-o",
60
+ "ExitOnForwardFailure=yes",
61
+ "-N",
62
+ "-L",
63
+ `${localPort}:127.0.0.1:${remotePort}`,
64
+ sshHostAlias(workspace)
65
+ ];
66
+ }
67
+ function buildCreateArgs(options) {
68
+ const args = ["create", options.workspace, "--yes", "--template", options.template];
69
+ if (options.templateVersion !== void 0) {
70
+ args.push("--template-version", options.templateVersion);
71
+ }
72
+ if (options.preset !== void 0) {
73
+ args.push("--preset", options.preset);
74
+ }
75
+ for (const [name, value] of Object.entries(options.parameters ?? {})) {
76
+ args.push("--parameter", `${name}=${value}`);
77
+ }
78
+ if (options.parameterFile !== void 0) {
79
+ args.push("--rich-parameter-file", options.parameterFile);
80
+ }
81
+ if (options.useParameterDefaults) {
82
+ args.push("--use-parameter-defaults");
83
+ }
84
+ for (const [name, value] of Object.entries(options.ephemeralParameters ?? {})) {
85
+ args.push("--ephemeral-parameter", `${name}=${value}`);
86
+ }
87
+ if (options.stopAfter !== void 0) {
88
+ args.push("--stop-after", options.stopAfter);
89
+ }
90
+ if (options.automaticUpdates !== void 0) {
91
+ args.push("--automatic-updates", options.automaticUpdates);
92
+ }
93
+ if (options.org !== void 0) {
94
+ args.push("--org", options.org);
95
+ }
96
+ return args;
97
+ }
98
+ function parseWorkspaceRef(ref) {
99
+ const slashCount = (ref.match(/\//g) ?? []).length;
100
+ if (slashCount > 1) {
101
+ throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
102
+ }
103
+ const [ownerOrName, maybeName] = ref.includes("/") ? ref.split("/", 2) : ["me", ref];
104
+ const name = (maybeName ?? ownerOrName).split(".")[0] ?? "";
105
+ if (name === "" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
106
+ throw new Error(`invalid workspace reference "${ref}"; expected [owner/]name[.agent]`);
107
+ }
108
+ return { owner: ownerOrName === "" ? "me" : ownerOrName, name };
109
+ }
110
+ function parseWorkspaceStatus(workspace) {
111
+ const ws = asRecord(workspace);
112
+ const build = asRecord(ws.latest_build);
113
+ const resources = Array.isArray(build.resources) ? build.resources : [];
114
+ const agents = [];
115
+ for (const resource of resources) {
116
+ const list = asRecord(resource).agents;
117
+ if (!Array.isArray(list)) continue;
118
+ for (const agent of list) {
119
+ const a = asRecord(agent);
120
+ agents.push({
121
+ name: typeof a.name === "string" ? a.name : "",
122
+ status: typeof a.status === "string" ? a.status : "connecting",
123
+ lifecycleState: typeof a.lifecycle_state === "string" ? a.lifecycle_state : "created"
124
+ });
125
+ }
126
+ }
127
+ return {
128
+ name: typeof ws.name === "string" ? ws.name : "",
129
+ buildStatus: typeof build.status === "string" ? build.status : "pending",
130
+ transition: typeof build.transition === "string" ? build.transition : "start",
131
+ agents
132
+ };
133
+ }
134
+ function parsePresetsOutput(stdout) {
135
+ const trimmed = stdout.trim();
136
+ if (trimmed === "" || /^no presets found/i.test(trimmed)) return [];
137
+ return parsePresetList(JSON.parse(trimmed));
138
+ }
139
+ function parsePresetList(json) {
140
+ if (!Array.isArray(json)) return [];
141
+ return json.map((entry) => {
142
+ const record = asRecord(entry);
143
+ const preset = asRecord(record.TemplatePreset ?? record);
144
+ const name = pickString(preset, "Name", "name") ?? "";
145
+ const description = pickString(preset, "Description", "description");
146
+ return {
147
+ name,
148
+ default: pickBoolean(preset, "Default", "default") ?? false,
149
+ ...description !== void 0 && description !== "" ? { description } : {}
150
+ };
151
+ });
152
+ }
153
+ function asRecord(value) {
154
+ return typeof value === "object" && value !== null ? value : {};
155
+ }
156
+ function pickString(record, ...keys) {
157
+ for (const key of keys) {
158
+ if (typeof record[key] === "string") return record[key];
159
+ }
160
+ return void 0;
161
+ }
162
+ function pickBoolean(record, ...keys) {
163
+ for (const key of keys) {
164
+ if (typeof record[key] === "boolean") return record[key];
165
+ }
166
+ return void 0;
167
+ }
168
+ var CoderCliTransport = class {
169
+ #coderBinary;
170
+ #sshBinary;
171
+ #url;
172
+ #token;
173
+ #extraEnv;
174
+ #loginShell;
175
+ #waitMode;
176
+ #silenceProxyStderr;
177
+ #portForwardTimeoutMs;
178
+ constructor(options = {}) {
179
+ this.#coderBinary = options.coderBinary ?? "coder";
180
+ this.#sshBinary = options.sshBinary ?? "ssh";
181
+ this.#url = options.url;
182
+ this.#token = options.token;
183
+ this.#extraEnv = options.env ?? {};
184
+ this.#loginShell = options.loginShell ?? true;
185
+ this.#waitMode = options.waitMode ?? "no";
186
+ this.#silenceProxyStderr = options.silenceProxyStderr ?? true;
187
+ this.#portForwardTimeoutMs = options.portForwardTimeoutMs ?? DEFAULT_PORT_FORWARD_TIMEOUT_MS;
188
+ }
189
+ #childEnv() {
190
+ return {
191
+ ...process.env,
192
+ ...this.#url ? { CODER_URL: this.#url } : {},
193
+ ...this.#token ? { CODER_SESSION_TOKEN: this.#token } : {},
194
+ ...this.#extraEnv
195
+ };
196
+ }
197
+ #sshArgs(options) {
198
+ const script = buildRemoteScript({
199
+ command: options.command,
200
+ workingDirectory: options.workingDirectory,
201
+ env: options.env
202
+ });
203
+ return buildSshArgs(options.workspace, script, {
204
+ coderBinary: this.#coderBinary,
205
+ loginShell: this.#loginShell,
206
+ waitMode: this.#waitMode,
207
+ silenceProxyStderr: this.#silenceProxyStderr
208
+ });
209
+ }
210
+ exec(options) {
211
+ return this.#run(this.#sshBinary, this.#sshArgs(options), {
212
+ stdin: options.stdin,
213
+ abortSignal: options.abortSignal
214
+ });
215
+ }
216
+ spawn(options) {
217
+ const child = nodeSpawn(this.#sshBinary, this.#sshArgs(options), {
218
+ stdio: [options.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"],
219
+ env: this.#childEnv(),
220
+ signal: options.abortSignal
221
+ });
222
+ writeStdin(child, options.stdin);
223
+ return toSpawnedProcess(child, this.#sshBinary, options.abortSignal);
224
+ }
225
+ async forwardPort(options) {
226
+ const localPort = await allocateLocalPort();
227
+ const child = nodeSpawn(
228
+ this.#sshBinary,
229
+ buildLocalForwardArgs(options.workspace, localPort, options.remotePort, {
230
+ coderBinary: this.#coderBinary,
231
+ waitMode: this.#waitMode,
232
+ silenceProxyStderr: this.#silenceProxyStderr
233
+ }),
234
+ {
235
+ stdio: ["ignore", "pipe", "pipe"],
236
+ env: this.#childEnv(),
237
+ signal: options.abortSignal
238
+ }
239
+ );
240
+ let closed = false;
241
+ child.once("close", () => {
242
+ closed = true;
243
+ });
244
+ child.once("error", () => {
245
+ closed = true;
246
+ });
247
+ let stderr = "";
248
+ child.stderr?.setEncoding("utf8");
249
+ const onStderr = (chunk) => {
250
+ stderr += chunk;
251
+ };
252
+ child.stderr?.on("data", onStderr);
253
+ try {
254
+ await waitForLocalPort(localPort, child, this.#portForwardTimeoutMs, options.abortSignal);
255
+ } catch (error) {
256
+ child.kill("SIGTERM");
257
+ const code = error?.code;
258
+ if (code === "ENOENT" || code === "EACCES") {
259
+ throw describeSpawnError(error, this.#sshBinary);
260
+ }
261
+ const detail = stderr.trim();
262
+ throw new Error(
263
+ `ssh -L forward (${options.workspace} :${options.remotePort}) failed to become ready` + (detail ? `: ${detail}` : ""),
264
+ { cause: error }
265
+ );
266
+ }
267
+ child.stderr?.off("data", onStderr);
268
+ return {
269
+ localHost: "127.0.0.1",
270
+ localPort,
271
+ get closed() {
272
+ return closed;
273
+ },
274
+ close: async () => {
275
+ if (closed) return;
276
+ closed = true;
277
+ child.kill("SIGTERM");
278
+ }
279
+ };
280
+ }
281
+ async start(workspace, options) {
282
+ await this.#runLifecycle(["start", workspace, "--yes"], workspace, "start", options);
283
+ }
284
+ async stop(workspace, options) {
285
+ await this.#runLifecycle(["stop", workspace, "--yes"], workspace, "stop", options);
286
+ }
287
+ async destroy(workspace, options) {
288
+ await this.#runLifecycle(["delete", workspace, "--yes"], workspace, "delete", options);
289
+ }
290
+ async status(workspace, options) {
291
+ const { owner, name } = parseWorkspaceRef(workspace);
292
+ const result = await this.#run(
293
+ this.#coderBinary,
294
+ ["list", "--output", "json", "--search", `owner:${owner} name:${name}`],
295
+ { abortSignal: options?.abortSignal }
296
+ );
297
+ if (result.exitCode !== 0) {
298
+ throw new Error(
299
+ `coder list (${workspace}) failed (exit ${result.exitCode}): ${(result.stderr || result.stdout).trim()}`
300
+ );
301
+ }
302
+ let parsed;
303
+ try {
304
+ parsed = JSON.parse(result.stdout);
305
+ } catch (error) {
306
+ throw new Error(`coder list (${workspace}) returned invalid JSON`, { cause: error });
307
+ }
308
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
309
+ return parseWorkspaceStatus(parsed[0]);
310
+ }
311
+ async create(options) {
312
+ const result = await this.#run(this.#coderBinary, buildCreateArgs(options), {
313
+ abortSignal: options.abortSignal
314
+ });
315
+ if (result.exitCode !== 0) {
316
+ throw new Error(
317
+ `coder create ${options.workspace} (template ${options.template}) failed (exit ${result.exitCode}): ${(result.stderr || result.stdout).trim()}`
318
+ );
319
+ }
320
+ }
321
+ async listPresets(options) {
322
+ const args = ["templates", "presets", "list", options.template, "--output", "json"];
323
+ if (options.templateVersion !== void 0) {
324
+ args.push("--template-version", options.templateVersion);
325
+ }
326
+ if (options.org !== void 0) {
327
+ args.push("--org", options.org);
328
+ }
329
+ const result = await this.#run(this.#coderBinary, args, {
330
+ abortSignal: options.abortSignal
331
+ });
332
+ if (result.exitCode !== 0) {
333
+ throw new Error(
334
+ `coder templates presets list ${options.template} failed (exit ${result.exitCode}): ${(result.stderr || result.stdout).trim()}`
335
+ );
336
+ }
337
+ try {
338
+ return parsePresetsOutput(result.stdout);
339
+ } catch (error) {
340
+ throw new Error(
341
+ `coder templates presets list ${options.template} returned invalid JSON: ` + result.stdout.trim().slice(0, 120),
342
+ { cause: error }
343
+ );
344
+ }
345
+ }
346
+ async #runLifecycle(args, workspace, verb, options) {
347
+ const result = await this.#run(this.#coderBinary, args, {
348
+ abortSignal: options?.abortSignal
349
+ });
350
+ if (result.exitCode !== 0) {
351
+ throw new Error(
352
+ `coder ${verb} ${workspace} failed (exit ${result.exitCode}): ${(result.stderr || result.stdout).trim()}`
353
+ );
354
+ }
355
+ }
356
+ #run(binary, args, options) {
357
+ return new Promise((resolve, reject) => {
358
+ const child = nodeSpawn(binary, args, {
359
+ stdio: [options.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"],
360
+ env: this.#childEnv(),
361
+ signal: options.abortSignal
362
+ });
363
+ let stdout = "";
364
+ let stderr = "";
365
+ child.stdout?.setEncoding("utf8");
366
+ child.stdout?.on("data", (chunk) => {
367
+ stdout += chunk;
368
+ });
369
+ child.stderr?.setEncoding("utf8");
370
+ child.stderr?.on("data", (chunk) => {
371
+ stderr += chunk;
372
+ });
373
+ child.on("error", (error) => reject(describeSpawnError(error, binary)));
374
+ child.on("close", (code) => {
375
+ resolve({ exitCode: code ?? 0, stdout, stderr });
376
+ });
377
+ writeStdin(child, options.stdin);
378
+ });
379
+ }
380
+ };
381
+ function describeSpawnError(error, binary) {
382
+ const code = error?.code;
383
+ if (code === "ENOENT" || code === "EACCES") {
384
+ return new Error(
385
+ `failed to launch "${binary}": ${code === "ENOENT" ? "not found on PATH" : "not executable"}. Install the Coder CLI (https://coder.com/docs/install) and run \`coder login\`, ensure an OpenSSH client is on PATH, or set explicit paths via new CoderCliTransport({ coderBinary, sshBinary }).`,
386
+ { cause: error }
387
+ );
388
+ }
389
+ return error;
390
+ }
391
+ function writeStdin(child, stdin) {
392
+ if (stdin === void 0 || child.stdin === null) return;
393
+ child.stdin.on("error", () => {
394
+ });
395
+ child.stdin.end(stdin);
396
+ }
397
+ function toSpawnedProcess(child, binary, abortSignal) {
398
+ const stdout = nodeReadableToWebStream(child.stdout);
399
+ const stderr = nodeReadableToWebStream(child.stderr);
400
+ let settled = false;
401
+ const wait = new Promise((resolve, reject) => {
402
+ const onError = (error) => finish(() => reject(describeSpawnError(error, binary)));
403
+ const onClose = (code) => finish(() => resolve({ exitCode: code ?? 0 }));
404
+ const onAbort = () => finish(() => reject(abortSignal?.reason ?? new Error("aborted")));
405
+ const finish = (fn) => {
406
+ if (settled) return;
407
+ settled = true;
408
+ child.off("error", onError);
409
+ child.off("close", onClose);
410
+ abortSignal?.removeEventListener("abort", onAbort);
411
+ fn();
412
+ };
413
+ child.on("error", onError);
414
+ child.on("close", onClose);
415
+ if (abortSignal) {
416
+ if (abortSignal.aborted) {
417
+ finish(() => reject(abortSignal.reason ?? new Error("aborted")));
418
+ } else {
419
+ abortSignal.addEventListener("abort", onAbort, { once: true });
420
+ }
421
+ }
422
+ });
423
+ return {
424
+ pid: child.pid,
425
+ stdout,
426
+ stderr,
427
+ wait: () => wait,
428
+ kill: async () => {
429
+ child.kill("SIGTERM");
430
+ }
431
+ };
432
+ }
433
+ function nodeReadableToWebStream(readable) {
434
+ if (readable === null) {
435
+ return new ReadableStream({
436
+ start(controller) {
437
+ controller.close();
438
+ }
439
+ });
440
+ }
441
+ return Readable.toWeb(readable);
442
+ }
443
+ function allocateLocalPort() {
444
+ return new Promise((resolve, reject) => {
445
+ const server = net.createServer();
446
+ server.unref();
447
+ server.once("error", reject);
448
+ server.listen(0, "127.0.0.1", () => {
449
+ const address = server.address();
450
+ const port = typeof address === "object" && address !== null ? address.port : 0;
451
+ server.close(() => {
452
+ if (port === 0) reject(new Error("failed to allocate a local port"));
453
+ else resolve(port);
454
+ });
455
+ });
456
+ });
457
+ }
458
+ function waitForLocalPort(port, child, timeoutMs, abortSignal) {
459
+ const deadline = Date.now() + timeoutMs;
460
+ return new Promise((resolve, reject) => {
461
+ let done = false;
462
+ const settle = (fn) => {
463
+ if (done) return;
464
+ done = true;
465
+ child.off("close", onClose);
466
+ child.off("error", onError);
467
+ abortSignal?.removeEventListener("abort", onAbort);
468
+ fn();
469
+ };
470
+ const onClose = (code) => settle(() => reject(new Error(`ssh -L forward exited early (code ${code ?? "null"})`)));
471
+ const onError = (error) => settle(() => reject(error));
472
+ const onAbort = () => settle(() => reject(abortSignal?.reason ?? new Error("aborted")));
473
+ child.on("close", onClose);
474
+ child.on("error", onError);
475
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
476
+ const attempt = () => {
477
+ if (done) return;
478
+ const socket = net.connect(port, "127.0.0.1");
479
+ socket.once("connect", () => {
480
+ socket.destroy();
481
+ settle(resolve);
482
+ });
483
+ socket.once("error", () => {
484
+ socket.destroy();
485
+ if (done) return;
486
+ if (Date.now() > deadline) {
487
+ settle(() => reject(new Error(`timed out waiting for local port ${port}`)));
488
+ return;
489
+ }
490
+ setTimeout(attempt, 200);
491
+ });
492
+ };
493
+ attempt();
494
+ });
495
+ }
496
+
497
+ // src/coder-workspace-provider.ts
498
+ import { createHash } from "crypto";
499
+ import { setTimeout as delay } from "timers/promises";
500
+
501
+ // src/file-io.ts
502
+ import path from "path";
503
+ var MISSING_FILE_EXIT = 66;
504
+ function resolveRemotePath(ctx, p) {
505
+ return p.startsWith("/") ? p : path.posix.join(ctx.defaultWorkingDirectory, p);
506
+ }
507
+ async function readBinaryFile(ctx, options) {
508
+ const abs = resolveRemotePath(ctx, options.path);
509
+ const quoted = shellQuote(abs);
510
+ const command = `if [ -f ${quoted} ]; then base64 < ${quoted}; else exit ${MISSING_FILE_EXIT}; fi`;
511
+ const result = await ctx.transport.exec({
512
+ workspace: ctx.workspace,
513
+ command,
514
+ abortSignal: options.abortSignal
515
+ });
516
+ if (result.exitCode === MISSING_FILE_EXIT) return null;
517
+ if (result.exitCode !== 0) {
518
+ throw new Error(`failed to read ${abs} (exit ${result.exitCode}): ${result.stderr.trim()}`);
519
+ }
520
+ const base64 = result.stdout.replace(/\s+/g, "");
521
+ return new Uint8Array(Buffer.from(base64, "base64"));
522
+ }
523
+ async function readFile(ctx, options) {
524
+ const bytes = await readBinaryFile(ctx, options);
525
+ if (bytes === null) return null;
526
+ return new ReadableStream({
527
+ start(controller) {
528
+ controller.enqueue(bytes);
529
+ controller.close();
530
+ }
531
+ });
532
+ }
533
+ async function readTextFile(ctx, options) {
534
+ const bytes = await readBinaryFile(ctx, {
535
+ path: options.path,
536
+ abortSignal: options.abortSignal
537
+ });
538
+ if (bytes === null) return null;
539
+ const text = Buffer.from(bytes).toString(normalizeEncoding(options.encoding));
540
+ if (options.startLine === void 0 && options.endLine === void 0) {
541
+ return text;
542
+ }
543
+ return sliceLines(text, options.startLine, options.endLine);
544
+ }
545
+ async function writeBinaryFile(ctx, options) {
546
+ const abs = resolveRemotePath(ctx, options.path);
547
+ const dir = path.posix.dirname(abs);
548
+ const command = `mkdir -p ${shellQuote(dir)} && base64 -d > ${shellQuote(abs)}`;
549
+ const base64 = Buffer.from(options.content).toString("base64");
550
+ const result = await ctx.transport.exec({
551
+ workspace: ctx.workspace,
552
+ command,
553
+ stdin: base64,
554
+ abortSignal: options.abortSignal
555
+ });
556
+ if (result.exitCode !== 0) {
557
+ throw new Error(`failed to write ${abs} (exit ${result.exitCode}): ${result.stderr.trim()}`);
558
+ }
559
+ }
560
+ async function writeFile(ctx, options) {
561
+ const bytes = await collectStream(options.content);
562
+ await writeBinaryFile(ctx, {
563
+ path: options.path,
564
+ content: bytes,
565
+ abortSignal: options.abortSignal
566
+ });
567
+ }
568
+ async function writeTextFile(ctx, options) {
569
+ const bytes = new Uint8Array(Buffer.from(options.content, normalizeEncoding(options.encoding)));
570
+ await writeBinaryFile(ctx, {
571
+ path: options.path,
572
+ content: bytes,
573
+ abortSignal: options.abortSignal
574
+ });
575
+ }
576
+ function sliceLines(text, startLine, endLine) {
577
+ const lines = text.split("\n");
578
+ const start = Math.max(1, startLine ?? 1) - 1;
579
+ const end = endLine === void 0 ? lines.length : Math.min(lines.length, endLine);
580
+ return lines.slice(start, end).join("\n");
581
+ }
582
+ function normalizeEncoding(encoding) {
583
+ if (encoding === void 0) return "utf8";
584
+ const normalized = encoding.toLowerCase().replace(/[-_]/g, "");
585
+ switch (normalized) {
586
+ case "utf8":
587
+ case "utf":
588
+ return "utf8";
589
+ case "utf16le":
590
+ case "ucs2":
591
+ return "utf16le";
592
+ case "latin1":
593
+ case "binary":
594
+ case "iso88591":
595
+ return "latin1";
596
+ case "ascii":
597
+ return "ascii";
598
+ case "base64":
599
+ return "base64";
600
+ case "hex":
601
+ return "hex";
602
+ default:
603
+ return "utf8";
604
+ }
605
+ }
606
+ async function collectStream(stream) {
607
+ const reader = stream.getReader();
608
+ const chunks = [];
609
+ let total = 0;
610
+ try {
611
+ for (; ; ) {
612
+ const { done, value } = await reader.read();
613
+ if (done) break;
614
+ if (value) {
615
+ chunks.push(value);
616
+ total += value.length;
617
+ }
618
+ }
619
+ } finally {
620
+ reader.releaseLock();
621
+ }
622
+ const out = new Uint8Array(total);
623
+ let offset = 0;
624
+ for (const chunk of chunks) {
625
+ out.set(chunk, offset);
626
+ offset += chunk.length;
627
+ }
628
+ return out;
629
+ }
630
+
631
+ // src/coder-workspace-session.ts
632
+ var CoderWorkspaceSession = class {
633
+ id;
634
+ defaultWorkingDirectory;
635
+ description;
636
+ #transport;
637
+ #workspace;
638
+ #ownsLifecycle;
639
+ #forwards = /* @__PURE__ */ new Map();
640
+ #ports;
641
+ #stopped = false;
642
+ constructor(config) {
643
+ this.#transport = config.transport;
644
+ this.#workspace = config.workspace;
645
+ this.#ownsLifecycle = config.ownsLifecycle;
646
+ this.#ports = [...config.ports];
647
+ this.id = config.id;
648
+ this.defaultWorkingDirectory = config.defaultWorkingDirectory;
649
+ this.description = `Coder workspace "${config.workspace}". Default working directory: ${config.defaultWorkingDirectory}. Exposed ports: ${this.#ports.length > 0 ? this.#ports.join(", ") : "none"}. Commands run inside the workspace via 'coder ssh'.`;
650
+ }
651
+ get ports() {
652
+ return this.#ports;
653
+ }
654
+ #execOptions(options) {
655
+ return {
656
+ workspace: this.#workspace,
657
+ command: options.command,
658
+ workingDirectory: options.workingDirectory ?? this.defaultWorkingDirectory,
659
+ env: options.env,
660
+ abortSignal: options.abortSignal
661
+ };
662
+ }
663
+ #fileIoContext() {
664
+ return {
665
+ transport: this.#transport,
666
+ workspace: this.#workspace,
667
+ defaultWorkingDirectory: this.defaultWorkingDirectory
668
+ };
669
+ }
670
+ // --- exec surface ---------------------------------------------------------
671
+ run = (options) => this.#transport.exec(this.#execOptions(options));
672
+ spawn = async (options) => this.#transport.spawn(this.#execOptions(options));
673
+ // --- file I/O surface -----------------------------------------------------
674
+ readFile = (options) => readFile(this.#fileIoContext(), options);
675
+ readBinaryFile = (options) => readBinaryFile(this.#fileIoContext(), options);
676
+ readTextFile = (options) => readTextFile(this.#fileIoContext(), options);
677
+ writeFile = (options) => writeFile(this.#fileIoContext(), options);
678
+ writeBinaryFile = (options) => writeBinaryFile(this.#fileIoContext(), options);
679
+ writeTextFile = (options) => writeTextFile(this.#fileIoContext(), options);
680
+ // --- network surface ------------------------------------------------------
681
+ getPortUrl = async (options) => {
682
+ if (this.#stopped) {
683
+ throw new Error("cannot resolve a port URL: the sandbox session is stopped");
684
+ }
685
+ let forward = this.#forwards.get(options.port);
686
+ if (forward !== void 0) {
687
+ const existing = await forward.catch(() => void 0);
688
+ if (existing === void 0 || existing.closed) {
689
+ this.#forwards.delete(options.port);
690
+ if (existing?.closed) void existing.close().catch(() => {
691
+ });
692
+ forward = void 0;
693
+ }
694
+ }
695
+ if (forward === void 0) {
696
+ forward = this.#transport.forwardPort({
697
+ workspace: this.#workspace,
698
+ remotePort: options.port
699
+ });
700
+ this.#forwards.set(options.port, forward);
701
+ forward.catch(() => this.#forwards.delete(options.port));
702
+ }
703
+ const resolved = await forward;
704
+ const scheme = localScheme(options.protocol ?? "ws");
705
+ return `${scheme}://${resolved.localHost}:${resolved.localPort}`;
706
+ };
707
+ setPorts = async (ports, _options) => {
708
+ const next = [...ports];
709
+ for (const [port, forward] of this.#forwards) {
710
+ if (!next.includes(port)) {
711
+ this.#forwards.delete(port);
712
+ void forward.then((f) => f.close()).catch(() => {
713
+ });
714
+ }
715
+ }
716
+ this.#ports = next;
717
+ };
718
+ // --- lifecycle ------------------------------------------------------------
719
+ stop = async () => {
720
+ if (this.#stopped) return;
721
+ this.#stopped = true;
722
+ await this.#closeForwards();
723
+ if (this.#ownsLifecycle) {
724
+ await this.#transport.stop(this.#workspace);
725
+ }
726
+ };
727
+ destroy = async () => {
728
+ this.#stopped = true;
729
+ await this.#closeForwards();
730
+ if (this.#ownsLifecycle) {
731
+ await this.#transport.destroy(this.#workspace);
732
+ }
733
+ };
734
+ /** Reduced view exposing only the base file/exec surface (no infra controls). */
735
+ restricted = () => ({
736
+ description: this.description,
737
+ readFile: this.readFile,
738
+ readBinaryFile: this.readBinaryFile,
739
+ readTextFile: this.readTextFile,
740
+ writeFile: this.writeFile,
741
+ writeBinaryFile: this.writeBinaryFile,
742
+ writeTextFile: this.writeTextFile,
743
+ spawn: this.spawn,
744
+ run: this.run
745
+ });
746
+ async #closeForwards() {
747
+ const forwards = [...this.#forwards.values()];
748
+ this.#forwards.clear();
749
+ await Promise.all(forwards.map((forward) => forward.then((f) => f.close()).catch(() => {
750
+ })));
751
+ }
752
+ };
753
+ function localScheme(protocol) {
754
+ switch (protocol) {
755
+ case "ws":
756
+ return "ws";
757
+ case "http":
758
+ case "https":
759
+ return "http";
760
+ }
761
+ }
762
+
763
+ // src/coder-workspace-provider.ts
764
+ var CODER_WORKSPACE_PROVIDER_ID = "coder-workspace";
765
+ var DEFAULT_BRIDGE_PORT = 4e3;
766
+ var DEFAULT_WORKING_DIRECTORY = "/home/coder";
767
+ var DEFAULT_READY_TIMEOUT_MS = 3e5;
768
+ var READY_POLL_INTERVAL_MS = 2e3;
769
+ var DEFAULT_NAME_PREFIX = "agent";
770
+ function createCoderWorkspace(settings) {
771
+ const transport = settings.transport ?? new CoderCliTransport();
772
+ const ports = settings.ports ?? [DEFAULT_BRIDGE_PORT];
773
+ const createMode = settings.create !== void 0;
774
+ const nameDerived = createMode && settings.workspace === void 0;
775
+ const readyTimeoutMs = settings.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
776
+ const resolveWorkspace = (sessionId) => {
777
+ if (typeof settings.workspace === "function") return settings.workspace(sessionId);
778
+ if (typeof settings.workspace === "string") return settings.workspace;
779
+ if (settings.create !== void 0) {
780
+ const name = deriveWorkspaceName(
781
+ settings.create.namePrefix ?? DEFAULT_NAME_PREFIX,
782
+ sessionId
783
+ );
784
+ const owner = settings.create.owner;
785
+ return owner !== void 0 && owner !== "" ? `${owner}/${name}` : name;
786
+ }
787
+ throw new Error("createCoderWorkspace: set `workspace`, `create`, or both.");
788
+ };
789
+ const resolveOwnership = (createdByProvider) => {
790
+ if (!createMode) return settings.ownsLifecycle ?? false;
791
+ const owns = settings.ownsLifecycle ?? true;
792
+ return nameDerived ? owns : owns && createdByProvider;
793
+ };
794
+ const buildSession = async (workspace, abortSignal) => {
795
+ const { createdByProvider } = await ensureWorkspace(
796
+ transport,
797
+ workspace,
798
+ settings,
799
+ readyTimeoutMs,
800
+ abortSignal
801
+ );
802
+ const defaultWorkingDirectory = settings.defaultWorkingDirectory ?? await resolveHomeDirectory(transport, workspace, abortSignal);
803
+ const session = new CoderWorkspaceSession({
804
+ transport,
805
+ workspace,
806
+ id: workspace,
807
+ defaultWorkingDirectory,
808
+ ports: [...ports],
809
+ ownsLifecycle: resolveOwnership(createdByProvider)
810
+ });
811
+ return { session, createdByProvider };
812
+ };
813
+ return {
814
+ specificationVersion: "harness-sandbox-v1",
815
+ providerId: CODER_WORKSPACE_PROVIDER_ID,
816
+ // `bridgePorts` intentionally left undefined: this provider binds one
817
+ // workspace per session rather than leasing ports from a shared sandbox.
818
+ createSession: async (options) => {
819
+ const workspace = resolveWorkspace(options?.sessionId);
820
+ const { session, createdByProvider } = await buildSession(workspace, options?.abortSignal);
821
+ const shouldFirstCreate = createMode ? createdByProvider : settings.ownsLifecycle ?? false;
822
+ if (shouldFirstCreate && options?.onFirstCreate) {
823
+ await options.onFirstCreate(session.restricted(), {
824
+ abortSignal: options.abortSignal
825
+ });
826
+ }
827
+ return session;
828
+ },
829
+ resumeSession: async (options) => {
830
+ const workspace = resolveWorkspace(options.sessionId);
831
+ const { session } = await buildSession(workspace, options.abortSignal);
832
+ return session;
833
+ }
834
+ };
835
+ }
836
+ async function ensureWorkspace(transport, workspace, settings, readyTimeoutMs, abortSignal) {
837
+ const create = settings.create;
838
+ if (create === void 0) {
839
+ if (settings.ensureStarted) {
840
+ await transport.start(workspace, { abortSignal });
841
+ }
842
+ return { createdByProvider: false };
843
+ }
844
+ const existing = await transport.status(workspace, { abortSignal });
845
+ let createdByProvider = false;
846
+ if (existing === null) {
847
+ if (create.validate ?? true) {
848
+ await validatePreset(transport, create, abortSignal);
849
+ }
850
+ await transport.create(toCreateOptions(workspace, create, abortSignal));
851
+ createdByProvider = true;
852
+ } else {
853
+ if (create.ifExists === "error") {
854
+ throw new Error(
855
+ `createCoderWorkspace: workspace "${workspace}" already exists (create.ifExists: 'error').`
856
+ );
857
+ }
858
+ if (isStopped(existing)) {
859
+ await transport.start(workspace, { abortSignal });
860
+ }
861
+ }
862
+ await waitForReady(transport, workspace, readyTimeoutMs, abortSignal);
863
+ return { createdByProvider };
864
+ }
865
+ function isStopped(status) {
866
+ return status.buildStatus === "stopped" || status.buildStatus === "stopping" || status.transition === "stop";
867
+ }
868
+ function toCreateOptions(workspace, create, abortSignal) {
869
+ return {
870
+ workspace,
871
+ template: create.template,
872
+ templateVersion: create.templateVersion,
873
+ preset: create.preset,
874
+ parameters: stringifyParams(create.parameters),
875
+ parameterFile: create.parameterFile,
876
+ useParameterDefaults: create.useParameterDefaults,
877
+ ephemeralParameters: stringifyParams(create.ephemeralParameters),
878
+ stopAfter: create.stopAfter,
879
+ automaticUpdates: create.automaticUpdates,
880
+ org: create.org,
881
+ abortSignal
882
+ };
883
+ }
884
+ function stringifyParams(params) {
885
+ if (params === void 0) return void 0;
886
+ const out = {};
887
+ for (const [key, value] of Object.entries(params)) {
888
+ out[key] = typeof value === "string" ? value : String(value);
889
+ }
890
+ return out;
891
+ }
892
+ async function validatePreset(transport, create, abortSignal) {
893
+ if (create.preset === void 0 || create.preset.toLowerCase() === "none") return;
894
+ let presets;
895
+ try {
896
+ presets = await transport.listPresets({
897
+ template: create.template,
898
+ templateVersion: create.templateVersion,
899
+ org: create.org,
900
+ abortSignal
901
+ });
902
+ } catch {
903
+ return;
904
+ }
905
+ if (presets.length === 0) return;
906
+ if (!presets.some((preset) => preset.name === create.preset)) {
907
+ const available = presets.map((preset) => `"${preset.name}"`).join(", ");
908
+ throw new Error(
909
+ `createCoderWorkspace: preset "${create.preset}" not found for template "${create.template}". Available presets: ${available || "(none)"}.`
910
+ );
911
+ }
912
+ }
913
+ async function waitForReady(transport, workspace, timeoutMs, abortSignal) {
914
+ const deadline = Date.now() + timeoutMs;
915
+ let last = "unknown";
916
+ for (; ; ) {
917
+ if (abortSignal?.aborted) throw abortSignal.reason ?? new Error("aborted");
918
+ const status = await transport.status(workspace, { abortSignal });
919
+ if (status !== null) {
920
+ last = `build=${status.buildStatus} agents=[` + status.agents.map((a) => `${a.name || "?"}:${a.status}/${a.lifecycleState}`).join(", ") + "]";
921
+ if (status.buildStatus === "failed") {
922
+ throw new Error(`createCoderWorkspace: workspace "${workspace}" build failed (${last}).`);
923
+ }
924
+ if (status.buildStatus === "canceled" || status.buildStatus === "deleted") {
925
+ throw new Error(
926
+ `createCoderWorkspace: workspace "${workspace}" is ${status.buildStatus} (${last}).`
927
+ );
928
+ }
929
+ const errored = status.agents.find(
930
+ (a) => a.lifecycleState === "start_error" || a.lifecycleState === "start_timeout"
931
+ );
932
+ if (errored) {
933
+ throw new Error(
934
+ `createCoderWorkspace: workspace "${workspace}" agent "${errored.name || "?"}" failed to start (lifecycle: ${errored.lifecycleState}).`
935
+ );
936
+ }
937
+ if (status.buildStatus === "running" && status.agents.some((a) => a.status === "connected" && a.lifecycleState === "ready")) {
938
+ return;
939
+ }
940
+ }
941
+ if (Date.now() >= deadline) {
942
+ throw new Error(
943
+ `createCoderWorkspace: timed out after ${timeoutMs}ms waiting for workspace "${workspace}" to become ready (last status: ${last}).`
944
+ );
945
+ }
946
+ await delay(READY_POLL_INTERVAL_MS, void 0, { signal: abortSignal });
947
+ }
948
+ }
949
+ function deriveWorkspaceName(prefix, sessionId) {
950
+ if (sessionId === void 0 || sessionId === "") {
951
+ throw new Error(
952
+ "createCoderWorkspace: create mode needs either an explicit `workspace` or a sessionId to derive a fresh per-session workspace name from."
953
+ );
954
+ }
955
+ const hash = createHash("sha1").update(sessionId).digest("hex").slice(0, 12);
956
+ const cleanPrefix = sanitizeNameSegment(prefix) || DEFAULT_NAME_PREFIX;
957
+ return `${cleanPrefix}-${hash}`.slice(0, 32);
958
+ }
959
+ function sanitizeNameSegment(value) {
960
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
961
+ }
962
+ async function resolveHomeDirectory(transport, workspace, abortSignal) {
963
+ try {
964
+ const result = await transport.exec({
965
+ workspace,
966
+ command: 'printf %s "$HOME"',
967
+ abortSignal
968
+ });
969
+ const home = result.stdout.trim();
970
+ if (result.exitCode === 0 && home.startsWith("/")) {
971
+ return home;
972
+ }
973
+ } catch {
974
+ }
975
+ return DEFAULT_WORKING_DIRECTORY;
976
+ }
977
+ export {
978
+ CODER_WORKSPACE_PROVIDER_ID,
979
+ CoderCliTransport,
980
+ CoderWorkspaceSession,
981
+ createCoderWorkspace
982
+ };