@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/README.md +45 -3
- package/dist/flue.d.ts +93 -0
- package/dist/flue.d.ts.map +1 -0
- package/dist/flue.js +112 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/sandbox.d.ts +258 -11
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +433 -61
- package/dist/worker.d.ts +2 -2
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/package.json +11 -3
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
|
|
92
|
-
|
|
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) =>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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:
|
|
325
|
+
code: failure.success ? failure.data.code : undefined,
|
|
159
326
|
body: payload,
|
|
160
327
|
});
|
|
161
328
|
}
|
|
162
|
-
return decodeWire(
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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('
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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') {
|