config

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: 16 Imported by: 0

Documentation

Index

Constants

View Source
const (
	BlockOriginNamespace = "namespace"
	// BlockOriginMCP carries the allow entries derived from declared MCP
	// servers (see MCPServer). Deliberately the lowest-precedence stored
	// origin above "namespace": it is a convenience — clawk inferring what
	// a declared server needs to reach — so an explicit `deny` written in
	// clawk.mod ("mod") or via the CLI ("custom") must still win over it.
	BlockOriginMCP    = "mcp"
	BlockOriginMod    = "mod"
	BlockOriginCustom = "custom"
)

Block origins stored on sandbox records, lowest to highest precedence. Frozen (see VMState) — and doubly so here: blockOriginRank sends unknown strings to the lowest precedence, so a renamed origin would silently invert which rules win.

View Source
const (
	MCPTransportHTTP  = "http"
	MCPTransportSSE   = "sse"
	MCPTransportStdio = "stdio"
)

MCP transport identifiers. Persisted in sandbox records — frozen (see VMState). These are the wire names Claude Code's MCP config uses, so a rendered server config can pass them straight through.

View Source
const DefaultNamespace = "default"

DefaultNamespace is the grouping a sandbox belongs to when none is set.

View Source
const DefaultPolicyName = "default"

DefaultPolicyName is reserved for the built-in dev allowlist; it can be loaded but never saved or deleted, and it is the chain a sandbox gets when no `use` list was ever written.

View Source
const RecordSchemaVersion = 1

RecordSchemaVersion is the current shape of a sandbox record's JSON, stamped onto Sandbox.RecordSchema at every Save. Bump only when a record written by this clawk would be misread by the previous one — additive omitempty fields don't count.

Variables

View Source
var DefaultAllowedDomains = []string{}/* 139 elements not displayed */

DefaultAllowedDomains are domains allowed by default for development. Originally seeded from Andrew Lock's microVM sandbox allow list: https://andrewlock.net/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/

Every entry here is a contract in both directions: removing one after release breaks users who rely on it, and each one is a destination the agent can reach without asking — i.e. a potential exfiltration endpoint. Additions must be (a) operated by the organization they claim to serve and (b) needed by a mainstream development workflow, not one project. When in doubt, leave it out — users can `clawk network allow` per sandbox or per namespace.

View Source
var ErrPolicyNotFound = errors.New("policy not found")

ErrPolicyNotFound is returned by LoadPolicy for a name with no record on disk (and no builtin).

Functions

func CurrentSchemaVersion

func CurrentSchemaVersion() int

CurrentSchemaVersion is the schema version this build targets.

func DefaultSerialGuestName added in v0.4.0

func DefaultSerialGuestName(hostPath string) string

DefaultSerialGuestName is the guest name used when a spec doesn't give one: the host device's basename, unchanged.

Keeping the name identical on both sides is the same choice reverse forwarding makes with ports — you refer to the thing by the name you already know it by, and there is no second identifier to keep straight. A Mac's /dev/cu.usbmodem1101 is an odd name for a Linux device node, but no tool cares, and `arduino-cli -p /dev/cu.usbmodem1101` reading the same on both sides is worth more than tidiness.

Returns "" for a glob, which has no meaningful basename; callers must require an explicit name in that case.

func DisplayName

func DisplayName(key string) string

DisplayName is the human-facing form of a store key. It currently returns the key unchanged — anchored sandboxes are keyed by a clean `<base>` and carry their binding in Sandbox.Anchor, so no key needs rewriting for display. Retained as the single seam every surface (`clawk list`/`status`, the workspace CLAUDE.md, the menubar) routes through, should display names ever need to diverge from keys again.

Types

type HostFile

type HostFile struct {
	HostPath  string `json:"host_path"`
	GuestPath string `json:"guest_path"`
	Mode      uint32 `json:"mode,omitempty"`
}

HostFile is a snapshot-on-up file pushed from host into guest. Sourced from clawk.mod `files (...)` and refreshed on every `clawk up` — edits on the host are NOT live (use HostShare for that). HostPath is tilde- and env-expanded at compose time; Mode == 0 preserves the host file's permissions.

type HostShare

type HostShare struct {
	HostPath  string `json:"host_path"`
	GuestPath string `json:"guest_path"`
	ReadOnly  bool   `json:"read_only"`
}

HostShare is a virtio-fs live mount from host directory into guest. Sourced from clawk.mod `shares (...)`. Reflects on the host immediately — credentials that rotate underneath (~/.aws after `aws sts assume-role`, ~/.config/gcloud) stay in sync without a clawk-up cycle. Defaults to ReadOnly=true at parse time so an accidental in-VM write can't clobber the host credential.

type MCPServer added in v0.4.0

