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

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/core.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { hintArg } from "./hint.js";
2
- export const VERSION = "0.1.0-beta.15";
2
+ export const VERSION = "0.1.0-beta.16";
3
3
  export const DOCS_URL = "https://promigence.ai";
4
4
  export const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
5
+ export function errorsDocsFor(code) {
6
+ return `${ERRORS_DOCS_URL}#${code}`;
7
+ }
5
8
  export const DEFAULT_API_URL = "https://api.promigence.ai/v1";
6
9
  export const KEY_CONTACT = "support@promigence.ai";
7
10
  export const WAIT_TIMEOUT_MS = 100_000;
@@ -10,10 +13,11 @@ export const IMAGE_BUILD_LIST_PRICE = 0.1656;
10
13
  export const MAX_OUTPUT_BYTES_DEFAULT = 64 * 1024;
11
14
  export const FILE_PUT_CHUNK_BYTES = 16 * 1024 * 1024;
12
15
  const PREVIEWS_OFF = /^previews are not enabled on this deployment\.?$/i;
13
- export const PREVIEWS_UNAVAILABLE = "preview URLs are not available in this beta yet";
16
+ export const PREVIEWS_UNAVAILABLE = "preview URLs are not turned on for this account";
14
17
  export function previewsFix(sandboxId = "SID", port = 8000) {
15
18
  return `Reach the port over SSH instead: \`promigence sandbox ssh ${hintArg(sandboxId)} -- -N -L ${hintArg(`${port}:localhost:${port}`)}\` ` +
16
- `forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C.`;
19
+ `forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C. ` +
20
+ "Write to support@promigence.ai to have preview URLs turned on.";
17
21
  }
