Build a runtime bundle, overlay it on any Linux task image, and call a remote function with sandbox.remote.
This quickstart walks the shortest end-to-end Agentix flow:
Declare deps in a normal Python project.
agentix build packages the project + every transitive dep into a
self-contained bundle — no FROM, nothing installed at run
time, just a uv venv plus a Nix closure under /nix/....
Deploy a sandbox: the bundle is bind-mounted onto a task base image
at /nix. Different task images, same runtime.
Call a remote function with sandbox.remote(fn, …).
Docker or Podman for the build executor and local sandbox backend
uv (optional) to commit a uv.lock. With a lock the build is
reproducible (uv sync --frozen); without one the build container
resolves dependencies fresh
bundle — the bundle from agentix build. Generic and
reusable. Holds Python, agentixx, every plugin, your code, all
transitive deps under /nix/store/... with libc vendored.
image — the task-specific base the workload runs in (a
SWE-bench task image, a customer environment, plain Ubuntu, …).
DockerProvider mounts the runtime’s /nix into the task container
at sandbox-create time. The Python interpreter, the runtime server, your
code, and the plugin binaries (bash, coreutils, …) all live under
/nix/runtime/bin/ and resolve regardless of the task image’s
distribution.
This is the real in-repo example at examples/hello-world — a flat
project: one main.py and one default.nix.
hello-world/├── pyproject.toml├── uv.lock # optional — `uv lock` for reproducible builds├── default.nix # adds ripgrep (rg) to the runtime PATH├── main.py # defines hello(); also host orchestrator└── README.md
pyproject.toml
main.py
default.nix
uv.lock (optional)
[build-system]requires = ["hatchling"]build-backend = "hatchling.build"[project]name = "hello-world"version = "0.1.0"requires-python = ">=3.11"dependencies = [ "agentixx>=0.2.0", "agentix-runtime-basic", # bash + files namespaces "agentix-provider-docker", # docker/podman backend][tool.agentix]nix = "default.nix" # extra system deps for the runtime# Hatchling cannot infer root-level main.py from the project name, so# include it explicitly in the wheel that agentix build installs.[tool.hatch.build.targets.wheel]only-include = ["main.py"]
import subprocessfrom agentix.provider import providersfrom agentix.provider.base import SandboxConfigdef hello() -> str: """Return the bundled ripgrep version from the runtime PATH.""" proc = subprocess.run( ["rg", "--version"], check=True, capture_output=True, text=True ) return proc.stdout.splitlines()[0]
rg is on the sandbox PATH because default.nix adds it. The same
hello() runs once on the host and once inside the sandbox.
[tool.agentix] nix = "default.nix" points the builder at this file.
The builder hands it the pinned Nixpkgs and merges the result into
/nix/runtime/bin.
A committed lock makes the build reproducible. Generate it with:
uv lock
Without a uv.lock, agentix build still works — the build container
resolves dependencies fresh instead of running uv sync --frozen.
cd examples/hello-worlduv syncuv run agentix build . --output dist/hello-world.bundle.tarBUNDLE=$(uv run agentix deploy docker dist/hello-world.bundle.tar --format json | jq -r .bundle)
agentix deploy --format json prints
{"bundle": ..., "platform": ..., "metadata": {...}}, so the cache path
reads cleanly with jq -r .bundle instead of parsing log lines.agentix build does, in order:
Stages your project + every plugin’s default.nix (each plugin
ships its system deps next to its Python module).
Runs Nix for the interpreter/system closures and installs the Python
dependency closure with uv inside the build container:
Writes a portable bundle tar containing manifest.json + nix/.
agentix deploy docker|podman unpacks that tar into a content-addressed
host cache. Docker-compatible sandbox runs bind-mount the cached nix/
tree at /nix. The runtime is a plain uv venv at /nix/runtime/venv
plus a symlinkJoin of the Nix closures at /nix/runtime/{bin,lib,...} —
there is no pip in the bundle and nothing is installed at run time.
main.py doubles as the host orchestrator: it constructs the Docker
provider, opens a sandbox over the deployed bundle, and runs hello()
inside it with sandbox.remote(fn, …).
main.py
import asyncioimport subprocessfrom agentix.provider import providersfrom agentix.provider.base import SandboxConfigdef hello() -> str: """Return the bundled ripgrep version from the runtime PATH.""" proc = subprocess.run( ["rg", "--version"], check=True, capture_output=True, text=True ) return proc.stdout.splitlines()[0]async def main(bundle: str) -> None: # String-driven selection at a dynamic boundary; typed code can also # `from agentix.provider.docker import DockerProvider` and construct it directly. provider = providers().get("docker")() config = SandboxConfig( # Task base image — workload environment. image="python:3.13-slim", # Cache path captured from `agentix deploy ... --format json`. bundle=bundle, ) async with provider.session(config) as sandbox: print(f"sandbox up at {sandbox.runtime_url}") print(f"Host result: {hello()}") print(f"Sandbox result: {await sandbox.remote(hello)}")
Run it against the bundle path captured above:
uv run python main.py --bundle "$BUNDLE"
Expected output (the sandbox rg comes from default.nix):
sandbox up at http://localhost:33803Host result: ripgrep 14.1.0Sandbox result: ripgrep 14.1.0
The sandbox is a live handle — await sandbox.remote(fn, …) is the
one-call path. There is no separate RuntimeClient to open in the
common case; the provider’s session(...) yields a sandbox you call
directly, then closes the connection and deletes the container on exit.
RuntimeClient(url) still exists for advanced/direct use against a
known runtime_url (e.g. an in-process server).
# Per sandbox — task image plus the cached runtime /nix.docker run -d --name <sid> -p 127.0.0.1:<port>:<port> \ -e AGENTIX_BIND_PORT=<port> \ --mount type=bind,source="$BUNDLE"/nix,target=/nix,readonly \ --entrypoint /nix/runtime/bootstrap.sh \ python:3.13-slim
The task container sees /nix/store/... and /nix/runtime/bin/... at
their canonical paths. Python’s RPATH (baked at Nix build time) finds
its libs under /nix/store/... because that’s exactly where the
mounted cache puts them.