archil
Python client for Archil disks and sandboxes. Create persistent disks and microVM sandboxes, run commands against them, and read/write disk contents through the S3-compatible object API — all from scripts, CI, or notebooks. It also ships drop-in agent tools that turn a disk into a ready-made filesystem toolset for OpenAI Agents, LangChain, and other frameworks.
archil talks to the Archil control plane over HTTPS and has no native dependencies.
Every method works both synchronously and asynchronously from a single implementation: disk.put_object(...) blocks, while disk.put_object.aio(...) returns a coroutine you can await. This is powered by synchronicity — the same approach Modal uses — so there's one source of truth and no duplicated sync/async logic.
Install
pip install archil
Library
import archil
# Configure once per process — falls back to ARCHIL_API_KEY / ARCHIL_REGION env vars.
archil.configure(api_key="key-...", region="aws-us-east-1")
# Create a disk. The returned disk is available for use immediately. `token`
# here is the disk token — the one-time credential for mounting.
result = archil.create_disk(name="my-disk")
disk = result.disk
print(f"Created {disk.id}, disk token: {result.token}")
# List and look up disks
all_disks = archil.list_disks()
d = archil.get_disk(disk.id)
Per-disk operations are methods on the Disk object itself, not top-level functions:
d = archil.get_disk("dsk-abc123")
# Run a command in a container with the disk mounted
res = d.exec("ls -la /mnt && cat /mnt/config.json")
print(res.stdout, res.stderr, res.exit_code)
# Manage who can mount the disk
from archil import TokenUser
user = d.add_user(TokenUser(nickname="ci"))
d.remove_user("token", user.identifier)
# Delete
d.delete()
Sandboxes
Use Archil.sandboxes or the module-level helpers to manage persistent microVMs:
sandbox = archil.create_sandbox(
name="prepared-environment",
vcpu_count=4,
mem_size_mib=8192,
)
result = sandbox.exec("uname -a")
print(result.stdout)
sandbox.stop()
fork = sandbox.fork(name="agent-task")
fork.stop()
fork.delete()
all_sandboxes = archil.list_sandboxes()
using_disk = archil.list_sandboxes(disk="dsk-abc123")
When the region has no capacity, sandbox create, start, resume, and fork back off
and retry for up to 10 seconds, logging a warning, then raise the 503. Change the
limit with Archil(max_throttle_wait=seconds) or ARCHIL_MAX_THROTTLE_WAIT; 0
raises immediately and inf waits indefinitely. Forking a running sandbox never
waits, so the source is not left paused.
Build a sandbox image from an OCI reference, then create sandboxes from it:
import os
from archil import Archil, RegistryAuth
client = Archil()
image = client.images.build(
source="ghcr.io/acme/agent:v3",
# Private images only.
registry_auth=RegistryAuth(username="bot", password=os.environ["GHCR_TOKEN"]),
)
agent = client.sandboxes.create(image_id=image.image_id)
print(agent.image_digest)
build() waits until the image is ready, joining a build already in progress.
It raises TimeoutError after 30 minutes by default (timeout= takes seconds).
Building a tag again rebuilds it in case the tag moved, so build once and reuse
image_id; a built digest reference (repo@sha256:...) returns immediately.
Archil never stores registry credentials: pass registry_auth on every build
of a private image, since without it the same source names the public image.
Interrupted builds are requested again up to three times. A failed build raises
ImageBuildError; its reason is invalid_source when the registry reported
the image missing or denied access, or interrupted. images.get(image_id)
returns the image's current state, and both methods have .aio variants.
image_id cannot be combined with base_image, and creating a sandbox from an
image without a successful build fails with a 409 ArchilApiError whose code
is image_not_ready.
Expose TCP ports publicly when creating a sandbox or later with expose_port:
web = archil.create_sandbox(base_image="python:3.12-slim", ports=[8080])
server = web.run("python -m http.server 8080 --bind 0.0.0.0")
server.disconnect() # The server keeps running.
hostname = web.expose_port(8080) # Returns the hostname, including if already public.
print(f"https://{hostname}") # Available once the server is listening.
print(web.list_ports()) # list[SandboxEndpoint] with port and hostname
web.unexpose_port(8080)
Ports must be between 1 and 65535. Exposing a port makes it publicly accessible
without authentication; your application must listen on that port. HTTP services
are reached through HTTPS on the returned hostname.
list_ports() returns only explicitly exposed ports; sandbox.endpoints contains
service-published ports. Deleting explicit exposure leaves a service publishing
the same port reachable and does not stop the listening process. Routing changes
may take a few seconds to propagate, and existing connections remain open.
These methods also have .aio variants for async callers.
For private HTTP access, create a port token without exposing the port publicly:
import httpx
private_web = archil.create_sandbox(base_image="python:3.12-slim")
server = private_web.run("python -m http.server 8080 --bind 0.0.0.0")
server.disconnect()
access = private_web.create_port_token(8080, ttl="1h")
# Once the server is listening:
response = httpx.get(
f"https://{access.hostname}/",
headers={"X-Archil-Token": access.token},
)
print(response.text)
metadata = private_web.get_port_token(access.id)
tokens = private_web.list_port_tokens()
for page in private_web.list_port_token_pages(page_size=10):
print(page.tokens)
private_web.delete_port_token(access)
private_web.delete()
The secret and hostname are returned only by create_port_token; get/list return
metadata. Save the secret when creating it. Omit ttl for no expiration, or use
a duration up to "8760h" (365 days). list_port_tokens follows all pages (its
limit caps the total); list_port_token_pages yields pages with next_cursor
for resuming. These methods also have .aio variants, including async iteration
with async for page in sandbox.list_port_token_pages.aio().
Public ports bypass token authentication. Revocation and expiry deny new
connections with HTTP 401; existing connections remain open.
Archil disks can be mounted inside the sandbox at creation. They are mounted on every boot and persist across stop/start and pause/resume. A sandbox with mounts must be stopped before it is forked; the fork inherits them:
workspace = archil.create_sandbox(
mounts=[
archil.SandboxMountSpec(disk="dsk-0123456789abcdef", path="/mnt/archil"),
archil.SandboxMountSpec(disk="dsk-fedcba9876543210", path="/mnt/models", subdirectory="llama", read_only=True),
archil.SandboxMountSpec(disk="dsk-0000000000000001", path="/workspace", conditional=True),
],
)
print([mount.path for mount in workspace.mounts])
workspace.stop()
workspace.start(mounts=[archil.SandboxMountSpec(disk="dsk-0123456789abcdef", path="/mnt/other")])
These disks are additional mounts; the sandbox retains its internal root disk.
A sole mount defaults to /mnt/archil; path is required for every mount when
more than one disk is mounted. Options: read_only, conditional
(concurrent writers without delegation checkouts), subdirectory, and queue_ms (how long a
plain mount waits for the disk's exclusive root delegation). A plain read-write mount holds
that delegation for as long as the sandbox does, including while paused, so use read_only
or conditional for disks that other clients or forks also write.
On a cold start, omit mounts to retain the current list, pass mounts=[] to
clear it, or supply a list to replace it. Mounts cannot be changed on resume or
while the sandbox is running. queue_ms cannot be combined with read_only or conditional.
Network egress can optionally be restricted when creating a sandbox:
import os
restricted = archil.create_sandbox(
network=archil.SandboxNetwork(
egress=archil.SandboxEgressPolicy(
default="deny",
allow=[
"github.com",
"*.github.com",
"140.82.112.0/20",
archil.SandboxEgressRule(
target="api.openai.com",
transform=archil.SandboxEgressTransform(
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"}
),
),
],
)
),
)
An egress policy accepts IPv4 addresses, CIDR ranges, exact domains, and *.
wildcard domains. The default action applies when no target matches. allow
and deny can both be specified; deny matches take precedence. Domain matching
applies to plaintext HTTP and HTTPS traffic, and a wildcard such as
*.github.com matches subdomains, not github.com itself. An object-form allow
rule can transform outbound HTTPS requests to its exact lowercase domain.
Header transformations overwrite values supplied by the sandbox; matching
plaintext HTTP requests are rejected. An object-form rule can instead set
forward_url to an absolute public HTTPS URL: the rule's requests go there, with
the original path appended, instead of to their original host, and Archil sets the
archil-forwarded-host, archil-forwarded-scheme, archil-forwarded-port,
archil-forwarded-path, and archil-sandbox-id headers. A transform on the same
rule applies first. Omit network for unrestricted egress. A
running sandbox's complete policy can be replaced without restarting it;
existing connections are not terminated:
effective = restricted.update_network(
archil.SandboxNetwork(
egress=archil.SandboxEgressPolicy(
default="deny",
allow=["api.github.com"],
)
)
)
print(effective, restricted.get_network())
# Restore unrestricted egress.
restricted.update_network(archil.SandboxNetwork())
Set hard and idle TTLs at creation or edit them while running. All values are seconds:
timed = archil.create_sandbox(max_ttl_seconds=3600, idle_ttl_seconds=300)
timed.set_timeout(7200)
timed.set_timeout(idle_ttl_seconds=60)
timed.set_timeout(3600, idle_ttl_seconds=300)
timed.set_timeout(idle_ttl_seconds=0)
print(timed.max_ttl_seconds, timed.idle_ttl_seconds)
The same options work with client.sandboxes.create.aio() and
sandbox.set_timeout.aio(). Omitted settings stay unchanged on edits. Updating
the hard TTL resets its deadline from now; an idle-only edit does not. Changes
made while inactive apply to the next powered-on session. Idle TTL accepts
0–86,400 seconds; omitted or zero at creation disables it, and zero on edits
disables it again.
Both TTLs pause the sandbox, preserving memory and processes. Open direct process connections prevent idle expiry, but not hard expiry. The idle countdown starts when the last connection closes; detached processes and service-port traffic do not keep it alive. Disable idle TTL for unattended jobs.
Sandboxes support 1–32 vCPUs and 256–65,536 MiB of memory. When omitted,
vcpu_count defaults to 1 and mem_size_mib defaults to 2,048 MiB. Sandbox
timeouts default to 24 hours and can be reset up to 24 hours from now.
Runtime-owned processes return immediately and can be disconnected without stopping the command. Reconnect by process ID and output cursor to continue where the previous connection stopped:
from archil import SandboxTerminal
process = sandbox.run(
"codex",
terminal=SandboxTerminal(cols=120, rows=40),
on_output=lambda output: print(output.data.decode(errors="replace"), end=""),
)
process.send_input("Review this repository\n")
process.resize(cols=160, rows=50)
process_id = process.id
cursor = process.cursor
process.disconnect()
resumed = sandbox.attach(process_id, offset=cursor)
result = resumed.wait()
Pass an absolute cwd to sandbox.run() or sandbox.exec() to choose the
working directory, for example sandbox.exec("pytest", cwd="/workspace/app").
Omitting it keeps the sandbox's default working directory.
Terminal processes merge output into stdout; non-terminal processes keep
stdout and stderr separate. on_output receives raw bytes with their stream
and absolute offset. close_stdin() delivers EOF to a non-terminal process.
send_input() streams large writes as 1 MiB WebSocket frames. disconnect()
only closes the client connection; kill() terminates the process. Set
collect_output=False to stream through on_output without retaining decoded
output in the process handle or result. Resize and kill use separate one-shot
process controls, so they do not wait behind stdin. kill() returns after the
control is acknowledged; wait() observes exit. max_concurrent_execs
limits attached process sessions; detached processes and one-shot controls do not
count. Pausing a sandbox disconnects attachments but preserves its processes
for reattachment after resume.
Processes end when their sandbox is stopped or expires. After reconnecting with
an offset, wait().stdout and wait().stderr contain the output received by
that handle from that offset, not output from before it. sandbox.exec() is the
one-call start-and-wait convenience for ordinary commands. It uses
sandbox.run() internally and does not create a durable control-plane exec
record; use sandbox.run() when you need the process handle.
Replace sandbox.processes.start() with sandbox.run() and
sandbox.processes.connect() with sandbox.attach(). The old methods and the
SandboxProcesses export remain available as deprecated compatibility APIs and will
be removed in the next version.
Transfer files directly between the local machine and a running sandbox:
sandbox.files.upload_file("./input.tar.gz", "/workspace/input.tar.gz")
sandbox.files.download_file("/workspace/result.json", "./result.json")
Transfers stream through sandbox.run() rather than buffering the whole
file in memory. Downloads request one bounded chunk at a time; a short or empty
chunk marks end-of-file. Uploads and downloads replace their destination only
after the transfer succeeds.
create, start, stop, pause, resume, and fork wait for completion by
default. The server handles the initial wait; if its wait budget expires first,
the SDK continues polling. Pass wait=False to return as soon as the lifecycle
operation is accepted. sandbox.exec() always waits for process exit.
fork pauses a running sandbox while the snapshot is taken and resumes it once
the fork is accepted. A paused or stopped sandbox is forked in place and left as
it is.
Delegations
A delegation grants a client exclusive write access to an inode on a shared disk. List the delegations currently held on a disk and forcibly revoke one to reclaim write access from a client that disconnected without checking it in:
disk = archil.get_disk("dsk-abc123")
for delegation in disk.list_delegations():
if delegation.is_orphaned:
disk.revoke_delegation(delegation)
Delegations are identified by their client_id and inode_id. The path is
resolved best-effort by the server and may be None.
Account-level API keys are top-level helpers:
archil.list_api_keys()
archil.create_api_key(name="ci-bot", description="GitHub Actions")
archil.delete_api_key("key-abc123")
Reading and writing objects
A Disk doubles as an S3-compatible bucket: read, write, delete, and list its files by key without mounting it. These methods talk to Archil's S3 endpoint using your same API key (no separate S3 credentials or SigV4 signing on your part).
import json
d = archil.get_disk("dsk-abc123")
report = {"generated": "2026-01", "rows": 1234}
# Write — accepts str or bytes. content_type is optional. Returns the etag.
result = d.put_object("reports/2026-01/data.json", json.dumps(report), "application/json")
# Read — returns bytes.
data = d.get_object("reports/2026-01/data.json")
text = data.decode("utf-8")
# Metadata / existence without downloading the body
meta = d.head_object("reports/2026-01/data.json") # None if absent
if d.object_exists("reports/2026-01/data.json"):
...
# Delete (idempotent — deleting a missing key succeeds)
d.delete_object("reports/2026-01/data.json")
POSIX ownership and directories
Pass uid, gid, and mode when the files will be used by a non-root process:
d.put_object(
"path/a/b/file.txt",
"hello",
uid=1000,
gid=1001,
mode=0o640,
)
If path/ already exists as 2000:2000 0700 and a/ and b/ are missing,
the resulting tree is:
| Path | Owner | Mode | Result |
|---|---|---|---|
path/ |
2000:2000 |
0700 |
Existing directory; unchanged |
path/a/ |
1000:1001 |
0755 |
Implicit parent created by the upload |
path/a/b/ |
1000:1001 |
0755 |
Implicit parent created by the upload |
path/a/b/file.txt |
1000:1001 |
0640 |
Published file with the requested attributes |
The requested file mode never applies to implicit parents; they use 0755 so
they remain traversable. Existing directories are never re-owned or re-moded,
so in this example a FUSE process running as uid 1000 still cannot traverse the
pre-existing path/ directory.
Create an explicit directory by putting an empty directory-marker key ending in
/:
d.put_object(
"path/a/private/",
b"",
uid=4000,
gid=4001,
mode=0o750,
)
This creates private/ as 4000:4001 0750. If the marker already exists, its
attributes are unchanged. When attributes are omitted, files default to
root:root 0644 and directories to root:root 0755. Automatic multipart
uploads and append-created files use the same directory rules.
The disk root itself defaults to root:root 0755, which means an
unprivileged process cannot create entries directly under the mount root. To
avoid a post-mount chown, set the root's owner and mode when creating the
disk (creation-time only; a later chown/chmod through a mount changes the
live attributes as usual):
from archil import RootAttrs
result = archil.create_disk(
name="my-disk",
root_attrs=RootAttrs(uid=1000, gid=1000, mode=0o755),
)
mode is octal (pass 0o750, not 750). On regions that don't support
rootAttrs yet the field is ignored and the disk is created with the
defaults — check the rootAttrs field on the created disk to confirm it
was applied.
rootAttrs only sets the root directory itself — it does not change how
later writes get their attributes:
- Through a mount, normal POSIX rules apply: entries are owned by the
creating process's uid/gid, with mode derived from its umask. A process
running as the
rootAttrsuid therefore owns everything it creates, with no attributes to pass anywhere. - Through
putObjectand friends, omitted attributes still mean the server defaults (root:root 0644files,root:root 0755directories) — the disk'srootAttrsis not used as a fallback. Keep passinguid/gidon object writes when a non-root process will read them.
list_objects auto-paginates by default, returning every matching key. The first argument is a key prefix; a non-recursive listing (the default) returns the immediate level as objects plus subdirectory common_prefixes:
result = d.list_objects("reports/") # one level
all_keys = d.list_objects("reports/", recursive=True) # whole subtree
first_100 = d.list_objects("reports/", limit=100) # cap the total
# Stream pages instead of buffering everything (large listings):
for page in d.list_objects_pages("reports/"):
for obj in page.objects:
print(obj.key, obj.size, obj.last_modified)
# Or drive pagination yourself:
page = d.list_objects("reports/", single_page=True)
if page.is_truncated:
nxt = d.list_objects("reports/", single_page=True, continuation_token=page.next_continuation_token)
Large uploads and bulk delete
put_object handles any size with one call. Small bodies go through a single request; large ones are uploaded as a multipart upload automatically — split into parts, uploaded with bounded concurrency, and assembled, aborting the upload if any part fails so nothing is left half-staged. You don't pick a different method for big files. For very large objects the part size is grown automatically so the upload never exceeds S3's 10,000-part limit.
# Small or multi-gigabyte — same call.
d.put_object("reports/2026-01/data.json", json.dumps(report), "application/json")
result = d.put_object(
"backups/2026-01.tar",
big_bytes,
content_type="application/x-tar",
multipart_threshold=5 * 1024 * 1024, # switch to multipart above 5 MiB; default = part_size
part_size=32 * 1024 * 1024, # >= 5 MiB; default 16 MiB
concurrency=8, # parts in flight at once; default 4
)
print(result.etag)
For manual control over the multipart lifecycle (e.g. uploading parts from separate processes), the raw S3 primitives live in the opt-in d.multipart namespace — create, upload_part, complete, abort, list_parts, list_uploads. Most code never needs these.
upload = d.multipart.create("big.bin")
p1 = d.multipart.upload_part("big.bin", upload.upload_id, 1, first_chunk)
p2 = d.multipart.upload_part("big.bin", upload.upload_id, 2, second_chunk)
d.multipart.complete("big.bin", upload.upload_id, [p1, p2])
delete_objects removes many keys in one round trip (auto-batched at S3's 1000-key limit). Unlike delete_object, per-key failures are returned rather than raised:
result = d.delete_objects(["a.txt", "logs/b.txt", "c.txt"])
for e in result.errors:
print(f"{e.key}: {e.code} {e.message}")
append_object appends bytes to an existing object (creating it if absent) — handy for log-style writes. Each call may append at most 1 MiB; append in chunks to grow past that.
d.append_object("logs/app.log", "first line\n")
d.append_object("logs/app.log", "second line\n") # concatenated
Transient failures (HTTP 429 and 5xx, plus network errors) are retried automatically with jittered exponential backoff before surfacing; caller errors (other 4xx) are not retried. The two non-idempotent operations — complete (multipart) and append_object — are not auto-retried, since a retry after a succeeded-but-unacknowledged call would return a spurious NoSuchUpload (complete) or duplicate the appended bytes (append).
Failures raise ArchilS3Error with status (HTTP status), code (the S3 error code, e.g. "NoSuchKey"), request_id, and the raw body on raw. get_object on a missing key raises a 404 — use head_object / object_exists to probe without catching. All SDK errors extend ArchilError, so except ArchilError handles control-plane and S3 failures uniformly.
The S3 endpoint is derived from your region automatically. To target a custom environment, pass s3_base_url to Archil(...) (or set the ARCHIL_S3_BASE_URL env var).
Sharing files
share mints a signed, time-limited link to a single file. Anyone with the link can download that file — no API key, no mounting. The link carries a cryptographically signed token (disk + key + expiry); when it expires it stops working.
d = archil.get_disk("dsk-abc123")
# Default lifetime is 24 hours.
link = d.share("reports/2026-01/summary.pdf")
print(link.url) # https://control.…/api/shared/<token>
print(link.expires_in) # 86400
# Set the lifetime in seconds (any positive integer, up to 604800 = 7 days):
week_link = d.share("reports/2026-01/summary.pdf", expires_in=604800)
Async
Every method on Archil, Disks, Disk, Sandboxes, Sandbox, and Tokens has an .aio variant that returns a coroutine. (The module-level helpers — configure, create_disk, create_sandbox, etc. — are synchronous convenience wrappers; from async code, construct Archil(...) directly and use .aio.) Construct the client directly and await:
import asyncio
from archil import Archil
async def main():
async with Archil(api_key="key-...", region="aws-us-east-1") as client:
d = await client.disks.get.aio("dsk-abc123")
await d.put_object.aio("a/b.txt", b"hello")
data = await d.get_object.aio("a/b.txt")
async for page in d.list_objects_pages.aio("a/"):
for obj in page.objects:
print(obj.key)
asyncio.run(main())
Multiple accounts or regions
For multi-tenant scripts, instantiate Archil directly instead of using the module-level configure:
from archil import Archil
prod = Archil(api_key=prod_key, region="aws-us-east-1")
staging = Archil(api_key=staging_key, region="aws-us-east-1")
prod_disks = prod.disks.list()
staging_disks = staging.disks.list()
Agent tools
Turn a disk — or a multi-disk workspace — into a ready-made filesystem toolset for popular agent frameworks, so you can hand an LLM a real filesystem to work in. The agent gets six tools: read_file, write_file, delete_file, list_files, grep, and run_bash (an arbitrary command in a container with the disk mounted).
import archil
from agents import Agent # OpenAI Agents SDK
d = archil.get_disk("dsk-abc123")
# Single disk → tools for your framework. The disk is the filesystem root (/).
agent = Agent(name="assistant", tools=d.agent_tools().for_openai_agents())
Or span several disks in one workspace (the same shape as exec's mounts). Each disk is a top-level directory (e.g. /data/…, /cache/…); file operations route to the right disk by path, and grep / list_files fan out across all of them. run_bash starts at the common root, so relative paths line up:
ws = archil.workspace({
"data": archil.get_disk("dsk-data"),
"cache": archil.get_disk("dsk-cache"),
})
tools = ws.agent_tools().for_langchain() # LangChain / LangGraph
A Workspace is a full filesystem in its own right — the same object API a Disk has (get_object / put_object / delete_object / list_objects / grep / exec; both satisfy the FileSystem protocol), so you can use it directly without the agent tools, and add or remove disks at runtime with ws.add_disk(name, disk) / ws.remove_disk(name). A workspace's keys carry the disk name as their first segment:
data = ws.get_object("data/reports/q1.csv") # routes to the "data" disk
ws.add_disk("scratch", archil.get_disk("dsk-tmp")) # mount another disk live
Pick the adapter for your framework — each returns that framework's native tool objects:
| Method | Framework | Returns |
|---|---|---|
.for_openai_agents() |
OpenAI Agents SDK | list[FunctionTool] |
.for_langchain() |
LangChain / LangGraph | list[StructuredTool] |
Frameworks are optional — install the matching extra:
pip install "archil[openai-agents]" # OpenAI Agents SDK
pip install "archil[langchain]" # LangChain / LangGraph
Expose a subset of tools with tools=[...] (e.g. read-only research: d.agent_tools(tools=["read_file", "list_files", "grep"])). To give an agent read-only access to a disk in a workspace, mount it with ExecMountSpec(disk=d, read_only=True) — write_file / delete_file then return an error instead of mutating it.
Connecting to a disk's data plane
To run a command against a disk, use Disk.exec() — it returns stdout, stderr, and an exit code from an Archil-managed container with the disk pre-mounted. No local filesystem involved.
To mount a disk as a real filesystem on your machine, use the archil CLI — it mounts through the OS kernel via FUSE, so any program can read and write files with standard APIs. Mounting from Python is not supported; use exec() or the S3-compatible object API instead.
Supported regions
| Region | Provider |
|---|---|
aws-us-east-1 |
AWS |
aws-us-west-2 |
AWS |
aws-eu-west-1 |
AWS |
gcp-us-central1 |
GCP |
FAQ
What's the difference between an API key and a disk token?
- API key — account-level credential for the control plane. You use one whenever you call
archil. Create and manage them at console.archil.com. Goes in theARCHIL_API_KEYenv var or theapi_keyargument. - Disk token — per-disk credential that lets a client mount a specific disk. Created automatically when you
create_disk(...)(the value is shown once; save it).
Support
Questions, feature requests, or issues? Reach us at support@archil.com.
To finish selected HTTP(S) requests before pausing, configure the sandbox network:
from archil import SandboxEgressPolicy, SandboxNetwork
sandbox = client.sandboxes.create(network=SandboxNetwork(egress=SandboxEgressPolicy(
default="allow",
drain_on_pause=[
"*.anthropic.com",
"api.openai.com",
"bedrock-runtime.*.amazonaws.com",
],
)))
sandbox = sandbox.pause()
Each entry is a lowercase hostname pattern without a scheme, port, or path.
* works anywhere; "*" selects every host. Leading *. excludes the apex domain.
All URL paths on a selected host count. Matching happens before forwarding. Selectors
do not grant network access. Pausing gates
new matches and waits for active responses, including streams, to reach the guest;
traffic to other hosts does not delay draining. The drain waits up to ten minutes by default,
starting when pause begins. Request errors stop counting; expiry proceeds with the snapshot.
Requests have no added timeout during normal operation. Only HTTP ports 80 and 443 are supported.
HTTPS clients must trust the sandbox egress CA. Connections may require retries after
resume. Configure selectors at creation: live updates cannot intercept existing
passthrough connections. A snapshot failure leaves the sandbox in failed with the
failure reason and raises SandboxPauseError when waiting. Inspect error.latest
for the resulting sandbox state.
Metadata
Release files for archil 0.17.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| archil-0.17.0.tar.gz | 90.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| archil-0.17.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 180.8 kB
Release files / archil-0.17.0.tar.gz
| Download URL | archil-0.17.0.tar.gz |
|---|---|
| Size | 90.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8fcec7446c98c4079e77ea62885ebe3470fb3a12874440241b8eb333b3ff07a8
|
|
BLAKE2b-256 checksum How to use checksums |
a622ce8d8964001405a3507554602a4773aca4026f2602c92725e813d5d4747b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / archil-0.17.0-py3-none-any.whl
| Download URL | archil-0.17.0-py3-none-any.whl |
|---|---|
| Size | 90.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a11e77b5ced019c1b0a1b514a2e96fc70e01c7e4e2270d3760aa9ea4b9de18d6
|
|
BLAKE2b-256 checksum How to use checksums |
d4854547792bc00d206812bee8ac3f71d08c8cbb03361894a1134db1d6a400fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log