Overview

Chalk sandboxes are lightweight, isolated execution environments for running arbitrary code — agent workloads, model inference, data pipelines, or any container-based task. Each sandbox runs inside a gVisor-hardened container with its own filesystem, network namespace, and resource limits.

Sandboxes are designed around three principles:

  1. Strong isolation. Workloads from different tenants (and different workloads from the same tenant) cannot observe or interfere with each other.
  2. Flexible deployment. The same sandbox abstraction runs on managed serverless infrastructure (compute_class=ComputeClass.HOST, the default) or on your own Kubernetes clusters (compute_class=ComputeClass.K8S).
  3. First-class GPU support. GPU-accelerated workloads use the same Sandbox API as CPU workloads — see GPU below for the compute_class requirement.

Isolation with gVisor

Every sandbox runs under gVisor, a container runtime that intercepts application system calls through a user-space kernel. Unlike traditional containers that share the host kernel directly, gVisor interposes a second layer of defense:

┌────────────────────────┐
│   Application process  │
├────────────────────────┤
│   gVisor (Sentry)      │  ← intercepts syscalls
├────────────────────────┤
│   Host kernel          │
└────────────────────────┘

This means a kernel exploit in one sandbox cannot compromise the host or other sandboxes. gVisor also restricts the set of available syscalls, reducing the attack surface exposed to untrusted code — particularly important for agent workloads that execute LLM-generated commands.

Each sandbox additionally receives its own:

  • Filesystem namespace. The root filesystem is ephemeral and private. Persistent storage is available through Volumes.
  • Network namespace. Sandboxes have isolated network stacks. Egress can be further restricted with Network Policies.
  • PID namespace. Processes inside a sandbox cannot see or signal processes in other sandboxes.

Deployment models

Managed serverless

Chalk operates a fleet of bare-metal nodes optimized for sandbox workloads. When you create a sandbox without additional configuration, it runs on this managed infrastructure:

from chalkcompute import Image, Sandbox

sandbox = Sandbox(image=Image.debian_slim()).run()

Managed serverless handles provisioning, scaling, and node maintenance. Sandboxes are scheduled across availability zones and can cold-start in under two seconds for common base images.

Self-hosted Kubernetes (EKS / GKE)

For workloads that must run within your cloud account — for compliance, data residency, or proximity to other infrastructure — Chalk can deploy sandboxes into your existing EKS or GKE clusters.

In this model, you install the Chalk node agent as a DaemonSet. The agent manages gVisor runtime configuration, volume mounts, and GPU device plugin integration. The control plane remains managed by Chalk; your cluster provides the compute.

# Install the Chalk node agent into your cluster
chalk compute install --cluster arn:aws:eks:us-east-1:123456789:cluster/my-cluster

The same Sandbox API works regardless of where the sandbox is scheduled — pass compute_class=ComputeClass.HOST (the default) for managed serverless, or compute_class=ComputeClass.K8S to schedule onto your own cluster. One exception: GPU workloads require compute_class=ComputeClass.K8S explicitly — managed serverless doesn’t support GPUs.


Quick start

Build an image, upload a local script, run it in a sandbox, and terminate the sandbox when the work is complete:

from chalkcompute import Image, Sandbox

image = (
    Image.debian_slim("3.12")
    .pip_install(["requests"])
    .add_local_file("./script.py", "/app/script.py")
    .workdir("/app")
)

sandbox = Sandbox(
    image=image,
    name="hello-sandbox",
    cpu="1",
    memory="2Gi",
).run()

result = sandbox.exec("python", "/app/script.py")
print(result.stdout_text)

sandbox.terminate()

Sandbox(...).run() builds the Image, reuses a cached build when possible, creates the sandbox, and waits for it to become ready. Local files declared with add_local_file or add_local_dir are uploaded as part of the image setup.


Resource allocation

CPU and memory

Specify resource requests when creating a sandbox:

from chalkcompute import Image, Sandbox

sandbox = Sandbox(
    image=Image.debian_slim(),
    cpu="4",
    memory="16Gi",
).run()

Resources are guaranteed (requests equal limits), so sandboxes are not subject to noisy-neighbor throttling.

The request must fit on a single host in a configured host pool. A sandbox that asks for more CPU or memory than any host provides fails to schedule with Unschedulable: no configured host pool can satisfy ..., which is resolved by sizing a host pool to fit it.

