Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
dda0d93
fix(store/s3): report a meta without an export as not found
CMGS Sep 20, 2026
6aeece9
fix(mcp): cap the logs tool's output like exec and read_file
CMGS Sep 20, 2026
a881cbc
fix(server): audit the buffered exec frame without its delimiter
CMGS Sep 20, 2026
e26f7d5
fix(pool): log a failed template listing at startup
CMGS Sep 20, 2026
f099f3b
fix(sdk/go): keep the relay connection when a stream open is refused
CMGS Sep 20, 2026
c0e0b56
fix(sdk/python): report a guest frame without its field as a Protocol…
CMGS Sep 20, 2026
b421b0d
fix(sdk/python): retire proxy_port's accept thread when the listener …
CMGS Sep 20, 2026
b114a4c
fix(sdk/openai): keep one sandbox handle per session
CMGS Sep 20, 2026
d3cbb62
fix(sdk/langchain): bound every tool's first claim by the call budget
CMGS Sep 20, 2026
de37522
fix(sdk/langchain): return SDK failures to the model as tool errors
CMGS Sep 20, 2026
a761567
fix(ci): lock the released silkd build and run the Go gate on its own…
CMGS Sep 20, 2026
4feb01a
fix(images): track the released silkd in the android default, assert …
CMGS Sep 20, 2026
f7f8d7d
fix(e2e): fail loudly instead of passing on an unmet premise
CMGS Sep 20, 2026
02fbbbb
review: godoc on the func it names, annotated constructor, tests ahea…
CMGS Sep 20, 2026
a824782
docs: the sandbox index fields and the intercepting proxy's idle ceiling
CMGS Sep 20, 2026
fc3f2f0
fix(sdk/python): keep the relay connection when a stream open is refused
CMGS Sep 20, 2026
dcbfd6f
fix(pool): let a warm 0 pool under warm_max grow, and return to empty
CMGS Sep 20, 2026
80222bb
fix(pool): close an archived claim's usage interval when retention pu…
CMGS Sep 20, 2026
537fa83
fix(pool): journal a hibernate that reconcile adopts
CMGS Sep 20, 2026
bf38fd9
fix(store/s3): do not tie a shared download to the caller that starte…
CMGS Sep 20, 2026
c48415d
fix(pool): run the store generation sweep off the housekeeping loop
CMGS Sep 20, 2026
d77b764
fix(packaging): stop sandboxd without signalling its service cgroup
CMGS Sep 20, 2026
7da27f7
fix(sdk/langchain, sdk/openai): default the claim to a one hour lease
CMGS Sep 20, 2026
a38e75d
docs: state what the code does where the pages said otherwise
CMGS Sep 20, 2026
f3f1a3c
review: keep a test fixture's type and its method together
CMGS Sep 20, 2026
f895a30
fix(pool): keep the empty-floor cutoff off a pool whose lead sizes it
CMGS Sep 20, 2026
18449ef
fix(store/s3): let a cancelled caller leave a stalled shared download
CMGS Sep 20, 2026
904d046
fix(pool): run one store generation sweep at a time
CMGS Sep 20, 2026
9b2c554
build: list zerolog as a direct sandboxd dependency
CMGS Sep 20, 2026
a33b56a
fix(pool): keep one claims.json retrier while the write keeps failing
CMGS Sep 20, 2026
a6eda20
fix(types, server): saturate wire seconds instead of wrapping them
CMGS Sep 20, 2026
d369b3e
perf(engine): size the preview port reader from the frame it reads
CMGS Sep 20, 2026
e4255e8
fix(sdk/python): raise a typed error when a send fails
CMGS Sep 20, 2026
b49bd8b
fix(ci): run the shell gate when the Makefile changes
CMGS Sep 20, 2026
0e57549
docs: correct this round's own sentences and six more promises
CMGS Sep 20, 2026
868089e
fix(pool): drain the latest claims sequence before the retrier lets go
CMGS Sep 20, 2026
88234dd
fix(pool): a woken archive gets the lease its claim asked for
CMGS Sep 20, 2026
abc7d02
fix(pool): pin a claim's egress policy layer when it is made
CMGS Sep 20, 2026
df530ce
fix(sdk): kill the command a canceled Run or a run timeout leaves behind
CMGS Sep 20, 2026
2e6d759
docs: lease, policy layer and cancel semantics as the code has them
CMGS Sep 20, 2026
019241b
fix(sdk/python): bound the kill a cut sends by one absolute deadline
CMGS Sep 20, 2026
51e6d90
docs: the exec cap may hold a reply up to 5 s while the kill lands
CMGS Sep 20, 2026
865cb47
fix(types): clamp a negative wire seconds count to the server default
CMGS Sep 20, 2026
9ed5cd6
fix(sdk/python): cancel the kill's watchdog before its connection is …
CMGS Sep 20, 2026
bfa3823
review: say what recommit and LeaseSeconds do now
CMGS Sep 20, 2026
f10b9b3
test(engine): pin the port reader to one bulk data frame
CMGS Sep 20, 2026
03bb9b3
docs: a wake regrants the lease the claim was granted
CMGS Sep 20, 2026
584e2fb
cut: inline the redirect walk DeleteTemplate alone used
CMGS Sep 20, 2026
8bcdbdd
cut: one routable-host check in config
CMGS Sep 20, 2026
d5ff5e0
cut: drop the pre-generation record layout readers
CMGS Sep 20, 2026
a50f82b
cut: one stub server per status in the peer probe tests
CMGS Sep 20, 2026
5454578
cut: one attach rendezvous fixture in the volume tests
CMGS Sep 20, 2026
e1a05f3
fix(sdk/python): raise typed errors from the agent handshake
CMGS Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Build silkd (musl static)
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools
(cd silkd && cargo build --release --target x86_64-unknown-linux-musl)
(cd silkd && cargo build --release --locked --target x86_64-unknown-linux-musl)
strip silkd/target/x86_64-unknown-linux-musl/release/silkd
mkdir -p dist-extra
tar -C silkd/target/x86_64-unknown-linux-musl/release \
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/sandboxd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ on:
- "mcp/**"
- "e2e/**"
- "protocol/**"
- "Makefile"
- ".golangci.yml"
- ".github/workflows/sandboxd.yml"
pull_request:
paths:
Expand All @@ -22,6 +24,8 @@ on:
- "mcp/**"
- "e2e/**"
- "protocol/**"
- "Makefile"
- ".golangci.yml"
- ".github/workflows/sandboxd.yml"
workflow_dispatch:

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/shell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ on:
branches: [main]
paths:
- "**.sh"
- "Makefile"
- ".github/workflows/shell.yml"
pull_request:
paths:
- "**.sh"
- "Makefile"
- ".github/workflows/shell.yml"
workflow_dispatch:

Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ $(LOCALBIN):

