permissions

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: May 16, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package permissions implements the central permission gate that decides whether each tool invocation may proceed.

The gate consults, in order:

  1. Bash denylist (built-in patterns; non-overridable) for bash calls.
  2. Path scope check for file tools.
  3. Config denylist patterns.
  4. Config allowlist patterns.
  5. Mode-specific resolution: ask → prompt user; allow → deny; yolo → approve.

The interactive prompt path is implemented by the host (TUI / CLI REPL); see prompter.go for the Prompter interface.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoPrompter = errors.New("permissions: interactive approval required but no prompter is configured")

ErrNoPrompter is returned when the gate would prompt but no prompter is configured (e.g. headless mode without an explicit allowlist).

Functions

func IsBashDenied

func IsBashDenied(command string) (denied bool, reason string)

IsBashDenied reports whether command matches any built-in denylist pattern. The reason is a short, user-facing string suitable for surfacing in a prompt or stderr.

func SortRecommendations

func SortRecommendations(recs []Recommendation)

SortRecommendations returns a stable ordering: more specific patterns (without `*`) first, then by Pattern lex order.

func SubagentSourceFromContext added in v1.2.0

func SubagentSourceFromContext(ctx context.Context) string

SubagentSourceFromContext returns the subagent source name a prior WithSubagentSource call stamped onto ctx. Empty when none was set (the parent agent's own tool calls).

func WithSubagentSource added in v1.2.0

func WithSubagentSource(ctx context.Context, name string) context.Context

WithSubagentSource returns ctx tagged with name as the originating subagent's identifier. Called by the BackgroundAgentManager when a subagent runs; read by the gate when constructing PromptRequest values so the prompter can show "[name] tool wants to ..." in its heading.

Types

type ApprovalLog

type ApprovalLog struct {
	Tool     string
	Key      string
	Decision Decision
	At       time.Time
}

ApprovalLog is one entry in the gate's per-session approval audit. It records every interactive permission decision the user made (excluding denials) so callers can later offer a "review approvals + recommend" workflow.

type Decision

type Decision int

Decision is the user's choice in an interactive permission prompt.

const (
	DecisionDeny             Decision = iota // reject this call
	DecisionAllowOnce                        // allow this call, ask again next time
	DecisionAllowSession                     // allow this exact request for the rest of the session
	DecisionAllowSessionTool                 // allow EVERY call to this tool for the rest of the session, regardless of args
	DecisionAllowAlways                      // persist a permanent allowlist entry, then allow
)

func (Decision) String

func (d Decision) String() string

String renders Decision for diagnostics.

type Gate

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

Gate is the central permission chokepoint consulted before each tool call. It holds the configured policy, the path scope, the bash denylist (built-in), and an optional Prompter for interactive use.

Gate is safe for concurrent use; tool handlers run in the agent's event-iteration goroutine, but the prompter call may yield while waiting for the user.

func FromConfig

func FromConfig(cfg *config.Config, projectRoot, userRoot string, prompter Prompter) (*Gate, error)

FromConfig builds a Gate from a Config plus the resolved project root and user-global root. The Prompter is wired separately since it depends on whether we're running interactively or headless.

func New

func New(opts Options) *Gate

New builds a Gate from the supplied options. The Mode defaults to "ask"; missing Policy/Scope default to permissive empties.

func (*Gate) Approvals

func (g *Gate) Approvals() []ApprovalLog

Approvals returns a defensive copy of the in-session approval log. Order is chronological. Safe for concurrent callers.

func (*Gate) CheckBash

func (g *Gate) CheckBash(ctx context.Context, command string) error

CheckBash gates a bash invocation. The denylist is checked first and is non-overridable. After that, policy + mode determine whether the call needs a prompt.

func (*Gate) CheckFileRead

func (g *Gate) CheckFileRead(ctx context.Context, toolName, path string) error

CheckFileRead gates a read-only file operation. Read access only fails if the path is out of scope (and the user can't or won't extend scope).

func (*Gate) CheckFileWrite

func (g *Gate) CheckFileWrite(ctx context.Context, toolName, path string) error

CheckFileWrite gates a mutating file operation. Out-of-scope paths are escalated via prompt; in-scope paths still go through mode-aware approval (ask mode prompts; allow/yolo proceed unless deny rule hits).

func (*Gate) CheckGeneric

func (g *Gate) CheckGeneric(ctx context.Context, toolName, key string) error

CheckGeneric gates an arbitrary tool call (used by MCP and skill toolsets, where we don't have a dedicated Check<Tool> method).

toolName is the namespace under which policy lookups happen (typically "mcp" or "skill"); key is the human-readable detail shown in prompts (typically the tool's full namespaced name plus a brief argument summary).

func (*Gate) HasPrompter

func (g *Gate) HasPrompter() bool

HasPrompter reports whether an interactive Prompter is wired. False means an ask-mode call would fail with ErrNoPrompter rather than reach a human — useful for callers (e.g. autonomous drivers) that want to fail fast at startup instead of on the first tool call.

func (*Gate) Mode

func (g *Gate) Mode() Mode

Mode reports the active permission mode.

func (*Gate) Scope

func (g *Gate) Scope() *PathScope

Scope exposes the path scope. Callers that mutate the scope should also persist the change via the config layer.

type Mode

type Mode string

Mode mirrors the permission modes recognized by config.PermissionsConfig.

const (
	ModeAsk   Mode = "ask"
	ModeAllow Mode = "allow"
	ModeYolo  Mode = "yolo"
)

type Options

type Options struct {
	Mode     Mode
	Policy   *Policy
	Scope    *PathScope
	Prompter Prompter // nil = no interactive path; ask-mode unresolved → deny
}

Options configures a Gate at construction time. All fields are optional; sensible defaults apply when omitted.

type Outcome

type Outcome int

Outcome is the result of consulting the policy. It is used by the gate to decide what to do next; it is not the final say (the gate also consults the bash denylist, the path scope, and the mode).

const (
	OutcomeUnmatched Outcome = iota // no allow/deny rule matched
	OutcomeAllow
	OutcomeDeny
)

type PathScope

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

PathScope restricts file tool access to a defined set of paths: the project root, the user-home root, and any explicit pattern in path_scope.allow. Out-of-scope access escalates to a prompt (in interactive mode) or fails immediately (in headless without a prompter).

Patterns supported in the allow list:

  • Exact absolute paths.
  • Directory trees ending with `/...` (e.g. "/etc/myapp/...").
  • Standard filepath glob patterns (passed through path/filepath.Match).

func NewPathScope

func NewPathScope(projectRoot, userRoot string, allow []string) (*PathScope, error)

NewPathScope constructs a scope from the project root, the user-global home, and an extra allowlist of patterns.

projectRoot may be empty, in which case only the user root and allowlist apply. userRoot may be empty for tests.

func (*PathScope) AddAlwaysAllow

func (s *PathScope) AddAlwaysAllow(pattern string)

AddAlwaysAllow appends a pattern to the in-memory allowlist. The caller is responsible for persisting the change to config.json.

func (*PathScope) AllowList

func (s *PathScope) AllowList() []string

AllowList returns a copy of the configured allowlist patterns.

func (*PathScope) Contains

func (s *PathScope) Contains(path string) (bool, error)

Contains reports whether path is in scope. The path is resolved to an absolute, cleaned form before comparison; symlinks are not followed (we trust the input).

func (*PathScope) Roots

func (s *PathScope) Roots() []string

Roots returns a copy of the configured scope roots.

type Policy

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

Policy interprets the allow/deny pattern lists from .agents/config.json's `permissions` block.

Pattern grammar:

<tool>:<glob>     — applies only when the request is for <tool>
<glob>            — applies to any tool (matched against request key)

`<glob>` is matched with path/filepath.Match, so it understands `*`, `?`, and character classes. The "key" of a request depends on the tool: for bash it is the command string, for file tools it is the resolved absolute path. Wildcards work the same for both.

func NewPolicy

func NewPolicy(allow, deny []string) (*Policy, error)

NewPolicy parses the configured allow/deny patterns. Bad patterns fail fast so misconfigurations surface at startup, not when the agent first triggers a tool.

func (*Policy) Match

func (p *Policy) Match(tool, key string) Outcome

Match returns OutcomeDeny if any deny rule matches the request, OutcomeAllow if any allow rule matches and no deny rule matches, otherwise OutcomeUnmatched. Deny always wins.

type PromptKind

type PromptKind int

PromptKind classifies what the gate is asking the user about.

const (
	PromptKindBash      PromptKind = iota // mutating shell command
	PromptKindFileWrite                   // file write/edit/create
	PromptKindPathScope                   // file access outside the in-scope roots
	PromptKindGeneric                     // anything else
)

type PromptRequest

type PromptRequest struct {
	Kind        PromptKind
	ToolName    string
	Detail      string // user-facing description (the bash command, the file path, etc.)
	PersistTool string // tool name to use when adding to allowlist (e.g. "bash")
	PersistKey  string // pattern to add to allowlist

	// Source identifies the agent context the request originated
	// from when it isn't the top-level parent agent. Empty for the
	// parent's own tool calls; populated (e.g. "watch-prod-cluster")
	// when a background subagent's tool call triggered the prompt.
	// Prompters that surface it to the user help the human know which
	// agent they're approving for — the gate populates this from the
	// SubagentSourceFromContext context value the spawn machinery
	// stamps on each subagent's ctx.
	Source string
}

PromptRequest carries everything the host needs to render a prompt.

The persistence target — what would be written to .agents/config.json if the user picks DecisionAllowAlways — is held in PersistKey/PersistTool so the prompter doesn't have to re-derive it from Detail.

type Prompter

type Prompter interface {
	AskApproval(ctx context.Context, req PromptRequest) (Decision, error)
}

Prompter is implemented by hosts that can interact with the user. Headless callers may pass nil; the gate treats a nil prompter as "no interactive path available".

func Serialize added in v1.2.0

func Serialize(inner Prompter) Prompter

Serialize returns a Prompter that wraps inner with a mutex so concurrent AskApproval calls run one at a time. Necessary when the gate is shared across multiple goroutines that might prompt the same underlying medium (e.g. several background subagents racing for os.Stdin via the inherited StdinPrompter).

Cancellation: a blocked caller's ctx is honored — once the mutex is acquired, the underlying inner.AskApproval sees the original ctx and can fail fast on ctx.Done(). Callers waiting in line just block on the mutex until their turn or ctx error during the inner call.

Passing nil returns nil so callers can chain Serialize(nil) without a guard.

func StdinPrompter added in v1.1.0

func StdinPrompter(in io.Reader, out io.Writer) Prompter

StdinPrompter returns a Prompter that renders permission requests to out and reads a single-character decision key from in. Suitable for interactive CLI use; callers should gate construction on runner.IsTerminal(in) so headless invocations fall back to ErrNoPrompter instead of blocking on stdin.

Decision keys (case-insensitive, one character followed by newline):

y         allow once
s         allow this exact request for the rest of the session
t         allow every call to this tool for the rest of the session
a         allow always (persist to the project's config allowlist)
n / empty deny

Invalid input reprompts. EOF or context cancellation returns the underlying error so the gate can surface a denial with context.

type Recommendation

type Recommendation struct {
	Pattern  string   // e.g. "bash:git *", "read_file:internal/tui/**"
	Reason   string   // one-line human explanation of what this covers
	Evidence []string // sample keys that motivated the recommendation
}

Recommendation describes one suggested allowlist entry the user could persist into .agents/config.json's `permissions.allow` block. Pattern is in the existing tool:glob form so a recommendation can be appended verbatim. Reason and Evidence let a picker explain WHY before the user agrees to broaden their permanent allowlist.

func Recommend

func Recommend(approvals []ApprovalLog) []Recommendation

Recommend turns a session's interactive approval log into a small list of suggested permanent allowlist entries.

Jump to

Keyboard shortcuts

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