GPU

GPU-accelerated workloads request a GPU type at creation time, but only run under the k8s compute class — sandboxes default to host (the gVisor isolation described above), which does not support GPUs. Set compute_class=ComputeClass.K8S alongside gpu:

from chalkcompute import ComputeClass, Image, Sandbox

sandbox = Sandbox(
    image="nvcr.io/nvidia/pytorch:24.01-py3",
    gpu="nvidia-l4",
    cpu="8",
    memory="64Gi",
    compute_class=ComputeClass.K8S,
).run()

The Chalk scheduler matches the request to a node with the appropriate GPU hardware and configures the NVIDIA device plugin and driver mounts automatically.

Setting `gpu` without `compute_class=ComputeClass.K8S` fails at creation time with `host-backed sandboxes don't support GPU; use k8s instead` — `host` is the default when `compute_class` is omitted.


Environment variables and secrets

Pass plaintext configuration through env or resolve credentials through Chalk-managed Secret values:

from chalkcompute import Image, Sandbox, Secret

sandbox = Sandbox(
    image=Image.debian_slim("3.12"),
    name="api-client",
    env={"LOG_LEVEL": "INFO"},
    secrets=[
        Secret.from_chalk_env("OPENAI_API_KEY"),
        Secret.from_chalk_integration("prod_postgres"),
    ],
).run()

Secrets are resolved at creation time and exposed to the sandbox as environment variables. See Secrets for the available Secret constructors.

Mounting volumes

Attach a Volume to share durable state between runs or expose a large dataset without baking it into the image:

from chalkcompute import Image, Sandbox, Volume

vol = Volume("training-data")
vol.put_file("inputs/example.txt", b"hello\n")

sandbox = Sandbox(
    image=Image.debian_slim("3.12"),
    name="volume-reader",
    volumes=[("training-data", "/data")],
).run()

result = sandbox.exec("cat", "/data/inputs/example.txt")
print(result.stdout_text)  # "hello\n"

sandbox.terminate()

Mounted volumes persist across sandbox lifecycles and can be shared by other sandboxes that mount the same volume.


Multi-tenancy

Chalk enforces tenant isolation at every layer of the stack:

LayerMechanism
RuntimegVisor kernel-level syscall interception per sandbox
NetworkSeparate network namespace per sandbox; no shared listening sockets
StorageVolumes are scoped to the owning environment; cross-tenant access is impossible
SchedulingWorkloads from different tenants are placed on separate host nodes by default
IdentityEach sandbox receives a unique workload identity — no shared credentials

For deployments with strict regulatory requirements, dedicated node pools can be configured so that a tenant’s workloads never share physical hardware with any other tenant.


Lifecycle

Sandboxes follow a straightforward lifecycle:

  1. Image resolution. If the image is an Image spec (e.g. Image.debian_slim().pip_install([...])), it is built and cached. Pre-built OCI images are used directly.
  2. Scheduling. The sandbox is placed on a node with sufficient resources. gVisor initializes the isolation boundary.
  3. Running. The sandbox accepts exec calls. Volumes are mounted and accessible.
  4. Termination. The sandbox is terminated explicitly or after its lifetime expires. Ephemeral filesystem state is discarded; volume data persists if synced.
from chalkcompute import Image, Sandbox

sandbox = Sandbox(
    image=Image.debian_slim().pip_install(["numpy"]),
    cpu="2",
    memory="4Gi",
    lifetime="3600s",
).run()

result = sandbox.exec("python", "-c", "import numpy; print(numpy.__version__)")
print(result.stdout_text)

sandbox.terminate()

lifetime is a protobuf duration string — a number of seconds followed by s (fractional seconds are allowed, e.g. "1.5s"), not a Go-style duration like "1h". Omitting it lets the sandbox run until it is terminated explicitly; see Long-running sandboxes for runs measured in hours or days.


Executing commands

Sandbox.exec(*command, timeout_secs=...) runs a process inside an already-running sandbox and returns its result after completion:

result = sandbox.exec("ls", "-la", "/app", timeout_secs=30)
print(result.stdout_text)
print(result.stderr_text)
print(result.exit_code)

The result captures stdout and stderr in full and includes the process exit code. Use sandbox.exec_stream(...) for streaming output or sandbox.exec_start(...) when you need interactive stdin and signal handling.

Lifecycle management

Attaching to an existing sandbox