## Tool versions
GOLANGCILINT_VERSION ?= v2.13.2
GO_VERSION := $(shell sed -n 's/^go //p' sandboxd/go.mod)
GOLANGCILINT_ROOT := $(LOCALBIN)/golangci-lint-$(GOLANGCILINT_VERSION)
GOLANGCILINT := $(GOLANGCILINT_ROOT)/golangci-lint

Expand All @@ -32,7 +33,7 @@ GOLANGCILINT := $(GOLANGCILINT_ROOT)/golangci-lint
.PHONY: golangci-lint
golangci-lint: $(GOLANGCILINT)
$(GOLANGCILINT):
GOBIN=$(GOLANGCILINT_ROOT) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCILINT_VERSION)
GOBIN=$(GOLANGCILINT_ROOT) GOTOOLCHAIN=go$(GO_VERSION) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCILINT_VERSION)

cloc: ## Count lines of code excluding tests (requires cloc)
cloc --exclude-dir=target,dist,node_modules --exclude-ext=json \
Expand Down
2 changes: 1 addition & 1 deletion boot/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ RUN set -e; \
make olddefconfig; \
for s in $arch_asserts DEVTMPFS BLK_DEV_INITRD \
VIRTIO_PCI VIRTIO_BLK VIRTIO_NET \
VIRTIO_CONSOLE VIRTIO_VSOCKETS \
VIRTIO_CONSOLE VIRTIO_VSOCKETS UNIX98_PTYS \
EROFS_FS EROFS_FS_ZIP OVERLAY_FS EXT4_FS \
NAMESPACES USER_NS SECCOMP_FILTER \
BRIDGE VETH BRIDGE_NETFILTER NF_NAT NFT_COMPAT \
Expand Down
18 changes: 14 additions & 4 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ sandboxd reads one JSON file (`-config`, default
}
```

As written the `egress` pool's guests reach nothing: on a bridge lane the NIC
is locked default-deny and the proxy door is bound only for a claim that has a
policy. Give the pool an [`egress`](egress.md) block before expecting traffic;
a tenant's claim of it (`acme` above) needs the tenant's own block as well and
gets the intersection of the two.

| field | default | meaning |
|---|---|---|
| `listen` | `:7777` | control- and data-plane HTTP listener |
Expand Down Expand Up @@ -120,10 +126,10 @@ sandboxd reads one JSON file (`-config`, default
| `checkpoint_peer_heal` | false | on a cluster, lets a node pull a checkpoint it lacks from a peer — found via a live probe, not gossip — rather than failing the branch; see [placement lifecycle](cluster.md#checkpoints-on-a-cluster). Three requirements, all enforced at config load: a nonempty `api_token` (the blob transfer between peers authenticates with it; without one the raw record stream would be open), `mesh.cluster_key` set (the pull presents the fleet `api_token` to an address learned from the peer probe, so the gossip layer carrying that address must itself be authenticated), and `checkpoint_ttl_hours` nonzero (a replica a delete broadcast missed becomes eligible for expiry after it, and its next successful hourly sweep removes it — so it is the finite eligibility point, not an exact ceiling). A shared checkpoint store (`checkpoint_store` kind `s3`) ignores this setting — every node already resolves every checkpoint directly, so there is nothing to heal |
| `warm_max` (pool entry) | 0 (static) | turns on the demand-adaptive watermark for that pool: the warm target rises from `warm` toward `warm_max` while claims arrive faster than the measured provision lead covers, and decays back over ~a minute of silence. The pool follows the target down: warm VMs above it are destroyed once the count has stayed above it for a full decay period, so a burst does not leave the pool parked at its high-water mark |
| `warmup` (pool entry) | unset | argv run in the golden VM after readiness and before its snapshot, so the files it touches are page-cache-resident in every clone, and again in every clone before it joins the warm pool, so those pages are already faulted into the restored VM when the first command runs — e.g. `["node", "-e", "0"]` on a Node flavor. It runs under the engine's 2-minute command timeout in silkd's base environment (`PATH`, `TERM`, and the guest image's proxy variables wherever nothing routes directly — the none lane and the locked bridge egress lane — with the proxy not yet serving, since a door pre-bound at refill only starts serving at claim); a non-zero exit or a timeout fails the golden build, so the pool stays unfilled until the config is fixed. Config-owned like `egress`: `PUT /v1/pools` rejects it, and a golden built with a different warmup is rebuilt |
| `max_claims` | 0 (unlimited) | node-wide cap on live claims; claim/fork/branch requests beyond it answer 429 with the pool state unharmed (on a cluster, normal warm-candidate placement applies, with volume claims limited to candidates holding every requested volume) |
| `max_claims` | 0 (unlimited) | node-wide cap on live claims; claim/fork/branch requests beyond it answer 429 with the pool state unharmed (on a cluster a non-volume claim tries a warm-peer redirect first; a volume claim answers the 429 with no redirect) |
| `audit_log` | false | append every relayed frame that opens an RPC — its op + addressing fields, never payloads — to `<data_dir>/audit.jsonl`; continuation `data`, `data_end`, `stdin`, and `stdin_close` frames pass through unrecorded. The file is size-rotated with one `.1` backup. Records are `{t, id, op}` plus whichever addressing fields the op carries (`argv`, `path`, `dest`, `from`, `to`, `url`, `session`, `port`), plus `method` (`GET`, `CONNECT`, `SOCKS5`, …), `decision` and `secret` (the ref name, never its value) on `egress` records; preview accesses record as op `preview`, one per request. An opening frame whose first line exceeds 4 KiB records as op `oversized` with no addressing fields |
| `idle_hibernate_seconds` | 0 (off) | node-wide idle policy for unpooled claims (template/checkpoint claims): a none-lane claim is hibernated once it has had no open data-plane connection (relay, buffered exec, preview) and no egress request in flight for this long; the clock restarts when the last connection closes, so a long command is never cut short. The next call that reaches the guest wakes it transparently. Per-pool `idle_hibernate_seconds` does the same for that pool's claims; pooled keys ignore the node-wide value, and egress pools reject it because they cannot resume safely. Opt in deliberately: a wake costs latency and the snapshot, so callers with their own idle logic must not pay twice |
| `archive_after_seconds` | 0 (off) | tier below hibernation: a hibernated claim idle this long is checkpointed to the store and its local VM dropped, freeing the node entirely; the next call that reaches the guest restores it transparently (a checkpoint restore's latency) with a fresh server-default 5m lease. Requires `idle_hibernate_seconds > 0` and must exceed it. Node-wide for unpooled keys; per-pool overrides for that pool |
| `archive_after_seconds` | 0 (off) | tier below hibernation: a hibernated claim idle this long is checkpointed to the store and its local VM dropped, freeing the node entirely; the next call that reaches the guest restores it transparently (a checkpoint restore's latency) with a fresh lease of the length the claim was granted (the server default of 5m when it asked for none); nothing renews a lease. Keep `idle_hibernate_seconds` under the leases your claims use, or a woken sandbox that idles again is destroyed by the reap before it can hibernate, its archive already consumed by the wake; one still in use when its lease ends is destroyed the same way. Requires `idle_hibernate_seconds > 0` and must exceed it. Node-wide for unpooled keys; per-pool overrides for that pool |
| `archive_delete_after_seconds` | 0 (keep) | purge an archived claim's store checkpoint this long after it was archived, reclaiming storage; the claim is then gone for good. On archive this retention window replaces the live claim deadline; 0 clears the deadline so the archive is kept forever. Same node-wide/per-pool split |
| `mesh` | unset | join a cluster ([Clusters](cluster.md)); unset = single node |
| `pools[]` | — | warm pools, keyed by `(template, net, size)`. `warm` defaults to 4 only when both `warm` and `warm_max` are omitted; an explicit `warm: 0` is kept, so `warm: 0` under a `warm_max` starts the pool empty and grows it on demand, and `warm: 0` alone registers the key with no warm VMs (`PUT /v1/pools` never defaults `warm`); `net` is `none` or `egress`; `size` is a tier, below. Retune online without a restart via [`PUT /v1/pools`](sandboxd-api.md#put-v1pools) — omitted pools drain. This is the **first-boot seed**: once a node takes a `PUT /v1/pools`, the applied set persists to `<data_dir>/pools.json` and overrides this section on every later boot (a startup log notes it); delete `pools.json` to return to config-owned pools. Egress stays config-owned either way. See [state ownership](cluster.md#state-ownership) |
Expand Down Expand Up @@ -299,7 +305,7 @@ here validates on load:

"max_claims": 200,
"audit_log": true,
"idle_hibernate_seconds": 300,
"idle_hibernate_seconds": 120,
"archive_after_seconds": 3600,
"archive_delete_after_seconds": 604800,

Expand Down Expand Up @@ -423,6 +429,7 @@ After=network-online.target
[Service]
ExecStart=/usr/local/bin/sandboxd -config /etc/sandboxd/config.json
Restart=on-failure
KillMode=process
Environment=SANDBOXD_LOG_LEVEL=info

[Install]
Expand All @@ -435,7 +442,10 @@ degradations. `journalctl` answers an empty range with `-- No entries --`, so
a check that counts lines reads one problem where there are none — count
records (`-o json | wc -l`) or test the output for emptiness.

Stopping sandboxd leaves VMs alive; the next start reconciles them. Claimed
Stopping sandboxd leaves VMs alive; the next start reconciles them. The unit's
`KillMode=process` is what guarantees it: systemd signals sandboxd alone, so a
VMM that cocoon could not move into its own scope, and any `cocoon` call still
in flight, outlive the stop instead of dying with the service's cgroup. Claimed
sandboxes are reaped when their TTL expires (default 5m, capped at 24h).

To empty a node for maintenance, cordon it first:
Expand Down
19 changes: 14 additions & 5 deletions docs/egress.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ supported*), no authentication (the socket path already carries the sandbox's
identity), and a `DOMAINNAME` destination is resolved host-side, so the guest
still needs no resolver. Each door serves at most 256 connections per sandbox
at a time; later ones sit unserved in the socket backlog until one closes, and
the proxy keeps at most 64 idle upstream connections per sandbox, so a guest
cannot turn its proxy into a host descriptor sink.
the proxy keeps at most 64 idle upstream connections per sandbox — 64 more for
decrypted traffic when the pool intercepts — so a guest cannot turn its proxy
into a host descriptor sink.

A SOCKS5 tunnel takes the decision an HTTP `CONNECT` to the same host takes,
through the same code: a rule with a nonempty `methods` list that omits
Expand All @@ -72,7 +73,9 @@ curl --socks5-hostname 127.0.0.1:1080 imaps://imap.example.com/ # allowed: {"s

A `net:"egress"` guest owns a real NIC on the host bridge and reaches the same
proxy over the same vsock path. To stop it bypassing the proxy, sandboxd locks
the NIC at claim: an nftables netdev table (`sandbox_egress_<tap>`) with an
the NIC at claim — until then a warm VM of the pool, and the pool's golden
build, sit on the bridge unlocked, running only the image's own services, never
tenant code: an nftables netdev table (`sandbox_egress_<tap>`) with an
ingress hook on the guest's tap drops every guest-initiated packet except IPv4
broadcast DHCP, so the guest's only routed egress is the audited vsock proxy. The
lock is fail-closed — a claim whose NIC cannot be locked is rejected, not handed
Expand Down Expand Up @@ -154,7 +157,10 @@ and the secret injection that rides it — is always an explicit act. Root
claims have no tenant layer and take the pool's policy whole; a key with no
configured pool — a promoted template, or an image cold-booted on demand — has
no pool layer, so a tenant's claim of one takes the tenant's policy alone (a
root claim of one stays denied). Secrets are registered separately and
root claim of one stays denied). Which layers a claim has is settled when it
is made and kept for its life: a `PUT /v1/pools` that adds or drops a pool
changes the claims made after it, never a live one (a record from before this
rule resolves against the live pool set). Secrets are registered separately and
referenced by name — the value comes from the environment, never the config
file.

Expand Down Expand Up @@ -199,7 +205,10 @@ woken sandbox binds at arm time.
request inside an intercepted tunnel. Exact ports only; `0` and repeats are
rejected at load.
- `secret`: injects the named registered secret's header. A guest-supplied value
for the same header is overwritten. On HTTPS the injection needs `intercept`.
for the same header is overwritten. Inside an HTTPS `CONNECT` tunnel the
injection needs `intercept`; an absolute-form `https://` request on the
forward door needs none — the proxy dials the origin over verified TLS
itself, checks the method, and injects.
- `intercept`: terminate a matched HTTPS CONNECT so the request is filtered by
method and the secret injected (see below). Only a pool rule may set it.
- No policy on a claim ⇒ no egress at all (the proxy is not started).
Expand Down
14 changes: 12 additions & 2 deletions docs/langchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@ schemas, sync-native with `asyncio.to_thread` async bridges):

