@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.
@@ -1,8 +1,10 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.15";
2
+ export declare const VERSION = "0.1.0-beta.16";
3
3
  export declare const DOCS_URL = "https://promigence.ai";
4
4
  /** The error reference; each code is an anchor on it (the API's `docs` link has the same form). */
5
5
  export declare const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
6
+ /** The row of `code` in the error reference. */
7
+ export declare function errorsDocsFor(code: string): string;
6
8
  export declare const DEFAULT_API_URL = "https://api.promigence.ai/v1";
7
9
  /** Where to write about a key. A key is self-serve: `promigence login` signs up or in and stores one. */
8
10
  export declare const KEY_CONTACT = "support@promigence.ai";
@@ -43,8 +45,8 @@ export interface PromigenceErrorInit {
43
45
  /** The API error's `retryable`: nothing ran and the same request, sent again as is, can succeed. */
44
46
  retryable?: boolean;
45
47
  }
46
- /** What a caller reads instead: a fact about this beta, not a mistake of theirs. */
47
- export declare const PREVIEWS_UNAVAILABLE = "preview URLs are not available in this beta yet";
48
+ /** What a caller reads instead: a fact about their account, not a mistake of theirs. */
49
+ export declare const PREVIEWS_UNAVAILABLE = "preview URLs are not turned on for this account";
48
50
  export declare function previewsFix(sandboxId?: string, port?: string | number): string;
