Skip to main content

StacyVM Python SDK

Python client for StacyVM — self-hosted compute sandboxes for AI agents and code execution.

pip install stacyvm
  • Python 3.9+.
  • Sync (Client) and async (AsyncClient) APIs with identical surface.
  • Single dependency: httpx.
  • Type hints everywhere, ships py.typed.

Table of contents


Quick start

from stacyvm import Client

client = Client("http://localhost:7423")

sandbox = client.spawn(image="python:3.12")
result = sandbox.exec("python3 -c 'print(40 + 2)'")
print(result.stdout)        # "42\n"

sandbox.destroy()

Or use the context manager — auto-destroys on exit:

with client.spawn(image="python:3.12") as sb:
    sb.write_file("/app/main.py", "print('hi')")
    print(sb.exec("python3 /app/main.py").stdout)
# sandbox destroyed here, even if an exception was raised

Connecting

from stacyvm import Client

# Simplest form
client = Client("http://localhost:7423")

# With options
client = Client(
    base_url="http://localhost:7423",
    api_key=os.environ["STACYVM_API_KEY"],
    user_id="alice@example.com",   # for pool mode
    timeout=60.0,                   # seconds, applied to all HTTP requests
)

# Use as a context manager to close the underlying httpx client
with Client("http://localhost:7423") as client:
    ...
Argument Type Default Notes
base_url str http://localhost:7423 Server URL
api_key str | None None Sent as X-API-Key header
user_id str | None None Sent as X-User-ID header (pool mode)
timeout float 30.0 Per-request timeout in seconds

Spawning sandboxes

sandbox = client.spawn(
    image="python:3.12",
    provider="docker",          # override server default
    memory_mb=1024,
    vcpus=2,
    ttl="1h",                   # "30s", "5m", "2h" — Go duration syntax
    owner_id="team-a",          # optional per-owner quota identity
    metadata={"user": "alice", "task": "data-analysis"},
)

All parameters are optional. Server defaults apply when omitted.

Parameter Type Description
image str Container or VM image (default alpine:latest)
provider str | None docker, firecracker, e2b, custom, proot, mock
memory_mb int | None RAM in MB
vcpus int | None Virtual CPUs
ttl str | None Auto-destroy after this duration
owner_id str | None Owner ID for per-owner quotas when no user_id header is set
template str | None Spawn from a server-side template by name
metadata dict[str, str] | None Free-form labels

Spawn from a template directly:

sandbox = client.spawn_template("python-dev")

Preflight quota and scheduler admission without creating a sandbox:

decision = client.admission(image="python:3.12", ttl="1h")
if not decision.allowed and decision.queueable:
    print(f"Request would queue because {decision.reason}")

Executing commands

result = sandbox.exec("python3 -c 'print(40+2)'")
# ExecResult(exit_code=0, stdout='42\n', stderr='', duration='127ms')

print(result.exit_code)
print(result.stdout)
print(result.duration)

With options:

result = sandbox.exec(
    "npm test",
    args=["--coverage"],            # appended to command
    env={"NODE_ENV": "test"},       # additional env vars
    workdir="/app",                 # cwd
    timeout="30s",                  # server-side kill after this duration
)
Parameter Type Description
command str Command to run
args list[str] | None Arguments appended to the command
env dict[str, str] | None Extra environment variables
workdir str | None Working directory inside the sandbox
timeout str | None Server-enforced timeout (Go duration string)

Streaming output

For long-running commands, stream stdout/stderr as it arrives. The server emits NDJSON; the SDK parses it into StreamChunk objects.

for chunk in sandbox.exec_stream("pip install pandas"):
    if chunk.stream == "stdout":
        print(chunk.data, end="")
    else:
        print(chunk.data, end="", file=sys.stderr)

exec_stream accepts the same arguments as exec except timeout (the connection itself bounds the session).


File operations

The SDK exposes the full file API. Paths are absolute inside the sandbox.

# Write
sandbox.write_file("/app/main.py", "print('hi')")
sandbox.write_file("/app/run.sh",  "#!/bin/sh\necho hi", mode="755")

# Read
code = sandbox.read_file("/app/main.py")

