sandbox

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 41 Imported by: 0

Documentation

Overview

Firecracker provider (Linux). Boots an OCI image as an ext4 rootfs with clawk-init as PID 1 and the clawk-pty-agent on vsock — the same guest stack as the vz provider — and talks to it over firecracker's hybrid vsock. No sshd: every guest session goes through the agent, like vz.

Networking mirrors vz too: the VM runs out of process under the __fcd daemon, which drives an in-process gvproxy (gvisor-tap-vsock) userspace TCP/IP stack as the guest's gateway and enforces the same per-connection egress allow-list + DNS-aware filtering (internal/netfilter.AllowList). gvproxy can't drive firecracker's TAP directly, so the daemon bridges the two with a frame pump (fcnet_linux.go).

Each sandbox gets its own L2 bridge and its own guest MAC, so the shared guest IP (every gvproxy hands out 192.168.127.2) is confined to one segment per VM — concurrent sandboxes no longer collide.

Known limitations:

  • The worktree rides in on its own disk built at Create time rather than live-mounted, so host edits don't propagate into a running guest. The default (firecracker-CI) kernel has no filesystem transport at all — no 9p, no FUSE, no virtio-fs. clawk's own published kernel has all three and boots here fine (`vm ( kernel … )`), so the missing piece is a host server wired to one of them, not the kernel. virtio-fs is the exception: firecracker ships no such device, whatever the guest supports.

Linux VM provider plumbing — bridge + TAP, firecracker-ci kernel fetch, loop-mounted rootfs. Consumed by the firecracker provider; kept separate because the asset/network plumbing is sizeable and unrelated to the hypervisor-specific spec building in firecracker_linux.go.

Index

Constants

View Source
const CurrentGuestABI = 1

CurrentGuestABI is the version of the guest-side contract baked into a sandbox's disk at create time. Two things move together under this one number: the clawk-init boot manifest schema (guestcfg.Version) and the pty-agent vsock protocol (vsockproto.ProtoVersion). It is recorded on the sandbox record (config.Sandbox.GuestABI) so a later host binary can tell — without booting anything — whether it still speaks this sandbox's dialect.

View Source
const DefaultDiskSizeGiB = 32

DefaultDiskSizeGiB is the virtual ceiling a VM root disk is grown to when the sandbox doesn't request its own size (clawk.mod `vm ( disk <size> )`). The image is sparse ext4: the guest's unwritten tail is a hole, so the ceiling bounds how far a box may grow rather than charging its full size.

The ceiling is deliberately generous: dependency caches (Go modules, Cargo registry) live on the per-VM rootfs, not on a shared mount, so a tight ceiling would fill mid-build. Physical usage is still bounded by the host's free disk; this only stops the guest hitting an artificial wall well before that.

