sandbox

package
v0.0.0-...-95517ba Latest Latest
Warning

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

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

Documentation

Overview

Package sandbox confines what Strument and the commands it runs may write.

The guarantee is deliberately narrow: **integrity, not confidentiality.** Writes cannot land outside the project and a few named places; reads and execution stay open across the whole filesystem. A mistaken or injected `rm -rf ~` becomes impossible; a determined exfiltration is no harder than it was. The threat model is mistakes and prompt injection with a human watching, not a misaligned agent working patiently over hundreds of turns.

Leaving reads open is what makes this affordable. Landlock's read rule also grants execution, so one rule over "/" covers every binary on the machine — /usr/bin, ~/.local/bin, ~/go/bin, ~/.cargo/bin — with nothing to enumerate and nothing to keep up to date. Confining reads instead is the choice that makes a sandbox break builds, and a sandbox that breaks builds gets removed.

See doc/security.md for the threat model and what the sandbox does not cover.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultWritable

func DefaultWritable(projectRoot, stateDir string, extra []string) []string

DefaultWritable is the set of places a session legitimately writes to.

Everything else on the filesystem is readable and executable and simply cannot be written. The list below is therefore the entire security decision, and each group is here for a reason that can be checked:

  • The project, because that is the work.
  • The state directory, because the transcript, the undo spill, the resume record and the lock live there, and a session that cannot write them fails at the end of a turn rather than the start.
  • The git directory when it is elsewhere, i.e. a worktree or a submodule, where .git is a file pointing outside the project.
  • A temporary directory, because build tools assume one exists.
  • Toolchain caches, because the first `go test` of a session writes one.

The cache group is where Strument deliberately parts company with Codex CLI and Claude Code, which grant only the working directory and a temp directory and leave users to discover the rest. That is defensible for a general assistant and wrong here: Strument's core loop is running the project's checks, and a sandbox whose first act is to break `go test` is a sandbox that gets switched off within the hour. The widening is real, and the thing it widens to is caches — which a model can only poison to sabotage itself.

Symlinks are not resolved here on purpose. Landlock anchors a rule to the inode a path resolves to, so a project root that is a symlink is handled already; and a symlink *inside* the project pointing outward stays unwritable, which is the correct answer rather than an oversight.

Types

type Availability

type Availability struct {
	// ABI is the Landlock ABI version the kernel supports, or 0 for none.
	ABI int
	// Err is why the probe failed, when it did.
	Err error
}

Availability is what a probe of the running kernel found.

func Probe

func Probe() Availability

Probe asks the kernel which Landlock ABI it supports.

Asked explicitly rather than inferred from a BestEffort() call, which is the library's documented footgun: on a kernel with no Landlock at all, a best-effort restriction succeeds and enforces nothing. A sandbox that silently enforces nothing is worse than none, because the user believes it is there. The version also decides which rules are worth requesting.

A modern kernel is not sufficient. Landlock must be compiled in and enabled, and it is absent on plenty of container and VM kernels — Claude Code's own development container runs 6.18 and returns ENOSYS.

func (Availability) Supported

func (a Availability) Supported() bool

Supported reports whether the kernel can enforce anything at all.

type Policy

type Policy struct {
	// Writable are the roots writes are permitted under, absolute and
	// symlink-resolved by the caller. A path that does not exist is skipped
	// rather than failing the policy: a project may have no state directory
	// yet, and a toolchain cache appears the first time the toolchain runs.
	Writable []string
}

Policy is what the sandbox permits. Reads and execution are unrestricted; this is only ever a list of places writes may land.

func (Policy) Apply

func (p Policy) Apply() error

Apply confines the calling process, and everything it later spawns, to the policy. It cannot be undone: Landlock is monotonic by design, which is what makes it safe to apply in a process that goes on to run untrusted commands.

The caller must have probed first. Apply refuses rather than silently enforcing nothing, because a sandbox believed to be present and absent is worse than one known to be absent.

func (Policy) Granted

func (p Policy) Granted() []string

Granted is the subset of Writable a rule can actually be anchored to.

Landlock resolves a path to an inode when the ruleset is installed, so a path that does not exist grants nothing: writeRule skips it, and the enforced policy is quietly narrower than the list it was built from. Skipping is the right behavior — a project need not have a state directory yet, and most machines have no ~/.m2 — but it is the wrong thing to *report*. A live run caught /sandbox listing a cache directory immediately above a build that was then refused permission to create it.

Membership is decided when the policy is applied and does not change afterwards. A directory created later in the session is still not writable, because there was nothing to anchor a rule to when the ruleset went in.

Jump to

Keyboard shortcuts

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