# List a directory
entries = sandbox.list_files("/app")
# [{"name": "main.py", "path": "/app/main.py", "size": 11, "is_dir": False, ...}, ...]

# Glob
tests = sandbox.glob_files("/app/**/*.test.py")

# Stat
info = sandbox.stat_file("/app/main.py")

# Move / rename
sandbox.move_file("/app/main.py", "/app/entry.py")

# Permissions
sandbox.chmod_file("/app/run.sh", "755")

# Delete
sandbox.delete_file("/app/temp.log")
sandbox.delete_file("/app/cache", recursive=True)
Method Returns Notes
write_file(path, content, mode=None) None mode is an octal string like "755"
read_file(path) str UTF-8 only — for binary, hit the REST API directly
list_files(path="/") list[dict] Directory entries
delete_file(path, recursive=False) None Set recursive=True for directories
move_file(old_path, new_path) None Move or rename
chmod_file(path, mode) None Octal string
stat_file(path) dict Single-entry metadata
glob_files(pattern) list[str] Matched paths

Live preview URLs

If your sandbox runs an HTTP server, get a public URL for any port. Backed by Traefik — see the main README.

sb = client.spawn(image="python:3.12")
sb.write_file("/app/server.py", """
from http.server import HTTPServer, BaseHTTPRequestHandler
class H(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200); self.end_headers(); self.wfile.write(b'hi')
HTTPServer(('0.0.0.0', 3000), H).serve_forever()
""")
sb.exec("python3 /app/server.py &")

print(sb.get_preview_url(3000))
# http://3000-sb-a1b2c3d4.localhost                  (local)
# https://3000-sb-a1b2c3d4.stacyide.xyz              (production)

get_preview_url is a regular synchronous method — it builds the URL from the sandbox ID and the server's preview_domain setting. Available on both Sandbox and AsyncSandbox.


Templates

Templates are server-side blueprints. Define once, spawn many times.

from stacyvm import Template

# Save a template (create or update)
client.templates.save(Template(
    name="python-dev",
    image="python:3.12-slim",
    memory_mb=1024,
    vcpus=2,
    ttl="1h",
    metadata={"language": "python"},
))

# Spawn from template
sb = client.spawn(template="python-dev")
# or:
sb = client.spawn_template("python-dev")

# List / get / delete
for t in client.templates.list():
    print(t.name, t.image)

client.templates.get("python-dev")
client.templates.delete("python-dev")

TTL and lifecycle

Sandboxes auto-destroy after their TTL expires. Extend mid-run for long jobs.

sandbox.extend_ttl("1h")       # bump by 1 hour
sandbox.extend_ttl()           # default: "30m"

sandbox.destroy()              # immediate teardown

# Refresh cached info from the server
sandbox.refresh()
print(sandbox.state)           # up-to-date state

Reattach to an existing sandbox by ID:

sb = client.get("sb-a1b2c3d4")
sb.exec("ls /app")

Pool mode (multi-user)

When the server runs in pool mode (multiple users sharing each VM), pass user_id so the server scopes the workspace correctly.

client = Client(
    base_url="https://stacy.example.com",
    user_id="alice@example.com",
)

sb = client.spawn(image="python:3.12")
# sandbox now lives under /workspace/alice@example.com/ inside a shared VM

print(client.pool_status())
# {"enabled": True, "vms": 3, "max_vms": 20, "total_users": 14, "max_users_per_vm": 5}

Async usage

AsyncClient mirrors Client exactly — every method is async, every sandbox is an AsyncSandbox.

import asyncio
from stacyvm import AsyncClient

async def main():
    async with AsyncClient("http://localhost:7423") as client:
        async with await client.spawn(image="python:3.12") as sb:
            result = await sb.exec("python3 -c 'print(\"hi\")'")
            print(result.stdout)

            # Streaming
            async for chunk in sb.exec_stream("pip install pandas"):
                print(chunk.data, end="")

asyncio.run(main())

Behavioural notes:

  • AsyncClient and AsyncSandbox support async with for cleanup.
  • await sb.destroy() is the async teardown.
  • sb.get_preview_url(port) is sync even on AsyncSandbox — it does no I/O.

