Skip to main content

Cloud sandboxes for AI agents

Project description

Nullspace

Open-source cloud sandboxes for AI agents. Create isolated Linux environments on demand, run commands, read/write files, and expose ports — all from a few lines of Python.

PyPI License

Install

uv pip install "${NULLSPACE_SDK_INSTALL_SPEC:-nullspace-sdk}"

For private beta, NULLSPACE_SDK_INSTALL_SPEC may point at a pinned PyPI version, a hosted wheel URL, or a private repo tag supplied in the handout.

For CLI usage:

uv pip install "nullspace-sdk[cli]"

For Claude Code or Codex local-agent setup, install the MCP extra and then install project-local Nullspace docs:

uv pip install "nullspace-sdk[cli,mcp]"
nullspace docs install --agent all

Or install from source:

uv pip install -e .              # from this directory
uv pip install -e ./sdks/python  # from repo root
uv pip install -e ".[cli]"              # from this directory, with the CLI
uv pip install -e "./sdks/python[cli]"  # from repo root, with the CLI

Docs:

Quickstart

Synchronous

from nullspace import Sandbox

with Sandbox.create() as sandbox:
    # Run a command
    result = sandbox.commands.run("echo 'Hello from Nullspace!'", shell=True)
    print(result.stdout)

    # Work with files
    sandbox.files.write("/hello.txt", "world")
    print(sandbox.files.read("/hello.txt"))

    # Expose a port
    server = sandbox.commands.run(
        "python3 -m http.server 8080 --bind 0.0.0.0",
        background=True,
        shell=True,
    )
    try:
        print(sandbox.get_url(8080))
        input("Open the URL, then press Enter to stop the server and destroy the sandbox...")
    finally:
        server.kill()

Asynchronous

import asyncio
from nullspace import AsyncSandbox


async def main() -> None:
    async with await AsyncSandbox.create() as sandbox:
        result = await sandbox.commands.run("echo 'Hello from Nullspace!'", shell=True)
        print(result.stdout)

        await sandbox.files.write("/hello.txt", "world")
        print(await sandbox.files.read("/hello.txt"))


asyncio.run(main())

Use shell=True for normal authored command strings. Use args when you want exact argument boundaries without shell parsing. The SDK does not infer shell mode from a plain string, and shell=True cannot be combined with args.

Path contract note:

  • No breaking change: /workspace remains the default mutable work tree for agent and repo-style flows.
  • General filesystem APIs also accept valid sandbox-scoped absolute paths such as /tmp/..., /data/..., and /srv/app/....
  • Endpoint-specific path rules remain explicit: user-controlled cwd and mount-path inputs still reject reserved runtime paths under /workspace/.nullspace, and /context/... only exists when a source-mount capable surface provides it.

File Uploads

Use write() for in-memory strings and bytes. Use upload_file() or upload() when the source is a local file path or readable binary stream. Use upload_dir() or upload() when the source is a local directory path.

from nullspace import Sandbox

with Sandbox.create() as sandbox:
    result = sandbox.files.upload_file("./dist/app.tar.gz")
    print(result.transport, result.target_path, result.bytes_uploaded)

Without an explicit destination, result.target_path is returned as the resolved absolute sandbox path. With the default path contract, that is typically /workspace/app.tar.gz.

The SDK picks direct upload for smaller known-length files and switches to resumable upload for larger files automatically. You can force resumable mode and wire a progress callback:

from nullspace import FileUploadError, Sandbox


def on_progress(event) -> None:
    print(event.phase, event.bytes_completed, event.bytes_total, event.transport)


with Sandbox.create() as sandbox:
    try:
        sandbox.files.upload_file(
            "./dist/model.bin",
            "/data/model.bin",
            resumable=True,
            progress=on_progress,
        )
    except FileUploadError as exc:
        if exc.upload_id:
            resumed = sandbox.files.resume_upload(
                exc.upload_id,
                "./dist/model.bin",
                progress=on_progress,
            )
            print(resumed.upload_id, resumed.bytes_uploaded)
        else:
            raise

Local directory paths use resumable tar upload. upload() dispatches directory paths to upload_dir() with the default merge conflict policy:

