Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3215788
sdk/python: a callback's SDK error drops the exec stream instead of p…
CMGS Nov 9, 2026
d971a9f
sdk/go: Run reports a stdin read failure even when the exit frame bea…
CMGS Nov 9, 2026
4a0cb95
pool: a retention purge records its reap before the archive_delete
CMGS Nov 9, 2026
277bc21
pool: drop releaseRowTun, which never released an address
CMGS Nov 9, 2026
321a3da
pool: a dropped move-in clears its staging dir before it frees the cl…
CMGS Nov 9, 2026
45605d4
ci: run the root-only pty test with the other root-only silkd tests
CMGS Nov 9, 2026
142d363
egress: give the SOCKS non-ASCII deny test a name that lowercases int…
CMGS Nov 9, 2026
77738dd
sdk: the LangChain toolkit and OpenAI adapter close their client when…
CMGS Nov 9, 2026
9d9237d
docs: drift against the code
CMGS Nov 9, 2026
fc82e99
docs: a preview wake can hand an archived claim to a peer
CMGS Nov 9, 2026
7819281
review: own-line comments start uppercase, the retention sweep logs a…
CMGS Nov 9, 2026
289c9ca
cut: balloonTo no longer marks the sweep on for an idle target
CMGS Nov 9, 2026
c671961
review: new(mode) for the fs_write mode, done's godoc names the unwra…
CMGS Nov 9, 2026
67b1ede
cut: SILKD_NET is an Option<bool>, and the egress relay spells its Ar…
CMGS Nov 9, 2026
e107d56
review: sysutil declares its consts above the statics that read them,…
CMGS Nov 9, 2026
92a51e3
review: socket.timeout is TimeoutError on the package's Python 3.10 f…
CMGS Nov 9, 2026
8cc7da7
ci: base names its one buildx secret, and the carrier merges read the…
CMGS Nov 9, 2026
2970c69
ci: os-images drop cache only on a manual no-cache run
CMGS Nov 9, 2026
1c27f9e
review: compress the kernel fetch, agent unit and W+X narrative comme…
CMGS Nov 9, 2026
a4fb52a
cut: test fixtures reuse their helpers and constants
CMGS Nov 9, 2026
ed3071d
cut: silkd tests name their connection tuple once and spell Arc clone…
CMGS Nov 9, 2026
f6fa3f4
pool: the reap purge benchmark waits out the previous round's teardown
CMGS Nov 9, 2026
889c1ee
cut: mcp tests drop the fs_stat replies read_file no longer asks for
CMGS Nov 9, 2026
513cd63
pool: an archive row purge records a reap at the row's deadline befor…
CMGS Nov 9, 2026
d3bb888
sdk/openai: create closes its client when the claim or the session st…
CMGS Nov 9, 2026
1776223
review: test fixtures name the hour retention, the purge benchmark wa…
CMGS Nov 9, 2026
45be153
ci: silkd's root-only step runs every integration test binary
CMGS Nov 9, 2026
d9e0b9f
review: tun_e2e groups silkd with the other external crates, as the o…
CMGS Nov 9, 2026
16f0ee5
review: comments say one fact where they sit, and the kernel fragment…
CMGS Nov 9, 2026
3ac6716
pool: an archive row purge records only its archive_delete again (rev…
CMGS Nov 9, 2026
e38c762
pool: the registry-down restart test follows the orphan replica row t…
CMGS Nov 9, 2026
ad42f3c
pool: the meminfo parse error names the field once; each field alread…
CMGS Nov 9, 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
4 changes: 2 additions & 2 deletions .github/workflows/build-boot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
id: ref
# Same 7-char short SHA docker/metadata-action's type=sha emits, so
# the ref matches the tag actually pushed below.
run: echo "image=ghcr.io/${{ github.repository }}/boot:$(cat boot/kernel/VERSION)-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
run: echo "image=ghcr.io/${{ github.repository }}/boot:${{ steps.ver.outputs.kernel }}-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

- name: Download digests
uses: actions/download-artifact@v8.0.2
Expand Down Expand Up @@ -149,7 +149,7 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository }}/boot@sha256:%s ' *)

