Skip to main content

Resoluto sandbox: store-mediated, Kata-isolated, cloud-agnostic execution substrate

Project description

resoluto-sandbox

PyPI Python License CI Docs

Run untrusted code โ€” AI-generated, third-party, or adversarial โ€” with a dedicated Linux kernel per run. resoluto-sandbox executes any program inside a Kata microVM (a hardware-virtualized guest with its own kernel, the isolation model serverless platforms use to run untrusted multi-tenant code) and moves data across the boundary through a durable store. A workload that gets root, forks a miner, or reaches for your credentials is confined to a VM with no host filesystem, no host network, and nothing you didn't hand it.

The security model is zero-trust: the workload is assumed hostile and granted nothing by default. Your program stays plain โ€” it reads argv, writes stdout/files, exits, and never imports resoluto.sandbox. A script that runs as uv run agent.py on your machine runs unchanged inside the microVM.

๐Ÿ“– Documentation ยท ๐Ÿค– llms.txt for AI agents

python status


Install

The Python package installs anywhere (Python 3.12+); its base is pydantic-only. Heavy deps are gated behind extras:

pip install resoluto-sandbox            # base โ€” pydantic only, no cloud/k8s deps
pip install "resoluto-sandbox[s3]"      # S3 / minio Conduit           (aioboto3)
pip install "resoluto-sandbox[k8s]"     # k8s backend                  (kubernetes-asyncio + aioboto3)
pip install "resoluto-sandbox[gcs]"     # GCS Conduit                  (gcloud-aio-storage)

uv is recommended for running programs/examples.

Requirements (host)

Running a sandbox needs an isolation host. The local backend (Kata microVMs) is Linux + KVM only โ€” the k8s backend's client runs anywhere (the microVMs run in your cluster).

Backend Runs the microVM on Host needs
local Linux with /dev/kvm โ€” bare metal, or a VM with nested virtualization Kata Containers, a nerdctl-full bundle (containerd + CNI), Docker (builds images), a local OCI registry, sudo
k8s your Kubernetes cluster a cluster with the Kata runtime class + an S3/minio store; resoluto-sandbox[k8s] on the client

macOS / Windows: Kata microVMs need a Linux kernel + KVM, so the local backend does not run natively. Use a Linux VM with nested virt enabled, or point the k8s backend at a remote cluster โ€” the client side runs on any OS.

local backend components (Linux)

scripts/local-backend-up.sh verifies these, then provisions the rest (dedicated containerd, CNI bridge, local.env, and a green Kata-microVM canary). Egress is runtime-managed per run (Sandbox.run(egress=[...])) โ€” the script provisions no firewall. It does not install the kernel/Kata/nerdctl for you:

Component Why How to get it
KVM (/dev/kvm) Kata boots a real VM your distro's virtualization packages (e.g. qemu-kvm/libvirt on Debian/Ubuntu, qemu on Fedora/Arch); on a VM, enable nested virtualization
Kata Containers โ†’ /opt/kata the VM runtime (kata-runtime, containerd-shim-kata-v2) static release tarball from kata-containers releases extracted to /opt/kata (verified with 3.31.0) โ€” not a distro package
nerdctl-full โ†’ /opt/resoluto-local containerd + nerdctl + CNI plugins, standalone from Docker/k3s the nerdctl-full-* release tarball from nerdctl releases (verified with 2.3.3)
Docker builds the base / provider / your own images (docker build, image build, image push) Docker Engine โ€” your distro's package or the official apt repo
OCI registry on localhost:5000 bridges Docker's image store โ†’ the sandbox's separate containerd (see Prebuilt provider images) local-backend-up.sh starts one for you if Docker is present; or run it yourself: docker run -d --restart unless-stopped -p 5000:5000 --name registry registry:2

Architecture: amd64 and arm64 Linux both work โ€” match the Kata/nerdctl tarball to your arch (the images build for the host arch automatically). Then:

bash scripts/local-backend-up.sh      # verify + provision โ†’ green canary
set -a; source local.env; set +a      # exports RESOLUTO_SANDBOX_IMAGE etc.

Quickstart

