template

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

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

View Source
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.

View Source
const GlobalModEnvVar = "CLAWK_GLOBAL_MOD"

GlobalModEnvVar names the environment variable that overrides the host-wide defaults file outright. Pointing it at a file makes a run reproducible regardless of what the host happens to have in ~/.config; pointing it at a path that does not exist is an error rather than a silent fall-through, so a typo in CI surfaces.

Named for the layer it overrides, so it pairs with --no-global and can't be misread as "the repo's clawk.mod", and prefixed like every other variable clawk reads (CLAWK_DEBUG, CLAWK_NET_MODE, CLAWK_MAX_VZ_DEVICES) so it doesn't collide with an unrelated ROOT_* in someone's shell.

View Source
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.

View Source
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

View Source
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.

View Source
var ErrGlobalMod = errors.New("host-wide clawk.mod")

ErrGlobalMod marks every OTHER host-wide-layer failure: unreadable, unparseable, out-of-scope directive, two candidate locations. Callers that try loaders in sequence (see the cli's resolveSource, which walks workspace → standalone → bare-git-repo and treats a failure as "not this shape") must test for it and surface it instead of moving on: a broken defaults file is not a hint to try somewhere else, and degrading to "no defaults" would hand back a sandbox quietly missing half its configuration.

View Source
var ErrNoGlobalMod = errors.New("no host-wide clawk.mod")

ErrNoGlobalMod reports that no host-wide defaults file exists. Not a failure — the overwhelmingly common case is a host that never wrote one.

View Source
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.

View Source
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.

View Source
var GlobalDisabled bool

GlobalDisabled skips the host-wide layer entirely — wired to `--no-global`. A package var rather than a parameter threaded through nine loader signatures: it is written once from PersistentPreRunE before any load, and tests set and restore it around a call.

Functions

func ExpandPath

func ExpandPath(p string) (string, error)

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

func FindGitRepo(dir string) (string, error)

FindGitRepo returns the nearest ancestor of dir containing a .git entry, inclusive of dir itself. Returns an error if no .git is found.

func GlobalModPath added in v0.4.0

func GlobalModPath() (string, error)

GlobalModPath resolves the host-wide defaults file, in order:

$CLAWK_GLOBAL_MOD                        explicit override (must exist)
$XDG_CONFIG_HOME/clawk/clawk.mod         default ~/.config/clawk/clawk.mod
~/.clawk/clawk.mod                       compatibility fallback

~/.config is the primary home because this file is the one thing in clawk's footprint a user hand-edits, symlinks out of a dotfiles repo and would be annoyed to lose: ~/.clawk is disposable machine state (VM disks, an image cache, per-sandbox records, a live OAuth token) that people exclude from backups and delete to start clean. Config must not be collateral.

Deliberately NOT os.UserConfigDir(): on darwin that is ~/Library/Application Support, which is the wrong place for a dotfile-managed text file. Honouring $XDG_CONFIG_HOME with a ~/.config fallback on every platform is what gh, git and nvim do on macOS, and what anyone writing this file expects.

Both non-env locations present is an error, never a silent precedence pick.

func IsResolvedVersion

func IsResolvedVersion(v string) bool

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

func IsTidyInputVersion(v string) bool

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

func MigrateFlat(src string) (string, error)

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

func RetiredWorkspaceFileError(path string) error

RetiredWorkspaceFileError is the loader-level rename hint produced when a clawk.work is encountered anywhere the old loader accepted one.

Types

type AgentDoc

type AgentDoc struct {
	Text string
	Path string
}

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

func ParseFileString(src string) (*File, error)

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 Global added in v0.4.0

type Global struct {
	// Path is the file it came from, for the note printed at create.
	Path string
	// Template is the file's sandbox block with every host-side path made
	// absolute against Path's directory (see absolutiseHostPaths), so it can
	// be merged under a repo template that resolves paths against its own
	// root.
	Template *Template
	// Policies are `policy <name> ( … )` blocks declared beside it — a
	// personal policy library, registered by the create paths exactly like a
	// repo's own.
	Policies []PolicyDef
	// ProfileMatched reports whether a clawk.mod.<profile> overlay beside the
	// global file existed and was applied, so a profile satisfied only by the
	// host-wide layer is not reported as matching nothing.
	ProfileMatched bool
}

Global is a loaded host-wide defaults layer.

func LoadGlobal added in v0.4.0

func LoadGlobal() (*Global, error)

LoadGlobal is LoadGlobalWithProfile with no profile.

func LoadGlobalWithProfile added in v0.4.0

func LoadGlobalWithProfile(profile string) (*Global, error)

LoadGlobalWithProfile loads and validates the host-wide defaults layer, applying a clawk.mod.<profile> overlay beside it when profile is non-empty. Returns ErrNoGlobalMod when there is no such file (or --no-global was passed) — callers treat that as "no defaults", not as a failure.

type MCPSpec added in v0.4.0

type MCPSpec struct {
	Name      string
	Transport string // config.MCPTransport* value
	URL       string
	Command   []string
	Headers   []string
	Env       []string

	// Line/Col record where the entry appeared so a conflict between two
	// repos declaring the same server name is reported at the right line.
	Line, Col int
}

MCPSpec is one entry in an `mcp (...)` block: an MCP server the project wants available to the agent inside the sandbox. Written as

<name> <url>                        http transport (the default)
<name> http|sse <url>               explicit remote transport
<name> stdio "<command> <args...>"  a local server process

followed by any number of `header "Name: value"` and `env NAME` modifiers on the same line.

Credential values never appear here — a `${VAR}` inside a header (or the implied `NAME=${NAME}` of an env entry) is kept verbatim and expanded by the runner inside the guest. See config.MCPServer.

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 / mcp / 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 SerialSpec added in v0.4.0

type SerialSpec struct {
	HostPath  string
	GuestName string

	// Line/Col record where the entry appeared so duplicate-name conflicts
	// can be reported back to the right line.
	Line, Col int
}

SerialSpec is one `serial (...)` entry: a host serial port presented as a device inside the guest.

  • HostPath: the device on this machine, e.g. /dev/cu.usbmodem1101. May be a glob, resolved when the port is opened rather than now.
  • GuestName: the bare name the device appears under in the guest's /dev. Empty defaults to the host device's basename — except for a glob, which has no basename and must name one explicitly.

type ShareSpec

type ShareSpec struct {
	HostPath  string
	GuestPath string // absolute guest mount point; empty = same as HostPath after tilde expansion
	ReadOnly  bool   // defaults to true at parse time (see parseSharesBlock)
	Line, Col int
}

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.

const (
	// SkillKindUnknown is the zero value, returned for paths that
	// match no rule. Callers should treat it as a parse error.
	SkillKindUnknown SkillKind = iota
	SkillKindLocalHome
	SkillKindLocalWorkspace
	SkillKindDistributed
)

func ClassifySkillPath

func ClassifySkillPath(path string) SkillKind

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.

func (SkillKind) String

func (k SkillKind) String() string

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

type SkillResolver interface {
	Resolve(SkillRef) (string, error)
}

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

type SkillResolverFunc func(SkillRef) (string, error)

SkillResolverFunc adapts a plain function into the SkillResolver interface. Useful for tests and the stub resolver below.

func (SkillResolverFunc) Resolve

func (f SkillResolverFunc) Resolve(s SkillRef) (string, error)

Resolve implements SkillResolver.

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)
	// ReverseForwards are the `reverse <spec>` entries of a forwards
	// block: host loopback ports exposed on the guest's loopback. Same
	// HOST:GUEST spelling as Forwards, opposite direction of travel.
	ReverseForwards []string
	Env             []string // env entries to export in the VM (canonical envspec form; see parseEnvBlock)

	// 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

	// Shares is the virtio-fs live-mount list (`shares (...)`).
	// See ShareSpec for semantics.
	Shares []ShareSpec

	// Serials is the forwarded serial-port list (`serial (...)`).
	// See SerialSpec for semantics.
	Serials []SerialSpec

	// MCP is the declared MCP server list (`mcp (...)`).
	// See MCPSpec for semantics.
	MCP []MCPSpec

	// 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

	// DiskMiB overrides the sparse ext4 root-disk ceiling, in mebibytes
	// (vm ( disk <size> )). Zero = the built-in sandbox.DefaultDiskSizeGiB.
	// The disk is sparse, so a larger ceiling mostly costs nothing until the
	// guest writes into it — the exception is the inode table, ~1/64 of the
	// ceiling, written at build time (see sandbox.DefaultDiskSizeGiB). Raise
	// it for repos with big dependency trees now that toolchain caches live
	// on the rootfs.
	DiskMiB uint64

	// SwapMiB is the guest swap device's capacity in mebibytes, declared as
	// `swap <size|off>` inside the `vm ( ... )` block. Zero = unset (the
	// built-in sandbox.DefaultSwapSizeMiB applies); negative = "off", no swap
	// device. The device is sparse, so the size bounds how much the guest may
	// swap rather than reserving anything up front.
	SwapMiB int64

	// 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