from nullspace import Sandbox

with Sandbox.create() as sandbox:
    result = sandbox.files.upload_dir(
        "./src",
        "/data/src",
        ignore_patterns=["*.pyc", "!pkg/__init__.py"],
    )
    print(result.kind, result.file_count, result.target_path)

Directory uploads honor .nullspaceignore from the source root, append any explicit ignore_patterns after it, preserve symlinks, and do not implicitly exclude .git or other dot-directories.

If a resumable directory upload fails mid-transfer, pass the same source directory back to resume_upload():

from nullspace import FileUploadError, Sandbox

with Sandbox.create() as sandbox:
    try:
        sandbox.files.upload_dir("./src", "/data/src")
    except FileUploadError as exc:
        if exc.upload_id:
            resumed = sandbox.files.resume_upload(exc.upload_id, "./src")
            print(resumed.upload_id, resumed.bytes_uploaded)
        else:
            raise

CLI Uploads

The bundled CLI now exposes the same upload surface under nullspace sandbox upload:

nullspace sandbox upload sb_123 ./dist/app.tar.gz /data/app.tar.gz
nullspace sandbox upload sb_123 ./src /data/src --exclude '*.pyc'
nullspace sandbox upload sb_123 - /tmp/stdin.bin

Resumable failures print a concrete next command when the source can be replayed:

nullspace sandbox upload sb_123 ./big.iso --resume up_123

Use --dry-run to preview local file or directory uploads, and add --json for machine-readable output.

Authentication

Set your API key as an environment variable:

export NULLSPACE_API_KEY=ns_live_...

For the bundled CLI, you can also save an API key with:

nullspace auth login

That writes ~/.nullspace/config.json for backward compatibility. The SDK accepts explicit api_key= / base_url= arguments; without those, the SDK and CLI read environment variables, project .env, ~/.config/nullspace/config.json, then legacy ~/.nullspace/config.json. The legacy config key api_url is accepted as an alias for base_url.

Or pass it directly:

from nullspace import Sandbox

sandbox = Sandbox.create(api_key="ns_live_...")
sandbox.kill()

Auto-Resume

Use auto_resume=True with paused timeout behavior when a sandbox should wake on its public URL after hibernating:

from nullspace import Sandbox

sandbox = Sandbox.create(on_timeout="pause", auto_resume=True)
url = sandbox.get_url(8080)

After the sandbox pauses, inbound HTTP or websocket traffic to its public URL wakes it and the original request is forwarded once the resumed execution is ready. Sandbox.connect(id) is still an explicit reconnect operation: it resumes a paused sandbox ID by snapshot route and returns the new running execution. Sandbox.get_info_by_id(id) is read-only and does not wake paused sandboxes.

Features

  • On-demand sandboxes — spin up isolated Linux environments in milliseconds
  • Command execution — run shell commands and capture stdout/stderr
  • File I/O — read, write, list, and search files inside the sandbox
  • Persistent volumes — tenant-scoped shared volume objects, direct volume.files management in Python and CLI, by-name lookup, and canonical volumes=[...] sandbox mounts
  • Port exposure — expose sandbox ports via public URLs
  • PTY sessions — interactive terminal sessions over WebSocket
  • PTY identity — use session_id for reconnect and management; numeric pid remains for legacy reconnect compatibility
  • Snapshot & resume — hibernate sandboxes and resume them later
  • Fork — clone a running sandbox (unique to Nullspace)

Current Limits

  • Volumes are currently a Firecracker-only SDK surface. The Python SDK and bundled CLI expose direct volume file management plus create-time mounts.
  • Shared mounts use close-to-open visibility, atomic rename, and flock plus traditional fcntl record locks; snapshot resume and fork remount them with fresh internal leases before the new sandbox becomes ready. This remounts external shared storage; it does not make Firecracker VM memory or mutable rootfs snapshots portable across incompatible runtime hosts.
  • The canonical mount shape is volumes=[...] with ref, mount_path, optional subpath, and read_only.

Persistent Volumes

from nullspace import Sandbox, Volume

shared = Volume.create("team-data")
same_shared = Volume.from_name("team-data")

