tool

package
v0.12.0 Latest Latest
Warning

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

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

Documentation

Overview

Package tool is the Tooling bounded context: the Tool interface, the ToolSpec the model sees, the Catalog (name→Tool registry with plan-mode filtering), and the filesystem seams (FileSystem, Workspace, and Environment) that every tool executes against.

FileSystem and Workspace live here, not in engine/port. ARCHITECTURE.md §2 names the Tooling context as their owner, and placing them here breaks the import cycle that would otherwise form: port references tool (LLMRequest.Tools is []tool.ToolSpec) and Tool.Execute takes an Environment, so if Environment lived in port, port and tool would import each other.

Allowed imports (ARCHITECTURE.md §3): the standard library and the session domain package only. It MUST NOT import port, adapter, agent, api, os, the OpenAI SDK, or any third-party library.

Package tool — cycle note: FileSystem, Workspace, and Environment are defined here, in the Tooling context that owns them (ARCHITECTURE.md §2), rather than in engine/port. This breaks the port↔tool import cycle that would form because port already imports tool (LLMRequest.Tools is []tool.ToolSpec) while Tool.Execute takes an Environment.

Index

Constants

View Source
const BashToolName = "Bash"

BashToolName is the catalog name of the Bash tool. It is the single authority for the name the Bash tool registers under (used in its Spec().Name) so a consumer can probe the catalog for bash enablement by referencing the constant rather than a local literal that could drift on a rename (see internal/adapter/server.Service.capabilities). It lives in the PORT package (not an adapter) because the permission evaluator special-cases the literal name — a tool named anything else would silently bypass the bash gate — so every Bash implementation must register under exactly this name, and engine/agent's own BashTool cannot import the fstools adapter to get it.

View Source
const MaxAgentBodyBytes = 32 * 1024

MaxAgentBodyBytes caps AgentDef.Body. The body is CHEAP relative to the description: it becomes a system-prompt layer that is in-context only for THAT ONE specialist engine's own turns (never summed across agents, never on the parent's requests), so it can afford generous headroom — 32 KiB lets a real specialist persona carry detailed instructions without losing the tail. (Contrast a skill body, which loads only on activation.) Same single-cap discipline as MaxAgentDescriptionBytes — one canonical cap every source truncates to. Changing it is the same one-time, deterministic prefix invalidation as the description cap.

View Source
const MaxAgentDescriptionBytes = 2000

