v0.4.0: swap, serial forwarding, MCP declarations, the pi runner, host-wide defaults - #15
Merged
Conversation
`require.LessOrEqual(t, d, time.Second)` failed on an emulated arm64 host at 1.19s while the cache was working perfectly. Wall-clock was the wrong instrument twice over. It is a proxy for "did not redo the work" whose budget depends entirely on the hardware the test lands on — and it was not even measuring the network, because resolveSource calls crane.Digest to turn the tag into a digest BEFORE the cache lookup, so every Build makes a registry round-trip whose latency the test cannot bound. A slow link fails it as surely as a slow CPU. The cache-hit path stats the done marker and returns; a miss re-pulls and rewrites disk.ext4. So an unchanged mtime, size and os.SameFile prove the artifact was reused — the property the timing bound stood in for, established deterministically. Verified sensitive by forcing a real rebuild: both signals flip. Unrelated to the release below: the test is unchanged since the initial release, so it stands on its own.
…r, host-wide defaults
The v0.4.0 feature set. Five threads, each pulling on the same premise — that
a sandbox should arrive configured rather than needing setup once it is up.
**Every sandbox boots with swap** (2 GiB default, `vm ( swap <size|off> )`).
The reason is the balloon controller, not the guest's appetite: under host
memory pressure clawk reclaims guest RAM against guest demand, and a guest with
nowhere to put cold anonymous pages answers with direct-reclaim stalls and
eventually its OOM killer. A multi-second stall in the agent is not merely slow
— a process that stops draining its socket lets the connection go idle, and on a
link whose NAT reaps idle mappings in under a minute that ends the streaming
response. It rides its own sparse virtio-blk device rather than a swapfile,
because swapon(2) rejects a file with holes and a swapfile would cost its full
size in real host bytes per sandbox. clawk-init writes the header itself rather
than shelling to mkswap, since the rootfs is an arbitrary OCI image. The
controller learned about swap at the same time and had to: paging out raises
MemAvailable and lowers PSI, so a squeezed guest reads as roomy on both signals
the controller uses. swapTrend holds reclaim while swap is GROWING and lets the
hold decay after two minutes of quiet, because occupancy latches — a slot is
freed only when its page faults back in — and reading the level as pressure
would retire reclaim for that sandbox permanently.
**Serial devices.** `clawk serial add <sandbox> /dev/cu.usbmodem1101` puts a
board plugged into the Mac inside the sandbox as `/dev/<name>`, so arduino-cli,
esptool and a monitor can run in there against real hardware. The USB device is
not passed through, because nothing clawk runs on can do that; what crosses is
the byte stream and the line settings, which is all any of those tools wanted.
The guest side is a PTY, the host side the real tty, opened only while a process
in the sandbox holds the device — which is also what makes auto-reset work,
since opening a port asserts DTR. A glob is resolved at open time, not at
configure time, so a board that re-enumerates into its bootloader under a
neighbouring name stays reachable. vz-only: the guest is the end that dials.
**`mcp ( … )`** declares MCP servers so a sandbox comes up with them configured,
no interactive login inside the VM. Each http/sse host is folded into a derived
`mcp` network layer ranking below anything you wrote, so a declared server is
reachable without a matching `network allow` and cannot override your own
`deny`. Credential values stay out of clawk: `clawk.mod` and the rendered guest
config hold a `${VAR}` reference, the value travels to the runner's process
environment at attach. A URL embedding credentials is refused, since it is the
one spelling that would defeat that.
**The pi runner, opencode wired up, per-runner state on the host.** `clawk run
pi` joins claude and codex, and opencode goes from registry-only to installed
and persisted. The bigger fix underneath: only `~/.claude` was host-mounted, so
codex's sessions and login lived on a rootfs vz re-clones every boot and were
discarded by a plain `down`/`up` — not just by `destroy`. Each runner's home is
now its own per-sandbox mount, opencode's two XDG dirs included.
**Host-wide defaults in `~/.config/clawk/clawk.mod`.** A kernel path, a token
alias, a personal skill mount and a house rule are properties of the host, not
of the repo they were written in. One anonymous `sandbox ( … )` block outside
any repo now supplies settings for every sandbox on the machine, as the lowest
layer of the chain. Scope is enforced, relative paths resolve against the file's
own directory, and `--no-global` drops the layer for a reproducible run. A
broken file is fatal on every create path rather than degrading to "no defaults"
— that silently discarded the repo's own clawk.mod too.
Also from the pre-release review: `env ( … )` values now reach the runner's own
process (not just its login shells) and a declaration wins over clawk's
same-named variable unconditionally, even when it fails to resolve — otherwise a
sandbox pointed at a third-party gateway got clawk's Anthropic token as an
Authorization header. And the serial hangup path flushes before letting go, so
`echo cmd > /dev/ttyACM0` is delivered rather than discarded.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five features, plus the fixes that came out of reviewing them. Three commits: the unrelated
ocitest fix, the feature set, and the changelog as the version bump.What's in it
Every sandbox boots with swap (2 GiB default,
vm ( swap <size|off> )). Motivated by the balloon controller rather than the guest's appetite: under host memory pressure clawk reclaims guest RAM against guest demand, and a guest with nowhere to put cold anonymous pages answers with direct-reclaim stalls and eventually its OOM killer. A multi-second stall in the agent process is not merely slow — a process that stops draining its socket lets the connection go idle, and on a link whose NAT reaps idle mappings in under a minute that ends the streaming response outright. It rides its own sparse virtio-blk device, becauseswapon(2)rejects a file with holes and a swapfile would cost its full size in real host bytes per sandbox.Serial devices.
clawk serial add <sandbox> /dev/cu.usbmodem1101puts a board plugged into the Mac inside the sandbox as/dev/<name>. The USB device isn't passed through — nothing clawk runs on can do that — but the byte stream and line settings are, which is allarduino-cli,esptooland a monitor ever wanted. Host port opens only while a guest process holds the device, which is also what makes auto-reset work. vz-only: the guest is the end that dials.mcp ( … )declares MCP servers so a sandbox comes up with them configured. Each http/sse host folds into a derivedmcpnetwork layer ranking below anything you wrote, so a declared server is reachable without a matchingnetwork allowand can't override your owndeny. Credential values stay out of clawk entirely —clawk.modand the rendered guest config hold a${VAR}reference.The pi runner, opencode wired up, per-runner state on the host. The fix underneath: only
~/.claudewas host-mounted, so codex's sessions and login lived on a rootfs vz re-clones every boot and were discarded by a plaindown/up, not just bydestroy.Host-wide defaults in
~/.config/clawk/clawk.mod— one anonymoussandbox ( … )block outside any repo, as the lowest layer of the precedence chain.--no-globaldrops it for a reproducible run.Review fixes worth calling out
env ( … )declaration now beats clawk's same-named variable unconditionally, including when it fails to resolve. Previously the carve-out was derived from resolved values, so a sandbox pointed at a third-party gateway got clawk's Anthropic token as anAuthorizationheader from any shell where the referenced variable was missing — and the request succeeded, so nothing surfaced.clawk.modis now fatal on every create path. Here-mode previously warned and continued, which discarded the repo's ownclawk.modtoo, and snapshot-at-create meant the misconfigured sandbox persisted.echo cmd > /dev/ttyACM0is delivered rather than discarded.POLLINandPOLLHUPare independent on a PTY master, and honouring the hangup first binned the bytes.mcpURL embedding credentials is refused — the one spelling that would put a secret value on host disk.ROOT_CLAWK_MOD→CLAWK_GLOBAL_MOD, for prefix consistency. Free now, breaking later.Testing
build,vet,gofmtand the full suites pass in both modules, and all three commits pass individually so the branch bisects. Serial was exercised end to end against a PTY standing in for a board — device appears, lazy attach, bytes both directions, mode changes, refusal of a second opener, liveserial add, detach.Not covered by CI: swap on a real boot (both providers), serial against actual hardware, and MCP end to end.
internal/e2eboots firecracker and needs/dev/kvm, so it structurally can't cover serial.Before tagging
pressure_darwin.gochanged and can't be cross-compiled, so this PR'smacos-14job is the first thing to type-check it.clawk-devbefore the tag.defaultImageis the mutable:v0, andpublish-clawk-devonly fires on pushes tomaintouchingimages/clawk-dev/**. Tagging first ships a binary advertisingclawk run pi/opencodeagainst an image that has neither.Fixes #14