with Sandbox.create(volumes=[shared.mount("/workspace/shared")]) as sandbox:
    sandbox.files.write("/workspace/shared/hello.txt", "persistent state")
    print([attachment.mount_path for attachment in sandbox.volumes])
    print(same_shared.id)

Direct volume file management uses the same persistent data without starting a sandbox first:

from pathlib import Path
import tempfile

from nullspace import Volume

shared = Volume.from_name("team-data", create_if_missing=True)
shared.files.make_dir("/datasets")
shared.files.write("/datasets/hello.txt", "hello from direct volume access\n")

with tempfile.TemporaryDirectory() as tmpdir:
    local_file = Path(tmpdir) / "artifact.txt"
    local_file.write_text("uploaded from local disk\n", encoding="utf-8")
    shared.files.upload_file(local_file, "/datasets/artifact.txt")
    shared.files.download_file("/datasets/artifact.txt", Path(tmpdir) / "artifact-copy.txt")
    shared.files.download_dir("/datasets", Path(tmpdir) / "datasets-copy")

print(shared.files.read("/datasets/hello.txt").strip())
print(shared.files.download_url("/datasets/artifact.txt"))
nullspace volume ls-files team-data /
nullspace volume upload team-data ./dist/model.bin /models/model.bin
nullspace volume download team-data /models/model.bin ./model.bin
nullspace volume download team-data /datasets/frontend ./frontend-copy
nullspace volume download team-data /datasets/frontend ./frontend-copy.tar --archive

See the full guides:

Templates

Template build and logging are Firecracker-only. Docker is not part of the supported long-term contract for this feature.

from nullspace import Sandbox, Template, default_build_logger, wait_for_timeout

builder = (
    Template()
    .from_ubuntu_image("22.04")
    .set_runtime_envs({"HELLO": "Hello from Nullspace!"})
    .set_start_cmd(
        "echo $HELLO > /tmp/boot.log",
        readiness=wait_for_timeout(5_000),
    )
)

build = Template.build(
    builder,
    name="hello-template",
    on_log_entry=default_build_logger(),
)

# Equivalent shorthand:
build = Template.build(builder, "hello-template:stable")

with Sandbox.create(template=build.name) as sandbox:
    print(sandbox.files.read("/tmp/boot.log").strip())

Use Template.build_in_background(...) plus build.get_status(...) for background builds, and TemplateBuild.connect(...) to reconnect to an existing build. Ref-based management helpers include Template.get_tags(...), Template.assign_tags(...), and Template.remove_tag(...).

For the full template guide and migration notes, see:

Links

License

Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nullspace_sdk-0.1.2.tar.gz (372.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nullspace_sdk-0.1.2-py3-none-any.whl (169.9 kB view details)

Uploaded Python 3

File details

Details for the file nullspace_sdk-0.1.2.tar.gz.

File metadata

  • Download URL: nullspace_sdk-0.1.2.tar.gz
  • Upload date:
  • Size: 372.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nullspace_sdk-0.1.2.tar.gz
Algorithm Hash digest
SHA256 808c19562db83c816c8d654fae5f6694a703cfb0123e27e6ef81bc25b388af30
MD5 92fc4f26bafcda799165aa186420cc3b
BLAKE2b-256 39fb1ad63b15298177aa2171103789e0e7ba4ecd06c10d882f75e9a0143de998

See more details on using hashes here.

Provenance

The following attestation bundles were made for nullspace_sdk-0.1.2.tar.gz:

Publisher: publish-pypi.yml on catamaran-research/nullspace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nullspace_sdk-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: nullspace_sdk-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 169.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nullspace_sdk-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 8de79c7e01ddc7bf9a11679e994bd52afa688697cf22dcf6fe2ab761d37f5d0c
MD5 e923c3b2942e4dfb29302f864633148d
BLAKE2b-256 6d1070d76afe15ddb3aa970925288cb6fbf268639adb18458ef40ec4ab42909c

See more details on using hashes here.

Provenance

The following attestation bundles were made for nullspace_sdk-0.1.2-py3-none-any.whl:

Publisher: publish-pypi.yml on catamaran-research/nullspace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page