gated

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package gated wraps any sandbox.Sandbox with a confirmation gate on command execution — the trust layer that makes the agent usable on a REAL repo rather than only a throwaway eval fixture. It is a decorator, not a change to the loop: every effect the agent causes already flows through the sandbox.Sandbox interface (sandbox/sandbox.go), so gating `run` is just wrapping Exec. The loop and the tools never learn the gate exists.

Scope is deliberately Exec only. File writes route through WriteFile, which is already path-confined to the sandbox root AND reviewed as a diff before any commit (the cmd/agent -review flow), so gating them in-loop would add prompts without adding safety. Arbitrary shell — which on the local backend runs on the host (IsolationNone) — is the real escape surface, so that is what the gate guards.

A blocked command is NOT an error: it returns a non-zero Result the model sees as an observation and can adapt to (Principle 6 — tool failures are observations, not crashes), exactly like a command that exited non-zero on its own. The agent simply learns "that wasn't allowed" and tries another way.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Chained

func Chained(line string) bool

Chained reports whether a command line can run more than the one command its prefix suggests — the exported form of the guard DefaultPolicy applies, for callers building their OWN prefix allowlists (the driver's session "always allow" list): a chained line must never ride in on a trusted prefix.

func Display

func Display(cmd sandbox.Command) string

Display renders a Command as the human would read it. The run tool always shells `sh -c "<command>"`, so the meaningful line is the -c argument — show that, not the wrapper. Anything else is shown as Path + Args.

Types

type Approver

type Approver interface {
	Approve(ctx context.Context, req Request) (bool, error)
}

Approver decides an Ask verdict. Approve returns true to run the command. An error (e.g. no interactive terminal to prompt on) is treated by the gate as a denial — failing closed, never open.

type ApproverFunc

type ApproverFunc func(context.Context, Request) (bool, error)

ApproverFunc adapts a plain func to an Approver.

func (ApproverFunc) Approve

func (f ApproverFunc) Approve(ctx context.Context, r Request) (bool, error)

Approve calls f.

type Policy

type Policy func(sandbox.Command) Verdict

Policy classifies a command into a Verdict. It is pure (no I/O): the human interaction lives in the Approver, so a Policy stays trivially testable and swappable (tighten it to AskAll, or loosen it per project).

type Request

type Request struct {
	Command sandbox.Command
	Display string
}

Request is what an Approver is asked to approve: the raw command plus a human-readable rendering of it (Display) so the prompt doesn't have to know the sh -c wrapping the run tool uses.

type Sandbox

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

Sandbox decorates an inner sandbox.Sandbox, gating Exec through a Policy and (on Ask) an Approver. Every other method passes straight through — the gate adds nothing to reads, writes, listing, capabilities, or teardown.

func New

func New(inner sandbox.Sandbox, approver Approver, policy Policy) *Sandbox

New wraps inner with a confirmation gate. A nil policy defaults to DefaultPolicy; a nil approver makes every Ask a denial (a fully un-attended gate that blocks anything not explicitly Allowed — fail closed).

func (*Sandbox) AppendFile

func (s *Sandbox) AppendFile(ctx context.Context, path string, data []byte, mode fs.FileMode) error

AppendFile forwards the optional sandbox.Appender capability. An inner without it gets errors.ErrUnsupported so the CALLER (agent.writeFileOp) keeps one policy point for the degraded read+concat path — the gate never silently slurps a whole file itself.

func (*Sandbox) Capabilities

func (s *Sandbox) Capabilities() sandbox.Capabilities

Capabilities, ReadFile, WriteFile, Remove, ListDir, Close all pass through unchanged.

func (*Sandbox) Close

func (s *Sandbox) Close() error

func (*Sandbox) Exec

func (s *Sandbox) Exec(ctx context.Context, cmd sandbox.Command) (*sandbox.Result, error)

Exec gates the command. Allow runs it; Deny blocks it; Ask consults the Approver. A blocked command returns a synthetic non-zero Result (never a Go error), so the agent observes the denial and continues (P6).

func (*Sandbox) ListDir

func (s *Sandbox) ListDir(ctx context.Context, path string) ([]sandbox.DirEntry, error)

func (*Sandbox) MakeDirAll added in v0.2.0

func (s *Sandbox) MakeDirAll(ctx context.Context, path string, mode fs.FileMode) error

func (*Sandbox) ReadFile

func (s *Sandbox) ReadFile(ctx context.Context, path string) ([]byte, error)

func (*Sandbox) ReadFileLimit

func (s *Sandbox) ReadFileLimit(ctx context.Context, path string, max int64) ([]byte, bool, error)

ReadFileLimit forwards the optional sandbox.LimitedReader capability when the inner sandbox has it (local and docker do). If it doesn't, degrade the same way agent.readBounded's own fallback does: full read, then cap what is KEPT — the read itself is unbounded, but the caller never holds more than max.

func (*Sandbox) Remove added in v0.2.0

func (s *Sandbox) Remove(ctx context.Context, path string) error

func (*Sandbox) Root

func (s *Sandbox) Root() string

Root forwards the inner sandbox's workspace root when it exposes one (the local backend does), so host-side Go introspection (agent.hostWorkspace → go doc / go list) resolves module versions against the project even under the gate. "" means "no root known" and callers fall back to the process cwd.

func (*Sandbox) Workdir

func (s *Sandbox) Workdir() string

Workdir forwards the optional sandbox.WorkdirReporter capability.

func (*Sandbox) WriteFile

func (s *Sandbox) WriteFile(ctx context.Context, path string, data []byte, mode fs.FileMode) error

type Verdict

type Verdict int

Verdict is a Policy's classification of a command before it runs. The zero value is Ask on purpose: a policy that doesn't recognize a command falls through to a human, so an unanticipated command is never silently allowed.

const (
	Ask   Verdict = iota // unsure — defer to the Approver (the safe default).
	Allow                // clearly safe (read/build/test) — run without asking.
	Deny                 // never run — and don't bother asking.
)

func AskAll

func AskAll(sandbox.Command) Verdict

AskAll is the strictest policy: every command is Ask. Use it when even reads should be confirmed (an untrusted task, a shared machine).

func DefaultPolicy

func DefaultPolicy(cmd sandbox.Command) Verdict

DefaultPolicy auto-allows the read/build/test allowlist and asks for everything else. It never returns Deny — a human can approve anything; the policy only decides what is quiet enough to skip the prompt. An unrecognized command falls through to Ask (the zero Verdict), which is the fail-safe.

A line with shell chaining or redirection (`&&`, `;`, `|`, `>`, `$(`, …) is ALWAYS Ask even if it starts with a safe prefix: "go test ./... && rm -rf ~" must not ride in on "go test". The allowlist only trusts a single, simple command — anything that can hide a second one is shown to the human.

Jump to

Keyboard shortcuts

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