sandbox

package
v0.224.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

sandbox

What - the sandbox bounded context: the policy that decides which paths the file tools may read and write, which bash commands run without a prompt, and how a denied path turns into a grant the user approves. Why - a security decision needs one chokepoint. Keeping it out of config leaves that package as schema and path constants, and keeping runtime grants apart from the configured policy keeps the system prompt byte-stable so the provider prompt cache stays warm. How - domain/ holds the policy value objects (Allowed with an Access, Denied with a Violation), the denial contract (DeniedError, ParseDenial) and the SandboxAccess approval name. The root package holds the process-wide Granted set and GrantFor, and applies the filesystem section of sandbox.yaml plus the carve-outs (skills, plugins, runtime dirs, memory, Go library dirs) through ValidateRead and ValidateWrite, and resolves the per-mode bash allow-list through IsBashCommandAllowed. infrastructure/ persists an "always" grant into ~/.infer/sandbox.yaml, the only policy file.

How it plugs in

  • Read, Tree, Grep, Wait, ImageDecode, TextToSpeech and the file writer call ValidateRead. Write, Edit, MultiEdit and Delete call ValidateWrite, which also rejects custom-tool directories and the read-only Go library carve-out.
  • Bash, Wait, the approval policy and command hooks call IsBashCommandAllowed and surface BashCommandRejectionHint when a command is denied. The system prompt lists BashAllowedCommands.
  • Decision order: denied entries first (a blocking one fails whatever its position, otherwise the first approval entry asks), then the built-in carve-outs, then the first matching allowed entry, then the user is asked. An empty allowed list is no boundary. A grant only unlocks what approval could have, so a granted directory still respects denied.
  • The default policy denies .infer/ with on_violation: approval, so the config dirs, whose files can hold tokens, ask before every read and write. The carve-outs skip that entry, so skills, plans, plugins, memory and runtime output stay open.
  • sandbox.yaml loads over the defaults, so a list the file leaves out keeps its default, and a file that fails to parse or validate stops the CLI from starting rather than falling back to a wider policy.
  • The agent's tool loop recovers a DeniedError from the flattened tool result with ParseDenial, raises a synthetic SandboxAccess approval for GrantFor (the exact path when a denied entry matched, otherwise the directory around it, except that a handback inside a config dir grants the exact file), and on approval calls Granted.Add. An auto-accept answer also calls PersistGrant unless a denied entry matched, since denied wins over any allowed entry it could write. PersistGrant refuses grants inside the config dirs, puts a write grant first and a read grant last, so a read grant never shadows a write entry.
  • ValidateWrite refuses ~/.infer/sandbox.yaml whatever denied says, a project copy is never loaded, and infer config set cannot reach the policy keys, so the agent cannot widen its own sandbox through the tools it runs.
  • Concurrency: tools run in parallel goroutines, so Granted is the only mutable sandbox state and sits behind an RWMutex. List returns a copy, so no check holds the lock while it walks the filesystem. cfg.Tools.Sandbox is never written after load and is read without a lock. SaveSandbox replaces the file through a rename, so a worker that starts while another persists a grant never reads a torn policy.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AllowedDirectories

func AllowedDirectories(cfg *config.Config) []string

AllowedDirectories are the anchored allowed paths, in order, for callers that want somewhere sensible to look.

func BashAllowedCommands

func BashAllowedCommands(cfg *config.Config, mode agentdomain.AgentMode) []string

BashAllowedCommands returns the effective bash allow-list for mode: the shared mode.all baseline unioned with that mode's own bucket. This is the ONE place an AgentMode maps to a tools.bash.mode.<bucket> list: Plan -> plan, AutoAccept -> auto, Standard/AutoWithJudge/unknown -> standard, ReadOnly -> baseline only. The system prompt lists it so the agent knows what runs.

func BashCommandRejectionHint

func BashCommandRejectionHint(cfg *config.Config, command string) string

BashCommandRejectionHint explains why the guard rejected command, or a path outside the sandbox, so the model can correct course instead of retrying. It returns "" when the command is simply not in the allow-list. A nil config skips the sandbox reason.

func GrantFor

GrantFor is the entry worth asking the user for after a denial: the exact path when a denied entry matched or it sits in a config dir, otherwise the directory around it so the agent can keep working there without a prompt per file.

func IsAnchored

func IsAnchored(rulePath string) bool

func IsBashCommandAllowed

func IsBashCommandAllowed(cfg *config.Config, command string, mode agentdomain.AgentMode) bool

IsBashCommandAllowed reports whether command is auto-approved in the given agent mode (see BashAllowedCommands for the mode-to-bucket mapping). The model is a pure allow-list with no separate deny list: anything the effective list does not match is denied - in chat mode it falls through to user approval, in headless agent mode it is rejected with a reason (see BashCommandRejectionHint).

  • If the effective allow-list contains the sentinel ".*" the mode is UNRESTRICTED: any single command is allowed and the clean-command guard is skipped (full autonomy - this is what mode.auto / headless `infer headless` uses). Tighten mode.auto.allow to a curated list to re-enable the guard.
  • Otherwise the clean-command guard runs first and rejects, regardless of the list: command substitution ($(...), backticks, <()/>()), multi-command chains/pipelines (top-level &&, ||, |, ;, &, newline), a surviving file-write redirect (>, >>), a file-writing option (sort -o, tree -o, git --output, uniq's output operand), dangerous find actions (-exec/-delete/...), and printing/publishing an expanded $VAR (echo/printf/gh ... $SECRET, which would leak the value).
  • The single clean command is then matched WHOLE against each allow entry (anchored as \A(?:entry)\z), so a bare "gh" allows only "gh" - an entry must opt into arguments explicitly (e.g. "gh issue.*").
  • Finally every path the command names must pass the sandbox check the file tools use, so an allow-listed command on a path outside the sandbox asks.

It is the single source of truth consulted by the Bash tool, the approval policy, and agent auto-approval, so all three agree on exactly what runs without prompting.

func IsUnrestricted

func IsUnrestricted(allow []string) bool

IsUnrestricted reports whether the allow-list contains the "allow any command" sentinel, which makes the mode unrestricted (and skips the clean-command guard). ".*", "^.*$", ".+", and a few trivially-equivalent forms qualify.

func ValidateRead

func ValidateRead(cfg *config.Config, path string) error

ValidateRead checks that a path and the file it resolves to through symlinks may both be read. Checking the target stops a link inside the sandbox from reaching a file outside it.

func ValidateWrite

func ValidateWrite(cfg *config.Config, path string) error

ValidateWrite is ValidateRead for writes. The userspace policy files and the custom-tool directories are never writable, whatever the rules say.

Types

type Grants

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

Grants are the entries the user approved after a denial. They extend the configured policy for the rest of the process and never feed the system prompt, which must stay byte-stable within a session.

var Granted Grants

Granted holds the process-wide grants. ponytail: process-wide like the port registry, fold into an injected guard if one process ever runs multiple configs.

func (*Grants) Add

func (g *Grants) Add(entry sandboxdomain.Allowed)

Add grants entry for the rest of the process. Idempotent.

func (*Grants) List

func (g *Grants) List() []sandboxdomain.Allowed

List returns a copy of the granted entries.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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