osfs

package
v0.0.35 Latest Latest
Warning

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

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

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

Constants

This section is empty.

Variables

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

func Canonicalize(base, path string) (string, error)

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

func LocalizeInRoot(path string) (string, bool)

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

func ResolveRoot(path string) (string, error)

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

func (f *FileSystem) Glob(ctx context.Context, pattern string) ([]string, error)

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

func (f *FileSystem) Read(_ context.Context, path string) ([]byte, error)

func (*FileSystem) Root

func (f *FileSystem) Root() string

Root returns the absolute workspace root.

func (*FileSystem) Stat

func (f *FileSystem) Stat(_ context.Context, path string) (tool.FileInfo, error)

Stat returns metadata for the file at the session-relative path.

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

func NewWorkspace(root string, opts ...Option) (*Workspace, error)

NewWorkspace returns a content Workspace rooted at the given directory.

func (*Workspace) AuthorityResourcePath

func (w *Workspace) AuthorityResourcePath(path string) (target, workspace string, err error)

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

func (w *Workspace) Glob(ctx context.Context, pattern string) ([]string, error)

Glob returns session-relative paths matching the shell-style pattern.

func (*Workspace) Grep

func (w *Workspace) Grep(ctx context.Context, pattern, pathGlob string) ([]tool.GrepMatch, error)

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

func (w *Workspace) Read(ctx context.Context, path string) ([]byte, error)

Read returns the contents of the file at the session-relative path.

func (*Workspace) ReadDir added in v0.0.27

func (w *Workspace) ReadDir(ctx context.Context, path string) ([]tool.FileInfo, error)

ReadDir returns the immediate children of a confined physical directory.

func (*Workspace) ReadVersion

func (w *Workspace) ReadVersion(ctx context.Context, path string) ([]byte, tool.FileVersion, error)

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

func (w *Workspace) RelaxedAuthorityResourcePath(path string) (target, workspace string, err error)

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

func (w *Workspace) Remove(ctx context.Context, path string) error

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

func (w *Workspace) Rename(ctx context.Context, oldPath, newPath string) error

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

func (*Workspace) Root

func (w *Workspace) Root() string

Root returns the absolute session root all paths are scoped to.

func (*Workspace) Stat

func (w *Workspace) Stat(ctx context.Context, path string) (tool.FileInfo, error)

Stat returns metadata for the file at the session-relative path.

func (*Workspace) Write

func (w *Workspace) Write(ctx context.Context, path string, data []byte) error

Write replaces the contents of the file at the session-relative path, creating parents as needed. This adapter-public bootstrap operation is deliberately not part of tool.Workspace; tools use CreateFile/ReplaceFile.

Jump to

Keyboard shortcuts

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