Skip to main content

numen-sandbox-py

Python client for the Numen sandbox API.

This is a 0.x release. Breaking changes can still ship in any version until 1.0.

Install

pip install numen-sandbox-py

Credentials

Put this file at ~/.config/numen/credentials.json:

{
  "client_id": "pcc_...",
  "client_secret": "..."
}

Quickstart

image is required. numen/base is the general platform image. The with block deletes the sandbox when the block ends. sb.delete() does the same.

from numen_sandbox_py import Client, Sandbox

with Client() as c, Sandbox.create(c, image="numen/base") as sb:
    print(sb.run(["python", "-c", "print(2+2)"]).stdout)

Concepts

A sandbox is one machine with one writable root filesystem, processes, and outbound internet access. Commands start in the workdir (/work when the image sets none). Sandbox is the handle for that machine. Client is the account: images, volumes, secrets, snapshots, tags, and the list of sandboxes.

A name is an optional lookup you choose. The service mints the id. Names are lowercase and stay until you delete the row. A second create with the same name raises NameTakenError; existing_id is the row that already has it.

Park stops running processes and keeps your files. A parked sandbox does not count against your CPU and memory. Open wakes it. Get reads the record and does not wake it. Reading or uploading files does not wake a parked sandbox.

A snapshot is the writable layer. A volume is a separate disk bind-mounted at the workdir. A secret is a named value stored for your account. Reading a secret returns its name and times, not the value. A tag points at a policy. Create the policy in the portal, then client.tags.create(name, policy_id=...). Labels are a string map you replace as a whole.

CPU is cpu_millicores. Memory is memory_bytes. Omit either one on create for 500 millicores and 4 GiB.

Where things are

Sandbox work is on Sandbox. Everything else is on Client.

Call What it does
client.list_sandboxes One page of sandboxes
client.list_sandboxes_iter Every sandbox, walking pages
client.list_images_iter Every image, walking pages
client.images.register Name an image that already exists
client.images.build Build from a Dockerfile
client.images.get / list / delete Read or remove an image
client.volumes.create / get / list / set_labels / delete Account disks
client.secrets.set / get / list / delete / list_references Named secret values
client.snapshots.get / list / set_labels / delete Saved writable layers
client.tags.create / get / list / update / delete Names that point at a policy
Sandbox.create / open / get Create, wake, or read a sandbox
sb.run / start / process / processes Run a command or keep a process
sb.upload / read / write / mkdir / rm / ls / stat / download / export_path Files on the writable root
sb.logs / logs_all Log lines
sb.park / open / get / set_size / set_labels / set_tags Lifecycle and metadata
sb.snapshot / clone / forward / wait / delete Copy, connect, wait, remove

c.build_image and c.get_secret are not methods. Use c.images.build and c.secrets.get.

Guides

Your own image

images.build sends a Dockerfile and the directory next to it. The name you give is what you later pass to create.

with Client() as c:
    c.images.build("my-agent", dockerfile="Dockerfile", context=".")
    with Sandbox.create(c, image="my-agent") as sb:
        print(sb.run(["python", "-c", "print(2+2)"]).stdout)
FROM python:3.12-slim

FROM is a public image. An image you have already built or registered, including numen/base, cannot be the base of a new one. images.register names an image you can already pull, and waits until it is ready (up to wait_sec, default 600).

Files

A relative path joins the workdir. An absolute path is on the writable root. Denied: /.numen, /proc, /sys, /dev, /run. upload writes one file, or a local directory as one tree. A directory upload replaces that guest directory. Pass overwrite=False to update only the paths you send.

sb.upload("/work/hello.py", "print('hi')\n", mode=0o755)
sb.upload("/work/app", local="./app")
print(sb.run(["python", "/work/hello.py"]).stdout)
print(sb.read("/work/hello.py"))

write applies a list, in order. Each item is {"mkdir": path}, {"remove": path, "recursive": True}, or {"write": {"path", "content", "mode"}}. mkdir creates parents. rm of a missing path succeeds unless missing_ok=False. download writes a guest file or directory onto your disk. export_path writes a tar of a subtree (gzip=True for gzip).

A process that stays up

