Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

promigence — Python SDK for Promigence

Verified, forkable sandboxes for agents, evals and RL. One runtime dependency (httpx), Python ≥ 3.9, sync and async clients, fully typed.

pip install ./promigence-<version>-py3-none-any.whl   # the package you were given (not on PyPI)
export PROMIGENCE_API_KEY=...            # or `promigence auth login`; see "Credentials" below

Quickstart

from promigence import Sandbox

image = "python:3.12"                     # the API pins a tag to its digest; NAME@sha256:… works too
# A new image's snapshot build is charged your build price (`promigence pricing`) once it verifies;
# later uses reuse it free.
# allow_build=True agrees to that; without it the create is refused before anything is sent.
with Sandbox.create(image=image, spend_cap=1, allow_build=True) as sb:
    result = sb.commands.run("python -c 'print(6 * 7)' > /tmp/answer.txt")
    print(result.exit_code)                                # 0 (non-zero raises CommandExitError)
    print(sb.files.read("/tmp/answer.txt"))                # "42\n"
# leaving the block kills the sandbox (sb.kill())

Async is the same shape: async with await AsyncSandbox.create(image=image, spend_cap=1, allow_build=True) as sb: and await sb.commands.run(...).

An image needs a tag or a digest: python alone is refused, because it would pin whatever python:latest is today (write python:latest to mean that). To pay for a build once and on purpose, build it with promigence snapshot create --image python:3.12 --alias py312 and create from the name: Sandbox.create("py312"), which never builds. on_build= receives the build's answer (build_price, deduped) before the wait; without it a new build's price is logged as a promigence warning.

The two surfaces

  • Sandbox / AsyncSandbox — a simple class for one sandbox: create, fork, connect, list, commands.run|list|kill|connect, files.read|write|remove|exists|list, pty, ports, kill, pause, resume, probe, set_timeout, is_running, get_info (get_host raises while preview URLs are not available in the private beta). A live fork works in two calls: client.sandboxes.snapshot(sandbox_id, alias=None, force=None) (it ends background execs; refused with secrets_in_snapshot unless force=True), then Sandbox.fork(snapshot_id, count=N). snapshots.bundle() is not supported from Python (export with the CLI). sandboxes.exec(..., terminal=True) does not give you a TTY (use sandboxes.pty): it makes the command own the sandbox — when it exits the sandbox is closed on its verdict, task_failed with a 7-day capture on a non-zero exit. Leave it False to keep using the sandbox.
  • Promigence / AsyncPromigence — the native client. Responses are the wire JSON (snake_case dicts, typed in promigence.types):
from promigence import Promigence

with Promigence() as client:
    build = client.snapshots.create(image="ghcr.io/org/env@sha256:...", verify="pytest -q", tier="small")
    done = client.snapshots.wait_build(build["build_id"])          # long-polled, bounded, never kills
    run = client.runs.start(done["build"]["snapshot_id"], count=50, spend_cap=5)
    client.runs.wait_done(run["run_id"])
    report = client.runs.report(run["run_id"])                     # env vs task failures, cost

Keys, organizations and projects: client.keys.list|create|revoke, client.orgs.list|set_default|create_key| members|invites|leave, client.orgs.projects.list|create|rename, and client.credits.offer(). Inviting people, roles, removals, deleting an organization or project, a project's cap, moving a project and the audit log need a signed-in person, so they are CLI commands (promigence org …, promigence project …). runs.list_page, snapshots.builds_page and billing.ledger_page return {items, next_cursor}: pass next_cursor back as cursor until it is None.

snapshots.create takes exactly one input form: image= (a tag such as python:3.12, which the API pins to its digest, or name@sha256:<digest>), dockerfile={"context": ..., "path": ...}, or repo= + a full 40-character commit= (with verify=, required for that form).

Give it a Dockerfile and we build it for you — a directory here, or a repository pinned to a full commit. Both refuse anything that moves, because a snapshot that cannot be rebuilt is not reproducible:

from promigence import pack_context

client.snapshots.create(dockerfile={"context": pack_context("./app"), "path": "ci/Dockerfile"},
                        setup=["pip install -e ."], verify="pytest -q")
client.snapshots.create(dockerfile={"context": "https://github.com/org/app@<40-char sha>"})

Live sessions

Background processes, directory listing and interactive terminals:

with Sandbox.create(image=image, spend_cap=1) as sb:
    # Background: returns a CommandHandle at once. `pid` is the exec_id (a string).
    server = sb.commands.run("python -m http.server 3000", background=True,
                             on_stdout=print, on_stderr=print)
    print([p.pid for p in sb.commands.list()])
    server.kill()                       # or sb.commands.kill(server.pid)

    print([e.name for e in sb.files.list("/work")])   # EntryInfo: type, size, permissions, …

    # Terminal over a WebSocket (stdlib client, Authorization header — never a query string).
    term = sb.pty.create(120, 40, on_data=lambda b: print(b.decode(errors="replace"), end=""))
    term.send_input("echo hi\n")
    term.resize(100, 30)
    term.eof()                          # or term.kill() (SIGKILL), term.disconnect() (SIGHUP)
    print(term.wait().exit_code)        # PtyExit: exit_code, signal, wall_ms, error
  • handle.wait() long-polls, returns a CommandResult, and raises CommandExitError on a non-zero exit — or an unknown one: an exit with no code is 128 + signal, else None, never 0.
  • A command killed at its own timeout_ms raises CommandTimeoutError, a CommandExitError with code="task_timeout", timed_out=True and a message naming the bound (exit_code stays 137).
  • Ctrl-C (or a cancelled task) during Sandbox.create ends whatever the create had started, also while its POST /runs is still unanswered, then the interruption propagates. handle.disconnect() stops reading; the process keeps running and commands.connect(pid) re-attaches with its output replayed from the first byte.
  • A background command cannot take secrets (its output is served by byte range and is not redacted); the SDK refuses it before sending, with the API's reason.
  • AsyncSandbox is the same surface with await; callbacks run on the event loop (CommandHandle polls on a daemon thread, PtySession reads on one).
  • Native client: sandboxes.exec_background, sandboxes.execs.list|get|read|signal|forget, sandboxes.ports.expose|list|revoke, sandboxes.files.list, sandboxes.pty.