type MCPServer struct {
	// Name is the server's identifier, as the agent sees it. Unique
	// within a sandbox.
	Name string `json:"name"`
	// Transport is one of the MCPTransport* constants above.
	Transport string `json:"transport"`
	// URL is the endpoint for the http and sse transports; empty for stdio.
	URL string `json:"url,omitempty"`
	// Command is the argv of a stdio server, already split into words.
	// Empty for http and sse.
	Command []string `json:"command,omitempty"`
	// Headers are extra HTTP request headers in "Name: value" form, for
	// http and sse. This is where a PAT goes:
	// "Authorization: Bearer ${SENTRY_TOKEN}".
	Headers []string `json:"headers,omitempty"`
	// Env names the environment variables handed to a stdio server. Each
	// is rendered as NAME=${NAME}, so the value comes from the runner's
	// environment rather than from this record.
	Env []string `json:"env,omitempty"`
}

MCPServer is one entry of a clawk.mod `mcp ( … )` block: an MCP server made available to the agent inside the sandbox. Sourced from the namespace and clawk.mod, snapshotted onto the record at create like every other template value, and rendered into the guest by sandbox.SeedClaudeMCP.

Credentials are deliberately NOT modeled here. A `${VAR}` reference in Headers or Env is stored and rendered verbatim; the runner expands it against its own process environment at connect time (confirmed for headers, env and args). That keeps this record — and the rendered guest config, which lives on host disk under the sandbox state dir — free of secret values, exactly like RequiredEnv. The value itself travels separately: declare the variable in `env ( … )` and it reaches the runner's environment via the vsock handshake (see sandbox.ResolveEnv).

Only static-credential transports are supported. Interactive OAuth (`claude mcp login`) stores its grant inside the guest and is out of scope: it can't be arranged at create time, which is the whole point of declaring servers here.

type Namespace

type Namespace struct {
	Name           string   `json:"name"`
	AllowedDomains []string `json:"allowed_domains,omitempty"`
	AllowedIPs     []string `json:"allowed_ips,omitempty"`
	// DeniedDomains are blocked outright (a block overrides any allow). They
	// come from inline `deny` entries; `deny source "<url>"` blocklists are
	// registered as source policies referenced via Use instead.
	DeniedDomains []string `json:"denied_domains,omitempty"`
	// DeniedIPs are the IP/CIDR counterparts of DeniedDomains, from
	// `deny ip <addr>` entries in a namespace manifest.
	DeniedIPs []string `json:"denied_ips,omitempty"`
	// Use names the policies forming the base of every member sandbox's
	// chain, in increasing precedence. nil means unspecified (the built-in
	// "default" applies unless a sandbox writes its own list); a non-nil
	// list is complete. Together with the inline allow/deny entries above,
	// these resolve live at up/reload — they are never copied into
	// sandbox records, so a namespace edit propagates to existing members.
	Use    []string    `json:"use,omitempty"`
	Files  []HostFile  `json:"files,omitempty"`
	Shares []HostShare `json:"shares,omitempty"`
	Env    []string    `json:"env,omitempty"`
	// MCP are MCP servers made available to every sandbox in the namespace.
	// This is the natural place for the org-wide set: which namespace a
	// sandbox lives in then decides what it can reach, with no per-sandbox
	// configuration. Merged with a repo's clawk.mod entries by name in
	// applyNamespaceDefaults. See MCPServer.
	MCP []MCPServer `json:"mcp,omitempty"`
	// Instructions and Memory seed every sandbox in the namespace: extra
	// CLAUDE.md guidance and baseline auto-memory respectively. They merge
	// with a repo's clawk.mod equivalents — namespace first, as the broader
	// scope — in applyNamespaceDefaults.
	Instructions []string `json:"instructions,omitempty"`
	Memory       string   `json:"memory,omitempty"`
}

Namespace holds per-namespace defaults merged into every sandbox created in it: a shared network allowlist plus files/shares/env injected into each sandbox (e.g. context markdown). Stored at namespaces/<name>/namespace.json, co-located with that namespace's sandboxes — so a namespace's whole footprint (config + sandboxes) is one directory. An empty namespace is fine: it's a pure grouping until you give it defaults.

type NetworkBlock

type NetworkBlock struct {
	Origin       string   `json:"origin"`
	Name         string   `json:"name,omitempty"`
	AllowDomains []string `json:"allow_domains,omitempty"`
	AllowIPs     []string `json:"allow_ips,omitempty"`
	DenyDomains  []string `json:"deny_domains,omitempty"`
	DenyIPs      []string `json:"deny_ips,omitempty"`
}

