permissions

package
v2.7.0-dev.5 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 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

View Source
const (
	BundleReadOnly       = "read_only"
	BundleDevTools       = "dev_tools"
	BundleCoreAgentTools = "core_agent_tools"
)

Built-in allow bundle names. Use these constants (rather than literal strings) when constructing a config so a rename surfaces at compile time.

View Source
const (
	ToolGateAllowed           = "allowed"
	ToolGateDenied            = "denied"
	ToolGatePrompted          = "prompted"
	ToolGateDeniedInAllowMode = "denied-allow-mode"
)

Tool-gate state strings exposed via the attach-mode /tools endpoint. Kept as bare strings (not a typed enum) so JSON consumers downstream (TUI, WebUI, operator scripts) don't have to import a Go package to reason about them.

Variables

View Source
var Bundles = map[string][]string{
	BundleReadOnly: {

		"bash:pwd",
		"bash:whoami",
		"bash:id",
		"bash:groups",
		"bash:hostname",
		"bash:uname",
		"bash:uname *",
		"bash:date",
		"bash:uptime",
		"bash:env",
		"bash:printenv",
		"bash:printenv *",

		"bash:ls",
		"bash:ls *",
		"bash:tree",
		"bash:tree *",

		"bash:cat *",
		"bash:head",
		"bash:head *",
		"bash:tail",
		"bash:tail *",
		"bash:wc",
		"bash:wc *",
		"bash:file *",

		"bash:echo",
		"bash:echo *",
		"bash:printf *",
		"bash:true",
		"bash:false",
		"bash:grep *",
		"bash:egrep *",
		"bash:fgrep *",
		"bash:rg *",
		"bash:sort *",
		"bash:uniq *",
		"bash:cut *",
		"bash:tr *",
		"bash:awk *",
		"bash:sed -n*",

		"bash:which *",
		"bash:type *",
		"bash:whereis *",
		"bash:command -v *",

		"bash:df",
		"bash:df *",
		"bash:du",
		"bash:du *",
		"bash:free",
		"bash:free *",
		"bash:ps",
		"bash:ps *",

		"bash:find *",
	},
	BundleDevTools: {

		"bash:git status*",
		"bash:git log*",
		"bash:git diff*",
		"bash:git show*",
		"bash:git branch*",
		"bash:git tag*",
		"bash:git remote*",
		"bash:git config --get *",
		"bash:git config --list*",
		"bash:git rev-parse*",
		"bash:git rev-list*",
		"bash:git ls-files*",
		"bash:git ls-remote*",
		"bash:git blame *",
		"bash:git reflog*",
		"bash:git stash list*",

		"bash:go version",
		"bash:go env*",
		"bash:go list*",
		"bash:go doc*",
		"bash:go vet*",

		"bash:gofmt -l*",
		"bash:gofmt -d*",
	},
	BundleCoreAgentTools: {

		"read_file:**",
		"list_dir:**",
		"grep:**",
	},
}

Bundles maps each built-in bundle name to its allowlist entries. Entries use the standard `<tool>:<glob>` grammar from policy.go.

`read_only` is enabled by default (via use_builtin_allow=true). `dev_tools` and `core_agent_tools` are opt-in via the `permissions.builtin_allow_extras` config field.

Conservative-by-construction: every entry here is a verb the LLM commonly uses for inspection, with no `-i`/`-w`/`--delete`-style mutating flag in the pattern. `find *` is the one knowing concession — find has a `-delete` / `-exec rm` escape hatch, but excluding it makes the read-only baseline frustrating in practice and the bash denylist still blocks `rm -rf /` style targets if the LLM tries to chain destructively through it.

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 KnownBundles

func KnownBundles() []string

KnownBundles returns the sorted list of recognized bundle names. Use this for config validation messages so the list stays in lockstep with the actual map.

func ResolveBuiltinAllow

func ResolveBuiltinAllow(useBuiltin bool, extras []string) ([]string, error)

