@promigence/sdk 0.1.0-beta.15 → 0.1.0-beta.17

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.d.ts CHANGED
@@ -11,9 +11,11 @@
11
11
  */
12
12
  import { inspect, type InspectOptions } from "node:util";
13
13
  import { type CommandResult, type CommandRunOpts, type ForkOpts, type OptimizeFor, type SandboxOpts, type Tier } from "./core.js";
14
- import { PromigenceClient, type BackgroundExecRecord, type DirEntry, type DirListOpts, type ExtractResult, type FileInfo, type FileMatch, type Page, type PortRecord, type AutoPauseMode, type SandboxRecord, type SearchOpts, type SecretBinding, type SnapshotBuild, type VerifySpec } from "./client.js";
14
+ import { PromigenceClient, type BackgroundExecRecord, type DirEntry, type DirListOpts, type ExtractResult, type FileInfo, type FileMatch, type Page, type PortExposeOpts, type PortRecord, type AutoPauseMode, type SandboxRecord, type SearchOpts, type SecretBinding, type SnapshotBuild, type VerifySpec } from "./client.js";
15
15
  import { CommandHandle } from "./handles.js";
16
16
  import type { PtyExit, PtySession, PtySize } from "./pty.js";
17
+ /** `setTimeout`'s fix when the longer timeout does not fit under the run's spend cap. */
18
+ export declare const EXTEND_OVER_CAP_FIX: string;
17
19
  /**
18
20
  * `commands.run` options for a foreground command. The call resolves with the whole result when
19
21
  * the command ends; `onStdout` / `onStderr` also receive its output as it is written.
@@ -150,6 +152,8 @@ export interface SandboxCreateOpts extends SandboxOpts {
150
152
  start?: string | string[];
151
153
  tier?: Tier;
152
154
  alias?: string;
155
+ /** With `alias`: take the name from the snapshot that has it (refused without this). */
156
+ moveAlias?: boolean;
153
157
  /** Name of a stored secret holding the registry credentials. */
154
158
  registrySecret?: string;
155
159
  /** Bound on the first build of an image (default 15 min). Expiry cancels nothing. */
@@ -196,7 +200,7 @@ export interface SandboxCreateOpts extends SandboxOpts {
196
200
  * inputs) was built before, or 202 with a `build_id`. The **body**, not the status, decides: an
197
201
  * idempotency replay answers 202 with a document that may already be `verified`.
198
202
  */