run waits until the command exits. Omit timeout for 60 seconds. 0 does not kill the command. start returns while the process is still running.

# Returns immediately. ends_sandbox defaults to False, so this exit leaves the sandbox up.
proc = sb.start(["python", "-u", "-c", "print(input())"])
proc.write_stdin("hello\n", eof=True)  # one line, then stdin closes and the program exits
for entry in proc.follow():  # lines until that exit
    print(entry.stream, entry.line)
proc.wait()  # already exited; the sandbox is still there

follow does not kill the process. Omit its timeout, or pass 0, to read until the process exits. A timeout ends the iteration and leaves the process running. from_start=True yields output already saved for that process, then live output. text=False yields raw chunks.

Park and open

sb.park()
sb.upload("/work/note.txt", "still here\n")
sb.open()

Park keeps files. Running processes stop. Open after a park starts a fresh session on those files. Close a forward before you park, and open a new one after open().

Sizing

Omit cpu_millicores and memory_bytes on create for 500 millicores and 4 GiB. Change the size only while the sandbox is parked:

sb.park()
sb.set_size(cpu_millicores=1000, memory_bytes=8 << 30)
sb.open()

0 on set_size keeps the current value. CapacityError means there is not enough room right now. Wait and retry. retry_after_sec is set when the service tells you how long.

Snapshots and clones

snapshot parks a running sandbox if it needs to, saves the writable layer, then opens the sandbox again. name is an optional lookup.

snap = sb.snapshot(name="before-edit")
copy = Sandbox.create(c, image=sb.image, from_snapshot_id=snap.id)

clone(n) saves the writable layer, creates n sandboxes from that copy, and deletes the snapshot. Each new sandbox gets its own layer. They do not share a volume. The new sandboxes are returned. The original stays.

Volumes

vol = c.volumes.create(name="data")
with Sandbox.create(c, image="numen/base", volume_id=vol.id) as sb:
    sb.upload("/work/note.txt", "ok\n")

That sandbox's workdir is the volume. Another create with the same volume_id fails while this one is attached.

get takes an id, or name=. set_labels replaces the whole map. delete is by id.

Forwarding a port

The program must already be listening on 0.0.0.0 inside the sandbox.

proc = sb.start(["python", "-m", "http.server", "8080", "--bind", "0.0.0.0"])
with sb.forward(8080) as fwd:
    fwd.send(b"GET / HTTP/1.0\r\n\r\n")
    print(fwd.recv())

recv waits up to 30 seconds. send and recv move bytes. Closing the session does not stop the process.

Secrets and tags

c.secrets.set("api-token", "value")
meta = c.secrets.get("api-token")
print(meta.name)
c.secrets.list_references("api-token")
tag = c.tags.create("reviewed", policy_id="...")
sb.set_tags([tag.name])

get does not return the secret value. list_references returns the policy ids that use it.

set_tags replaces the sandbox's tags. Each name must already exist. tags.update changes which policy a tag points at.

Async

AsyncClient and AsyncSandbox have the same methods. await them, and use async with.

from numen_sandbox_py import AsyncClient, AsyncSandbox

async with AsyncClient() as c:
    async with await AsyncSandbox.create(c, image="numen/base") as sb:
        result = await sb.run(["python", "-c", "print(2+2)"])
        print(result.stdout)

Reference

timeout on these calls is seconds. Omit it to keep that call's default. 0 means no limit where the method says so. Client(..., timeout_ms=...) is the only place the unit is milliseconds: it limits how long the client waits for a reply.

Client

Client(base_url="", *, timeout_ms=None, credentials=None, credentials_file="", client_id="", client_secret="", token_url="", token="")

close() drops the connection. Use with Client() as c.

list_sandboxes(*, labels=None, tags=None, statuses=None, page_size=0, page_token="", timeout=None) returns one page. Labels are combined. list_sandboxes_iter(**kwargs) yields every row. list_images_iter does the same for images.

client.images

register(name, *, from_, timeout=None, wait_until_ready=True, wait_sec=600) names an existing image.

build(name, *, dockerfile="Dockerfile", context=".", timeout=None, wait_until_ready=True, wait_sec=600) builds and, by default, waits until the image is ready.