| tool | what it does |
|---|---|
| `sandbox_exec` | run a shell command, cut off after 5 minutes with the reply saying so; the budget is one wall clock over the claim, the dial and the command, so a first call that waits on a slow or redirecting cluster still answers inside it; stdout/stderr/exit code; disk state persists across calls |
| `sandbox_exec` | run a shell command, cut off after 5 minutes with the reply saying so (the cut also kills the command in the guest, which can hold the reply up to 5 s more); the budget is one wall clock over the claim, the dial and the command, so a first call that waits on a slow or redirecting cluster still answers inside it; stdout/stderr/exit code; disk state persists across calls |
| `sandbox_write_file` | write a text file (atomic on the guest; the parent directory must exist) |
| `sandbox_read_file` | read a text file |
| `sandbox_list_dir` | list a directory as JSON |

**Branching**: `CocoonToolkit(..., from_checkpoint="ck_...")` claims the
The claim's lease is `ttl_seconds`, one hour by default: nothing renews a
lease, and an agent run outlives the node's 5-minute default. Once the lease
ends the sandbox is gone and every later tool call reports it.

A failed call — a missing path, a guest error, an expired or unreachable
sandbox — comes back to the model as the tool's error text, not as an
exception out of the agent run. Every tool's first call claims the sandbox
inside the same 5-minute budget.