18
22
  export async function withPreviewsFix(call, sandboxId, port = "PORT") {
19
23
  try {
@@ -52,7 +56,7 @@ export class PromigenceError extends Error {
52
56
  this.name = new.target.name;
53
57
  this.code = code;
54
58
  this.fix = previewsOff ? previewsFix() : fixBeyondMessage(init.message, init.fix);
55
- this.docs = init.docs ?? DOCS_URL;
59
+ this.docs = init.docs ?? errorsDocsFor(code);
56
60
  this.exitCode = init.exitCode ?? 1;
57
61
  this.cliExit = init.exitCode ?? 1;
58
62
  this.guide = `promigence agent-guide --section errors --code ${hintArg(code)}`;
@@ -99,6 +103,21 @@ export class PreviewsUnavailableError extends InvalidArgumentError {
99
103
  super(PREVIEWS_UNAVAILABLE, previewsFix(sandboxId, port), "unsupported");
100
104
  }
101
105
  }
106
+ const SANDBOX_ENDED = /^sandbox (\S+) has ended \(`([a-z_]+)`\)/;
107
+ export function sandboxEndedIn(message) {
108
+ const m = SANDBOX_ENDED.exec(message.trim());
109
+ return m ? { sandboxId: m[1], state: m[2] } : undefined;
110
+ }
111
+ export const SANDBOX_ENDED_FIX = "Start a new sandbox: Sandbox.create(). This one's getInfo() says how it ended (`outcome`).";
112
+ export class SandboxEndedError extends InvalidArgumentError {
113
+ sandboxId;
114
+ state;
115
+ constructor(message, sandboxId, state, fix = SANDBOX_ENDED_FIX) {
116
+ super(message, fix, "usage");
117
+ this.sandboxId = sandboxId;
118
+ this.state = state;
119
+ }
120
+ }
102
121
  export class EnvFailedError extends PromigenceError {
103
122
  constructor(code, message, fix) {
104
123
  super({ code, message, fix, exitCode: 3 });
@@ -216,6 +235,7 @@ export const EXIT_BY_CODE = {
216
235
  env_failed: 3, snapshot_rejected: 6, build_failed: 3, snapshot_failed: 4,
217
236
  not_connected: 7,
218
237
  service_restart: 3,
238
+ digest_mismatch: 4,
219
239
  fork_failed: 3, start_timeout: 3, readiness_failed: 3, verify_too_slow: 3,
220
240
  verify_each_failed: 3, toolchain_mismatch: 3, sandbox_unresponsive: 3, oom_within_tier: 3,
221
241
  disk_full_within_tier: 3, sandbox_interrupted: 3, integrity_mismatch: 3, egress_unavailable: 3,
package/dist/http2.d.ts CHANGED
@@ -197,10 +197,18 @@ export declare const __http2Internals: {
197
197
  gapUs: number;
198
198
  pace: string;
199
199
  };
200
+ /** The offset that turns the thread's stamps into trace times (`clockOffset`), read from the two clocks given. */
201
+ clockOffset(hr: () => number, now: () => number): number;
200
202
  /** Tests: make the transport thread stop answering (it reads nothing more), as a stuck one would. */
201
203
  wedgeThreadForTest(): void;
202
204
  /** Tests: make the transport thread fail as a crash would. */
203
205
  crashThreadForTest(): void;
206
+ /**
207
+ * Tests: the transport thread gives no spaced request its turn until `releaseTurnsForTest`. Both go as messages, in order
208
+ * with the requests and their cancels, so what is sent between them reaches the thread while every spaced request waits.
209
+ */
210
+ holdTurnsForTest(): void;
211
+ releaseTurnsForTest(): void;
204
212
  /** The early lookup, for tests: start one with a platform, DNS servers and hosts file of the test's own. */
205
213
  early: {
206
214
  start(host: string): boolean;
package/dist/http2.js CHANGED
@@ -1340,6 +1340,12 @@ const THREAD_GRACE_MS = 1_000;
1340
1340
  const THREAD_SWEEP_MS = 250;
1341
1341
  const THREAD_MARKS = 4096;
1342
1342
  const THREAD_PHASES = ["w.got", "w.sent", "w.head", "w.end"];
1343
+ function clockOffset(hr = () => Number(process.hrtime.bigint()) / 1e6, now = () => performance.now(), n = 5) {
1344
+ let best = -Infinity;
1345
+ for (let i = 0; i < n; i++)
1346
+ best = Math.max(best, hr() - now());
1347
+ return best;
1348
+ }
1343
1349
  class TransportThread {
1344
1350
  origin;
1345
1351
  worker;
@@ -1365,7 +1371,7 @@ class TransportThread {
1365
1371
  posts = 0;
1366
1372
  answered = 0;
1367
1373
  pace = THREAD_DEFAULTS.pace;
1368
- hrOffset = Number(process.hrtime.bigint()) / 1e6 - performance.now();
1374
+ hrOffset = clockOffset();
1369
1375
  constructor(origin, worker, port, acks) {
1370
1376
  this.origin = origin;
1371
1377
  this.worker = worker;
@@ -2054,6 +2060,9 @@ export const __http2Internals = {
2054
2060
  threadKnobs(env) {
2055
2061
  return threadKnobs(env);
2056
2062
  },
2063
+ clockOffset(hr, now) {
2064
+ return clockOffset(hr, now);
2065
+ },
2057
2066
  wedgeThreadForTest() {
2058
2067
  try {
2059
2068
  transportThread?.port.postMessage({ t: "wedge-for-test" });
@@ -2066,6 +2075,18 @@ export const __http2Internals = {
2066
2075
  }
2067
2076
  catch { }
2068
2077
  },
2078
+ holdTurnsForTest() {
2079
+ try {
2080
+ transportThread?.port.postMessage({ t: "hold-turns-for-test" });
2081
+ }
2082
+ catch { }
2083
+ },
2084
+ releaseTurnsForTest() {
2085
+ try {
2086
+ transportThread?.port.postMessage({ t: "release-turns-for-test" });
2087
+ }
2088
+ catch { }
2089
+ },
2069
2090
  early: {
2070
2091
  start(host) {
2071
2092
  return earlyLookupFor(host.toLowerCase()) !== undefined;
@@ -739,6 +739,11 @@ export interface BuildQueued {
739
739
  * True when a verification command was given.
740
740
  */
741
741
  verified: boolean;
742
+ /**
743
+ * What to know before the build ends: with `move_alias`, which snapshot gives up the alias
744
+ * to it once it is stored.
745
+ */
746
+ warnings?: Array<string>;
742
747
  }
743
748
  /**
744
749
  * Builds in the period.
@@ -3074,7 +3079,7 @@ export interface NameCreate {
3074
3079
  */
3075
3080
  name: string;
3076
3081
  }
3077
- export type Network = "none" | "egress";
3082
+ export type Network = "none" | "egress" | "allow";
3078
3083
  /**
3079
3084
  * An allowlist: the only hosts the sandbox may reach.
3080
3085
  */
@@ -3528,6 +3533,11 @@ export interface Port {
3528
3533
  * Unix nanoseconds.
3529
3534
  */
3530
3535
  created_unix_ns?: number | null;
3536
+ /**
3537
+ * When the URL stops working on its own, Unix nanoseconds; `null` when it works until it is
3538
+ * revoked or the sandbox ends.
3539
+ */
3540
+ expires_unix_ns?: number | null;
3531
3541
  /**
3532
3542
  * The same publication addressed by name rather than by path. Apps that emit absolute paths
3533
3543
  * need this form.
@@ -3539,8 +3549,8 @@ export interface Port {
3539
3549
  port?: number | null;
3540
3550
  sandbox_id: string;
3541
3551
  /**
3542
- * The token the URL carries. Pass it to `DELETE /v1/sandboxes/{id}/ports/{token_or_port}` to
3543
- * revoke this one URL.
3552
+ * The token the URL carries. Pass it in the body of `POST /v1/sandboxes/{id}/ports/revoke`
3553
+ * (`{"token": …}`) to revoke this one URL.
3544
3554
  */
3545
3555
  token?: string | null;
3546
3556
  /**
@@ -3563,6 +3573,25 @@ export interface PortOpen {
3563
3573
  * 1–65535.
3564
3574
  */
3565
3575
  port: number;
3576
+ /**
3577
+ * Seconds until the URL stops working on its own, 1 to 2592000 (30 days). Absent: it works
3578
+ * until it is revoked or the sandbox ends.
3579
+ */
3580
+ ttl_s?: number | null;
3581
+ }
3582
+ /**
3583
+ * `POST /v1/sandboxes/{id}/ports/revoke`: revoke one URL by its token, or every URL of a port,
3584
+ * with what it names in the body. The token never travels in a URL.
3585
+ */
3586
+ export interface PortRevoke {
3587
+ /**
3588
+ * A port: every URL minted for it is revoked. Exactly one of `token` and `port`.
3589
+ */
3590
+ port?: number | null;
3591
+ /**
3592
+ * The token of the one URL to revoke.
3593
+ */
3594
+ token?: string | null;
3566
3595
  }
3567
3596
  /**
3568
3597
  * `POST /v1/billing/portal` — a link to the billing portal.
@@ -3571,7 +3600,7 @@ export interface PortalSession {
3571
3600
  url: string;
3572
3601
  }
3573
3602
  /**
3574
- * `DELETE /v1/sandboxes/{id}/ports/{token_or_port}`.
3603
+ * `POST /v1/sandboxes/{id}/ports/revoke` and `DELETE /v1/sandboxes/{id}/ports/{token_or_port}`.
3575
3604
  */
3576
3605
  export interface PortsRevoked {
3577
3606
  ok: boolean;
@@ -4158,6 +4187,11 @@ export interface ReplayStats {
4158
4187
  success_pct: number;
4159
4188
  }
4160
4189
  export interface Requested {
4190
+ /**
4191
+ * With `network: "allow"`: the only destinations the run's sandboxes may reach, as the run
4192
+ * named them. Absent for the other modes.
4193
+ */
4194
+ allow?: Array<string>;
4161
4195
  allow_partial: boolean;
4162
4196
  count: number;
4163
4197
  /**
@@ -4736,6 +4770,12 @@ export interface Sandbox {
4736
4770
  */
4737
4771
  holdout_passed?: boolean | null;
4738
4772
  holdout_score?: number | null;
4773
+ /**
4774
+ * `PROMIGENCE_INDEX`: this sandbox's place in its run, from 0. A retry, a replay or a
4775
+ * reopened failure keeps its original's. Absent when the run's command set the variable
4776
+ * itself.
4777
+ */
4778
+ index?: number | null;
4739
4779
  /**
4740
4780
  * True when this is a one-time wait: once the waiting sandboxes can start, the rest of the
4741
4781
  * run starts at once too. False otherwise.
@@ -4763,6 +4803,12 @@ export interface Sandbox {
4763
4803
  retry_of?: string | null;
4764
4804
  sandbox_id: string;
4765
4805
  score?: number | null;
4806
+ /**
4807
+ * `PROMIGENCE_SEED`: the number this sandbox's commands were given in that variable. A
4808
+ * retry, a replay or a reopened failure keeps its original's. Absent when the run's command
4809
+ * set the variable itself.
4810
+ */
4811
+ seed?: number | null;
4766
4812
  state: SandboxState;
4767
4813
  t_end?: string | null;
4768
4814
  t_handoff?: string | null;
@@ -5692,6 +5738,11 @@ export interface SessionTrial {
5692
5738
  */
5693
5739
  export interface SnapshotAliasSet {
5694
5740
  alias: string | null;
5741
+ /**
5742
+ * Take the name from the snapshot of the project that has it, which stays, unnamed. Without
5743
+ * it, a name another snapshot has is refused.
5744
+ */
5745
+ move_alias?: boolean;
5695
5746
  }
5696
5747
  /**
5697
5748
  * What an image occupies in storage: `allocated`, which can be less than its size.
@@ -5745,6 +5796,11 @@ export interface SnapshotCreate {
5745
5796
  * A container image, `REF@sha256:<digest>`; a tag is resolved to its digest first.
5746
5797
  */
5747
5798
  image?: string | null;
5799
+ /**
5800
+ * With `alias`: take the name from the snapshot of the project that has it, which stays,
5801
+ * unnamed. Without it, an `alias` another snapshot has is refused and nothing is built.
5802
+ */
5803
+ move_alias?: boolean;
5748
5804
  /**
5749
5805
  * `github:` repositories: a branch, a tag or a commit sha, resolved to the commit it names
5750
5806
  * when the build is accepted. Default: the repository's default branch.
@@ -5815,9 +5871,10 @@ export interface SnapshotDeduped {
5815
5871
  alias_applied?: boolean | null;
5816
5872
  /**
5817
5873
  * The alias that was asked for, when the snapshot did not hold it. The snapshot takes it
5818
- * (`alias_applied: true`) as a new build would: another snapshot of the project that had it
5819
- * keeps its row, without a name. A snapshot has one alias, so a name it had before gives
5820
- * way, and `warnings` says which.
5874
+ * (`alias_applied: true`) as a new build would: a name another snapshot of the project has
5875
+ * is taken only with `move_alias`, and that snapshot keeps its row, without a name. A
5876
+ * snapshot has one alias, so a name it had before gives way. `warnings` says which names
5877
+ * moved.
5821
5878
  */
5822
5879
  alias_requested?: string | null;
5823
5880
  /**
package/dist/sandbox.d.ts CHANGED
@@ -14,6 +14,8 @@ import { type CommandResult, type CommandRunOpts, type ForkOpts, type OptimizeFo
14
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";
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.
@@ -312,7 +316,14 @@ export declare class Sandbox {
312
316
  resize(pid: number, size: PtySize): Promise<void>;
313
317
  kill(pid: number): Promise<boolean>;
314
318
  };
315
- /** Preview URLs for ports inside the sandbox (`getHost` gives the address alone). */
319
+ /**
320
+ * Preview URLs for ports inside the sandbox (`getHost` gives the address alone).
321
+ *
322
+ * Anyone holding a preview URL can open it; revoke it any time, and it ends with the sandbox.
323
+ * If your account's preview URLs aren't on, `expose` throws `PreviewsUnavailableError`; write to
324
+ * support@promigence.ai to have them turned on, and meanwhile reach the port with
325
+ * `promigence sandbox ssh <sid> -- -N -L <port>:localhost:<port>`.
326
+ */
316
327
  readonly ports: {
317
328
  expose(port: number): Promise<PortRecord>;
318
329
  list(): Promise<PortRecord[]>;
@@ -458,13 +469,16 @@ export declare class Sandbox {
458
469
  }>;
459
470
  isRunning(): Promise<boolean>;
460
471
  /**
461
- * `PATCH /sandboxes/:id` — extend this sandbox's timeout.
472
+ * `PATCH /sandboxes/:id` — set this sandbox's timeout, in milliseconds.
462
473
  *
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.
474
+ * The timeout is the sandbox's total lifetime, counted from its start, not from now:
475
+ * `setTimeout(600_000)` on a sandbox that has run 2 minutes leaves it 8 more.
476
+ *
477
+ * - Raising it commits the added seconds, which has to fit in what is left of the spend cap you set
478
+ * (the run's, or the org default): past it, it throws `SpendCapError` (409) rather than silently
479
+ * extending.
480
+ * - Lowering it goes down to what the sandbox has run plus 60 s, and no further
481
+ * (`InvalidArgumentError` below that). To stop it now, `kill()` it.
468
482
  */
469
483
  setTimeout(ms: number): Promise<void>;
470
484
  /**
@@ -500,6 +514,10 @@ export declare class Sandbox {
500
514
  *
501
515
  * Async, because the address is a capability minted on demand rather than
502
516
  * a hostname derived from the id. An existing URL for the port is reused, not re-minted.
517
+ *
518
+ * If your account's preview URLs aren't on, this throws `PreviewsUnavailableError`; write to
519
+ * support@promigence.ai to have them turned on, and meanwhile reach the port with
520
+ * `promigence sandbox ssh <sid> -- -N -L <port>:localhost:<port>`.
503
521
  */
504
522
  getHost(port: number): Promise<string>;
505
523
  /**
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)) ?? {};
@@ -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.16",
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",