ResolveBuiltinAllow returns the merged set of allowlist patterns implied by useBuiltin + extras, deduplicated and stably ordered.

useBuiltin=false drops everything regardless of extras (matches the `permissions.use_builtin_allow: false` master-switch semantics).

Unknown bundle names in extras error rather than silently no-op so typos surface at config-validation time, not by quietly missing permissions at runtime.

func SortRecommendations

func SortRecommendations(recs []Recommendation)

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

func SubagentSourceFromContext

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 WithSessionGate

func WithSessionGate(ctx context.Context, g *Gate) context.Context

WithSessionGate stores the per-session sub-gate on ctx so downstream tool wrappers can opt into routing permission checks through THIS session's gate (and therefore THIS session's prompter) instead of the gate captured at tool-construction time.

The multi-session daemon needs this because MCP toolsets + built-in tool registries are constructed once at startup (the template gate). Without a session-aware override, every tool call's permission prompt would land on the daemon's startup-time PromptBroker — which isn't subscribed to by the per-session attach client. Result: alice's tool prompts go nowhere; the tool hangs forever waiting for an approval that never arrives.

agent.Run wraps runCtx with WithSessionGate(a.gate) so any tool wrapper that propagates ctx through to its gate check (notably pkg/tools.gatedTool used by both built-in tools and MCP toolsets via GateToolset) sees the per-session sub-gate via SessionGateFromContext and prefers it.

Nil gate is a no-op (returns the input ctx unchanged) so call sites don't need a guard.

func WithSubagentSource

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 Access

type Access uint8

Access is a bitmask of file operations a PathScope entry grants. Splitting read from write lets the allow-list say "agent may read this tree but writes still prompt" — closer to what an operator usually wants when granting access to a sibling repo. Operations outside file I/O (bash, generic tool calls) are gated separately via Policy and don't consult Access.

const (
	AccessNone      Access = 0
	AccessRead      Access = 1 << 0
	AccessWrite     Access = 1 << 1
	AccessReadWrite        = AccessRead | AccessWrite
)

func ParseAccess

func ParseAccess(s string) (Access, error)

ParseAccess parses the access spec used by --allow-path and the config file's allow_paths entries. Accepts the short forms (r, w, rw) and the long forms (read, write, readwrite / read+write) so operators can use whichever reads better in their config. Case insensitive. Returns an error for any other input — silent fallback to a permissive default would hide typos that quietly broaden access.

func (Access) Allows

func (a Access) Allows(op Access) bool

Allows reports whether a carries every bit in op. Designed to be called with one of AccessRead / AccessWrite (single-bit checks) from the gate, though the bitmask semantics generalize.

func (Access) String

func (a Access) String() string

String renders Access in the short form the config + CLI accept (`r`, `w`, `rw`) so logs / errors round-trip with ParseAccess.

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
	DecisionAllowSessionVerb                 // allow every bash command starting with this verb for the session (e.g. all "git *")
	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.

Multi-session deployments build one template Gate at daemon start (typically via FromConfig) and derive a per-session sub-gate for each agent via DeriveForSession. Sub-gates share the template's daemon-wide configuration (policy / scope / requirePlanArtifact) but carry their own per-session mutable state (sessionAllow / sessionAllowTools / sessionAllowVerbs / approvals / planRecorded / prompter / mode). See docs/multi-session-design.md.

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.

Built-in allow bundles are merged on top of the configured Allow patterns: the read_only bundle is on by default and can be turned off with permissions.use_builtin_allow=false; additional bundles listed in permissions.builtin_allow_extras add to the merge. See builtin_allow.go for the bundle catalog.

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 SessionGateFromContext

func SessionGateFromContext(ctx context.Context) (g *Gate, ok bool)

SessionGateFromContext returns the per-session sub-gate previously stamped on ctx by WithSessionGate. ok is false (and the returned gate is nil) when no session gate is on the context — typically the single-user / pre-multi-session code paths. Callers fall back to their constructor-time gate in that case.

func (*Gate) AddAllowPatterns

func (g *Gate) AddAllowPatterns(patterns []string) error

