Compute
Architecture, isolation model, and security controls for Chalk compute sandboxes.
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:
compute_class=ComputeClass.HOST, the default) or on your own Kubernetes clusters (compute_class=ComputeClass.K8S).Sandbox API as CPU workloads — see GPU below for the compute_class requirement.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:
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.
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-clusterThe 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.
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.
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-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.
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.
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.
Chalk enforces tenant isolation at every layer of the stack:
| Layer | Mechanism |
|---|---|
| Runtime | gVisor kernel-level syscall interception per sandbox |
| Network | Separate network namespace per sandbox; no shared listening sockets |
| Storage | Volumes are scoped to the owning environment; cross-tenant access is impossible |
| Scheduling | Workloads from different tenants are placed on separate host nodes by default |
| Identity | Each 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.
Sandboxes follow a straightforward lifecycle:
Image spec (e.g. Image.debian_slim().pip_install([...])), it is built and cached. Pre-built OCI images are used directly.exec calls. Volumes are mounted and accessible.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.
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.
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.
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)sandbox.terminate() # immediate termination + cleanup
sandbox.terminate(grace_period_seconds=30) # allow processes to shut down firstTermination cleans up the sandbox and its ephemeral filesystem. Persistent volume data remains available according to the volume’s sync and versioning semantics.
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.
Omit lifetime to run until you terminate the sandbox yourself. Set it to bound
the run:
| Value | Effect |
|---|---|
| omitted | The 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.
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.
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()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.
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")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:
sub claim to an appropriate role or policy.No secrets need to be injected into the sandbox environment.
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.
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:
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.
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.
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 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_ed25519Then 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/.sshare rejected, whether or not Managed SSH is configured for that sandbox.
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.
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.