promigence 0.1.0-beta.8 → 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 CHANGED
@@ -62,6 +62,12 @@ Every command takes `--json`.
62
62
 
63
63
  ## From TypeScript
64
64
 
65
+ In your project (the global install gives you the command only):
66
+
67
+ ```sh
68
+ npm i promigence
69
+ ```
70
+
65
71
  ```ts
66
72
  import { Sandbox } from "promigence";
67
73
 
@@ -1,5 +1,6 @@
1
- import { PromigenceClient, type ExecResponse, type OptimizeFor, type RunReport, type SandboxRef, type ScoreMode, type SecretRecord } from "./sdk/index.js";
1
+ import { PromigenceClient, type ExecResponse, type OptimizeFor, type RunReport, type SandboxRef, type ScoreMode, type SecretRecord, type SnapshotBuild } from "./sdk/index.js";
2
2
  import { type Mode } from "./io.js";
3
+ import { type BuildPrice } from "./pricing.js";
3
4
  import { type Command, type NextHint } from "./surface.js";
4
5
  export interface Ctx {
5
6
  cmd: Command;
@@ -46,6 +47,7 @@ export declare function gradeOpts(ctx: Ctx): {
46
47
  score?: ScoreMode;
47
48
  } | undefined;
48
49
  export declare function secretListLines(s: SecretRecord): string[];
50
+ export declare function buildPriceOf(b: SnapshotBuild): BuildPrice | undefined;
49
51
  export declare function verifyTiming(v: Record<string, unknown> | undefined): string | undefined;
50
52
  export declare function buildLogLines(log: string[] | undefined, state: string): string[];
51
53
  export declare function snapshotSource(s: {
package/dist/commands.js CHANGED
@@ -10,7 +10,7 @@ import { DevcontainerError, detectOne, load, planJson, planLines, planSetup, res
10
10
  import { detect, ecosystemOfCommands, imageCreateCommand, readConfig } from "./init.js";
11
11
  import { CREDENTIAL_REMEDY, confirmOverride, credentialRefusal, emitUploadList, localTarNoMetadataArgs } from "./uploadguard.js";
12
12
  import { emit, errorEnvelope, errorTextLines, exitWith, forkLine, forkSummary, gradeLine, ms, noCommandFix, okEnvelope, parseMaxOutput, recordNotice, sandboxGrade, parseCommandTimeout, parseCount, parseDuration, parseUsd, pickExit, reportSummaryLines, toPromigenceError, totalsLine, usageError, write, writeBytes, } from "./io.js";
13
- import { buildPrice } from "./pricing.js";
13
+ import { buildPrice, buildPriceWords } from "./pricing.js";
14
14
  import { DOCS_URL, WAIT_TIMEOUT_DEFAULT } from "./surface.js";
15
15
  import { escapeLine, fieldForTerminal, forTerminal, jsonLine, sh, shellArg } from "./term.js";
16
16
  import { snapshotArg } from "./bundle.js";
@@ -430,6 +430,10 @@ function buildFields(b) {
430
430
  out["source"] = b.source;
431
431
  if (typeof b.verified === "boolean")
432
432
  out["verified"] = b.verified;
433
+ for (const k of ["build_price", "per_second_usd", "max_seconds", "billable_seconds", "cost_usd"]) {
434
+ if (typeof b[k] === "number")
435
+ out[k] = b[k];
436
+ }
433
437
  const ab = b;
434
438
  if (ab.alias_requested !== undefined)
435
439
  out["alias_requested"] = ab.alias_requested;
@@ -440,6 +444,11 @@ function buildFields(b) {
440
444
  function isBuilding(b) {
441
445
  return b.state === "queued" || b.state === "running";
442
446
  }
447
+ export function buildPriceOf(b) {
448
+ return typeof b.build_price === "number" && typeof b.per_second_usd === "number" && typeof b.max_seconds === "number"
449
+ ? { most_usd: b.build_price, per_second_usd: b.per_second_usd, max_s: b.max_seconds }
450
+ : undefined;
451
+ }
443
452
  function buildExit(b) {
444
453
  if (b.state === "verified" || b.state === "built")
445
454
  return 0;
@@ -474,9 +483,15 @@ function buildLines(b) {
474
483
  const timing = verifyTiming(b.verification);
475
484
  if (timing)
476
485
  lines.push(` verify: ${timing}`);
486
+ const cost = typeof b.cost_usd === "number" && !isBuilding(b) ? b.cost_usd : undefined;
487
+ const ran = typeof b.billable_seconds === "number" ? ` for the ${b.billable_seconds} s it ran` : "";
477
488
  if (b.error) {
478
489
  lines.push(` ${b.error.guard ? `guard ${b.error.guard}: ` : ""}${b.error.message ?? b.error.code ?? "rejected"}`);
479
- lines.push(` nothing was stored${b.error.stored === false ? "" : "?"} and nothing was charged`);
490
+ const stored = `nothing was stored${b.error.stored === false ? "" : "?"}`;
491
+ lines.push(cost !== undefined && cost > 0 ? ` ${stored}; billed ${usd(cost)}${ran}` : ` ${stored} and nothing was charged`);
492
+ }
493
+ else if (cost !== undefined && cost > 0) {
494
+ lines.push(` billed ${usd(cost)}${ran}`);
480
495
  }
481
496
  for (const w of b.warnings ?? [])
482
497
  lines.push(` warning: ${w}`);
@@ -875,14 +890,22 @@ const snapshotCreate = async (ctx) => {
875
890
  const reused = started.deduped === true;
876
891
  const joinedBuild = started.joined === true;
877
892
  const replay = buildReplayNote(started, str(ctx, "--idempotency-key") !== undefined);
878
- const charged = reused || joinedBuild || replay !== undefined ? 0 : (typeof started.build_price === "number" ? started.build_price : await buildPrice(c));
893
+ const free = reused || joinedBuild || replay !== undefined;
894
+ const builtAt = tier ?? (typeof started["tier"] === "string" ? started["tier"] : undefined);
895
+ const perSecond = buildPriceOf(started);
896
+ const flat = !free && perSecond === undefined && typeof started.build_price === "number" ? started.build_price : undefined;
897
+ const said = free || flat !== undefined ? perSecond : (perSecond ?? await buildPrice(c, builtAt));
898
+ const charged = free ? 0 : flat ?? said?.most_usd ?? 0;
879
899
  submitted["build_price"] = charged;
880
- if (reused || joinedBuild || replay !== undefined)
900
+ if (!free && said !== undefined)
901
+ Object.assign(submitted, { per_second_usd: said.per_second_usd, max_seconds: said.max_s });
902
+ if (free)
881
903
  submitted["charged"] = false;
882
904
  Object.assign(submitted, replayFields(started));
883
905
  await write(process.stderr, ctx.mode === "json"
884
906
  ? JSON.stringify({
885
907
  hint: "build_price", build_price: charged, unit: "USD", ...(tier === undefined ? {} : { tier }),
908
+ ...(!free && said !== undefined ? { per_second_usd: said.per_second_usd, max_seconds: said.max_s } : {}),
886
909
  ...(reused ? { deduped: true } : {}), ...(joinedBuild ? { joined: true } : {}),
887
910
  ...(replay === undefined ? {} : { replayed: true }),
888
911
  }) + "\n"
@@ -892,7 +915,9 @@ const snapshotCreate = async (ctx) => {
892
915
  ? `build_price: $0.00 - an identical build already ran (${started.build_id}); reusing it, nothing charged\n`
893
916
  : joinedBuild
894
917
  ? `build_price: $0.00 - joining an identical build already running (${started.build_id}); it is charged once\n`
895
- : `build_price: $${charged.toFixed(2)} (tier ${tier ?? "default"}) - building ${label}\n`);
918
+ : flat !== undefined
919
+ ? `build_price: $${flat.toFixed(2)} (tier ${tier ?? "default"}) - building ${label}\n`
920
+ : `build_price: ${buildPriceWords(said, builtAt ?? "default")} - building ${label}\n`);
896
921
  if (sources.length && ctx.mode !== "json" && !reused) {
897
922
  await write(process.stderr, sources
898
923
  .map((s) => ` also ${s.repo}@${s.commit.slice(0, 7)} -> ${s.path ?? "beside the workspace, under its own name"}`)
@@ -920,7 +945,7 @@ const snapshotCreate = async (ctx) => {
920
945
  const follow = sh `promigence snapshot status ${started.build_id} --wait`;
921
946
  const err = new PromigenceError({
922
947
  code: "aborted_by_client",
923
- message: `interrupted by ${sig}; build ${started.build_id} goes on and nothing was cancelled (it is charged once it verifies)`,
948
+ message: `interrupted by ${sig}; build ${started.build_id} goes on and nothing was cancelled (it is billed per second until it ends)`,
924
949
  fix: `Follow it: ${follow}.`,
925
950
  exitCode: 130,
926
951
  });
@@ -1587,7 +1612,8 @@ async function runImageSnapshot(ctx, c) {
1587
1612
  checkImageRef(image);
1588
1613
  const count = parseCount(required(ctx, "--count", "Pass --count N (number of sandboxes to fork)."), "--count");
1589
1614
  const cap = await spendCap(ctx, c);
1590
- const price = await buildPrice(c);
1615
+ const most = await buildPrice(c);
1616
+ const price = most.most_usd;
1591
1617
  const timeout = str(ctx, "--timeout");
1592
1618
  const quoted = await c.runs.quote({
1593
1619
  snapshot: DEFAULT_SNAPSHOT, count, ...(timeout === undefined ? {} : { timeoutMs: parseDuration(timeout, "--timeout") }),
@@ -1595,29 +1621,43 @@ async function runImageSnapshot(ctx, c) {
1595
1621
  const needed = Math.ceil((price + (quoted ?? 0)) * 100) / 100;
1596
1622
  if (cap < price + (quoted ?? 0)) {
1597
1623
  const existing = (quoted ?? 0) <= cap ? await existingImageSnapshot(c, image) : undefined;
1598
- throw new SpendCapError(`a new image's snapshot build costs ${moneyUsd(price)} on top of the run` +
1624
+ throw new SpendCapError(`a new image's snapshot build can cost up to ${moneyUsd(price)} on top of the run` +
1599
1625
  `${quoted === undefined ? "" : ` (about ${moneyUsd(quoted)})`}, over --cap ${moneyUsd(cap)}; nothing was built or started`, (existing === undefined ? "" : `This image already has snapshot ${existing}: run it under this cap with ${sh `promigence run --snapshot ${existing}`} instead of --image (any start command or settings it was made with come with it). Or `) +
1600
- `${existing === undefined ? "Pass" : "pass"} --cap ${needed.toFixed(2)} or more (a build is charged once, only when this image has no snapshot yet, and reused free after), ` +
1626
+ `${existing === undefined ? "Pass" : "pass"} --cap ${needed.toFixed(2)} or more (a build is billed per second while it runs, only when this image has no snapshot yet, and reused free after), ` +
1601
1627
  `or build it first, ${sh `promigence snapshot create --image ${image} --wait`}, and run its snapshot with --snapshot.`);
