@nimbus-sh/sdk 0.1.4 → 0.2.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/sandbox.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @nimbus-sh/sdk/sandbox - programmatic Nimbus sandbox handle.
3
3
  */
4
+ import { buildPreviewHost, isPreviewHostSafeSid, readPreviewHostSuffix, } from '@nimbus-sh/worker/preview-host';
5
+ import { z } from 'zod/v4';
4
6
  export class NimbusRemoteError extends Error {
5
7
  status;
6
8
  code;
@@ -13,6 +15,150 @@ export class NimbusRemoteError extends Error {
13
15
  this.body = options.body;
14
16
  }
15
17
  }
18
+ const RemoteRpcSuccessSchema = z.object({
19
+ ok: z.literal(true),
20
+ result: z.unknown().optional(),
21
+ }).passthrough();
22
+ const RemoteRpcFailureSchema = z.object({
23
+ ok: z.boolean().optional(),
24
+ error: z.string().optional(),
25
+ message: z.string().optional(),
26
+ code: z.string().optional(),
27
+ }).passthrough();
28
+ const WireBytesSchema = z.object({
29
+ __nimbusWireType: z.literal('bytes'),
30
+ base64: z.string(),
31
+ }).passthrough();
32
+ const UndefinedResultSchema = z.undefined();
33
+ const UnknownResultSchema = z.unknown();
34
+ /** `_rpcWriteFile` answers with the byte count the VFS wrote. */
35
+ const WriteFileResultSchema = z.number();
36
+ const StringOrNullSchema = z.string().nullable();
37
+ const Uint8ArrayOrNullSchema = z.instanceof(Uint8Array).nullable();
38
+ const BooleanResultSchema = z.boolean();
39
+ const ReadyResultSchema = z.object({
40
+ ok: z.literal(true),
41
+ preinstalled: z.array(z.string()),
42
+ });
43
+ const ExecResultSchema = z.object({
44
+ command: z.string(),
45
+ exitCode: z.number(),
46
+ success: z.boolean(),
47
+ stdout: z.string(),
48
+ stderr: z.string(),
49
+ duration: z.number(),
50
+ timestamp: z.number(),
51
+ });
52
+ const ProcessSchema = z.object({
53
+ pid: z.number(),
54
+ command: z.string(),
55
+ argv: z.array(z.string()),
56
+ cwd: z.string(),
57
+ state: z.string(),
58
+ exitCode: z.number().nullable(),
59
+ startTime: z.number(),
60
+ endTime: z.number().nullable(),
61
+ longRunning: z.boolean(),
62
+ attachedTty: z.boolean().optional().default(false),
63
+ });
64
+ const PortSchema = z.object({
65
+ port: z.number(),
66
+ pid: z.number(),
67
+ registeredAt: z.number(),
68
+ });
69
+ const StartResultSchema = z.object({
70
+ command: z.string(),
71
+ pid: z.number(),
72
+ process: ProcessSchema,
73
+ ports: z.array(PortSchema),
74
+ startedAt: z.number(),
75
+ });
76
+ const FileStatSchema = z.object({
77
+ type: z.string(),
78
+ size: z.number(),
79
+ ctime: z.number().optional(),
80
+ mtime: z.number(),
81
+ mode: z.number(),
82
+ });
83
+ const DirectoryEntrySchema = z.object({
84
+ name: z.string(),
85
+ type: z.string(),
86
+ });
87
+ const RuntimeSummarySchema = z.object({
88
+ name: z.string(),
89
+ version: z.string(),
90
+ root: z.string(),
91
+ abi: z.string(),
92
+ bins: z.array(z.string()),
93
+ sizeBytes: z.number(),
94
+ license: z.string(),
95
+ });
96
+ const AvailableRuntimeSchema = z.object({
97
+ name: z.string(),
98
+ abi: z.string(),
99
+ defaultVersion: z.string(),
100
+ versions: z.array(z.object({
101
+ version: z.string(),
102
+ sizeBytes: z.number(),
103
+ license: z.string(),
104
+ })),
105
+ });
106
+ const RuntimeListSchema = z.object({
107
+ installed: z.array(RuntimeSummarySchema),
108
+ available: z.array(AvailableRuntimeSchema),
109
+ });
110
+ const ProcessControlResultSchema = z.object({
111
+ ok: z.boolean(),
112
+ pid: z.number(),
113
+ });
114
+ const ProcessLogChunkSchema = z.object({
115
+ seq: z.number(),
116
+ ts: z.number(),
117
+ stream: z.enum(['stdout', 'stderr']),
118
+ data: z.string(),
119
+ binary: z.boolean().optional(),
120
+ });
121
+ const ProcessExitInfoSchema = z.object({
122
+ code: z.number(),
123
+ at: z.number(),
124
+ reason: z.string().optional(),
125
+ });
126
+ const ProcessLogsResultSchema = z.object({
127
+ pid: z.number(),
128
+ chunks: z.array(ProcessLogChunkSchema),
129
+ text: z.string(),
130
+ cursor: z.number(),
131
+ truncated: z.boolean(),
132
+ exit: ProcessExitInfoSchema.nullable(),
133
+ });
134
+ const ExposedPortSchema = z.object({
135
+ port: z.number(),
136
+ listening: z.boolean(),
137
+ pid: z.number().nullable(),
138
+ registeredAt: z.number().nullable(),
139
+ });
140
+ const UnexposedPortSchema = z.object({
141
+ port: z.number(),
142
+ ok: z.boolean(),
143
+ });
144
+ const DestroyResultSchema = z.object({
145
+ ok: z.literal(true),
146
+ killed: z.number(),
147
+ destroyedAt: z.number(),
148
+ reason: z.string().nullable(),
149
+ });
150
+ const ToolPathInputSchema = z.object({
151
+ path: z.string().optional(),
152
+ }).passthrough();
153
+ const ToolWriteFileInputSchema = z.object({
154
+ path: z.string().optional(),
155
+ content: z.union([z.string(), z.instanceof(Uint8Array)]).optional(),
156
+ data: z.union([z.string(), z.instanceof(Uint8Array)]).optional(),
157
+ }).passthrough();
158
+ const ToolDeleteFileInputSchema = z.object({
159
+ path: z.string().optional(),
160
+ recursive: z.boolean().optional(),
161
+ }).passthrough();
16
162
  export class Nimbus {
17
163
  config;
18
164
  static fromEnv(env, config = {}, options = {}) {
@@ -24,6 +170,9 @@ export class Nimbus {
24
170
  return new Nimbus({ kind: 'binding', namespace: binding }, {
25
171
  ...config,
26
172
  endpoint: options.endpoint ?? config.endpoint,
173
+ // The binding is the deployment's own answer for whether port previews
174
+ // have a host suffix, so in-Worker callers never restate it in config.
175
+ previewHostSuffix: readPreviewHostSuffix(env) ?? config.previewHostSuffix,
27
176
  });
28
177
  }
29
178
  static connect(options) {
@@ -88,35 +237,49 @@ export class NimbusSandbox {
88
237
  stub() {
89
238
  if (this.target.kind === 'remote')
90
239
  return this.remoteStub();
91
- const namespace = this.target.namespace;
92
- const id = namespace.idFromName(this.doName);
93
- return namespace.get(id);
240
+ const id = this.target.namespace.idFromName(this.doName);
241
+ return this.target.namespace.get(id);
94
242
  }
95
243
  remoteStub() {
96
244
  return {
97
- _rpcReady: (options) => this.remoteRpc('ready', [options]),
98
- _rpcExec: (command, options) => this.remoteRpc('exec', [command, options]),
99
- _rpcStartProcess: (command, options) => this.remoteRpc('startProcess', [command, options]),
100
- _rpcRunCode: (code, options) => this.remoteRpc('runCode', [code, options]),
101
- _rpcReadFile: (path) => this.remoteRpc('readFile', [path]),
102
- _rpcReadFileBytes: (path) => this.remoteRpc('readFileBytes', [path]),
103
- _rpcWriteFile: (path, content) => this.remoteRpc('writeFile', [path, content]),
104
- _rpcReaddir: (path) => this.remoteRpc('readdir', [path]),
105
- _rpcExists: (path) => this.remoteRpc('exists', [path]),
106
- _rpcMkdir: (path) => this.remoteRpc('mkdir', [path]),
107
- _rpcDeleteFile: (path, options) => this.remoteRpc('deleteFile', [path, options]),
108
- _rpcInstallRuntime: (spec, options) => this.remoteRpc('installRuntime', [spec, options]),
109
- _rpcEnsureRuntimes: (specs, options) => this.remoteRpc('ensureRuntimes', [specs, options]),
110
- _rpcListRuntimes: () => this.remoteRpc('listRuntimes', []),
111
- _rpcListProcesses: () => this.remoteRpc('listProcesses', []),
112
- _rpcKillProcess: (pid) => this.remoteRpc('killProcess', [pid]),
113
- _rpcProcessLogs: (pid, options) => this.remoteRpc('processLogs', [pid, options]),
114
- _rpcListPorts: () => this.remoteRpc('listPorts', []),
115
- _rpcExposePort: (port) => this.remoteRpc('exposePort', [port]),
116
- _rpcUnexposePort: (port) => this.remoteRpc('unexposePort', [port]),
245
+ _rpcReady: (options) => this.remoteRpc('ready', [options], ReadyResultSchema),
246
+ _rpcExec: (command, options) => this.remoteRpc('exec', [command, options], ExecResultSchema),
247
+ _rpcStartProcess: (command, options) => this.remoteRpc('startProcess', [command, options], StartResultSchema),
248
+ _rpcRunCode: (code, options) => this.remoteRpc('runCode', [code, options], ExecResultSchema),
249
+ _rpcReadFile: (path) => this.remoteRpc('readFile', [path], StringOrNullSchema),
250
+ _rpcReadFileBytes: (path) => this.remoteRpc('readFileBytes', [path], Uint8ArrayOrNullSchema),
251
+ _rpcWriteFile: async (path, content) => {
252
+ // The byte count is the wire contract but not part of the public
253
+ // `files.write` surface, so it is validated and dropped. Declaring
254
+ // this `undefined` made every remote write throw after succeeding.
255
+ await this.remoteRpc('writeFile', [path, content], WriteFileResultSchema);
256
+ },
257
+ _rpcStat: (path) => this.remoteRpc('stat', [path], FileStatSchema.nullable()),
258
+ _rpcLstat: (path) => this.remoteRpc('lstat', [path], FileStatSchema.nullable()),
259
+ _rpcRename: (from, to) => this.remoteRpc('rename', [from, to], UndefinedResultSchema),
260
+ _rpcChmod: (path, mode) => this.remoteRpc('chmod', [path, mode], UndefinedResultSchema),
261
+ _rpcFsReadRange: (path, offset, length) => this.remoteRpc('readRange', [path, offset, length], Uint8ArrayOrNullSchema),
262
+ _rpcReaddir: (path) => this.remoteRpc('readdir', [path], z.array(DirectoryEntrySchema)),
263
+ _rpcExists: (path) => this.remoteRpc('exists', [path], BooleanResultSchema),
264
+ _rpcMkdir: (path) => this.remoteRpc('mkdir', [path], UndefinedResultSchema),
265
+ _rpcDeleteFile: (path, options) => this.remoteRpc('deleteFile', [path, options], UndefinedResultSchema),
266
+ _rpcInstallRuntime: (spec, options) => this.remoteRpc('installRuntime', [spec, options], UnknownResultSchema),
267
+ _rpcEnsureRuntimes: (specs, options) => this.remoteRpc('ensureRuntimes', [specs, options], UnknownResultSchema),
268
+ _rpcListRuntimes: () => this.remoteRpc('listRuntimes', [], RuntimeListSchema),
269
+ _rpcListProcesses: () => this.remoteRpc('listProcesses', [], z.array(ProcessSchema)),
270
+ _rpcKillProcess: (pid) => this.remoteRpc('killProcess', [pid], ProcessControlResultSchema),
271
+ _rpcWriteProcessInput: (pid, data) => this.remoteRpc('writeProcessInput', [pid, data], ProcessControlResultSchema),
272
+ _rpcEndProcessInput: (pid) => this.remoteRpc('endProcessInput', [pid], ProcessControlResultSchema),
273
+ _rpcResizeProcess: (pid, size) => this.remoteRpc('resizeProcess', [pid, size], ProcessControlResultSchema),
274
+ _rpcSignalProcess: (pid, signal) => this.remoteRpc('signalProcess', [pid, signal], ProcessControlResultSchema),
275
+ _rpcProcessLogs: (pid, options) => this.remoteRpc('processLogs', [pid, options], ProcessLogsResultSchema),
276
+ _rpcListPorts: () => this.remoteRpc('listPorts', [], z.array(PortSchema)),
277
+ _rpcExposePort: (port) => this.remoteRpc('exposePort', [port], ExposedPortSchema),
278
+ _rpcUnexposePort: (port) => this.remoteRpc('unexposePort', [port], UnexposedPortSchema),
279
+ _rpcDestroy: (options) => this.remoteRpc('destroy', [options], DestroyResultSchema),
117
280
  };
118
281
  }
119
- async remoteRpc(op, args) {
282
+ async remoteRpc(op, args, resultSchema) {
120
283
  if (this.target.kind !== 'remote') {
121
284
  throw new Error('Nimbus internal error: remoteRpc called on non-remote target');
122
285
  }
@@ -151,32 +314,40 @@ export class NimbusSandbox {
151
314
  });
152
315
  }
153
316
  }
154
- if (!response.ok || payload?.ok !== true) {
155
- const message = payload?.error ?? payload?.message ?? `Nimbus remote API request failed (${response.status})`;
317
+ const success = RemoteRpcSuccessSchema.safeParse(payload);
318
+ if (!response.ok || !success.success) {
319
+ const failure = RemoteRpcFailureSchema.safeParse(payload);
320
+ const message = failure.success
321
+ ? failure.data.error ?? failure.data.message ?? `Nimbus remote API request failed (${response.status})`
322
+ : `Nimbus remote API request failed (${response.status})`;
156
323
  throw new NimbusRemoteError(message, {
157
324
  status: response.status,
158
- code: payload?.code,
325
+ code: failure.success ? failure.data.code : undefined,
159
326
  body: payload,
160
327
  });
161
328
  }
162
- return decodeWire(payload.result);
329
+ return resultSchema.parse(decodeWire(success.data.result));
163
330
  }
164
331
  async ready() {
165
332
  if (!this.readyPromise) {
166
333
  const preinstall = this.profile.runtimes?.preinstall ?? [];
167
334
  for (const spec of preinstall)
168
335
  this.assertRuntimeAllowed(spec, 'preinstall');
169
- this.readyPromise = this.stub()._rpcReady({ preinstall }).then(() => undefined);
336
+ this.readyPromise = this.rpc(this.stub()._rpcReady({ preinstall })).then(() => undefined);
170
337
  }
171
338
  return this.readyPromise;
172
339
  }
173
340
  async exec(command, options = {}) {
174
341
  await this.ready();
175
- return this.stub()._rpcExec(command, this.execOptions(options));
342
+ return this.rpc(this.stub()._rpcExec(command, this.execOptions(options)));
176
343
  }
344
+ /**
345
+ * Start a command in the background. Returns as soon as the process has a
346
+ * pid — it does not wait for the command to finish.
347
+ */
177
348
  async startProcess(command, options = {}) {
178
349
  await this.ready();
179
- return this.stub()._rpcStartProcess(command, this.execOptions(options));
350
+ return this.rpc(this.stub()._rpcStartProcess(command, this.execOptions(options)));
180
351
  }
181
352
  async runCode(code, options = {}) {
182
353
  const language = options.language ?? 'javascript';
@@ -184,102 +355,164 @@ export class NimbusSandbox {
184
355
  this.assertRuntimeAllowed(language, options.install === 'ifMissing' ? 'onDemand' : 'use');
185
356
  }
186
357
  await this.ready();
187
- return this.stub()._rpcRunCode(code, {
358
+ return this.rpc(this.stub()._rpcRunCode(code, {
188
359
  ...this.execOptions(options),
189
360
  language,
190
361
  install: options.install ?? 'never',
191
- });
362
+ }));
363
+ }
364
+ async destroy(options = {}) {
365
+ this.readyPromise = null;
366
+ return this.rpc(this.stub()._rpcDestroy(options));
192
367
  }
193
368
  files = {
194
369
  read: async (path) => {
195
370
  await this.ready();
196
- return this.stub()._rpcReadFile(path);
371
+ return this.rpc(this.stub()._rpcReadFile(path));
197
372
  },
198
373
  readBytes: async (path) => {
199
374
  await this.ready();
200
- return this.stub()._rpcReadFileBytes(path);
375
+ return this.rpc(this.stub()._rpcReadFileBytes(path));
201
376
  },
202
377
  write: async (path, content) => {
203
378
  await this.ready();
204
- return this.stub()._rpcWriteFile(path, content);
379
+ return this.rpc(this.stub()._rpcWriteFile(path, content));
380
+ },
381
+ stat: async (path) => {
382
+ await this.ready();
383
+ return this.rpc(this.stub()._rpcStat(path));
384
+ },
385
+ /** stat without following a symlink leaf. */
386
+ lstat: async (path) => {
387
+ await this.ready();
388
+ return this.rpc(this.stub()._rpcLstat(path));
389
+ },
390
+ rename: async (from, to) => {
391
+ await this.ready();
392
+ return this.rpc(this.stub()._rpcRename(from, to));
393
+ },
394
+ chmod: async (path, mode) => {
395
+ await this.ready();
396
+ return this.rpc(this.stub()._rpcChmod(path, mode));
397
+ },
398
+ /** Read `length` bytes at `offset` without materializing the whole file. */
399
+ readRange: async (path, offset, length) => {
400
+ await this.ready();
401
+ return this.rpc(this.stub()._rpcFsReadRange(path, offset, length));
205
402
  },
206
403
  list: async (path = this.root) => {
207
404
  await this.ready();
208
- return this.stub()._rpcReaddir(path);
405
+ return this.rpc(this.stub()._rpcReaddir(path));
209
406
  },
210
407
  mkdir: async (path) => {
211
408
  await this.ready();
212
- return this.stub()._rpcMkdir(path);
409
+ return this.rpc(this.stub()._rpcMkdir(path));
213
410
  },
214
411
  exists: async (path) => {
215
412
  await this.ready();
216
- return this.stub()._rpcExists(path);
413
+ return this.rpc(this.stub()._rpcExists(path));
217
414
  },
218
415
  delete: async (path, options = {}) => {
219
416
  await this.ready();
220
- return this.stub()._rpcDeleteFile(path, options);
417
+ return this.rpc(this.stub()._rpcDeleteFile(path, options));
221
418
  },
222
419
  };
223
420
  runtimes = {
224
421
  available: async () => {
225
422
  await this.ready();
226
- return (await this.stub()._rpcListRuntimes()).available;
423
+ return (await this.rpc(this.stub()._rpcListRuntimes())).available;
227
424
  },
228
425
  installed: async () => {
229
426
  await this.ready();
230
- return (await this.stub()._rpcListRuntimes()).installed;
427
+ return (await this.rpc(this.stub()._rpcListRuntimes())).installed;
231
428
  },
232
429
  list: async () => {
233
430
  await this.ready();
234
- return this.stub()._rpcListRuntimes();
431
+ return this.rpc(this.stub()._rpcListRuntimes());
235
432
  },
236
433
  install: async (spec, options = {}) => {
237
434
  this.assertRuntimeAllowed(spec, 'onDemand');
238
435
  await this.ready();
239
- return this.stub()._rpcInstallRuntime(spec, options);
436
+ return this.rpc(this.stub()._rpcInstallRuntime(spec, options));
240
437
  },
241
438
  ensure: async (specs, options = {}) => {
242
439
  const list = Array.isArray(specs) ? specs : [specs];
243
440
  for (const spec of list)
244
441
  this.assertRuntimeAllowed(spec, 'onDemand');
245
442
  await this.ready();
246
- return this.stub()._rpcEnsureRuntimes(list, options);
443
+ return this.rpc(this.stub()._rpcEnsureRuntimes(list, options));
247
444
  },
248
445
  };
249
446
  processes = {
250
447
  list: async () => {
251
448
  await this.ready();
252
- return this.stub()._rpcListProcesses();
449
+ return this.rpc(this.stub()._rpcListProcesses());
253
450
  },
254
451
  kill: async (pid) => {
255
452
  await this.ready();
256
- return this.stub()._rpcKillProcess(pid);
453
+ return this.rpc(this.stub()._rpcKillProcess(pid));
454
+ },
455
+ write: async (pid, data) => {
456
+ await this.ready();
457
+ return this.rpc(this.stub()._rpcWriteProcessInput(pid, data));
458
+ },
459
+ endInput: async (pid) => {
460
+ await this.ready();
461
+ return this.rpc(this.stub()._rpcEndProcessInput(pid));
462
+ },
463
+ resize: async (pid, size) => {
464
+ await this.ready();
465
+ return this.rpc(this.stub()._rpcResizeProcess(pid, size));
466
+ },
467
+ signal: async (pid, signal) => {
468
+ await this.ready();
469
+ return this.rpc(this.stub()._rpcSignalProcess(pid, signal));
257
470
  },
258
471
  logs: async (pid, options = {}) => {
259
472
  await this.ready();
260
- return this.stub()._rpcProcessLogs(pid, options);
473
+ return this.rpc(this.stub()._rpcProcessLogs(pid, options));
474
+ },
475
+ attach: (pid, options = {}) => {
476
+ return new NimbusProcessAttachment(this, pid, options);
261
477
  },
262
478
  };
263
479
  ports = {
264
480
  list: async () => {
265
481
  await this.ready();
266
- return this.stub()._rpcListPorts();
482
+ return this.rpc(this.stub()._rpcListPorts());
267
483
  },
268
484
  expose: async (port) => {
269
485
  await this.ready();
270
- const result = await this.stub()._rpcExposePort(port);
486
+ const result = await this.rpc(this.stub()._rpcExposePort(port));
271
487
  return { ...result, url: this.portUrl(port) };
272
488
  },
273
489
  unexpose: async (port) => {
274
490
  await this.ready();
275
- return this.stub()._rpcUnexposePort(port);
491
+ return this.rpc(this.stub()._rpcUnexposePort(port));
276
492
  },
277
493
  url: (port) => this.portUrl(port),
278
494
  };
279
495
  tools(options = {}) {
280
496
  const namespace = options.namespace ?? this.profile.tools?.namespace ?? 'nimbus';
281
497
  const kind = options.kind ?? this.profile.tools?.kind ?? 'nimbus';
282
- const callPath = (input) => typeof input === 'string' ? input : String(input?.path ?? '');
498
+ const callPath = (input) => {
499
+ if (typeof input === 'string')
500
+ return input;
501
+ return ToolPathInputSchema.parse(input).path ?? '';
502
+ };
503
+ const writeFileInput = (input) => {
504
+ const parsed = ToolWriteFileInputSchema.parse(input);
505
+ return {
506
+ path: parsed.path ?? '',
507
+ content: parsed.content ?? parsed.data ?? '',
508
+ };
509
+ };
510
+ const deleteFileInput = (input) => {
511
+ if (typeof input === 'string')
512
+ return { path: input, recursive: false };
513
+ const parsed = ToolDeleteFileInputSchema.parse(input);
514
+ return { path: parsed.path ?? '', recursive: parsed.recursive === true };
515
+ };
283
516
  return {
284
517
  name: options.name ?? namespace,
285
518
  kind,
@@ -291,13 +524,23 @@ export class NimbusSandbox {
291
524
  exec: { execute: (command, opts) => this.exec(command, opts) },
292
525
  runCode: { execute: (code, opts) => this.runCode(code, opts) },
293
526
  readFile: { execute: (input) => this.files.read(callPath(input)) },
294
- writeFile: { execute: (input) => this.files.write(callPath(input), input.content ?? input.data ?? '') },
527
+ writeFile: { execute: (input) => {
528
+ const parsed = writeFileInput(input);
529
+ return this.files.write(parsed.path, parsed.content);
530
+ } },
295
531
  listFiles: { execute: (input = this.root) => this.files.list(callPath(input) || this.root) },
296
532
  readdir: { execute: (input = this.root) => this.files.list(callPath(input) || this.root) },
297
- deleteFile: { execute: (input) => this.files.delete(callPath(input), { recursive: !!input?.recursive }) },
533
+ deleteFile: { execute: (input) => {
534
+ const parsed = deleteFileInput(input);
535
+ return this.files.delete(parsed.path, { recursive: parsed.recursive });
536
+ } },
298
537
  exists: { execute: (input) => this.files.exists(callPath(input)) },
299
538
  startProcess: { execute: (command, opts) => this.startProcess(command, opts) },
300
539
  killProcess: { execute: (input) => this.processes.kill(typeof input === 'number' ? input : input.pid) },
540
+ writeProcessInput: { execute: (input) => this.processes.write(input.pid, input.data) },
541
+ endProcessInput: { execute: (input) => this.processes.endInput(typeof input === 'number' ? input : input.pid) },
542
+ resizeProcess: { execute: (input) => this.processes.resize(input.pid, { columns: input.columns, rows: input.rows }) },
543
+ signalProcess: { execute: (input) => this.processes.signal(input.pid, input.signal) },
301
544
  logs: { execute: (input) => this.processes.logs(typeof input === 'number' ? input : input.pid, typeof input === 'number' ? {} : input) },
302
545
  exposePort: { execute: (input) => this.ports.expose(typeof input === 'number' ? input : input.port) },
303
546
  unexposePort: { execute: (input) => this.ports.unexpose(typeof input === 'number' ? input : input.port) },
@@ -321,11 +564,16 @@ export class NimbusSandbox {
321
564
  'net_inbound',
322
565
  'process_spawn',
323
566
  'process_long',
567
+ 'process_attached_stdio',
568
+ 'terminal_resize',
569
+ 'ansi_output',
324
570
  ];
325
571
  if (hasRuntime('python'))
326
572
  caps.push('python');
573
+ if (hasRuntime('ruby'))
574
+ caps.push('ruby');
327
575
  if (hasRuntime('clang'))
328
- caps.push('native_binary');
576
+ caps.push('wasi', 'clang_wasi');
329
577
  return caps;
330
578
  }
331
579
  execOptions(options) {
@@ -348,7 +596,20 @@ export class NimbusSandbox {
348
596
  throw new Error(`Nimbus runtime '${name}' is not preinstalled and on-demand runtime installs are disabled by sandbox profile '${this.profileName}'`);
349
597
  }
350
598
  }
599
+ /**
600
+ * Browser-facing URL for an exposed port, or undefined when the deployment
601
+ * is not addressable (no `endpoint`, no configured preview base).
602
+ *
603
+ * The URL carries NO credential. On a deployment with auth enforced it is
604
+ * the destination, not the ticket: the session mints a single-use attach
605
+ * token for it at `GET /s/<id>/api/preview-url?port=<n>`, which is what the
606
+ * session shell opens and what an embedder should hand to a browser.
607
+ */
351
608
  portUrl(port) {
609
+ const hostSuffix = this.config.previewHostSuffix;
610
+ if (hostSuffix && !this.profile.preview?.pathStyle && isPreviewHostSafeSid(this.id)) {
611
+ return `https://${buildPreviewHost(this.id, port, hostSuffix)}/`;
612
+ }
352
613
  const explicit = this.profile.preview?.baseUrl;
353
614
  if (explicit) {
354
615
  const base = trimTrailingSlashes(explicit.replace('{sessionId}', encodeURIComponent(this.id)));
@@ -359,6 +620,81 @@ export class NimbusSandbox {
359
620
  return undefined;
360
621
  return `${endpoint}/s/${encodeURIComponent(this.id)}/port/${port}/`;
361
622
  }
623
+ async rpc(promise) {
624
+ const value = await promise;
625
+ disposeSdkRpcResult(value);
626
+ return value;
627
+ }
628
+ }
629
+ export class NimbusProcessAttachment {
630
+ sandbox;
631
+ pid;
632
+ options;
633
+ cursor = null;
634
+ constructor(sandbox, pid, options = {}) {
635
+ this.sandbox = sandbox;
636
+ this.pid = pid;
637
+ this.options = options;
638
+ }
639
+ async write(data) {
640
+ return this.sandbox.processes.write(this.pid, data);
641
+ }
642
+ async endInput() {
643
+ return this.sandbox.processes.endInput(this.pid);
644
+ }
645
+ async resize(size) {
646
+ return this.sandbox.processes.resize(this.pid, size);
647
+ }
648
+ async signal(signal) {
649
+ return this.sandbox.processes.signal(this.pid, signal);
650
+ }
651
+ async kill() {
652
+ return this.sandbox.processes.kill(this.pid);
653
+ }
654
+ async logs(options = {}) {
655
+ const result = await this.sandbox.processes.logs(this.pid, options);
656
+ this.cursor = result.cursor;
657
+ return result;
658
+ }
659
+ stream(options = {}) {
660
+ const attach = this;
661
+ const pollIntervalMs = boundedPollInterval(options.pollIntervalMs ?? this.options.pollIntervalMs);
662
+ const signal = options.signal ?? this.options.signal;
663
+ const initialLines = options.lines ?? this.options.lines;
664
+ const initialBytes = options.bytes ?? this.options.bytes;
665
+ return {
666
+ async *[Symbol.asyncIterator]() {
667
+ if (signal?.aborted)
668
+ return;
669
+ let cursor = attach.cursor;
670
+ if (cursor === null) {
671
+ const initial = await attach.logs({
672
+ ...(initialBytes !== undefined ? { bytes: initialBytes } : {}),
673
+ ...(initialBytes === undefined && initialLines !== undefined ? { lines: initialLines } : {}),
674
+ });
675
+ cursor = initial.cursor;
676
+ for (const chunk of initial.chunks)
677
+ yield chunk;
678
+ if (initial.exit || signal?.aborted)
679
+ return;
680
+ }
681
+ while (!signal?.aborted) {
682
+ await sleep(pollIntervalMs, signal);
683
+ if (signal?.aborted)
684
+ return;
685
+ const next = await attach.logs({ cursor });
686
+ cursor = next.cursor;
687
+ for (const chunk of next.chunks)
688
+ yield chunk;
689
+ if (next.exit)
690
+ return;
691
+ }
692
+ },
693
+ };
694
+ }
695
+ [Symbol.asyncIterator]() {
696
+ return this.stream()[Symbol.asyncIterator]();
697
+ }
362
698
  }
363
699
  function idComponent(value, field) {
364
700
  const text = String(value);
@@ -381,6 +717,29 @@ function isIdComponent(value) {
381
717
  }
382
718
  return true;
383
719
  }
720
+ function boundedPollInterval(value) {
721
+ if (!Number.isFinite(value))
722
+ return 100;
723
+ return Math.max(25, Math.min(5000, Math.floor(Number(value))));
724
+ }
725
+ function sleep(ms, signal) {
726
+ if (signal?.aborted)
727
+ return Promise.resolve();
728
+ return new Promise((resolve) => {
729
+ let done = false;
730
+ let timer;
731
+ const finish = () => {
732
+ if (done)
733
+ return;
734
+ done = true;
735
+ clearTimeout(timer);
736
+ signal?.removeEventListener('abort', finish);
737
+ resolve();
738
+ };
739
+ timer = setTimeout(finish, ms);
740
+ signal?.addEventListener('abort', finish, { once: true });
741
+ });
742
+ }
384
743
  function isNimbusTarget(value) {
385
744
  return !!value && typeof value === 'object' && 'kind' in value;
386
745
  }
@@ -403,6 +762,22 @@ function trimTrailingSlashes(value) {
403
762
  end--;
404
763
  return value.slice(0, end);
405
764
  }
765
+ function disposeSdkRpcResult(value) {
766
+ if ((typeof value !== 'object' && typeof value !== 'function') || value === null)
767
+ return;
768
+ const disposerKey = Symbol.dispose;
769
+ if (!disposerKey)
770
+ return;
771
+ const disposer = Reflect.get(value, disposerKey);
772
+ if (typeof disposer !== 'function')
773
+ return;
774
+ try {
775
+ Reflect.apply(disposer, value, []);
776
+ }
777
+ catch {
778
+ // Disposal only releases Worker RPC bookkeeping. Preserve SDK behavior.
779
+ }
780
+ }
406
781
  async function resolveHeaders(input) {
407
782
  if (!input)
408
783
  return undefined;
@@ -441,12 +816,9 @@ function encodeWire(value) {
441
816
  return value;
442
817
  }
443
818
  function decodeWire(value) {
444
- if (value
445
- && typeof value === 'object'
446
- && value.__nimbusWireType === 'bytes'
447
- && typeof value.base64 === 'string') {
448
- return base64ToBytes(value.base64);
449
- }
819
+ const bytes = WireBytesSchema.safeParse(value);
820
+ if (bytes.success)
821
+ return base64ToBytes(bytes.data.base64);
450
822
  if (Array.isArray(value))
451
823
  return value.map(decodeWire);
452
824
  if (value && typeof value === 'object') {