AddAllowPatterns extends the live policy with additional allow patterns and is safe to call concurrently with in-flight Match calls. Used by the TUI's /allow slash command to make new permissions take effect immediately rather than only after a restart. Returns the same error shape as NewPolicy when a pattern is malformed.

func (*Gate) AddDenyPatterns

func (g *Gate) AddDenyPatterns(patterns []string) error

AddDenyPatterns is the symmetric extension for deny entries, used by /deny. Deny always wins in Match so adding here can override a previously-allowed pattern mid-session.

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. An allow-list entry that grants read (r or rw) short-circuits the prompt; write-only entries (w) for the same path still escalate via promptForPath.

func (*Gate) CheckFileWrite

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

CheckFileWrite gates a mutating file operation. Paths the scope grants write to (w or rw) still go through mode-aware approval (ask mode prompts; allow/yolo proceed unless deny rule hits). Paths not covered for writes — even if the same scope entry permits reads — escalate via the path-scope prompt.

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) ClearPlanRecorded

func (g *Gate) ClearPlanRecorded()

ClearPlanRecorded resets the per-gate planRecorded flag. Called by the /replan slash handler when the operator rejects the current plan; the model is forced back through record_plan before any further mutating tool call.

func (*Gate) DeriveForSession

func (template *Gate) DeriveForSession(sessionID string, prompter Prompter) *Gate

DeriveForSession returns a per-session sub-gate derived from this (template) gate. The sub-gate shares the template's daemon-wide configuration by reference — Policy, PathScope, requirePlanArtifact — and carries its own per-session mutable state: sessionAllow / sessionAllowTools / sessionAllowVerbs / approvals / planRecorded start empty, and Mode is copied so per-session SetMode (e.g., a TUI chip toggle) doesn't bleed into the template or sibling sessions.

prompter is the per-session interactive handler — typically the HTTP-driven broker for an attach-mode session, or stdin for a local interactive run. nil disables interactive prompting on this sub-gate (ask-mode calls then fail with ErrNoPrompter, same as a directly-constructed Gate without a prompter).

sessionID is stored for diagnostics; an empty string is accepted for back-compat with callers that haven't threaded it through yet.

Limitations (documented because operators read this surface):

  • Policy mutations via AddAllowPatterns / AddDenyPatterns mutate the shared template Policy and therefore affect every derived sub-gate. /allow + /deny are intentionally daemon-wide today per docs/multi-session-design.md §"Per-substrate isolation rules"; per-session policy carve-outs are a follow-up.
  • PathScope mutations via AddAlwaysAllow (triggered by DecisionAllowAlways) similarly mutate the shared scope.

Both limitations are by design for v2.4 — the typical operator model is "one config, many users" with per-user authorization on top of a shared substrate. Per-session policy/scope isolation can layer on later without changing this method's shape.

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) IsPlanRecorded

func (g *Gate) IsPlanRecorded() bool

IsPlanRecorded reports whether a plan has been recorded this session. Exposed so the TUI can render a "plan recorded" badge and so /replan can short-circuit if no plan exists to revoke.

func (*Gate) MarkPlanRecorded

func (g *Gate) MarkPlanRecorded()

MarkPlanRecorded flips the per-gate planRecorded flag. Called by the record_plan tool's handler after the plan artifact has been written. Idempotent — calling twice is harmless.

After this returns, subsequent mutating tool calls bypass the plan-first pre-check and resume the configured Mode's normal gating semantics.

func (*Gate) Mode

func (g *Gate) Mode() Mode

Mode reports the active permission mode. Acquires g.mu to pair with SetMode's writer — without the read lock, SetMode would race with every other mode reader (gateRequest, promptForPath, ToolGateState).

func (*Gate) PlanRequired

func (g *Gate) PlanRequired() bool

PlanRequired reports whether RequirePlanArtifact was set at construction. Exposed so TUI / config-introspection code can tell the difference between "no plan recorded but it's not required" and "no plan recorded and we're gated".

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.

