@promigence/sdk 0.1.0-beta.7 → 0.1.0-beta.9
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 +67 -6
- package/dist/cjs/client.js +38 -14
- package/dist/cjs/core.d.ts +17 -6
- package/dist/cjs/core.js +12 -4
- package/dist/cjs/credstore.d.ts +3 -2
- package/dist/cjs/credstore.js +17 -4
- package/dist/cjs/github.d.ts +1 -1
- package/dist/cjs/gitlab.d.ts +1 -1
- package/dist/cjs/models.gen.d.ts +227 -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 +41 -10
- package/dist/cjs/sandbox.js +10 -3
- package/dist/cjs/ssh.js +1 -0
- package/dist/client.d.ts +67 -6
- package/dist/client.js +39 -15
- package/dist/core.d.ts +17 -6
- package/dist/core.js +10 -3
- package/dist/credstore.d.ts +3 -2
- package/dist/credstore.js +17 -4
- package/dist/github.d.ts +1 -1
- package/dist/gitlab.d.ts +1 -1
- package/dist/models.gen.d.ts +227 -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 +41 -10
- package/dist/sandbox.js +10 -3
- 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";
|
|
@@ -118,7 +136,7 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
118
136
|
/**
|
|
119
137
|
* A container image instead of a snapshot — a tag (`python:3.12`, pinned to its digest by the
|
|
120
138
|
* API) or `registry/name@sha256:<64 hex>`: the snapshot of this image is built on first use
|
|
121
|
-
* (a new build is
|
|
139
|
+
* (a new build is billed per second while it runs, at its tier's rate, `client.pricing()`; pass
|
|
122
140
|
* `allowBuild: true` to allow it) and
|
|
123
141
|
* reused after (same digest + same inputs → the same snapshot). A name with no tag or digest is
|
|
124
142
|
* refused: it would pin whatever `:latest` is today. The fields below shape that snapshot and
|
|
@@ -138,17 +156,17 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
138
156
|
buildTimeoutMs?: number;
|
|
139
157
|
/**
|
|
140
158
|
* Let `Sandbox.create({ image })` build the image's snapshot when none exists yet. A new build
|
|
141
|
-
*
|
|
142
|
-
* is free. Without it, a create from an image is refused before anything is sent: whether the
|
|
159
|
+
* is billed per second while it runs, at most `build_price` (your plan's rates, `client.pricing()`);
|
|
160
|
+
* reusing a snapshot already built is free. Without it, a create from an image is refused before anything is sent: whether the
|
|
143
161
|
* image's snapshot exists is only known once a build is submitted, and a submitted build cannot
|
|
144
162
|
* be withdrawn. The other way to pay once and on purpose: `promigence snapshot create --image
|
|
145
163
|
* REF --alias NAME`, then `Sandbox.create("NAME")`.
|
|
146
164
|
*/
|
|
147
165
|
allowBuild?: boolean;
|
|
148
166
|
/**
|
|
149
|
-
* Called with the `POST /snapshots` answer before any wait: `build_price
|
|
150
|
-
* `joined` when nothing new is charged. Without it,
|
|
151
|
-
* process warning (stderr) before the build is waited for.
|
|
167
|
+
* Called with the `POST /snapshots` answer before any wait: `build_price` (the most the build can
|
|
168
|
+
* cost) and `per_second_usd`, and `deduped` or `joined` when nothing new is charged. Without it,
|
|
169
|
+
* a new build's price is reported as a process warning (stderr) before the build is waited for.
|
|
152
170
|
*/
|
|
153
171
|
onBuild?: (build: SnapshotBuild) => void | Promise<void>;
|
|
154
172
|
/**
|
|
@@ -156,6 +174,20 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
156
174
|
* nobody), then it rejects with `TimeoutError` ("aborted by the caller's signal").
|
|
157
175
|
*/
|
|
158
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;
|
|
159
191
|
}
|
|
160
192
|
/**
|
|
161
193
|
* Create-or-reuse the snapshot for an image digest and return its `snapshot_id`.
|
|
@@ -166,9 +198,8 @@ export interface SandboxCreateOpts extends SandboxOpts {
|
|
|
166
198
|
*/
|
|
167
199
|
export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "registrySecret" | "buildTimeoutMs">, hooks?: {
|
|
168
200
|
/**
|
|
169
|
-
* The `POST /snapshots` answer, before any wait: `build_price`
|
|
170
|
-
* or `joined` (nothing new charged). An implicit build costs money, so a caller
|
|
171
|
-
* that prints prices prints this one too.
|
|
201
|
+
* The `POST /snapshots` answer, before any wait: `build_price` (the most it can cost) and
|
|
202
|
+
* whether it was `deduped` or `joined` (nothing new charged). An implicit build costs money, so a caller that prints prices prints this one too.
|
|
172
203
|
*/
|
|
173
204
|
onBuild?: (build: SnapshotBuild) => void | Promise<void>;
|
|
174
205
|
/** Ends the wait for a build at once; the build itself, once submitted, is not withdrawn. */
|
|
@@ -343,7 +374,7 @@ export declare class Sandbox {
|
|
|
343
374
|
/**
|
|
344
375
|
* Fork of 1 from a snapshot (`snap_<id>` | `<alias>` | `<alias>@<commit7>`), or from the
|
|
345
376
|
* snapshot of an image: `Sandbox.create({ image: 'python:3.12', allowBuild: true })` builds it
|
|
346
|
-
* on first use (a new build at
|
|
377
|
+
* on first use (a new build billed per second at its tier's rate, bounded by `buildTimeoutMs`, default 15 min) and reuses it
|
|
347
378
|
* free after. An image needs `allowBuild: true` and a tag or digest; without either it is
|
|
348
379
|
* refused before any request.
|
|
349
380
|
*
|
package/dist/sandbox.js
CHANGED
|
@@ -65,12 +65,16 @@ function buildNotice(image) {
|
|
|
65
65
|
return (b) => {
|
|
66
66
|
if (buildIsFree(b))
|
|
67
67
|
return;
|
|
68
|
-
const price = typeof b.build_price === "number"
|
|
69
|
-
|
|
68
|
+
const price = typeof b.build_price === "number"
|
|
69
|
+
? typeof b.per_second_usd === "number"
|
|
70
|
+
? `billed per second while it builds ($${Number(b.per_second_usd.toPrecision(12))}/s), at most $${b.build_price.toFixed(2)}`
|
|
71
|
+
: `build_price $${b.build_price.toFixed(2)}, charged once it verifies`
|
|
72
|
+
: "billed at your plan's rates";
|
|
73
|
+
process.emitWarning(`building a snapshot of ${image}: ${price} (later creates from this image reuse it free)`, { code: "PROMIGENCE_BUILD" });
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
function buildNotAllowed(image) {
|
|
73
|
-
return new InvalidArgumentError(`Sandbox.create({ image: ${JSON.stringify(image)} }) builds that image's snapshot unless one already exists, and a new build is
|
|
77
|
+
return new InvalidArgumentError(`Sandbox.create({ image: ${JSON.stringify(image)} }) builds that image's snapshot unless one already exists, and a new build is billed per second while it runs (\`promigence pricing\` shows the rates; reusing a snapshot already built is free). Nothing was sent.`, `To allow the build: Sandbox.create({ image: ${JSON.stringify(image)}, allowBuild: true }). Or build it once, ` +
|
|
74
78
|
`\`promigence snapshot create --image ${hintArg(image)} --alias NAME\`, and create from its name: Sandbox.create("NAME").`);
|
|
75
79
|
}
|
|
76
80
|
export async function imageSnapshot(client, opts, hooks = {}) {
|
|
@@ -317,6 +321,9 @@ export class Sandbox {
|
|
|
317
321
|
...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
|
|
318
322
|
...(opts.onData ? { onData: opts.onData } : {}),
|
|
319
323
|
...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
|
|
324
|
+
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
325
|
+
reconnect: opts.reconnect ?? opts.cmd === undefined,
|
|
326
|
+
...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
|
|
320
327
|
onExit: (e) => { this.terminals.delete(pid); opts.onExit?.(e); },
|
|
321
328
|
});
|
|
322
329
|
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.9",
|
|
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",
|