The sandbox runs your program in a Kata microVM โ€” there is no zero-isolation mode โ€” so it needs a one-time host provisioning first: /dev/kvm + Kata + a dedicated containerd + the sandbox image. The provisioning script does all of it and writes local.env (which is git-ignored and does not ship in the repo). From a repo clone:

bash scripts/local-backend-up.sh     # provisions the local backend + image, writes local.env, ends on a green canary
set -a; source local.env; set +a     # exports RESOLUTO_SANDBOX_IMAGE

uv run python examples/run_hello_in_sandbox.py   # the bare mechanics โ€” a plain program, isolated

Then, in your own code:

from resoluto.sandbox import Sandbox

result = Sandbox(backend="local").run(
    ["python", "-c", "print('hello from the sandbox')"]
)
print(result.output)   # hello from the sandbox
print(result.ok)       # True

The result captures the output, the exit code, and any files you asked to collect (output_paths). stdin is not supported โ€” pass inputs via argv, env, or workspace files.

The local backend runs in a Kata microVM and needs a sandbox image present in its dedicated containerd (not your regular Docker daemon โ€” see Prebuilt provider images for the transfer step) โ€” pass Sandbox(backend="local", image="โ€ฆ") (default resoluto-sandbox-base:<installed wheel version> โ€” default_local_image(), never a floating tag; build it from Dockerfile.base or resoluto-sandbox image build). Run argv with the guest's python and paths relative to workspace, not host absolute paths.

Run an arbitrary program, isolated โ€” the sandbox runs any untrusted program; an LLM agent is just one example. run_agent_in_sandbox.py <provider> is symmetric across every provider image the sandbox ships โ€” the name you pass selects the matching prebuilt image, payload, credential, and egress host (nothing privileges one provider). Each payload is a plain program that never imports the library:

set -a; source local.env; set +a          # provision the local Kata backend first; build the provider image too
export OPENAI_API_KEY=...                  # each provider brings its OWN credential; the sandbox just forwards it
uv run python examples/run_agent_in_sandbox.py openai "In five words, why isolate an agent?"
#   provider : openai  (resoluto-sandbox:openai-agents-0.17.7)
#   INPUT    : 'In five words, why isolate an agent?'
#   OUTPUT   : 'Untrusted code cannot escape containment.'
# ...or `claude` (CLAUDE_CODE_OAUTH_TOKEN) / `langchain` (ANTHROPIC_API_KEY) โ€” same driver, different image.

For the bare mechanics without an LLM, see examples/run_hello_in_sandbox.py. The end-to-end verification harnesses that drive a minimal agent through BOTH backends (local + k8s) live in tests/smoke/ (smoke_both_backends.py, smoke_llm.py).


The program contract

A sandbox program reads argv, writes to stdout / files, exits with a code, and never imports resoluto.sandbox. A script that works as uv run agent.py works unchanged inside the sandbox; test runners, LLM agents, and shell scripts all qualify. Dependencies are your program's concern โ€” put uv run / pip install in your argv, or bake them into the image.


How it works

The host and the sandbox never hold a live connection. They rendezvous through a durable key/value store (the Conduit): the sandbox is passive โ€” it writes append-only JSONL chunks and a final result.json; the host launches it, tails the chunks, and reaps it. The same flow works whether the sandbox is a microVM next to you or a pod in a cluster, and a network blip can't wedge a run.

Components

Component What it is What it does
Your program Any script/binary โ€” plain Reads argv/env, writes stdout/files, exits. Never imports resoluto.sandbox.
Sandbox Thin Python facade Sandbox(backend=...).run(argv, ...) โ€” one call, identical for every backend.
SubstrateBackend The one backend impl Drives the 3-phase flow (stage โ†’ run โ†’ collect). Holds one SandboxRuntime + one Conduit.
SandboxRuntime (ABC) The isolation/placement seam Launches, checks status, destroys the isolated sandbox. Impls: KataNerdctlSandboxRuntime (local), K8sSandboxRuntime (k8s).
Conduit (ABC) The durable exchange seam put / get / list_prefix on a key/value store โ€” the ONLY channel between host and sandbox. Impls: LocalConduit (bind-mounted dir), S3Conduit (minio/S3), StdoutConduit, GcsConduit (unverified).
runner_main The in-guest entrypoint Runs inside the sandbox only โ€” your program never sees it. Verifies the egress canary, stages inputs from the Conduit, execs your argv, ships spans/heartbeat/result back to the Conduit.
sandbox image The OCI image the sandbox boots Must contain your program's runtime + the resoluto-sandbox wheel. Prebuilt overlays: resoluto-sandbox:claude-agent-sdk-<ver>, :langchain-<ver>, :openai-agents-<ver> (see Images below).

