Documentation
¶
Overview ¶
Package fs exposes LLM-callable filesystem tools (read, write, edit, apply_patch, glob, grep) on top of minimal per-operation ports. Local, sandbox, and remote backends implement only the capabilities they provide; the tools themselves are thin adapters that marshal LLM JSON into port calls and back.
The local backend accepts one already-open os.Root with an absolute Name. Hosts retain that authority for additional inspection or policy instead of resolving its pathname again. NewLocalExecutor derives its own handle from the supplied root; the host and executor each close the handle they own. Sharing a directory does not synchronize separate host and executor operations.
**Text files only.** Backend implementations MUST reject files that look binary (NUL byte in the first 8 KiB is a good default heuristic) and reject Write content that contains NUL bytes. Use the shell tool if you need to manipulate binary data.
**Tools stay thin.** All content processing — line windowing, binary detection, exact replacement, and complete writes — lives in the backend, not the tool. The tool's job is JSON in, JSON out.
ApplyPatchTool.MutationPaths exposes prospective patch endpoints without I/O, using LocalExecutor's parser and supported-operation rules. Hosts may discover this optional method through core/tool.Capability for approval or locking. Filesystem authority and hunk applicability are checked during execution; ApplyPatchResponse, including a partial response on error, reports actual effects. Mutation tools preserve ordinary backend errors and acknowledged observations through core/tool.CallError. Such evidence is available for evaluation and reconciliation, not a completed result to feed back to the model. Backends establish definite failure or permission refusal explicitly with core/tool.Failure.
Why Glob and Grep have dedicated ports (instead of "walk + match" in the tool layer): a remote backend cannot afford to ship every file across the wire to pattern-match on the agent side. Pushing bulk queries into their ports keeps remote implementations one round-trip per call.
Index ¶
- Variables
- func ReadLineNumber(err error) int
- type ApplyPatchRequest
- type ApplyPatchResponse
- type ApplyPatchTool
- type EditRequest
- type EditResponse
- type EditTool
- type Editor
- type GlobRequest
- type GlobResponse
- type GlobTool
- type Globber
- type GrepFileCount
- type GrepInput
- type GrepLine
- type GrepLineKind
- type GrepOutputMode
- type GrepRequest
- type GrepResponse
- type GrepTool
- type Grepper
- type LocalExecutor
- func (l *LocalExecutor) ApplyPatch(ctx context.Context, in ApplyPatchRequest) (_ ApplyPatchResponse, err error)
- func (l *LocalExecutor) Close() error
- func (l *LocalExecutor) Edit(ctx context.Context, in EditRequest) (_ EditResponse, err error)
- func (l *LocalExecutor) Glob(ctx context.Context, in GlobRequest) (_ GlobResponse, err error)
- func (l *LocalExecutor) Grep(ctx context.Context, in GrepInput) (_ GrepResponse, err error)
- func (l *LocalExecutor) Read(ctx context.Context, in ReadInput) (_ ReadOutput, err error)
- func (l *LocalExecutor) Write(ctx context.Context, in WriteRequest) (_ WriteResponse, err error)
- type PatchApplier
- type PatchFileResponse
- type ReadInput
- type ReadOutput
- type ReadRequest
- type ReadResponse
- type ReadTool
- type Reader
- type WriteRequest
- type WriteResponse
- type WriteTool
- type Writer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrNilExecutor = errors.New("fs: executor must not be nil") ErrInvalidRoot = errors.New("fs: executor root is invalid") ErrEmptyPath = errors.New("fs: path must not be empty") ErrInvalidInput = errors.New("fs: operation input is invalid") ErrPathOutsideRoot = errors.New("fs: path is outside the executor root") ErrEmptyPattern = errors.New("fs: pattern must not be empty") // ErrMutationRejected proves that a write, edit, or patch stopped before // changing any file or directory. Backends must not return it after commit // begins, even when the only acknowledged changes are partial. ErrMutationRejected = errors.New("fs: mutation rejected without changes") ErrBinaryFile = errors.New("fs: file appears to be binary; only text files are supported") // ErrFileTooLarge reports that the operation returned no partial file. ErrFileTooLarge = errors.New("fs: file exceeds the operation input limit") // ErrLineTooLarge preserves the one-based offending line through // ReadLineNumber. ErrLineTooLarge = errors.New("fs: line exceeds the operation line limit") )
Functions ¶
func ReadLineNumber ¶
ReadLineNumber returns the one-based line attached to an ErrLineTooLarge failure, or zero when the error is not line-specific.
Types ¶
type ApplyPatchRequest ¶
type ApplyPatchRequest struct {
Patch string `` /* 187-byte string literal not displayed */
}
ApplyPatchRequest accepts Git-compatible create, modify, delete, and rename patches.
type ApplyPatchResponse ¶
type ApplyPatchResponse struct {
Files []PatchFileResponse `json:"files"`
Hunks int `json:"hunks"`
}
ApplyPatchResponse reports acknowledged file mutations even when ApplyPatch returns an error. Files are listed in commit order. An interrupted move is reported as a created destination until the source has actually been removed.
type ApplyPatchTool ¶
type ApplyPatchTool struct {
// contains filtered or unexported fields
}
ApplyPatchTool preserves both complete and partial PatchApplier outcomes.
func NewApplyPatchTool ¶
func NewApplyPatchTool(executor PatchApplier) (*ApplyPatchTool, error)
func (*ApplyPatchTool) Call ¶
func (a *ApplyPatchTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*ApplyPatchTool) Definition ¶
func (a *ApplyPatchTool) Definition() chat.ToolDefinition
func (*ApplyPatchTool) MutationPaths ¶ added in v0.35.0
func (a *ApplyPatchTool) MutationPaths(arguments []byte) ([]string, error)
MutationPaths returns the sorted, unique file endpoints named by an invocation. A rename includes both its source and destination; /dev/null is never a target. Paths are lexically cleaned with Git's a/ and b/ prefixes removed. Relative paths remain relative to the backend root and absolute paths remain absolute.
It uses the same patch parser and operation validation as LocalExecutor and performs no I/O. A successful query does not establish filesystem authority, target identity, hunk applicability, or execution success. Hosts resolve these prospective paths for policy and locking; ApplyPatchResponse alone acknowledges actual effects. Invalid arguments or unsupported patches return no paths.
Example ¶
package main
import (
"fmt"
"os"
toolfs "github.com/Tangerg/scope/tools/fs"
)
func main() {
path, err := os.Getwd()
if err != nil {
panic(err)
}
root, err := os.OpenRoot(path)
if err != nil {
panic(err)
}
defer root.Close()
executor, err := toolfs.NewLocalExecutor(root)
if err != nil {
panic(err)
}
defer executor.Close()
patch, err := toolfs.NewApplyPatchTool(executor)
if err != nil {
panic(err)
}
// Querying the prospective endpoints neither reads nor changes these files.
paths, err := patch.MutationPaths([]byte(`{"patch":"diff --git a/old.txt b/new.txt\nsimilarity index 100%\nrename from old.txt\nrename to new.txt\n"}`))
if err != nil {
panic(err)
}
fmt.Println(paths)
}
Output: [new.txt old.txt]
func (*ApplyPatchTool) Unwrap ¶ added in v0.19.0
func (a *ApplyPatchTool) Unwrap() toolcontract.Tool
type EditRequest ¶
type EditRequest struct {
Path string `json:"path" jsonschema:"minLength=1" jsonschema_description:"File path, absolute or relative to the workspace root."`
OldString string `` /* 292-byte string literal not displayed */
NewString string `` /* 188-byte string literal not displayed */
ReplaceAll bool `` /* 141-byte string literal not displayed */
}
EditRequest drives one atomic exact-text replacement in the executor. Whitespace is significant, including indentation and string literal content.
type EditResponse ¶
type EditResponse struct {
Replacements int `json:"replacements"`
}
type EditTool ¶
type EditTool struct {
// contains filtered or unexported fields
}
EditTool delegates validation and atomic replacement to its Editor.
func NewEditTool ¶
func (*EditTool) Call ¶
func (e *EditTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*EditTool) Definition ¶
func (e *EditTool) Definition() chat.ToolDefinition
func (*EditTool) Unwrap ¶ added in v0.19.0
func (e *EditTool) Unwrap() toolcontract.Tool
type Editor ¶
type Editor interface {
Edit(ctx context.Context, request EditRequest) (EditResponse, error)
}
Editor keeps read-modify-write atomic inside the filesystem authority owner. ErrMutationRejected reports a definite rejection with no mutation. Other errors do not establish the outcome and must not be treated as safe to retry. Responses retain acknowledged replacements even on error.
type GlobRequest ¶
type GlobRequest struct {
Pattern string `json:"pattern" jsonschema:"minLength=1" jsonschema_description:"Doublestar path pattern, such as **/*.go or src/**/*.ts."`
Path string `json:"path,omitempty" jsonschema_description:"Directory to search under. Defaults to the workspace root."`
IgnoreCase bool `json:"ignore_case,omitzero" jsonschema_description:"Match path components case-insensitively. Default false."`
MaxResults int `` /* 153-byte string literal not displayed */
}
GlobRequest narrows the executor's immutable authority to a relative subtree; Path can never replace or broaden that root.
type GlobResponse ¶
type GlobTool ¶
type GlobTool struct {
// contains filtered or unexported fields
}
func NewGlobTool ¶
func (*GlobTool) Call ¶
func (g *GlobTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*GlobTool) ConcurrencyPolicy ¶ added in v0.19.0
func (g *GlobTool) ConcurrencyPolicy() func(toolcontract.Invocation) (string, bool)
func (*GlobTool) Definition ¶
func (g *GlobTool) Definition() chat.ToolDefinition
func (*GlobTool) Unwrap ¶ added in v0.19.0
func (g *GlobTool) Unwrap() toolcontract.Tool
type Globber ¶
type Globber interface {
Glob(ctx context.Context, request GlobRequest) (GlobResponse, error)
}
Globber lets remote backends search paths without exposing directory walking as many tool calls. Like Reader, it must support concurrent backend calls.
type GrepFileCount ¶
type GrepInput ¶
type GrepInput struct {
Pattern string // regex
Path string // file or directory below the executor's authority root
Glob string // optional file filter ("*.go", "**/*.ts", ...)
FileType string // rg-style ("go", "ts", "rust", ...). Backend decides mapping.
IgnoreCase bool
Multiline bool
// Context is the symmetric "lines before AND after" shortcut.
// BeforeContext / AfterContext override per-side when non-zero.
Context int
BeforeContext int
AfterContext int
// OutputMode picks the shape of GrepResponse. Its zero value resolves to
// [GrepOutputContent].
OutputMode GrepOutputMode
MaxResults int
}
type GrepLine ¶
type GrepLine struct {
Path string `json:"path"`
Line int `json:"line"` // 1-based
Text string `json:"text"`
Kind GrepLineKind `json:"kind"`
}
type GrepLineKind ¶
type GrepLineKind string
const ( GrepLineMatch GrepLineKind = "match" GrepLineContext GrepLineKind = "context" )
func (GrepLineKind) String ¶
func (g GrepLineKind) String() string
func (GrepLineKind) Valid ¶
func (g GrepLineKind) Valid() bool
type GrepOutputMode ¶
type GrepOutputMode string
const ( // GrepOutputContent returns structured matching and context lines. GrepOutputContent GrepOutputMode = "content" GrepOutputFilesWithMatches GrepOutputMode = "files_with_matches" GrepOutputCount GrepOutputMode = "count" )
func (GrepOutputMode) Normalize ¶ added in v0.24.0
func (g GrepOutputMode) Normalize() (GrepOutputMode, error)
func (GrepOutputMode) Valid ¶
func (g GrepOutputMode) Valid() bool
type GrepRequest ¶
type GrepRequest struct {
Pattern string `json:"pattern" jsonschema:"minLength=1" jsonschema_description:"Regular expression in ripgrep syntax."`
Path string `json:"path,omitempty" jsonschema_description:"File or directory to search. Defaults to the workspace root."`
FileGlob string `json:"file_glob,omitempty" jsonschema_description:"Optional file filter glob, such as **/*.go."`
FileType string `json:"file_type,omitempty" jsonschema_description:"Optional ripgrep file type, such as go, ts, or rust."`
IgnoreCase bool `json:"ignore_case,omitzero" jsonschema_description:"Case-insensitive search. Default false."`
Multiline bool `json:"multiline,omitzero" jsonschema_description:"Allow patterns to span line breaks. Default false. Requires ripgrep."`
BeforeContextLines int `` /* 167-byte string literal not displayed */
AfterContextLines int `` /* 165-byte string literal not displayed */
OutputMode GrepOutputMode `` /* 182-byte string literal not displayed */
MaxResults int `` /* 152-byte string literal not displayed */
}
GrepRequest patterns use ripgrep syntax. Matches stay within a single line unless Multiline is set.
type GrepResponse ¶
type GrepResponse struct {
Lines []GrepLine `json:"lines,omitempty"`
Files []string `json:"files,omitempty"`
Counts []GrepFileCount `json:"counts,omitempty"`
Truncated bool `json:"truncated,omitzero"`
}
Exactly one of Lines, Files, or Counts is populated according to OutputMode.
type GrepTool ¶
type GrepTool struct {
// contains filtered or unexported fields
}
func NewGrepTool ¶
func (*GrepTool) Call ¶
func (g *GrepTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*GrepTool) ConcurrencyPolicy ¶ added in v0.19.0
func (g *GrepTool) ConcurrencyPolicy() func(toolcontract.Invocation) (string, bool)
func (*GrepTool) Definition ¶
func (g *GrepTool) Definition() chat.ToolDefinition
func (*GrepTool) Unwrap ¶ added in v0.19.0
func (g *GrepTool) Unwrap() toolcontract.Tool
type Grepper ¶
type Grepper interface {
Grep(ctx context.Context, in GrepInput) (GrepResponse, error)
}
Grepper lets a backend own its content-search engine and filesystem boundary. Like Reader, it must support concurrent backend calls.
type LocalExecutor ¶
type LocalExecutor struct {
// contains filtered or unexported fields
}
LocalExecutor is the reference local filesystem backend. Its constructor grants one immutable directory-tree authority; operation inputs may narrow that authority but cannot replace or escape it.
- Glob uses the platform-neutral doublestar matcher and never follows directory symlinks while walking. Cancellation is checked between filesystem operations even when the pattern has no matches.
- Grep opens files through os.Root and feeds their contents to ripgrep. It skips hidden entries and symlinks during traversal, uses doublestar path filters, and does not consult ignore files. Missing rg returns ErrRipgrepUnavailable.
- Mutations serialize within this executor and pin parent directories. Directory aliases are supported; leaf symlinks are rejected.
- Read normalises CRLF→LF and strips UTF-8 BOM; Write and Edit restore both when the existing file uses them.
func NewLocalExecutor ¶
func NewLocalExecutor(root *os.Root) (*LocalExecutor, error)
NewLocalExecutor derives an independent directory handle from root, never reopening its pathname. Host policies can retain root and inspect the same directory that the executor uses, even after its pathname is replaced.
root must be open and have an absolute Name, which defines the namespace for absolute operation paths without consulting the process working directory. The caller retains ownership of root and must also Close the returned executor. Closing either handle does not revoke the other. Host policies own any synchronization they require across inspection and execution; sharing a root does not make separately issued filesystem operations atomic.
func (*LocalExecutor) ApplyPatch ¶
func (l *LocalExecutor) ApplyPatch(ctx context.Context, in ApplyPatchRequest) (_ ApplyPatchResponse, err error)
func (*LocalExecutor) Close ¶ added in v0.22.0
func (l *LocalExecutor) Close() error
Close releases the directory authority and prevents new operations. Operations that already acquired their own directory handle may finish independently.
func (*LocalExecutor) Edit ¶
func (l *LocalExecutor) Edit(ctx context.Context, in EditRequest) (_ EditResponse, err error)
func (*LocalExecutor) Glob ¶
func (l *LocalExecutor) Glob(ctx context.Context, in GlobRequest) (_ GlobResponse, err error)
func (*LocalExecutor) Grep ¶
func (l *LocalExecutor) Grep(ctx context.Context, in GrepInput) (_ GrepResponse, err error)
func (*LocalExecutor) Read ¶
func (l *LocalExecutor) Read(ctx context.Context, in ReadInput) (_ ReadOutput, err error)
Read does not lock — concurrent reads are fine and a slightly stale read while another goroutine writes is acceptable (atomic-rename in Write means the caller sees either the old file in full or the new file in full, never a torn write).
func (*LocalExecutor) Write ¶
func (l *LocalExecutor) Write(ctx context.Context, in WriteRequest) (_ WriteResponse, err error)
type PatchApplier ¶
type PatchApplier interface {
ApplyPatch(ctx context.Context, request ApplyPatchRequest) (ApplyPatchResponse, error)
}
PatchApplier validates a complete patch before mutation and reports every acknowledged file effect, including on error. A multi-file patch is not a filesystem transaction: commit failures can leave earlier changes applied. Implementations may create parent directories while committing files. Patch endpoints must follow ApplyPatchTool.MutationPaths so hosts can inspect the complete prospective write set before invoking the backend. A backend may return core/tool.Failure for an established unsuccessful outcome; that value owns the complete model-visible output. An ordinary error preserves uncertainty, and the Tool carries the response as core/tool.CallError evidence. ErrMutationRejected explicitly establishes rejection before any mutation.
type PatchFileResponse ¶
type PatchFileResponse struct {
Path string `json:"path"`
Hunks int `json:"hunks"`
Created bool `json:"created,omitzero"`
Deleted bool `json:"deleted,omitzero"`
// MovedFrom is set only after a move removes its source.
MovedFrom string `json:"moved_from,omitempty"`
}
PatchFileResponse preserves create, delete, and move identity separately. LocalExecutor reports paths relative to its authority root, even when patch headers use absolute paths.
type ReadInput ¶
type ReadInput struct {
Path string
Offset int // 0-based line offset; negative is clamped to 0
Limit int // 0 = read to end of file
MaxInputBytes int64 // 0 = executor default
MaxLineBytes int // 0 = executor default
MaxOutputBytes int // 0 = executor default
PartialLine bool // admit a UTF-8 prefix when the output cap splits a line
}
type ReadOutput ¶
type ReadRequest ¶
type ReadRequest struct {
Path string `json:"path" jsonschema:"minLength=1" jsonschema_description:"File path, absolute or relative to the workspace root."`
StartLine int `` /* 131-byte string literal not displayed */
MaxLines int `` /* 140-byte string literal not displayed */
}
StartLine is one-based to match editors, grep, and language servers.
type ReadResponse ¶
type ReadResponse struct {
Content string `json:"content"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
TotalLines int `json:"total_lines"`
Truncated bool `json:"truncated,omitzero"`
}
StartLine and EndLine are one-based and inclusive.
type ReadTool ¶
type ReadTool struct {
// contains filtered or unexported fields
}
ReadTool adapts a concurrent-safe Reader and declares parallel calls. The Reader owns synchronization across tool instances and Processes.
func NewReadTool ¶
Example ¶
package main
import (
"fmt"
"os"
toolfs "github.com/Tangerg/scope/tools/fs"
)
func main() {
path, err := os.Getwd()
if err != nil {
panic(err)
}
root, err := os.OpenRoot(path)
if err != nil {
panic(err)
}
defer root.Close()
executor, err := toolfs.NewLocalExecutor(root)
if err != nil {
panic(err)
}
defer executor.Close()
read, err := toolfs.NewReadTool(executor)
if err != nil {
panic(err)
}
definition := read.Definition()
fmt.Println(definition.Name)
}
Output: read
func (*ReadTool) Call ¶
func (r *ReadTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*ReadTool) ConcurrencyPolicy ¶ added in v0.19.0
func (r *ReadTool) ConcurrencyPolicy() func(toolcontract.Invocation) (string, bool)
func (*ReadTool) Definition ¶
func (r *ReadTool) Definition() chat.ToolDefinition
func (*ReadTool) Unwrap ¶ added in v0.19.0
func (r *ReadTool) Unwrap() toolcontract.Tool
type Reader ¶
type Reader interface {
Read(ctx context.Context, in ReadInput) (ReadOutput, error)
}
Reader is the read backend used by ReadTool. Implementations must support concurrent calls, including calls through other tools sharing the same backend. Private buffers, cursors, and caches remain the backend's concern. This promise permits ReadTool to advertise parallel reads; it is stronger than the absence of filesystem writes. LocalExecutor satisfies the contract.
type WriteRequest ¶
type WriteResponse ¶
type WriteResponse struct {
BytesWritten int `json:"bytes_written"`
}
type WriteTool ¶
type WriteTool struct {
// contains filtered or unexported fields
}
func NewWriteTool ¶
func (*WriteTool) Call ¶
func (w *WriteTool) Call(ctx context.Context, invocation toolcontract.Invocation) (chat.ToolOutput, error)
func (*WriteTool) Definition ¶
func (w *WriteTool) Definition() chat.ToolDefinition
func (*WriteTool) Unwrap ¶ added in v0.19.0
func (w *WriteTool) Unwrap() toolcontract.Tool
type Writer ¶
type Writer interface {
Write(ctx context.Context, request WriteRequest) (WriteResponse, error)
}
Writer is the narrow backend port consumed by WriteTool. On error the response retains acknowledged writes; an ordinary error does not establish whether a remote write committed or whether it is safe to repeat. ErrMutationRejected explicitly establishes that no mutation began.