Server introspection

client.health()             # {"status": "ok", "version": "0.5.1", "uptime": "2h13m"}
client.list()               # list[SandboxInfo] — all active sandboxes
client.pool_status()        # pool VM and user counts
client.quota_summary()      # QuotaSummary — redacted owner quota policy counts
client.prune()              # int — count of expired sandboxes destroyed

Errors

All exceptions inherit from StacyVMError. Catch the base class for general handling, or specific subclasses for granular control.

from stacyvm import (
    StacyVMError,
    SandboxNotFound,
    ProviderError,
    ConnectionError,
)

try:
    sandbox.exec("python3 main.py")
except SandboxNotFound as e:
    print(f"Sandbox {e.sandbox_id} no longer exists")
except ProviderError as e:
    print(f"Provider error ({e.code}): {e}")
except ConnectionError as e:
    print(f"Network issue: {e}")
except StacyVMError as e:
    print(f"API error ({e.code}): {e}")
Exception When Properties
StacyVMError Base — any API error code, message
SandboxNotFound 404 from server sandbox_id, code="not_found"
ProviderError 5xx from server code
ConnectionError Network failure message

Type reference

Public exports from stacyvm:

from stacyvm import (
    Client,
    AsyncClient,
    Sandbox,
    AsyncSandbox,
    Template,
    TemplateManager,
    # Models
    ExecResult,
    QuotaSummary,
    SandboxInfo,
    SpawnAdmissionDecision,
    StreamChunk,
    # Exceptions
    StacyVMError,
    SandboxNotFound,
    ProviderError,
    ConnectionError,
)

Selected dataclasses:

@dataclass
class ExecResult:
    exit_code: int
    stdout: str
    stderr: str
    duration: str = ""        # e.g. "127ms"

@dataclass
class SandboxInfo:
    id: str
    state: str                # "creating" | "running" | "stopped" | "destroyed" | "error"
    provider: str
    image: str
    memory_mb: int = 512
    vcpus: int = 1
    created_at: str = ""
    expires_at: str = ""
    metadata: dict = field(default_factory=dict)
    preview_domain: str = "localhost"

@dataclass
class StreamChunk:
    stream: str               # "stdout" | "stderr"
    data: str

@dataclass
class Template:
    name: str
    image: str
    memory_mb: int = 512
    vcpus: int = 1
    ttl: str = "30m"
    provider: str = ""
    metadata: dict = field(default_factory=dict)

Examples

Working examples live in examples/python/:

  • Basic spawn → exec → destroy
  • Streaming output
  • Live preview (Flask/FastAPI inside a sandbox)
  • Pool mode with multiple users
  • Async patterns with asyncio.gather
  • Template-driven workflows

Building from source

git clone https://github.com/StacyOs/stacyvm
cd stacyvm/sdk/python
pip install -e ".[dev]"
pytest

License

Apache 2.0 — see LICENSE.

Metadata

Release files for stacyvm 0.1.1

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

Source distribution (sdist)

Source distribution for stacyvm 0.1.1
File Size Uploaded
stacyvm-0.1.1.tar.gz 23.0 kB Details

Built distribution (wheel)

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

Total release size: 39.2 kB

Release files / stacyvm-0.1.1.tar.gz

Download URL stacyvm-0.1.1.tar.gz
Size 23.0 kB
Tags Source
SHA-256 checksum
How to use checksums
ffc7d4095ee643ec59e1eb60dbd5580930e90b56ef368c792042578606f2a355
BLAKE2b-256 checksum
How to use checksums
e1bec0ab878c132f36225e189d602d952d9fdc002affd501240a57bb07d17e42
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 24, 2026.

Transparency log

Release files / stacyvm-0.1.1-py3-none-any.whl

Download URL stacyvm-0.1.1-py3-none-any.whl
Size 16.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62e70a66e818d6e29f8590cd613e6c4751eab048113f3dbe0228b0e21a1aece5
BLAKE2b-256 checksum
How to use checksums
06eca690d666b73e7e7f19f1ea1afe06df68b9f634e64cd02d34f6ab4c0e351f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

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