Architecture โ€” local vs. k8s

Same isolation model on both backends โ€” a Kata microVM with its own guest kernel, egress denied by default until you open exactly what you need, and a fail-closed canary that verifies isolation before your program runs. Only the placement and the Conduit implementation differ:

LOCAL BACKEND  (single host, no cluster)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Host process (your code)                       โ”‚
โ”‚   Sandbox(backend="local")                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    nerdctl
                    (dedicated containerd)
                     โ”‚
                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ISOLATED: Kata microVM                         โ”‚
โ”‚ (own guest kernel)                             โ”‚
โ”‚ runner_main -> your argv                       โ”‚
โ”‚ egress: NONE by default (no network interface) โ”‚
โ”‚ open per-run -> CNI bridge + SNI proxy         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    bind mount (its own run prefix)
                     โ”‚
                     โ–ผ
LocalConduit (host dir)
K8S BACKEND  (Kubernetes cluster)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Host process (your code)                       โ”‚
โ”‚   Sandbox(backend=SubstrateBackend(            โ”‚
โ”‚     K8sSandboxRuntime(...)))                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    kube API
                    (pinned context)
                     โ”‚
                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ISOLATED: Kata microVM pod                     โ”‚
โ”‚ (own guest kernel)                             โ”‚
โ”‚ runner_main -> your argv                       โ”‚
โ”‚ egress: k8s NetworkPolicy, default-deny        โ”‚
โ”‚ (DNS + store only, until opened)               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    HTTPS (S3 API)
                     โ”‚
                     โ–ผ
S3Conduit (minio / S3 bucket)

local needs /dev/kvm + nerdctl + a dedicated containerd, provisioned by scripts/local-backend-up.sh (ends in a green Kata-microVM canary). k8s needs a cluster with Kata Containers installed, an S3-compatible store, and a pinned kube context. Full setup โ€” including the vendor-neutral k8s stack โ€” is in docs/backends.md.

Data flow โ€” the Conduit

The host and the sandbox never talk to each other directly โ€” every arrow below touches the Conduit, never the other side. That's what makes a network blip harmless: nothing is waiting on a live connection.

Phase Host does Conduit holds Sandbox does
1. Stage put_dir(workspace) โ€” writes the input archive the input archive, keyed to this run (not launched yet)
2. Run launches the sandbox, then only tails the Conduit spans + heartbeat + result.json + output archive, as runner_main writes them canary โ†’ stages inputs from the Conduit โ†’ execs your argv โ†’ ships output to the Conduit
3. Collect list_prefix + get โ†’ assembles RunResult, reaps the sandbox the final chunks, until reaped destroyed

Live output still streams to stream (default sys.stdout) as it happens during 2. Run โ€” the table above is what's durable, not the only thing you see.

Security model

Zero-trust: the workload is assumed hostile and granted nothing by default. Every crossing of the boundary is explicit.

