Documentation
¶
Overview ¶
Package template implements clawk's resource file language.
A clawk.mod is a list of typed blocks — `sandbox [<name>] ( ... )`, `policy <name> ( ... )`, `namespace <name> ( ... )` — one filename for every shape. The sandbox block is a template describing a reusable sandbox (provider, repositories, network policy). It is NOT tied to a particular branch — the branch is supplied at `clawk work` time, so the same template can spawn many per-ticket sandboxes. A sandbox block with `includes` is a multi-repo workspace root; without it, the file configures its own repo.
Example workspace clawk.mod:
sandbox acme (
vm (
provider vz
)
includes (
~/code/k8s-deploy
~/code/monorepo
)
network (
use default corp-egress
allow github.com
allow *.github.com
allow ip 10.0.0.5
allow ip 192.168.10.0/24
)
)
policy corp-egress (
allow ip 10.20.0.0/16
deny telemetry.corp.com
)
Index ¶
- Constants
- Variables
- func ExpandPath(p string) (string, error)
- func FindGitRepo(dir string) (string, error)
- func IsResolvedVersion(v string) bool
- func IsTidyInputVersion(v string) bool
- func MigrateFlat(src string) (string, error)
- func RetiredWorkspaceFileError(path string) error
- type AgentDoc
- type File
- type FileSpec
- type NamespaceDef
- type PolicyDef
- type Repo
- type ShareSpec
- type SkillKind
- type SkillRef
- type SkillResolver
- type SkillResolverFunc
- type SkillRewrite
- type Template
- type TidyResult
- type Token
- type TokenKind
- type Workspace
- func FindWorkspace(dir string) (*Workspace, error)
- func FindWorkspaceWithProfile(dir, profile string) (*Workspace, error)
- func LoadClawkfilePathWithProfile(path, profile string) (*Workspace, error)
- func LoadStandaloneClawkfile(dir string) (*Workspace, error)
- func LoadStandaloneClawkfileWithProfile(dir, profile string) (*Workspace, error)
- func LoadWorkspace(path string) (*Workspace, error)
- func LoadWorkspaceWithProfile(path, profile string) (*Workspace, error)
- func WorkspaceFromGitRepo(dir string) (*Workspace, error)
Constants ¶
const FormatVersion = 1
FormatVersion is the newest clawk.mod format version this parser understands. A file may declare the version it requires with a top-level `clawk <n>` directive; a file without one is version 1. Bump this only for changes an older parser would silently misread — purely additive directives don't need a bump, because older parsers already reject unknown directives by name with a clear error.
const RepoFileName = "clawk.mod"
RepoFileName is the single clawk config filename. Whether it describes a multi-repo workspace or one repo is structural: a sandbox block with `includes ( ... )` is a workspace root. Profiles extend a clawk.mod with a "clawk.mod.<profile>" overlay file.
const RetiredWorkspaceFileName = "clawk.work"
RetiredWorkspaceFileName is the pre-cutover multi-repo filename. It is never loaded — the loaders recognise it only to emit a rename hint.
Variables ¶
var ErrAlreadyTyped = errors.New("file already uses the typed-block grammar")
ErrAlreadyTyped reports that a file already parses as the typed-block grammar and needs no migration.
var ErrNoWorkspace = errors.New(
"no workspace clawk.mod (sandbox block with 'includes') found in this directory or any parent")
ErrNoWorkspace is returned when FindWorkspace can't find a workspace clawk.mod (a sandbox block with `includes`) in dir or any ancestor.
var ErrSkipResolution = errors.New("skip resolution")
ErrSkipResolution is the sentinel a SkillResolver returns to leave a skill untouched. Local skills always skip; distributed skills with an already-pinned version may skip.
Functions ¶
func ExpandPath ¶
ExpandPath resolves leading ~ (home dir) and $HOME. It does NOT make the path absolute — callers decide the base directory to resolve against. In workspace mode that's the workspace root.
Other env vars are NOT expanded: we don't want to silently substitute host-side secrets into template-visible paths.
func FindGitRepo ¶
FindGitRepo returns the nearest ancestor of dir containing a .git entry, inclusive of dir itself. Returns an error if no .git is found.
func IsResolvedVersion ¶
IsResolvedVersion reports whether v is in a form that tidy would leave alone — a semver tag or a pseudo-version. Anything else (a branch name, "latest", a short SHA) is a tidy input that needs to be rewritten before the file can be considered pinned.
func IsTidyInputVersion ¶
IsTidyInputVersion reports whether v is a valid version-shaped token the user might write before tidy runs: branch names, "latest", short SHAs. A return of false means the parser should reject the token as not a version at all.
func MigrateFlat ¶
MigrateFlat rewrites a pre-cutover flat clawk.mod (or clawk.work) body into the typed-block grammar: the top-level `name X` directive, if any, moves into the sandbox header and everything else is indented into `sandbox [X] ( … )`. The transform is text-level so comments, blank lines, and alignment survive verbatim; the result is validated with ParseFileString before being returned, so a file the migrator can't handle errors instead of being rewritten into something broken.
Input that already parses as the typed grammar returns ErrAlreadyTyped.
func RetiredWorkspaceFileError ¶
RetiredWorkspaceFileError is the loader-level rename hint produced when a clawk.work is encountered anywhere the old loader accepted one.
Types ¶
type AgentDoc ¶
AgentDoc is one entry in an `agent (...)` block — a unit of persistent instructions or memory seed. Exactly one field is set: Text is inline markdown from a quoted string (one line); Path is a markdown file, relative to the clawk.mod directory, whose content is read at compose. The file form is how multi-line markdown is carried — its backticks, fences and quotes never have to survive the DSL's string lexing.
type File ¶
type File struct {
Sandbox *Template // nil when the file defines no sandbox
Policies []PolicyDef
Namespaces []NamespaceDef
// FormatVersion is the value of the file's `clawk <n>` directive;
// zero when the directive is absent (which means version 1). Files
// requiring a version newer than FormatVersion fail at parse with an
// upgrade hint, so a future format change degrades into a readable
// error on old clawks instead of silent misparsing.
FormatVersion int
}
File is the parsed form of a typed-block clawk file: a sequence of `<kind> [<name>] ( ... )` blocks with kinds sandbox, policy and namespace. There is no implicit root document and no flat top-level directives — a clawk.mod IS its block list. See ParseFileString for the migration story away from the legacy flat grammar.
func ParseFileString ¶
ParseFileString parses the typed-block grammar. Exactly one sandbox block is allowed per file (a second is an error at its line). Legacy flat files (any known top-level directive like `name`, `vm`, `network` outside a typed block) produce an error that includes a migration hint: wrap the body in `sandbox ( ... )` and move `name <x>` into the header.
type FileSpec ¶
type FileSpec struct {
HostPath string
GuestPath string
Mode fs.FileMode
// Line/Col record where the entry appeared so duplicate-guest-path
// conflicts can be reported back to the right line.
Line, Col int
}
FileSpec is one entry in a `files (...)` block: a host file copied into the guest at sandbox create and re-pushed on every `clawk up`. Snapshot semantics — edits on the host propagate when the user next runs `up`, not live. Use ShareSpec for files that rotate underneath (AWS STS, etc.).
- HostPath: host-side path. Tilde and $VAR are expanded at compose time, not at parse time, because the parsing host (CI) and the run host (laptop) may differ.
- GuestPath: absolute guest path. Empty falls back to HostPath verbatim (with ~ resolved to the guest agent's $HOME).
- Mode: zero = preserve the host file's mode; non-zero overrides.
type NamespaceDef ¶
type NamespaceDef struct {
Name string // required
Template *Template // body parsed with the existing template grammar subset
}
NamespaceDef is one `namespace <name> ( ... )` block: a named overlay of the per-namespace template subset (network / files / shares / env / agent). VM shape, includes and lifecycle hooks are sandbox-level concerns and are rejected inside a namespace body.
type PolicyDef ¶
type PolicyDef struct {
Name string // required for policy blocks
// Allow/Deny lists use the same entry grammar as a `network ( ... )`
// block: bare domains and `ip <ADDR>` literals/CIDRs.
AllowDomains, AllowIPs, DenyDomains, DenyIPs []string
// Sources are URLs of external blocklists (`source "<url>"` entries),
// fetched and expanded into denied domains by the caller at apply time.
Sources []string
// Refresh is how often Sources are re-fetched, from `refresh <dur>`.
// Zero = none (fetch once).
Refresh time.Duration
// Line/Col record where the block appeared so cross-block conflicts
// (duplicate policy names, dangling `use` references) can be reported
// back to the right line by the caller.
Line, Col int
}
PolicyDef is one `policy <name> ( ... )` block: a named, reusable network egress policy referenced from network blocks via `use <name>`. Resolution of references happens at compose time, not parse time — a clawk file may use a policy defined elsewhere (e.g. registered by `clawk apply`).
type Repo ¶
type Repo struct {
Name string // human-facing name used by --only and display
Path string // absolute path on host (the `includes` entry, resolved)
RepoPath string // absolute path to the containing git repo
Clawkfile *Template // nil if the repo has no Clawkfile at its root
}
Repo is one git repository brought into a sandbox by a workspace. The Clawkfile (if present at the repo root) carries that repo's allow / forwards / setup.
type ShareSpec ¶
type ShareSpec struct {
ShareSpec is one entry in a `shares (...)` block: a host directory live-mounted into the guest via virtio-fs. Edits on the host are visible inside the guest without clawk involvement — the host owns the file lifecycle.
Adding or removing a share requires `clawk down && clawk up` so the provider re-emits its virtio-fs device list. Disk state is preserved across that cycle; `clawk destroy` is not required.
type SkillKind ¶
type SkillKind int
SkillKind classifies the resolution path of a SkillRef.
func ClassifySkillPath ¶
ClassifySkillPath maps a raw entry path to its SkillKind. It does not touch the filesystem — the classification is purely lexical so the parser stays decoupled from disk state.
type SkillRef ¶
type SkillRef struct {
// Path is the raw entry as written in clawk.mod, before any
// expansion of ~ or $HOME. Tidy preserves it verbatim and only ever
// rewrites Version.
Path string
// Version is empty for local skills. For distributed skills it is
// either a tag ("v1.2.3"), a Go-style pseudo-version
// ("v0.0.0-yyyymmddhhmmss-12charSHA"), or — only as input to tidy —
// a branch name / "latest" / short SHA that tidy will resolve and
// rewrite.
Version string
// Kind classifies the path shape. Set by ClassifySkillPath; the
// parser populates it after each entry is read.
Kind SkillKind
// Line / Col anchor diagnostics back to the source. The rewriter
// also uses them to find the byte range to patch.
Line int
Col int
// VersionLine / VersionCol point at the version token, or zero
// when no version was written. The rewriter uses these to splice
// resolved versions in place.
VersionLine int
VersionCol int
}
SkillRef is a single entry in a `skills (...)` block.
Three shapes are valid:
- SkillKindLocalHome: ~/foo or $HOME/foo — a path under the user's home directory. Versions are forbidden.
- SkillKindLocalWorkspace: ./foo — a path relative to the workspace root. Versions are forbidden.
- SkillKindDistributed: <host.tld>/path — a remote skill addressed like a Go module. Versions are required after `clawk mod tidy`; before tidy, branch names and the literal "latest" are accepted as resolution inputs that will be rewritten.
type SkillResolver ¶
SkillResolver turns a SkillRef into a pinned version. The returned string must satisfy IsResolvedVersion or RewriteSkillVersions errors — half-resolved values would defeat the whole point of tidy.
Returning ErrSkipResolution leaves the entry untouched (used for local skills, which never carry a version).
var StubRemoteResolver SkillResolver = SkillResolverFunc(func(s SkillRef) (string, error) { if s.Kind != SkillKindDistributed { return "", ErrSkipResolution } if IsResolvedVersion(s.Version) { return "", ErrSkipResolution } return "", fmt.Errorf( "remote skill resolution is not yet implemented; pin %s manually with a vMAJOR.MINOR.PATCH tag", s.Path) })
StubRemoteResolver is the resolver used by the initial implementation: it pins nothing, just shapes the error so the user sees an actionable "implement me" rather than a silent miss. Replaced by a real git-fetch resolver in a follow-up.
type SkillResolverFunc ¶
SkillResolverFunc adapts a plain function into the SkillResolver interface. Useful for tests and the stub resolver below.
type SkillRewrite ¶
type SkillRewrite struct {
Path string
OldVersion string // empty when a version is being inserted for the first time
NewVersion string
Line int
InsertedNew bool // true when no version token existed before the rewrite
}
SkillRewrite is a single (path → new version) rewrite that happened.
type Template ¶
type Template struct {
Name string // repo name override (defaults to dir basename)
// SandboxName is the block-header name from the typed-block grammar
// (`sandbox <name> ( ... )`, see ParseFileString). Empty when the header
// omits it — the loaders fold a non-empty header into Name, so downstream
// naming is one field.
SandboxName string
Provider string // e.g., "vz", "firecracker"; empty = default
Includes []string // repo paths the workspace composes
Domains []string // domain allow list additions
IPs []string // literal IP / CIDR allow list
// Use lists named network policies referenced by `use <name> ...` inside
// a `network ( ... )` block, in declaration order. Resolution against
// `policy` blocks happens at compose time, not parse time.
Use []string
// DenyDomains and DenyIPs become guardrail denies in the sandbox's
// "mod" policy block: they refuse the destination outright (a denied
// domain covers its subdomains), override lower layers' allows, and
// suppress the interactive prompt. See composeNetworkPolicy.
DenyDomains []string
DenyIPs []string
// DenySources are URLs of external blocklists (hosts files / EasyList /
// plain domain lists, uBlock-style) fetched and parsed into denied domains
// by the caller. Written as `deny source "<url>"` inside a `network ( … )`
// block.
DenySources []string
Forwards []string // port forward specs (PORT or HOST:GUEST)
Env []string // env var NAMES (not values) to pull from host and export in the VM
// Lifecycle hooks. Each is a list of shell commands run inside the
// VM at the named moment.
//
// OnCreate runs once after first boot, before the runner attaches.
// Hard fails the up — see Sandbox.CreatePending.
// OnUp runs every clawk-up after the VM is healthy.
// OnDown runs every clawk-down before VM stop. (reserved; not yet wired)
// OnEnter runs every clawk-run before the runner spawns. (reserved; not yet wired)
OnCreate []string
OnUp []string
OnDown []string
OnEnter []string
// Skills is the require-style list of Claude skills the project
// assumes are available, identified by path (local or distributed).
Skills []SkillRef
// Files is the snapshot-on-up file list (`files (...)`).
// See FileSpec for semantics.
Files []FileSpec
// See ShareSpec for semantics.
Shares []ShareSpec
// Instructions and Memory come from the `agent ( ... )` block: extra
// persistent CLAUDE.md guidance and a baseline auto-memory seed. Each is
// an ordered list of AgentDocs (inline text or a markdown file) resolved
// to content at compose, where the namespace's equivalents layer ahead.
Instructions []AgentDoc
Memory []AgentDoc
// Nested enables hardware nested virtualization for the sandbox.
// Bare directive: present and true, absent and false. There's no
// `nested false` form — profile overlays cannot un-set.
Nested bool
// CPU is the vCPU count exposed to the guest. Zero = provider default.
// VZ and KVM don't reserve host CPU time for idle guest vCPUs, so this is
// effectively a burst ceiling rather than a reservation.
CPU uint
// MemoryMiB is the baseline memory target in mebibytes. When set together
// with MemoryMaxMiB > MemoryMiB, the provider configures a virtio-balloon
// that reclaims (max - baseline) back to the host at boot and deflates on
// guest pressure (deflate_on_oom). Zero = follow MemoryMaxMiB (no balloon).
MemoryMiB uint64
// MemoryMaxMiB is the hard cap on guest memory in mebibytes — the amount
// the guest sees at boot. Zero = provider default.
MemoryMaxMiB uint64
// IdleTimeoutSec is the sandbox's idle-stop timeout in seconds: how
// long the VM may sit with no attached session and a quiescent guest
// before the daemon stops it to reclaim host memory. Declared as
// `idle_timeout <dur|off>` inside the `vm ( ... )` block. Zero = unset
// (the provider default applies); negative = never stop ("off").
IdleTimeoutSec int64
// Image is an OCI image reference (e.g. "golang:1.25") the sandbox
// boots as its root filesystem. Declared as `image <ref>` inside the
// `vm ( ... )` block. Empty = the built-in clawk-dev default.
Image string
// Kernel overrides the guest kernel the vz provider direct-boots: a
// local vmlinux path or an http(s) URL. Declared as `kernel <path|url>`
// inside the `vm ( ... )` block. Empty = the default Kata kernel.
Kernel string
}
Template is the parsed form of a clawk.mod's `sandbox ( ... )` block.
Directives are grouped: scalar VM settings live in `vm ( ... )` and egress policy in `network ( ... )`; `forwards` / `files` / `shares` / `skills` / `env` / `on <event>` sit at the block's top level. A sandbox block with `includes ( ... )` is a workspace root composing member repos; without it the block configures the repo it sits in.
Callers (LoadWorkspace / LoadStandaloneClawkfile) decide which subset is valid for their context and reject out-of-place directives.
func ParseString ¶
ParseString parses a typed-block clawk file and returns its sandbox template. It is a thin wrapper over ParseFileString for callers that only care about the sandbox block; a file without one is an error. Legacy flat files fail inside ParseFileString with a wrap-in-sandbox migration hint.
func (*Template) Merge ¶
Merge folds the directives of `over` on top of `t`, mutating t in place. This is the overlay semantics used by profiles: the base file declares defaults, the profile file adds more. Scalars in `over` win only when they are non-empty; lists are unioned (appended, duplicates removed later by the sandbox-composition step).
We intentionally do NOT allow profiles to shrink the config: there's no way to say "remove this allowed domain". That keeps profiles analysable — a reviewer sees only additions relative to the base.
type TidyResult ¶
type TidyResult struct {
Rewrites []SkillRewrite
Source string // post-rewrite source. Equal to input when Rewrites is empty.
}
TidyResult describes what RewriteSkillVersions changed. Useful for `clawk mod tidy` to print a one-line "rewrote N versions" summary without the caller diff'ing strings.
func RewriteSkillVersions ¶
func RewriteSkillVersions(src string, tmpl *Template, resolver SkillResolver) (TidyResult, error)
RewriteSkillVersions applies a SkillResolver to every distributed skill in the parsed template and returns a new source with the resolved versions spliced in. Comments, alignment, blank lines, and entry order are preserved — only version tokens change.
Local skills are never rewritten; the resolver should return ErrSkipResolution for them, but RewriteSkillVersions also short-circuits before calling the resolver for safety.
The function does not parse — it works off `tmpl.Skills`, which already carries the source positions populated by the parser.
type Token ¶
type Token struct {
Kind TokenKind
Val string // only meaningful for TokIdent
Line int // 1-based
Col int // 1-based
}
Token is a single lexical unit with source position (for error messages).
func Lex ¶
Lex tokenises src into a flat slice. Errors are returned with line/col from the first malformed byte. Comments are stripped: "#" or "//" run to end of line, and "/* ... */" spans lines until its close. "//" and "/*" open a comment only at a token boundary, so a bare URL value such as https://host/path stays a single identifier; an inline comment needs whitespace before it.
Whitespace other than newlines is separator-only; newlines are significant because our grammar uses them as statement terminators (like go.mod).
type Workspace ¶
type Workspace struct {
Root string // absolute path to the directory containing the clawk.mod
File *Template // parsed workspace-level sandbox block (provider, allow, forwards, ...)
Repos []Repo // every repo included by the workspace, in declaration order
// Policies collects every `policy <name> ( ... )` block declared across
// the loaded files — workspace file and its overlay first (broader
// scope), then each repo's clawk.mod and overlay. The create paths
// register them into the host policy store, so later (nearer) blocks
// win a name collision.
Policies []PolicyDef
}
Workspace describes a dev environment rooted at a clawk.mod. A multi-repo workspace is a clawk.mod whose sandbox block has `includes ( ... )`; a single-repo environment is the degenerate case with one synthesised repo.
func FindWorkspace ¶
FindWorkspace is FindWorkspaceWithProfile with no profile.
func FindWorkspaceWithProfile ¶
FindWorkspaceWithProfile walks up from dir looking for a workspace root: the nearest clawk.mod whose sandbox block has `includes`. An includeless clawk.mod along the way is a single-repo file — the walk continues past it, so a workspace root above still wins (exactly the old clawk.work walk). Parse failures along the walk surface as errors rather than being skipped: a leftover flat-grammar file must produce its migration hint, not a silent fall-back to defaults. A clawk.work at any level gets the rename hint.
func LoadClawkfilePathWithProfile ¶
LoadClawkfilePathWithProfile loads an explicitly-given clawk.mod path, picking workspace or single-repo semantics from the file's own shape: a sandbox block with `includes` is a workspace root, anything else is the standalone single-repo case rooted at the file's directory.
func LoadStandaloneClawkfile ¶
LoadStandaloneClawkfile is LoadStandaloneClawkfileWithProfile with no profile.
func LoadStandaloneClawkfileWithProfile ¶
LoadStandaloneClawkfileWithProfile reads a clawk.mod at the given directory and synthesises a single-repo workspace rooted at that repo, optionally overlaying a clawk.mod.<profile> file.
func LoadWorkspace ¶
LoadWorkspace is LoadWorkspaceWithProfile with no profile.
func LoadWorkspaceWithProfile ¶
LoadWorkspaceWithProfile parses a workspace clawk.mod, resolves every listed include, and applies an optional profile overlay to both the workspace file itself and each repo's Clawkfile.
If profile is non-empty:
- "<workspace-path>.<profile>" (e.g. clawk.mod.investigation) is loaded and merged onto the base workspace, if it exists. Missing overlay is tolerated — the profile may only affect per-repo policy.
- Each repo's "clawk.mod.<profile>" is loaded and merged onto its base clawk.mod, if either exists.
A profile name that matches NOTHING across the workspace is treated as an error, so typos surface loudly.
func WorkspaceFromGitRepo ¶
WorkspaceFromGitRepo synthesises a single-repo workspace using dir's containing git repo, with no Clawkfile. Used by `clawk work` as a last-resort fallback when no clawk.mod is present: the repo gets defaults only (no forwards, no setup, default allow list) and the user keeps a one-line invocation. Errors when dir is not inside a git repo.