sandbox

package
v0.64.7 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package sandbox wraps agent processes in an OS-level sandbox. It supports pluggable backends: "safehouse" (macOS Seatbelt, the original backend) and "nono" (cross-platform: Landlock+seccomp on Linux, Seatbelt on macOS).

The daemon resolves a merged sandbox policy and calls Wrap, which dispatches to the configured backend. A backend turns (command, args) plus the policy in WrapOpts into the actual command line to exec. Availability reports whether a backend can enforce on the current host, so the daemon can fail closed when the sandbox is enabled but cannot be enforced.

Index

Constants

View Source
const (
	BackendSafehouse = "safehouse"
	BackendNono      = "nono"
)

Backend identifiers used in config (`[sandbox] backend = "..."`).

View Source
const MinNonoVersion = "0.66.0"

MinNonoVersion is the minimum nono version graith requires. nono is pre-1.0 and its profile schema / CLI can shift between releases, so graith pins a floor and refuses to run below it rather than risk emitting a mis-shaped profile. Facts in this backend are pinned to nono v0.66.0.

Variables

This section is empty.

Functions

func Available

func Available() bool

Available reports whether the default (safehouse) backend can enforce. Retained for callers that predate pluggable backends.

func AvailableCommand

func AvailableCommand(command string) bool

AvailableCommand reports whether the safehouse backend can enforce with the given binary name. Retained for backward compatibility.

func BuildQueryProfile added in v0.64.0

func BuildQueryProfile(opts WrapOpts) (path string, warnings []string, err error)

BuildQueryProfile compiles a graith policy (opts) into a nono profile and writes it to a temp file for use as `nono why` query context. It reuses the same profile emitter as the run path, so `gr sandbox why` answers reflect the exact profile a real session would run under. The caller must remove the returned path; warnings mirror those the run path would emit.

func Wrap

func Wrap(command string, args []string, opts WrapOpts) (string, []string, error)

Wrap dispatches to the configured backend and returns the command+args to exec. An empty opts.Backend defaults to safehouse for backward compatibility.

Types

type Availability added in v0.64.0

type Availability struct {
	// CanEnforce is false when the backend cannot enforce at all (binary
	// missing, wrong OS, kernel too old, version below the pin). The daemon
	// must fail closed in that case.
	CanEnforce bool
	// Degraded is true when enforcement works but some controls are missing
	// (e.g. Landlock present but no network-filtering ABI). Filesystem
	// confinement still holds; the daemon runs but should surface the state.
	Degraded bool
	// Detail is a human-readable explanation for logs / doctor / errors.
	Detail string
}

Availability describes whether a backend can enforce a sandbox right now, and if so whether enforcement is degraded (some requested controls are unavailable but core filesystem confinement still holds).

func CheckAvailability added in v0.64.0

func CheckAvailability(backend, command string, req Requirements) (Availability, error)

CheckAvailability reports whether the named backend can enforce on this host, given the controls req requires (e.g. network filtering). command overrides the backend binary; empty uses the backend default.

type Backend added in v0.64.0

type Backend interface {
	// Name is the config value that selects this backend.
	Name() string
	// Availability reports whether this backend can enforce on this host now,
	// given the controls the policy requires. command overrides the backend
	// binary; empty uses the backend default.
	Availability(command string, req Requirements) Availability
	// Wrap returns the command and args to exec so the child runs sandboxed.
	Wrap(command string, args []string, opts WrapOpts) (string, []string, error)
}

Backend turns a resolved sandbox policy into a wrapped command.

type NetworkPolicy added in v0.64.0

type NetworkPolicy struct {
	// Block denies all outbound network (nono network.block = true).
	Block bool
	// AllowDomains is the L7 proxy allowlist (nono network.allow_domain).
	AllowDomains []string
}

NetworkPolicy is the resolved egress policy passed to a backend. It mirrors config.SandboxNetworkConfig. Nil ⇒ no restriction. It maps onto nono v0.66.0's profile network section (network.block / network.allow_domain).

func (*NetworkPolicy) IsSet added in v0.64.0

func (n *NetworkPolicy) IsSet() bool

IsSet reports whether any egress restriction is requested.

type Requirements added in v0.64.0

type Requirements struct {
	// Network is true when a network policy is requested. On Linux this
	// requires Landlock ABI v4; a host that only supports filesystem
	// enforcement must fail closed rather than pretend to block egress.
	Network bool
}

Requirements describes the enforcement controls a resolved policy needs, so Availability can fail closed when the host cannot enforce one of them. Today only network filtering raises the floor beyond base filesystem enforcement (it needs Landlock ABI v4 / kernel 6.7+ on Linux).