Guarantee How
Own kernel Each run is a Kata microVM with its own Linux kernel โ€” hardware-virtualized, not a shared-kernel namespace/cgroup container. A guest-kernel exploit stops at the VM.
Unprivileged workload The program runs as a non-root user with no Linux capabilities and no privilege escalation. The optional docker-in-docker mode is guest-scoped (privileged-without-host-devices) and still drops the workload to a non-root user.
No ambient host access No host filesystem, no host devices, no host kernel memory, no host control-plane sockets (containerd/Docker), and its own PID namespace. Host credentials are never forwarded โ€” the guest gets only a scoped, short-lived store token.
Isolated per run Each run is keyed to its own store prefix. On local the guest is bind-mounted only that prefix; on k8s it holds only a prefix-scoped credential. Runs cannot read each other's state.
Default-deny egress A fresh workload has no network โ€” on local, no network interface at all; on k8s, a default-deny NetworkPolicy. You grant specific domains per run. Cloud metadata (IMDS 169.254.169.254) and RFC1918 private ranges are refused even on an allowlist match, so an opened domain can't pivot into your network.
Data in, contained out Inputs and outputs cross as data, never as control. The host never executes what the workload produces; output collection is scoped to the paths you declare and rejects path-traversal and symlink escapes.
No silent downgrade VM-grade or nothing. The Kata runtime-class guard is unconditional and egress is fail-closed; a weaker posture is only ever selected explicitly. A fail-closed canary proves isolation is in force before your program starts.

Trust assumption. The boundary is a virtual machine, so the trusted computing base is the hypervisor and the host kernel โ€” the same assumption behind any virtualization or serverless platform. Keeping the isolation host (KVM, Kata, your kernel) patched is the residual risk, and the only one.

Egress โ€” deny by default

Untrusted code should not be able to phone home, and by default it can't.

  • local โ€” the default run gets no network interface. There is nothing to reach: no internet, no DNS, no LLM, no registries. The store is a host bind mount, not a network endpoint, so a run is fully functional with zero network. Opening an allowlist attaches a NIC on an isolated bridge, routed through a built-in SNI proxy that forwards only connections whose TLS SNI matches a domain you listed.
  • k8s โ€” the pod reaches its S3 store over the network, so it always has an interface, governed by a default-deny NetworkPolicy: the store and DNS only, until you open specific domains.

Grant egress per run, by domain:

Sandbox(backend="local").run(argv)                                 # no network (default)
Sandbox(backend="local").run(argv, egress=["api.anthropic.com"])   # only Anthropic, this run
Sandbox(backend="local").run(argv, egress=["registry.npmjs.org", "*.openai.com"])

The allowlist matches by domain (TLS SNI), not IP, so it survives the address rotation of CDN-backed APIs; it does no CA/MITM. Private ranges and cloud metadata are refused even on a match. None/[] returns to the no-network default. Open registry.npmjs.org and npm install reaches the registry and nothing else; drop it and the same install fails.

run(egress=[...]) is enforced by the local backend. On k8s, egress is a per-runtime EgressConfig rendered to a default-deny NetworkPolicy; public_https=True is the explicit escape hatch that allows all outbound HTTPS for trusted code. See docs/networking.md.


Sandbox.run() reference

Sandbox(backend="local").run(
    argv,                 # program + arguments
    *,
    workspace=None,       # dir staged into the sandbox at /workspace; None = nothing staged at all
    stdin=None,           # unsupported โ€” raises NotImplementedError
    env=None,             # dict overlaid on the sandbox environment
    env_file=None,        # dotenv file merged UNDER env (env wins) โ€” convenience, not security (see below)
    secrets=None,         # dict[str, str | SecretKeyRef] โ€” see "Secrets" below
    output_paths=None,    # glob patterns collected back as artifacts
    stream=None,          # live output sink; None echoes to sys.stdout
    egress=None,          # domains allowed for THIS run (local); None/[] = no network at all
) -> RunResult

RunResult: exit_code, output, errors (always empty โ€” stdout/stderr are merged), artifacts, result (parsed result.json if the program wrote one), ok (exit_code == 0).


Secrets

Three mechanisms, for three different jobs โ€” none is a drop-in replacement for another:

Mechanism Where the plaintext lands Use for
env_file="path/to/.env" Literal env entry (pod spec / nerdctl -e) โ€” same exposure as env= Local dev config/secrets; pure convenience, not a security boundary
secrets={"VAR": SecretKeyRef("my-secret", "key")} Never touches resoluto-sandbox at all โ€” kubelet materializes it k8s only. References a Secret object that already exists (created by kubectl, External Secrets Operator, or anything else)
secrets={"VAR": "vault:secret/data/x#key"} Resolved inside the guest by a SecretProvider โ€” the host never sees the value Portable across local/k8s. Ships as an ABC only today โ€” see secrets.py; implement a concrete provider (Vault, AWS Secrets Manager, GCP Secret Manager, ...) and dispatch it in secrets_from_env()
from resoluto.sandbox import Sandbox
from resoluto.sandbox.secrets import SecretKeyRef