get(name), list(*, page_size=0, page_token=""), delete(name).

client.volumes

create(*, name="", labels=None) returns a volume. get(id="", *, name="") needs exactly one of id or name. list(*, labels=None, page_size=0, page_token=""). set_labels(id, labels=None) replaces the map. delete(id).

client.secrets

set(name, value) stores the value. get(name) returns the name and times, not the value. list(*, page_size=0, page_token=""). delete(name). list_references(name) returns the policy ids that use it.

client.snapshots

get(id="", *, name="") needs exactly one of id or name. list(*, labels=None, source_sandbox_id="", page_size=0, page_token=""). set_labels(id, labels=None) replaces the map. delete(id). size_bytes on a snapshot is the stored size, in bytes.

client.tags

create(name, *, policy_id=""). get(id). list(*, page_size=0, page_token=""). update(id, *, policy_id=""). delete(id).

Sandbox.create

Sandbox.create(client, *, image="", name="", env=None, workdir="", lifetime_sec=0, idle_timeout_sec=0, from_snapshot_id="", volume_id="", labels=None, tags=None, cpu_millicores=None, memory_bytes=None)

image is required. The service mints the id. name is optional. Empty workdir is the image working directory, or /work. Omit CPU or memory for 500 millicores and 4 GiB. Put files in with upload after create. A taken name raises NameTakenError. You can set idle_timeout_sec so the sandbox parks after that many seconds of inactivity. Omit it and the sandbox stays up.

Sandbox.open(client, id="", *, name="") wakes by id or name. Exactly one of them. It does not create.

Sandbox.get(client, id="", *, name="") reads by id or name and does not wake. Exactly one of them.

sb.open() wakes this sandbox. sb.get() refreshes this record and does not wake.

Properties

id, status, name, image, env, workdir, labels, tags, volume_id, from_snapshot_id, cpu_millicores, memory_bytes, lifetime_sec, idle_timeout_sec, lifetime_remaining_sec, idle_timeout_remaining_sec, exit_code.

Sandbox methods

run(argv, *, env=None, cwd="", timeout=None, stdin=None, text=True, stdout=None, stderr=None) runs a command and returns ExecResult (stdout, stderr, exit_code). text=False returns bytes. Omit timeout for 60 seconds. 0 does not kill the command. stdout and stderr can be writers that receive output as it arrives. A timeout raises CommandTimeoutError and keeps the partial output on the error.

start(argv, *, env=None, cwd="", timeout=None, service_name="", holds_idle=False, ends_sandbox=False) returns a ProcessHandle. Omit timeout for a process that stays up.

process(process_id) and processes() return handles for processes that are already there.

upload(path, content=None, *, local=None, mode=None, timeout=None, overwrite=True) takes content or local, not both. mode defaults to 0644. overwrite applies to a directory upload.

read(path, *, timeout=None) returns bytes. download(path, dest, *, timeout=None) writes them locally. ls(path, *, max_depth=1, timeout=None) lists one directory; max_depth=0 is the whole tree. stat(path) returns FileInfo (name, path, type, size, mtime_ms). export_path(path, *, dest, gzip=False, timeout=None) writes a tar.

write(ops, *, timeout=None), mkdir(path), rm(path, *, recursive=False, missing_ok=True).

logs(*, stream="", timeout=None) and logs_all(...) return log lines. logs_all walks every page.

park() stops processes and keeps files. set_size(*, cpu_millicores=0, memory_bytes=0) only while parked. set_labels(labels) and set_tags(tags) replace the whole map or list.

snapshot(*, name="", labels=None) returns a Snapshot. clone(n=1, *, concurrency=8) returns new sandboxes and deletes the temporary snapshot.

forward(port, *, base_url=None) returns a ForwardSession. wait(*, status="", timeout=60) waits until the sandbox reaches that status. delete() removes it. Deleting a sandbox that is already gone succeeds.

ProcessHandle

id, status, service_name, exit_code.

poll() reads the current status. wait(*, timeout=60) waits until it exits. A timeout raises ProcessWaitTimeoutError and leaves the process running. kill(signal=9) signals it. write_stdin(data=b"", *, eof=False) sends bytes or text. follow(*, timeout=None, from_start=False, text=True) yields lines, or raw chunks when text=False.

