@promigence/sdk 0.1.0-beta.6 → 0.1.0-beta.8
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 +3 -2
- package/dist/cjs/client.d.ts +100 -24
- package/dist/cjs/client.js +44 -31
- package/dist/cjs/core.d.ts +10 -1
- package/dist/cjs/core.js +14 -4
- package/dist/cjs/credstore.d.ts +3 -2
- package/dist/cjs/credstore.js +17 -4
- package/dist/cjs/models.gen.d.ts +218 -22
- package/dist/cjs/proxy.js +2 -1
- package/dist/cjs/pty.d.ts +65 -3
- package/dist/cjs/pty.js +270 -34
- package/dist/cjs/sandbox.d.ts +34 -4
- package/dist/cjs/sandbox.js +3 -2
- package/dist/cjs/ssh.js +1 -0
- package/dist/client.d.ts +100 -24
- package/dist/client.js +46 -33
- package/dist/core.d.ts +10 -1
- package/dist/core.js +12 -3
- package/dist/credstore.d.ts +3 -2
- package/dist/credstore.js +17 -4
- package/dist/models.gen.d.ts +218 -22
- package/dist/proxy.js +2 -1
- package/dist/pty.d.ts +65 -3
- package/dist/pty.js +269 -33
- package/dist/sandbox.d.ts +34 -4
- package/dist/sandbox.js +3 -2
- package/dist/ssh.js +1 -0
- package/package.json +1 -1
package/dist/pty.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { CALLER_HEADER } from "./caller.js";
|
|
3
3
|
import { requestVia } from "./proxy.js";
|
|
4
4
|
export const PTY_INPUT_CHUNK = 64 * 1024;
|
|
5
|
+
export const PTY_INPUT_KEEP = 256 * 1024;
|
|
6
|
+
export const PTY_RESUME_HEADER = "promigence-resume";
|
|
5
7
|
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
6
8
|
export const PTY_COMMAND_HEADER = "promigence-pty-command";
|
|
7
9
|
export const PTY_SECRETS_HEADER = "promigence-pty-secrets";
|
|
@@ -92,11 +94,17 @@ export class PtySession {
|
|
|
92
94
|
rows;
|
|
93
95
|
closed = false;
|
|
94
96
|
done;
|
|
97
|
+
pending;
|
|
98
|
+
resumeToken;
|
|
99
|
+
inputSent = 0;
|
|
100
|
+
inputBase = 0;
|
|
101
|
+
inputLog = Buffer.alloc(0);
|
|
95
102
|
constructor(socket, open, done) {
|
|
96
103
|
this.socket = socket;
|
|
97
104
|
this.pid = open.pid;
|
|
98
105
|
this.cols = open.cols;
|
|
99
106
|
this.rows = open.rows;
|
|
107
|
+
this.resumeToken = open.resume;
|
|
100
108
|
this.done = done;
|
|
101
109
|
void done.then(() => { this.#ended = true; });
|
|
102
110
|
}
|
|
@@ -104,10 +112,67 @@ export class PtySession {
|
|
|
104
112
|
get ended() {
|
|
105
113
|
return this.#ended;
|
|
106
114
|
}
|
|
107
|
-
|
|
115
|
+
holdInput() {
|
|
116
|
+
this.pending ??= [];
|
|
117
|
+
}
|
|
118
|
+
rebind(socket, open) {
|
|
119
|
+
this.socket = socket;
|
|
120
|
+
this.resumeToken = open.resume;
|
|
121
|
+
this.inputSent = 0;
|
|
122
|
+
this.inputBase = 0;
|
|
123
|
+
this.inputLog = Buffer.alloc(0);
|
|
124
|
+
this.flush(open);
|
|
125
|
+
}
|
|
126
|
+
resumed(socket, open) {
|
|
127
|
+
this.socket = socket;
|
|
128
|
+
this.resumeToken = open.resume ?? this.resumeToken;
|
|
129
|
+
const took = open.input ?? this.inputSent;
|
|
130
|
+
if (took < this.inputSent) {
|
|
131
|
+
const from = Math.max(took, this.inputBase) - this.inputBase;
|
|
132
|
+
const again = this.inputLog.subarray(from);
|
|
133
|
+
this.inputSent = Math.max(took, this.inputBase);
|
|
134
|
+
for (let at = 0; at < again.length; at += PTY_INPUT_CHUNK) {
|
|
135
|
+
this.writeNow(0x2, again.subarray(at, at + PTY_INPUT_CHUNK));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.flush(open);
|
|
139
|
+
}
|
|
140
|
+
flush(open) {
|
|
141
|
+
const queued = this.pending ?? [];
|
|
142
|
+
this.pending = undefined;
|
|
143
|
+
if (open.cols !== this.cols || open.rows !== this.rows) {
|
|
144
|
+
this.resize({ cols: this.cols, rows: this.rows });
|
|
145
|
+
}
|
|
146
|
+
for (const f of queued)
|
|
147
|
+
this.writeNow(f.opcode, f.payload);
|
|
148
|
+
}
|
|
149
|
+
writeNow(opcode, payload) {
|
|
108
150
|
if (this.closed || this.socket.destroyed)
|
|
109
151
|
return;
|
|
110
152
|
this.socket.write(encodeFrame(opcode, payload));
|
|
153
|
+
if (opcode === 0x2) {
|
|
154
|
+
this.inputSent += payload.length;
|
|
155
|
+
this.inputLog = Buffer.concat([this.inputLog, payload]);
|
|
156
|
+
if (this.inputLog.length > PTY_INPUT_KEEP) {
|
|
157
|
+
const drop = this.inputLog.length - PTY_INPUT_KEEP;
|
|
158
|
+
this.inputLog = this.inputLog.subarray(drop);
|
|
159
|
+
this.inputBase += drop;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
get hungUp() {
|
|
164
|
+
return this.closed;
|
|
165
|
+
}
|
|
166
|
+
send(opcode, payload) {
|
|
167
|
+
if (this.closed)
|
|
168
|
+
return;
|
|
169
|
+
if (this.pending) {
|
|
170
|
+
if (this.pending.reduce((n, f) => n + f.payload.length, 0) + payload.length <= 1 << 20) {
|
|
171
|
+
this.pending.push({ opcode, payload: Buffer.from(payload) });
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.writeNow(opcode, payload);
|
|
111
176
|
}
|
|
112
177
|
write(data) {
|
|
113
178
|
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
@@ -134,16 +199,49 @@ export class PtySession {
|
|
|
134
199
|
disconnect() {
|
|
135
200
|
if (this.closed)
|
|
136
201
|
return;
|
|
202
|
+
this.pending = undefined;
|
|
137
203
|
this.send(0x8, Buffer.from([0x03, 0xe8]));
|
|
138
204
|
this.closed = true;
|
|
139
205
|
this.socket.end();
|
|
140
206
|
}
|
|
141
207
|
}
|
|
142
|
-
export
|
|
143
|
-
|
|
208
|
+
export const PTY_RECONNECT_CODE = 1012;
|
|
209
|
+
export const PTY_RECONNECT_FOR_MS = 30_000;
|
|
210
|
+
export const PTY_RECONNECT_FIRST_WAIT_MS = 250;
|
|
211
|
+
export const PTY_RECONNECT_MAX_WAIT_MS = 4_000;
|
|
212
|
+
export const PTY_RESTART_EXIT = {
|
|
213
|
+
exitCode: null,
|
|
214
|
+
signal: null,
|
|
215
|
+
error: {
|
|
216
|
+
code: "service_restart",
|
|
217
|
+
message: "the service restarted and the terminal could not reconnect within 30 s; open it again",
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
export const PTY_RESTART_CLOSED_EXIT = {
|
|
221
|
+
exitCode: null,
|
|
222
|
+
signal: null,
|
|
223
|
+
error: { code: "service_restart", message: "the service restarted and closed the terminal; open it again" },
|
|
224
|
+
};
|
|
225
|
+
const CLOSED_EXIT = {
|
|
226
|
+
exitCode: null,
|
|
227
|
+
signal: null,
|
|
228
|
+
error: { code: "closed", message: "the terminal connection closed without an exit record" },
|
|
229
|
+
};
|
|
230
|
+
class LegRefused {
|
|
231
|
+
status;
|
|
232
|
+
error;
|
|
233
|
+
restart;
|
|
234
|
+
constructor(status, error, restart = false) {
|
|
235
|
+
this.status = status;
|
|
236
|
+
this.error = error;
|
|
237
|
+
this.restart = restart;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function openLeg(opts, url, resume) {
|
|
144
241
|
const key = randomBytes(16).toString("base64");
|
|
145
242
|
const req = requestVia(url, {
|
|
146
243
|
method: "GET",
|
|
244
|
+
agent: false,
|
|
147
245
|
headers: {
|
|
148
246
|
connection: "Upgrade",
|
|
149
247
|
upgrade: "websocket",
|
|
@@ -152,58 +250,51 @@ export function connectPty(opts) {
|
|
|
152
250
|
authorization: `Bearer ${opts.apiKey}`,
|
|
153
251
|
"user-agent": opts.userAgent,
|
|
154
252
|
...(opts.caller ? { [CALLER_HEADER]: opts.caller } : {}),
|
|
155
|
-
...(
|
|
253
|
+
...(resume ? { [PTY_RESUME_HEADER]: resume } : {}),
|
|
254
|
+
...(opts.command && !resume
|
|
156
255
|
? { [PTY_COMMAND_HEADER]: Buffer.from(opts.command, "utf8").toString("base64") }
|
|
157
256
|
: {}),
|
|
158
|
-
...(opts.secrets?.length
|
|
257
|
+
...(opts.secrets?.length && !resume
|
|
159
258
|
? {
|
|
160
259
|
[PTY_SECRETS_HEADER]: Buffer.from(JSON.stringify(opts.secrets), "utf8").toString("base64"),
|
|
161
260
|
}
|
|
162
261
|
: {}),
|
|
163
262
|
},
|
|
164
263
|
});
|
|
165
|
-
|
|
264
|
+
let resolveEnded;
|
|
265
|
+
const ended = new Promise((r) => { resolveEnded = r; });
|
|
266
|
+
const opened = new Promise((resolve, reject) => {
|
|
166
267
|
let settled = false;
|
|
167
|
-
const fail = (
|
|
268
|
+
const fail = (status, body, retryAfter, restart = false) => {
|
|
269
|
+
if (settled)
|
|
270
|
+
return;
|
|
168
271
|
settled = true;
|
|
169
|
-
reject(
|
|
170
|
-
}
|
|
272
|
+
reject(new LegRefused(status, opts.refuse(status, body, retryAfter), restart));
|
|
273
|
+
};
|
|
171
274
|
const timer = setTimeout(() => {
|
|
172
275
|
req.destroy();
|
|
173
|
-
fail(
|
|
276
|
+
fail(504, JSON.stringify({ error: { code: "timeout", message: `terminal handshake to ${url.host} took over ${opts.timeoutMs ?? 30_000} ms`, fix: "Retry; if it repeats, check the sandbox is ready (`promigence run report <run_id>`)." } }));
|
|
174
277
|
}, opts.timeoutMs ?? 30_000);
|
|
175
|
-
req.on("error", (e) => { clearTimeout(timer); fail(
|
|
278
|
+
req.on("error", (e) => { clearTimeout(timer); fail(0, JSON.stringify({ error: { code: "unreachable", message: `cannot reach ${url.host}: ${e.code ?? e.message}` } })); });
|
|
176
279
|
req.on("response", (res) => {
|
|
177
280
|
clearTimeout(timer);
|
|
178
281
|
const parts = [];
|
|
179
282
|
res.on("data", (d) => parts.push(d));
|
|
180
283
|
const retryAfter = res.headers["retry-after"];
|
|
181
|
-
res.on("end", () => fail(
|
|
284
|
+
res.on("end", () => fail(res.statusCode ?? 0, Buffer.concat(parts).toString("utf8"), typeof retryAfter === "string" ? retryAfter : undefined));
|
|
182
285
|
});
|
|
183
286
|
req.on("upgrade", (res, socket, head) => {
|
|
184
287
|
clearTimeout(timer);
|
|
185
288
|
if (res.headers["sec-websocket-accept"] !== wsAccept(key)) {
|
|
186
289
|
socket.destroy();
|
|
187
|
-
fail(
|
|
290
|
+
fail(502, JSON.stringify({ error: { code: "internal", message: "the terminal server answered the WebSocket handshake with the wrong Sec-WebSocket-Accept" } }));
|
|
188
291
|
return;
|
|
189
292
|
}
|
|
190
293
|
socket.setNoDelay(true);
|
|
191
294
|
const reader = new FrameReader();
|
|
192
295
|
let exit;
|
|
193
|
-
let
|
|
194
|
-
let
|
|
195
|
-
const done = new Promise((r) => { resolveDone = r; });
|
|
196
|
-
let finished = false;
|
|
197
|
-
const finish = () => {
|
|
198
|
-
if (finished)
|
|
199
|
-
return;
|
|
200
|
-
finished = true;
|
|
201
|
-
const e = exit ?? { exitCode: null, signal: null, error: { code: "closed", message: "the terminal connection closed without an exit record" } };
|
|
202
|
-
opts.onExit?.(e);
|
|
203
|
-
resolveDone(e);
|
|
204
|
-
if (!settled)
|
|
205
|
-
fail(opts.refuse(502, JSON.stringify({ error: { code: e.error?.code ?? "internal", message: e.error?.message ?? "the terminal ended before it opened" } })));
|
|
206
|
-
};
|
|
296
|
+
let closeCode;
|
|
297
|
+
let wasOpen = false;
|
|
207
298
|
const onFrames = (chunk) => {
|
|
208
299
|
for (const f of reader.push(chunk)) {
|
|
209
300
|
if (f.opcode === 0x2) {
|
|
@@ -217,14 +308,18 @@ export function connectPty(opts) {
|
|
|
217
308
|
catch {
|
|
218
309
|
continue;
|
|
219
310
|
}
|
|
220
|
-
if (v["event"] === "open" && !
|
|
221
|
-
|
|
311
|
+
if (v["event"] === "open" && !wasOpen) {
|
|
312
|
+
wasOpen = true;
|
|
313
|
+
settled = true;
|
|
314
|
+
resolve({
|
|
315
|
+
socket,
|
|
222
316
|
pid: typeof v["req_id"] === "number" ? v["req_id"] : 0,
|
|
223
317
|
cols: Number(v["cols"] ?? 80),
|
|
224
318
|
rows: Number(v["rows"] ?? 24),
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
319
|
+
...(typeof v["resume"] === "string" ? { resume: v["resume"] } : {}),
|
|
320
|
+
...(v["reattached"] === true ? { reattached: true } : {}),
|
|
321
|
+
...(typeof v["input"] === "number" ? { input: v["input"] } : {}),
|
|
322
|
+
});
|
|
228
323
|
}
|
|
229
324
|
else if (v["event"] === "exit") {
|
|
230
325
|
exit = {
|
|
@@ -244,17 +339,158 @@ export function connectPty(opts) {
|
|
|
244
339
|
socket.write(encodeFrame(0xa, f.payload));
|
|
245
340
|
}
|
|
246
341
|
else if (f.opcode === 0x8) {
|
|
342
|
+
if (closeCode === undefined)
|
|
343
|
+
closeCode = f.payload.length >= 2 ? f.payload.readUInt16BE(0) : 1005;
|
|
247
344
|
if (!socket.destroyed)
|
|
248
345
|
socket.end(encodeFrame(0x8, f.payload.subarray(0, 2)));
|
|
249
346
|
}
|
|
250
347
|
}
|
|
251
348
|
};
|
|
252
349
|
socket.on("data", onFrames);
|
|
253
|
-
socket.on("close",
|
|
350
|
+
socket.on("close", () => {
|
|
351
|
+
if (!wasOpen) {
|
|
352
|
+
const restart = closeCode === PTY_RECONNECT_CODE && exit === undefined;
|
|
353
|
+
const e = restart ? PTY_RESTART_CLOSED_EXIT : (exit ?? CLOSED_EXIT);
|
|
354
|
+
fail(restart ? 503 : 502, JSON.stringify({ error: { code: e.error?.code ?? "internal", message: e.error?.message ?? "the terminal ended before it opened" } }), undefined, restart);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
resolveEnded({ code: closeCode, exit });
|
|
358
|
+
});
|
|
254
359
|
socket.on("error", () => { });
|
|
255
360
|
if (head.length)
|
|
256
361
|
onFrames(head);
|
|
257
362
|
});
|
|
258
363
|
req.end();
|
|
259
364
|
});
|
|
365
|
+
return { opened, ended };
|
|
366
|
+
}
|
|
367
|
+
function sizedUrl(url, size) {
|
|
368
|
+
const u = new URL(url);
|
|
369
|
+
u.searchParams.set("cols", String(Math.floor(size.cols)));
|
|
370
|
+
u.searchParams.set("rows", String(Math.floor(size.rows)));
|
|
371
|
+
return u;
|
|
372
|
+
}
|
|
373
|
+
function resumeUrl(url, size, received) {
|
|
374
|
+
const u = sizedUrl(url, size);
|
|
375
|
+
u.searchParams.set("received", String(received));
|
|
376
|
+
return u;
|
|
377
|
+
}
|
|
378
|
+
function worthRetrying(status) {
|
|
379
|
+
return status === 0 || status === 429 || status >= 500;
|
|
380
|
+
}
|
|
381
|
+
export function connectPty(opts) {
|
|
382
|
+
const reconnect = opts.reconnect ?? opts.command === undefined;
|
|
383
|
+
const resumeOn = opts.resume ?? true;
|
|
384
|
+
let received = 0;
|
|
385
|
+
const legOpts = {
|
|
386
|
+
...opts,
|
|
387
|
+
onData: (d) => {
|
|
388
|
+
received += d.length;
|
|
389
|
+
opts.onData?.(d);
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
let resolveDone;
|
|
393
|
+
const done = new Promise((r) => { resolveDone = r; });
|
|
394
|
+
let finished = false;
|
|
395
|
+
const finish = (e) => {
|
|
396
|
+
if (finished)
|
|
397
|
+
return;
|
|
398
|
+
finished = true;
|
|
399
|
+
opts.onExit?.(e);
|
|
400
|
+
resolveDone(e);
|
|
401
|
+
};
|
|
402
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
403
|
+
const follow = (session, leg) => {
|
|
404
|
+
void leg.ended.then(async ({ code, exit }) => {
|
|
405
|
+
if (exit !== undefined || session.hungUp) {
|
|
406
|
+
finish(exit ?? CLOSED_EXIT);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const restart = code === PTY_RECONNECT_CODE;
|
|
410
|
+
const dropped = code === undefined || code === 1006;
|
|
411
|
+
let resumable = resumeOn && session.resumeToken !== undefined && (restart || dropped);
|
|
412
|
+
if (!restart && !resumable) {
|
|
413
|
+
finish(CLOSED_EXIT);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!resumable && !reconnect) {
|
|
417
|
+
finish(PTY_RESTART_CLOSED_EXIT);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
session.holdInput();
|
|
421
|
+
const t0 = Date.now();
|
|
422
|
+
for (let attempt = 0;; attempt++) {
|
|
423
|
+
const wait = Math.min(PTY_RECONNECT_FIRST_WAIT_MS * 2 ** attempt, PTY_RECONNECT_MAX_WAIT_MS) * (0.5 + Math.random() / 2);
|
|
424
|
+
if (Date.now() - t0 + wait > (opts.reconnectForMs ?? PTY_RECONNECT_FOR_MS))
|
|
425
|
+
break;
|
|
426
|
+
await sleep(wait);
|
|
427
|
+
if (session.hungUp) {
|
|
428
|
+
finish(CLOSED_EXIT);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const next = resumable
|
|
432
|
+
? openLeg(legOpts, resumeUrl(opts.url, session, received), session.resumeToken)
|
|
433
|
+
: openLeg(legOpts, sizedUrl(opts.url, session));
|
|
434
|
+
try {
|
|
435
|
+
const o = await next.opened;
|
|
436
|
+
if (session.hungUp) {
|
|
437
|
+
o.socket.destroy();
|
|
438
|
+
finish(CLOSED_EXIT);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (resumable) {
|
|
442
|
+
session.resumed(o.socket, o);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
received = 0;
|
|
446
|
+
session.rebind(o.socket, o);
|
|
447
|
+
}
|
|
448
|
+
follow(session, next);
|
|
449
|
+
opts.onReconnected?.({ resumed: resumable });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
const refused = e instanceof LegRefused ? e : undefined;
|
|
454
|
+
if (resumable && refused?.status === 404) {
|
|
455
|
+
resumable = false;
|
|
456
|
+
if (!restart) {
|
|
457
|
+
finish(CLOSED_EXIT);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (!reconnect) {
|
|
461
|
+
finish(PTY_RESTART_CLOSED_EXIT);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (refused && !worthRetrying(refused.status)) {
|
|
467
|
+
const err = refused.error;
|
|
468
|
+
finish({ exitCode: null, signal: null, error: { code: typeof err.code === "string" ? err.code : "internal", message: err.message } });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
finish(restart ? PTY_RESTART_EXIT : CLOSED_EXIT);
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
return (async () => {
|
|
477
|
+
const t0 = Date.now();
|
|
478
|
+
for (let attempt = 0;; attempt++) {
|
|
479
|
+
const first = openLeg(legOpts, new URL(opts.url));
|
|
480
|
+
try {
|
|
481
|
+
const o = await first.opened;
|
|
482
|
+
const session = new PtySession(o.socket, o, done);
|
|
483
|
+
follow(session, first);
|
|
484
|
+
return session;
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
const wait = Math.min(PTY_RECONNECT_FIRST_WAIT_MS * 2 ** attempt, PTY_RECONNECT_MAX_WAIT_MS) * (0.5 + Math.random() / 2);
|
|
488
|
+
if (e instanceof LegRefused && e.restart && reconnect && Date.now() - t0 + wait <= (opts.reconnectForMs ?? PTY_RECONNECT_FOR_MS)) {
|
|
489
|
+
await sleep(wait);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
throw e instanceof LegRefused ? e.error : e;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
})();
|
|
260
496
|
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -88,6 +88,24 @@ export interface PtyCreateOpts extends Partial<PtySize> {
|
|
|
88
88
|
cmd?: string;
|
|
89
89
|
/** Handshake bound (default 30 s). */
|
|
90
90
|
timeoutMs?: number;
|
|
91
|
+
/**
|
|
92
|
+
* When the connection drops, or the service is updated, the same terminal is picked up again
|
|
93
|
+
* over a new connection, where it left off: the same shell or command, still running.
|
|
94
|
+
* Default: on.
|
|
95
|
+
*/
|
|
96
|
+
resume?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* When the service is updated and the terminal cannot be picked up again, the session goes on
|
|
99
|
+
* over a new connection in a NEW shell (what ran in the old one has ended), with its callbacks
|
|
100
|
+
* and its size, for up to 30 s of trying; past that it ends with `error.code`
|
|
101
|
+
* `"service_restart"`. Default: on for the login shell, off with `cmd` (reconnecting would start
|
|
102
|
+
* the command again).
|
|
103
|
+
*/
|
|
104
|
+
reconnect?: boolean;
|
|
105
|
+
/** Called after each reconnect: `resumed` says whether it is the same terminal or a new shell. */
|
|
106
|
+
onReconnected?: (how: {
|
|
107
|
+
resumed: boolean;
|
|
108
|
+
}) => void;
|
|
91
109
|
}
|
|
92
110
|
/** The default terminal command. */
|
|
93
111
|
export declare const DEFAULT_PTY_CMD = "TERM=${TERM:-xterm-256color} exec /bin/sh -l";
|
|
@@ -134,7 +152,6 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
134
152
|
alias?: string;
|
|
135
153
|
/** Name of a stored secret holding the registry credentials. */
|
|
136
154
|
registrySecret?: string;
|
|
137
|
-
cpuClass?: string;
|
|
138
155
|
/** Bound on the first build of an image (default 15 min). Expiry cancels nothing. */
|
|
139
156
|
buildTimeoutMs?: number;
|
|
140
157
|
/**
|
|
@@ -157,6 +174,20 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
157
174
|
* nobody), then it rejects with `TimeoutError` ("aborted by the caller's signal").
|
|
158
175
|
*/
|
|
159
176
|
signal?: AbortSignal;
|
|
177
|
+
/**
|
|
178
|
+
* Stored secrets the sandbox receives, as environment variables: a name binds the secret to a
|
|
179
|
+
* variable of the same name, `{ name, as }` to a different one. Every command run against the
|
|
180
|
+
* sandbox gets them. As `Sandbox.fork` takes them.
|
|
181
|
+
*/
|
|
182
|
+
secrets?: SecretBinding[];
|
|
183
|
+
/**
|
|
184
|
+
* The only destinations the sandbox may reach: `["github.com", "*.npmjs.org"]`. Everything
|
|
185
|
+
* else is refused, and the refusal names the host. It wins over `allowInternetAccess`. As
|
|
186
|
+
* `Sandbox.fork` takes it.
|
|
187
|
+
*/
|
|
188
|
+
allowHosts?: string[];
|
|
189
|
+
/** What to favour, `start` or `compute`. As `Sandbox.fork` takes it. */
|
|
190
|
+
optimizeFor?: OptimizeFor;
|
|
160
191
|
}
|
|
161
192
|
/**
|
|
162
193
|
* Create-or-reuse the snapshot for an image digest and return its `snapshot_id`.
|
|
@@ -165,7 +196,7 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
165
196
|
* inputs) was built before, or 202 with a `build_id`. The **body**, not the status, decides: an
|
|
166
197
|
* idempotency replay answers 202 with a document that may already be `verified`.
|
|
167
198
|
*/
|
|
168
|
-
export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "registrySecret" | "
|
|
199
|
+
export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "registrySecret" | "buildTimeoutMs">, hooks?: {
|
|
169
200
|
/**
|
|
170
201
|
* The `POST /snapshots` answer, before any wait: `build_price` and whether it was `deduped`
|
|
171
202
|
* or `joined` (nothing new charged). An implicit build costs money, so a caller
|
|
@@ -422,8 +453,7 @@ export declare class Sandbox {
|
|
|
422
453
|
ok: boolean;
|
|
423
454
|
ms: number;
|
|
424
455
|
checked: string | string[];
|
|
425
|
-
first_bad_path
|
|
426
|
-
probe: Record<string, unknown>;
|
|
456
|
+
first_bad_path?: string | null;
|
|
427
457
|
}>;
|
|
428
458
|
isRunning(): Promise<boolean>;
|
|
429
459
|
/**
|
package/dist/sandbox.js
CHANGED
|
@@ -87,8 +87,6 @@ export async function imageSnapshot(client, opts, hooks = {}) {
|
|
|
87
87
|
create.alias = opts.alias;
|
|
88
88
|
if (opts.registrySecret !== undefined)
|
|
89
89
|
create.registrySecret = opts.registrySecret;
|
|
90
|
-
if (opts.cpuClass !== undefined)
|
|
91
|
-
create.cpuClass = opts.cpuClass;
|
|
92
90
|
let build = (await client.snapshots.create(create)) ?? {};
|
|
93
91
|
if (hooks.onBuild)
|
|
94
92
|
await hooks.onBuild(build);
|
|
@@ -319,6 +317,9 @@ export class Sandbox {
|
|
|
319
317
|
...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
|
|
320
318
|
...(opts.onData ? { onData: opts.onData } : {}),
|
|
321
319
|
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
320
|
+
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
321
|
+
reconnect: opts.reconnect ?? opts.cmd === undefined,
|
|
322
|
+
...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
|
|
322
323
|
onExit: (e) => { this.terminals.delete(pid); opts.onExit?.(e); },
|
|
323
324
|
});
|
|
324
325
|
pid = session.pid;
|
package/dist/ssh.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promigence/sdk",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.8",
|
|
4
4
|
"description": "The fastest sandboxes for AI agents and code. The Promigence TypeScript SDK: snapshot an environment once, fork it as often as you need, run commands in every copy. A simple Sandbox class and the full PromigenceClient, with zero runtime dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.promigence.ai",
|