# env_file โ€” host reads a dotenv file, merges it into the env (env= wins on conflict)
Sandbox(backend="local").run(argv, env_file=".env")

# SecretKeyRef โ€” k8s-native, zero fetch code, ignored on the local backend
Sandbox(backend="k8s", image=...).run(argv, secrets={"ANTHROPIC_API_KEY": SecretKeyRef("anthropic-key", "api_key")})

# SecretProvider ref โ€” guest resolves it itself; host only holds a scoped RESOLUTO_SECRETS_* credential
Sandbox(backend="local").run(argv, secrets={"ANTHROPIC_API_KEY": "vault:secret/data/anthropic#api_key"})

The host never mints credentials for the SecretProvider path โ€” same posture as the Conduit's RESOLUTO_STORE_WRITE_TOKEN today: you mint an already-scoped, short-lived credential yourself (a Vault token, an AWS STS AssumeRole triple, a GCP impersonation-minted OAuth2 token โ€” never a static key file) and pass it via RESOLUTO_SECRETS_KIND/RESOLUTO_SECRETS_* env vars; resoluto-sandbox only plumbs it to the guest and calls .get(ref) right before your program execs.


CLI

resoluto-sandbox run -- echo hi                          # local backend (default)
resoluto-sandbox run --backend k8s --image <img> -- python agent.py
resoluto-sandbox run --env-file .env -- python3 agent.py  # dotenv merged into the sandbox env
resoluto-sandbox doctor                                  # check backend readiness

-- separates sandbox options from the program argv. --env-file is CLI-only for now โ€” secrets= (SecretKeyRef/SecretProvider) is a Python API construct (needs a typed value, not just a string) and has no CLI flag yet.


Prebuilt provider images

Each overlay pins one SDK version and tags itself by it โ€” the tag says exactly what's inside. image build builds with Docker and pushes to the registry the local backend pulls from (see the box below), so after it runs the image is ready โ€” no manual transfer step. Bring the backend up first (scripts/local-backend-up.sh) so the registry exists:

resoluto-sandbox image build --provider claude   # -> pushed localhost:5000/resoluto-sandbox:claude-agent-sdk-0.2.110
resoluto-sandbox image build --provider openai   # -> pushed localhost:5000/resoluto-sandbox:openai-agents-0.17.7  (also serves `openrouter`)
resoluto-sandbox image build --provider all      # base once, then every overlay
Provider Bakes Example agent Auth
claude @anthropic-ai/claude-code + claude-agent-sdk examples/payloads/claude_agent.py Claude Max/Pro subscription (claude setup-token) or ANTHROPIC_API_KEY โ€” see docs/auth.md
langchain bare langchain + langgraph โ€” no LLM integration examples/payloads/langchain_agent.py Depends which integration you add โ€” see below
openai openai-agents examples/payloads/openai_agent.py OPENAI_API_KEY โ€” pay-as-you-go API only, OPENAI_MODEL override
openrouter (reuses the openai image) examples/payloads/openai_agent.py (via OPENAI_BASE_URL) OPENROUTER_API_KEY โ€” OpenAI-compatible endpoint https://openrouter.ai/api/v1, default model mistralai/mistral-small-3.2-24b-instruct, OPENROUTER_MODEL override

The langchain image is bare on purpose. LangChain itself is provider-agnostic โ€” it has no built-in way to call an LLM. To actually use it, extend the image with the matching integration package for whichever provider you want:

FROM resoluto-sandbox:langchain-1.3.11
RUN pip install --break-system-packages langchain-anthropic   # or langchain-openai, etc.

examples/payloads/langchain_agent.py demonstrates the Anthropic integration specifically (needs langchain-anthropic + ANTHROPIC_API_KEY, model override ANTHROPIC_MODEL) โ€” it will ImportError against the plain prebuilt langchain image until you extend it this way.