199
- export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "registrySecret" | "buildTimeoutMs">, hooks?: {
203
+ export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "moveAlias" | "registrySecret" | "buildTimeoutMs">, hooks?: {
200
204
  /**
201
205
  * The `POST /snapshots` answer, before any wait: `build_price` (the most it can cost) and
202
206
  * whether it was `deduped` or `joined` (nothing new charged). An implicit build costs money, so a caller that prints prices prints this one too.
@@ -285,6 +289,7 @@ export declare class Sandbox {
285
289
  /**
286
290
  * Run `cmd` and resolve with its result. A non-zero exit throws `CommandExitError`; a command
287
291
  * killed at its own `timeoutMs` throws `CommandTimeoutError` (a `CommandExitError` too).
292
+ * With `throwOnExit: false` both resolve instead, and the result's `timedOut` tells them apart.
288
293
  */
289
294
  run(cmd: string, opts?: ForegroundRunOpts): Promise<CommandResult>;
290
295
  /** Background commands in this sandbox (running and ended, until killed with forget). */
@@ -312,9 +317,20 @@ export declare class Sandbox {
312
317
  resize(pid: number, size: PtySize): Promise<void>;
313
318
  kill(pid: number): Promise<boolean>;
314
319
  };
315
- /** Preview URLs for ports inside the sandbox (`getHost` gives the address alone). */
320
+ /**
321
+ * Preview URLs for ports inside the sandbox (`getHost` gives the address alone).
322
+ *
323
+ * Anyone holding a preview URL can open it; revoke it any time, and it ends with the sandbox.
324
+ * If your account's preview URLs aren't on, `expose` throws `PreviewsUnavailableError`; write to
325
+ * support@promigence.ai to have them turned on, and meanwhile reach the port with
326
+ * `promigence sandbox ssh <sid> -- -N -L <port>:localhost:<port>`.
327
+ */
316
328
  readonly ports: {
317
- expose(port: number): Promise<PortRecord>;
329
+ /**
330
+ * Publish `port`: `{ length: "long" }` for a longer, harder-to-guess URL (apps with sensitive
331
+ * data); omitted, the length this sandbox's project or organization chose (short by default).
332
+ */
333
+ expose(port: number, opts?: PortExposeOpts): Promise<PortRecord>;
318
334
  list(): Promise<PortRecord[]>;
319
335
  /** A token revokes that URL; a port number every URL for it. Returns the revoked tokens. */
320
336
  revoke(tokenOrPort: string | number): Promise<string[]>;
@@ -458,13 +474,16 @@ export declare class Sandbox {
458
474
  }>;
459
475
  isRunning(): Promise<boolean>;
460
476
  /**
461
- * `PATCH /sandboxes/:id` — extend this sandbox's timeout.
477
+ * `PATCH /sandboxes/:id` — set this sandbox's timeout, in milliseconds.
478
+ *
479
+ * The timeout is the sandbox's total lifetime, counted from its start, not from now:
480
+ * `setTimeout(600_000)` on a sandbox that has run 2 minutes leaves it 8 more.
462
481
  *
463
- * The timeout is the sandbox's total lifetime, not "from now", and the difference is billing:
464
- * commits *seconds* up front and `billable_seconds <= committed_seconds`, so an extension is a new
465
- * commitment that has to fit under the spend cap you set (the run's, or the org default). It throws
466
- * `SpendCapError` (409) rather than silently extending — which is the behaviour a spend
467
- * cap exists to produce, and the one difference here worth a caller knowing about.
482
+ * - Raising it commits the added seconds, which has to fit in what is left of the spend cap you set
483
+ * (the run's, or the org default): past it, it throws `SpendCapError` (409) rather than silently
484
+ * extending.
485
+ * - Lowering it goes down to what the sandbox has run plus 60 s, and no further
486
+ * (`InvalidArgumentError` below that). To stop it now, `kill()` it.
468
487
  */
469
488
  setTimeout(ms: number): Promise<void>;
470
489
  /**
@@ -476,8 +495,9 @@ export declare class Sandbox {
476
495
  * the window may be. Bring it back with `resume()`, which is the warm path.
477
496
  *
478
497
  * `mode: "activity"` is for an agent that runs inside the sandbox on its own: it also waits
479
- * until nothing inside has used CPU, moved a byte or held a connection open for the window
480
- * (`AutoPauseMode`). Omitted, the mode is left as it is (`"requests"` unless set).
498
+ * until, for the whole window, the sandbox has used under 5% of one core, sent and received no
499
+ * network byte and held no connection out open; disk writes do not count (`AutoPauseMode`).
500
+ * Omitted, the mode is left as it is (`"requests"` unless set).
481
501
  */
482
502
  setAutoPause(seconds: number, mode?: AutoPauseMode): Promise<void>;
483
503
  /** The idle rule in force: seconds before it pauses itself, `0` when it never does. */
@@ -499,7 +519,12 @@ export declare class Sandbox {
499
519
  * full `url` with its scheme.
500
520
  *
501
521
  * Async, because the address is a capability minted on demand rather than
502
- * a hostname derived from the id. An existing URL for the port is reused, not re-minted.
522
+ * a hostname derived from the id. An existing URL for the port is reused, not re-minted; a new
523
+ * one has the length this sandbox's project or organization chose (short by default).
524
+ *
525
+ * If your account's preview URLs aren't on, this throws `PreviewsUnavailableError`; write to
526
+ * support@promigence.ai to have them turned on, and meanwhile reach the port with
527
+ * `promigence sandbox ssh <sid> -- -N -L <port>:localhost:<port>`.
503
528
  */
504
529
  getHost(port: number): Promise<string>;
505
530
  /**
package/dist/sandbox.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { inspect } from "node:util";
2
- import { ApiError, CommandExitError, EnvFailedError, IMAGE_BUILD_TIMEOUT_MS, InvalidArgumentError, NotFoundError, TimeoutError, UnsupportedError, WAIT_TIMEOUT_MS, withPreviewsFix, } from "./core.js";
2
+ import { ApiError, CommandExitError, EnvFailedError, IMAGE_BUILD_TIMEOUT_MS, InvalidArgumentError, NotFoundError, SpendCapError, TimeoutError, UnsupportedError, WAIT_TIMEOUT_MS, withPreviewsFix, } from "./core.js";
3
3
  import { PromigenceClient, LONG_POLL_MS, checkImageRef, checkImageVersioned, checkSandboxId, errorFor, newIdempotencyKey, toCommandResult, isWaiting, } from "./client.js";
4
4
  import { CommandHandle } from "./handles.js";
5
5
  import { CommandTimeoutError, answerTimedOut } from "./command-errors.js";
6
6
  import { hintArg } from "./hint.js";
7
+ export const EXTEND_OVER_CAP_FIX = "Raising the timeout must fit in what is left of the run's spend cap, and this does not: start a new " +
8
+ "sandbox with the longer `timeoutMs` and a `spendCap` that covers it.";
7
9
  const EXEC_TIMEOUT_DEFAULT_MS = 15 * 60_000;
8
10
  function textSink(cb) {
9
11
  if (!cb)
@@ -37,6 +39,15 @@ export function ptyCommand(opts) {
37
39
  parts.push(opts.cmd ?? DEFAULT_PTY_CMD);
38
40
  return parts.join(" ");
39
41
  }
42
+ function allowList(hosts) {
43
+ if (!Array.isArray(hosts)) {
44
+ throw new InvalidArgumentError("allowHosts must be a list of hosts", 'Pass allowHosts: ["github.com"].');
45
+ }
46
+ if (!hosts.some((h) => typeof h !== "string" || h.trim() !== "")) {
47
+ throw new InvalidArgumentError("allowHosts names no host, so the allow-list is empty", 'Name each host the sandbox may reach: allowHosts: ["github.com"]. For no network pass allowInternetAccess: false; to reach anything leave allowHosts out.');
48
+ }
49
+ return hosts;
50
+ }
40
51
  function permString(mode) {
41
52
  const r = (m) => `${m & 4 ? "r" : "-"}${m & 2 ? "w" : "-"}${m & 1 ? "x" : "-"}`;
42
53
  return r((mode >> 6) & 7) + r((mode >> 3) & 7) + r(mode & 7);
@@ -89,6 +100,8 @@ export async function imageSnapshot(client, opts, hooks = {}) {
89
100
  create.tier = opts.tier;
90
101
  if (opts.alias !== undefined)
91
102
  create.alias = opts.alias;
103
+ if (opts.moveAlias)
104
+ create.moveAlias = true;
92
105
  if (opts.registrySecret !== undefined)
93
106
  create.registrySecret = opts.registrySecret;
94
107
  let build = (await client.snapshots.create(create)) ?? {};
@@ -348,7 +361,7 @@ export class Sandbox {
348
361
  },
349
362
  };
350
363
  this.ports = {
351
- expose: (port) => this.#client.sandboxes.ports.expose(this.sandboxId, port),
364
+ expose: (port, opts) => this.#client.sandboxes.ports.expose(this.sandboxId, port, opts),
352
365
  list: () => this.#client.sandboxes.ports.list(this.sandboxId),
353
366
  revoke: async (tokenOrPort) => (await this.#client.sandboxes.ports.revoke(this.sandboxId, tokenOrPort)).revoked,
354
367
  };
@@ -362,6 +375,9 @@ export class Sandbox {
362
375
  return format === "bytes" ? bytes : new TextDecoder().decode(bytes);
363
376
  }),
364
377
  write: async (path, data, opts = {}) => {
378
+ if (typeof path !== "string") {
379
+ throw new InvalidArgumentError(`files.write takes one path and its data; got ${Array.isArray(path) ? "an array" : path === null ? "null" : typeof path} as the path`, "Write each file with its own call: for (const f of files) await sandbox.files.write(f.path, f.data).");
380
+ }
365
381
  let body;
366
382
  if (typeof data === "string" || data instanceof Uint8Array)
367
383
  body = data;
@@ -556,8 +572,8 @@ export class Sandbox {
556
572
  start.timeoutMs = opts.timeoutMs;
557
573
  if (opts.allowInternetAccess === false)
558
574
  start.network = "none";
559
- if (opts.allowHosts?.length)
560
- start.network = { allow: opts.allowHosts };
575
+ if (opts.allowHosts !== undefined && opts.allowHosts !== null)
576
+ start.network = { allow: allowList(opts.allowHosts) };
561
577
  if (opts.metadata)
562
578
  start.labels = opts.metadata;
563
579
  if (opts.secrets?.length)
@@ -695,7 +711,15 @@ export class Sandbox {
695
711
  return rec.state === "ready" || rec.state === "running";
696
712
  }
697
713
  async setTimeout(ms) {
698
- await this.#client.sandboxes.patch(this.sandboxId, { timeoutMs: ms });
714
+ try {
715
+ await this.#client.sandboxes.patch(this.sandboxId, { timeoutMs: ms });
716
+ }
717
+ catch (e) {
718
+ if (e instanceof SpendCapError && e.code === "cap_exceeded") {
719
+ Object.defineProperty(e, "fix", { value: EXTEND_OVER_CAP_FIX, enumerable: true, configurable: true });
720
+ }
721
+ throw e;
722
+ }
699
723
  this.timeoutMs = ms;
700
724
  }
701
725
  async setAutoPause(seconds, mode) {
@@ -290,6 +290,7 @@ function start(init) {
290
290
  let rrAny = 0;
291
291
  let pumpTimer;
292
292
  let pumpSoon;
293
+ let turnsHeld = false;
293
294
  const freeNow = (l, now) => {
294
295
  for (let i = 0; i < l.length; i++) {
295
296
  const k = (rr + i) % l.length;
@@ -304,6 +305,8 @@ function start(init) {
304
305
  const pump = () => {
305
306
  pumpTimer = undefined;
306
307
  pumpSoon = undefined;
308
+ if (turnsHeld)
309
+ return;
307
310
  while (waiting.length > 0) {
308
311
  const l = live();
309
312
  if (l.length === 0) {
@@ -346,7 +349,7 @@ function start(init) {
346
349
  rrAny = (rrAny % l.length) + 1;
347
350
  return e;
348
351
  }
349
- if (waiting.length === 0) {
352
+ if (waiting.length === 0 && !turnsHeld) {
350
353
  const now = hrNow();
351
354
  const e = freeNow(l, now);
352
355
  if (e !== undefined) {
@@ -527,6 +530,12 @@ function start(init) {
527
530
  }
528
531
  else if (m?.t === "crash-for-test")
529
532
  setImmediate(() => { throw new Error("transport thread crash (test)"); });
533
+ else if (m?.t === "hold-turns-for-test")
534
+ turnsHeld = true;
535
+ else if (m?.t === "release-turns-for-test") {
536
+ turnsHeld = false;
537
+ pump();
538
+ }
530
539
  });
531
540
  void open();
532
541
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promigence/sdk",
3
- "version": "0.1.0-beta.15",
3
+ "version": "0.1.0-beta.17",
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",