- name: Inspect
Expand Down
54 changes: 7 additions & 47 deletions .github/workflows/build-os-images.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Reusable: the images gating workflow calls this after the boot/silkd
# carriers build, passing which changed and their sha-pinned refs. It detects
# changed image dirs for the matrix, builds base then flavors, and pushes to
# ghcr; base gets its same-dir *.sh files as buildx secrets.
# ghcr; base gets install-agent.sh as a buildx secret.
# - a changed carrier (boot or silkd) refolds into every image; otherwise
# only the git-changed image dirs rebuild;
# - a base change rebuilds every flavor (they are FROM base);
Expand Down Expand Up @@ -87,7 +87,6 @@ jobs:
if [[ "$CARRIER_CHANGED" == "true" || "$MANUAL" == "true" || -z "$BASE" ]] ||
git diff --name-only "$BASE" "$SHA" -- "$wf/build-os-images.yml" "$wf/images.yml" | grep -q .; then
all_names
all_changed=""
else
all_changed=$(git diff --name-only "$BASE" "$SHA" -- 'os-image/' ':!*.md' | sort -u)
declare -A names
Expand All @@ -103,29 +102,13 @@ jobs:
fi
fi

# Build matrix entries with a per-entry no-cache flag. Cache must be
# dropped when any non-Dockerfile input changed: shared files
# directly under os-image/{name}/ (depth 3) or tag-specific files
# next to the Dockerfile (depth 4) — both are injected as buildx
# secrets, which never bust docker layer cache by content.
no_cache=false
if [[ "$MANUAL_NO_CACHE" == "true" ]]; then
no_cache=true
fi
base_entries=()
flavor_entries=()
for name in "${!names[@]}"; do
needs_no_cache="false"
if [[ "$MANUAL_NO_CACHE" == "true" ]]; then
needs_no_cache="true"
elif [[ -n "$all_changed" ]]; then
while IFS= read -r f; do
[[ -z "$f" ]] && continue
depth=$(echo "$f" | awk -F'/' '{print NF}')
base_name=$(basename "$f")
if [[ "$depth" -eq 3 ]] || { [[ "$depth" -eq 4 ]] && [[ "$base_name" != "Dockerfile" ]]; }; then
needs_no_cache="true"
break
fi
done <<< "$(echo "$all_changed" | grep "^os-image/$name/")"
fi

platforms="linux/amd64,linux/arm64"
if [[ -f "os-image/$name/platforms" ]]; then
platforms=$(cat "os-image/$name/platforms")
Expand All @@ -135,7 +118,7 @@ jobs:
[[ -z "$dockerfile" ]] && continue
tag=$(echo "$dockerfile" | cut -d'/' -f3)
if [[ -n "$tag" ]]; then
entry="{\"name\":\"$name\",\"tag\":\"$tag\",\"no_cache\":\"$needs_no_cache\",\"platforms\":\"$platforms\"}"
entry="{\"name\":\"$name\",\"tag\":\"$tag\",\"no_cache\":\"$no_cache\",\"platforms\":\"$platforms\"}"
if [[ "$name" == "base" ]]; then
base_entries+=("$entry")
else
Expand Down Expand Up @@ -205,29 +188,6 @@ jobs:
hash=$(find "os-image/${{ matrix.name }}" -type f ! -name Dockerfile ! -name '*.md' -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1)
echo "hash=$hash" >> "$GITHUB_OUTPUT"