49
51
  /**
50
52
  * A `fix` that opens by repeating the message says only what it adds: the API's `usage` fix is
@@ -97,7 +99,13 @@ export declare class PromigenceError extends Error {
97
99
  guide: string;
98
100
  };
99
101
  }
100
- /** Bad arguments (e.g. an unknown template, a missing spend cap). exit 2 */
102
+ /**
103
+ * Bad arguments (e.g. an unknown template, a missing spend cap). exit 2
104
+ *
105
+ * `code` says who refused: `invalid_argument` is the SDK, before anything was sent; `usage` is the
106
+ * API, refusing what it was sent. Nothing ran and nothing was billed either way, so catch this
107
+ * class (or exit 2) to handle both.
108
+ */
101
109
  export declare class InvalidArgumentError extends PromigenceError {
102
110
  /**
103
111
  * `code` defaults to `invalid_argument` — what the SDK raises for a bad argument it catches
@@ -122,6 +130,26 @@ export declare function previewsOff(message: string): boolean;
122
130
  export declare class PreviewsUnavailableError extends InvalidArgumentError {
123
131
  constructor(sandboxId?: string, port?: string | number);
124
132
  }
133
+ /** The sandbox and its state when an API message is the "has ended" refusal, else `undefined`. */
134
+ export declare function sandboxEndedIn(message: string): {
135
+ sandboxId: string;
136
+ state: string;
137
+ } | undefined;
138
+ /** What a caller of this SDK does about an ended sandbox. */
139
+ export declare const SANDBOX_ENDED_FIX = "Start a new sandbox: Sandbox.create(). This one's getInfo() says how it ended (`outcome`).";
140
+ /**
141
+ * A call to a sandbox that has ended (`state` `ended` or `failed`): it does not come back. Still an
142
+ * `InvalidArgumentError` with code `usage` and exit 2, as the API answers it, so a catch written
143
+ * against earlier releases still catches it; catch this class to tell an ended sandbox from a bad
144
+ * argument. `sandboxId` and `state` name the sandbox and how it stands.
145
+ */
146
+ export declare class SandboxEndedError extends InvalidArgumentError {
147
+ /** The sandbox that has ended. */
148
+ readonly sandboxId: string;
149
+ /** Its state: `ended`, or `failed` for one that ended on a failure. */
150
+ readonly state: string;
151
+ constructor(message: string, sandboxId: string, state: string, fix?: string);
152
+ }
125
153
  /** The environment failed (our fault, never the task's). exit 3 */
126
154
  export declare class EnvFailedError extends PromigenceError {
127
155
  constructor(code: string, message: string, fix: string);
package/dist/cjs/core.js CHANGED
@@ -1,15 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EXIT_BY_CODE = exports.NotReleasedError = exports.ApiError = exports.ServiceRestartError = exports.TimeoutError = exports.RateLimitError = exports.UnreachableError = exports.NotFoundError = exports.AuthError = exports.NotVerifiedError = exports.SpendCapError = exports.CommandExitError = exports.EnvFailedError = exports.PreviewsUnavailableError = exports.UnsupportedError = exports.InvalidArgumentError = exports.PromigenceError = exports.PREVIEWS_UNAVAILABLE = exports.FILE_PUT_CHUNK_BYTES = exports.MAX_OUTPUT_BYTES_DEFAULT = exports.IMAGE_BUILD_LIST_PRICE = exports.IMAGE_BUILD_TIMEOUT_MS = exports.WAIT_TIMEOUT_MS = exports.KEY_CONTACT = exports.DEFAULT_API_URL = exports.ERRORS_DOCS_URL = exports.DOCS_URL = exports.VERSION = void 0;
3
+ exports.EXIT_BY_CODE = exports.NotReleasedError = exports.ApiError = exports.ServiceRestartError = exports.TimeoutError = exports.RateLimitError = exports.UnreachableError = exports.NotFoundError = exports.AuthError = exports.NotVerifiedError = exports.SpendCapError = exports.CommandExitError = exports.EnvFailedError = exports.SandboxEndedError = exports.SANDBOX_ENDED_FIX = exports.PreviewsUnavailableError = exports.UnsupportedError = exports.InvalidArgumentError = exports.PromigenceError = exports.PREVIEWS_UNAVAILABLE = exports.FILE_PUT_CHUNK_BYTES = exports.MAX_OUTPUT_BYTES_DEFAULT = exports.IMAGE_BUILD_LIST_PRICE = exports.IMAGE_BUILD_TIMEOUT_MS = exports.WAIT_TIMEOUT_MS = exports.KEY_CONTACT = exports.DEFAULT_API_URL = exports.ERRORS_DOCS_URL = exports.DOCS_URL = exports.VERSION = void 0;
4
+ exports.errorsDocsFor = errorsDocsFor;
4
5
  exports.previewsFix = previewsFix;
5
6
  exports.withPreviewsFix = withPreviewsFix;
6
7
  exports.fixBeyondMessage = fixBeyondMessage;
7
8
  exports.previewsOff = previewsOff;
9
+ exports.sandboxEndedIn = sandboxEndedIn;
8
10
  exports.exitForCode = exitForCode;
9
11
  const hint_js_1 = require("./hint.js");
10
- exports.VERSION = "0.1.0-beta.15";
12
+ exports.VERSION = "0.1.0-beta.16";
11
13
  exports.DOCS_URL = "https://promigence.ai";
12
14
  exports.ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
15
+ function errorsDocsFor(code) {
16
+ return `${exports.ERRORS_DOCS_URL}#${code}`;
17
+ }
13
18
  exports.DEFAULT_API_URL = "https://api.promigence.ai/v1";
14
19
  exports.KEY_CONTACT = "support@promigence.ai";
15
20
  exports.WAIT_TIMEOUT_MS = 100_000;
@@ -18,10 +23,11 @@ exports.IMAGE_BUILD_LIST_PRICE = 0.1656;
18
23
  exports.MAX_OUTPUT_BYTES_DEFAULT = 64 * 1024;
19
24
  exports.FILE_PUT_CHUNK_BYTES = 16 * 1024 * 1024;
20
25
  const PREVIEWS_OFF = /^previews are not enabled on this deployment\.?$/i;
21
- exports.PREVIEWS_UNAVAILABLE = "preview URLs are not available in this beta yet";
26
+ exports.PREVIEWS_UNAVAILABLE = "preview URLs are not turned on for this account";
22
27
  function previewsFix(sandboxId = "SID", port = 8000) {
23
28
  return `Reach the port over SSH instead: \`promigence sandbox ssh ${(0, hint_js_1.hintArg)(sandboxId)} -- -N -L ${(0, hint_js_1.hintArg)(`${port}:localhost:${port}`)}\` ` +
24
- `forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C.`;
29
+ `forwards http://localhost:${port} here to port ${port} in the sandbox until Ctrl-C. ` +
30
+ "Write to support@promigence.ai to have preview URLs turned on.";
25
31
  }
26
32
  async function withPreviewsFix(call, sandboxId, port = "PORT") {
27
33
  try {
@@ -60,7 +66,7 @@ class PromigenceError extends Error {
60
66
  this.name = new.target.name;
61
67
  this.code = code;
62
68
  this.fix = previewsOff ? previewsFix() : fixBeyondMessage(init.message, init.fix);
63
- this.docs = init.docs ?? exports.DOCS_URL;
69
+ this.docs = init.docs ?? errorsDocsFor(code);
64
70
  this.exitCode = init.exitCode ?? 1;
65
71
  this.cliExit = init.exitCode ?? 1;
66
72
  this.guide = `promigence agent-guide --section errors --code ${(0, hint_js_1.hintArg)(code)}`;
@@ -111,6 +117,22 @@ class PreviewsUnavailableError extends InvalidArgumentError {
111
117
  }
112
118
  }
113
119
  exports.PreviewsUnavailableError = PreviewsUnavailableError;
120
+ const SANDBOX_ENDED = /^sandbox (\S+) has ended \(`([a-z_]+)`\)/;
121
+ function sandboxEndedIn(message) {
122
+ const m = SANDBOX_ENDED.exec(message.trim());
123
+ return m ? { sandboxId: m[1], state: m[2] } : undefined;
124
+ }
125
+ exports.SANDBOX_ENDED_FIX = "Start a new sandbox: Sandbox.create(). This one's getInfo() says how it ended (`outcome`).";
126
+ class SandboxEndedError extends InvalidArgumentError {
127
+ sandboxId;
128
+ state;
129
+ constructor(message, sandboxId, state, fix = exports.SANDBOX_ENDED_FIX) {
130
+ super(message, fix, "usage");
131
+ this.sandboxId = sandboxId;
132
+ this.state = state;
133
+ }
134
+ }
135
+ exports.SandboxEndedError = SandboxEndedError;
114
136
  class EnvFailedError extends PromigenceError {
115
137
  constructor(code, message, fix) {
116
138
  super({ code, message, fix, exitCode: 3 });
@@ -240,6 +262,7 @@ exports.EXIT_BY_CODE = {
240
262
  env_failed: 3, snapshot_rejected: 6, build_failed: 3, snapshot_failed: 4,
241
263
  not_connected: 7,
242
264
  service_restart: 3,
265
+ digest_mismatch: 4,
243
266
  fork_failed: 3, start_timeout: 3, readiness_failed: 3, verify_too_slow: 3,
244
267
  verify_each_failed: 3, toolchain_mismatch: 3, sandbox_unresponsive: 3, oom_within_tier: 3,
245
268
  disk_full_within_tier: 3, sandbox_interrupted: 3, integrity_mismatch: 3, egress_unavailable: 3,
@@ -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/cjs/http2.js CHANGED
@@ -1351,6 +1351,12 @@ const THREAD_GRACE_MS = 1_000;
1351
1351
  const THREAD_SWEEP_MS = 250;
1352
1352
  const THREAD_MARKS = 4096;
1353
1353
  const THREAD_PHASES = ["w.got", "w.sent", "w.head", "w.end"];
1354
+ function clockOffset(hr = () => Number(process.hrtime.bigint()) / 1e6, now = () => performance.now(), n = 5) {
1355
+ let best = -Infinity;
1356
+ for (let i = 0; i < n; i++)
1357
+ best = Math.max(best, hr() - now());
1358
+ return best;
1359
+ }
1354
1360
  class TransportThread {
1355
1361
  origin;
1356
1362
  worker;
@@ -1376,7 +1382,7 @@ class TransportThread {
1376
1382
  posts = 0;
1377
1383
  answered = 0;
1378
1384
  pace = THREAD_DEFAULTS.pace;
1379
- hrOffset = Number(process.hrtime.bigint()) / 1e6 - performance.now();
1385
+ hrOffset = clockOffset();
1380
1386
  constructor(origin, worker, port, acks) {
1381
1387
  this.origin = origin;
1382
1388
  this.worker = worker;
@@ -2066,6 +2072,9 @@ exports.__http2Internals = {
2066
2072
  threadKnobs(env) {
2067
2073
  return threadKnobs(env);
2068
2074
  },
2075
+ clockOffset(hr, now) {
2076
+ return clockOffset(hr, now);
2077
+ },
2069
2078
  wedgeThreadForTest() {
2070
2079
  try {
2071
2080
  transportThread?.port.postMessage({ t: "wedge-for-test" });
@@ -2078,6 +2087,18 @@ exports.__http2Internals = {
2078
2087
  }
2079
2088
  catch { }
2080
2089
  },
2090
+ holdTurnsForTest() {
2091
+ try {
2092
+ transportThread?.port.postMessage({ t: "hold-turns-for-test" });
2093
+ }
2094
+ catch { }
2095
+ },
2096
+ releaseTurnsForTest() {
2097
+ try {
2098
+ transportThread?.port.postMessage({ t: "release-turns-for-test" });
2099
+ }
2100
+ catch { }
2101
+ },
2081
2102
  early: {
2082
2103
  start(host) {
2083
2104
  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
  /**
@@ -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
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Sandbox = exports.DEFAULT_SNAPSHOT = exports.DEFAULT_PTY_CMD = void 0;
3
+ exports.Sandbox = exports.DEFAULT_SNAPSHOT = exports.DEFAULT_PTY_CMD = exports.EXTEND_OVER_CAP_FIX = void 0;
4
4
  exports.shellQuote = shellQuote;
5
5
  exports.ptyCommand = ptyCommand;
6
6
  exports.toEntryInfo = toEntryInfo;
@@ -14,6 +14,8 @@ const client_js_1 = require("./client.js");
14
14
  const handles_js_1 = require("./handles.js");
15
15
  const command_errors_js_1 = require("./command-errors.js");
16
16
  const hint_js_1 = require("./hint.js");
17
+ exports.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 " +
18
+ "sandbox with the longer `timeoutMs` and a `spendCap` that covers it.";
17
19
  const EXEC_TIMEOUT_DEFAULT_MS = 15 * 60_000;
18
20
  function textSink(cb) {
19
21
  if (!cb)
@@ -47,6 +49,15 @@ function ptyCommand(opts) {
47
49
  parts.push(opts.cmd ?? exports.DEFAULT_PTY_CMD);
48
50
  return parts.join(" ");
49
51
  }
52
+ function allowList(hosts) {
53
+ if (!Array.isArray(hosts)) {
54
+ throw new core_js_1.InvalidArgumentError("allowHosts must be a list of hosts", 'Pass allowHosts: ["github.com"].');
55
+ }
56
+ if (!hosts.some((h) => typeof h !== "string" || h.trim() !== "")) {
57
+ throw new core_js_1.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.');
58
+ }
59
+ return hosts;
60
+ }
50
61
  function permString(mode) {
51
62
  const r = (m) => `${m & 4 ? "r" : "-"}${m & 2 ? "w" : "-"}${m & 1 ? "x" : "-"}`;
52
63
  return r((mode >> 6) & 7) + r((mode >> 3) & 7) + r(mode & 7);
@@ -99,6 +110,8 @@ async function imageSnapshot(client, opts, hooks = {}) {
99
110
  create.tier = opts.tier;
100
111
  if (opts.alias !== undefined)
101
112
  create.alias = opts.alias;
113
+ if (opts.moveAlias)
114
+ create.moveAlias = true;
102
115
  if (opts.registrySecret !== undefined)
103
116
  create.registrySecret = opts.registrySecret;
104
117
  let build = (await client.snapshots.create(create)) ?? {};
@@ -372,6 +385,9 @@ class Sandbox {
372
385
  return format === "bytes" ? bytes : new TextDecoder().decode(bytes);
373
386
  }),
374
387
  write: async (path, data, opts = {}) => {
388
+ if (typeof path !== "string") {
389
+ throw new core_js_1.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).");
390
+ }
375
391
  let body;
376
392
  if (typeof data === "string" || data instanceof Uint8Array)
377
393
  body = data;
@@ -566,8 +582,8 @@ class Sandbox {
566
582
  start.timeoutMs = opts.timeoutMs;
567
583
  if (opts.allowInternetAccess === false)
568
584
  start.network = "none";
569
- if (opts.allowHosts?.length)
570
- start.network = { allow: opts.allowHosts };
585
+ if (opts.allowHosts !== undefined && opts.allowHosts !== null)
586
+ start.network = { allow: allowList(opts.allowHosts) };
571
587
  if (opts.metadata)
572
588
  start.labels = opts.metadata;
573
589
  if (opts.secrets?.length)
@@ -705,7 +721,15 @@ class Sandbox {
705
721
  return rec.state === "ready" || rec.state === "running";
706
722
  }
707
723
  async setTimeout(ms) {
708
- await this.#client.sandboxes.patch(this.sandboxId, { timeoutMs: ms });
724
+ try {
725
+ await this.#client.sandboxes.patch(this.sandboxId, { timeoutMs: ms });
726
+ }
727
+ catch (e) {
728
+ if (e instanceof core_js_1.SpendCapError && e.code === "cap_exceeded") {
729
+ Object.defineProperty(e, "fix", { value: exports.EXTEND_OVER_CAP_FIX, enumerable: true, configurable: true });
730
+ }
731
+ throw e;
732
+ }
709
733
  this.timeoutMs = ms;
710
734
  }
711
735
  async setAutoPause(seconds, mode) {
@@ -292,6 +292,7 @@ function start(init) {
292
292
  let rrAny = 0;
293
293
  let pumpTimer;
294
294
  let pumpSoon;
295
+ let turnsHeld = false;
295
296
  const freeNow = (l, now) => {
296
297
  for (let i = 0; i < l.length; i++) {
297
298
  const k = (rr + i) % l.length;
@@ -306,6 +307,8 @@ function start(init) {
306
307
  const pump = () => {
307
308
  pumpTimer = undefined;
308
309
  pumpSoon = undefined;
310
+ if (turnsHeld)
311
+ return;
309
312
  while (waiting.length > 0) {
310
313
  const l = live();
311
314
  if (l.length === 0) {
@@ -348,7 +351,7 @@ function start(init) {
348
351
  rrAny = (rrAny % l.length) + 1;
349
352
  return e;
350
353
  }
351
- if (waiting.length === 0) {
354
+ if (waiting.length === 0 && !turnsHeld) {
352
355
  const now = hrNow();
353
356
  const e = freeNow(l, now);
354
357
  if (e !== undefined) {
@@ -529,6 +532,12 @@ function start(init) {
529
532
  }
530
533
  else if (m?.t === "crash-for-test")
531
534
  setImmediate(() => { throw new Error("transport thread crash (test)"); });
535
+ else if (m?.t === "hold-turns-for-test")
536
+ turnsHeld = true;
537
+ else if (m?.t === "release-turns-for-test") {
538
+ turnsHeld = false;
539
+ pump();
540
+ }
532
541
  });
533
542
  void open();
534
543
  }