NetworkBlock is one origin-labeled layer of a sandbox's network policy. Origins mirror the chain in the design doc: "namespace" and "mod" carry file-composed entries, "custom" carries CLI edits and persisted interactive grants. Policy-referenced layers ("default", "policy", "source") are never stored on the sandbox — they resolve from Use at up/reload so a policy edit propagates without rewriting sandbox records.

type NetworkPolicy

type NetworkPolicy struct {
	// AllowedDomains, AllowedIPs and DeniedDomains are the legacy flat
	// policy of pre-block sandbox records. Normalize folds them into the
	// "custom" block on load; new records are written block-shaped and
	// leave these empty.
	AllowedDomains []string `json:"allowed_domains,omitempty"`
	AllowedIPs     []string `json:"allowed_ips,omitempty"`
	// DeniedDomains are blocked outright — the domain and every subdomain.
	// A block overrides any allow and suppresses the interactive prompt, so
	// the agent is refused immediately without asking again. Entries are
	// registrable (root) domains: "telemetry.example.com" is blocked by an
	// entry of "example.com".
	DeniedDomains []string `json:"denied_domains,omitempty"`

	// Use names the policies whose blocks form the base of this sandbox's
	// chain, in increasing precedence. nil means the chain was never made
	// explicit and resolves to ["default"]; a non-nil list is complete —
	// include "default" where wanted.
	Use []string `json:"use,omitempty"`
	// Blocks are the sandbox's own policy layers, lowest precedence first.
	// They sit above every Use-referenced policy; the "custom" block (CLI
	// edits and persisted interactive grants) stays last, above the "mod"
	// block (entries composed from clawk.mod).
	Blocks []NetworkBlock `json:"blocks,omitempty"`
}

NetworkPolicy controls outbound network access from the sandbox.