Use Sandbox.from_id or Sandbox.from_name to reconnect to a sandbox from another process, instead of creating a second one:

from chalkcompute import Sandbox

sandbox = Sandbox.from_id("sandbox-id")
# or, if it was created with a name=:
sandbox = Sandbox.from_name("hello-sandbox")

Sandbox.list_all() returns SandboxInfo for every sandbox in the current environment, useful for finding an id or name to attach to.

Inspecting status

sandbox.info is None until the sandbox has been started or attached to. sandbox.refresh() both refreshes and returns the current SandboxInfo, so prefer its return value over sandbox.info when you’ve just refreshed:

info = sandbox.refresh()
print(info.status)  # "pending", "running", "succeeded", "failed", etc.
print(sandbox.id)

Terminating

sandbox.terminate()                          # immediate termination + cleanup
sandbox.terminate(grace_period_seconds=30)   # allow processes to shut down first

Termination cleans up the sandbox and its ephemeral filesystem. Persistent volume data remains available according to the volume’s sync and versioning semantics.


Long-running sandboxes

Sandboxes can run for hours or days. Two independent settings govern how long one lasts: lifetime bounds its total run time, and restart_policy decides whether Chalk recreates it when its backing instance goes away.

Setting a lifetime

Omit lifetime to run until you terminate the sandbox yourself. Set it to bound the run:

ValueEffect
omittedThe sandbox runs until it is terminated explicitly
"86400s"The sandbox is terminated 24 hours after it starts
"604800s"The sandbox is terminated 7 days after it starts

There is no maximum value. lifetime="0s" raises ValueError rather than meaning “no limit”; omit the argument for that.

When a lifetime expires, Chalk stops the sandbox immediately without draining in-flight work, and records the result as succeeded. Write anything you need to keep to a mounted volume before that point.

Surviving node drains and preemption

A sandbox running for days outlives the node it started on. By default, the backing instance terminating ends the sandbox. Set restart_policy to have Chalk recreate it instead:

from chalkcompute import Image, RestartPolicy, Sandbox, Volume

Volume("training-checkpoints")  # created if it does not already exist

sandbox = Sandbox(
    image=Image.debian_slim("3.12").pip_install(["torch"]),
    name="multi-day-training",
    cpu="8",
    memory="32Gi",
    volumes=[("training-checkpoints", "/checkpoints")],
    restart_policy=RestartPolicy.ALWAYS,
).run()

Leaving restart_policy unset behaves the same as RestartPolicy.NEVER. A recreated sandbox starts from its image again with an empty ephemeral filesystem, so write progress to a mounted Volume and have the workload resume from it on startup.

Running work across sessions

Sandbox.exec blocks until the command finishes, so start work measured in hours or days as a detached process:

sandbox.exec(
    "sh",
    "-c",
    "nohup python /app/train.py --checkpoint-dir /checkpoints > /checkpoints/train.log 2>&1 &",
)

Reattach by name from another process, or on another day, to check on it:

from chalkcompute import Sandbox

sandbox = Sandbox.from_name("multi-day-training")
print(sandbox.refresh().status)
print(sandbox.exec("tail", "-n", "20", "/checkpoints/train.log").stdout_text)

Within a single exec call, auto_reattach (enabled by default) reconnects and replays buffered output after a transient transport disconnect, so brief network interruptions need no handling of their own.

Terminate the sandbox once the work is done. A sandbox reserves the CPU, memory, and any GPU you requested for as long as it exists, and those resources are billed as provisioned rather than as used, so an idle sandbox costs the same as a busy one. RestartPolicy.ALWAYS keeps bringing it back until you terminate it:

sandbox.terminate()

Security

The following controls govern identity, network access, and credentials for sandbox workloads. They also apply to higher-level abstractions like Scaling Groups, which share the same isolation primitives.

Workload Identity Federation

Every sandbox launched through chalkcompute runs with a unique cloud identity and a corresponding Chalk identity. These identities are scoped to the individual workload — no two sandboxes share credentials, and workloads never run with delegate credentials from the calling user.

Workload identities are issued automatically at creation time:

from chalkcompute import Image, Sandbox

sandbox = Sandbox(image=Image.debian_slim()).run()

# The sandbox is running with its own identity —
# it can authenticate to Chalk APIs without additional configuration.
result = sandbox.exec("chalk", "query", "--in", "user.id=1", "--out", "user.score")