type WhyQuery added in v0.64.0

type WhyQuery struct {
	// Path + Op form a filesystem query.
	Path string
	Op   string // "read", "write", or "readwrite"

	// Host + Port form a network query.
	Host string
	Port int
}

WhyQuery describes a single allow/deny question to ask nono's oracle (`nono why`). Exactly one of the two query shapes is used:

  • a filesystem query: Path set, Op one of "read"/"write"/"readwrite".
  • a network query: Host set, Port optional (defaults to 443).

The query is evaluated against the graith-generated profile in WrapOpts, so the answer reflects graith's own sandbox policy, not nono's bare defaults.

func (WhyQuery) Validate added in v0.64.0

func (q WhyQuery) Validate() error

Validate reports whether the query is well-formed: exactly one of the filesystem (Path/Op) or network (Host) shapes, with a valid Op.

type WhyResult added in v0.64.0

type WhyResult struct {
	Status       string `json:"status"`
	Reason       string `json:"reason,omitempty"`
	Details      string `json:"details,omitempty"`
	Access       string `json:"access,omitempty"`
	GrantedPath  string `json:"granted_path,omitempty"`
	Source       string `json:"source,omitempty"`
	PolicySource string `json:"policy_source,omitempty"`
	SuggestFlag  string `json:"suggested_flag,omitempty"`
}

WhyResult is the decoded answer from `nono why --json`. nono's schema varies by query kind; the common fields are Status ("allowed"/"denied") and Reason. The remaining fields are populated best-effort from whichever keys nono emits so graith can render a useful explanation without pinning every variant.

func WhyForProfile added in v0.64.0

func WhyForProfile(command, profilePath string, q WhyQuery) (WhyResult, error)

WhyForProfile answers a WhyQuery by shelling out to `nono why` against a graith-generated profile. It is the introspection primitive behind `gr sandbox why`. command overrides the nono binary ("" uses the default).

nono emits its JSON decision on stdout and exits 0 for both allow and deny; a decode failure (or non-empty stderr with no JSON) is reported as an error so a nono CLI shift surfaces loudly rather than silently misreporting.

func (WhyResult) Allowed added in v0.64.0

func (r WhyResult) Allowed() bool

Allowed reports whether the queried access would be permitted.

func (WhyResult) Explanation added in v0.64.0

func (r WhyResult) Explanation() string

Explanation renders a one-line human summary of the decision, folding in whichever explanatory fields nono provided.

type WrapOpts

type WrapOpts struct {
	// Backend selects the sandbox backend. Empty is invalid at the daemon
	// layer (backend must be chosen explicitly); Wrap defaults it to safehouse
	// for backward compatibility of the low-level helper.
	Backend string

	WorktreeDir string
	ReadDirs    []string
	WriteDirs   []string
	// ReadFiles / WriteFiles grant single files rather than whole directories,
	// for paths that can't be a directory grant without over-sharing (notably
	// single files directly in $HOME, e.g. an agent's ~/.claude.json). Paths are
	// already ~/glob-expanded and absolute. ReadFiles is read-only (nono
	// filesystem.read_file); WriteFiles is read+write (nono filesystem.allow_file,
	// like WriteDirs → allow, not the write-only filesystem.write). The safehouse
	// backend folds them into its read-only / read-write dir lists.
	ReadFiles  []string
	WriteFiles []string
	Features   []string
	EnvKeys    []string

	// SignalMode maps to nono's security.signal_mode ("isolated",
	// "allow_same_sandbox", "allow_all"). Empty inherits nono's default. The
	// safehouse backend ignores it.
	SignalMode string

	// Network is the optional egress policy. Nil means no network restriction
	// (nono is allow-by-default). When set, the nono backend emits a
	// network.block / network.allow_domain section; the safehouse backend has
	// no network primitive and warns that it cannot enforce it.
	Network *NetworkPolicy

	// BackendCommand overrides the backend binary name/path. Empty means the
	// backend's own default ("safehouse" / "nono"). Formerly SafehouseCommand.
	BackendCommand string

	// ProfilePath, for the nono backend, is where the generated per-session
	// profile is written. The daemon points it under RuntimeDir so it is
	// readable inside the sandbox and survives for the process lifetime and
	// resume. Empty means the backend writes to an os.CreateTemp file.
	ProfilePath string
}

WrapOpts is the resolved, expanded sandbox policy for a single process. Paths are already `~`- and glob-expanded and made absolute by the daemon before they reach a backend.

Jump to

Keyboard shortcuts

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