MaxAgentDescriptionBytes caps AgentDef.Description. The description is the EXPENSIVE field: it is ALWAYS-IN-CONTEXT routing metadata (it rides the Subagent tool's Spec().Description tail on EVERY request, summed across ALL registered agents, and is part of the byte-stable prompt-cache prefix), so an unbounded one would inflate every prompt and break prefix caching. It therefore stays CONSERVATIVE — 2000 bytes is roomy for a routing sentence or two but still bounds the per-request, all-agents tax. It is the ONE canonical cap every source shares: the filesystem frontmatter parser truncates to it on discovery, and a remote-driver client re-truncates wire metadata to it defensively (the conformance suite asserts every listed def respects it).

Changing this value is a ONE-TIME prompt-cache-prefix invalidation (the truncation point moves), but discovery stays deterministic — the same input always truncates to the same bytes.

View Source
const ToolSearchName = "ToolSearch"

ToolSearchName is the catalog name of the built-in progressive-disclosure hydration tool.

Variables

View Source
var (
	// ErrInvalidMemoryKey reports a new-write key outside ValidMemoryKey's grammar.
	ErrInvalidMemoryKey = errors.New("invalid lifecycle memory key")
	// ErrSecretMemoryValue reports a rejected high-confidence credential shape.
	ErrSecretMemoryValue = errors.New("secret-shaped memory value or description")
	// ErrInstructionMemory reports a model-authored user memory that attempts to
	// persist a role or instruction override as future system context.
	ErrInstructionMemory = errors.New("instruction-shaped operator memory")
	// ErrMemoryNotFound reports a lifecycle mutation requiring a missing record.
	ErrMemoryNotFound = errors.New("memory record not found")
)
View Source
var (
	ErrSkillNotFound      = errors.New("tool: skill not found")
	ErrSkillAssetNotFound = errors.New("tool: skill asset not found")
)

Sentinel errors every SkillSource implementation returns (wrapped, so errors.Is holds) for an unknown skill (SkillBody/ListSkillAssets) or an unknown skill/asset pair (ReadSkillAsset).

View Source
var ErrDuplicateTool = fmt.Errorf("tool: duplicate registration")

ErrDuplicateTool is returned by Catalog.Register when a tool with the same name is already registered.

View Source
var ErrEnvironmentNoWorkspace = errors.New("tool: Environment requires a non-nil Workspace")

ErrEnvironmentNoWorkspace is the sentinel NewEnvironment returns when a caller tries to build an Environment with a nil Workspace. A Workspace is mandatory for a coherent environment, but not every tool consumes it; FS tools do, remote/self-contained tools may ignore the environment's capabilities.

View Source
var ErrNoShell = errors.New("tool: no shell available")

ErrNoShell is the sentinel a CommandRunner returns when it has no shell to execute against (e.g. the in-memory runner, or a shell-less remote pod). The Bash tool surfaces it to the model as a tool-level error rather than aborting the harness.

View Source
var ErrSearchBackendDown = errors.New("tool: search backend unavailable")

ErrSearchBackendDown is the sentinel a SearchProvider returns when a real, configured (or default) backend was attempted but is unreachable, rate-limited, or returned a fault/malformed result. It is the MANDATORY-degradation signal (issue #26): the WebSearch tool surfaces it as a model-visible message naming the upgrade path (set BRAVE_API_KEY or SEARXNG_URL) — never a silent empty result or a hang. It is DISTINCT from ErrSearchUnavailable (operator-disabled): a backend-down condition is environmental/transient, not a deployment choice.

View Source
var ErrSearchUnavailable = errors.New("tool: no search provider configured")

ErrSearchUnavailable is the sentinel a SearchProvider returns when web search is DISABLED on the deployment — the operator-set kill switch (--websearch=off), for which no backend is wired at all. The WebSearch tool surfaces it to the model as an honest "disabled on this deployment" message rather than aborting the harness — mirroring the ErrNoShell precedent the Bash tool uses for a shell-less CommandRunner. It is DISTINCT from ErrSearchBackendDown: this means "intentionally off", that means "a configured/default backend tried and failed".

Functions

func CanonicalMemoryText

func CanonicalMemoryText(value string) string

CanonicalMemoryText returns the single representation used to classify and render memory text. It repairs malformed UTF-8 and removes Unicode format characters plus controls other than LF and tab, matching the final memory profile, tool-result, and wire projections.

func DirectiveShapedUserMemory

func DirectiveShapedUserMemory(value string) bool

DirectiveShapedUserMemory reports narrow, high-confidence attempts to turn a model-authored user fact into a role or instruction override. It intentionally ignores ordinary preferences and prose discussing security.

func LedgerKey

func LedgerKey(root, path string) string

LedgerKey is the I/O-FREE lexical normalization a Workspace read-ledger uses to converge ordinary forms of the same in-root file onto one key, so a file read by relative path and then mutated by absolute in-root path (or vice versa) matches without any filesystem inspection. It performs NO Lstat/EvalSymlinks/ RPC: physical symlink aliases may conservatively produce distinct entries (a safe false-negative that forces another Read).

  • A session-RELATIVE path keys by its cleaned slash form (filepath.Clean, ToSlash). filepath.IsLocal reports not-relative for absolute/slash-prefixed operands; a relative path that climbs above the root ("../x") still keys by its cleaned form (the ledger is a lookup, not a confinement gate — confinement is the Workspace's business at use time).
  • An ordinary ABSOLUTE <root>/<rel> path is reduced with filepath.Rel so it converges with the relative <rel> form.
  • An absolute path that does NOT lie under root (an out-of-root relaxed-read path, or any path whose cleaned form climbs above root) keys by its cleaned absolute form, so a `..`-carrying lexical alias matches in both directions.

root MUST be the already-canonicalized absolute session root a Workspace reports via Root() (osfs canonicalizes it at construction; memfs passes its logical root). A relative path with an absolute root, or an absolute path with a relative root, is handled defensively: the former keys by the cleaned relative form, the latter by the cleaned absolute form.

func SecretShapedMemoryValue

func SecretShapedMemoryValue(key, value string) bool

SecretShapedMemoryValue reports only high-confidence credential shapes. It deliberately does not reject instruction-like or security-related prose.

func ValidMemoryKey

func ValidMemoryKey(key string) bool

ValidMemoryKey reports whether key follows the strict grammar for new writes: lowercase ASCII path segments beginning with a letter, with digits, '-' and '_' allowed thereafter, and a 128-byte total limit.

func ValidSkillAssetName

func ValidSkillAssetName(name string) bool

ValidSkillAssetName reports whether name is a valid LOGICAL asset name: non-empty, slash-separated, relative, no empty/"."/".."/ segments, no backslash, and no control or line-separator rune — the ONE validator every implementation/consumer shares.

func ValidateMemoryContent

func ValidateMemoryContent(key, value, description string) error

ValidateMemoryContent rejects high-confidence credentials in either value or description without changing the legacy key grammar.

func ValidateMemoryContentWrite

func ValidateMemoryContentWrite(key, value, description string, attribution MemoryAttribution) error

ValidateMemoryContentWrite is ValidateMemoryContent plus the narrow model-authored operator-instruction check, without imposing the lifecycle key grammar on legacy stores.

func ValidateMemoryEntry

func ValidateMemoryEntry(entry MemoryEntry) error

ValidateMemoryEntry applies lifecycle validation to an entire entry, including its description.

func ValidateMemoryEntryWrite

func ValidateMemoryEntryWrite(entry MemoryEntry, attribution MemoryAttribution) error

ValidateMemoryEntryWrite validates all persisted text at the authoritative write boundary. Instruction scanning is intentionally limited to model-authored user-scope facts: ordinary facts, Unicode prose, and user-authored imports remain accepted, while direct role/system override payloads cannot become durable system-prompt context.

func ValidateMemoryWrite

func ValidateMemoryWrite(key, value string) error

ValidateMemoryWrite applies the strict key grammar and content validation used by lifecycle writes. It validates the key before inspecting content so malformed keys consistently report ErrInvalidMemoryKey rather than a content-classification result.

func WithMemoryAttribution

func WithMemoryAttribution(ctx context.Context, attribution MemoryAttribution) context.Context

WithMemoryAttribution returns a child context carrying provenance facts for a future lifecycle write. It grants no authority.

func WithMemorySource

func WithMemorySource(ctx context.Context, source MemorySource) context.Context

WithMemorySource returns a child context with source handles replaced while preserving any writer and origin attribution already present.

Types

type AgentDef

type AgentDef struct {
	// Name is the def's stable identifier. It is the value the model passes to
	// the Subagent tool's `agent` arg to route to this def, and the team-member
	// AgentType handle. Agent names live in their OWN namespace and are NOT
	// validated against the tool catalog (an agent may share a name with a tool
	// without conflict).
	Name string
	// Description is the one-line summary: the cheap, always-in-context metadata
	// that steers the model on WHEN to route to this def. Single-line and
	// byte-capped by the source.
	Description string
	// Tools is the OPTIONAL allowlist of catalog tool names. Absent => the call
	// site's default set.
	Tools []string
	// DisallowedTools is an OPTIONAL subtractive filter applied AFTER
	// Tools/default.
	DisallowedTools []string
	// Model is the OPTIONAL model selector: an alias (sonnet/opus/haiku), a full
	// id, or "inherit"/empty (=> parent model). Aliases are resolved ONLY in the
	// composition layer, never here.
	Model string
	// Provider is the OPTIONAL provider-id selector (e.g. "openai",
	// "openrouter"). Empty => inherit the parent/session provider. It is PURE
	// DATA, orthogonal to Model: the id => provider resolution happens ONLY in
	// the composition layer. Mirrors Model exactly — a hint the composition
	// layer resolves.
	Provider string
	// PermissionMode is the OPTIONAL session permission mode hint
	// (default|plan|acceptEdits). Stored as the raw string; the domain mode
	// value object is resolved in the composition layer.
	PermissionMode string
	// MaxTurns is the OPTIONAL per-run turn cap. Zero => the caller default.
	MaxTurns int
	// MaxToolCalls is the OPTIONAL per-run tool-call cap. Zero => the caller
	// default. It mirrors MaxTurns: the composition layer maps it into the def's
	// session limits so a def's child (and its team-member session) is bounded
	// by it; a zero field falls back to the call site's default limit.
	MaxToolCalls int
	// Color is an OPTIONAL UX hint only (e.g. a TUI tag colour); it NEVER
	// affects execution.
	Color string
	// Skills is an OPTIONAL list of skill names to PRELOAD into this def's
	// engine. The composition layer resolves each name against the active skills
	// and injects the matched skill's body into the def's system prompt, so the
	// specialist starts with those playbooks already in context. An unknown name
	// is a non-fatal composition-time diagnostic.
	Skills []string
	// MCPServers is an OPTIONAL list of MCP servers to scope to this def's
	// engine. Each entry is EITHER a REFERENCE (a bare server name — the def
	// gets that already-configured main server's tools) OR an INLINE
	// streamable-HTTP server spec (name + url + optional headers — the def
	// connects its OWN server, whose tools never enter the main conversation).
	// An inline entry with no URL collapses to a reference.
	MCPServers []AgentMCPServer
	// Hooks is an OPTIONAL phase → shell-command map scoping lifecycle hooks to
	// this def's engine. Keys are governance hook phase names; an unknown phase
	// is a non-fatal composition-time diagnostic (validated in composition, not
	// here — the value object is taxonomy-free).
	Hooks map[string]string
	// Memory is the OPTIONAL persistent per-agent memory TIER selector. It is a
	// raw string, NEVER a path or locator (same discipline as Origin/Model/
	// Provider): the composition layer resolves the tier to a concrete directory.
	//   ""        => no memory (cold start, today's behaviour);
	//   "user"    => a cross-project per-agent dir under the XDG config base;
	//   "project" => workspace-relative, trust-gated like other project-tier
	//                artifacts (read only when the workspace is trusted).
	// The dir's MEMORY.md head is injected (read-only in v1) into the def's
	// system prompt at startup, so the specialist accumulates domain knowledge
	// across sessions. A scoped write path is deliberately deferred; the
	// directory scheme is forward-compatible with adding it later.
	Memory string
	// Body is the markdown content of the definition: the specialist's full
	// instructions, composed into the engine's system prompt by the composition
	// layer.
	Body string
	// Origin is the admission tier this def entered through (observability
	// only; see AgentOrigin). A tier label, NEVER a location.
	Origin AgentOrigin
}

AgentDef is a pure value object: one agent definition's metadata and system-prompt body. It carries no behaviour, no infrastructure types, and NO path/dir/root concept — where a definition came from is the source implementation's private business (Origin is a tier label, never a locator).

Only Name and Description are REQUIRED (they are the routing metadata the Subagent tool enumerates always-in-context). Everything else is optional: a def with only name+description+body is a pure prompt persona on the call site's default tool set and the parent model.

type AgentDefSource

type AgentDefSource interface {
	ListAgentDefs(ctx context.Context) ([]AgentDef, error) // sorted by Name, unique
}

AgentDefSource is the read-only seam agent definitions cross into the harness. Where a definition comes from — directories, a database, a registry process — is entirely the implementation's private business; no path, directory, or root concept appears here, so the engine cannot tell a filesystem source from a remote one.

Lifecycle: sources are SNAPSHOT-semantics — ListAgentDefs is stable for the life of the source (the harness resolves once at build; per-def child engines are built once, and the build-once trust-gate-completeness invariant depends on resolve-once).

type AgentMCPServer

type AgentMCPServer struct {
	// Name is the server's identifier. For a reference it must match a
	// configured main server's Name; for an inline server it becomes the
	// mcp__<name>__ tool namespace. Required for both forms.
	Name string
	// URL is the inline server's streamable-HTTP endpoint. Empty => this entry
	// is a REFERENCE to an already-configured main server (no new connection).
	URL string
	// Headers are extra HTTP headers for an inline server. SECRET-SHAPED (e.g.
	// Authorization): never logged and never projected into any inventory/
	// snapshot surface; it rides the driver wire only because driver dials
	// refuse non-local cleartext entirely. Ignored for a reference entry.
	Headers map[string]string
}

AgentMCPServer is one entry of a def's MCPServers. It is EITHER a reference to an already-configured (main) server — Name set, URL empty — OR an inline streamable-HTTP server the def connects on its own — Name + URL (+ optional Headers). IsReference reports which. It carries no transport object and no infrastructure type.

func (AgentMCPServer) IsReference

func (s AgentMCPServer) IsReference() bool

IsReference reports whether this entry references an already-configured main server (URL empty) rather than describing an inline server to connect.

type AgentOrigin

type AgentOrigin string

AgentOrigin classifies the ADMISSION TIER an agent definition entered the registry through. It is a tier label, NEVER a location: no implementation may put a path, directory, URL, or any locator in it (that is the adapter's private business). Enforcement of trust happens at SOURCE CONSTRUCTION time in the composition layer (an untrusted workspace's project tier is never constructed); Origin exists for observability and inspection only.

NOTE: this deliberately mirrors SkillOrigin rather than sharing a type. The THIRD origin-bearing seam has since arrived (prompt.RuleOrigin, issue #329) and extraction was re-evaluated and DEFERRED: the label sets are not identical (RuleOrigin has no "explicit" tier — rules carry no operator-flag lane), so a shared type would force a superset one seam must never mint.

const (
	AgentOriginExplicit AgentOrigin = "explicit" // operator-configured location/flag
	AgentOriginProject  AgentOrigin = "project"  // workspace-tier (trust-gated at construction)
	AgentOriginUser     AgentOrigin = "user"     // user-tier (never trust-gated)
	AgentOriginDriver   AgentOrigin = "driver"   // operator-configured remote driver
)

The CLOSED admission-tier label set — implementations must never mint a new label (a consumer that does not recognise one normalizes to Driver).

type AuthorityResourceResolver

type AuthorityResourceResolver interface {
	AuthorityResourcePath(path string) (target, workspace string, err error)
}

AuthorityResourceResolver derives the physical, workspace-confined identity of a local path for authority evaluation. Implementations must resolve symlinks using the same rules as filesystem access and reject an escape or any ambiguous path. The returned path and workspace are absolute paths; callers may project them into a policy resource descriptor without forwarding raw tool arguments.

It is an optional extension because authority evaluation is not a requirement of ordinary Workspace consumers. An authority-bound execution fails closed if its workspace does not implement it.

type Catalog

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

Catalog is a name→Tool registry. It is the registration seam for the core tools (and, later, MCP tools). It also computes the plan-mode-filtered view of the catalog, in which only read-only tools are visible.

func NewCatalog

func NewCatalog() *Catalog

NewCatalog returns an empty Catalog.

func (*Catalog) AdvertisedSpecs

func (c *Catalog) AdvertisedSpecs(mode session.PermissionMode) []ToolSpec

AdvertisedSpecs returns the per-turn tool inventory under progressive disclosure: for each available tool (mode-filtered, ordered by name) it returns the tool's Advertised() metadata spec when the tool implements Disclosable, otherwise its full Spec(). Because a tool that does not implement Disclosable falls back to its full Spec(), a catalog of only non-disclosable tools yields a result identical to Specs(mode) — making full-spec disclosure the default. The mode filter (plan-only exclusion in non-plan modes; read-only in plan mode) is applied first, so a PlanOnly tool is never advertised outside plan mode here either.

func (*Catalog) Available

func (c *Catalog) Available(mode session.PermissionMode) []Tool

Available returns the tools usable under the given permission mode, ordered by name. In ModePlan, non-read-only tools are filtered out (plan-mode read-only gating). In non-plan modes, a tool implementing PlanOnly (issue #206's PresentPlan signalling tool) is filtered OUT: it is registered everywhere so the shared and per-session catalog name-sets stay equal, but advertised/callable only in plan mode — the catalog projection is the gate, and the dispatcher's name+mode check is defense-in-depth on top of it.

func (*Catalog) Lookup

func (c *Catalog) Lookup(name string) (Tool, bool)

Lookup returns the tool registered under name and true, or nil and false.

func (*Catalog) MustRegister

func (c *Catalog) MustRegister(t Tool)

MustRegister adds t and panics on a duplicate. It is a convenience for static registration at startup where a duplicate is a programming error.

func (*Catalog) Register

func (c *Catalog) Register(t Tool) error

Register adds t to the catalog keyed by its Spec().Name. It returns ErrDuplicateTool if a tool with that name is already registered.

func (*Catalog) Specs

func (c *Catalog) Specs(mode session.PermissionMode) []ToolSpec

Specs returns the ToolSpecs of the tools available under the given permission mode, ordered by name. In ModePlan only read-only tools are exposed, enforcing plan-mode read-only gating at the catalog level before dispatch. In non-plan modes a tool implementing PlanOnly (issue #206's PresentPlan signalling tool) is EXCLUDED — a plan-only tool is registered everywhere but advertised only in plan mode (see PlanOnly's doc-comment for the projection gate rationale).

func (*Catalog) Tools

func (c *Catalog) Tools() []Tool

Tools returns all registered tools, ordered by name for determinism.

type CommandResult

type CommandResult struct {
	// Stdout is the captured standard output (already truncated by the adapter).
	Stdout string
	// Stderr is the captured standard error (already truncated by the adapter).
	Stderr string
	// ExitCode is the process exit status.
	ExitCode int
}

CommandResult is the outcome of a CommandRunner.Run invocation.

type CommandRunner

type CommandRunner interface {
	// Run executes command and returns its result. A non-zero exit is reported
	// via CommandResult.ExitCode (not error); error is for execution faults
	// (cancellation, timeout, or a missing shell — see ErrNoShell). The command
	// runs in the runner's bound namespace root.
	Run(ctx context.Context, command string) (CommandResult, error)
}

CommandRunner executes a shell command. Implementations may run it locally (/bin/sh), in a remote environment, or refuse it (no shell available). The agent loop never references this type — only the Bash tool depends on it, which is what makes the Bash tool (and therefore any command execution) optional in the catalog.

BOUND RUNNER (issue #462). A CommandRunner is bound to a single namespace at construction: it privately stores its root and Run executes against it. The per-call workdir parameter is GONE — a runner serves exactly one Environment (the main session, or a forked child whose runner is bound to the child namespace), so the command's cwd always matches the workspace the tool executes against. A runner that can no longer serve a shell returns ErrNoShell.

type CommandStreamer

type CommandStreamer interface {
	// RunStreaming runs command under the same shell rules as CommandRunner.Run
	// — the runner's BOUND namespace root, no per-call workdir (issue #462) —
	// with stdout and stderr written INTERLEAVED into out in the order the OS
	// delivers them. The CALLER owns bounding (e.g. a tail ring): the runner
	// writes everything it receives and does NOT also buffer or cap the stream.
	//
	// Cancellation and timeout semantics match Run: governed by ctx, with the
	// runner's default timeout applied when ctx has no deadline, and a
	// cancel/timeout reported as a harness-level err (the partial output
	// written so far stands). A non-zero exit is NOT an error — it is reported
	// via exitCode, which replaces CommandResult for this path; err is
	// reserved for execution faults (cancellation, timeout, or a missing
	// shell — see ErrNoShell).
	RunStreaming(ctx context.Context, command string, out io.Writer) (exitCode int, err error)
}

CommandStreamer is an OPTIONAL CommandRunner capability for callers that need the command's output streamed to a caller-owned sink instead of captured into the runner's internal (head-capped, first-bytes-win) buffers — e.g. a background command whose RECENT output the caller wants in a bounded tail ring, which a first-bytes capture cannot provide. Discover it with a type assertion on a CommandRunner; a runner that does not implement it simply declines, and the caller must fail soft (an honest "not supported by this runner"), never fall back to Run and silently lose the tail.

type Disclosable

type Disclosable interface {
	Tool
	// Advertised returns the cheap, metadata-only spec rendered into the per-turn
	// tool inventory under progressive disclosure. Spec() remains the full,
	// hydrate-on-demand specification.
	Advertised() ToolSpec
}

Disclosable is the OPTIONAL capability a Tool MAY implement to participate in progressive tool disclosure (pattern 9). A disclosable tool advertises a lightweight, metadata-only ToolSpec (typically name + a one-line description, with no or an empty Schema) until the model hydrates the full Spec() on demand via the ToolSearch tool. A tool that does NOT implement Disclosable is always advertised with its full Spec(), so the default catalog view is unchanged.

type Environment

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

Environment is the concrete, immutable execution environment a Tool.Execute runs against. It bundles the three things a tool needs from the host to run in one session/fork namespace:

  • an EnvironmentRef naming the backend family + opaque identity;
  • a NON-NULL Workspace (the rooted, path-scoped filesystem seam — every tool reads through it, and the agent-facing Edit/Write enforce their read-before-edit / conditional-mutation invariants through it);
  • an OPTIONAL bound CommandRunner (present when the host has a shell for this namespace — the main session, a worktree or force-copy fork child; absent for a file-less / in-memory / shell-less namespace). The Bash tool reads it off the Environment; a nil runner surfaces ErrNoShell.

It is NOT a service locator: it carries no policy, hooks, MCP, memory, forker, or merger. Those stay on the engine's Deps / the composition root. The Environment is the per-namespace CAPABILITY bundle threaded through the loop and handed to each Tool.Execute (as the env parameter), binding a Workspace + optional CommandRunner into one per-namespace seam.

It is immutable: construct once with NewEnvironment, read via the accessors. A fork constructs a fresh child Environment bound to the child namespace (EnvironmentForker); the parent Environment is never mutated.

The ZERO VALUE Environment{} has a nil Workspace and a nil CommandRunner and is INVALID for execution: NewEnvironment/MustEnvironment enforce a non-nil Workspace (the one mandatory capability), so every valid Environment carries a non-nil Workspace. Callers at trust boundaries MUST use NewEnvironment (handling its error) rather than assuming a zero value is usable; MustEnvironment is for construction sites where a nil Workspace is a programmer error.

func MustEnvironment

func MustEnvironment(ref session.EnvironmentRef, ws Workspace, runner CommandRunner) Environment

MustEnvironment constructs an Environment like NewEnvironment but panics on a nil workspace. Use it only at construction sites where a nil workspace is a programmer error (composition roots, test fixtures); prefer NewEnvironment at boundaries that read a config value.

func NewEnvironment

func NewEnvironment(ref session.EnvironmentRef, ws Workspace, runner CommandRunner) (Environment, error)

NewEnvironment constructs an immutable Environment from a ref, a NON-NULL workspace, and an OPTIONAL bound command runner. A nil workspace is rejected (it is the one mandatory capability); a nil runner is allowed and means "no shell in this namespace" (the Bash tool surfaces ErrNoShell).

func (Environment) CommandRunner

func (e Environment) CommandRunner() CommandRunner

CommandRunner returns the bound command runner, or nil when the namespace has no shell. The Bash tool surfaces nil as ErrNoShell.

func (Environment) Ref

Ref returns the Environment's backend identity.

func (Environment) Workspace

func (e Environment) Workspace() Workspace

Workspace returns the rooted, path-scoped filesystem seam. It is non-nil for an Environment built through NewEnvironment/MustEnvironment (which reject a nil workspace); the zero-value Environment{} has a nil Workspace and is invalid for execution. Most tools consume it through Tool.Execute's env parameter rather than reading it from the Environment directly.

type EnvironmentForker

type EnvironmentForker interface {
	// Fork creates an isolated child Environment derived from base, returning
	// the child Environment, a cleanup func, and an OPTIONAL degraded-fork
	// advisory. label is a short, human-meaningful tag (e.g. the branch idea or
	// task name) implementations MAY fold into the child path or worktree
	// branch for observability; it need not be unique and is not load-bearing.
	// Implementations may use git worktrees, a copy, or an overlay; the agent
	// never knows which. The returned cleanup is always non-nil when err is nil
	// and removes the child's backing storage.
	//
	// advisory is an OPTIONAL human-readable note about a DEGRADED-but-usable
	// fork — empty in the normal case. It is the channel for "the child is
	// usable, but not exactly the workspace you'd expect": an implementation
	// that could not fully reproduce the base (e.g. an overlay that fell back to
	// a clean checkout) returns a note describing what the child is actually
	// seeing, and the agent MAY prepend it to the child's view so the child
	// reasons honestly about the degradation rather than silently. It is
	// generic (not tied to any one isolation strategy) and never load-bearing
	// for safety — purely informational.
	Fork(ctx context.Context, base Environment, label string) (child Environment, cleanup func() error, advisory string, err error)
}

EnvironmentForker is the environment-isolation seam for fork-join parallelism (harness pattern 8, ADR 0211). It produces an isolated CHILD Environment (Workspace + command runner bound to the child namespace + ref) derived from a base Environment so a forked agent loop can read — and, when its catalog allows it, WRITE — without racing on, or mutating, the shared base tree.

It replaces the former WorkspaceForker (issue #462). The forker now returns a COMPLETE child Environment — Workspace AND a command runner bound to the child namespace — so a forked child's Bash observes the SAME child namespace its Read/Write do, never the parent's. The runner is bound by the forker (the forker owns the git-worktree / force-copy isolation), so the per-call workdir the old seam threaded is gone.

It lives here in engine/tool, next to Environment, for the same layering reason Environment does: port already imports tool, so a separate package would risk the port↔tool import cycle. The interface replaces the old WorkspaceForker in this intentional breaking change (issue #462); the two seams are NOT maintained in parallel.

Implementations may isolate via a git worktree, a recursive directory copy, or an overlay; the agent never knows (or cares) which. The contract is only:

  • the returned child is a fully usable Environment rooted at an isolated namespace (Workspace + bound runner);
  • writes through the child do NOT affect the base tree;
  • cleanup tears the child down (removes the worktree/copy) and is safe to call exactly once after the child is no longer in use.

type EnvironmentMerger

type EnvironmentMerger interface {
	// Merge applies the diff of the fork at child (its working tree vs its
	// HEAD) into the parent Environment's workspace. On conflict it returns a
	// non-nil error describing the conflict; the fork (the child Environment)
	// is left in place for manual resolution.
	Merge(ctx context.Context, child, parent Environment) error
}

EnvironmentMerger is the OPTIONAL seam by which a preserved fork's changes are merged BACK into the parent Environment. It serves ONE delegation path: Parallel's single-branch fast path (a Parallel run with exactly one branch and join=first/judge applies the winner's diff to the parent), so a delegated implementer's edits actually LAND without a manual copy/merge step. (The writable Subagent does NOT use this seam — mode:"read-write" edits the parent tree directly during the run; see ADR 0041.)

It replaces the former ForkMerger (issue #462). Merge now receives the CHILD and PARENT Environments (not a forkRoot string + parent Workspace): the merger reads the child's identity and workspace off the child Environment and applies its diff into the parent Environment's workspace. No forkRoot string crosses the core interface.

It lives here in engine/tool, next to EnvironmentForker, for the same layering reason (port already imports tool). The interface replaces the old ForkMerger in this intentional breaking change (issue #462); the two seams are NOT maintained in parallel.

Contract:

  • Merge applies the fork's working-tree-vs-HEAD diff to the parent workspace. It MUST NOT force: on a conflict it returns a non-nil error naming the conflict so the operator can resolve manually. The fork is left intact (the caller still owns its cleanup / reaper slot) so a failed merge is recoverable.
  • The merge runs in the PARENT workspace under the PARENT's trust posture, not the fork's — the fork's content is untrusted child-authored data, but applying a diff is a parent-side operation (the same trust the parent's own Edit/Write carries). Composition decides whether to wire a merger at all — when wired, auto-merge is DEFAULT-ON (no flag; see ADR 0039). The composition-injected merger is SERIALIZED process-wide (a single mutex in a serializing decorator) so concurrent merges from Parallel never interleave their writes into a parent workspace.
  • nil merger (the default) means no auto-merge: the historical no-auto-merge boundary holds unchanged. ParallelTool.ReadOnly()/SubagentTool.ReadOnly() stay true so read-only fan-out keeps batching in parallel; but a CALL that will actually merge is excluded from the concurrent read batch via MutatesParent (dispatch-serial, flushed alone — see agent.parentMutatingCaller), so its post-run merge never overlaps a sibling parent read, and cross-run merge-vs-merge is serialized by the shared SerializingMerger mutex.

type FileInfo

type FileInfo struct {
	// Name is the base name of the file.
	Name string
	// Size is the length in bytes.
	Size int64
	// Mode is the file mode bits.
	Mode fs.FileMode
	// ModTime is the last-modification time.
	ModTime time.Time
	// IsDir reports whether the entry is a directory.
	IsDir bool
}

FileInfo is the minimal, provider-neutral file metadata the tools need. It is a subset of io/fs.FileInfo carried as plain fields so adapters (osfs, memfs) can populate it without leaking os types into the domain.

type FileSystem

type FileSystem interface {
	// Read returns the entire contents of the file at path.
	Read(ctx context.Context, path string) ([]byte, error)
	// Stat returns metadata for the file at path.
	Stat(ctx context.Context, path string) (FileInfo, error)
	// Glob returns the paths matching the shell-style pattern.
	Glob(ctx context.Context, pattern string) ([]string, error)
}

FileSystem is the low-level, path-oriented filesystem seam. Adapters implement it over the real OS (osfs) and over memory (memfs). Paths are interpreted by the adapter; the session-scoping and the Edit read-ledger live in Workspace, which composes a FileSystem.

type FileVersion

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

FileVersion is the opaque, comparable content version a Workspace attaches to a version-bearing read (ADR 0208). It is an adapter-minted token (a content hash, an inode+mtime pair, a remote ETag, …) the caller compares for equality with another FileVersion from the SAME adapter and passes back to a conditional mutation. It carries NO meaning outside equality and is NEVER used as a sentinel: there is deliberately no "any"/"wildcard"/"unversioned" FileVersion that means "overwrite unconditionally" — an unconditional overwrite is a distinct adapter/bootstrap operation that is deliberately NOT part of the Workspace capability passed to Tool.Execute. The zero value is an unusable placeholder; RecordedVersion returns ok=false (not a zero FileVersion sentinel) for a path that was never recorded.

func NewFileVersion

func NewFileVersion(token string) FileVersion

NewFileVersion mints a FileVersion from an adapter-private token. It is the constructor adapters use to wrap the opaque token they computed (a content hash, an ETag, …); the field stays unexported so callers cannot inspect it. The agent-facing Read/Edit/Write tools never call this — they only receive FileVersions from ReadVersion/CreateFile/ReplaceFile and pass them back. An empty token is still a valid opaque token, never an "any version" sentinel.

func (FileVersion) Equal

func (f FileVersion) Equal(o FileVersion) bool

Equal reports whether two valid FileVersions carry the same opaque token. The zero value is invalid and never equals any version, including another zero value.

func (FileVersion) Token

func (f FileVersion) Token() (token string, ok bool)

Token returns the adapter-private opaque token this FileVersion carries, and ok reports whether the version is valid (a non-zero FileVersion). The zero value returns ("", false) — it is never an "any version" sentinel — so a caller can distinguish "no version recorded" from "an adapter minted the empty-string token". It is the serializer hook for a planned remote backend that must round-trip an adapter-minted version over the wire: the backend stores the token verbatim and reconstructs the FileVersion with NewFileVersion(token) on the way back. Callers MUST treat the token as opaque (compare with Equal, never inspect its bytes); only a serializer owned by the SAME adapter that minted the version ever reads it.

type GrepMatch

type GrepMatch struct {
	// Path is the session-relative file the match occurred in.
	Path string
	// Line is the 1-based line number of the match.
	Line int
	// Text is the matching line's content.
	Text string
}

GrepMatch is a single Workspace.Grep hit.

type MemoryAttribution

type MemoryAttribution struct {
	Writer MemoryWriter
	Origin MemoryOrigin
	Source MemorySource
}

MemoryAttribution carries optional provenance facts for a lifecycle write. These values are informational: stores must never derive authorization from context attribution.

func MemoryAttributionFromContext

func MemoryAttributionFromContext(ctx context.Context) (MemoryAttribution, bool)

MemoryAttributionFromContext returns the provenance facts carried by ctx.

type MemoryConvergenceStore

type MemoryConvergenceStore interface {
	MemoryLifecycleStore
	RememberIfCurrent(ctx context.Context, entry MemoryEntry, expected MemoryCurrent) (MemoryRecord, error)
}

MemoryConvergenceStore is the additive create-or-update CAS capability used by convergence workflows. Implementations compare and append atomically.

type MemoryCurrent

type MemoryCurrent struct {
	Exists  bool
	Version MemoryVersion
}

MemoryCurrent identifies the complete expected current state for an atomic mutation. Exists=false means the key must be absent; Exists=true requires the exact opaque Version.

type MemoryEntry

type MemoryEntry struct {
	// Key is the opaque lookup key (e.g. "pref/test-runner").
	Key string
	// Value is the stored text. The store treats it as opaque bytes. It is
	// EMPTY in MemoryStore.Index results, which omit values by design (the
	// tier-0 index carries only the routing table, not the payload).
	Value string
	// Description is an optional one-line summary used as the tier-0 index hook.
	// When empty on write, the store derives one from the value's first line; so
	// Index results always carry a non-empty Description even for entries that
	// were stored without one.
	Description string
	// UpdatedAt is the wall-clock time the entry was last written.
	UpdatedAt time.Time
}

MemoryEntry is a single cross-session memory record: an opaque key, its stored value, an optional one-line description, and the wall-clock time it was last written. It is the unit returned by MemoryStore.Recall, MemoryStore.List and MemoryStore.Index.

type MemoryLifecycleStore

type MemoryLifecycleStore interface {
	RememberVersioned(ctx context.Context, entry MemoryEntry, expected MemoryVersion) (MemoryRecord, error)
	Inspect(ctx context.Context, key string) (MemoryRecord, bool, error)
	ForgetVersioned(ctx context.Context, key string, expected MemoryVersion) (MemoryRecord, error)
	UndoLatest(ctx context.Context, key string, expected MemoryVersion) (MemoryRecord, error)
}

MemoryLifecycleStore is the optional additive capability for stores that retain versioned memory history. MemoryStore remains unchanged. Mutations are atomic. RememberVersioned with an empty expected version is an unconditional last-write-wins update, matching MemoryStore.RememberEntry; a non-empty expected version enables compare-and-swap. ForgetVersioned and UndoLatest always require a non-empty expected version.

type MemoryOrigin

type MemoryOrigin string

MemoryOrigin identifies the workflow that produced a revision.

const (
	// MemoryOriginExplicit identifies an explicit remember operation.
	MemoryOriginExplicit MemoryOrigin = "explicit"
	// MemoryOriginLearning identifies automatic completed-trajectory learning.
	MemoryOriginLearning MemoryOrigin = "learning"
	// MemoryOriginConsolidation identifies background dream/consolidation writes.
	MemoryOriginConsolidation MemoryOrigin = "consolidation"
	// MemoryOriginImported identifies a legacy or imported revision.
	MemoryOriginImported MemoryOrigin = "imported"
	// MemoryOriginUndo identifies a compensating undo revision.
	MemoryOriginUndo MemoryOrigin = "undo"
)

type MemoryRecord

type MemoryRecord struct {
	Current   MemoryRevision
	Revisions []MemoryRevision
}

MemoryRecord is the current revision plus its revision history. Revisions are ordered oldest to newest; Current is repeated explicitly for cheap remote projections and profile reads.

type MemoryRevision

type MemoryRevision struct {
	Key         string
	Value       string
	Description string
	Version     MemoryVersion
	Status      MemoryStatus
	Writer      MemoryWriter
	Origin      MemoryOrigin
	Source      MemorySource
	UpdatedAt   time.Time
}

MemoryRevision is one immutable revision of a durable memory record.

type MemorySource

type MemorySource struct {
	SessionID  string
	ProposalID string
}

MemorySource identifies the optional durable session associated with a revision. Empty means unavailable; callers must not invent one.

type MemoryStatus

type MemoryStatus string

MemoryStatus is the durable lifecycle state of a memory revision.

const (
	// MemoryStatusActive marks the current readable revision.
	MemoryStatusActive MemoryStatus = "active"
	// MemoryStatusSuperseded marks a revision replaced by a later active value.
	MemoryStatusSuperseded MemoryStatus = "superseded"
	// MemoryStatusDeleted marks a tombstone revision.
	MemoryStatusDeleted MemoryStatus = "deleted"
)

type MemoryStore

type MemoryStore interface {
	// RememberEntry stores e, overwriting any existing entry under e.Key and
	// bumping its UpdatedAt. e.Description is the optional one-line tier-0 hook;
	// an empty description means "derive from the value's first non-empty line
	// on Index". An empty (or whitespace-only) key is rejected with an error.
	RememberEntry(ctx context.Context, e MemoryEntry) error
	// Recall returns the entry for the exact key. The boolean reports whether an
	// entry was found; a miss is (zero, false, nil), not an error.
	Recall(ctx context.Context, key string) (MemoryEntry, bool, error)
	// List returns all entries whose key has the given prefix, sorted by key for
	// deterministic output. An empty prefix returns every entry. Unlike Index and
	// Search, List returns FULL entries — Value included — so consumers (e.g. a
	// consolidation planner, a prefix-fallback read) can load payloads from it.
	List(ctx context.Context, prefix string) ([]MemoryEntry, error)
	// Forget deletes the entry for key. Deleting a missing key is not an error.
	Forget(ctx context.Context, key string) error
	// Index returns the tier-0 routing table: every entry as (key, description,
	// updated-at) with the VALUE OMITTED, sorted by key for deterministic output.
	// The implementation fills Description (explicit, else derived from the
	// value's first non-empty line) but does NOT apply the tier-0 size cap —
	// capping/rendering is the consumer's concern.
	Index(ctx context.Context) ([]MemoryEntry, error)
	// Search returns up to k entries relevant to query, best-first. Like Index,
	// results carry (key, description, updated-at) with the VALUE OMITTED — the
	// value may participate in scoring but is never returned; callers Recall a
	// key to load it. Ordering must be deterministic for identical store state
	// and query; entries with no relevance to the query are dropped, not padded.
	// An empty or whitespace-only query yields an empty slice, NOT an error.
	// k <= 0 selects the implementation's default page size. Ranking is
	// implementation-defined (the reference adapter uses local lexical BM25).
	Search(ctx context.Context, query string, k int) ([]MemoryEntry, error)
}

MemoryStore is the seam for conservative, cross-session ("tiered") memory (harness pattern 3). It is defined here, alongside Workspace and CommandRunner, for the same layering reason: the memory tools depend on it the way the Bash tool depends on CommandRunner, and keeping the interface in engine/tool avoids the port↔tool import cycle a separate package would risk.

SCOPING: a MemoryStore is scoped per STORE INSTANCE — the composition root constructs one instance per scope (a per-project store for project memory, a per-user store for the user model), so entries written in one session are visible to later sessions over the SAME scope and never across scopes. Implementations must be safe for concurrent use and durable across process restarts; HOW they achieve that (file locking, a remote service, ...) is adapter-internal and must not leak into this contract.

Conformance: engine/adapter/memconformance is the shared behavioral suite every implementation must pass (the flock-file reference adapter runs it today; remote drivers run it over their client).

type MemoryVersion

type MemoryVersion string

MemoryVersion is an opaque revision token. Callers may persist and compare it, but must not interpret its contents.

type MemoryVersionConflictError

type MemoryVersionConflictError struct {
	Key      string
	Expected MemoryVersion
	Actual   MemoryVersion
}

MemoryVersionConflictError reports a failed lifecycle compare-version operation. Actual is empty when no current record exists.

func (*MemoryVersionConflictError) Error

type MemoryWriter

type MemoryWriter string

MemoryWriter identifies the kind of actor that produced a revision. It is attribution only and conveys no authority.

const (
	// MemoryWriterUser identifies a direct user-authored revision.
	MemoryWriterUser MemoryWriter = "user"
	// MemoryWriterModel identifies a model-authored revision.
	MemoryWriterModel MemoryWriter = "model"
	// MemoryWriterSystem identifies a harness-authored revision.
	MemoryWriterSystem MemoryWriter = "system"
)

type PlanOnly

type PlanOnly interface {
	Tool
	// PlanOnlyTool is the marker method. It carries no behaviour; implementing it
	// (alongside Tool) opts the tool into plan-mode-only advertisement.
	PlanOnlyTool()
}

PlanOnly is the OPTIONAL capability a Tool MAY implement to declare that it is a plan-mode signalling tool: registered everywhere (so the shared and per-session catalog name-sets stay equal — guarded by TestPerSessionCatalogMatchesSharedCatalog) but advertised/callable ONLY in ModePlan. The catalog's mode projection (Available / Specs / AdvertisedSpecs) EXCLUDES a PlanOnly tool from every non-plan mode, so it is never offered to the model in default/acceptEdits.

This is the projection gate for PresentPlan (issue #206): the plan-approval signalling tool is registered into every catalog (name-set equality holds) but the projection hides it outside plan mode. The dispatcher's name+mode check (sess.Mode == ModePlan && c.Name == "PresentPlan") is defense-in-depth ON TOP of this gate, not the sole gate. A tool that does NOT implement PlanOnly is advertised in every mode it is otherwise eligible for (read-only tools in plan mode, all tools in default/acceptEdits), so the default catalog view is unchanged for every non-plan-signalling tool.

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

Search is the built-in ToolSearch hydration tool for progressive tool disclosure (pattern 9). Given a substring query it returns the FULL ToolSpec of each matching tool in the catalog, so the model can pull a tool's schema into context on demand after seeing only its advertised metadata. It is read-only and excludes itself from results. It is registered under ToolSearchName.

func NewToolSearch

func NewToolSearch(cat *Catalog) *Search

NewToolSearch constructs a ToolSearch backed by cat. The composition root (or NewEngine, when progressive disclosure is enabled) registers it into the same catalog it queries.

func (*Search) Execute

Execute returns the full specs of catalog tools whose name or description contains the query (case-insensitive substring). An empty query returns all tools. ToolSearch never returns itself. Results are ordered by name.

func (*Search) ReadOnly

func (*Search) ReadOnly() bool

ReadOnly reports that ToolSearch only reads the catalog, so it may run in parallel with other read-only tools.

func (*Search) Spec

func (*Search) Spec() ToolSpec

Spec returns the model-facing specification of the ToolSearch tool.

type SearchProvider

type SearchProvider interface {
	// Search runs q and returns ranked results, best-first. The Limit on q is
	// already clamped by the tool before the call (the tool is the bounding choke
	// point); an implementation MAY further cap but must never EXCEED it. A
	// not-configured backend returns ErrSearchUnavailable; any other non-nil error
	// is a backend fault the tool renders as a model-facing tool error.
	Search(ctx context.Context, q SearchQuery) ([]SearchResult, error)
}

SearchProvider is the outbound web-search seam the WebSearch tool depends on, the way the Bash tool depends on CommandRunner. It lives here in engine/tool, NOT engine/port, for the same layering reason FileSystem/Workspace/CommandRunner do: it is a TOOL collaborator injected at execution, never a loop port the agent.Engine references. The agent loop never names this type — only the WebSearch tool does, which is what keeps web search optional in the catalog.

Implementations must be safe for concurrent use: the read-parallel dispatcher fans out N concurrent WebSearch calls per turn, so an adapter that performs network I/O must carry its OWN per-call timeout AND a concurrency/rate limit internally (never relying on the dispatcher or the tool to bound egress).

type SearchQuery

type SearchQuery struct {
	// Query is the verbatim search string. Never mutated by an adapter.
	Query string
	// Limit is the maximum number of results to return, already clamped by the
	// tool to its default-when-absent and hard-max bounds.
	Limit int
	// Site, when non-empty, restricts results to a single site/domain (e.g.
	// "go.dev"). Adapter maps it onto the backend's site filter if supported.
	Site string
	// Freshness, when non-empty, is an opaque recency hint (e.g. "day", "week",
	// "month") an adapter MAY map onto its backend's recency filter.
	Freshness string
}

SearchQuery is one web-search request. Query is the verbatim user/model query — implementations MUST pass it through unchanged (secret-scanning is the guardrails layer's job, not the search adapter's). Limit is the clamped result cap. Site and Freshness are optional refinements an adapter MAY map onto its backend's parameters (or ignore if unsupported).

type SearchResult

type SearchResult struct {
	// Title is the result's title/headline.
	Title string
	// URL is the candidate source URL (the handle for a follow-up WebFetch).
	URL string
	// Snippet is a short excerpt/summary of the result.
	Snippet string
	// Date is an optional publication/last-modified date string, as the backend
	// reported it (no parsing/normalisation in the domain).
	Date string
	// Source is an optional source/publisher name (the attribution axis).
	Source string
}

SearchResult is one discovered source. URL is the candidate the model can then pass to WebFetch to retrieve the full page; the other fields are compact, attribution-oriented metadata. Every field is UNTRUSTED external content — the WebSearch tool fences it before it reaches the model.

type SkillAsset

type SkillAsset struct {
	Name       string
	Size       int64 // payload size in bytes (advisory; readers re-enforce caps)
	Executable bool  // advisory source metadata; the Skill tool never materializes or executes it
}

SkillAsset describes one auxiliary payload of a skill, addressed by LOGICAL name. A logical name is a slash-separated, RELATIVE identifier in the skill's own namespace (e.g. "references/api.md", "scripts/run.sh") — the exact namespace skill instruction bodies already reference. Not an OS path: no leading separator, no "."/".." segments, no backslashes, no NUL (ValidSkillAssetName is the single shared validator).

type SkillMeta

type SkillMeta struct {
	Name        string      // stable activation key; non-empty
	Description string      // one-line trigger metadata; non-empty, single-line, byte-capped by the source
	Origin      SkillOrigin // admission tier (observability only)
	HasAssets   bool        // whether ListSkillAssets will return at least one asset
	// License is the optional `license` frontmatter field (e.g. "MIT",
	// "Apache-2.0"). ADVISORY/observability metadata only — never trust-bearing,
	// never a gate; empty when the skill omits it. Carried verbatim from the
	// source (byte-capped defensively), never interpreted.
	License string
	// Compatibility is the optional `compatibility` frontmatter field (a free-form
	// advisory string such as "mecatl >= 0.1"). ADVISORY/observability metadata
	// only — never trust-bearing, never enforced as a gate; empty when the skill
	// omits it. Surfaced as an advisory note on activation, never parsed.
	Compatibility string
	// Metadata is the optional `metadata` frontmatter map (string→string), a
	// free-form advisory bag (e.g. {author: stacklok, version: "1"}). ADVISORY/
	// observability metadata only — never trust-bearing; nil/empty when the skill
	// omits it. Values are byte-capped defensively; the whole map drops to nil on
	// an oversized entry/count. Never interpreted by the harness.
	Metadata map[string]string
	// AllowedTools is the optional `allowed-tools` frontmatter field
	// (agentskills.io, Experimental): a list of tool names the skill EXPECTS to
	// use. ADVISORY METADATA ONLY — it names the tools the skill anticipates
	// calling, surfaced as a note on Skill activation so the model learns the
	// author's intent. It is NEVER a permission grant: the permission evaluator
	// (governance/port.PermissionPolicy/engine/agent dispatch) NEVER reads it.
	// Every call still resolves through the normal deny-dominant policy — at
	// every posture, including yolo — so a skill declaring `allowed-tools: "Bash"`
	// does NOT pre-approve, loosen, or auto-approve a Bash call. nil/empty when the
	// skill omits it (a skill without the field renders byte-identically to
	// before). The parser splits the YAML value on whitespace and defensively
	// caps the count at ≤64 names and each name at ≤64 chars (recording a
	// non-fatal warning note on overflow, keeping the parsed prefix).
	AllowedTools []string
}

SkillMeta is the always-in-context metadata layer of one skill: what the Skill tool's description enumerates. Pure value object — no body, no behaviour, NO file/path/root concept.

type SkillOrigin

type SkillOrigin string

SkillOrigin classifies the ADMISSION TIER a skill entered the catalog through. It is a tier label, NEVER a location: no implementation may put a path, directory, URL, or any locator in it (that is the adapter's private business). Enforcement of trust happens at SOURCE CONSTRUCTION time in the composition layer (an untrusted workspace's project tier is never constructed); Origin exists for observability and inspection only.

const (
	SkillOriginExplicit SkillOrigin = "explicit" // operator-configured location/flag
	SkillOriginProject  SkillOrigin = "project"  // workspace-tier (trust-gated at construction)
	SkillOriginUser     SkillOrigin = "user"     // user-tier (never trust-gated)
	SkillOriginDriver   SkillOrigin = "driver"   // operator-configured remote driver
)

The CLOSED admission-tier label set — implementations must never mint a new label (a consumer that does not recognise one normalizes to Driver).

type SkillSource

type SkillSource interface {
	ListSkills(ctx context.Context) ([]SkillMeta, error)                     // sorted by Name, unique
	SkillBody(ctx context.Context, name string) (string, error)              // unknown → ErrSkillNotFound
	ListSkillAssets(ctx context.Context, name string) ([]SkillAsset, error)  // unknown → ErrSkillNotFound
	ReadSkillAsset(ctx context.Context, skill, asset string) ([]byte, error) // unknown → ErrSkillAssetNotFound; invalid name → error, never content
}

SkillSource is the read-only seam skills cross into the harness: a LOGICAL BUNDLE of identity + trigger metadata (ListSkills), an instruction body (SkillBody), and auxiliary payloads addressed by logical name (ListSkillAssets/ReadSkillAsset). Where a bundle comes from — directories, a database, a registry process — is entirely the implementation's private business; no path, directory, or root concept appears here, so the engine cannot tell a filesystem source from a remote one.

Lifecycle: sources are SNAPSHOT-semantics — ListSkills is stable for the life of the source (the harness resolves once at build; there is no watch seam, deliberately matching the build-once trust-gate invariant).

type Tool

type Tool interface {
	// Spec returns the model-facing specification of the tool.
	Spec() ToolSpec
	// ReadOnly reports whether the tool only reads state (so the dispatcher may
	// run it in parallel with other read-only tools) versus mutating state
	// (which must run serially).
	ReadOnly() bool
	// Execute runs the tool. ctx carries cancellation; in is the model's call;
	// env is the session-scoped Environment (Workspace + optional bound
	// CommandRunner + backend ref). It returns a ToolResult (with IsError set
	// on a tool-level failure that should be fed back to the model) and a
	// non-nil error only for harness-level failures.
	Execute(ctx context.Context, in session.ToolCall, env Environment) (session.ToolResult, error)
}

Tool is the contract every tool implements. ReadOnly drives the loop's read-parallel / mutate-serial dispatch. Execute runs the tool against a session-scoped Environment (Workspace + optional bound CommandRunner + ref) and returns a domain ToolResult.

type ToolSpec

type ToolSpec struct {
	// Name is the tool's catalog name.
	Name string
	// Description is the model-facing documentation for the tool.
	Description string
	// Schema is the JSON schema describing the tool's Args.
	Schema json.RawMessage
}

ToolSpec is what the model sees for a tool: its name, a documentation-quality description (when to use / when not / example / limits), and the JSON schema for its arguments. ToolSpecs are stable across turns so the LLM adapter can cache them.

type VersionMismatchError

type VersionMismatchError struct {
	// Path is the session-relative (or canonical) path that mismatched.
	Path string
}

VersionMismatchError is the error ReplaceFile returns when the file's current authoritative version does not equal the old version the caller supplied — a concurrent mutation landed between the caller's read and its conditional replace. Callers classify it with errors.As. It is the model-visible "changed since you read it" condition surfaced by the Edit/Write tools.

func (*VersionMismatchError) Error

func (e *VersionMismatchError) Error() string

Error implements the error interface without exposing either opaque version.

type Workspace

type Workspace interface {
	// WorkspaceReader is the read-only subset (Root + plain Read + Stat);
	// embedding it keeps the read methods defined once and lets a *Workspace
	// satisfy a read-only consumer. Non-agent consumers use the plain Read;
	// agent-facing tools use ReadVersion below.
	WorkspaceReader

	// ReadVersion returns the contents of the file at path AND the authoritative
	// FileVersion the adapter currently holds for it. It is the version-bearing
	// read the built-in Read tool uses (recording the returned version via
	// RecordRead). It reads the SAME backing store as the plain Read; the only
	// difference is it also mints and returns a FileVersion.
	ReadVersion(ctx context.Context, path string) ([]byte, FileVersion, error)

	// CreateFile creates a NEW file at path with the given content, atomically.
	// It fails (wrapping fs.ErrExist) if a file already exists at path — it is
	// create-only, NEVER an overwrite, so it can never silently clobber an
	// existing file. Parent directories are created as needed. It returns the
	// new file's FileVersion. The agent-facing Write tool uses it for a
	// not-yet-existing path.
	CreateFile(ctx context.Context, path string, data []byte) (FileVersion, error)

	// ReplaceFile conditionally replaces the contents of the file at path with
	// data, ONLY if the file's CURRENT authoritative FileVersion equals old.
	// On success it returns the new FileVersion. On a version mismatch (the
	// file changed between the caller's ReadVersion and this call — including a
	// concurrent mutation through the same backend) it returns a
	// *VersionMismatchError; the caller re-reads and retries. If the file does
	// not exist it returns an error wrapping fs.ErrNotExist. It is the
	// conditional CAS the agent-facing Edit and existing-file Write tools finish
	// with, against the CURRENT version their own ReadVersion just returned. old must be a FileVersion the SAME adapter
	// minted (from ReadVersion or a prior CreateFile/ReplaceFile); a zero
	// FileVersion is never a valid "any version" sentinel — it always mismatches.
	ReplaceFile(ctx context.Context, path string, old FileVersion, data []byte) (FileVersion, error)

	// Glob returns session-relative paths matching the shell-style pattern.
	Glob(ctx context.Context, pattern string) ([]string, error)
	// Grep returns the matches of a regular expression across files selected by
	// an optional path glob. Results are capped/shaped by the adapter.
	Grep(ctx context.Context, pattern, pathGlob string) ([]GrepMatch, error)

	// RecordRead records that path was read at the authoritative version. It is
	// a PURE IN-MEMORY store: it performs NO I/O and stores the EXACT version
	// passed (the caller supplies the FileVersion its ReadVersion returned). A
	// later RecordedVersion lookup compares against this stored token. The
	// built-in Read tool calls it with the version ReadVersion minted; the
	// built-in Edit/Write tools call it after a successful CreateFile/ReplaceFile
	// so a subsequent same-turn Edit stays valid. Ledger-key normalization must
	// also perform NO I/O: ordinary absolute <root>/<rel> and relative <rel>
	// forms should converge lexically, while physical symlink aliases may
	// conservatively miss and force another Read.
	RecordRead(path string, version FileVersion)

	// RecordedVersion returns the version previously recorded for path via
	// RecordRead, performing NO I/O. ok is false if path was never recorded or
	// the live Workspace/ledger was rebuilt. It is the I/O-free
	// read-before-mutate lookup: the agent-facing Edit/Write tools call it to
	// assert the file was read this session; they then separately ReadVersion
	// for the CURRENT version and compare, so a file that changed since the
	// recorded read is caught by the version comparison, not by this lookup.
	RecordedVersion(path string) (version FileVersion, ok bool)
}

Workspace is the session-scoped seam every Tool executes against. It scopes all paths to a single session root (rejecting escapes such as "../"), exposes the read/search operations the 7 core tools need, and carries the per-session read-ledger + the explicit, unambiguous mutation operations the built-in Edit/Write tools enforce their invariants through (ADR 0208).

All paths are relative to the session root unless documented otherwise; adapters must reject any path that resolves outside the root.

VERSION PROTOCOL (ADR 0208). The Workspace capability exposes only the explicit create-only / conditional-replace-by-version pair, so a tool mutation can never silently clobber a concurrent change:

  • ReadVersion returns the content AND the authoritative FileVersion the adapter currently holds for path. The built-in Read tool records that version via RecordRead (a pure in-memory store, NO I/O) so a later Edit/Write can assert read-before-mutate-and-unchanged.
  • Existing-file Write and Edit: require a recorded version, ReadVersion again to get the CURRENT version, compare the recorded version with the current version (unchanged-since), and finish with ReplaceFile against the CURRENT version — the conditional CAS that survives a change that lands between the Edit's own ReadVersion and its ReplaceFile.
  • New-file Write: CreateFile (atomic create-only; fails if the path already exists, so it never silently clobbers).

ADAPTER ATOMICITY CONTRACT. CreateFile and the compare+mutation in ReplaceFile are atomic with respect to concurrent calls through the same live Workspace/backend handle: a conditional replace sees either the pre- or the post-mutation version, never a torn middle. osfs provides the stronger guarantee across Workspace instances in this process using fixed canonical-path lock stripes. Other adapters need not globally serialize independent Workspace instances unless their backend contract says so. A NON-COOPERATING POSIX writer (a shell command, an external editor) that bypasses the Workspace seam can still race a conditional replace — this is honest best-effort same-process CAS, NOT kernel-level locking; a future remote transport will provide true backend CAS (ADR 0208, remote transport deferred).

type WorkspaceReader

type WorkspaceReader interface {
	// Root returns the absolute session root all paths are scoped to.
	Root() string
	// Read returns the contents of the file at the session-relative path.
	Read(ctx context.Context, path string) ([]byte, error)
	// Stat returns metadata for the file at the session-relative path.
	Stat(ctx context.Context, path string) (FileInfo, error)
}

WorkspaceReader is the READ-ONLY subset of Workspace: a rooted, path-scoped reader that can fetch a file and stat it, without any mutate capability. It is the narrow seam non-tool consumers take when they only need to LOOK at the workspace — e.g. the permission-config resolver (issue #13), which reads `.mecatl/settings.yaml` and stats it to revalidate its cache, but must never write. Passing a WorkspaceReader (not a full Workspace) to those consumers makes the read-only contract a compile-time guarantee.

It carries the PLAIN (non-versioned) Read/Stat: non-agent consumers that only inspect the tree (permission config, prompt discovery, the agent-def/skill sources) never participate in the read-ledger / conditional-mutation protocol (ADR 0208) and do not need a FileVersion. The agent-facing built-in Read/Edit/Write tools use the version-bearing ReadVersion + CreateFile/ ReplaceFile on the full Workspace, NOT this plain Read.

Workspace embeds it, so any *Workspace is usable where a WorkspaceReader is expected. Paths are session-relative and adapters reject escapes.

Jump to

Keyboard shortcuts

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