AllowedDomains support wildcards like "*.example.com" and are matched at DNS-resolution time. AllowedIPs accept plain addresses or CIDR ranges like "10.0.0.0/24" and are checked on every TCP SYN, catching direct-IP connections (useful for provisioning new servers that don't have DNS yet).

func (*NetworkPolicy) Block

func (n *NetworkPolicy) Block(origin string) *NetworkBlock

Block returns the policy's layer with the given origin, appending an empty one if absent. The returned pointer is valid until Blocks next grows — mutate it before any further Block call.

func (*NetworkPolicy) Normalize

func (n *NetworkPolicy) Normalize()

Normalize migrates a legacy flat record into block form and restores the block-order invariant. Idempotent; the store applies it on every load.

type Phase

type Phase struct {
	Repo     string      `json:"repo"`
	Branch   string      `json:"branch"`
	Status   PhaseStatus `json:"status"`
	Order    int         `json:"order"`
	Worktree string      `json:"worktree,omitempty"` // path to the git worktree on host
	Setup    []string    `json:"setup,omitempty"`    // commands to run in the VM after every boot (template `on up`)

	// OnCreate is the list of commands to run once after the very first
	// boot of the sandbox, before the runner attaches. Sourced from the
	// repo Clawkfile's `on create` block. Hard-fails the up — see
	// Sandbox.CreatePending.
	OnCreate []string `json:"on_create,omitempty"`
	// OnCreateAt records the wall-clock time `on create` last completed
	// successfully for this phase. Zero means it has not run yet (or the
	// sandbox is in the create-pending state). Used by up.go to decide
	// whether to run `on create` again after a failure.
	OnCreateAt time.Time `json:"on_create_at,omitempty"`

	// InPlace, if true, means Worktree points at the user's actual
	// directory rather than a dedicated git worktree. Set by
	// `clawk here`. Destroy skips `git worktree remove` for these.
	InPlace bool `json:"in_place,omitempty"`
}

type PhaseStatus

type PhaseStatus string
const (
	PhaseStatusPending PhaseStatus = "pending"
	PhaseStatusActive  PhaseStatus = "active"
	PhaseStatusMerged  PhaseStatus = "merged"
)

type Policy

type Policy struct {
	Name         string   `json:"name"`
	AllowDomains []string `json:"allow_domains,omitempty"`
	AllowIPs     []string `json:"allow_ips,omitempty"`
	DenyDomains  []string `json:"deny_domains,omitempty"`
	DenyIPs      []string `json:"deny_ips,omitempty"`
	// Source is an external blocklist URL (hosts/Adblock/plain formats).
	// Fetched entries live in the sibling cache file, not here.
	Source string `json:"source,omitempty"`
	// Refresh bounds cache staleness, as a Go duration string ("24h").
	Refresh string `json:"refresh,omitempty"`
}

Policy is a named, reusable block of network rules, referenced from sandboxes via `use <name>`. Stored at policies/<name>/policy.json.

func BuiltinDefaultPolicy

func BuiltinDefaultPolicy() *Policy

BuiltinDefaultPolicy is the "default" chain block: the dev allowlist. It is never stored on disk — LoadPolicy synthesizes it so a `use default` entry always resolves.

func (*Policy) RefreshInterval

func (p *Policy) RefreshInterval() (time.Duration, error)

RefreshInterval returns the policy's refresh interval, defaulting to 24h when unset.

type PolicyCache

type PolicyCache struct {
	FetchedAt    time.Time `json:"fetched_at"`
	ETag         string    `json:"etag,omitempty"`
	DenyDomains  []string  `json:"deny_domains"`
	AllowDomains []string  `json:"allow_domains,omitempty"` // @@ exceptions
}

PolicyCache holds entries fetched from Policy.Source. Regenerable; stored at policies/<name>/cache.json.

type PortForward

type PortForward struct {
	HostPort  int `json:"host_port"`
	GuestPort int `json:"guest_port"`
}

PortForward maps a host port to a guest port. It expresses both forwarding directions; which one applies depends on the field it is stored in.

  • Sandbox.Forwards (outbound): host 127.0.0.1:HostPort → guest GuestPort, served by gvproxy. Applied at VM start time; changes require an `up` cycle to take effect.
  • Sandbox.ReverseForwards (inbound): guest 127.0.0.1:GuestPort → host 127.0.0.1:HostPort, tunnelled over vsock (see internal/revfwd). Applied live to a running sandbox.

The HostPort:GuestPort spelling is deliberately the same in both — a spec always reads "this host port, that guest port", so `3000:80` maps the same pair of ports whichever direction is being configured.

func (PortForward) String

func (p PortForward) String() string

type Provider

type Provider string

Provider identifies which VM backend runs a sandbox. Values are persisted in sandbox records and clawk.mod files — frozen (see VMState); legacyProviderVFKit below is what a rename costs.

const (
	// ProviderVZ runs sandboxes on macOS via Apple's
	// Virtualization.framework (no external VMM binary) with gvproxy for
	// userspace networking and ACL enforcement. The default on macOS.
	ProviderVZ Provider = "vz"
	// ProviderFirecracker runs sandboxes on Linux via Firecracker.
	// Networking is TAP-on-bridge with no host-side filtering.
	ProviderFirecracker Provider = "firecracker"
)

func (Provider) Normalize

func (p Provider) Normalize() Provider

Normalize maps retired provider identifiers onto their current equivalent so configs written before a rename keep working. Unknown values pass through unchanged for the caller to reject with a clear error.

type Sandbox

type Sandbox struct {
	Name     string   `json:"name"`
	Provider Provider `json:"provider"`
	Profile  string   `json:"profile,omitempty"` // active overlay profile (if any)
	// Namespace groups the sandbox for organization and (Phase 2)
	// per-namespace defaults. Empty is treated as DefaultNamespace; use
	// NamespaceName for the resolved value.
	Namespace string `json:"namespace,omitempty"`
	// Anchor is the directory this sandbox is bound to, set for sandboxes
	// created by the bare `clawk` invocation (addressed by being in the
	// directory rather than by a typed name). Empty means the sandbox is
	// explicitly named. Its presence is the cwd-vs-ticket discriminator.
	Anchor string `json:"anchor,omitempty"`
	// DesiredState is the lifecycle state the user wants: VMStateRunning after
	// `clawk up`, VMStateStopped after `clawk down`. Empty means no explicit
	// intent yet. It's *spec* (desired), distinct from VMState below (*status*,
	// observed) — and it's what a future server-side reconciler converges to,
	// so the imperative up/down commands are already declarative edits.
	DesiredState VMState `json:"desired_state,omitempty"`
	// ResourceVersion increments on every spec write. Groundwork for optimistic
	// concurrency once there are multiple writers (the cloud control plane);
	// today it's just a monotonic "this record changed" counter.
	ResourceVersion int `json:"resource_version,omitempty"`
	// RecordSchema is the schema version of this record's JSON shape,
	// stamped by Store.Save on every write (RecordSchemaVersion). Distinct
	// from the store-wide version in meta.json: that one drives directory
	// migrations, this one says which clawk shape wrote THIS record, so a
	// selective per-record migration is possible without guesswork. Zero
	// means the record predates the field: schema 1.
	RecordSchema int           `json:"record_schema,omitempty"`
	Phases       []Phase       `json:"phases"`
	Network      NetworkPolicy `json:"network"`
	Forwards     []PortForward `json:"forwards,omitempty"`
	// ReverseForwards are host loopback services exposed on the guest's
	// own loopback — the inbound counterpart of Forwards. Unlike Forwards
	// (a gvproxy binding fixed at VM start) these are tunnelled over vsock
	// by the daemon, so edits apply to a running sandbox. See
	// internal/revfwd.
	ReverseForwards []PortForward `json:"reverse_forwards,omitempty"`
	// Serials are host serial ports presented as PTYs in the guest — see
	// SerialDevice and internal/serialfwd. Like ReverseForwards these are
	// tunnelled over vsock and apply to a running sandbox.
	Serials []SerialDevice `json:"serials,omitempty"`
	// Files is the list of host->guest file copies refreshed on every
	// `clawk up`. See HostFile. Empty = no snapshots.
	Files []HostFile `json:"files,omitempty"`
	// Shares is the list of host->guest virtio-fs live mounts. See
	// HostShare. Empty = no live mounts.
	//
	// Changes to this list require `clawk down && clawk up` to re-emit
	// the provider's device list, but the disk image survives.
	Shares []HostShare `json:"shares,omitempty"`
	// Instructions are extra persistent-guidance blocks rendered into the
	// generated workspace CLAUDE.md (see sandbox.WorkspaceDocFile), sourced
	// from the namespace and a repo's clawk.mod. Each entry is a markdown
	// block read on every boot — the place for "always ask before X" or
	// project conventions that must survive a throwaway VM.
	Instructions []string `json:"instructions,omitempty"`
	// MCP is the set of MCP servers made available to the agent inside the
	// sandbox, sourced from the namespace and clawk.mod's `mcp ( … )`
	// block. Rendered into the guest before first boot, and each http/sse
	// entry's host is folded into the network policy's "mcp" block so a
	// declared server is reachable without a separate `network allow`.
	// See MCPServer.
	MCP []MCPServer `json:"mcp,omitempty"`
	// Memory is seed content for the agent's auto-memory MEMORY.md, sourced
	// from the namespace and clawk.mod. Written into the memory dir once on
	// first boot and never afterward (see sandbox.SeedClaudeMemory), so a
	// fresh sandbox starts with baseline knowledge without clobbering memory
	// the agent has since accumulated.
	Memory string `json:"memory,omitempty"`
	// Image is the OCI image reference this sandbox boots as its root
	// filesystem (clawk.mod `vm ( image <ref> )`). The provider builds an
	// ext4 rootfs from the image (with clawk-init and the pty-agent
	// injected), boots it via direct-kernel, and the sandbox runs
	// sshd-free — all host access goes over the vsock agent.
	Image string `json:"image,omitempty"`
	// Kernel overrides the guest kernel the vz provider direct-boots: a
	// local vmlinux path or an http(s) URL. Empty = the default Kata
	// kernel. Declared via clawk.mod `vm ( kernel <path|url> )` or the
	// --kernel flag. The main use is supplying a KVM-enabled kernel for
	// nested virtualization (the stock Kata kernel has KVM disabled, so
	// the guest has no /dev/kvm).
	Kernel string `json:"kernel,omitempty"`
	// GuestABI records the guest-contract version (the clawk-init boot
	// manifest schema + the pty-agent vsock protocol; see
	// sandbox.CurrentGuestABI) baked into this sandbox's disk at create.
	// Guest binaries are never updated in place, so this is the record a
	// later host consults to fail readably ("recreate this sandbox")
	// instead of hitting an in-guest version error mid-boot. Zero means
	// the record predates the field: ABI 1.
	GuestABI int `json:"guest_abi,omitempty"`
	// RequiredEnv holds the env entries this sandbox wants exported inside
	// the VM, declared in clawk.mod via `env ( … )`. Each string is a
	// canonical envspec entry — a bare name like "GITHUB_TOKEN" (passthrough),
	// an alias/default like "GH=${ACME_GH_TOKEN:-}" or a literal like
	// "EDITOR=vim". Only names and defaults/literals are stored: secret
	// *values* are read from the host shell at sandbox-create/up time and
	// written to /etc/profile.d/99-clawk-env.sh in the guest, never persisted
	// to the sandbox state file. See internal/envspec.
	RequiredEnv []string `json:"required_env,omitempty"`
	// NestedVirt opts the VM into hardware-assisted nested
	// virtualization so the guest can run its own VMs (Docker with KVM,
	// Firecracker, etc.). Requires macOS 15+ and M3-or-newer Apple
	// Silicon. Enabled per-sandbox at create time; changes require a
	// destroy+recreate to take effect.
	NestedVirt bool `json:"nested_virt,omitempty"`

	// CPU is the vCPU count exposed to the guest. Zero means the provider
	// picks a default. Not a reservation — KVM and VZ don't charge host CPU
	// time for idle vCPUs.
	CPU uint `json:"cpu,omitempty"`

	// MemoryMiB is the baseline memory target in mebibytes. When
	// MemoryMaxMiB > MemoryMiB, providers that support virtio-balloon
	// reclaim (max - baseline) back to the host at boot and let the guest
	// grow on demand via deflate_on_oom. Zero = no ballooning.
	MemoryMiB uint64 `json:"memory_mib,omitempty"`

	// MemoryMaxMiB is the guest-visible hard cap on memory in mebibytes —
	// the amount allocated at boot. Zero = provider default.
	MemoryMaxMiB uint64 `json:"memory_max_mib,omitempty"`

	// DiskMiB is the sparse ext4 root-disk ceiling in mebibytes, from
	// clawk.mod `vm ( disk <size> )`. Zero = sandbox.DefaultDiskSizeGiB.
	// The disk is sparse, so this bounds how far the guest may grow without
	// reserving the full size up front — but the build does write an inode
	// table proportional to the ceiling (~1/64 of it; see
	// sandbox.DefaultDiskSizeGiB), so a bigger ceiling is cheap, not free.
	// It's baked into the rootfs at build time, so a change takes effect on
	// the next rootfs (re)build.
	DiskMiB uint64 `json:"disk_mib,omitempty"`

	// SwapMiB is the sparse swap device's capacity in mebibytes, from
	// clawk.mod `vm ( swap <size|off> )`. Zero = sandbox.DefaultSwapSizeMiB;
	// negative = no swap device at all. Unlike DiskMiB this is not baked into
	// the rootfs — the device is a separate file, resized on the next boot.
	SwapMiB int64 `json:"swap_mib,omitempty"`

	// IdleTimeoutSec is how long the sandbox may sit idle (no attached
	// session, quiescent guest) before its VM daemon stops it to reclaim
	// host memory. Zero = the built-in default; negative = never stop.
	// Declared via clawk.mod `vm ( idle_timeout <dur|off> )`. An idle stop
	// is a park, not a `clawk down`: DesiredState stays running and any
	// attach boots the VM back.
	IdleTimeoutSec int64 `json:"idle_timeout_sec,omitempty"`

	// Setup holds the workspace-level `on up` commands, sourced from a
	// workspace clawk.mod (a sandbox block with `includes`). Unlike
	// Phase.Setup — which runs per phase in that phase's worktree — these run
	// once per boot at the guest workspace root (the parent of every phase
	// worktree): the CWD-independent, VM-wide slot for a swapfile, a shared
	// daemon, or a sysctl tweak. Empty for single-repo sandboxes, whose
	// `on up` is per-phase. Snapshotted at create like every other clawk.mod
	// value.
	Setup []string `json:"setup,omitempty"`

	// OnCreate holds the workspace-level `on create` commands from a workspace
	// clawk.mod — the VM-wide, once-ever counterpart to Phase.OnCreate, run at
	// the guest workspace root before the runner first attaches (a global
	// toolchain install, a shared clone). It participates in the same
	// create-pending/retry flow as the per-phase hooks: a failure marks the
	// sandbox create-pending and is retried on the next `clawk up`. Empty for
	// single-repo sandboxes, whose `on create` is per-phase.
	OnCreate []string `json:"on_create,omitempty"`
	// OnCreateAt records when the workspace-level OnCreate last completed
	// successfully. Zero means it has not run yet (or the sandbox is
	// create-pending). Mirrors Phase.OnCreateAt.
	OnCreateAt time.Time `json:"on_create_at,omitempty"`

	// --- Status: observed runtime state, NOT user-authoritative ---------------
	// A reconciled cache; the provider/OS is the source of truth. Reconcile via
	// observe() before trusting these for a decision (clawk list/status/migrate
	// already do).
	VMState VMState `json:"vm_state"`
	// StopReason records why the VM last left the running state, when the
	// stop wasn't an explicit user verb. Today the only value is
	// StopReasonIdle (the daemon parked an idle VM). Cleared on every boot
	// and on explicit `clawk down`, so a bare "stopped" always means the
	// user asked for it.
	StopReason StopReason `json:"stop_reason,omitempty"`
	VMPid      int        `json:"vm_pid,omitempty"`
	GuestIP    string     `json:"guest_ip,omitempty"`
	GatewayIP  string     `json:"gateway_ip,omitempty"`
	MACAddress string     `json:"mac_address,omitempty"`

	// CreatePending is set when one of the phase `on create` commands has
	// failed at least once. The VM is left running so the user can shell in
	// and investigate; runner attach is refused with an actionable message;
	// the next `clawk up` re-runs `on create` from scratch. `clawk destroy`
	// is the explicit reset.
	CreatePending bool `json:"create_pending,omitempty"`
	// CreatePendingReason carries the human-readable failure (phase name +
	// failing command + provider error) so `clawk status` can surface it
	// without re-running anything. Cleared on a successful `on create`.
	CreatePendingReason string `json:"create_pending_reason,omitempty"`

	// SessionProject is the stable identifier of the session-history repo
	// this sandbox's Claude Code conversations belong to (see
	// internal/sessions.ProjectID). Sandboxes that work on the same repo set
	// share one history repo — each on its own branch — so a fresh sandbox
	// for the same project boots with prior transcripts and memory. Computed
	// and persisted on first bring-up; empty on sandboxes created before the
	// feature, in which case it is derived again from the phases. Currently
	// populated by the vz provider only.
	SessionProject string `json:"session_project,omitempty"`

	CreatedAt time.Time `json:"created_at"`

	// PRRefreshedAt is the wall-clock time of the last successful
	// PR-state refresh via `gh`. v2 derives Phase.Status from PR
	// state; this timestamp gates a 60-second cache so `clawk
	// status` doesn't shell out on every invocation.
	PRRefreshedAt time.Time `json:"pr_refreshed_at,omitempty"`
}

func (*Sandbox) DisplayName

func (s *Sandbox) DisplayName() string

DisplayName returns the sandbox's human-facing name. See DisplayName.

func (*Sandbox) Key

func (s *Sandbox) Key() string

Key is the sandbox's store key, "<namespace>/<name>" — the identity the store resolves to an on-disk location. Bare names passed to the store (e.g. from CLI args) resolve to the default namespace; this is the explicit form for callers that hold the sandbox.

func (*Sandbox) NamespaceName

func (s *Sandbox) NamespaceName() string

NamespaceName is the sandbox's namespace, resolving the empty zero value to DefaultNamespace so callers never have to special-case it.

type SerialDevice added in v0.4.0

type SerialDevice struct {
	// HostPath is the device on the host, e.g. /dev/cu.usbmodem1101 on
	// macOS or /dev/ttyACM0 on Linux. It may be a glob, which is resolved
	// at open time rather than when it is configured — boards that
	// re-enumerate into a bootloader often come back under a neighbouring
	// name, and `/dev/cu.usbmodem*` survives that where a literal path
	// doesn't. A glob that matches more than one device at open time is an
	// error, not a guess.
	HostPath string `json:"host_path"`

	// GuestName is the basename the PTY is symlinked to inside the guest,
	// at /dev/<GuestName>. Never a path: the guest refuses anything with a
	// directory part, and so does the host on the way in.
	GuestName string `json:"guest_name"`
}

SerialDevice is a host serial port presented as a PTY inside the guest — an Arduino or ESP32 on the Mac's USB, reachable from tooling in the sandbox. Bytes and line configuration are tunnelled over vsock (see internal/serialfwd); the USB device itself is not passed through, because no hypervisor clawk targets can do that.

Like ReverseForwards these apply live to a running sandbox, and for the same reason they are vz-only: the guest is the end that dials.

func (SerialDevice) IsGlob added in v0.4.0

func (s SerialDevice) IsGlob() bool

IsGlob reports whether HostPath is a pattern to resolve at open time rather than a literal device path.

func (SerialDevice) String added in v0.4.0

func (s SerialDevice) String() string

String renders the device in the HOST:GUEST spelling the CLI accepts, collapsing to the bare host path when the guest name is the default.

type StopReason

type StopReason string

StopReason qualifies a VMStateStopped that the user didn't ask for. Values are persisted in sandbox records — frozen (see VMState).

const StopReasonIdle StopReason = "idle"

StopReasonIdle marks a VM the daemon stopped after the sandbox sat idle past its idle timeout. Distinct from an explicit `clawk down` so the CLI can render "stopped (idle)" and the attach path knows a mid-attach shutdown was a park it should transparently boot through.

const StopReasonSuspended StopReason = "suspended"

StopReasonSuspended marks a VM stopped by `clawk snapshot`: its memory + device state sits in a suspend file next to the VM, and the next boot (resume, up, or any attach verb) restores it exactly where it left off instead of cold-booting. Rendered as "stopped (suspended)".

type Store

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

func NewStore

func NewStore() (*Store, error)

func NewStoreAt

func NewStoreAt(rootDir string) *Store

NewStoreAt creates a Store rooted at an explicit directory, skipping the home-dir resolution and legacy migration NewStore does. Used by `clawk image gc` (which targets a caller-supplied clawk root) and by tests.

func (*Store) CacheDir

func (s *Store) CacheDir() string

CacheDir returns the shared cache directory.

func (*Store) Delete

func (s *Store) Delete(name string) error

func (*Store) DeletePolicy

func (s *Store) DeletePolicy(name string) error

DeletePolicy removes a policy's whole directory — record and cache both.

func (*Store) Exists

func (s *Store) Exists(name string) bool

func (*Store) HistoryDir

func (s *Store) HistoryDir() string

HistoryDir returns the root holding per-project session-history repos — the bare git repos that version Claude Code conversations across sandboxes (see internal/sessions). Lives outside per-sandbox dirs so destroying one sandbox never touches the shared history.

<rootDir>/history/<projectID>.git

func (*Store) List

func (s *Store) List() ([]Sandbox, error)

func (*Store) ListNamespaces

func (s *Store) ListNamespaces() ([]Namespace, error)

ListNamespaces returns every namespace present on disk — those with a namespace.json and those that merely hold sandboxes.

func (*Store) ListPolicies

func (s *Store) ListPolicies() ([]*Policy, error)

ListPolicies returns every policy record on disk, sorted by name. The builtin "default" is synthesized, not stored, so it never appears here.

func (*Store) Load

func (s *Store) Load(name string) (*Sandbox, error)

func (*Store) LoadNamespace

func (s *Store) LoadNamespace(name string) (*Namespace, error)

LoadNamespace returns a namespace's config. A namespace directory with no namespace.json yet (e.g. one that only holds sandboxes) returns an empty config, so "no record" reads as "no defaults" rather than an error.

func (*Store) LoadPolicy

func (s *Store) LoadPolicy(name string) (*Policy, error)

LoadPolicy returns a policy by name. The reserved name "default" resolves to BuiltinDefaultPolicy; an absent record returns ErrPolicyNotFound.

func (*Store) LoadPolicyCache

func (s *Store) LoadPolicyCache(name string) (*PolicyCache, error)

LoadPolicyCache returns a policy's fetched-entry cache. An absent cache reads as an empty one (zero FetchedAt → always stale), so "never fetched" needs no special casing at call sites.

func (*Store) NamespaceConfigExists

func (s *Store) NamespaceConfigExists(name string) bool

NamespaceConfigExists reports whether a namespace has a config record (vs. being a bare grouping that only holds sandboxes).

func (*Store) NamespaceConfigPath

func (s *Store) NamespaceConfigPath(name string) string

NamespaceConfigPath is the on-disk path of a namespace's config record (for `clawk namespace edit`).

func (*Store) RootDir

func (s *Store) RootDir() string

RootDir returns the top-level clawk directory (~/.clawk on a real host, or a temp dir for tests via NewStoreAt). Callers that need host-scoped artifacts living outside the per-sandbox subdirectories — provision.sh overrides, the long-lived OAuth token, image cache roots — anchor on this path.

func (*Store) RunMigrations

func (s *Store) RunMigrations(w io.Writer) error

RunMigrations replays every pending migration step in order, advancing the recorded version after each. Idempotent: steps already applied are skipped, and a step is safe to re-run. A disruptive step that defers running sandboxes does not advance the version (so it retries) and, by strict ordering, blocks later steps until it fully applies.

func (*Store) Save

func (s *Store) Save(sb *Sandbox) error

func (*Store) SaveNamespace

func (s *Store) SaveNamespace(ns *Namespace) error

func (*Store) SavePolicy

func (s *Store) SavePolicy(p *Policy) error

SavePolicy writes a policy record, validating its name first (the builtin "default" is reserved and cannot be overwritten).

func (*Store) SavePolicyCache

func (s *Store) SavePolicyCache(name string, c *PolicyCache) error

SavePolicyCache writes the fetched-entry cache for a policy. The cache is regenerable, so it lives beside the record rather than inside it — refresh is a one-file rewrite picked up by every referencing sandbox.

func (*Store) SchemaVersion

func (s *Store) SchemaVersion() int

SchemaVersion is the store's recorded on-disk schema version (0 if unset).

func (*Store) StateDir

func (s *Store) StateDir(sandboxName string) string

StateDir returns the per-sandbox persistent state directory — host storage that lives OUTSIDE the sandbox's VMDir and therefore survives destroy + recreate cycles. Used to mount Claude Code's conversation and memory directories from a stable location per sandbox name.

Layout:

<rootDir>/state/<sandboxName>/
  claude/
    projects/   # mounted as /home/agent/.claude/projects/
    memory/     # mounted as /home/agent/.claude/memory/

The directory is created lazily by callers that need it.

func (*Store) VMDir

func (s *Store) VMDir(sandboxName string) string

VMDir returns the VM state directory for a sandbox.

func (*Store) WorktreeDir

func (s *Store) WorktreeDir(sandboxName string) string

WorktreeDir returns the worktree directory for a sandbox.

type VMState

type VMState string
const (
	VMStateStopped VMState = "stopped"
	VMStateRunning VMState = "running"
)

VMState values are persisted in sandbox records: frozen — never rename an existing value, only add. Same rule for StopReason, Provider and the BlockOrigin* constants below; the vfkit→vz Provider rename left Normalize() carrying migration code forever, which is the tax this rule avoids.

Jump to

Keyboard shortcuts

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