func (*Gate) SessionID

func (g *Gate) SessionID() string

SessionID returns the session identifier this gate was derived for, or "" when the gate was built directly via New / FromConfig (i.e., it IS the template). Useful for diagnostics; not exposed in the JSON Snapshot.

func (*Gate) SetMode

func (g *Gate) SetMode(m Mode)

SetMode replaces the gate's permission mode at runtime. Used by the embedded TUI when the operator cycles the permission-mode chip (R-PERM-6 in core-tui). Unknown modes are silently ignored so a future TUI value can't smuggle in semantics the gate doesn't recognize.

func (*Gate) SetPrompter

func (g *Gate) SetPrompter(p Prompter)

SetPrompter swaps the gate's interactive prompter. Used when the process changes UI mode mid-startup — e.g. core-agent's main.go constructs the gate with a stdin prompter for the headless path, then the TUI replaces it with one that sends messages into the bubble-tea program. Set to nil to disable interactive prompting (ask-mode calls then fail with ErrNoPrompter).

func (*Gate) Snapshot

func (g *Gate) Snapshot() Snapshot

Snapshot returns the current gate configuration.

func (*Gate) ToolGateState

func (g *Gate) ToolGateState(toolName string) string

ToolGateState classifies a tool name against the configured policy without actually requesting permission. Used by the attach-mode /tools endpoint so the TUI / WebUI / operator can see whether a tool would be allowed, denied, or prompted before the model tries it.

Semantics:

  • "denied" — a deny pattern matches the bare tool name (no key). Denials with key globs (e.g. "bash:sudo *") cannot be pre-computed without a candidate key and are reported as "prompted".
  • "allowed" — mode is "yolo" (gate is bypassed), OR an allow pattern matches the bare tool name + no deny does.
  • "prompted" — mode is "ask" and no preempting allow/deny applies.
  • "denied-allow-mode" — mode is "allow" and no allowlist entry covers the tool (so it would be refused with a "requires an allowlist entry" error).

This is a pre-flight projection, not a guarantee — interactive approvals at runtime can grant access that's not in the snapshot.

type Mode

type Mode string

Mode mirrors the permission modes recognized by config.PermissionsConfig.

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

	// ModePlan disables all tool execution — every gate call returns
	// an error. Used by core-tui's "plan" chip (R-PERM-7) for
	// read-and-think sessions that shouldn't touch the world. The
	// operator cycles out via Shift+Tab when ready to act.
	ModePlan Mode = "plan"

	// ModeAcceptEdits auto-allows file-write tool calls (and
	// out-of-scope write paths) without prompting; every other tool
	// kind still flows through the normal Ask path. Used by core-
	// tui's "acceptEdits" chip so the operator can stream a
	// refactor without clicking through every diff modal.
	ModeAcceptEdits Mode = "acceptEdits"
)

type Options

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

	// RequirePlanArtifact, when true, denies mutating tool calls
	// (write_file/edit_file/delete_file/bash, spawn family, MCP
	// tools, and anything else not in planExemptTools) until the
	// model has called the record_plan tool at least once this
	// session. Read tools and record_plan itself are exempt so
	// research happens normally and the model has an escape valve.
	//
	// Composes with every existing Mode. Even ModeYolo respects
	// the plan-first pre-check; once a plan is recorded, the mode's
	// usual semantics resume. See docs/plan-first-design.md.
	RequirePlanArtifact bool
}

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 / path_scope.allow_paths. 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).

Each entry carries its own Access level (r / w / rw). The legacy allow-list (string slice) maps to AccessReadWrite for backward compatibility — anyone who already wrote to that list expected the agent to be able to do anything inside the entry.

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. Every entry in allow gets AccessReadWrite — the legacy semantics. Use NewPathScopeFromEntries when callers need per-entry access levels.

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

func NewPathScopeFromEntries

func NewPathScopeFromEntries(projectRoot, userRoot string, entries []pathEntry) (*PathScope, error)