Two image stores โ€” and how a built image reaches the sandbox. docker build lands the image in your regular Docker daemon. The local backend does not use Docker: it launches Kata microVMs via nerdctl against its OWN dedicated containerd (/run/resoluto-local/containerd/, set up by scripts/local-backend-up.sh) โ€” a separate image store that can't see what docker build produced. The bridge between the two is the on-box registry:

  • resoluto-sandbox image build builds with Docker and pushes to the registry (localhost:5000 by default; set RESOLUTO_SANDBOX_REGISTRY for k8s or a shared registry).
  • The examples reference the registry-qualified tag (localhost:5000/resoluto-sandbox:โ€ฆ, via images.pullable()), and the backend pulls it on demand โ€” localhost registries are insecure/HTTP by default, so nerdctl run pulls with no extra flag.

So after image build there is nothing else to do โ€” the image is in the registry and the first run pulls it into the containerd (then it's cached). This mirrors how the base image already flows (local-backend-up.sh pushes it to the same registry). It's the exact mechanism RESOLUTO_SANDBOX_IMAGE uses โ€” a localhost:5000/โ€ฆ reference.

No registry available? Set RESOLUTO_SANDBOX_REGISTRY="" (build stays a bare tag) and transfer the image into the containerd directly instead:

docker save resoluto-sandbox:openai-agents-0.17.7 \
  | sudo "$RESOLUTO_LOCAL_NERDCTL" --address /run/resoluto-local/containerd/containerd.sock \
      --namespace resoluto-local load

Bring your own image

The registry bridge is not provider-specific โ€” any image the backend can pull works, so your own Dockerfile is a first-class citizen. Build it, publish it with image push, and pass the reference as image= (Python) / --image (CLI):

cat > Dockerfile <<'DOCKER'
FROM localhost:5000/resoluto-sandbox:langchain-1.3.11     # or the base, or any image
RUN pip install --break-system-packages langchain-anthropic
DOCKER

docker build -t my-langchain-agent:1.0 .
resoluto-sandbox image push my-langchain-agent:1.0        # -> pushed localhost:5000/my-langchain-agent:1.0

resoluto-sandbox run --image localhost:5000/my-langchain-agent:1.0 -- python my_agent.py
Sandbox(backend="local", image="localhost:5000/my-langchain-agent:1.0").run([...])

image push tags the local image for the configured registry (RESOLUTO_SANDBOX_REGISTRY, default localhost:5000) and pushes it; an already registry-qualified tag is pushed as-is. Equivalently, skip it by building straight to the registry tag (docker build -t localhost:5000/my-agent:1.0 . && docker push โ€ฆ). Either way the backend pulls it on first run. (FROM a provider tag needs that image in your Docker store first โ€” resoluto-sandbox image build --provider langchain โ€” or FROM localhost:5000/โ€ฆ to pull the base from the registry.)

Verified end to end against the real Kata sandbox (all three: canary passes, workspace stages, the script runs and reaches its auth check). claude and openai run against the plain prebuilt image; langchain needs the one-line extended image from above (built as my-langchain-anthropic:0.1.0 here):

from resoluto.sandbox import Sandbox

# workspace="examples/payloads" stages that DIRECTORY'S CONTENTS at /workspace โ€” argv paths are
# relative to that root, never prefixed with the directory again.
r = Sandbox(backend="local", image="my-langchain-anthropic:0.1.0").run(
    ["python3", "langchain_agent.py", "Say hello in five words"],
    workspace="examples/payloads",
    env={"ANTHROPIC_API_KEY": "..."},
)
print(r.output)

Swap the image= tag + example script + env var to switch providers โ€” everything else is identical:

Sandbox(backend="local", image="resoluto-sandbox:claude-agent-sdk-0.2.110").run(
    ["python3", "claude_agent.py", "Say hello in five words"], workspace="examples/payloads",
    env={"CLAUDE_CODE_OAUTH_TOKEN": "..."},   # never set ANTHROPIC_API_KEY alongside this
)
Sandbox(backend="local", image="resoluto-sandbox:openai-agents-0.17.7").run(
    ["python3", "openai_agent.py", "Say hello in five words"], workspace="examples/payloads",
    env={"OPENAI_API_KEY": "..."},
)

Or via the CLI (--workspace is REQUIRED to stage anything โ€” without it /workspace is empty and your script won't be found; run from the repo root so . stages examples/ alongside it):

resoluto-sandbox run --workspace examples/payloads --image resoluto-sandbox:openai-agents-0.17.7 -- python3 openai_agent.py "hi"

On k8s, retag + push to your registry (docs/backends.md), then inject the same tag through SubstrateBackend(image=...) โ€” see docs/concepts.md for the full wiring.


Status

Feature Status
backend="local" โ€” Kata microVM via nerdctl + a dedicated containerd, runtime-managed per-run egress works today (run scripts/local-backend-up.sh)
backend="k8s" โ€” Kata microVM pod + object-store Conduit + NetworkPolicy egress works today โ€” needs a Kata cluster + store + kube context
Conduit + LocalConduit, StdoutConduit, S3Conduit (minio/S3) works today
GcsConduit provided, unverified โ€” experimental
Language-neutral wire spec published โ€” see spec/PROTOCOL.md
Prebuilt image matrix + image build CLI works today โ€” resoluto-sandbox image build --provider claude|langchain|openai|all

The Resoluto ecosystem

Three independent, plug-and-play packages under one resoluto.* namespace and one design language. Each stands alone โ€” use any without the others.

Package Role
resoluto-sandbox Store-mediated, Kata-isolated execution substrate
resoluto-agent Pluggable LLM agent-provider plugins
resoluto-engine Durable orchestrator of sandboxed, gate-verified agentic work

resoluto-engine builds on resoluto-agent (the provider contract) and optionally resoluto-sandbox (isolation); resoluto-sandbox and resoluto-agent depend on nothing else in the ecosystem.


Further reading

  • docs/concepts.md โ€” the program contract, the run lifecycle, the Conduit
  • docs/backends.md โ€” backend setup + the vendor-neutral k8s stack install
  • docs/networking.md โ€” egress isolation (the canary + per-backend enforcement)
  • docs/auth.md โ€” credentials: passing each provider's secret to the guest (the subscription path is local-dev only)
  • spec/PROTOCOL.md โ€” the language-neutral host โ†” sandbox wire protocol
  • examples/ โ€” run_agent_in_sandbox.py <claude|langchain|openai> runs any provider's agent isolated (symmetric across all three images); run_hello_in_sandbox.py is the bare mechanics. payloads/ holds the plain programs run inside (hello.py, claude_agent.py, langchain_agent.py, openai_agent.py, one per prebuilt provider image). See examples/README.md.

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

resoluto_sandbox-0.1.0.tar.gz (346.3 kB view details)

Uploaded Source

Built Distribution

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

resoluto_sandbox-0.1.0-py3-none-any.whl (79.4 kB view details)

Uploaded Python 3

File details

Details for the file resoluto_sandbox-0.1.0.tar.gz.

File metadata

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

File hashes

Hashes for resoluto_sandbox-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b2845755fe2ffb2e08554f2496ae564d2b1310bd4f1bf213a36723820dbf33b3
MD5 f16dcf915a14c4e7fc0775525a604d64
BLAKE2b-256 8b5e559b2c415502cf96d65ebaa31863f7387d13eb020a8983c97164d97fdf3d

See more details on using hashes here.

Provenance

The following attestation bundles were made for resoluto_sandbox-0.1.0.tar.gz:

Publisher: workflow.yml on DeepBlueCoding/resoluto-sandbox

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

File details

Details for the file resoluto_sandbox-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for resoluto_sandbox-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eadf08ff7c47a3b2674cd62e79f1f01580259eb6babab4c04f9108347995dcfd
MD5 1347de31bbdd48ce974441843789296b
BLAKE2b-256 790b31eb5acb0c4b3cff0d6a7283549058dfcf051cbe668a6d481c79689f42af

See more details on using hashes here.

Provenance

The following attestation bundles were made for resoluto_sandbox-0.1.0-py3-none-any.whl:

Publisher: workflow.yml on DeepBlueCoding/resoluto-sandbox

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