OIDC Federation

Chalk workload identities are OIDC-compliant. If your organization runs services that accept federated tokens (e.g. an internal model registry or a secrets manager), you can configure them to trust the Chalk identity provider directly. This lets sandboxes authenticate to your infrastructure without static credentials:

  1. Register the Chalk OIDC issuer as a trusted identity provider in your service.
  2. Map the workload’s sub claim to an appropriate role or policy.
  3. The sandbox can then exchange its identity token for access to your service at runtime.

No secrets need to be injected into the sandbox environment.

Kernel hardening

Sandboxes restrict the syscalls and host access available to the workload by default. Control this with security_policy:

from chalkcompute import ContainerSecurityPolicy, Image, KernelPolicy, Sandbox

sandbox = Sandbox(
    image=Image.debian_slim(),
    security_policy=ContainerSecurityPolicy(kernel_policy=KernelPolicy.RESTRICTED),
).run()

kernel_policy is KernelPolicy.RESTRICTED (the default, both when security_policy is omitted and when set explicitly) or KernelPolicy.OPEN, which relaxes the sandboxing controls RESTRICTED enables — use it only for workloads that need syscalls or host access RESTRICTED blocks.

MCP Gateway

The MCP Gateway lets sandboxes interact with Model Context Protocol servers exposed at your enterprise — without giving the sandbox direct access to the underlying credentials.

When a sandbox calls the MCP Gateway, it authenticates using its workload identity (see above). The gateway validates the identity, then proxies the request to the upstream MCP server using credentials managed by your organization. The sandbox never sees the real credential.

┌─────────────┐        WIF token         ┌─────────────┐     real credential     ┌─────────────┐
│   Sandbox   │ ──────────────────────▸  │ MCP Gateway │ ──────────────────────▸ │  MCP Server │
└─────────────┘                          └─────────────┘                         └─────────────┘

This is particularly useful for agent workloads. A code-generation agent may need to call tool-use APIs, search indexes, or retrieval services. With the gateway:

  • The agent can leak its own temporary token without compromising the real upstream credential.
  • You retain centralized control over which MCP servers are reachable and by which workloads.
  • Audit logs capture every proxied call, tied to the originating workload identity.

Network Policy

By default, sandboxes have unrestricted egress. For production workloads — especially autonomous agents — restrict outbound traffic with a NetworkPolicy bound to the sandbox’s network_policy (singular):

from chalkcompute import Image, NetworkPolicy, Sandbox

policy = NetworkPolicy(
    allowed_hosts=[
        "api.openai.com",
        "github.com",
        "pypi.org",
    ],
)

sandbox = Sandbox(
    image=Image.debian_slim(),
    network_policy=policy,
).run()

allowed_hosts matches exact hostnames — there’s no wildcard or subdomain matching, so list every host a workload needs (github.com and api.github.com are different entries). Requests to any other destination are blocked. Attaching a policy with an empty allowed_hosts denies all hostname-level egress by default — it doesn’t fall back to unrestricted.

`NetworkPolicy` accepts one policy per sandbox — `network_policy` takes a single `NetworkPolicy`, not a list.

CIDR-level routes

Use allowed_routes and denied_routes for destinations that don’t have a stable hostname — a VPC-peered range or an on-premise network, for example. denied_routes takes precedence over both allowed_routes and allowed_hosts:

from chalkcompute import NetworkPolicy

policy = NetworkPolicy(
    allowed_routes=[
        NetworkPolicy.Route("10.0.0.0/8", ports=[(5432, 5432)]),
    ],
    denied_routes=["169.254.169.254/32"],  # block the cloud metadata endpoint
)

Route(route, *, ports=...) takes a destination CIDR and an optional list of (start_port, end_port) ranges; an empty ports allows every port on that route.

Per-host request rules

For per-host request matching or rewriting — for example, substituting a Secret into an auth header before forwarding to an internal service — pass allowed_hosts as a mapping of hostname to a list of NetworkPolicy.Rule instead of a plain list:

from chalkcompute import NetworkPolicy

