workspace

package
v0.3.0 Latest Latest
Warning

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

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

Documentation

Overview

Package workspace confines all filesystem access to one root directory, so a tool or agent that reads and writes files cannot escape its sandbox through path traversal or a symlink.

Index

Constants

View Source
const DefaultMaxReadBytes int64 = 10 << 20

DefaultMaxReadBytes is the read bound a Workspace uses when its caller sets no MaxReadBytes. The bound fails closed: an unset field yields a bounded workspace, not an unbounded one.

View Source
const Unbounded int64 = -1

Unbounded removes the read bound. Set it on Options.MaxReadBytes for a whole Workspace, or pass it to ReadFileLimit for one call.

Variables

View Source
var ErrBlankRoot = errors.New("workspace: Root is blank")

ErrBlankRoot reports that Options.Root is blank after TrimSpace.

View Source
var ErrEscape = errors.New("workspace: path escapes root")

ErrEscape reports that a path resolves outside a Workspace's root, through traversal or a symlink.

View Source
var ErrInvalidLimit = errors.New("workspace: invalid read limit")

ErrInvalidLimit reports a read bound that is neither Unbounded, nor zero, nor a positive value at or under maxReadLimit.

View Source
var ErrSecretPath = errors.New("workspace: path is a secret path")

ErrSecretPath reports that Options.Deny refuses a path. Two rules return it. The name check matches the cleaned root-relative path against the matcher. The symlink walk refuses any path whose components hold a symlink, because a permitted name can otherwise alias a denied file; that refusal names "symlink component" in its text. Four limits apply. The walk is check-then-use: a concurrent writer inside the root can swap a component between the check and the open, and os.Root still confines the target to the root, so the limit is on secrecy and not on confinement. A hard link to a denied file is not detected, because a hard link carries no distinguishing mode bit. A case-insensitive or Unicode-normalizing filesystem opens a denied file under a spelling the byte-exact matcher permits. The matcher is a name policy and not a content policy, so a permitted name holding a secret is not denied.

View Source
var ErrTooLarge = errors.New("workspace: file exceeds read limit")

ErrTooLarge reports that a file is longer than the read's effective bound. It wraps no filesystem error, because it is this package's own policy refusal.

Functions

This section is empty.

Types

type Options

type Options struct {
	// Root is the directory the Workspace confines access to.
	Root string
	// MaxReadBytes bounds one read. Zero selects DefaultMaxReadBytes
	// and Unbounded removes the bound. See Validate.
	MaxReadBytes int64
	// Deny refuses a path it matches, and refuses any path holding a
	// symlink component. A nil Deny denies nothing. See ErrSecretPath.
	Deny *secretpath.Matcher
}

Options configures one Workspace at open time. See OpenWith.

func (Options) Validate

func (o Options) Validate() error

Validate reports whether o names a usable Workspace. Root must not be blank; a blank Root returns ErrBlankRoot. MaxReadBytes must be Unbounded, zero, or a positive value at or under maxReadLimit. Deny may be nil, which denies nothing.

type Workspace

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

Workspace confines filesystem access to one resolved root directory. It holds an open os.Root, so a Workspace owns a file descriptor and needs Close.

func Open

func Open(root string) (*Workspace, error)

Open resolves root to an absolute, symlink-free real path, opens it with os.OpenRoot, and returns a Workspace bound to the open root. root must exist and be a directory. Open is OpenWith(Options{Root: root}), so its reads carry DefaultMaxReadBytes. Close the result.

func OpenWith

func OpenWith(opts Options) (*Workspace, error)

OpenWith validates opts and opens a Workspace on opts.Root, the same way Open does, under the read bound opts names. It returns opts.Validate's error unchanged. Close the result.

func (*Workspace) Close

func (w *Workspace) Close() error

Close closes the Workspace's open root. Close is idempotent, matching os.Root.Close. Every method returns an error matching fs.ErrClosed after Close. Close on a nil or zero-value Workspace returns nil, so a deferred Close before an error check is safe.

func (*Workspace) List

func (w *Workspace) List(path string) ([]os.DirEntry, error)

List reads the directory at path, relative to the Workspace's root, through the open root. It returns the entries sorted by filename, matching os.ReadDir; (*os.File).ReadDir returns raw directory order.

func (*Workspace) ReadFile

func (w *Workspace) ReadFile(path string) ([]byte, error)

ReadFile reads the file at path, relative to the Workspace's root, under the Workspace's own read bound. It is ReadFileLimit(path, 0) and adds no rule of its own.

func (*Workspace) ReadFileLimit

func (w *Workspace) ReadFileLimit(path string, limit int64) ([]byte, error)

ReadFileLimit reads the file at path, relative to the Workspace's root, under a per-call bound. A zero limit uses the Workspace's MaxReadBytes, a positive limit replaces it, up or down, and Unbounded removes it for this call only. Any other value returns ErrInvalidLimit and opens no file. A file longer than the effective bound returns ErrTooLarge and no bytes. A read of a directory returns the raw filesystem error.

func (*Workspace) Root

func (w *Workspace) Root() string

Root returns the Workspace's resolved absolute root path.

func (*Workspace) Stat

func (w *Workspace) Stat(path string) (os.FileInfo, error)

Stat reports file info for path, relative to the Workspace's root, through the open root.

func (*Workspace) WriteFile

func (w *Workspace) WriteFile(path string, data []byte) error

WriteFile writes data to the file at path, relative to the Workspace's root, through the open root, creating any missing parent directory under the root. It creates a new file with mode 0o600 and a new directory with mode 0o700, and it leaves an existing file's or directory's mode alone. Both syscalls run through classify, because an escaping intermediate component is refused by the directory creation before the write runs.

Jump to

Keyboard shortcuts

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