1602
1628
  }
1603
1629
  await write(process.stderr, ctx.mode === "json"
1604
1630
  ? jsonLine(process.stderr, { hint: "image snapshot", image })
1605
1631
  : `image ${image}: creating or reusing its snapshot (a first build takes minutes)\n`);
1606
1632
  let charged = 0;
1633
+ let perSecond = false;
1607
1634
  let buildId;
1608
1635
  const buildOpts = { image, allowBuild: true };
1609
1636
  const snapshotId = await imageSnapshot(c, buildOpts, {
1610
1637
  onBuild: async (b) => {
1611
1638
  const free = b.deduped === true || b.joined === true || b.idempotent_replay === true || b.state === "verified" || b.state === "built";
1639
+ const said = buildPriceOf(b);
1640
+ perSecond = !free && said !== undefined;
1612
1641
  charged = free ? 0 : (typeof b.build_price === "number" ? b.build_price : price);
1613
1642
  buildId = b.build_id;
1614
1643
  await write(process.stderr, ctx.mode === "json"
1615
- ? jsonLine(process.stderr, { hint: "build_price", build_price: charged, unit: "USD", image, ...(b.build_id ? { build_id: b.build_id } : {}), ...(free ? { charged: false } : {}) })
1644
+ ? jsonLine(process.stderr, {
1645
+ hint: "build_price", build_price: charged, unit: "USD", image,
1646
+ ...(perSecond && said ? { per_second_usd: said.per_second_usd, max_seconds: said.max_s } : {}),
1647
+ ...(b.build_id ? { build_id: b.build_id } : {}), ...(free ? { charged: false } : {}),
1648
+ })
1616
1649
  : free
1617
1650
  ? `build_price: $0.00 - this image's snapshot already exists; reusing it, nothing charged\n`
1618
- : `build_price: $${charged.toFixed(2)} - building ${image}'s snapshot (charged once it verifies; counted in --cap)\n`);
1651
+ : perSecond && said
1652
+ ? `build_price: ${buildPriceWords(said)} - building ${image}'s snapshot (billed per second while it builds; counted in --cap)\n`
1653
+ : `build_price: $${charged.toFixed(2)} - building ${image}'s snapshot (charged once it verifies; counted in --cap)\n`);
1619
1654
  },
1620
1655
  });
1656
+ if (perSecond && buildId !== undefined) {
1657
+ const done = await c.snapshots.status(buildId).catch(() => undefined);
1658
+ if (done !== undefined && typeof done.cost_usd === "number" && !isBuilding(done))
1659
+ charged = done.cost_usd;
1660
+ }
1621
1661
  await write(process.stderr, ctx.mode === "json"
1622
1662
  ? jsonLine(process.stderr, { hint: "image snapshot ready", image, snapshot_id: snapshotId })
1623
1663
  : fieldForTerminal(process.stderr, `image ${image}: snapshot ${snapshotId}`) + "\n");
@@ -20,7 +20,7 @@ export interface TierEntry {
20
20
  per_second_usd: number;
21
21
  }
22
22
  export declare const TIERS: readonly TierEntry[];