policy = NetworkPolicy(
    allowed_hosts={
        "internal-api.example.com": [
            NetworkPolicy.Rule(
                match=NetworkPolicy.Match(
                    path=NetworkPolicy.Matcher(starts_with="/v1/"),
                ),
                transforms=[
                    NetworkPolicy.Transformer(
                        headers={
                            "Authorization": "Bearer __chalk_secret_API_TOKEN",
                        },
                        headers_secrets={
                            "__chalk_secret_API_TOKEN": "INTERNAL_API_TOKEN",
                        },
                    ),
                ],
                forward_url="https://internal-api.example.com",
            ),
        ],
    },
)

Each Rule can match on request path (exact, starts_with, or regex), HTTP method, query-string entries, and headers — a rule’s transforms and forward_url only apply to requests matching every dimension set on match. The route matches the original request before any transforms run.

Transformer.headers_secrets maps each literal placeholder token to the name of a Chalk Secret. For a matching rule, Chalk first applies the literal values in Transformer.headers, then replaces each placeholder occurrence in ordinary request-header values with that Secret’s contents. Placeholders can also appear in headers supplied by the workload. Chalk does not substitute within pseudo-headers, authority or framing headers, hop-by-hop headers, or Chalk-owned routing headers.

Placeholder matching is case-sensitive. When occurrences overlap, Chalk selects the leftmost match and then the longest placeholder that starts at that position. Replacement is non-recursive, so inserted Secret contents are not scanned for additional placeholders. An absent placeholder has no effect, and an empty Secret value removes the matched placeholder text. Empty placeholder tokens are rejected. The sandbox never receives the Secret contents.

Managed SSH

Managed SSH lets a sandbox authenticate to approved SSH destinations using a Chalk Secret, without exposing the private key to the sandbox itself. First, store the private key:

chalk secret set GITHUB_DEPLOY_KEY < /path/to/privkey/id_ed25519

Then configure the destination when creating the sandbox:

from chalkcompute import Image, SandboxClient, SshDest

sandbox_client = SandboxClient.from_env()
sandbox = sandbox_client.create(
    image=Image.debian_slim(),
    managed_ssh={
        "github": SshDest(
            "github.com",
            "GITHUB_DEPLOY_KEY",
            username="git",
        )
    },
)

private_key_secret is the name of a Chalk Secret whose value is an SSH private key. The map key creates the SSH alias used to connect, so the example supports ssh github and git clone github:example/repo.git. The default port is 22, but can be overridden by setting an explicit port.

By default, SSH accepts the server’s host key on the first connection and remembers it for the rest of the sandbox’s lifetime. Later connections fail if the server presents a different key. Set host_keys to override this, and to require specific host keys from the first connection. For example:

managed_ssh={
    "a-server": SshDest(
        "cloud.example.com",
        "KEY_FOR_SANDBOX_USE",
        username="my_user",
        host_keys=[
            "ssh-ed25519 AAAA...",
            "ecdsa-sha2-nistp256 AAAA...",
        ],
        port=2222,
    )
}

Important: Managed SSH destinations are a separate security domain from Network Policy. A configured destination’s resolved IP address and port remain reachable even if they overlap with denied_routes.

Internally, Chalk loads the configured keys into an ssh-agent and exposes its socket through both SSH_AUTH_SOCK and IdentityAgent in /root/.ssh/config. Sandbox processes can ask the agent to sign with any configured key, but cannot read the private-key material.

Important: Sandbox images containing any existing /root/.ssh are rejected, whether or not Managed SSH is configured for that sandbox.

Sandbox-to-sandbox and on-premise connectivity

Each sandbox gets its own network namespace with no shared listening sockets (see Multi-tenancy above), and there is currently no direct sandbox-to-sandbox or on-premise tunneling mechanism — workloads that need to exchange data should share it through a mounted Volume or an external service reachable over the network, rather than a peer-to-peer connection between sandboxes.


When to use a Sandbox

Use a Sandbox for a task you address directly as a single workload: running generated agent code, a one-off data backfill, or a per-session workspace that is torn down when the session ends. Sandboxes also run for hours or days, as Long-running sandboxes describes.

For replicated, HTTP-fronted workloads with autoscaling, use a Scaling Group. For serverless, function-shaped invocations, use Functions.

Run length alone does not decide between a sandbox and a scaling group, since both survive disconnects and can be reattached to by name. Choose by how the workload is reached: a sandbox is one instance you run commands in over exec, while a scaling group serves an HTTP endpoint backed by replicas that Chalk scales. A long-lived agent session that you drive through a web UI belongs in a scaling group, which is why Deep coding agents uses one.