Documentation
¶
Overview ¶
Package fstools implements the correctness- and security-critical filesystem tool bodies of the mecatl kit — Read, ListDir, Edit, Write, Copy, Move, Remove, Grep, Glob, and an OPTIONAL Shell — as tool.Tool values executing against an injected tool.Workspace (and, for Shell, an injected tool.CommandRunner). It travels WITH the importable engine module so an external consumer of engine/agent gets these tools — and their enforced invariants — by import, not by re-deriving them:
- Edit's three invariants: read-before-edit (+ unchanged-since), exact match, and uniqueness-unless-replace_all.
- Write's read-before-overwrite (Edit invariant #1 for existing files).
- Read/Grep/Glob output caps with clear truncation markers.
- Shell's partial-output-preserving timeout/cancel handling and the trailer that always survives the output cap.
These bodies depend only on engine/session + engine/tool (+ stdlib). They never touch the real OS: the filesystem tools go through the tool.Workspace and optional tool.WorkspaceNamespace seams, and Shell goes through the injected tool.CommandRunner — so a consumer picks the FileSystem/Workspace and shell backend. The reference in-memory Workspace is engine/adapter/memfs; the honest no-op is engine/adapter/nofs.
Opt-in / opt-out ¶
The catalog is composed, not fixed. A consumer may:
- take everything: register All() (Read/ListDir/Edit/Write/Copy/Move/Remove/ Grep/Glob) via Register, then add NewShellTool() only when a shell is configured;
- take a subset: register only the tool values it wants;
- swap a tool by name: register its own Tool under the same Spec().Name in place of one of these;
- ignore the package entirely and supply its own tools.
Shell is deliberately NOT in All(): it needs a tool.CommandRunner and command execution is optional. A deployment with no shell simply never constructs one.
Recoverable vs harness errors ¶
Each tool parses its session.ToolCall.Args (JSON), runs against the seam, and returns a session.ToolResult. Recoverable, model-addressable failures (a missing argument, a failed Edit invariant, a non-existent file) are returned as an *error* ToolResult via session.NewToolError so the model can read and recover from them; the Go error return is reserved for harness-level faults the model cannot act on.
Every tool carries a documentation-quality ToolSpec.Description (the model's onboarding manual, gauntlet #10) and a correct ReadOnly() value, which drives the agent loop's read-parallel / mutate-serial dispatch (gauntlet #4).
Output cap ¶
MaxOutputBytes (25,000 bytes) mirrors internal/adapter/toolkit.MaxOutputBytes EXACTLY — keep those two byte-identical. It is redefined locally (not imported) because engine/ is its own module and toolkit lives under the host repo's internal/adapter tree, which the engine module must not import. The related domain bound engine/session.MaxToolResultTextBytes (25 KiB = 25,600 bytes) is a slightly LARGER upper bound on any tool-result text block that these tool caps sit under — deliberately not byte-identical.
Index ¶
Constants ¶
const MaxOutputBytes = 25_000
MaxOutputBytes caps the byte length of a single tool's textual result. It is the fstools output cap; tools append a truncation marker (see truncate) when they trim to it. It mirrors internal/adapter/toolkit.MaxOutputBytes EXACTLY (both 25,000 bytes); the domain block bound engine/session.MaxToolResultTextBytes (25 KiB = 25,600 bytes) is a larger upper bound these caps sit under.
const ShellToolName = tool.ShellToolName
ShellToolName aliases tool.ShellToolName, the single authority for the name the Shell tool registers under (the permission evaluator special-cases the literal, so the constant lives in the port package where every implementation — this adapter's AND engine/agent's background-capable ShellTool — can import it). Callers probe the catalog for Shell enablement by referencing the constant rather than a local literal that could drift on a rename (see internal/adapter/server.Service.capabilities).
const TruncationMarker = "\n... [output truncated: exceeded 25000 bytes]"
TruncationMarker is the suffix truncate appends when it trims a body to the byte cap. It is exported so a caller that must reserve room for content AFTER a truncated body (Shell's timeout/cancel trailer, which must survive the cap) can account for the marker's length without hard-coding the literal.
Variables ¶
This section is empty.
Functions ¶
func All ¶
All returns the always-available filesystem tools as a fresh slice, in the canonical catalog order. Shell is NOT included: it requires a tool.CommandRunner and is optional — add it separately via NewShellTool when a runner is configured.
func NewShellTool ¶ added in v0.15.0
NewShellTool constructs the Shell tool. The runner is NOT captured here — it is read off the tool.Environment at Execute time (issue #462). The composition root registers the returned tool ONLY when a runner is available for the namespace; without one, the catalog has no Shell and the agent runs shell-less.
func Register ¶
Register adds the always-available filesystem tools (everything in All(), i.e. NOT Shell) to cat. It returns the first registration error (e.g. a name collision) encountered, or nil on success. To enable command execution, additionally register NewShellTool(), e.g. cat.MustRegister(fstools.NewShellTool()).
Types ¶
type CopyTool ¶ added in v0.15.0
type CopyTool struct{}
CopyTool copies one regular file to an absent destination.
func (CopyTool) Execute ¶ added in v0.15.0
func (CopyTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute copies a regular file without overwriting an existing destination.
type EditTool ¶
type EditTool struct{}
EditTool replaces an exact substring in a file, enforcing the three Edit invariants (read-before-edit, exact-match, uniqueness) against the Workspace read-ledger. It mutates state, so ReadOnly is false.
func (EditTool) Execute ¶
func (EditTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute enforces the three Edit invariants and writes the modified file via a conditional replace.
type GlobTool ¶
type GlobTool struct{}
GlobTool lists files matching a glob pattern. It does not mutate state, so ReadOnly is true.
func (GlobTool) Execute ¶
func (GlobTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute runs the glob and returns capped, sorted paths.
type GrepTool ¶
type GrepTool struct{}
GrepTool searches file contents for a regular expression. It does not mutate state, so ReadOnly is true.
func (GrepTool) Execute ¶
func (GrepTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute runs the search and returns capped, formatted matches.
type ListDirTool ¶ added in v0.15.0
type ListDirTool struct{}
ListDirTool lists the immediate children of one directory.
func (ListDirTool) Execute ¶ added in v0.15.0
func (ListDirTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute lists the immediate children of a directory.
func (ListDirTool) ReadOnly ¶ added in v0.15.0
func (ListDirTool) ReadOnly() bool
ReadOnly reports that ListDir does not mutate the workspace.
func (ListDirTool) Spec ¶ added in v0.15.0
func (ListDirTool) Spec() tool.ToolSpec
Spec returns the model-facing specification of the ListDir tool.
type MoveTool ¶ added in v0.15.0
type MoveTool struct{}
MoveTool moves a file or directory to an absent destination.
func (MoveTool) Execute ¶ added in v0.15.0
func (MoveTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute moves a path without overwriting an existing destination.
type ReadTool ¶
type ReadTool struct{}
ReadTool reads a file and returns its contents with 1-based line-number prefixes, recording the read in the Workspace ledger so Edit's read-before-edit invariant can later be satisfied.
func (ReadTool) Execute ¶
func (ReadTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute reads the file, returns it with line-number prefixes, and records the read in the Workspace ledger.
type RemoveTool ¶ added in v0.15.0
type RemoveTool struct{}
RemoveTool removes one file or empty physical directory non-recursively.
func (RemoveTool) Execute ¶ added in v0.15.0
func (RemoveTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute removes one path non-recursively.
func (RemoveTool) ReadOnly ¶ added in v0.15.0
func (RemoveTool) ReadOnly() bool
ReadOnly reports that Remove mutates the workspace.
func (RemoveTool) Spec ¶ added in v0.15.0
func (RemoveTool) Spec() tool.ToolSpec
Spec returns the model-facing specification of the Remove tool.
type ShellTool ¶ added in v0.15.0
type ShellTool struct{}
ShellTool runs a shell command via the CommandRunner bound to the tool.Environment it executes against (issue #462). It is statically classified as non-read-only: deciding whether a specific command is read-only is governance's job, not this tool's.
The runner is read off the Environment at Execute time, NOT captured at construction: a bound runner is part of the per-namespace Environment (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, never a stale shared parent base. A namespace with no shell (env.CommandRunner == nil) surfaces ErrNoShell honestly rather than aborting. The composition root decides whether to REGISTER a Shell tool at all based on runner availability; a shell-less catalog simply omits Shell.
Residual: this fixes the runner's working DIRECTORY, not Shell's trust model. Unlike path-scoped Edit/Write (confined by os.Root), Shell can still escape its cwd via absolute paths or `cd` — that is inherent to running a shell, the same as in the main session. The fix removes the ACCIDENTAL shared-base mutation (a fork branch's relative-path Shell landing in the parent base), which is what ParallelTool.ReadOnly() / the read-only-share / mutating-fork isolation needs.
func (ShellTool) Execute ¶ added in v0.15.0
func (ShellTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute runs the command, honoring an optional timeout, and returns combined output with the exit code. The runner is read off env; a shell-less namespace (nil runner) surfaces ErrNoShell.
type WriteTool ¶
type WriteTool struct{}
WriteTool writes a file. Creating a new file needs no prior read; overwriting an existing file requires a read-before-overwrite, mirroring Edit invariant #1. It mutates state, so ReadOnly is false.
func (WriteTool) Execute ¶
func (WriteTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute writes the file, enforcing read-before-overwrite on existing paths via the version protocol: a NEW file uses create-only; an EXISTING file requires a recorded version, re-reads the current version, and finishes with a conditional replace against that current version. No unconditional operation is used by the agent-facing Write tool.