The ceiling is not free, though: the inode table is materialized at build time at one 256-byte inode per 16 KiB of disk, so a padded rootfs costs about 1/64 of its ceiling in real host bytes before the guest writes anything — ~512 MiB at 32 GiB, ~1 GiB at 64 GiB. That is the price of not running out of inodes on a file-heavy build (see compactext4's inodesForBlocks); it's why the default is 32 and not 256. Per-VM clones reflink off the cached image, so the charge lands once per distinct (image, size) cache entry rather than once per sandbox.

OCI rootfs disks are padded sparse ext4 images sized to this ceiling (or the per-sandbox override) at build time.

View Source
const DefaultSwapSizeMiB = 2048

DefaultSwapSizeMiB is the swap device's capacity when the sandbox doesn't say otherwise. It is a ceiling on how much the guest may swap, not an allocation: the backing file is sparse and materializes host bytes only as pages are actually written to it. A sandbox that never swaps carries a 2 GiB device that costs a few hundred bytes of directory entry.

It does not shrink again on its own, though. Nothing in the stack punches the holes back: swapon(2) is asked for SWAP_FLAG_DISCARD, but neither firecracker's virtio-blk nor vz's advertises discard, so the kernel drops the flag and freed swap pages stay allocated on the host until the sandbox is destroyed. Read the number as a high-water mark — which is the reason not to make it larger just because the device is sparse.

View Source
const GuestHome = "/home/" + GuestUser

GuestHome is the agent user's home dir. Kept as a constant (not `/home/` + GuestUser expression) so it can be concatenated at compile time in other constants.

View Source
const GuestMCPConfigPath = GuestHome + "/.claude/mcp/clawk.json"

GuestMCPConfigPath is where clawk renders the sandbox's declared MCP servers inside the guest, and the path passed to the runner's --mcp-config flag (see cli.mcpConfigArgs).

It deliberately sits inside ~/.claude/ rather than at either of the places the runner would find on its own:

  • ~/.claude.json (user scope) is clawk's onboarding marker and carries a documented concurrent-write race (anthropics/claude-code#28847).
  • .mcp.json in a project root would land inside a git worktree mounted from the host, i.e. clawk would be writing config into the user's repo.

Living under ~/.claude/ also means it arrives through the per-sandbox PersistentAgentShares mount with the rest of the seeded state, and the sessions history repo ignores it by construction — that gitignore denies everything (`/*`) and only re-admits transcripts and memory.

View Source
const GuestSwappiness = 80

GuestSwappiness is the vm.swappiness clawk-init sets on a swap-enabled guest. Above the kernel's default 60 on purpose: the pages we want the guest to give up under balloon inflation are cold anonymous ones (an idle agent heap), and the page cache we want it to keep is a repo and toolchain an active build reads constantly. The default's more even split trades the wrong way for this workload.

View Source
const GuestUser = "agent"

GuestUser is the non-root Linux user the agent runs as inside every sandbox. "agent" mirrors Docker AI Sandbox's convention (docker/sandbox-templates:claude-code, UID 1000, /home/agent), which keeps our docs and the wider ecosystem's docs interchangeable.

All user-facing paths inside the VM are derived from this constant (GuestHome, WorkspaceRoot, ~/.claude/... mounts, etc.), so if we ever switch the base image's user, nothing else has to change.

View Source
const MinSupportedGuestABI = 1

MinSupportedGuestABI is the oldest recorded guest ABI this host binary still boots and attaches to. Raising it turns every sandbox created before the new floor into a "recreate me" error, so raise it only alongside an actual manifest/protocol break — and say so in the changelog.

View Source
const NinepBasePort uint32 = 1100

NinepBasePort is the first guest vsock port used by the host 9p cache servers; each ToolchainCacheShares entry gets NinepBasePort+index. Chosen clear of the fixed control ports (1024 pty-agent, 1025 time-sync, 1026 ssh-agent, 1027 mem-report, 1028 reverse-forward) with headroom so adding a cache never collides.

View Source
const OCICmdline = "console=hvc0 root=/dev/vda rw psi=1 init=" + guestcfg.InitPath +
	" clawk.cfg=/dev/vdb"

OCICmdline boots the image's flattened rootfs with the injected init. hvc0 is the vz virtio-console; root is the OCI-built ext4 on the first virtio-blk; clawk.cfg names the manifest config disk. psi=1 enables the kernel's pressure-stall information (CONFIG_PSI_DEFAULT_DISABLED is set in the Kata kernel) so the guest agent can report /proc/pressure/memory to the host balloon controller.

View Source
const OCIConfigDiskName = "guestcfg.img"

OCIConfigDiskName is the manifest disk's filename inside a sandbox's VM directory.

View Source
const OCISwapDevice = "/dev/vdc"

OCISwapDevice is where the swap disk lands in a vz OCI sandbox. Disks are attached in spec order after the rootfs, so vda=rootfs, vdb=guestcfg, and swap is the next one. Keep in lock-step with buildOCISandboxSpec's Spec.Disks in internal/cli/vzd.go.

View Source
const SwapDiskName = "swap.img"

SwapDiskName is the swap device's filename inside a sandbox's VM directory. Removed with the rest of the VM dir on destroy.

View Source
const ToolchainCachesEnabled = false

ToolchainCachesEnabled gates ToolchainCacheShares. It is false: every entry is served over 9p-over-vsock (each carries a NinePVSockPort), and that transport caused frequent, hard-to-diagnose breakage — the Go module cache and Cargo registry rely on file locking and read-only/atomic-rename semantics 9p-over-vsock does not honour reliably, surfacing as "checksum mismatch" module failures, EACCES and stalled locks, and half-written cache entries. Re-downloading a module set per VM is cheaper than that breakage (and is why sandbox.DefaultDiskSizeGiB is 32 — the caches land on the rootfs instead).

Flip this to true to restore cache sharing once the 9p transport is hardened; the specs below stay compiled and tested so nothing rots in the meantime, and the tests that assert them gate on this same constant.

View Source
const WorkspaceRoot = GuestHome + "/workspace"

WorkspaceRoot is where worktrees get mounted inside the guest. Lives under the agent user's home (same convention as Docker AI Sandbox), not at the filesystem root — many agent tools (e.g., the Claude Code skills installer) create dotfiles next to the current working directory, and $HOME is writable by default. A root-owned /workspace causes "EACCES mkdir /workspace/.agents" errors.

View Source
const WorkspaceShareTag = "workspace"

WorkspaceShareTag is the virtio-fs tag for the single consolidated worktree share: the host worktree parent (store.WorktreeDir) mounted at WorkspaceRoot. Every managed (non-in-place) worktree lives under that one host dir, so folding them into a single virtio-fs device — instead of one device per phase — keeps sandboxes with many repos under the device ceiling Apple's Virtualization.framework enforces (see machine/vz). Both the vz share list (collectSandboxShares) and the guest mount manifest (OCIGuestManifest) key off this exact tag, so they must never diverge.

Variables

View Source
var AgentStateDirs = []AgentStateDir{
	{Agent: "claude", Sub: "claude", Tag: "claude_home", GuestPath: GuestHome + "/.claude"},
	{Agent: "codex", Sub: "codex", Tag: "codex_home", GuestPath: GuestHome + "/.codex"},
	{Agent: "pi", Sub: "pi", Tag: "pi_home", GuestPath: GuestHome + "/.pi"},
	{Agent: "opencode", Sub: "opencode-data", Tag: "opencode_data",
		GuestPath: GuestHome + "/.local/share/opencode"},
	{Agent: "opencode", Sub: "opencode-config", Tag: "opencode_config",
		GuestPath: GuestHome + "/.config/opencode"},
}

AgentStateDirs lists the runner home directories clawk persists per sandbox. Every entry is one virtio-fs device, so the list is the contract both the device side (cli.collectSandboxShares) and the guest manifest (OCIGuestManifest) iterate.

Why each runner needs one: the vz rootfs is re-cloned from the image on EVERY boot (see suspendBootRootFS — a per-boot-disposable disk is the design), so anything a runner writes under $HOME on the rootfs is gone after a plain `clawk down && clawk up`, never mind a destroy. Claude survived that because its ~/.claude was mounted from the host; codex's sessions, history, and login did not, so every restart looked like a fresh install. Persisting each runner's home dir is what makes the documented "conversation memory persists across destroys" promise true for more than one runner.

Guest paths, all whole-home mounts rather than curated subdir lists — the ephemeral parts (caches, logs) measure in hundreds of KB and excluding them isn't worth the bookkeeping:

claude  ~/.claude       projects/, memory/, settings.json, .credentials.json
codex   ~/.codex        sessions/, history.jsonl, auth.json, config.toml
pi      ~/.pi           agent/{sessions,settings.json,auth.json,trust.json}

opencode is the one runner that needs two, because it follows the XDG split rather than keeping a single home (verified with `opencode debug paths`, which is the authority for its layout):

opencode  ~/.local/share/opencode  auth.json, mcp-auth.json, opencode.db, repos/
opencode  ~/.config/opencode       opencode.jsonc

Its other two XDG dirs are deliberately left on the disposable rootfs. ~/.local/state/opencode holds only locks/, and a lock that outlives the VM it was taken in is worse than no lock at all — a hard stop would leave one behind for the next boot to trip over. ~/.cache/opencode is a cache by name and contract; the only real cost is re-downloading cache/bin per sandbox, which is the same trade the toolchain caches make (see ToolchainCachesEnabled).

Cost note: every entry here is a PCIe device on every sandbox, against the ceiling documented in machine/vz (32, with a field-confirmed failure at 34). Five entries plus the workspace, the per-repo aliases, and the default capability shares still leaves room for a normal multi-repo sandbox, but this list is not free to extend — a sixth runner wanting three dirs is where the consolidation trick (one mount plus a dir-override env var, e.g. OPENCODE_CONFIG_DIR) starts paying for its complexity.

Functions

func CheckGuestABI

func CheckGuestABI(sb *config.Sandbox) error

CheckGuestABI refuses, with recreate guidance, when the guest binaries baked into sb's disk are outside what this host supports. Called before boot and before attach: it is the readable version of the failure the guest itself would otherwise produce mid-boot (clawk-init manifest check) or mid-attach (pty-agent handshake check).

func ClearOAuthToken

func ClearOAuthToken(clawkRootDir string) error

ClearOAuthToken removes the persisted token. Removing a missing file is not an error — `clawk auth clear` should be idempotent so users can run it after losing track of state.

func ConsoleTail

func ConsoleTail(path string, n int) string

ConsoleTail returns the last n non-empty lines of a guest console log, framed for embedding in a boot-failure message. Returns "" when the log is missing or empty — callers append it to their error text unconditionally.

Boot failures are diagnosed from the guest console essentially every time (kernel panics, clawk-init errors, missing init binaries); making the CLI surface it directly turns "go read this file" into an answer.

func DeclaredEnvNames added in v0.4.0

func DeclaredEnvNames(sb *config.Sandbox) map[string]bool

DeclaredEnvNames is the set of guest variable names a sandbox's `env ( … )` block declares, independent of whether any of them can be resolved right now.

Separate from ResolveEnv because the two answer different questions, and conflating them is a security bug: "which names did the user speak for" must not depend on the host shell. cli.buildVSockEnv suppresses clawk's own injected variables for every declared name, and deriving that set from ResolveEnv's output meant an entry that failed to resolve (a ${HOST:?msg} whose host var left the shell) silently handed the name back to clawk — re-injecting the Anthropic OAuth token into a sandbox whose clawk.mod had explicitly disowned it. See config.MCPServer for why that token must not reach a third-party endpoint.

Entries that don't parse are skipped: they name nothing usable, and ResolveEnv reports them.

func EnsureSwapDisk added in v0.4.0

func EnsureSwapDisk(vmDir string, sizeMiB uint64) (string, error)

EnsureSwapDisk makes vmDir hold a sparse swap device of sizeMiB and returns its path. A sizeMiB of 0 removes any device a previous configuration left behind and returns "" — callers use the empty path as "attach nothing".

Resizing an existing device just truncates it. Swap contents are worthless across a boot (the guest re-formats whenever the header doesn't match the device), so there is nothing to preserve, and truncation keeps the file sparse where a rewrite would not.

func GuestWorkspace

func GuestWorkspace(p Provider) string

GuestWorkspace returns the per-provider guest workspace path, falling back to the package default when the provider does not implement WorkspaceProvider.

func HasManagedWorktree

func HasManagedWorktree(sb *config.Sandbox) bool

OCIGuestManifest assembles the clawk-init boot manifest for sb: the guest-side description of the user, network, mounts, and snapshot files clawk-init applies at boot. stateDir is the per-sandbox persistent state dir (Claude state), cacheDir the clawk cache (toolchain shares), rootDir the clawk root (~/.clawk).

Mount tags MUST stay in lock-step with the share enumeration the vz daemon exposes (collectSandboxShares in internal/cli/vzd.go) — a tag listed here but not exported by vz fails to mount in the guest, and vice versa silently hides a share. HasManagedWorktree reports whether sb has at least one non-in-place phase worktree — i.e. a worktree clawk created under store.WorktreeDir rather than pointing at the user's own repo. Only then is the consolidated WorkspaceShareTag mount emitted (an in-place-only sandbox keeps WorkspaceRoot a plain guest dir with per-phase sub-mounts). Shared by the spec side (collectSandboxShares) and the manifest side (OCIGuestManifest) so both agree on whether the parent device exists.

func InPlaceWorktreeTag

func InPlaceWorktreeTag(worktreePath string) string

InPlaceWorktreeTag is the virtio-fs tag for an in-place worktree device (from `clawk here`, where the worktree is the user's own directory). Keyed on the full worktree path. Note the guest mount POINT keeps the readable basename; only the opaque tag is bounded.

func InitNetNSHelpers added in v0.3.0

func InitNetNSHelpers()

InitNetNSHelpers dispatches the two hidden namespace subcommands before cobra runs, and exits the process when it handles one. Returns normally when os.Args is an ordinary invocation.

Must be called at the very top of main: these paths are re-execs of the clawk binary as a helper, not CLI commands, and must never touch the CLI's config store or flag parsing.

func LogTail added in v0.3.0

func LogTail(path string, n int, label string) string

LogTail is ConsoleTail for any log file, with a caller-chosen label. The host-side daemon log needs the same treatment as the guest console: it holds the real cause of most boot failures, and on a first `clawk` in a directory the rollback deletes it moments later.

func NetModeHandoff added in v0.3.0

func NetModeHandoff(mode NetMode, why string) []string

NetModeHandoff is the environment a child process needs to inherit this process's network-mode decision verbatim — the mode so it cannot probe its way to a different answer, and the reason so it reports the truth about why.

func NetNSAvailable added in v0.3.0

func NetNSAvailable() (bool, string)

NetNSAvailable reports whether this host can create the unprivileged user+network namespace rootless mode needs, and if not, why — phrased as the thing a user would have to change.

func OAuthTokenPath

func OAuthTokenPath(clawkRootDir string) string

OAuthTokenPath returns the on-disk location of the persisted token. Public so the CLI can echo it back in `clawk auth status` and the docs can reference an unambiguous string.

func OCIGuestManifest

func OCIGuestManifest(sb *config.Sandbox, stateDir, cacheDir, rootDir string) (guestcfg.Manifest, error)

func OCIRootFS

func OCIRootFS(sb *config.Sandbox, cacheDir string, bins guestbuild.Binaries) machine.OCIImage

OCIRootFS builds the machine.OCIImage rootfs spec for sb. Every caller (provider Create pre-build, vzd boot) must produce the identical value or the digest-keyed disk cache misses and a second disk gets built.

func RecordedGuestABI

func RecordedGuestABI(sb *config.Sandbox) int

RecordedGuestABI is sb's guest ABI, resolving the zero value (records written before the field existed) to ABI 1.

func RenderMCPConfig added in v0.4.0

func RenderMCPConfig(servers []config.MCPServer) ([]byte, bool, error)

RenderMCPConfig builds the guest MCP config for a sandbox's declared servers. Returns ok=false when there is nothing to declare.

`${VAR}` references in headers and env values are passed through verbatim: the runner expands them against its own process environment when it connects, so no credential value is ever written here. That matters because this file is created on the HOST, inside the sandbox state dir — the same reason config.Sandbox.RequiredEnv stores names rather than values. The values reach the runner's environment through the vsock handshake instead (ResolveEnv → cli.buildVSockEnv).

A stdio server's env is rendered as NAME=${NAME} for the same reason: clawk names the variable, the runner supplies the value.

func RepoShareTag

func RepoShareTag(repoPath string) string

RepoShareTag is the virtio-fs tag for a managed repo's src_ alias (the device that re-exposes the repo at its original host path so worktree .git backpointers resolve in the guest). Keyed on the full repo path.

func ResolveEnv added in v0.4.0

func ResolveEnv(sb *config.Sandbox) ([]string, error)

ResolveEnv resolves a sandbox's declared `env ( … )` entries against the host process environment, returning them as canonical NAME=value strings in declaration order.

Resolution follows the envspec grammar: a bare passthrough / ${HOST} alias takes the host value (empty + a warning when unset); ${HOST:-x} / ${HOST-x} fall back to a default; a bare/quoted literal is used verbatim; and ${HOST:?msg} / ${HOST?msg} make a missing variable an error.

It is the single resolution step behind both delivery paths, so a hard failure or an unset-variable warning reads the same either way:

  • EnvFile below, which renders /etc/profile.d/99-clawk-env.sh for every login shell in the guest.
  • the pty agent's vsock handshake (internal/cli.buildVSockEnv), which spawns the runner directly — no login shell, no /etc/profile — and so has to carry the values itself.

Entries that fail to resolve are skipped but still reported, so the returned slice always holds everything that DID resolve: strict callers (sandbox create) treat a non-nil error as fatal, while best-effort callers (agent attach, which must not become unusable just because one variable left the host shell) can warn and carry on with the rest.

Values are never persisted — they're read from the host env at call time, which is why both callers re-resolve on every use.

func RootDiskSizeMiB added in v0.3.0

func RootDiskSizeMiB(sb *config.Sandbox) int

RootDiskSizeMiB is the ext4 root-disk floor for sb, in MiB: the per-sandbox `vm ( disk <size> )` override when set, otherwise DefaultDiskSizeGiB. It is a sparse floor (see machine.OCIImage.SizeMiB) — larger image content wins and the unused remainder stays a hole. Raising it is cheap but not free: see DefaultDiskSizeGiB on the inode-table cost.

func SaveOAuthToken

func SaveOAuthToken(clawkRootDir, token string) error

SaveOAuthToken persists the token at ~/.clawk/claude-oauth-token with mode 0600. Atomic via write-tmp-rename so a Ctrl-C mid-write can't leave an empty file.

func SeedClaudeMCP added in v0.4.0

func SeedClaudeMCP(stateRoot string, servers []config.MCPServer) error

SeedClaudeMCP writes (or clears) the guest MCP config in the per-sandbox state dir that PersistentAgentShares mounts at ~/.claude/. Run by the provider during sandbox preparation, alongside SeedClaudeStateDir and before the VM boots — so the servers are in place for the runner's first connection attempt, with no post-boot step and no `on create` hook.

Rewritten on every call, like settings.json: a clawk.mod edit propagates on the next `up`. An empty server list removes the file rather than leaving a stale one behind, so deleting an `mcp ( … )` entry actually retires the server.

Note this only arranges the config. Whether a server then authenticates is up to the credential its headers/env reference — see config.MCPServer.

func SeedClaudeMemory

func SeedClaudeMemory(stateRoot, seed string) error

SeedClaudeMemory writes seed into the agent's auto-memory entrypoint (<stateRoot>/claude/memory/MEMORY.md — the path the autoMemoryDirectory setting points at) the FIRST time a sandbox boots, when no memory file exists yet. It never overwrites: once the agent — or a prior session folded in via internal/sessions — has written memory, that wins. An empty seed or an already-present file is a no-op. Best-effort like SeedClaudeStateDir: failing to seed baseline knowledge must not block boot.

func SeedClaudeStateDir

func SeedClaudeStateDir(stateRoot, clawkRootDir string) error

SeedClaudeStateDir writes the host-snapshot files (settings.json, CLAUDE.md, .credentials.json) directly into the per-sandbox state dir that PersistentAgentShares mounts at ~/.claude/. Run by the provider during sandbox preparation, BEFORE the VM boots — so by the time virtio-fs mounts the dir, the files are already in place.

Semantics:

  • settings.json: overwritten on every call. Lets clawk refresh its forced overrides (bypass permissions, etc.) and propagate host settings.json edits into already-persisted sandboxes on re-create.

  • CLAUDE.md: overwritten on every call from host ~/.claude/CLAUDE.md (if present). Matches the old snapshot-at-create semantics exactly — host edits flow into newly-created sandboxes.

  • .credentials.json: written ONLY when (a) no long-lived OAuth token is configured (env var or clawk root file) and (b) the state dir doesn't already have a credentials file. The second condition preserves a refreshed token across destroy/recreate cycles, replacing the previous copy-in/copy-out dance through auth/credentials.json.

Returns the first error encountered. Best-effort: a missing host CLAUDE.md isn't an error (there's no source to copy), but failure to write into the state dir is.

func StaleLegacyBridge added in v0.3.0

func StaleLegacyBridge() (name string, stale bool)

StaleLegacyBridge reports whether the pre-per-sandbox shared bridge is still on this host with nothing attached — dead weight from an older clawk that doctor can offer to clean up. A bridge that still has members is left alone: something (a sandbox from the older clawk, or an unrelated tool that happened to pick the name) is using it.

func StateDirHasCredentials added in v0.3.0

func StateDirHasCredentials(stateRoot string) bool

StateDirHasCredentials reports whether the sandbox will boot with keychain-path credentials in place — either just seeded by SeedClaudeStateDir or persisted (and refreshed) by a previous session.

Callers use it to decide the onboarding marker: credentials are only usable if claude skips its first-run wizard, and the wizard's login step never checks for them (see ClaudeJSONMarkerFile). Runs after SeedClaudeStateDir in every boot path, so a first boot sees the file the seed just wrote.

func StdinIsTerminal added in v0.3.0

func StdinIsTerminal() bool

StdinIsTerminal reports whether we can prompt the user for a password. Exported because doctor's verdict on bridge mode turns on the same question.

func SudoIPWorksUnprompted added in v0.3.0

func SudoIPWorksUnprompted() bool

SudoIPWorksUnprompted reports whether `sudo ip` runs without asking for a password — the one privileged thing bridge mode needs.

It probes with `ip -V`, the actual binary under the actual sudoers rules: side-effect free, and accurate for a sudoers that permits only `ip` (where probing `true` or `-v` would wrongly report that a password is needed).

func SwapDiskMiB added in v0.4.0

func SwapDiskMiB(sb *config.Sandbox) uint64

SwapDiskMiB is the swap capacity for sb, in MiB, or 0 when the sandbox has swap disabled. Mirrors RootDiskSizeMiB's shape: an explicit positive override wins, negative means off, and unset takes the default.

func SwapDiskPath added in v0.4.0

func SwapDiskPath(vmDir string) string

SwapDiskPath is the swap device's host path inside vmDir.

func WriteOCIGuestConfig

func WriteOCIGuestConfig(sb *config.Sandbox, vmDir, stateDir, cacheDir, rootDir string) error

WriteOCIGuestConfig (re)writes the manifest config disk for sb into vmDir. Called before every boot so share/file changes propagate without a destroy.

Types

type AgentStateDir added in v0.4.0

type AgentStateDir struct {
	// Agent is the runner name in the CLI's agent registry, purely for
	// documentation and error messages.
	Agent string
	// Sub is the subdirectory under the sandbox's state root.
	Sub string
	// Tag is the virtio-fs tag; unique across every share on one VM.
	Tag string
	// GuestPath is where the runner looks for its state inside the VM.
	GuestPath string
}

AgentStateDir maps one coding-agent runner's home directory onto the per-sandbox host storage that backs it. See AgentStateDirs.

type Bar

type Bar struct {
	// Label names the item, shown left of the bar (e.g. "layer 3/12").
	Label string
	// Frac is the completion fraction, 0..1.
	Frac float64
}

Bar is one labelled sub-progress bar (e.g. a single layer's download) rendered on its own line beneath the current step.

type CaptureExecProvider

type CaptureExecProvider interface {
	ExecCapture(sb *config.Sandbox, command ...string) (output string, err error)
}

CaptureExecProvider is optionally implemented by providers that can run a guest command non-interactively and return its combined output. Narrated hooks (`on create` / `on up`) use it to keep successful command output off the screen and show it only on failure.

type ExitError

type ExitError struct{ Code int }

ExitError reports that an interactive guest session (a shell or an attached agent) finished with a non-zero exit status that should become clawk's own process exit code.

Provider Shell methods and the CLI's interactive-session helpers return it instead of calling os.Exit in place: that lets the cobra command's deferred cleanup run and keeps a single os.Exit site in cmd/clawk, which unwraps it with errors.As. Carrying the status as an error is also what preserves the exact code — a plain returned error collapses to exit 1.

It is deliberately NOT used for internal command execution (ExecCapture, phase-setup scripts): those failures are ordinary errors that should surface a message and exit 1, not hijack the process exit code.

func (*ExitError) Error

func (e *ExitError) Error() string

type FirecrackerProvider

type FirecrackerProvider struct {
	// contains filtered or unexported fields
}

FirecrackerProvider implements the Provider + agent interfaces using the machine/firecracker backend.

func NewFirecrackerProvider

func NewFirecrackerProvider(store *config.Store) *FirecrackerProvider

func (*FirecrackerProvider) Create

func (f *FirecrackerProvider) Create(sb *config.Sandbox) error

Create stages everything the VM boots from: the firecracker-CI kernel, the cross-compiled guest binaries, the OCI rootfs, the worktree disk, and the clawk-init manifest config disk.

func (*FirecrackerProvider) DaemonSpec

func (f *FirecrackerProvider) DaemonSpec(sb *config.Sandbox, allow *netfilter.AllowList) (machine.Spec, func(), error)

DaemonSpec returns the machine.Spec the __fcd daemon boots, plus a cleanup to run when the daemon exits. It runs in the daemon process; the returned spec carries a UserMode net in TAP-bridge mode so the firecracker backend brings up gvproxy (with allow as the egress filter) bridged to the guest's NIC.

Rootless mode builds that bridge inside a fresh user+network namespace here and now — it needs no privilege, so the daemon can do it despite having no terminal. Bridge mode instead uses host devices that ensureBridgeHostNet created in the CLI, and only verifies them, because creating them needs a sudo the daemon could never authenticate (see ensureBridgeHostNet).

func (*FirecrackerProvider) Destroy

func (f *FirecrackerProvider) Destroy(sb *config.Sandbox) error

Destroy stops the VM and removes its host devices and state.

Device teardown is best-effort and deliberately non-interactive (runSudoQuiet): a destroy that can't reach sudo should still delete the VM dir, not stop to ask for a password on the way out. It is also driven by what actually exists rather than by the current mode — a rootless sandbox has no host devices to remove (its namespace took them with it), while a sandbox created back when the host used bridge mode still does, and those must not be leaked just because rootless works now.

func (*FirecrackerProvider) Exec

func (f *FirecrackerProvider) Exec(sb *config.Sandbox, command ...string) error

Exec runs a command in the guest over the vsock agent. Used for the coding-agent attach (claude), so it's interactive.

func (*FirecrackerProvider) ExecCapture

func (f *FirecrackerProvider) ExecCapture(sb *config.Sandbox, command ...string) (string, error)

ExecCapture runs a command non-interactively and returns its combined output over the agent's frame protocol.

func (*FirecrackerProvider) FCStateDir

func (f *FirecrackerProvider) FCStateDir(sb *config.Sandbox) string

FCStateDir exposes the machine-library state dir to the __fcd daemon.

func (*FirecrackerProvider) GuestWorkspaceRoot

func (f *FirecrackerProvider) GuestWorkspaceRoot() string

GuestWorkspaceRoot: firecracker has no `agent` user and no virtio-fs, so the worktree is baked into the rootfs under /workspace.

func (*FirecrackerProvider) NetModeForLog added in v0.3.0

func (f *FirecrackerProvider) NetModeForLog() (NetMode, string)

NetModeForLog reports the mode the daemon will use and why it isn't rootless, for the daemon's startup log line.

func (*FirecrackerProvider) Shell

func (f *FirecrackerProvider) Shell(sb *config.Sandbox, workdir string) error

Shell opens an interactive login shell in the guest over the vsock agent.

func (*FirecrackerProvider) Start

func (f *FirecrackerProvider) Start(sb *config.Sandbox) error

Start spawns the detached __fcd daemon — which owns gvproxy, the frame pump, and the firecracker VM for the VM's lifetime — and returns once the pty-agent answers over vsock. The VM must outlive this CLI invocation, so (like the vz provider) the work runs in a child process, not in-process.

func (*FirecrackerProvider) Status

func (f *FirecrackerProvider) Status(sb *config.Sandbox) (string, error)

func (*FirecrackerProvider) Stop

func (f *FirecrackerProvider) Stop(sb *config.Sandbox) error

Stop signals the __fcd daemon, which tears down the VM, the frame pump, and gvproxy. A missing/stale pidfile is not an error.

The timeout MUST exceed the daemon's own graceful-stop budget (gracefulStop gives m.Stop 15s: CtrlAltDel wait 10s + SIGTERM-firecracker wait 5s). The minimal OCI guest doesn't power off on CtrlAltDel, so the daemon always spends ~10s there before falling through to SIGTERM the firecracker child. If we SIGKILL the daemon before that completes, firecracker is orphaned — it keeps the guest TAP open, and the next boot fails with "Open tap device failed: Resource busy". 25s leaves margin over the 15s budget plus the daemon's post-stop cleanup.

type HostFile

type HostFile struct {
	HostPath  string
	Content   []byte
	GuestPath string
	Mode      uint32
	Owner     string // "user:group" or "" for root
}

HostFile is a single host file snapshotted into the VM at boot from the guest manifest. Static — edits on the host require a fresh sandbox (or a future `clawk sync`) to propagate. Used for items that don't fit virtiofs's dir-only mount model.

Exactly one of HostPath or Content must be set. Content is used when the source isn't a file — e.g. a secret extracted from the macOS Keychain.

func ClaudeJSONMarkerFile

func ClaudeJSONMarkerFile(phases []config.Phase, hasAuth bool) HostFile

ClaudeJSONMarkerFile returns the HostFile that pre-creates ~/.claude.json. Two concerns share the file:

  • hasCompletedOnboarding, whenever the sandbox boots with usable credentials (hasAuth) — either a long-lived token or a seeded .credentials.json. ~/.claude.json lives on the per-boot disposable rootfs, so this marker IS the file claude reads at every startup; without the flag it re-runs the first-run wizard — including its OAuth step, which fires on the flag alone and never looks at the credentials sitting in ~/.claude/. The agent is asked to log in on every boot despite valid credentials.
  • hasTrustDialogAccepted=true for every phase worktree path the sandbox mounts, the per-repo source mounts those worktrees point at, and the workspace root. Without these, a fresh sandbox prompts "is this a project you trust?" on first claude launch in each directory — a 100% pointless gate in a VM whose only contents the user just chose to mount.

hasAuth=false leaves the flag off deliberately: with no credentials to skip to, suppressing the wizard would strand the agent in a REPL it can't authenticate from. The wizard's login step is the way out.

Always emitted: the trust block is sandbox-shape-dependent (we only know the paths after PrepareVM resolves phases), but it's always wanted.

func DefaultHostFiles

func DefaultHostFiles(clawkRootDir string) []HostFile

DefaultHostFiles returns host files snapshot-copied into each sandbox via the guest manifest. Today, the only items here are those that live OUTSIDE the per-sandbox ~/.claude/ mount:

  • /etc/profile.d/98-clawk-claude-oauth.sh (env var for the long- lived OAuth token, when configured)
  • ~/.claude.json (onboarding marker, at home root, not inside ~/.claude/)
  • ~/.gitconfig
  • ~/.ssh/known_hosts

Files that DO live inside ~/.claude/ — settings.json, CLAUDE.md, .credentials.json — are pre-seeded into the per-sandbox state dir by SeedClaudeStateDir before the VM boots; they appear at the canonical paths through the PersistentAgentShares mount, no snapshot-file involvement needed.

clawkRootDir resolves the optional long-lived OAuth token (~/.clawk/claude-oauth-token, see oauth_token.go). When configured, the profile.d export and onboarding marker are emitted; the rotating keychain blob lives only in the seeded state dir.

func EnvFile

func EnvFile(sb *config.Sandbox) (HostFile, bool, error)

EnvFile synthesizes an /etc/profile.d script that exports every sandbox-required env var. Entries come from sb.RequiredEnv (declared in clawk.mod, in canonical envspec form); values are resolved by ResolveEnv against the host's process env at this call, and a resolution failure (e.g. an unset ${HOST:?msg}) is fatal here — it fails sandbox creation with a clear message.

Written into /etc/profile.d/99-clawk-env.sh so every login shell (ssh, interactive bash, phase setup scripts, the `bash -lc` agent fallback) picks up the values without any per-tool configuration. The primary agent path does NOT go through a login shell — see ResolveEnv.

Returns ok=false if the sandbox has no required env — saves the caller from having to filter empty HostFiles.

func OAuthTokenEnvFile

func OAuthTokenEnvFile(token string) HostFile

OAuthTokenEnvFile returns the HostFile that drops an /etc/profile.d/ script exporting CLAUDE_CODE_OAUTH_TOKEN into every login shell inside the sandbox.

Mode is 0644 root-owned, deliberately readable by the agent user — /etc/profile.d/*.sh that aren't readable get silently skipped by /etc/profile, and the only non-root principal that ever runs inside the VM is `agent` anyway. The token is no more sensitive than the ~/.claude/.credentials.json blob already shipped.

The value is shell-escaped (backslash-escaping backslash, dollar, backtick, double-quote) so tokens that happen to contain shell metacharacters survive the export. Claude Code tokens are base64-ish today, but pinning that assumption into the export layer would silently break the day Anthropic changes the format.

func WorkspaceDocFile

func WorkspaceDocFile(sb *config.Sandbox) HostFile

WorkspaceDocFile returns a CLAUDE.md that gets dropped at /home/agent/workspace/CLAUDE.md on first boot. Claude Code auto-loads a CLAUDE.md in its startup CWD, so whatever's in here becomes part of the agent's instructions the moment it starts.

The content describes WHERE the agent is (a clawk VM, not the user's laptop), WHAT the layout looks like (per-phase worktrees), and WHICH constraints apply (egress allowlist, mounts, git/ssh setup). Knowing these upfront stops the agent from wasting turns investigating the environment or being surprised by a blocked connection mid-task.

type HostShare

type HostShare struct {
	HostPath       string
	Tag            string
	GuestPath      string
	ReadOnly       bool
	NinePVSockPort uint32
}

HostShare is a host directory shared into every sandbox. Tag must be unique across all shares on one VM (vz uses it as the virtio-fs mount identifier). GuestPath is where the guest mounts the share inside.

NinePVSockPort, when non-zero, marks the share for the 9p-over-vsock transport: the host runs a ninep server (internal/ninep) rooted at HostPath on that guest vsock port, and a 9p-capable clawk-init mounts it over 9p instead of virtio-fs. The virtio-fs device (Tag) is still attached as the fallback for older guests. Used for the toolchain caches, whose file counts make Apple's virtio-fs exhaust the host's kern.maxfiles. Zero = virtio-fs only (every other share). See ToolchainCacheShares.

func DefaultHostShares

func DefaultHostShares() []HostShare

DefaultHostShares returns host agent capability dirs that are safe to live read-write-share across host and VM.

We do NOT share all of ~/.claude, even though that would match local multi-terminal behavior. The Claude Code issue tracker documents real corruption from concurrent access:

  • anthropics/claude-code#28847 (.claude.json race corrupts state)
  • anthropics/claude-code#25609 (OAuth refresh race revokes tokens)
  • anthropics/claude-code#10039 (macOS Keychain deletes .credentials.json, breaking Linux co-mounted sessions)

Sharing Claude agents/commands and Codex skills is safe: user-authored capability dirs with low write contention. Sharing .claude.json, credentials, projects/, file-history/, or the rest of ~/.codex would risk concurrent writers. Each sandbox authenticates independently — one-time cost per sandbox in exchange for no cross-session corruption.

"Not shared with the host" is not the same as "not persisted": the runners' state dirs still survive down/up and destroy through PersistentAgentShares, which gives each sandbox its OWN host directory. These sub-mounts land inside those homes, so PersistentAgentShares must be assembled first or the parent mount shadows them.

~/.claude/skills is deliberately NOT shared. A skill like gstack carries a large node_modules tree, and virtio-fs caches an inode (and host fd) per file it touches; across several running sandboxes that inflates the Virtualization.framework XPC processes' fd count enough to exhaust the host's system-wide open-file table (kern.maxfiles → ENFILE). Skills that a sandbox needs can be brought in per-sandbox via an explicit `shares (...)` entry instead.

func PersistentAgentShares added in v0.4.0

func PersistentAgentShares(stateRoot string) []HostShare

PersistentAgentShares returns the per-sandbox host shares that carry each coding agent's home directory across down/up and destroy/recreate cycles. Call sites already resolved a Store so they pass the state root directly rather than re-deriving the path.

Host directories are created idempotently so virtiofs never encounters a missing source; a directory that can't be created drops its share rather than failing sandbox creation, matching ToolchainCacheShares. The guest mount points are per-sandbox storage, so they do NOT suffer the shared-state races documented on DefaultHostShares — two sandboxes can't touch the same path because each sandbox name maps to a distinct host dir.

For claude specifically, the whole-dir mount is also what lets settings.json, CLAUDE.md and .credentials.json be seeded straight into the synced dir by SeedClaudeStateDir before the mount happens (no snapshot-file vs share-mount layering issue), and lets claude's atomic write-rename credential refresh persist naturally.

Mount ordering: these shares must come BEFORE DefaultHostShares in the assembled share list. Its sub-mounts land INSIDE these homes (~/.claude/agents, ~/.claude/commands, ~/.codex/skills), and Linux would shadow them under a later parent mount.

Opt out by passing an empty stateRoot.

func ToolchainCacheShares

func ToolchainCacheShares(cacheDir string) []HostShare

ToolchainCacheShares returns host shares that back the dependency caches of common language toolchains. Returns nothing while ToolchainCachesEnabled is false — see there for why.

When enabled: mounting one host directory per cache means a module/crate is downloaded once and reused across every sandbox — preventing the multi-GB-per-sandbox blowup we observed where each new clone re-downloaded the same Go module set.

Each guest path is the toolchain's *default* cache location, so no env vars and no provision.sh changes are required: the mount is transparent. Host dirs are created on demand (virtiofs refuses missing source paths); MkdirAll failures silently drop the offending share rather than failing sandbox creation, matching the behavior of PersistentAgentShares.

Caches included (well-documented concurrent safety, real payoff):

Go modules         ~/go/pkg/mod         — file-locked, append-only;
                                          standard CI sharing pattern.
Cargo registry+git ~/.cargo/registry/{index,cache}, ~/.cargo/git/db
                                       — Cargo holds cross-process
                                         locks; the same set every
                                         Rust GHA workflow caches.

Caches deliberately excluded:

Go build cache (~/.cache/go-build) — NOT shared. It stays per-VM on
  the rootfs (Go's default location). Two reasons: (1) the build
  cache is not safe for concurrent builds against one directory
  (golang/go#43645) — racing `go build`s in different sandboxes can
  delete/move entries mid-read and corrupt each other's lookups,
  unlike the append-only, file-locked module cache; (2) it's the
  churniest tree we'd share — rewritten on every build — and over
  Apple's virtio-fs each guest-cached inode pins a host fd in the VM
  XPC process, so it dominated the descriptor blowup we saw. A build
  cache is cheap to repopulate per-VM, so the sharing payoff never
  justified the hazard.
pnpm store / uv cache / Bun cache — all three hardlink from cache
  into the working tree (node_modules, .venv). Hardlinks don't
  cross filesystems, so on a virtiofs mount these tools silently
  fall back to copying — strictly worse than no sharing, since you
  pay the copy cost AND lose dedup. Revisit only if worktrees move
  onto the same shared mount layout.
Cargo target/ — locked per-project; cross-project sharing is an
  unimplemented Rust project goal.
~/.cargo/bin and ~/.cargo/registry/src — bin/ holds executables,
  not cache; src/ is cheap to re-extract from cache/ and sharing
  it would just balloon disk usage.
Zig and pip — small footprint here; pip is superseded by uv.

Pass cacheDir = "" to opt out.

func UserHostShares

func UserHostShares(shares []config.HostShare) []HostShare

UserHostShares converts a sandbox's user-declared shares (config.HostShare from `shares (...)` in clawk.mod) into the internal HostShare form with stable virtio-fs tags.

Tags are derived from the guest path so the same share survives `clawk down && clawk up` without re-randomising — the guest manifest, the provider's --device list, and the runtime mount loop all key off the tag, and a churned tag means a stale mount entry that fails on next boot. SHA-256 hex prefix gives 8 hex chars (32 bits), collision-resistant inside a single sandbox's share list while staying short enough for virtio-fs tag length constraints.

type MockProvider

type MockProvider struct {
	Created   []string
	Started   []string
	Stopped   []string
	Destroyed []string
	Running   map[string]bool
}

MockProvider is a test double for the VM provider.

func NewMockProvider

func NewMockProvider() *MockProvider

func (*MockProvider) Create

func (m *MockProvider) Create(sb *config.Sandbox) error

func (*MockProvider) Destroy

func (m *MockProvider) Destroy(sb *config.Sandbox) error

func (*MockProvider) Exec

func (m *MockProvider) Exec(sb *config.Sandbox, command ...string) error

Exec implements ShellProvider for testing.

func (*MockProvider) Shell

func (m *MockProvider) Shell(sb *config.Sandbox, workdir string) error

Shell implements ShellProvider for testing.

func (*MockProvider) Start

func (m *MockProvider) Start(sb *config.Sandbox) error

func (*MockProvider) Status

func (m *MockProvider) Status(sb *config.Sandbox) (string, error)

func (*MockProvider) Stop

func (m *MockProvider) Stop(sb *config.Sandbox) error

type NetMode added in v0.3.0

type NetMode string

NetMode is how a sandbox's host-side network devices come into being.

const (
	NetModeRootless NetMode = "rootless"
	NetModeBridge   NetMode = "bridge"
)

func NetModePinned added in v0.3.0

func NetModePinned() (NetMode, bool)

NetModePinned reports the CLAWK_NET_MODE override, if a valid one is set. A pin is honored as-is: someone who pinned rootless does not want a silent fallback to a mode that may ask for their password.

func SelectNetMode added in v0.3.0

func SelectNetMode() (NetMode, string)

SelectNetMode reports the mode to use and, when it isn't rootless, a complete phrase saying why — "pinned by CLAWK_NET_MODE=bridge" for a deliberate override, "rootless networking unavailable: …" for a host that cannot do it.

Callers render the phrase verbatim rather than prefixing their own reason. The two facts are different — one is the operator's own configuration, the other a host capability — and reporting a pin as "rootless unavailable" handed users fixes for a restriction that wasn't there.

type OAuthTokenSource

type OAuthTokenSource string

OAuthTokenSource describes where a token came from. Used by `clawk auth status` so the user can see which copy is winning — the env var trumps the file on every Claude Code invocation, and a surprised user otherwise can't tell why edits to the file aren't taking effect.

const (
	OAuthTokenSourceNone OAuthTokenSource = ""
	OAuthTokenSourceEnv  OAuthTokenSource = "env"
	OAuthTokenSourceFile OAuthTokenSource = "file"
)

func LoadOAuthToken

func LoadOAuthToken(clawkRootDir string) (string, OAuthTokenSource)

LoadOAuthToken returns the long-lived Claude Code OAuth token to propagate into sandboxes, along with which source it came from. A token == "" / source == OAuthTokenSourceNone means none configured.

Resolution order matches the precedence we want Claude Code itself to see at runtime:

  1. CLAUDE_CODE_OAUTH_TOKEN in the host process env — convenient for CI and one-off shells.
  2. ~/.clawk/claude-oauth-token — what `clawk auth set-token` persists. Mode 0600.

Both forms produce the same effect inside the sandbox: an env var exported via /etc/profile.d/. Whitespace is trimmed so users who piped through `pbpaste` and got a trailing newline aren't surprised by a "token doesn't look valid" error inside the VM.

type PlainProgress

type PlainProgress struct{}

PlainProgress is the no-frills fallback: one line per completed step, no spinner, no transient detail. Suitable for pipes and logs.

func (PlainProgress) Close

func (PlainProgress) Close()

func (PlainProgress) Detail

func (PlainProgress) Detail(string, ...any)

func (PlainProgress) SetBars

func (PlainProgress) SetBars([]Bar)

func (PlainProgress) SetFraction

func (PlainProgress) SetFraction(float64)

func (PlainProgress) Skip

func (PlainProgress) Skip()

func (PlainProgress) Step

func (PlainProgress) Step(format string, args ...any)

func (PlainProgress) StepDone

func (PlainProgress) StepDone(format string, args ...any)

type Progress

type Progress interface {
	Step(format string, args ...any)
	Detail(format string, args ...any)
	StepDone(format string, args ...any)
	// Skip abandons the current step silently — for steps that turn out
	// to be cache hits, where a checkmark line would just be noise.
	Skip()
	// SetFraction attaches a completion fraction (0..1) to the current
	// step — renderers draw it as a progress bar. Negative clears it
	// (back to indeterminate). Implementations may ignore it.
	SetFraction(frac float64)
	// SetBars replaces the set of per-item sub-progress bars drawn under
	// the current step — one bar per concurrent layer download. Nil or
	// empty clears them. Implementations may ignore it.
	SetBars(bars []Bar)
	Close()
}

Progress narrates long-running provider work (image pulls, rootfs builds) to the user. Providers call it sequentially: Step begins a unit of work, Detail updates its live status line, StepDone replaces the step with a completion summary. Close stops any rendering; callers should defer it as soon as they obtain a tracker.

The CLI installs a spinner-based implementation on interactive terminals (see internal/cli); everything else gets PlainProgress.

type Provider

type Provider interface {
	// Create prepares the VM (image, config, etc.).
	Create(sb *config.Sandbox) error
	// Start launches the VM.
	Start(sb *config.Sandbox) error
	// Stop gracefully stops the VM.
	Stop(sb *config.Sandbox) error
	// Destroy removes all VM artifacts.
	Destroy(sb *config.Sandbox) error
	// Status returns the current VM state as a string.
	Status(sb *config.Sandbox) (string, error)
}

Provider abstracts VM lifecycle management. Implementations: vz (macOS), firecracker (Linux).

type RootExecProvider

type RootExecProvider interface {
	ExecRoot(sb *config.Sandbox, command ...string) error
}

RootExecProvider is optionally implemented by providers that can run a one-shot command as root inside the guest without relying on sudo. OCI-image sandboxes need this: arbitrary images frequently ship no sudo, but their agent runs as root and can spawn root children directly.

type ShellProvider

type ShellProvider interface {
	Shell(sb *config.Sandbox, workdir string) error
	Exec(sb *config.Sandbox, command ...string) error
}

ShellProvider is optionally implemented by providers that can open interactive shells directly (vz over the vsock agent).

type WorkspaceProvider

type WorkspaceProvider interface {
	GuestWorkspaceRoot() string
}

WorkspaceProvider is optionally implemented by providers whose guest mounts phase worktrees somewhere other than sandbox.WorkspaceRoot. Firecracker boots a bare-root rootfs without an `agent` user, so its shares live under /workspace; vz creates the agent user via clawk-init and mounts under /home/agent/workspace.

Callers that need to address phase paths inside the guest (run.go's agentStartDir, up.go's runPhaseSetup) should query this interface when present and fall back to sandbox.WorkspaceRoot otherwise.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL