Documentation
¶
Overview ¶
Package osfs implements tool.FileSystem over the real operating-system filesystem and a tool.Workspace that scopes every path under a single session root. The canonical path forms a Workspace method accepts are:
- session-RELATIVE paths (the usual form), interpreted relative to the root;
- ABSOLUTE paths that canonicalize INSIDE the workspace root (the same physical file a relative path would reach, addressed by its absolute alias) — accepted by all five FS tools (Read/Write/Stat and, via the Edit read-ledger, Edit/Glob/Grep's callers).
Every other path is rejected as a correctness invariant: an absolute path that resolves OUTSIDE the workspace root, any ".." traversal that climbs out, and a symlink that escapes (whether addressed relatively or absolutely) all fail with ErrPathEscape.
Absolute-path acceptance is canonicalize-then-reject: an absolute path is EvalSymlinks-resolved against the deepest EXISTING ancestor (so a not-yet- existing leaf being Written is still vetted through its real parent), and the resulting real path is compared against the (already EvalSymlinks-resolved at construction) workspace root. A symlink inside the workspace whose target resolves OUTSIDE the workspace is rejected at resolution time, BEFORE the path reaches *os.Root — defense-in-depth on top of *os.Root's own containment. On accept the path is reduced to its slash-separated root-relative form and flows through the SAME *os.Root as a relative path, so the os.Root symlink containment for in-root paths is preserved.
The Workspace also carries the per-session version ledger (RecordRead/RecordedVersion) and mints sha256 content versions from ReadVersion. Edit uses the recorded/current comparison plus final ReplaceFile to enforce read-before-edit-and-unchanged. The ledger normalizes keys via resolvePath, so a file read by absolute path and then edited by relative path (or vice versa) matches — the key is canonical, not the verbatim argument.
Index ¶
- Variables
- func Canonicalize(base, path string) (string, error)
- func LocalizeInRoot(path string) (string, bool)
- func NewCommandRunner(dir string) (tool.CommandRunner, error)
- func NewCommandRunnerShell(dir, shell string, opts ...CommandRunnerOption) (tool.CommandRunner, error)
- func ResolveRoot(path string) (string, error)
- type CommandRunner
- func (r *CommandRunner) BoundWorkspaceRoot() string
- func (r *CommandRunner) Run(ctx context.Context, command string) (tool.CommandResult, error)
- func (r *CommandRunner) RunStreaming(ctx context.Context, command string, out io.Writer) (int, error)
- func (r *CommandRunner) RunStreamingWithEnvironment(ctx context.Context, command string, overlay tool.CommandEnvironmentOverlay, ...) (int, error)
- func (r *CommandRunner) RunStreamingWithTemporaryScope(ctx context.Context, command string, scope tool.TemporaryScope, out io.Writer) (int, error)
- func (r *CommandRunner) RunWithEnvironment(ctx context.Context, command string, overlay tool.CommandEnvironmentOverlay) (tool.CommandResult, error)
- func (r *CommandRunner) RunWithTemporaryScope(ctx context.Context, command string, scope tool.TemporaryScope) (tool.CommandResult, error)
- func (r *CommandRunner) ShellPath() string
- type CommandRunnerOption
- type FileSystem
- type Option
- type Workspace
- func (w *Workspace) AuthorityResourcePath(path string) (target, workspace string, err error)
- func (w *Workspace) CopyFile(ctx context.Context, source, destination string) (tool.FileVersion, error)
- func (w *Workspace) CreateFile(ctx context.Context, path string, data []byte) (tool.FileVersion, error)
- func (w *Workspace) Glob(ctx context.Context, pattern string) ([]string, error)
- func (w *Workspace) Grep(ctx context.Context, pattern, pathGlob string) ([]tool.GrepMatch, error)
- func (w *Workspace) Read(ctx context.Context, path string) ([]byte, error)
- func (w *Workspace) ReadDir(ctx context.Context, path string) ([]tool.FileInfo, error)
- func (w *Workspace) ReadVersion(ctx context.Context, path string) ([]byte, tool.FileVersion, error)
- func (w *Workspace) RelaxedAuthorityResourcePath(path string) (target, workspace string, err error)
- func (w *Workspace) Remove(ctx context.Context, path string) error
- func (w *Workspace) Rename(ctx context.Context, oldPath, newPath string) error
- func (w *Workspace) ReplaceFile(ctx context.Context, path string, old tool.FileVersion, data []byte) (tool.FileVersion, error)
- func (w *Workspace) Root() string
- func (w *Workspace) Stat(ctx context.Context, path string) (tool.FileInfo, error)
- func (w *Workspace) Write(ctx context.Context, path string, data []byte) error
Constants ¶
This section is empty.
Variables ¶
var ErrPathEscape = errors.New("osfs: path escapes workspace root")
ErrPathEscape is returned when a session-relative path resolves outside the Workspace root.
Functions ¶
func Canonicalize ¶
Canonicalize resolves a path — absolute, or relative against base (an ALREADY-canonicalized root, as produced by ResolveRoot) — to its canonical absolute form WITHOUT opening an *os.Root and WITHOUT serving any content: the exact resolveInRoot/resolveRoot algorithm (deepest EXISTING ancestor + EvalSymlinks + unresolved tail re-appended). An unverifiable ancestor (a non-ErrNotExist stat error) fails safe with an ErrPathEscape error, matching resolveInRoot. The only I/O is the Lstat/EvalSymlinks ancestor resolution resolveInRoot itself performs. The path-escape-posture composition classifier consumes this with LocalizeInRoot so its in-root/escape verdict is single-sourced with the tool body (docs/acceptance/path-escape-posture.md Scenario 1) instead of reimplementing the algorithms.
func LocalizeInRoot ¶
LocalizeInRoot reports the lexical root-relative form of a session-RELATIVE path: the exact lexical computation the tool body and *os.Root perform on a relative operand (filepath.Clean — resolvePath cleans a relative path lexically and hands it to the os.Root, which refuses any ".." traversal that climbs out of the root). It performs NO filesystem I/O — the first containment gate is lexical — so this predicate is precisely the question "would the workspace root's os.Root refuse this relative path before any symlink check?". An absolute or slash-prefixed path is NOT a relative operand (resolvePath routes those to resolveInRoot) and reports not-in-root.
func NewCommandRunner ¶
func NewCommandRunner(dir string) (tool.CommandRunner, error)
NewCommandRunner returns a local tool.CommandRunner that executes commands via /bin/sh -c, rooted at dir as the working directory. dir is resolved to an absolute, symlink-evaluated path so the runner's cwd matches the Workspace root. Use this from the composition root only when a shell is desired; omit it (and the Shell tool) to run shell-less.
func NewCommandRunnerShell ¶
func NewCommandRunnerShell(dir, shell string, opts ...CommandRunnerOption) (tool.CommandRunner, error)
NewCommandRunnerShell is like NewCommandRunner but lets the caller pick the shell binary (e.g. "/bin/bash"). An empty shell is rejected: a shell-less deployment must omit the runner (and the Shell tool) entirely rather than construct a runner with no shell.
func ResolveRoot ¶
ResolveRoot exposes the EXACT path canonicalization the Workspace uses to confine Write/Edit (abs + EvalSymlinks, falling back to a cleaned abs path when the path does not yet exist). Callers that reason about whether a directory is inside or outside a workspace root (e.g. the SkillDraft quarantine trust-boundary check in cmd/mecated) MUST canonicalize through this so their comparison matches the enforcement layer — using filepath.Abs alone diverges on a symlinked workspace and would let a dir validation believes is "outside" actually resolve inside the os.Root.
Types ¶
type CommandRunner ¶
type CommandRunner struct {
// contains filtered or unexported fields
}
CommandRunner runs shell commands via /bin/sh -c with a fixed working directory (the session root). It is the local implementation of tool.CommandRunner; a Workspace no longer runs commands itself, so a shell-less deployment simply omits this runner.
func (*CommandRunner) BoundWorkspaceRoot ¶ added in v0.0.24
func (r *CommandRunner) BoundWorkspaceRoot() string
BoundWorkspaceRoot reports the immutable command namespace root. Placement binding uses it to prove that a returned runner and Workspace share one root.
func (*CommandRunner) Run ¶
func (r *CommandRunner) Run(ctx context.Context, command string) (tool.CommandResult, error)
Run runs command via /bin/sh -c, capturing (and truncating) stdout/stderr and the exit code. The working directory is the runner's BOUND root (issue #462): the runner is bound to a single namespace at construction, so the command's cwd always matches the workspace the tool executes against. Cancellation and timeout are governed by ctx; when ctx has no deadline a default timeout is applied. On cancel/timeout the WHOLE process group is SIGKILLed (POSIX; see procgroup.Configure), so a backgrounded grandchild (e.g. `make`'s compiler children) dies with the shell instead of being orphaned; WaitDelay stays the portable backstop. A non-zero exit is reported via the returned CommandResult.ExitCode, not as an error.
func (*CommandRunner) RunStreaming ¶
func (r *CommandRunner) RunStreaming(ctx context.Context, command string, out io.Writer) (int, error)
RunStreaming is the tool.CommandStreamer half of the runner: it runs command exactly as Run does (same shell resolution, bound-root cwd, default timeout, process-group kill, WaitDelay backstop, env) but streams stdout and stderr INTERLEAVED into out in the order the OS delivers them, instead of capturing them into the capped buffers. The CALLER owns bounding (e.g. a bounded tail ring for a background command's recent output); this path does NOT cap or retain the stream itself. The returned exitCode replaces CommandResult for this path: a non-zero exit is reported there, not as an error.
func (*CommandRunner) RunStreamingWithEnvironment ¶ added in v0.0.26
func (r *CommandRunner) RunStreamingWithEnvironment(ctx context.Context, command string, overlay tool.CommandEnvironmentOverlay, out io.Writer) (int, error)
RunStreamingWithEnvironment streams command output with overlay applied only to this invocation. It preserves the runner's bound root and does not retain the overlay.
func (*CommandRunner) RunStreamingWithTemporaryScope ¶ added in v0.0.26
func (r *CommandRunner) RunStreamingWithTemporaryScope(ctx context.Context, command string, scope tool.TemporaryScope, out io.Writer) (int, error)
RunStreamingWithTemporaryScope is RunStreaming with a trusted temporary scope selection. System scope never allocates a managed lease.
func (*CommandRunner) RunWithEnvironment ¶ added in v0.0.26
func (r *CommandRunner) RunWithEnvironment(ctx context.Context, command string, overlay tool.CommandEnvironmentOverlay) (tool.CommandResult, error)
RunWithEnvironment runs command with overlay applied only to this invocation. It preserves the runner's bound root and does not retain the overlay.
func (*CommandRunner) RunWithTemporaryScope ¶ added in v0.0.26
func (r *CommandRunner) RunWithTemporaryScope(ctx context.Context, command string, scope tool.TemporaryScope) (tool.CommandResult, error)
RunWithTemporaryScope selects the closed temporary-storage scope for this invocation. System scope never allocates a managed lease.
func (*CommandRunner) ShellPath ¶ added in v0.0.34
func (r *CommandRunner) ShellPath() string
ShellPath reports the configured shell path without resolving symlinks. The Shell tool uses its basename only for the bounded POSIX compatibility diagnostic; execution continues to use this runner's exact configured path.
type CommandRunnerOption ¶
type CommandRunnerOption func(*CommandRunner)
CommandRunnerOption configures a CommandRunner at construction.
func WithCommandEnvList ¶
func WithCommandEnvList(env []string) CommandRunnerOption
WithCommandEnvList sets the COMPLETE process environment ("KEY=VALUE" entries) used for every Run, REPLACING the inherited os.Environ() rather than augmenting it. This is how the composition root hardens the team-member shell against a shared `.git`: it computes a fully scrubbed-and-neutralised environment (via gitenv.Scrub — inherited GIT_* danger REMOVED, not just overridden) and hands the complete list here. Because the option REPLACES the environment, removing an inherited variable (e.g. GIT_EXTERNAL_DIFF) is possible — an append-only option could not. A nil/empty list leaves the runner unhardened (the main-session default, which inherits os.Environ() unchanged). osfs holds no git knowledge: it just runs with whatever complete environment it is given.
func WithCommandWaitDelay ¶
func WithCommandWaitDelay(d time.Duration) CommandRunnerOption
WithCommandWaitDelay overrides the runner's cmd.WaitDelay (default defaultCommandWaitDelay): the bound on how long Run waits for the output pipes inherited by grandchildren to close after the shell exits or the context is cancelled. Non-positive values are ignored (keep the default — a zero WaitDelay would restore the unbounded pipe wait). Primarily a test seam.
func WithManagedTemporaryWorkspace ¶ added in v0.0.26
func WithManagedTemporaryWorkspace(workspace *managedtemp.Workspace) CommandRunnerOption
WithManagedTemporaryWorkspace makes foreground Run calls allocate one private managed command lease from workspace. The overlay is constructed internally after the runner's already-scrubbed base environment; callers cannot supply lease paths through shell text or tool arguments.
func WithSystemTemporaryDirectory ¶ added in v0.0.26
func WithSystemTemporaryDirectory(dir string) CommandRunnerOption
WithSystemTemporaryDirectory sets the configured/inherited system temporary directory used for explicit system-scope Shell calls.
type FileSystem ¶
type FileSystem struct {
// contains filtered or unexported fields
}
FileSystem implements tool.FileSystem over the real OS filesystem, rooted at a session workspace directory. The canonical path forms its methods accept are session-relative paths and absolute paths that resolve inside the workspace root (reduced to their root-relative form via resolveInRoot).
Every file operation goes through an *os.Root opened on the workspace root, which refuses both lexical ".." escapes and symlink traversal that would leave the root. This closes the gap a purely lexical cleanPath left open: the model can create a symlink inside the workspace (via Shell `ln -s /etc/passwd evil`), and both resolveInRoot (for absolute addresses) and os.Root (for all in-root paths) refuse to follow it out of the root.
func NewFileSystem ¶
func NewFileSystem(root string, opts ...Option) (*FileSystem, error)
NewFileSystem returns a FileSystem rooted at the given directory. The root is resolved to an absolute, symlink-evaluated path, created if missing, then opened as an *os.Root so all subsequent operations are confined to it.
func (*FileSystem) Glob ¶
Glob returns session-relative paths matching the glob pattern, sorted. The pattern supports the "**" globstar (matching across directory separators recursively) in addition to the usual shell-style "*", "?", "[…]" and "{…}" metacharacters. The pattern is interpreted relative to the root; matches that resolve outside the root are discarded.
func (*FileSystem) Root ¶
func (f *FileSystem) Root() string
Root returns the absolute workspace root.
type Option ¶
type Option func(*fsOptions)
Option configures a FileSystem (and the Workspace composing it) at construction.
func WithRelaxedReads ¶
func WithRelaxedReads() Option
WithRelaxedReads lets Read and Stat — and ONLY Read and Stat — serve an ABSOLUTE path that canonicalizes OUTSIDE the workspace root. It is an EXPLICIT construction option, DEFAULT OFF: the zero-value workspace keeps the canonicalize-then-reject behaviour (ErrPathEscape). The composition layer enables it for the MAIN session's workspace only, at the yolo/auto operator postures where Shell already reads the same bytes (the honesty fix — docs/acceptance/path-escape-posture.md Scenario 2), and always pairs it with the root-aware wrapping permission policy that refuses pseudo-fs (/proc, /sys, /dev) before the tool body: this option exists for THAT pairing, and a relaxed workspace without the policy wrapper is a mis-wire.
Serving opens a FRESH *os.Root on the target's LEXICAL parent directory and serves the leaf through it — never a bare os.Open — so a symlink inside the target dir that escapes further is refused by that root's containment, exactly as the workspace root's own containment refuses an in-root escape. Write, Edit, Glob, and Grep stay workspace-confined regardless of this option (the relax is read-only).
func WithRelaxedWrites ¶
func WithRelaxedWrites() Option
WithRelaxedWrites lets Write — and, through it, the Edit tool's mutation path — serve an ABSOLUTE path that canonicalizes OUTSIDE the workspace root. It is an EXPLICIT construction option, DEFAULT OFF: the zero-value workspace keeps the ordinary canonicalize-then-reject behaviour (ErrPathEscape). The composition layer enables it for the MAIN session's workspace only, at the yolo/auto operator postures (never a child engine), and always pairs it with the root-aware wrapping permission policy that resolves a write escape Allow at yolo / Ask at auto and hard-denies pseudo-fs (/proc, /sys, /dev) before the tool body — docs/acceptance/ path-escape-posture.md Scenario 3. A relaxed workspace without the policy wrapper is a mis-wire: the workspace's job is only to SERVE the path the policy already authorized.
Serving opens a FRESH *os.Root on the target's LEXICAL parent directory (vetted by the same Canonicalize-based vetRelaxedParent containment check the relaxed read uses) and writes the leaf through it — never a bare os.WriteFile — so a symlinked component that escapes further is refused by that root's containment, exactly as the workspace root's own containment refuses an in-root escape (ADR-0047). Glob and Grep stay workspace-confined regardless of this option.
type Workspace ¶
type Workspace struct {
// contains filtered or unexported fields
}
Workspace is the session-scoped seam over the real OS filesystem. It composes a FileSystem, performs an in-Go recursive Grep, and carries the read-ledger plus the explicit create-only / conditional-replace mutation operations (ADR 0208). Command execution is NOT part of the Workspace: it lives behind the separate CommandRunner type (see NewCommandRunner) so the harness can run without any shell at all.
func NewWorkspace ¶
NewWorkspace returns a content Workspace rooted at the given directory.
func (*Workspace) AuthorityResourcePath ¶
AuthorityResourcePath resolves path to the physical target identity used for ordinary confined workspace access. A target outside the physical workspace root is rejected; relaxed serving must opt in through RelaxedAuthorityResourcePath.
func (*Workspace) CopyFile ¶ added in v0.0.27
func (w *Workspace) CopyFile(ctx context.Context, source, destination string) (tool.FileVersion, error)
CopyFile copies one regular file to a new destination. The destination create is exclusive and the operation is independent of the read ledger.
func (*Workspace) CreateFile ¶
func (w *Workspace) CreateFile(ctx context.Context, path string, data []byte) (tool.FileVersion, error)
CreateFile creates a NEW file at path with the given content, atomically. It fails (wrapping fs.ErrExist) if a file already exists. Parent directories are created as needed. It serializes aliases through process-wide physical-target lock striping and performs the create with O_CREATE|O_EXCL (ADR 0208 §5).
func (*Workspace) Grep ¶
Grep returns the matches of a regular expression across files selected by an optional path glob (relative to root), within an aggregate safety budget. Binary-looking files (those containing a NUL byte) are skipped. The search honors ctx cancellation.
func (*Workspace) ReadDir ¶ added in v0.0.27
ReadDir returns the immediate children of a confined physical directory.
func (*Workspace) ReadVersion ¶
ReadVersion returns the contents of the file at path AND the authoritative FileVersion (sha256 of the content). It reads through the same resolvePath/ resolveInRoot/relaxed-read path as the plain Read, so the content and the version are a consistent snapshot of the on-disk file.
func (*Workspace) RelaxedAuthorityResourcePath ¶
RelaxedAuthorityResourcePath resolves the physical target identity for the explicitly relaxed serving path. It is adapter-specific: escapeWorkspace uses it only after its ordinary escape policy has authorized the operation. The relaxed filesystem seam serves absolute operands only; relative traversal is never forwarded into the out-of-root carve-out.
func (*Workspace) Remove ¶ added in v0.0.27
Remove deletes one file or empty directory. It never recursively removes a directory and does not participate in the read ledger.
func (*Workspace) Rename ¶ added in v0.0.27
Rename moves a confined file or directory without intentionally replacing an existing destination. Cooperating workspace mutations are serialized; as with the existing local CAS contract, arbitrary external POSIX writers do not participate in these process-local locks.
func (*Workspace) ReplaceFile ¶
func (w *Workspace) ReplaceFile(ctx context.Context, path string, old tool.FileVersion, data []byte) (tool.FileVersion, error)
ReplaceFile conditionally replaces the contents of the file at path with data, only if the file's current authoritative version equals old. On a version mismatch it returns a *tool.VersionMismatchError; on a missing file it returns an error wrapping fs.ErrNotExist. It serializes against other same-path mutations through process-wide canonical-path lock striping, so the compare+write is atomic with respect to cooperating Workspace writers (ADR 0208 §5).