func ParseString(src string) (*Template, error)

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) Clone added in v0.4.0

func (t *Template) Clone() *Template

Clone returns a deep-enough copy of t for Merge to write into without touching the original's backing arrays. Every field is either a scalar or a slice of values, so copying the slices is sufficient — used by the host-wide defaults layer, which is loaded once and merged under several templates.

func (*Template) Merge

func (t *Template) Merge(over *Template)

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

func Lex(src string) ([]Token, error)

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).

func (Token) String

func (t Token) String() string

type TokenKind

type TokenKind int

TokenKind identifies a lexical token category.

const (
	TokEOF TokenKind = iota
	TokNewline
	TokLParen
	TokRParen
	TokEquals // '=', separates an env entry's name from its value
	TokIdent
	TokString // double-quoted, supports \" \\ \n \t escapes
)

func (TokenKind) String

func (k TokenKind) String() string

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 — the host-wide clawk.mod first, then the workspace
	// file and its overlay (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

	// GlobalPath is the host-wide clawk.mod folded in as the lowest layer,
	// or "" when there is none. Reporting only — its directives have already
	// been merged into File or a Repo's Clawkfile by the time a caller sees
	// this. See global.go.
	GlobalPath string

	// GlobalProfileMatched records that a clawk.mod.<profile> overlay beside
	// the host-wide file was applied, so a profile satisfied only by the
	// host-wide layer isn't reported as matching nothing.
	GlobalProfileMatched bool
}

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