**Branching**: `CocoonToolkit(..., from_checkpoint="ck_...")` — the checkpoint
pins the template, lane and size, so `template` and `net` are ignored — claims the
sandbox from a [checkpoint](sdk-python.md#checkpoints--branching-and-time-travel)'s
captured moment instead of a clean template — agents start from prepared
state (dependencies installed, repo cloned) in milliseconds, and every
Expand Down
10 changes: 7 additions & 3 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ sandbox-mcp .`

## Tools

Every tool call is capped at 5 minutes.
Every tool call is capped at 5 minutes; an `exec` that hits the cap is killed
in the guest, which can hold its reply up to 5 s past the cap.

| tool | what it does |
|---|---|
| `create_sandbox` | claim a microVM and return its id plus deadline; optional `template`, `net` (`none` default, or `egress`), `size` (`small` default, `medium`, `large`, `xlarge`, `2xlarge`) and `ttl_seconds` (0 means one hour); warm claims take milliseconds, nothing renews the deadline |
| `exec` | run a shell command to completion; returns stdout, stderr and the exit code, each stream keeping its first 1 MiB with `truncated` set past that; a cut-off or dropped run returns the output collected so far next to an `error` field; a hibernated sandbox wakes transparently |
| `spawn` | start a command detached and return its pid; output goes to a 256 KiB ring buffer that `logs` replays |
| `ps` | list tracked processes (exec, spawn, pty) with state, exit code and start time |
| `logs` | replay up to 256 KiB of a tracked process's newest whole stdout/stderr chunks (+ exit code once ended) |
| `logs` | replay up to 256 KiB of a tracked process's newest whole stdout/stderr chunks (+ exit code once ended); an exited process is forgotten after 5 minutes |
| `kill` | signal a tracked process (0 = SIGKILL); an exited process is a no-op success |
| `write_file` / `read_file` / `list_dir` | atomic whole-file write (parent must exist); whole-file text read of a regular file up to 1 MiB (a larger file, a directory or a device is an error, invalid UTF-8 replaced, missing path is an error); one-level listing of `{name, kind, size}` entries |
| `fork` | clone into N children (1 to the node's `max_fork_count`, default 16) carrying exact memory + disk state, all-or-nothing; the parent keeps running and each child lives one hour |
Expand All @@ -44,7 +45,10 @@ Every tool call is capped at 5 minutes.
| `node_info` | warm pools, live claims, drain state, capacity and mesh peers |

Sandbox handles (and their tokens) are held by the server process for the
session. Checkpoints outlive sessions: `branch_checkpoint` accepts any known
session, and released with it: when the MCP client disconnects or the server
is signalled, it destroys every sandbox that session claimed, whatever
`ttl_seconds` asked for. `checkpoint` first if the state must survive.
Checkpoints outlive sessions: `branch_checkpoint` accepts any known
id without a listing round-trip. If the connected node does not hold it, the
claim follows a live owner probe and redirect, or heals the checkpoint locally
when peer healing is enabled. `delete_checkpoint` is different: it acts on the
Expand Down
5 changes: 3 additions & 2 deletions docs/openai-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pair over the sync [Python SDK](sdk-python.md), bridged with
| SDK surface | cocoon |
|---|---|
| `create` | `Client.new` — one claimed sandbox per session |
| session `exec` | `Sandbox.run` (stdout/stderr/exit); a per-call `timeout` is `run`'s wall clock and surfaces as `TimeoutError` |
| session `exec` | `Sandbox.run` (stdout/stderr/exit); a per-call `timeout` is `run`'s wall clock: it kills the command in the guest and surfaces as `TimeoutError` |
| `read` / `write` | `Sandbox.read_file` / `write_file`; a missing path on `read` → `FileNotFoundError`; a per-call `user` raises `NotImplementedError` (fs verbs carry no user on the wire) |
| `persist_workspace` / `hydrate_workspace` | `Sandbox.pull` / `push` (tar) |
| exposed port | `Sandbox.proxy_port` |
Expand All @@ -41,7 +41,8 @@ pair over the sync [Python SDK](sdk-python.md), bridged with

`CocoonSandboxClientOptions` carries the node address, api token, template
ref, network lane (empty for the node's default, or `none`/`egress`), and
TTL. The session state is
`ttl_seconds`, which defaults to one hour: nothing renews a lease, and a
session outlives the node's 5-minute default. The session state is
JSON-serializable, so a run can be resumed against the same sandbox after a
process restart. Requires Python 3.10+ (the Agents SDK floor); the
underlying `cocoonsandbox` stays 3.9+.
Loading
Loading