Units

Every duration is milliseconds and says so (timeout_ms, wait_timeout_ms), matching the wire, on purpose: the unit is in the name.

Credentials

Resolved in this order: explicit api_key= / api_url= → PROMIGENCE_API_KEY / PROMIGENCE_API_URL → the key promigence login stored (~/.config/promigence/credentials when it is there, else the system keychain; honours XDG_CONFIG_HOME; shared with the CLI) → the compatibility variables, which only ever carry a Promigence key and are only ever used together: a key from an argument, PROMIGENCE_API_KEY or the credentials file never goes where a compatibility variable points.

A key and its URL come from the same place: the stored key goes only to the api_url stored with it (the default when there is none), and PROMIGENCE_API_KEY not to an api_url= passed without api_key=. A URL from a higher place than the key's leaves the client without a key, and every authenticated call raises InvalidArgumentError naming the fix (client.credentials_problem says why). A credentials file, or its directory, owned by another user or writable by others is ignored with a warning on the promigence logger.

Every run is capped: spend_cap= → PROMIGENCE_SPEND_CAP → your org's default_spend_cap.

Errors

Everything raised on purpose is a PromigenceError with code, message, fix, docs, status, request_id and the CLI exit_code: AuthError, NotFoundError, RateLimitError (retry_after_ms), SpendCapError, PromigenceTimeoutError, UnreachableError, InvalidArgumentError, EnvFailedError (ours; credited back when our side shows the cause), NotVerifiedError,

NotReleasedError, CommandExitError (exit_code is the command's; CommandTimeoutError when the command ran past its own timeout_ms), ApiError. An API refusal of your arguments keeps the API's code (usage) as the TypeScript SDK does.

Only 429 and 503 are retried (at most 3 times, jittered, honouring Retry-After up to 10 s).

Changes

0.1.0b7

  • A sandbox's record names the snapshot it started from (snapshot_id) and when it was created (created_at), and a reattached sandbox carries both.
  • A paused sandbox's record says when it will be deleted if it is not resumed (pause_expires_at), when a deletion date applies.
  • snapshots.create() no longer takes cpu_class: passing it raises TypeError before any request, as a removed option does.
  • snapshots.bundle() no longer calls a route the API does not serve: it raises UnsupportedError naming promigence snapshot export, which builds the bundle (a missing snapshot is still NotFoundError).
  • The result of storing a secret (SecretStored) includes scan_responses, as the API returns it.
  • The docstrings and types name only what the API returns: a sandbox's stdin errors are a 400 usage, and the response types drop fields the API never sends.
  • The version follows the npm packages (0.1.0-beta.7 there).

0.1.0b6

  • DEFAULT_SNAPSHOT's description names what the default sandbox carries. No behaviour changed.
  • The version follows the npm packages (0.1.0-beta.6 there).

0.1.0b5

  • Packing a directory for a Dockerfile build applies its .dockerignore with the build's own rules, and always sends the Dockerfile and its ignore file. .git is not sent unless the ignore file says !.git. Files that look like credentials are refused unless allow_secrets=True. A snapshot built from a packed directory gets a new id once, because what is sent changed.
  • A file write keeps the existing file's mode; a new file is 0644 unless a mode is given.
  • The version follows the npm packages (0.1.0-beta.5 there).

0.1.0b4

  • runs.start() drops a deprecated option that never had an effect and was no longer sent. Passing it now raises TypeError, which names it, before any request: remove it from the call. (inject still warns and is ignored.)
  • A sandbox's handle no longer says where it runs: that attribute of Sandbox is always None, and the constructor still accepts it and ignores it. The typed responses drop the fields the API never sends to a customer key.
  • The version follows the npm packages (0.1.0-beta.4 there).

Development

cd sdk-py && python3 -m venv .venv && .venv/bin/pip install -e '.[dev]' && .venv/bin/pytest -q

Metadata

Release files for promigence 0.1.0b7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for promigence 0.1.0b7
File Size Uploaded
promigence-0.1.0b7.tar.gz 151.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for promigence 0.1.0b7
File Interpreter ABI Platform
promigence-0.1.0b7-py3-none-any.whl Python 3 none any Details

Total release size: 314.7 kB

Release files / promigence-0.1.0b7.tar.gz

Download URL promigence-0.1.0b7.tar.gz
Size 151.8 kB
Tags Source
SHA-256 checksum
How to use checksums
9e27920bdbe311aa8ee86617f0ed02d18145ea5194611f4b08053055e03f8ed8
BLAKE2b-256 checksum
How to use checksums
1bf44d1d05eb062210e8d6b6717df9fb98bdb9aaad650bded05cdc266db4510c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / promigence-0.1.0b7-py3-none-any.whl

Download URL promigence-0.1.0b7-py3-none-any.whl
Size 163.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c2ad63e0976196d31e5e52e78476a38bd840ad6726d7dcba8aeff62df4da9d04
BLAKE2b-256 checksum
How to use checksums
5bbfb065f0e7d643dc5877d81452d7995b123ebd4d48427b3acef89dbc0b7a06
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page