func FindWorkspace(dir string) (*Workspace, error)

FindWorkspace is FindWorkspaceWithProfile with no profile.

func FindWorkspaceWithProfile

func FindWorkspaceWithProfile(dir, profile string) (*Workspace, error)

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

func LoadClawkfilePathWithProfile(path, profile string) (*Workspace, error)

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

func LoadStandaloneClawkfile(dir string) (*Workspace, error)

LoadStandaloneClawkfile is LoadStandaloneClawkfileWithProfile with no profile.

func LoadStandaloneClawkfileWithProfile

func LoadStandaloneClawkfileWithProfile(dir, profile string) (*Workspace, error)

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

func LoadWorkspace(path string) (*Workspace, error)

LoadWorkspace is LoadWorkspaceWithProfile with no profile.

func LoadWorkspaceWithProfile

func LoadWorkspaceWithProfile(path, profile string) (*Workspace, error)

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

func WorkspaceFromGitRepo(dir string) (*Workspace, error)

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.

func (*Workspace) FilterRepos

func (w *Workspace) FilterRepos(only []string) (*Workspace, error)

FilterRepos returns a new workspace containing only repos whose Name is in the `only` list. Unknown names produce an error listing what IS known — typos should surface loudly.

Jump to

Keyboard shortcuts

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