23
- export declare const BUILD_PRICE = 1;
23
+ export declare const BUILD_MAX_S = 3600;
24
24
  export declare const PRICING: {
25
25
  readonly vcpu_second_usd: 0.000014;
26
26
  readonly gib_second_usd: 0.0000045;
package/dist/generated.js CHANGED
@@ -158,12 +158,12 @@ export const ERRORS = [
158
158
  { code: "capacity_limit", category: "env", http: 409, exit: 3, billed: "no",
159
159
  message: "{requested} {tier} sandboxes at once is more than one run can start at once; at most {max_at_once}",
160
160
  fix: "Not billed; nothing started. Run at most {max_at_once} at once (lower `--count`), or use a smaller tier." },
161
- { code: "build_timeout", category: "other", http: 422, exit: 4, billed: "no",
161
+ { code: "build_timeout", category: "other", http: 422, exit: 4, billed: "yes",
162
162
  message: "the build ran past its {limit_min}-minute limit",
163
- fix: "Not billed; nothing stored. Shorten `setup` (at most 64 steps, {limit_min} minutes in all) or bake the slow part into the image." },
164
- { code: "dockerfile_build_failed", category: "other", http: 400, exit: 2, billed: "no",
163
+ fix: "Nothing was stored; the {limit_min} minutes it ran are billed. Shorten `setup` (at most 64 steps, {limit_min} minutes in all) or bake the slow part into the image." },
164
+ { code: "dockerfile_build_failed", category: "other", http: 400, exit: 2, billed: "yes",
165
165
  message: "your image could not be built from the Dockerfile: {detail}",
166
- fix: "Nothing was stored and nothing was billed. Fix the Dockerfile (the lines above are the end of your own build output) and run `promigence snapshot create --dockerfile PATH ...` again." },
166
+ fix: "Nothing was stored; the time the build ran is billed. Fix the Dockerfile (the lines above are the end of your own build output) and run `promigence snapshot create --dockerfile PATH ...` again." },
167
167
  { code: "image_unresolvable", category: "other", http: 422, exit: 2, billed: "no",
168
168
  message: "`{image}` could not be resolved to a digest: {detail}",
169
169
  fix: "Nothing was stored and nothing was billed. Check the name and tag; a private image needs `registry_secret` naming a stored secret that holds `user:password` (`promigence secrets set`). You can also pass the image already pinned as NAME@sha256:<digest>." },
@@ -263,9 +263,9 @@ export const CLIENT_ERRORS = [
263
263
  { code: "env_failed", category: "env", http: 0, exit: 3, billed: "no",
264
264
  message: "the sandbox ended before hand-over",
265
265
  fix: "Ours, not billed. The run's report names the failure's own code (`promigence run report RUN`); start the sandbox again." },
266
- { code: "snapshot_rejected", category: "other", http: 0, exit: 6, billed: "no",
266
+ { code: "snapshot_rejected", category: "other", http: 0, exit: 6, billed: "unless_ours",
267
267
  message: "a build guard refused it; nothing was stored",
268
- fix: "Not charged. Read the build log (`promigence snapshot status BUILD --log 80`): the guard named in the error says what to change (verify exited non-zero, a setup step failed, a time or size bound), then build again." },
268
+ fix: "The time the build ran is billed, unless it ended on our side. Read the build log (`promigence snapshot status BUILD --log 80`): the guard named in the error says what to change (verify exited non-zero, a setup step failed, a time or size bound), then build again." },
269
269
  { code: "build_failed", category: "env", http: 0, exit: 3, billed: "no",
270
270
  message: "the build could not finish; nothing was stored",
271
271
  fix: "Not charged. Build again; if it repeats, email support@promigence.ai with the build id." },
@@ -291,7 +291,7 @@ export const TIERS = [
291
291
  { name: "bench-8x16", vcpus: 8, mem_gib: 16, disk_gib: 20, sellable: false, per_second_usd: 0 },
292
292
  { name: "bench-4x8", vcpus: 4, mem_gib: 8, disk_gib: 40, sellable: false, per_second_usd: 0 },
293
293
  ];
294
- export const BUILD_PRICE = 1;
294
+ export const BUILD_MAX_S = 3600;
295
295
  export const PRICING = {
296
296
  vcpu_second_usd: 0.000014,
297
297
  gib_second_usd: 0.0000045,
package/dist/github.js CHANGED
@@ -119,7 +119,7 @@ const githubStatus = async (ctx) => {
119
119
  lines.push("pending:", ...s.pending.map((p) => ` installation ${p.installation_id}${p.account ? ` (${p.account})` : ""}: ${p.message}`));
120
120
  }
121
121
  if (s.tracks.length)
122
- lines.push("tracked branches (each push rebuilds, at the build price):", ...s.tracks.map(trackLine));
122
+ lines.push("tracked branches (each push rebuilds, billed per second):", ...s.tracks.map(trackLine));
123
123
  const next = s.connections.length
124
124
  ? nexts(["promigence repos", "the repositories the connection can read"])
125
125
  : nexts(["promigence github connect", "install the App on your GitHub account"]);
@@ -289,12 +289,12 @@ export async function githubCreate(ctx) {
289
289
  };
290
290
  if (track) {
291
291
  const branch = ref ?? "the default branch";
292
- const each = await buildPrice(client(ctx));
292
+ const each = (await buildPrice(client(ctx), tier)).most_usd;
293
293
  await write(process.stderr, ctx.mode === "json"
294
294
  ? jsonLine(process.stderr, { hint: "track", repo, branch, alias, build_price_each: each, unit: "USD" })
295
295
  : glPath
296
- ? `tracking: every new commit on ${branch} (checked every minute) rebuilds this snapshot and moves ${alias} to it, at the build price ($${each.toFixed(2)} per build), charged to your org; stop with \`${sh `promigence gitlab untrack ${alias}`}\`\n`
297
- : `tracking: every push to ${branch} rebuilds this snapshot and moves ${alias} to it, at the build price ($${each.toFixed(2)} per build), charged to your org; stop with \`${sh `promigence github untrack ${alias}`}\`\n`);
296
+ ? `tracking: every new commit on ${branch} (checked every minute) rebuilds this snapshot and moves ${alias} to it, each build billed per second while it runs (up to $${each.toFixed(2)}), charged to your org; stop with \`${sh `promigence gitlab untrack ${alias}`}\`\n`
297
+ : `tracking: every push to ${branch} rebuilds this snapshot and moves ${alias} to it, each build billed per second while it runs (up to $${each.toFixed(2)}), charged to your org; stop with \`${sh `promigence github untrack ${alias}`}\`\n`);
298
298
  }
299
299
  return { opts, label, submitted, tier };
300
300
  }
package/dist/gitlab.js CHANGED
@@ -120,7 +120,7 @@ const gitlabStatus = async (ctx) => {
120
120
  lines.push("waiting for an owner's confirmation:", ...s.pending.map((p) => ` ${p.username} (${p.connection_id}): ${p.message}`));
121
121
  }
122
122
  if (s.tracks.length) {
123
- lines.push("tracked branches (each new commit rebuilds, at the build price; checked every minute):", ...s.tracks.map(trackLine));
123
+ lines.push("tracked branches (each new commit rebuilds, billed per second; checked every minute):", ...s.tracks.map(trackLine));
124
124
  }
125
125
  if (s.connections.length) {
126
126
  lines.push("optional, for instant rebuilds: a project webhook (push events) with each connection's URL and token in --json (webhook_url, webhook_token)");
package/dist/guide.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CLIENT_ERRORS, CLIENT_ERROR_BY_CODE, ERRORS, ERROR_BY_CODE, TIERS, BUILD_PRICE, PRICING, } from "./generated.js";
1
+ import { CLIENT_ERRORS, CLIENT_ERROR_BY_CODE, ERRORS, ERROR_BY_CODE, TIERS, BUILD_MAX_S, PRICING, } from "./generated.js";
2
2
  export const BILLED_TEXT = {
3
3
  no: "no: nothing is billed for this",
4
4
  yes: "yes: the time used is billed",
@@ -55,7 +55,7 @@ function renderCore() {
55
55
  " verify → snapshot → fork(N) → exec → validity report",
56
56
  "",
57
57
  "- **snapshot** — an image (pinned) + ordered `setup` steps + a `verify` command that, if given, must exit 0.",
58
- " If verify fails nothing is stored and nothing is charged. A branch is not a pin.",
58
+ " If verify fails nothing is stored; the build's own time is billed. A branch is not a pin.",
59
59
  "- **fork** — a warm copy of that workspace, with its `start` service ready;",
60
60
  " N at once, each checked before you get it.",
61
61
  "- **validity report** — per sandbox, and this split is the product: **env** failures are ours",
@@ -245,7 +245,8 @@ function renderCost(prices) {
245
245
  const vcpu = prices?.vcpu_second_usd ?? PRICING.vcpu_second_usd;
246
246
  const gib = prices?.gib_second_usd ?? PRICING.gib_second_usd;
247
247
  const granularity = prices?.granularity_seconds ?? PRICING.granularity_seconds;
248
- const build = prices?.build_usd ?? BUILD_PRICE;
248
+ const buildMax = prices?.build_max_s ?? BUILD_MAX_S;
249
+ const buildHours = buildMax % 3600 === 0 ? `${buildMax / 3600} h` : `${Math.ceil(buildMax / 60)} min`;
249
250
  const view = prices?.view;
250
251
  const source = prices === undefined
251
252
  ? "List prices when this CLI was built. Live, and your plan's: `promigence pricing`."
@@ -269,8 +270,10 @@ function renderCost(prices) {
269
270
  ...tiers.map((t) => `| \`${t.name}\` | ${t.vcpus} | ${t.mem_gib} GiB | ${t.disk_gib} GiB | $${t.per_second_usd} | ${perHour(t.per_second_usd)} |`),
270
271
  ...(typeof limit === "number" ? ["", `Your plan runs sandboxes of up to ${limit} vCPU.`] : []),
271
272
  "",
272
- `Building a snapshot costs $${build.toFixed(2)} once, printed on stderr **before** the build starts.`,
273
- "A build that fails `verify` or a guard stores nothing and is not charged.",
273
+ `Building a snapshot bills per second at its tier's rate while it runs, at most ${buildHours} (the most it can`,
274
+ "cost is printed on stderr **before** the build starts; what it cost, when it ends). A build that",
275
+ "fails `verify` or a guard stores nothing and is billed the time it ran; one that fails on our",
276
+ "side is not billed, and reusing an identical build is free.",
274
277
  "",
275
278
  "`env` failures (`promigence agent-guide --section errors`): one before hand-over is not billed,",
276
279
  "e.g. " + ERRORS.filter((e) => e.category === "env").slice(0, 3).map((e) => `\`${e.code}\``).join(", ") + ". After hand-over, a failure our",
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { SCHEMA, GUIDE_POINTER, GOLDEN_PATH, COMMANDS, LISTED_COMMANDS, GROUPS,
3
3
  export { ALL_ERROR_CODES, BILLED_TEXT, BUDGETS, SECTIONS, estimateTokens, guideJson, lookupError, renderGuide, renderLlmsTxt,
4
4
  renderGuide as renderAgentGuide, } from "./guide.js";
5
5
  export type { Section } from "./guide.js";
6
- export { CLIENT_ERRORS, CLIENT_ERROR_BY_CODE, ERRORS, ERROR_BY_CODE, TIERS, BUILD_PRICE, PRICING } from "./generated.js";
6
+ export { CLIENT_ERRORS, CLIENT_ERROR_BY_CODE, ERRORS, ERROR_BY_CODE, TIERS, BUILD_MAX_S, PRICING } from "./generated.js";
7
7
  export type { ErrorEntry, TierEntry } from "./generated.js";
8
8
  export { FENCE_BEGIN, FENCE_END, FENCE_MAX_LINES, fenceBody, install, spliceFence } from "./install.js";
9
9
  export type { Artifact, InstallResult } from "./install.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from "./sdk/index.js";
2
2
  export { SCHEMA, GUIDE_POINTER, GOLDEN_PATH, COMMANDS, LISTED_COMMANDS, GROUPS, GLOBAL_FLAGS, EXIT_CODES, LIVE_COMMANDS, LIVE_SUMMARY, NOT_LIVE_SUMMARY, STATUS_LINE, DESCRIPTION, compactNames, EXIT_PRECEDENCE, EXIT_PRECEDENCE_TEXT, OUTPUT_MODE_PRECEDENCE, WAIT_TIMEOUT_DEFAULT, REQUEST_TIMEOUT_DEFAULT, GUIDE_COMMON_ERRORS, findCommand, knownFlags, nearestFlag, unknownFlags, renderHelp, renderCommandHelp, signInLine, notReleasedEnvelope, notReleasedLines, } from "./surface.js";
3
3
  export { ALL_ERROR_CODES, BILLED_TEXT, BUDGETS, SECTIONS, estimateTokens, guideJson, lookupError, renderGuide, renderLlmsTxt, renderGuide as renderAgentGuide, } from "./guide.js";
4
- export { CLIENT_ERRORS, CLIENT_ERROR_BY_CODE, ERRORS, ERROR_BY_CODE, TIERS, BUILD_PRICE, PRICING } from "./generated.js";
4
+ export { CLIENT_ERRORS, CLIENT_ERROR_BY_CODE, ERRORS, ERROR_BY_CODE, TIERS, BUILD_MAX_S, PRICING } from "./generated.js";
5
5
  export { FENCE_BEGIN, FENCE_END, FENCE_MAX_LINES, fenceBody, install, spliceFence } from "./install.js";
6
6
  export { detect, gitFacts, normalizeRemote, readConfig, renderConfig, stripRemoteCredentials } from "./init.js";
7
7
  export { DEVCONTAINER_NAMES, DEVCONTAINER_SPEC, DevcontainerError, detectOne, discover, load, parseJsonc, planJson, planLines, planSetup, resolveGiven, shellQuote, snapshotEnv, snapshotUsers, substituteString, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
package/dist/init.js CHANGED
@@ -344,7 +344,7 @@ export function renderConfig(c, notes = [], build) {
344
344
  ];
345
345
  if (c.alias)
346
346
  lines.push(`alias = ${tomlString(c.alias)}`);
347
- lines.push("", "# Ordered; each must exit 0.", `setup = ${tomlArray(c.setup)}`, "", "# The command that decides whether the snapshot exists at all. If it does not exit 0,", "# nothing is stored and nothing is charged.", `verify = ${tomlString(c.verify)}`, "", "[guards]", "# Checked by the builder before anything is stored. A failed guard names itself.", `toolchain_present = ${tomlArray(c.toolchain)}`);
347
+ lines.push("", "# Ordered; each must exit 0.", `setup = ${tomlArray(c.setup)}`, "", "# The command that decides whether the snapshot exists at all. If it does not exit 0,", "# nothing is stored (the build's time is billed).", `verify = ${tomlString(c.verify)}`, "", "[guards]", "# Checked by the builder before anything is stored. A failed guard names itself.", `toolchain_present = ${tomlArray(c.toolchain)}`);
348
348
  if (c.lockfile)
349
349
  lines.push(`lockfile_unchanged = ${tomlString(c.lockfile)}`);
350
350
  lines.push("install_sane = true", "verify_warm_ratio = true", "");
package/dist/mcp.js CHANGED
@@ -138,7 +138,7 @@ export const MCP_TOOLS = [
138
138
  type: "object",
139
139
  properties: {
140
140
  snapshot: { type: "string", description: "snap_<id>, an alias, or alias@commit7. Default promigence/base. Not with image." },
141
- image: { type: "string", description: "a tag such as python:3.12 (the API pins it to a digest) or registry/name@sha256:<64 hex>. A new image's snapshot is built on first use (minutes, charged at your build price: \`promigence pricing\`) and reused free after." },
141
+ image: { type: "string", description: "a tag such as python:3.12 (the API pins it to a digest) or registry/name@sha256:<64 hex>. A new image's snapshot is built on first use (minutes, billed per second while it builds at its tier's rate: \`promigence pricing\`) and reused free after." },
142
142
  tier: { type: "string", description: "Machine size for an image's snapshot; a snapshot already has one.", enum: ["xsmall", "small", "medium", "large"] },
143
143
  timeout_s: seconds("Sandbox lifetime", "; it is killed when this expires (default 15 minutes)"),
144
144
  network,
@@ -158,7 +158,9 @@ export const MCP_TOOLS = [
158
158
  }
159
159
  const asked = typeof a["cap_usd"] === "number" ? a["cap_usd"] : undefined;
160
160
  const capped = asked !== undefined && s.cap !== undefined ? Math.min(asked, s.cap) : asked ?? s.cap;
161
- const build = typeof a["image"] === "string" && capped !== undefined ? await buildPrice(s.client()) : 0;
161
+ const build = typeof a["image"] === "string" && capped !== undefined
162
+ ? (await buildPrice(s.client(), typeof a["tier"] === "string" ? a["tier"] : undefined)).most_usd
163
+ : 0;
162
164
  let runCost = 0;
163
165
  if (typeof a["image"] === "string" && capped !== undefined) {
164
166
  const p = await livePricing(s.client());
@@ -171,7 +173,7 @@ export const MCP_TOOLS = [
171
173
  const needed = (Math.ceil((build + runCost) * 100) / 100).toFixed(2);
172
174
  throw new PromigenceError({
173
175
  code: "cap_exceeded",
174
- message: `a new image's snapshot build costs $${build.toFixed(2)}, over ${which} $${capped.toFixed(2)}` +
176
+ message: `a new image's snapshot build can cost up to $${build.toFixed(2)}, over ${which} $${capped.toFixed(2)}` +
175
177
  `${capped >= build ? ` with the sandbox's own cost (about $${runCost.toFixed(2)})` : ""}; nothing was built or started`,
176
178
  fix: `Pass snapshot instead (build it once outside the agent: ${sh `promigence snapshot create --image ${a["image"]} --alias NAME`}), or ` +
177
179
  (which === "cap_usd" ? `pass cap_usd ${needed} or more.` : `start the server with --cap ${needed} or more.`),
package/dist/pricing.d.ts CHANGED
@@ -22,17 +22,27 @@ export interface Prices {
22
22
  preemptible_vcpu_second_usd: number;
23
23
  preemptible_gib_second_usd: number;
24
24
  build_usd: number;
25
+ build_max_s: number;
25
26
  granularity_seconds: number;
26
27
  tiers: PriceTier[];
27
28
  }
28
29
  export declare function publishedPrices(): Prices;
30
+ export declare const DEFAULT_BUILD_TIER = "small";
29
31
  export declare function compiledPrices(note?: string, reason?: string): Prices;
30
32
  export declare function pricesFrom(view: PricingView): Prices;
31
33
  export declare function livePricing(c: PromigenceClient, opts?: {
32
34
  newAccount?: boolean;
33
35
  strict?: boolean;
34
36
  }): Promise<Prices>;
35
- export declare function buildPrice(c: PromigenceClient): Promise<number>;
37
+ export interface BuildPrice {
38
+ most_usd: number;
39
+ per_second_usd: number;
40
+ max_s: number;
41
+ }
42
+ export declare function buildPriceIn(p: Prices, tier?: string): BuildPrice;
43
+ export declare function buildPrice(c: PromigenceClient, tier?: string): Promise<BuildPrice>;
44
+ export declare function buildLimit(s: number): string;
45
+ export declare function buildPriceWords(p: BuildPrice, tier?: string): string;
36
46
  export declare function quickClient(ctx: Ctx): PromigenceClient;
37
47
  export declare function rate(n: number): string;
38
48
  export declare function pricingLines(p: Prices, opts?: {
package/dist/pricing.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { PromigenceClient } from "./sdk/index.js";
2
2
  import { bool, clientOpts, nexts } from "./commands.js";
3
- import { BUILD_PRICE, PRICING, TIERS } from "./generated.js";
3
+ import { BUILD_MAX_S, PRICING, TIERS } from "./generated.js";
4
4
  import { emit, okEnvelope, recordNotice } from "./io.js";
5
5
  import { usd } from "./money.js";
6
6
  export const COMPILED_NOTE = "list prices at build time; could not reach the API";
@@ -9,10 +9,12 @@ const nano = (n) => Math.round(n * 1e9) / 1e9;
9
9
  export function publishedPrices() {
10
10
  return { ...compiledPrices(PUBLISHED_NOTE), source: "published" };
11
11
  }
12
+ export const DEFAULT_BUILD_TIER = "small";
12
13
  export function compiledPrices(note = COMPILED_NOTE, reason) {
13
14
  const perSecond = (v, g) => (t) => nano(t.vcpus * v + t.mem_gib * g);
14
15
  const od = perSecond(PRICING.vcpu_second_usd, PRICING.gib_second_usd);
15
16
  const pe = perSecond(PRICING.preemptible_vcpu_second_usd, PRICING.preemptible_gib_second_usd);
17
+ const buildTier = TIERS.find((t) => t.name === DEFAULT_BUILD_TIER);
16
18
  return {
17
19
  source: "compiled",
18
20
  note,
@@ -21,7 +23,8 @@ export function compiledPrices(note = COMPILED_NOTE, reason) {
21
23
  gib_second_usd: PRICING.gib_second_usd,
22
24
  preemptible_vcpu_second_usd: PRICING.preemptible_vcpu_second_usd,
23
25
  preemptible_gib_second_usd: PRICING.preemptible_gib_second_usd,
24
- build_usd: BUILD_PRICE,
26
+ build_usd: buildTier ? nano(od(buildTier) * BUILD_MAX_S) : 0,
27
+ build_max_s: BUILD_MAX_S,
25
28
  granularity_seconds: PRICING.granularity_seconds,
26
29
  tiers: TIERS.filter((t) => t.sellable)
27
30
  .slice()
@@ -41,6 +44,7 @@ export function pricesFrom(view) {
41
44
  preemptible_vcpu_second_usd: view.rates.preemptible_vcpu_second_usd,
42
45
  preemptible_gib_second_usd: view.rates.preemptible_gib_second_usd,
43
46
  build_usd: view.rates.build_usd,
47
+ build_max_s: view.limits?.build_max_s ?? BUILD_MAX_S,
44
48
  granularity_seconds: view.granularity_seconds,
45
49
  tiers: view.tiers.map((t) => ({ ...t })),
46
50
  };
@@ -72,8 +76,22 @@ export async function livePricing(c, opts = {}) {
72
76
  return compiledPrices(COMPILED_NOTE, err?.message ?? String(e));
73
77
  }
74
78
  }
75
- export async function buildPrice(c) {
76
- return (await livePricing(c)).build_usd;
79
+ export function buildPriceIn(p, tier) {
80
+ const t = tier === undefined ? undefined : p.tiers.find((x) => x.name === tier);
81
+ const max = p.build_max_s > 0 ? p.build_max_s : BUILD_MAX_S;
82
+ if (t !== undefined) {
83
+ return { most_usd: nano(t.per_second_usd * max), per_second_usd: t.per_second_usd, max_s: max };
84
+ }
85
+ return { most_usd: p.build_usd, per_second_usd: nano(p.build_usd / max), max_s: max };
86
+ }
87
+ export async function buildPrice(c, tier) {
88
+ return buildPriceIn(await livePricing(c), tier);
89
+ }
90
+ export function buildLimit(s) {
91
+ return hours(s);
92
+ }
93
+ export function buildPriceWords(p, tier) {
94
+ return `up to ${usd(p.most_usd)} (${tier === undefined ? "" : `tier ${tier}, `}${rate(p.per_second_usd)}/s for at most ${buildLimit(p.max_s)})`;
77
95
  }
78
96
  export function quickClient(ctx) {
79
97
  const opts = clientOpts(ctx);
@@ -97,7 +115,7 @@ export function pricingLines(p, opts = {}) {
97
115
  head,
98
116
  ` rates ${rate(p.vcpu_second_usd)} per vCPU-second + ${rate(p.gib_second_usd)} per GiB-second, billed per second (rounded up to ${p.granularity_seconds} s)`,
99
117
  ` preempt. ${rate(p.preemptible_vcpu_second_usd)} per vCPU-second + ${rate(p.preemptible_gib_second_usd)} per GiB-second (preemptible runs)`,
100
- ` build ${usd(p.build_usd)} per new snapshot build, charged once it verifies`,
118
+ ` build a new snapshot build is billed per second at its tier's rate while it runs, at most ${hours(p.build_max_s)} (up to ${usd(p.build_usd)} at ${DEFAULT_BUILD_TIER}); not billed when it fails on our side`,
101
119
  ];
102
120
  if (v) {
103
121
  const eg = v.rates.egress_gb_usd;
@@ -4,9 +4,11 @@ export const ACTIONS_RUNNER_SHA256 = "70920811a4f8ad4328818682bca5c6469c1c942fab
4
4
  export const GITLAB_RUNNER_VERSION = "19.4.1";
5
5
  export const GITLAB_RUNNER_SHA256 = "5a159b36c5ff54fda9248e00bb0cdfb62c1112ab3741f5282c04a41e94da1306";
6
6
  export const GITLAB_RUNNER_DEB_SHA256 = "47d2fd991e0b17af6f85af8fe3cc0cc429aae782d4f9982724c1f11a4de4a47c";
7
+ export const RUNNER_NODE_VERSION = "v24.21.0";
8
+ export const RUNNER_NODE_SHA256 = "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6";
7
9
  export const GITLAB_HELPER_IMAGE = `registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-v${GITLAB_RUNNER_VERSION}`;
8
10
  export const GITLAB_DEFAULT_JOB_IMAGE = "ubuntu:24.04";
9
- export const RUNNER_SPEC_REV = { github: 4, gitlab: 4 };
11
+ export const RUNNER_SPEC_REV = { github: 5, gitlab: 4 };
10
12
  export const RUNNER_TIERS = ["small", "medium", "large"];
11
13
  export const GITHUB_CONFIG_PATH = "/root/.ci-runner-config";
12
14
  export const GITLAB_CONFIG_PATH = "/etc/gitlab-runner/config.toml";
@@ -79,8 +81,12 @@ export function runnerBuildSteps(s) {
79
81
  export function runnerImageSpec(kind, tier) {
80
82
  if (kind === "github") {
81
83
  const url = `https://github.com/actions/runner/releases/download/v${ACTIONS_RUNNER_VERSION}/actions-runner-linux-x64-${ACTIONS_RUNNER_VERSION}.tar.gz`;
84
+ const nodeUrl = `https://nodejs.org/dist/${RUNNER_NODE_VERSION}/node-${RUNNER_NODE_VERSION}-linux-x64.tar.xz`;
82
85
  const setup = [
83
86
  APT_STEP,
87
+ `set -eu; curl -fsSL --retry 3 -o /tmp/node.tar.xz ${nodeUrl}; sha256sum /tmp/node.tar.xz; ` +
88
+ `echo '${RUNNER_NODE_SHA256} /tmp/node.tar.xz' | sha256sum -c -; ` +
89
+ "tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 --no-same-owner; rm -f /tmp/node.tar.xz",
84
90
  "set -eu; id -u runner >/dev/null 2>&1 || useradd --create-home --shell /bin/bash --uid 1001 runner; " +
85
91
  "groupadd -f docker; usermod -aG docker runner; " +
86
92
  "printf 'runner ALL=(ALL) NOPASSWD:ALL\\nDefaults:runner !requiretty\\n' > /etc/sudoers.d/runner; " +
@@ -95,6 +101,8 @@ export function runnerImageSpec(kind, tier) {
95
101
  ];
96
102
  const verify = `set -eu; cd ${GITHUB_RUNNER_ROOT}; setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner ./config.sh --version; ` +
97
103
  "git --version; jq --version; python3 --version; lsb_release -rs; " +
104
+ `test "$(node --version)" = ${RUNNER_NODE_VERSION}; ` +
105
+ "setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner sh -c 'node --version && npm --version && npx --version'; " +
98
106
  "setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner sh -c 'docker --version && docker compose version && docker buildx version'; " +
99
107
  `sudo -n -u runner true; id runner; test -x ${GITHUB_LAUNCHER}`;
100
108
  const platform_setup = [
package/dist/runners.js CHANGED
@@ -3,10 +3,10 @@ import { chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, wr
3
3
  import { join } from "node:path";
4
4
  import { createInterface } from "node:readline";
5
5
  import { configDir, PromigenceError } from "./sdk/index.js";
6
- import { bool, client, nexts, str } from "./commands.js";
6
+ import { bool, buildPriceOf, client, nexts, str } from "./commands.js";
7
7
  import { fitLine } from "./capacity.js";
8
8
  import { TIERS } from "./generated.js";
9
- import { COMPILED_NOTE, livePricing } from "./pricing.js";
9
+ import { buildPriceIn, buildPriceWords, COMPILED_NOTE, livePricing } from "./pricing.js";
10
10
  import { emit, errorEnvelope, errorTextLines, okEnvelope, parseCount, parseDuration, parseUsd, usageError, write } from "./io.js";
11
11
  import { escapeLine, jsonLine, sh } from "./term.js";
12
12
  import { GITHUB_CONFIG_PATH, GITHUB_LAUNCHER, GITLAB_CONFIG_PATH, GITLAB_LAUNCHER, RUNNER_TIERS, isRunnerTier, runnerAlias, runnerBuildSteps, runnerImageSpec, } from "./runners-spec.js";
@@ -900,7 +900,8 @@ async function ensureSnapshot(ctx, c, kind, tier, mid) {
900
900
  throw e;
901
901
  }
902
902
  const prices = await livePricing(c);
903
- const price = usd(prices.build_usd);
903
+ const most = buildPriceIn(prices, tier);
904
+ const price = `up to ${usd(most.most_usd)}, billed per second while it builds`;
904
905
  const listed = prices.source === "compiled" ? ` (${COMPILED_NOTE})` : "";
905
906
  const yes = bool(ctx, "--yes");
906
907
  const what = `the ${kind === "github" ? "GitHub Actions" : "GitLab"} runner snapshot for ${tier} (${shape(tier)})`;
@@ -909,17 +910,21 @@ async function ensureSnapshot(ctx, c, kind, tier, mid) {
909
910
  if (!interactive) {
910
911
  if (mid)
911
912
  return undefined;
912
- throw usageError(`${what} is not built yet: building it costs ${price} once (about two minutes)${listed}`, `Pass --yes to build it, or build it first: ${sh `promigence runners ${kind} --prepare --tier ${tier} --yes`}`);
913
+ throw usageError(`${what} is not built yet: building it once costs ${price} (about two minutes)${listed}`, `Pass --yes to build it, or build it first: ${sh `promigence runners ${kind} --prepare --tier ${tier} --yes`}`);
913
914
  }
914
915
  const ok = await confirm(`${what} is not built yet. Build it now? One time, ${price}${listed}, about two minutes. [y/N] `);
915
916
  if (!ok)
916
917
  return undefined;
917
918
  }
918
919
  const b = await c.snapshots.create({ image: s.image, setup: runnerBuildSteps(s), verify: { cmd: s.verify }, tier, alias: s.alias });
919
- const charged = b.deduped || b.joined ? 0 : typeof b.build_price === "number" ? b.build_price : prices.build_usd;
920
+ const charged = b.deduped || b.joined ? 0 : typeof b.build_price === "number" ? b.build_price : most.most_usd;
921
+ const said = buildPriceOf(b);
920
922
  await write(process.stderr, ctx.mode === "json"
921
- ? jsonLine(process.stderr, { hint: "build_price", build_price: charged, unit: "USD", tier, alias: s.alias, build_id: b.build_id })
922
- : `building ${s.alias}: ${usd(charged)}${charged === 0 ? " (an identical build exists)" : ""}, build ${b.build_id}; jobs start once it is verified\n`);
923
+ ? jsonLine(process.stderr, {
924
+ hint: "build_price", build_price: charged, unit: "USD", tier, alias: s.alias, build_id: b.build_id,
925
+ ...(charged > 0 && said ? { per_second_usd: said.per_second_usd, max_seconds: said.max_s } : {}),
926
+ })
927
+ : `building ${s.alias}: ${charged === 0 ? `${usd(0)} (an identical build exists)` : said ? buildPriceWords(said) : usd(charged)}, build ${b.build_id}; jobs start once it is verified\n`);
923
928
  let build = b;
924
929
  if (!(b.deduped || b.state === "verified")) {
925
930
  const w = await c.snapshots.waitBuild(b.build_id, { waitTimeoutMs: BUILD_WAIT_MS });
@@ -933,7 +938,7 @@ async function ensureSnapshot(ctx, c, kind, tier, mid) {
933
938
  throw new PromigenceError({
934
939
  code: "env_failed",
935
940
  message: `the runner snapshot build ${b.build_id} ended ${build.state}${build.error?.message ? `: ${build.error.message}` : ""}${log.length ? ` (last log line: ${log[log.length - 1]})` : ""}`,
936
- fix: `Nothing was charged for a build that did not verify. Read ${sh `promigence snapshot status ${b.build_id} --log 80`}, and tell support@promigence.ai if it is not your network.`,
941
+ fix: `${typeof build.cost_usd === "number" && build.cost_usd > 0 ? `Nothing was stored; the time it ran was billed (${usd(build.cost_usd)}).` : "Nothing was stored or charged."} Read ${sh `promigence snapshot status ${b.build_id} --log 80`}, and tell support@promigence.ai if it is not your network.`,
937
942
  exitCode: 3,
938
943
  });
939
944
  }
@@ -1208,7 +1213,7 @@ const runnersGithub = async (ctx) => {
1208
1213
  if (snapshots.has(t))
1209
1214
  return snapshots.get(t);
1210
1215
  if (!bool(ctx, "--yes")) {
1211
- poolRef?.note(`a job asks for promigence-${t}: its runner snapshot is not built (${sh `promigence runners github --prepare --tier ${t} --yes`}, ${usd(setup.prices.build_usd)} once)`);
1216
+ poolRef?.note(`a job asks for promigence-${t}: its runner snapshot is not built (${sh `promigence runners github --prepare --tier ${t} --yes`}, once: up to ${usd(buildPriceIn(setup.prices, t).most_usd)}, billed per second)`);
1212
1217
  return undefined;
1213
1218
  }
1214
1219
  if (building.has(t))
@@ -420,7 +420,19 @@ export interface SnapshotBuild {
420
420
  /** `true` when the snapshot this build made has since been deleted. */
421
421
  snapshot_deleted?: boolean;
422
422
  tier_assigned?: string;
423
+ /**
424
+ * The most the build can cost, USD, said before it runs: `max_seconds` at `per_second_usd`. A build
425
+ * is billed per second for the time it runs; what it cost is `cost_usd` once it has ended.
426
+ */
423
427
  build_price?: number;
428
+ /** What one second of the build costs, USD: its tier's per-second rate. */
429
+ per_second_usd?: number;
430
+ /** The longest the build may run, in seconds: it is billed for at most this long. */
431
+ max_seconds?: number;
432
+ /** Once it has ended, the seconds it is billed for; 0 when it failed on our side. */
433
+ billable_seconds?: number;
434
+ /** Once it has ended, what it cost, USD; 0 when nothing was charged. */
435
+ cost_usd?: number;
424
436
  repo?: string;
425
437
  commit?: string;
426
438
  verification?: Record<string, unknown>;
@@ -847,7 +859,10 @@ export interface PricingView {
847
859
  gib_second_usd: number;
848
860
  preemptible_vcpu_second_usd: number;
849
861
  preemptible_gib_second_usd: number;
850
- /** One snapshot build, charged once it verifies. */
862
+ /**
863
+ * The most one snapshot build of the default tier can cost (`limits.build_max_s` seconds at that
864
+ * tier's rate). A build is billed per second at its own tier's rate for the time it runs.
865
+ */
851
866
  build_usd: number;
852
867
  /** Per GiB of data out above the month's allowance; `null` when not priced. */
853
868
  egress_gb_usd: number | null;
@@ -872,6 +887,8 @@ export interface PricingView {
872
887
  max_egress_gb_month: number | null;
873
888
  /** Outbound mail ports open. */
874
889
  smtp: boolean;
890
+ /** The longest a snapshot build may run, in seconds: the most one build is billed for. */
891
+ build_max_s?: number;
875
892
  };
876
893
  /** The free credit a new account gets (an account's own: `GET /credits/offer`). */
877
894
  free_credit: {
@@ -1447,7 +1464,11 @@ export interface PromigenceClientOpts {
1447
1464
  * From the CLI: `promigence --request-timeout DUR` or `PROMIGENCE_REQUEST_TIMEOUT`.
1448
1465
  */
1449
1466
  requestTimeoutMs?: number;
1450
- /** Retries on 429/503 only (default 3). */
1467
+ /**
1468
+ * How many times a request is sent again (default 3): after a 429 or a 503, and after a 502 or
1469
+ * 504 that is not the API's own answer when sending twice is harmless (a read, a delete, or a
1470
+ * create with its idempotency key).
1471
+ */
1451
1472
  maxRetries?: number;
1452
1473
  /** Backoff base when the server sends no `Retry-After` (default 250 ms). */
1453
1474
  retryBaseMs?: number;
@@ -1885,7 +1906,7 @@ interface RequestOpts {
1885
1906
  signal?: AbortSignal;
1886
1907
  /** Skip the Authorization header (unauthenticated routes). */
1887
1908
  anonymous?: boolean;
1888
- /** Called with the response headers of the attempt that answered (not of a retried 429/503). */
1909
+ /** Called with the response headers of the attempt that answered (not of one that was retried). */
1889
1910
  onHeaders?: (headers: Headers) => void;
1890
1911
  /** Extra request headers (e.g. `promigence-replaced-by`). Never the authorization. */
1891
1912
  headers?: Record<string, string>;
@@ -1960,8 +1981,10 @@ export declare class PromigenceClient {
1960
1981
  * rule `request()` applies, for the calls that cannot go through it (byte transfers, the
1961
1982
  * opening of an exec stream, the terminal handshake). Retried: a 429, and a 503 that says
1962
1983
  * nothing started — any 503 for a request safe to send twice, else only one carrying a
1963
- * `Retry-After` (every refusal at the door sets one) or a refusal code. At most `maxRetries`
1964
- * times, and never a `Retry-After` longer than MAX_RETRY_SLEEP_MS: that one is the caller's.
1984
+ * `Retry-After` (every refusal at the door sets one) or a refusal code — and, for a request
1985
+ * safe to send twice, a 502 or 504 that is not the API's own answer (it carries no error
1986
+ * code). At most `maxRetries` times, and never a `Retry-After` longer than
1987
+ * MAX_RETRY_SLEEP_MS: that one is the caller's.
1965
1988
  */
1966
1989
  private refusalBackoff;
1967
1990
  /** A long-poll request must outlive its own `wait_ms`; everything else keeps the default. */
@@ -1998,7 +2021,7 @@ export declare class PromigenceClient {
1998
2021
  }): Promise<StatusResponse>;
1999
2022
  /**
2000
2023
  * `GET /pricing` — your organization's price list, live: the per-second rates, every sandbox
2001
- * shape on sale and its price, the build price, the limits, the free credit and the plan they
2024
+ * shape on sale and its price (a snapshot build bills at its tier's), the limits, the free credit and the plan they
2002
2025
  * come from, read on every call from what billing charges. Needs a key: without one this throws
2003
2026
  * `AuthError` and sends nothing. The published list prices are on the pricing page.
2004
2027
  *
@@ -837,7 +837,8 @@ export class PromigenceClient {
837
837
  refusalBackoff(status, retryAfterMs, code, attempt, replayable, signal) {
838
838
  if (attempt >= this.maxRetries || signal?.aborted)
839
839
  return undefined;
840
- if (status !== 429 && status !== 503)
840
+ const gateway = (status === 502 || status === 504) && replayable && code === undefined;
841
+ if (status !== 429 && status !== 503 && !gateway)
841
842
  return undefined;
842
843
  if ((retryAfterMs ?? 0) > MAX_RETRY_SLEEP_MS)
843
844
  return undefined;
@@ -960,12 +961,17 @@ export class PromigenceClient {
960
961
  throw new UnreachableError(`cannot reach ${url}: ${transportDetail(e)}`);
961
962
  }
962
963
  const retryAfterMs = parseRetryAfter(headerOf(res, "retry-after"));
963
- let retryable = (res.status === 429 || res.status === 503) && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS;
964
+ const gateway = (res.status === 502 || res.status === 504) && replayable;
965
+ let retryable = (res.status === 429 || res.status === 503 || gateway) && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS;
964
966
  let body;
965
967
  if (retryable && res.status === 503 && !replayable && retryAfterMs === undefined) {
966
968
  body = await res.text().catch(() => "");
967
969
  retryable = REFUSED_503_CODES.has(errorCodeOf(body) ?? "");
968
970
  }
971
+ if (retryable && gateway) {
972
+ body = await res.text().catch(() => "");
973
+ retryable = errorCodeOf(body) === undefined;
974
+ }
969
975
  if (retryable && attempt < this.maxRetries) {
970
976
  const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
971
977
  const jitter = Math.random();
@@ -973,6 +979,8 @@ export class PromigenceClient {
973
979
  ? retryAfterMs + jitter * Math.min(250, this.retryBaseMs)
974
980
  : backoff * (0.5 + jitter * 0.5);
975
981
  this.stats.retries++;
982
+ if (gateway && opts.method === "DELETE")
983
+ resentDelete = true;
976
984
  if (body === undefined)
977
985
  try {
978
986
  await res.text();
@@ -1,5 +1,5 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.8";
2
+ export declare const VERSION = "0.1.0-beta.9";
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";
@@ -14,12 +14,14 @@ export declare const WAIT_TIMEOUT_MS = 100000;
14
14
  */
15
15
  export declare const IMAGE_BUILD_TIMEOUT_MS: number;
16
16
  /**
17
- * The list price of one new snapshot build this SDK was built with, in USD (charged once it
18
- * verifies; a dedupe or a join is free). Prices change: the live one is `client.pricing()`'s
19
- * `rates.build_usd` (this key's organization's), and a build's own arrives as `build_price` in
20
- * the `POST /snapshots` answer. This is only the fallback when the API cannot be asked.
17
+ * The most one new snapshot build of the default tier can cost at the list prices this SDK was
18
+ * built with, in USD: its time limit (an hour) at that tier's per-second rate. A build is billed per
19
+ * second for the time it runs; reusing or joining an identical build is free. Prices change: the
20
+ * live figure is `client.pricing()`'s `rates.build_usd` (this key's organization's), and a build's
21
+ * own arrives as `build_price` in the `POST /snapshots` answer. This is only the fallback when the
22
+ * API cannot be asked.
21
23
  */
22
- export declare const IMAGE_BUILD_LIST_PRICE = 1;
24
+ export declare const IMAGE_BUILD_LIST_PRICE = 0.1656;
23
25
  /** Inline output budget for an exec before `truncated` + `_ref`. */
24
26
  export declare const MAX_OUTPUT_BYTES_DEFAULT: number;
25
27
  /** One request of a `files.put`. **Not a file-size limit** — `put` loops. */
package/dist/sdk/core.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { hintArg } from "./hint.js";
2
- export const VERSION = "0.1.0-beta.8";
2
+ export const VERSION = "0.1.0-beta.9";
3
3
  export const DOCS_URL = "https://promigence.ai";
4
4
  export const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
5
5
  export const DEFAULT_API_URL = "https://api.promigence.ai/v1";
6
6
  export const KEY_CONTACT = "support@promigence.ai";
7
7
  export const WAIT_TIMEOUT_MS = 100_000;
8
8
  export const IMAGE_BUILD_TIMEOUT_MS = 15 * 60_000;
9
- export const IMAGE_BUILD_LIST_PRICE = 1.0;
9
+ export const IMAGE_BUILD_LIST_PRICE = 0.1656;
10
10
  export const MAX_OUTPUT_BYTES_DEFAULT = 64 * 1024;
11
11
  export const FILE_PUT_CHUNK_BYTES = 16 * 1024 * 1024;
12
12
  const PREVIEWS_OFF = /^previews are not enabled on this deployment\.?$/i;
@@ -34,7 +34,7 @@ export interface GithubPending {
34
34
  state: string;
35
35
  message: string;
36
36
  }
37
- /** A snapshot that follows a branch: every push builds it again (at the build price). */
37
+ /** A snapshot that follows a branch: every push builds it again (each build billed per second). */
38
38
  export interface GithubTrack {
39
39
  track_id: string;
40
40
  repo: string;
@@ -37,7 +37,7 @@ export interface GitlabPending {
37
37
  expires_at: string;
38
38
  message: string;
39
39
  }
40
- /** A snapshot that follows a GitLab branch: each new commit builds it again (at the build price). */
40
+ /** A snapshot that follows a GitLab branch: each new commit builds it again (each build billed per second). */
41
41
  export interface GitlabTrack {
42
42
  track_id: string;
43
43
  repo: string;
@@ -346,16 +346,29 @@ export interface BuildJoined {
346
346
  alias?: string | null;
347
347
  alias_applied?: boolean | null;
348
348
  /**
349
- * The alias that was asked for, when the running build carries another. It was not applied;
350
- * `warnings` says what to do once the build verifies.
349
+ * The alias that was asked for, when the build in progress was to carry another, or none.
350
+ * The build carries it now (`alias_applied: true`) and its snapshot is stored under it;
351
+ * `warnings` names the alias the build was to carry.
351
352
  */
352
353
  alias_requested?: string | null;
354
+ /**
355
+ * Once the build has ended, the seconds it is billed for: the time it ran, whether it was
356
+ * stored, its own `setup` or `verify` failed, or it ran to its time limit; 0 when it failed
357
+ * on our side. Absent while it is queued or running, and for a build priced per build.
358
+ */
359
+ billable_seconds?: number | null;
353
360
  build_id: string;
354
361
  /**
355
- * What the build cost, in USD.
362
+ * The most the build can cost, in USD, as said when it was started: `max_seconds` at
363
+ * `per_second_usd`. What it was billed is `cost_usd`.
356
364
  */
357
365
  build_price: number;
358
366
  commit: string;
367
+ /**
368
+ * Once the build has ended, what it cost in USD; 0 when nothing was charged. Absent while it
369
+ * is queued or running.
370
+ */
371
+ cost_usd?: number | null;
359
372
  /**
360
373
  * RFC 3339.
361
374
  */
@@ -378,10 +391,20 @@ export interface BuildJoined {
378
391
  * Always true here.
379
392
  */
380
393
  joined: boolean;
394
+ /**
395
+ * The longest the build may run, in seconds: it is billed for at most this long. Absent for
396
+ * a build priced per build.
397
+ */
398
+ max_seconds?: number | null;
381
399
  /**
382
400
  * What to do next, ready to run.
383
401
  */
384
402
  next: Array<string>;
403
+ /**
404
+ * What one second of the build costs, in USD: its tier's per-second rate. Absent for a build
405
+ * priced per build, before builds were billed per second.
406
+ */
407
+ per_second_usd?: number | null;
385
408
  repo: string;
386
409
  setup: Array<string>;
387
410
  /**
@@ -412,8 +435,8 @@ export interface BuildJoined {
412
435
  /**
413
436
  * True when the build finished `verified` and its verification command passed. False while
414
437
  * it is queued or running, when it failed or was rejected, and for an image built without
415
- * `verify` (state `built`: stored, but nothing was verified). Nothing is stored and nothing
416
- * is charged if the command exits non-zero.
438
+ * `verify` (state `built`: stored, but nothing was verified). Nothing is stored if the
439
+ * command exits non-zero; the time the build ran is billed.
417
440
  */
418
441
  verified: boolean;
419
442
  /**
@@ -455,7 +478,8 @@ export interface BuildQueued {
455
478
  alias?: string | null;
456
479
  build_id: string;
457
480
  /**
458
- * What the build costs, in USD, said before it runs.
481
+ * The most the build can cost, in USD, said before it runs: `max_seconds` at
482
+ * `per_second_usd`. It is billed per second for the time it runs.
459
483
  */
460
484
  build_price: number;
461
485
  commit: string;
@@ -469,10 +493,18 @@ export interface BuildQueued {
469
493
  * given, so a client can say which earlier request this repeats.
470
494
  */
471
495
  idempotent_replay_age_s?: number | null;
496
+ /**
497
+ * The longest the build may run, in seconds: it is billed for at most this long.
498
+ */
499
+ max_seconds?: number | null;
472
500
  /**
473
501
  * What to do next, ready to run.
474
502
  */
475
503
  next: Array<string>;
504
+ /**
505
+ * What one second of the build costs, in USD: its tier's per-second rate.
506
+ */
507
+ per_second_usd?: number | null;
476
508
  repo: string;
477
509
  /**
478
510
  * What it is being built from. The `kind` field says which of the three forms it is.
@@ -511,12 +543,24 @@ export interface BuildView {
511
543
  * the snapshot is deleted: the name may belong to another snapshot by then.
512
544
  */
513
545
  alias?: string | null;
546
+ /**
547
+ * Once the build has ended, the seconds it is billed for: the time it ran, whether it was
548
+ * stored, its own `setup` or `verify` failed, or it ran to its time limit; 0 when it failed
549
+ * on our side. Absent while it is queued or running, and for a build priced per build.
550
+ */
551
+ billable_seconds?: number | null;
514
552
  build_id: string;
515
553
  /**
516
- * What the build cost, in USD.
554
+ * The most the build can cost, in USD, as said when it was started: `max_seconds` at
555
+ * `per_second_usd`. What it was billed is `cost_usd`.
517
556
  */
518
557
  build_price: number;
519
558
  commit: string;
559
+ /**
560
+ * Once the build has ended, what it cost in USD; 0 when nothing was charged. Absent while it
561
+ * is queued or running.
562
+ */
563
+ cost_usd?: number | null;
520
564
  /**
521
565
  * RFC 3339.
522
566
  */
@@ -529,10 +573,20 @@ export interface BuildView {
529
573
  * RFC 3339.
530
574
  */
531
575
  finished_at?: string | null;
576
+ /**
577
+ * The longest the build may run, in seconds: it is billed for at most this long. Absent for
578
+ * a build priced per build.
579
+ */
580
+ max_seconds?: number | null;
532
581
  /**
533
582
  * What to do next, ready to run.
534
583
  */
535
584
  next: Array<string>;
585
+ /**
586
+ * What one second of the build costs, in USD: its tier's per-second rate. Absent for a build
587
+ * priced per build, before builds were billed per second.
588
+ */
589
+ per_second_usd?: number | null;
536
590
  repo: string;
537
591
  setup: Array<string>;
538
592
  /**
@@ -563,8 +617,8 @@ export interface BuildView {
563
617
  /**
564
618
  * True when the build finished `verified` and its verification command passed. False while
565
619
  * it is queued or running, when it failed or was rejected, and for an image built without
566
- * `verify` (state `built`: stored, but nothing was verified). Nothing is stored and nothing
567
- * is charged if the command exits non-zero.
620
+ * `verify` (state `built`: stored, but nothing was verified). Nothing is stored if the
621
+ * command exits non-zero; the time the build ran is billed.
568
622
  */
569
623
  verified: boolean;
570
624
  /**
@@ -2460,7 +2514,7 @@ export interface LedgerEntryView {
2460
2514
  */
2461
2515
  seconds: number;
2462
2516
  /**
2463
- * What was charged: a sandbox id or a build id.
2517
+ * What was charged: a sandbox id or a build id; for credit, what it is for (e.g. "Credit from Promigence").
2464
2518
  */
2465
2519
  subject: string;
2466
2520
  tier: string;
@@ -3103,6 +3157,11 @@ export interface PricingFreeCredit {
3103
3157
  * The limits in force.
3104
3158
  */
3105
3159
  export interface PricingLimits {
3160
+ /**
3161
+ * The longest a snapshot build may run, in seconds, and so the most seconds one build is
3162
+ * billed for.
3163
+ */
3164
+ build_max_s?: number | null;
3106
3165
  /**
3107
3166
  * A sandbox's timeout when its run names none, in seconds.
3108
3167
  */
@@ -3161,8 +3220,11 @@ export interface PricingPlan {
3161
3220
  */
3162
3221
  export interface PricingRates {
3163
3222
  /**
3164
- * One snapshot build, charged once it verifies. A build that fails `verify` stores nothing
3165
- * and is not charged; reusing an identical build is free.
3223
+ * The most one snapshot build of the default tier can cost: `limits.build_max_s` seconds at
3224
+ * that tier's per-second rate. A build is billed per second at its own tier's rate (`tiers`)
3225
+ * for the time it runs, whether it is stored, its own `setup` or `verify` fails, or it runs to
3226
+ * its time limit; a failure on our side, and reusing or joining an identical build, cost
3227
+ * nothing.
3166
3228
  */
3167
3229
  build_usd: number;
3168
3230
  /**
@@ -5201,8 +5263,7 @@ export interface SnapshotCreate {
5201
5263
  */
5202
5264
  export type SnapshotCreated = BuildQueued | BuildJoined | SnapshotDeduped;
5203
5265
  /**
5204
- * Inputs that are already built: the existing snapshot, and whether the alias that was asked
5205
- * for could be applied to it.
5266
+ * Inputs that are already built: the existing snapshot, named with the alias that was asked for.
5206
5267
  */
5207
5268
  export interface SnapshotDeduped {
5208
5269
  /**
@@ -5211,8 +5272,10 @@ export interface SnapshotDeduped {
5211
5272
  alias?: string | null;
5212
5273
  alias_applied?: boolean | null;
5213
5274
  /**
5214
- * The alias that was asked for, when it differs from the snapshot's. An unnamed snapshot
5215
- * takes the name (`alias_applied: true`); a named one keeps its own and `warnings` says so.
5275
+ * The alias that was asked for, when the snapshot did not hold it. The snapshot takes it
5276
+ * (`alias_applied: true`) as a new build would: another snapshot of the project that had it
5277
+ * keeps its row, without a name. A snapshot has one alias, so a name it had before gives
5278
+ * way, and `warnings` says which.
5216
5279
  */
5217
5280
  alias_requested?: string | null;
5218
5281
  /**
@@ -136,7 +136,7 @@ export interface SandboxCreateOpts extends SandboxOpts {
136
136
  /**
137
137
  * A container image instead of a snapshot — a tag (`python:3.12`, pinned to its digest by the
138
138
  * API) or `registry/name@sha256:<64 hex>`: the snapshot of this image is built on first use
139
- * (a new build is charged your build price once it verifies, `client.pricing()`; pass
139
+ * (a new build is billed per second while it runs, at its tier's rate, `client.pricing()`; pass
140
140
  * `allowBuild: true` to allow it) and
141
141
  * reused after (same digest + same inputs → the same snapshot). A name with no tag or digest is
142
142
  * refused: it would pin whatever `:latest` is today. The fields below shape that snapshot and
@@ -156,17 +156,17 @@ export interface SandboxCreateOpts extends SandboxOpts {
156
156
  buildTimeoutMs?: number;
157
157
  /**
158
158
  * Let `Sandbox.create({ image })` build the image's snapshot when none exists yet. A new build
159
- * costs `build_price` (your plan's, `client.pricing()`; charged once it verifies); reusing a snapshot already built
160
- * 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
161
161
  * image's snapshot exists is only known once a build is submitted, and a submitted build cannot
162
162
  * be withdrawn. The other way to pay once and on purpose: `promigence snapshot create --image
163
163
  * REF --alias NAME`, then `Sandbox.create("NAME")`.
164
164
  */
165
165
  allowBuild?: boolean;
166
166
  /**
167
- * Called with the `POST /snapshots` answer before any wait: `build_price`, and `deduped` or
168
- * `joined` when nothing new is charged. Without it, a new build's price is reported as a
169
- * 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.
170
170
  */
171
171
  onBuild?: (build: SnapshotBuild) => void | Promise<void>;
172
172
  /**
@@ -198,9 +198,8 @@ export interface SandboxCreateOpts extends SandboxOpts {
198
198
  */
199
199
  export declare function imageSnapshot(client: PromigenceClient, opts: Pick<SandboxCreateOpts, "image" | "setup" | "verify" | "start" | "tier" | "alias" | "registrySecret" | "buildTimeoutMs">, hooks?: {
200
200
  /**
201
- * The `POST /snapshots` answer, before any wait: `build_price` and whether it was `deduped`
202
- * or `joined` (nothing new charged). An implicit build costs money, so a caller
203
- * 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.
204
203
  */
205
204
  onBuild?: (build: SnapshotBuild) => void | Promise<void>;
206
205
  /** Ends the wait for a build at once; the build itself, once submitted, is not withdrawn. */
@@ -375,7 +374,7 @@ export declare class Sandbox {
375
374
  /**
376
375
  * Fork of 1 from a snapshot (`snap_<id>` | `<alias>` | `<alias>@<commit7>`), or from the
377
376
  * snapshot of an image: `Sandbox.create({ image: 'python:3.12', allowBuild: true })` builds it
378
- * on first use (a new build at your build price, bounded by `buildTimeoutMs`, default 15 min) and reuses it
377
+ * on first use (a new build billed per second at its tier's rate, bounded by `buildTimeoutMs`, default 15 min) and reuses it
379
378
  * free after. An image needs `allowBuild: true` and a tag or digest; without either it is
380
379
  * refused before any request.
381
380
  *
@@ -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" ? `build_price $${b.build_price.toFixed(2)}` : "charged your build price";
69
- process.emitWarning(`building a snapshot of ${image}: ${price}, charged once it verifies (later creates from this image reuse it free)`, { code: "PROMIGENCE_BUILD" });
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 charged your build price once it verifies (\`promigence pricing\` shows it; 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, ` +
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 = {}) {
package/dist/surface.js CHANGED
@@ -975,7 +975,7 @@ export const COMMANDS = [
975
975
  { name: "--idle-timeout", arg: "DUR", summary: "stop an on-demand runner no job took (90s)" },
976
976
  { name: "--kill-on-exit", summary: "Ctrl-C cancels running jobs instead of waiting" },
977
977
  { name: "--prepare", summary: "build the --tier runner snapshot, then exit" },
978
- { name: "--yes", summary: "build a missing runner snapshot (the build price, per size) without asking" },
978
+ { name: "--yes", summary: "build a missing runner snapshot (billed per second, per size) without asking" },
979
979
  { name: "--github-api", arg: "URL", summary: "GitHub Enterprise Server's API URL" },
980
980
  { name: "--poll", arg: "DUR", summary: "how often GitHub is asked for queued jobs", hidden: true },
981
981
  json,
@@ -1001,7 +1001,7 @@ export const COMMANDS = [
1001
1001
  { name: "--idle-timeout", arg: "DUR", summary: "with --api-token-env: a runner no job took exits (90s)" },
1002
1002
  { name: "--kill-on-exit", summary: "Ctrl-C cancels running jobs instead of waiting" },
1003
1003
  { name: "--prepare", summary: "build the --tier runner snapshot, then exit" },
1004
- { name: "--yes", summary: "build a missing runner snapshot (the build price) without asking" },
1004
+ { name: "--yes", summary: "build a missing runner snapshot (billed per second) without asking" },
1005
1005
  { name: "--poll", arg: "DUR", summary: "how often GitLab is asked for pending jobs", hidden: true },
1006
1006
  json,
1007
1007
  ],
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # Promigence — promigence@0.1.0-beta.8
1
+ # Promigence — promigence@0.1.0-beta.9
2
2
 
3
3
  > Verified, reproducible, forkable execution environments for agents. The CLI is the
4
4
  > `promigence` command (package `promigence`, which is also the TypeScript SDK).
@@ -127,7 +127,7 @@
127
127
 
128
128
  ---
129
129
 
130
- # Promigence — agent guide (promigence@0.1.0-beta.8)
130
+ # Promigence — agent guide (promigence@0.1.0-beta.9)
131
131
 
132
132
  Promigence is an **environment runtime**: verified, reproducible, forkable execution
133
133
  environments, billed per second (vCPU + memory). The model is one line:
@@ -135,7 +135,7 @@ environments, billed per second (vCPU + memory). The model is one line:
135
135
  verify → snapshot → fork(N) → exec → validity report
136
136
 
137
137
  - **snapshot** — an image (pinned) + ordered `setup` steps + a `verify` command that, if given, must exit 0.
138
- If verify fails nothing is stored and nothing is charged. A branch is not a pin.
138
+ If verify fails nothing is stored; the build's own time is billed. A branch is not a pin.
139
139
  - **fork** — a warm copy of that workspace, with its `start` service ready;
140
140
  N at once, each checked before you get it.
141
141
  - **validity report** — per sandbox, and this split is the product: **env** failures are ours
@@ -205,5 +205,5 @@ Highest-precedence code wins: 2 > 5 > 6 > 7/8/9/10 > 11 > 3 > 12 > 4 > 1 > 0.
205
205
  · `promigence <command> --help` · https://promigence.ai
206
206
  Same surface as MCP tools: `promigence mcp` (stdio; `claude mcp add promigence -- promigence mcp`).
207
207
 
208
- Live in v0.1.0-beta.8: login · signup · logout · auth login · keys list/create/revoke · org list/create/switch/members/invite/invites/uninvite/resend/role/remove/leave/join/audit/rename/delete · project list/create/rename/delete/cap/move · me · billing card/usage/history/portal · doctor · ping · init · examples · templates +show · agent-guide · mcp · snapshot create/status/list/inspect/verify/export/delete/builds/alias · image resolve · github connect/status/disconnect/untrack · repos · gitlab connect/status/disconnect/untrack · run +quote/report/status/cancel/kill/replay/open/captures/list · fork · exec +output/ps/stdin/kill · files put/get/ls/search/mkdir/mv/cp/chmod/rm · agent start · sandbox kill/network/shell/ssh/list/pause/resume/snapshot/info/extend/autopause/probe · secrets set/list/delete · bench burst · runners github/gitlab · status · referrals +apply · pricing · credits +claim
208
+ Live in v0.1.0-beta.9: login · signup · logout · auth login · keys list/create/revoke · org list/create/switch/members/invite/invites/uninvite/resend/role/remove/leave/join/audit/rename/delete · project list/create/rename/delete/cap/move · me · billing card/usage/history/portal · doctor · ping · init · examples · templates +show · agent-guide · mcp · snapshot create/status/list/inspect/verify/export/delete/builds/alias · image resolve · github connect/status/disconnect/untrack · repos · gitlab connect/status/disconnect/untrack · run +quote/report/status/cancel/kill/replay/open/captures/list · fork · exec +output/ps/stdin/kill · files put/get/ls/search/mkdir/mv/cp/chmod/rm · agent start · sandbox kill/network/shell/ssh/list/pause/resume/snapshot/info/extend/autopause/probe · secrets set/list/delete · bench burst · runners github/gitlab · status · referrals +apply · pricing · credits +claim
209
209
  Not on the hosted service yet: port expose/list/revoke (preview URLs).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promigence",
3
- "version": "0.1.0-beta.8",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "The fastest sandboxes for AI agents and code. Snapshot an environment once, fork it as often as you need, run commands in every copy, and get a report that tells environment failures from task failures. Billed per second, with a quote before a run and a hard spend cap. One package: the promigence command and the TypeScript SDK.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.promigence.ai",