- name: Discover secret files
id: secrets
run: |
base="os-image/${{ matrix.name }}"
tag="${{ matrix.tag }}"
{
echo "files<<EOF"
for f in "$base"/*.sh; do
[ -f "$f" ] || continue
name="$(basename "$f")"
id="sandbox_$(echo "${name%.sh}" | tr '.-' '__')"
echo "${id}=${f}"
done
for f in "$base/$tag"/*; do
[ -f "$f" ] || continue
name="$(basename "$f")"
[ "$name" = "Dockerfile" ] && continue
id="$(echo "$name" | tr '.-' '__')"
echo "${id}=${f}"
done
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Build and push
uses: docker/build-push-action@v7.4.0
with:
Expand All @@ -245,7 +205,7 @@ jobs:
INPUTS_HASH=${{ steps.inputs.outputs.hash }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
secret-files: ${{ steps.secrets.outputs.files }}
secret-files: sandbox_install_agent=os-image/base/install-agent.sh
no-cache: ${{ matrix.no_cache == 'true' }}
cache-from: type=gha,scope=${{ matrix.name }}-${{ matrix.tag }}
cache-to: type=gha,mode=max,scope=${{ matrix.name }}-${{ matrix.tag }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-silkd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ github.repository }}/silkd@sha256:%s ' *)

- name: Inspect
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/silkd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ jobs:

- name: root-only tests
working-directory: silkd
run: sudo -E env "PATH=$PATH" "HOME=$HOME" cargo test --locked --test exec_e2e --test git_e2e --test fs_e2e --test find_e2e --test tun_e2e -- --ignored --skip throughput
run: sudo -E env "PATH=$PATH" "HOME=$HOME" cargo test --locked --test '*' -- --ignored --skip throughput
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,14 @@ sandboxd](docs/deploy.md) covers release binaries, the full config and systemd.
`boot/**`, `silkd/**`, or `os-image/**` it builds the
changed carriers (via `build-boot.yml` / `build-silkd.yml`,
`workflow_call`) then os-images, in order, so the chain is deterministic
- `build-os-images.yml` — bakes base + flavors FROM the sha-pinned carriers
- `build-os-images.yml` — bakes base + flavors FROM the carriers, sha-pinned
when rebuilt in the same run
- `release.yml` — on a version tag, checks the tag against silkd's version,
then builds the release binaries and archives
- `publish-pypi.yml` — on an `sdk-*-v*` tag, builds and publishes the
matching package via PyPI Trusted Publishing (OIDC, per-package
environment)

On a fresh repo run build-boot first — images build `FROM` the boot artifact.

## Boot chain

```
Expand Down
14 changes: 2 additions & 12 deletions boot/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
xz-utils curl ca-certificates python3 \
&& rm -rf /var/lib/apt/lists/*
ARG KERNEL_VERSION
# Mirror chain: KERNEL_MIRROR (cdn.kernel.org by default), then kernel.org's
# edge mirror and an rsync mirror. Download to a file first — a mid-stream
# failure must not leave a half-extracted tree — and verify against the pinned SHA256
# (kernel/SHA256, updated together with VERSION): the fallback mirrors are
# third parties and this kernel boots untrusted-code sandboxes.
ARG KERNEL_MIRROR=https://cdn.kernel.org/pub/linux/kernel
WORKDIR /linux
COPY kernel/SHA256 /tmp/SHA256
# The hash is taken from the exact line naming this kernel version, so a
# multi-line SHA256 file (e.g. keeping the previous kernel's entry during a
# bump) can never pair the tarball with the wrong hash.
# Mirrors are third parties, so the tarball is checked against this version's kernel/SHA256 line before extraction.
RUN set -e; \
hash=$(awk -v f="linux-${KERNEL_VERSION}.tar.xz" '$2==f{print $1; exit}' /tmp/SHA256); \
[ -n "$hash" ] || { echo "kernel/SHA256 has no entry for linux-${KERNEL_VERSION}.tar.xz"; exit 1; }; \
Expand All @@ -36,10 +29,7 @@ RUN set -e; \
ARG TARGETARCH
COPY kernel/sandbox.config /tmp/sandbox.config
COPY kernel/sandbox-arm64.config /tmp/sandbox-arm64.config
# merge_config only warns and olddefconfig silently drops renamed or
# dependency-orphaned symbols — assert the boot- and container-critical set
# so a kernel bump cannot lose one with a green build. Boot entry and console
# are per-arch: amd64 = PVH, arm64 = direct Image boot + PL031 RTC.
# olddefconfig silently drops renamed or orphaned symbols, so assert the boot- and container-critical set.
RUN set -e; \
case "$TARGETARCH" in \
amd64) make x86_64_defconfig kvm_guest.config; \
Expand Down
7 changes: 3 additions & 4 deletions boot/kernel/sandbox.config
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# sandbox guest kernel fragment, applied over `x86_64_defconfig` +
# `kvm_guest.config` (see boot/Dockerfile). Everything the boot path touches
# sandbox guest kernel fragment, applied over the arch defconfig (plus
# `kvm_guest.config` on x86_64; see boot/Dockerfile). Everything the boot path touches
# is built in (=y): no modules to load, no initramfs modprobe, no udev settle.

## No module support at all — the strongest "nothing to wait for" guarantee.
Expand Down Expand Up @@ -196,6 +196,5 @@ CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y
# CONFIG_KEXEC is not set
# CONFIG_FTRACE is not set
CONFIG_DEBUG_INFO_NONE=y
# Boot-time W+X page-table scan: debug-only cost on every boot (it also
# flagged 6 W+X pages on the PVH path — tracked separately).
# Boot-time W+X page-table scan: debug-only cost on every boot.
# CONFIG_DEBUG_WX is not set
4 changes: 3 additions & 1 deletion docs/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,9 @@ release and adoption an evacuation uses:
`disk_reserve`, under `memory_limit` and under `max_running`.
- When a peer adopts, the owner forgets the claim and the wake answers 404, as
after an evacuation. The client's `Lookup` finds the peer, which wakes the
claim there. The hand-off counts in `sandboxd_wake_handoffs_total`.
claim there. The hand-off counts in `sandboxd_wake_handoffs_total`. A preview
URL minted before the hand-off stops working until it is minted again, as
after an evacuation.
- When no peer adopts (no peer, or every peer full or closed), the owner takes
the row back and the wake answers 429 as before. A node runs one hand-off at
a time, and after one fails it asks no peer for 30 seconds, so clients
Expand Down
4 changes: 2 additions & 2 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ golden fail the same way, it is retired again at most once every 30 s.

The systemd unit is
[`packaging/sandboxd.service`](https://github.com/cocoonstack/sandbox/blob/main/packaging/sandboxd.service);
install it as shipped. Its `KillMode` and `TimeoutStopSec` are explained below.
install it as shipped. The unit runs `/usr/local/bin/sandboxd -config /etc/sandboxd/config.json`, and its `KillMode` and `TimeoutStopSec` are explained below.
`LimitCORE=0` keeps a crash from writing sandboxd's memory, which holds every
running claim's host-only env values, to a core file. For the same reason,
run sandboxd hosts with swap off or encrypted.
Expand Down Expand Up @@ -1148,7 +1148,7 @@ all at once or not at all. Every field falls into one of three classes:
- per pool: `egress`, `warmup`, `capture_trim`, `storage`, `reader`, `egress_upstream_env`, `tun`, for existing keys and for keys new in the file;
- `tun`: `exits`, `mtu`, `port` and `limits`; its `subnet`, `listen`, `vni`, `senders` and `reuse_after_seconds`, and adding or removing the block, need a restart;
- `egress_classes`: each class's `egress` and `egress_upstream_env`;
- node-wide: `egress_internal_allow`, `secrets`, `egress_upstream`, `egress_usage_bytes`, `egress_usage_interval_seconds`, `egress_conn_rate`, `disk_reserve`, `memory_limit`, `max_claims`, `max_running`, `move_concurrency`, `max_fork_count`, `release_delay_seconds`, `vmm_restart`, `sync_claims`, `checkpoint_ttl_hours`, and the unpooled defaults `idle_hibernate_seconds`, `archive_after_seconds` and `archive_delete_after_seconds`.
- node-wide: `egress_internal_allow`, `secrets`, `egress_upstream`, `egress_usage_bytes`, `egress_usage_interval_seconds`, `egress_conn_rate`, `disk_reserve`, `memory_limit`, `balloon`, `max_claims`, `max_running`, `move_concurrency`, `max_fork_count`, `release_delay_seconds`, `vmm_restart`, `sync_claims`, `checkpoint_ttl_hours`, and the unpooled defaults `idle_hibernate_seconds`, `archive_after_seconds` and `archive_delete_after_seconds`.
- **Left to their API**: a pool's targets (`warm`, `warm_max`, idle and archive
durations) belong to `PUT /v1/pools`. A reload never changes them and lists
them as `ignored`. A pool key new in the file gets its settings at once and stays
Expand Down
2 changes: 1 addition & 1 deletion docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ keep an idle relay for 30 seconds and send later RPCs on it; use mode C of
## Relay connection reuse

Every data-plane RPC used to pay a TCP dial, an HTTP Upgrade and, behind a
TLS edge, a handshake. From proto 2 silkd serves RPCs back to back, and both
TLS edge, a handshake. silkd now serves RPCs back to back on one connection, and both
SDKs park up to 8 idle relay connections per handle for a 30s idle window.

`e2e/cmd/rpcbench`, warm `rt:24.04` sandbox, `net=none`, n=200 `fs_stat`
Expand Down
10 changes: 5 additions & 5 deletions docs/sandboxd-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ or the operator token, which claims as root but never with `volumes` (403).
0, and the next call that reaches the guest restores it with a fresh lease
of the claim's length, so its next expiry archives it again; a call that
wakes it between the hibernate and the export grants that fresh lease too.
A failed hibernate or export at expiry keeps the claim and retries on every
reap tick (5 s); it never falls back to destroy. A failed claim (`failed` set, see
A failed hibernate or export at expiry keeps the claim and retries it a
minute later; it never falls back to destroy. A failed claim (`failed` set, see
[dead VMMs](deploy.md#dead-vmms-and-host-restarts)) is destroyed at expiry even
under `archive`. A volume claim or an egress-lane claim
cannot hibernate, so `archive` on one answers 409 with the same error as a
Expand Down Expand Up @@ -962,7 +962,7 @@ has no `preview_listen`. The signed token embeds the sandbox id, port, and
owner `advertise_addr`, so any node's preview listener can serve it (forwarding
to the owner's main listener) and a released sandbox's URL answers 404 —
no revocation list. A browser request to the minted URL answers 503
`lease lost; retry` for a replicated claim while its node's lease is overdue, 503 `sandbox moving to another node; retry` while an [evacuation](#post-v1drain) has yet to move it, and 429 when it has to wake a sandbox while the disk reserve is low or the node is at `max_running`. See [deploy](deploy.md#preview-urls).
`lease lost; retry` for a replicated claim while its node's lease is overdue, 503 `sandbox moving to another node; retry` while an [evacuation](#post-v1drain) has yet to move it, and 429 when it has to wake a sandbox while the disk reserve is low or the node is at `max_running`; a wake that [hands an archived claim to a peer](cluster.md#waking-an-archived-claim-on-a-full-node) answers 404, as after a move. See [deploy](deploy.md#preview-urls).

## POST /v1/sandboxes/{id}/checkpoint

Expand Down Expand Up @@ -1273,8 +1273,8 @@ archive/unarchive/archive-delete counters, `sandboxd_dns_malformed_total` (guest
for average latency, `sandboxd_claim_duration_seconds{tier}`, a histogram of pool
claims by tier, `sandboxd_claim_redirects_total`, claims this node handed to a
peer, `sandboxd_wakes_refused_total{reason}`, wakes `max_running` (`max_running`),
`disk_reserve` (`disk`) refused with 429, and wakes `memory_limit` (`memory`)
refused here whether or not a hand-off ran, `sandboxd_wake_handoffs_total`,
`disk_reserve` (`disk`) and `memory_limit` (`memory`)
refused here, whether or not a hand-off ran, `sandboxd_wake_handoffs_total`,
archived claims this node [handed to a peer with room](cluster.md#waking-an-archived-claim-on-a-full-node)
instead, and `sandboxd_wake_memory_fallbacks_total`, archived wakes over
`memory_limit` this node woke itself because no hand-off ran or no peer adopted. `sandboxd_balloon_inflations_total`, `sandboxd_balloon_deflations_total` and `sandboxd_balloon_failures_total` count the balloon sweep's changes and the ones that failed, which retry a minute later. On a node with a `tun` block it adds the packet path's counters:
Expand Down
4 changes: 2 additions & 2 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ out, err := sb.Exec(ctx, "cat", "/tmp/state") // sessions & memory intact
guest does can fall between the snapshot point and the stop. The handle
stays valid: the first call that reaches the guest restores the VM (adding
roughly a restore's latency, tens of milliseconds on bare metal). On a node at
[`max_running`](deploy.md#configuration) that call returns a 429 `*APIError`,
[`max_running`](deploy.md#configuration), or below `disk_reserve`, that call returns a 429 `*APIError`,
which the SDK does not retry, and the sandbox stays hibernated. The TTL
keeps running — a hibernated sandbox is still reaped at its deadline, so
claim with a `WithTimeout` that covers the idle period or `Renew` before it
Expand Down Expand Up @@ -705,7 +705,7 @@ sb = client.Attach(ownerAddr, id, token) // bind a known handle, no lookup roun

err = client.PutTenant(ctx, sandbox.TenantSpec{Name: "u:42", Token: tok, MaxClaims: 10, EgressClass: "desk"})
err = client.DeleteTenant(ctx, "u:42")
res, _ = client.PutTenantCluster(ctx, sandbox.TenantSpec{Name: "u:42", MaxClaims: 20}) // token kept
res, _ = client.PutTenantCluster(ctx, sandbox.TenantSpec{Name: "u:42", MaxClaims: 20, EgressClass: "desk"}) // token kept
res, _ = client.DeleteTenantCluster(ctx, "u:42")
page, _ := client.Tenants(ctx, "", 0) // page.Tenants: names, caps, live claims, removed tenants; never tokens; page.Next pages
page, _ = client.SetTenants(ctx, specs) // replace the whole set
Expand Down
Loading
Loading