NewPathScopeFromEntries is the access-aware constructor. Each entry's access is preserved verbatim; callers (FromConfig, tests) build the slice from whatever shape their input has — typed AllowPaths entries, parsed --allow-path flags, etc.

func (*PathScope) AccessFor

func (s *PathScope) AccessFor(path string) (Access, error)

AccessFor returns the access level granted for path. Paths inside any configured root yield AccessReadWrite (roots are trusted by definition). Otherwise the allow-list is scanned and the longest-prefix match wins — a narrower entry can carve a more- permissive exception inside a less-permissive parent. Returns AccessNone when nothing covers the path.

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

func (*PathScope) AddAlwaysAllow

func (s *PathScope) AddAlwaysAllow(pattern string, access Access)

AddAlwaysAllow appends a pattern to the in-memory allowlist with the given access level. The caller is responsible for persisting the change to config.json. Used by the gate when an interactive prompt resolves to DecisionAllowAlways — the access bit reflects the op the prompt was for, not a blanket rw grant.

func (*PathScope) AllowList

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

AllowList returns a copy of the configured allowlist patterns. Access levels are dropped; callers needing the per-pattern level should reach for AccessFor on a specific path instead. Kept on the surface so debug commands / snapshot serializers that pre-dated Access still work.

func (*PathScope) Contains

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

Contains reports whether path is in scope at all (any access). Preserved for snapshot / serializer callers that don't care about per-op access. Equivalent to AccessFor(path) != AccessNone.

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.

The mutex guards live policy extension via AddAllow/AddDeny so the TUI's /allow and /deny slash commands can patch the policy from one goroutine while Match is consulting it from another (typically the agent's tool-call goroutine).

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) AddAllow

func (p *Policy) AddAllow(patterns []string) error

AddAllow validates and appends patterns to the allow set. Existing patterns are skipped (idempotent — the /allow slash command can be retried without growing the policy). Bad patterns abort the whole call without partial mutation so the on-disk config and the live policy stay in sync after a failed parse.

func (*Policy) AddDeny

func (p *Policy) AddDeny(patterns []string) error

AddDeny is the symmetric extension for deny rules. Deny always wins in Match, so adding a deny pattern mid-session can override a previously-allowed rule without restart.

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.

func (*Policy) RawPatterns

func (p *Policy) RawPatterns() (allow, deny []string)

RawPatterns returns the original "tool:pattern" strings the Policy was built from, as two slices (allow, deny). The patterns are reconstituted (tool prefix re-added when present) so the output round-trips with the JSON config form. Used by Gate.Snapshot() to surface configured policy without leaking the parsed rule struct.

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

	// Verb, when populated, is the first whitespace-separated token of
	// a bash command (e.g. "git" for "git push origin main"). The TUI
	// modal uses this to offer DecisionAllowSessionVerb — broaden to
	// every command starting with this verb for the rest of the
	// session. Empty when the gate couldn't safely extract a verb
	// (path scripts, quoted commands, non-bash prompts); the modal
	// suppresses the [v] option in that case.
	Verb string

	// 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

	// Access is the file operation being requested when Kind ==
	// PromptKindPathScope (AccessRead or AccessWrite). The
	// DecisionAllowAlways branch uses it to persist the matching
	// access bit on the new PathScope entry — granting rw via the
	// prompt requires two approvals (one for each op) instead of a
	// blanket grant the operator didn't explicitly choose. Zero
	// (AccessNone) for non-path-scope prompts.
	Access Access
}

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

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

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.

type Snapshot

type Snapshot struct {
	Mode  Mode     `json:"mode"`
	Allow []string `json:"allow,omitempty"`
	Deny  []string `json:"deny,omitempty"`
}

Snapshot is a read-only view of the gate's configured policy + mode, suitable for surfacing to operators (attach-mode /tools endpoint, the TUI's tool catalog) without exposing the gate's internal state. The returned slices are defensive copies. Does not include session-level approvals (those are inherently fleeting and per-request); use Approvals() for the per-session audit log.

Jump to

Keyboard shortcuts

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