Documentation
¶
Overview ¶
Package permissions implements the central permission gate that decides whether each tool invocation may proceed.
The gate consults, in order:
- Bash denylist (built-in patterns; non-overridable) for bash calls.
- Path scope check for file tools.
- Config denylist patterns.
- Config allowlist patterns.
- 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 ¶
- Variables
- func IsBashDenied(command string) (denied bool, reason string)
- func SortRecommendations(recs []Recommendation)
- func SubagentSourceFromContext(ctx context.Context) string
- func WithSubagentSource(ctx context.Context, name string) context.Context
- type ApprovalLog
- type Decision
- type Gate
- func (g *Gate) Approvals() []ApprovalLog
- func (g *Gate) CheckBash(ctx context.Context, command string) error
- func (g *Gate) CheckFileRead(ctx context.Context, toolName, path string) error
- func (g *Gate) CheckFileWrite(ctx context.Context, toolName, path string) error
- func (g *Gate) CheckGeneric(ctx context.Context, toolName, key string) error
- func (g *Gate) HasPrompter() bool
- func (g *Gate) Mode() Mode
- func (g *Gate) Scope() *PathScope
- type Mode
- type Options
- type Outcome
- type PathScope
- type Policy
- type PromptKind
- type PromptRequest
- type Prompter
- type Recommendation
Constants ¶
This section is empty.
Variables ¶
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 ¶
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
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
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 ¶
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 )
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
type Mode ¶
type Mode string
Mode mirrors the permission modes recognized by config.PermissionsConfig.
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).
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 ¶
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 ¶
AddAlwaysAllow appends a pattern to the in-memory allowlist. The caller is responsible for persisting the change to config.json.
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.
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
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
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.