ForwardSession

sandbox_id. send(data) sends bytes. recv(*, timeout=30) returns bytes, or b"" when the other side closes. close() ends the session. Use with sb.forward(port) as fwd.

Result types

ExecResult has stdout, stderr, and exit_code. A non-zero exit is still a normal return. ExecBytesResult is the same with bytes. FileInfo has name, path, type, size, and mtime_ms. FileList has entries. SandboxInfo is the record create, get, park, and set_size return. Process is the record on a ProcessHandle. Image, Volume, Snapshot, Secret, and Tag are the catalog records. A list page is sandboxes, volumes, secrets, snapshots, images, or tags, plus next_page_token. Snapshot.size_bytes and Volume.size_bytes are the stored size, in bytes.

Errors

Every named error is a SandboxError. The message is message. Fields you can read: sandbox_id, path, op, op_index, process_id, request_id, status, exit_code, stdout, stderr, retry_after_sec. NameTakenError.existing_id is the row that already has the name.

Error When
NameTakenError That name is already used. Open existing_id, or pick another name.
SandboxNotFoundError No sandbox, snapshot, or volume with that id or name.
SandboxNotReadyError The call needs a different status. set_size needs the sandbox to be parked.
SandboxConflictError The sandbox is already doing that.
SandboxStateError The status does not allow this call.
CapacityError Not enough CPU, memory, disk, or free slots. Wait and retry. Use retry_after_sec when it is set.
AuthenticationError The credentials were rejected.
InvalidArgumentError A bad argument. Empty image is this error.
CommandTooLargeError The command, env, and working directory are too large.
CommandTimeoutError run killed the command because timeout elapsed. Partial output is on the error.
ProcessWaitTimeoutError wait returned before the process exited.
SandboxWaitTimeoutError wait returned before the sandbox reached the status.
FileNotFoundError That path is not there.
DirectoryNotEmptyError rm of a non-empty directory without recursive.
FileExistsError The path is already there.
FileTooLargeError The file or export is over the limit.
InvalidPathError The path is on the denylist, or it is the workdir itself on rm.
ProcessNotFoundError No process with that id.
ExecutableNotFoundError The program is not in the image.
GuestRuntimeDeadError The sandbox can no longer run commands.

Units and timeouts

Seconds on sandbox and process calls: run, start, wait, follow, upload, read, and the catalog timeout argument. timeout_ms exists only on Client, and it limits how long the client waits for a reply.

0 means no limit: run does not kill the command, and a follow or file call does not stop on its own. Omit run's timeout for 60 seconds. Omit a file call's timeout for 60 seconds. ProcessHandle.wait defaults to 60 seconds.

Metadata

Release files for numen-sandbox-py 0.24.0

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

Source distribution (sdist)

Source distribution for numen-sandbox-py 0.24.0
File Size Uploaded Provenance
numen_sandbox_py-0.24.0.tar.gz 82.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for numen-sandbox-py 0.24.0
File Interpreter ABI Platform
numen_sandbox_py-0.24.0-py3-none-any.whl Python 3 none any Details

Total release size: 172.3 kB

Release files / numen_sandbox_py-0.24.0.tar.gz

Download URL numen_sandbox_py-0.24.0.tar.gz
Size 82.3 kB
Tags Source
SHA-256 checksum
How to use checksums
9673e2a3a4b8b9636dae9a553ec9103f854c5f4c3836d4b2fc12bee09bb48e54
BLAKE2b-256 checksum
How to use checksums
e10c8feb81f69e2af17358966a66ea97a3dc65a9abfed9ba2e24f6b7503ea090
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / numen_sandbox_py-0.24.0-py3-none-any.whl

Download URL numen_sandbox_py-0.24.0-py3-none-any.whl
Size 90.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8d6d8460b30432a050296b577a2349c376590b6fe5117631633690754e52f820
BLAKE2b-256 checksum
How to use checksums
8ce5d00703965ed033e01aedee711d4f9fcfd48ae3e413a4bcb4b4e1b498cee2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for numen-sandbox-py 0.24.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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