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
- Variables
- func CheckGuestABI(sb *config.Sandbox) error
- func ClearOAuthToken(clawkRootDir string) error
- func ConsoleTail(path string, n int) string
- func DeclaredEnvNames(sb *config.Sandbox) map[string]bool
- func EnsureSwapDisk(vmDir string, sizeMiB uint64) (string, error)
- func GuestWorkspace(p Provider) string
- func HasManagedWorktree(sb *config.Sandbox) bool
- func InPlaceWorktreeTag(worktreePath string) string
- func InitNetNSHelpers()
- func LogTail(path string, n int, label string) string
- func NetModeHandoff(mode NetMode, why string) []string
- func NetNSAvailable() (bool, string)
- func OAuthTokenPath(clawkRootDir string) string
- func OCIGuestManifest(sb *config.Sandbox, stateDir, cacheDir, rootDir string) (guestcfg.Manifest, error)
- func OCIRootFS(sb *config.Sandbox, cacheDir string, bins guestbuild.Binaries) machine.OCIImage
- func RecordedGuestABI(sb *config.Sandbox) int
- func RenderMCPConfig(servers []config.MCPServer) ([]byte, bool, error)
- func RepoShareTag(repoPath string) string
- func ResolveEnv(sb *config.Sandbox) ([]string, error)
- func RootDiskSizeMiB(sb *config.Sandbox) int
- func SaveOAuthToken(clawkRootDir, token string) error
- func SeedClaudeMCP(stateRoot string, servers []config.MCPServer) error
- func SeedClaudeMemory(stateRoot, seed string) error
- func SeedClaudeStateDir(stateRoot, clawkRootDir string) error
- func StaleLegacyBridge() (name string, stale bool)
- func StateDirHasCredentials(stateRoot string) bool
- func StdinIsTerminal() bool
- func SudoIPWorksUnprompted() bool
- func SwapDiskMiB(sb *config.Sandbox) uint64
- func SwapDiskPath(vmDir string) string
- func WriteOCIGuestConfig(sb *config.Sandbox, vmDir, stateDir, cacheDir, rootDir string) error
- type AgentStateDir
- type Bar
- type CaptureExecProvider
- type ExitError
- type FirecrackerProvider
- func (f *FirecrackerProvider) Create(sb *config.Sandbox) error
- func (f *FirecrackerProvider) DaemonSpec(sb *config.Sandbox, allow *netfilter.AllowList) (machine.Spec, func(), error)
- func (f *FirecrackerProvider) Destroy(sb *config.Sandbox) error
- func (f *FirecrackerProvider) Exec(sb *config.Sandbox, command ...string) error
- func (f *FirecrackerProvider) ExecCapture(sb *config.Sandbox, command ...string) (string, error)
- func (f *FirecrackerProvider) FCStateDir(sb *config.Sandbox) string
- func (f *FirecrackerProvider) GuestWorkspaceRoot() string
- func (f *FirecrackerProvider) NetModeForLog() (NetMode, string)
- func (f *FirecrackerProvider) Shell(sb *config.Sandbox, workdir string) error
- func (f *FirecrackerProvider) Start(sb *config.Sandbox) error
- func (f *FirecrackerProvider) Status(sb *config.Sandbox) (string, error)
- func (f *FirecrackerProvider) Stop(sb *config.Sandbox) error
- type HostFile
- type HostShare
- type MockProvider
- func (m *MockProvider) Create(sb *config.Sandbox) error
- func (m *MockProvider) Destroy(sb *config.Sandbox) error
- func (m *MockProvider) Exec(sb *config.Sandbox, command ...string) error
- func (m *MockProvider) Shell(sb *config.Sandbox, workdir string) error
- func (m *MockProvider) Start(sb *config.Sandbox) error
- func (m *MockProvider) Status(sb *config.Sandbox) (string, error)
- func (m *MockProvider) Stop(sb *config.Sandbox) error
- type NetMode
- type OAuthTokenSource
- type PlainProgress
- type Progress
- type Provider
- type RootExecProvider
- type ShellProvider
- type WorkspaceProvider
Constants ¶
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
const OCIConfigDiskName = "guestcfg.img"
OCIConfigDiskName is the manifest disk's filename inside a sandbox's VM directory.
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.
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.
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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
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
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 ¶
GuestWorkspace returns the per-provider guest workspace path, falling back to the package default when the provider does not implement WorkspaceProvider.
func HasManagedWorktree ¶
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 ¶
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
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
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
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 ¶
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 OCIRootFS ¶
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 ¶
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
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 ¶
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
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
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 ¶
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
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 ¶
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 ¶
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
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
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
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
SwapDiskPath is the swap device's host path inside vmDir.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 {
}
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
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 ¶
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 ¶
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) Exec ¶
func (m *MockProvider) Exec(sb *config.Sandbox, command ...string) error
Exec implements ShellProvider for testing.
type NetMode ¶ added in v0.3.0
type NetMode string
NetMode is how a sandbox's host-side network devices come into being.
func NetModePinned ¶ added in v0.3.0
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
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:
- CLAUDE_CODE_OAUTH_TOKEN in the host process env — convenient for CI and one-off shells.
- ~/.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 ¶
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.