Documentation
¶
Overview ¶
Package agentpolicy decides what an agent may do: a rule grammar over tool calls, an engine that becomes the loop's BeforeToolCall hook, a journal of every verdict, and an answer source for the calls the engine defers. Guards over content live in the guard package and become the loop's BeforeModelCall, OutputGuard and ShouldStopAfterTurn hooks; a guard and a reviewer backed by a model live in classify.
A policy is three lists of rules and a default:
rules, _ := agentpolicy.ParseRules("bash(git status:*) bash(npm test:*)")
eng, err := agentpolicy.Build(agentpolicy.Policy{
Allow: rules,
Deny: must(agentpolicy.ParseRules("bash(rm:*)")),
Default: agentpolicy.Ask(),
}, map[string]agentpolicy.ToolMatcher{
"bash": {Match: agentpolicy.PrefixMatcher("command")},
})
cfg.BeforeToolCall = eng.BeforeToolCall()
Precedence is deny, then ask, then allow, then the default, always. A deny blocks the call with a reason that names the rule; an ask defers it to the caller, who answers through the loop's Resume, and holds the calls of the same batch the policy allows, which Engine.Release answers from the caller's answer. When a tool's matcher splits a call into several subjects, a shell command into its subcommands say, the call is denied if any subject is, asked about if any subject is, and allowed only when every subject is.
A deny rule with no specifier, or one whose tool name is a glob, takes the tool out of the request rather than refusing its calls: Engine.ToolProvider is the hook value that does it. One engine serves every agent of a product, since the policy is the product's and not a loop's, and it remembers what it defers under the call's own run. The rules change under it: Engine.SetPolicy for a tool list that changed, Engine.Grant and Engine.GrantOver for an "always allow", and Engine.GrantSet for a rule set that lasts as long as its source, a skill's allowed-tools, which Engine.Revoke takes back.
The engine never calls a model or opens a socket: the same policy and the same call always give the same verdict. Every verdict, every grant and every reviewer's answer reaches the observer exactly once, so a product can record it beside the session.
Index ¶
- Constants
- Variables
- type Default
- type DenialBound
- type Engine
- func (e *Engine) Answers(ctx context.Context, r Reviewer, end *agentturn.RunEnd) ([]agentturn.Answer, error)
- func (e *Engine) BeforeToolCall() func(context.Context, agentturn.ToolCallInfo) (*agentturn.ToolDecision, error)
- func (e *Engine) Decide(ctx context.Context, info agentturn.ToolCallInfo) (*agentturn.ToolDecision, error)
- func (e *Engine) Deferred(runID, callID string) (Verdict, bool)
- func (e *Engine) Filter(tools []agenttool.Tool) []agenttool.Tool
- func (e *Engine) Forget(runID string)
- func (e *Engine) Grant(ctx context.Context, r Rule) error
- func (e *Engine) GrantOver(ctx context.Context, v Verdict, r Rule) (granted bool, reason string)
- func (e *Engine) GrantSet(ctx context.Context, set RuleSet) (granted []Rule, refused []Refusal)
- func (e *Engine) Grants() []RuleSet
- func (e *Engine) Policy() Policy
- func (e *Engine) PolicyOf(source string) RuleSet
- func (e *Engine) Release(ctx context.Context, end *agentturn.RunEnd, answers ...agentturn.Answer) ([]agentturn.Answer, error)
- func (e *Engine) Removes(tool string) (Rule, bool)
- func (e *Engine) ResetReviews()
- func (e *Engine) Revoke(ctx context.Context, source string) int
- func (e *Engine) Runs() []string
- func (e *Engine) SetPolicy(p Policy) error
- func (e *Engine) Sources() []Source
- func (e *Engine) ToolProvider(base func(context.Context) []agenttool.Tool) func(context.Context) []agenttool.Tool
- func (e *Engine) Withheld() []Rule
- type Matcher
- type Option
- type Outcome
- type Policy
- type Refusal
- type Review
- type Reviewer
- type ReviewerFunc
- type Rule
- type RuleSet
- type Source
- type Subject
- type Subjects
- type ToolMatcher
- type Tools
- type Verdict
Constants ¶
const ( // ByPolicy is a rule the harness evaluated on its own, which is // every decision the engine makes. ByPolicy = "policy" // ByAgent is another model: a [Reviewer] backed by one, as the // classify package's is. ByAgent = "agent" // ByHuman is a person the front asked, which is what a [Reviewer] // that prompts one sets on its [Review]. ByHuman = "human" )
Who a decision names as its decider, in the session format's words.
Variables ¶
var ( // ErrNoDefault is returned when the policy's Default was left unset. ErrNoDefault = errors.New("agentpolicy: policy default is not set; use Allow(), Deny() or Ask()") // ErrNoMatcher is returned when a rule has a specifier and its tool // has no matcher, so the rule could never match. The error names // the rule. ErrNoMatcher = errors.New("agentpolicy: no matcher for the rule's tool") // ErrToolGlob is returned for a rule whose tool name is a glob that // the engine will not honour: one in the allow list, where the // reference refuses it too, and one with a specifier, which names // no matcher. The error names the rule. ErrToolGlob = errors.New("agentpolicy: tool-name glob") )
Errors returned by Build and the grants. Every error the package produces begins with "agentpolicy:".
var DefaultDenialBound = DenialBound{Consecutive: 3, Total: 10, Window: 50}
DefaultDenialBound is Codex's: three consecutive refusals, or ten within the last fifty reviews.
var ErrDenialBound = errors.New("agentpolicy: reviewer denial bound reached")
ErrDenialBound is returned by Engine.Answers, with the answers, when the DenialBound was reached.
var ErrUnanswered = errors.New("agentpolicy: pending call has no answer")
ErrUnanswered is returned by Engine.Release, with the answers it built, when a call the run left pending has no answer: the caller did not answer it and the engine did not hold it, so resuming with the answers would fail. The error names every such call.
Functions ¶
This section is empty.
Types ¶
type Default ¶
type Default struct {
// contains filtered or unexported fields
}
Default is what applies to a subject no rule matches. The zero value is unset, and Build refuses a policy whose default is unset, so a deny list written on its own never allows everything else by accident. Build one with Allow, Deny or Ask.
type DenialBound ¶
DenialBound is when Engine.Answers stops answering: after Consecutive refusals in a row, or Total refusals within the last Window reviews. A zero field means no bound on that axis. Every answer that is not an approval counts as a refusal, a timeout and a failed review included.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the runtime form of a Policy: the policy in force, the matchers it is evaluated with, the grants made since it was built and the calls it has deferred. It is safe for concurrent use.
func Build ¶
Build validates the policy against the matchers and returns the runtime form. It fails with ErrNoDefault when the default is unset, with ErrNoMatcher when a rule has a specifier and its tool has no matcher, and with ErrToolGlob when a rule's tool name is a glob the engine will not honour, so a rule that could never match is refused here rather than ignored at the first call. The lists are copied; the caller's slices are not retained.
func (*Engine) Answers ¶
func (e *Engine) Answers(ctx context.Context, r Reviewer, end *agentturn.RunEnd) ([]agentturn.Answer, error)
Answers reviews the calls end left pending and returns one Answer per call for agentturn's Resume, in the order of end.Pending. An approval runs the call inside the loop, with the reviewer's arguments when it gave any, and carries the reviewer's note. A refusal, a timeout and a review that failed each answer the call with a refusal the model reads, and a refusal tells the model not to pursue the same outcome by another route; only a cancelled ctx returns an error instead of answers. Every answer is a Verdict through the observer: Allow for an approval, Block otherwise, with the reason and with Verdict.By naming who answered, the reviewer through Review.By or the policy for the answers the engine makes on its own.
The reviewer sees each deferred call as the hook saw it, with the verdict that deferred it, for the calls the engine asked about in that run; a call of a run it has forgotten is reviewed with the call alone and a verdict whose Action is Defer and whose Reason is empty. A call the engine held for an ask is not reviewed: the policy allowed it, and Engine.Release answers it from the asked calls' answers. A call an abort cut off or one found unanswered in a seeded transcript, whose tool may have run, is not reviewed either: it is answered with a refusal that says so, and does not count toward the bound, so the model decides whether to ask for it again.
When the DenialBound is reached, every refusal among the answers is built with agentturn.Refuse, so Resume appends the outputs and ends the run with StopRefused instead of calling the model, the held calls are refused with it, and the answers are returned with ErrDenialBound so the front can say why. The bound counts across calls to Answers until Engine.ResetReviews.
Answers completes with Engine.Release, so a pending call it could not answer is ErrUnanswered, returned with the answers and joined with ErrDenialBound when both hold.
func (*Engine) BeforeToolCall ¶
func (e *Engine) BeforeToolCall() func(context.Context, agentturn.ToolCallInfo) (*agentturn.ToolDecision, error)
BeforeToolCall returns the hook value for agentturn.Config.
func (*Engine) Decide ¶
func (e *Engine) Decide(ctx context.Context, info agentturn.ToolCallInfo) (*agentturn.ToolDecision, error)
Decide is the verdict for one call. The call is split into its subjects by the tool's Subjects, one subject when it has none, and each subject is decided with the fixed precedence: deny, then ask, then allow, then the default. The verdicts fold to the most restrictive: the call is blocked if any subject is denied, deferred if any subject asks, and allowed only when every subject is. A splitter that fails blocks the call with its error as the reason.
A call the policy allows is held, deferred with Verdict.Held set, when another call of its batch asks, so nothing the model asked for in the same turn runs before the user has answered; the loop runs the hook for every call of the batch before any executes, so the engine sees the ask wherever it sits in the batch. Engine.Release answers the held calls once the asked ones are answered. A blocked call is blocked whatever its batch holds.
The decision names the policy as its decider. The verdict reaches the observer before Decide returns, and the same policy and the same call in the same batch always give the same verdict.
One engine serves every agent of a product. What it defers is remembered under the call's own run, so a decision in a sub-agent's run never forgets what the main agent is waiting on.
func (*Engine) Deferred ¶ added in v0.0.2
Deferred returns the verdict that deferred the call in the run, asked or held, so a front can show why a pending call waits and tell the calls it must answer, the asked ones, from the ones Engine.Release answers, the held ones. It reports false for a call the engine did not defer, one of another run, and one already answered: Engine.Release and Engine.Answers forget a call as they answer it, and a product that records a verdict does so from the observer.
func (*Engine) Filter ¶ added in v0.0.3
Filter returns the tools of the list the policy does not remove, in order. It is the Offer the round 1 study asked for: a bare-name deny, or a deny whose tool-name glob matches, withholds the tool from the model rather than refusing its calls one at a time.
The filtering is not journalled. It answers what the model is offered, once per turn, not what was decided about a call, and a front that shows the user what a policy withheld reads Engine.Removes for the rule.
func (*Engine) Forget ¶ added in v0.0.3
Forget drops what the engine remembers of a run: the calls it deferred there and never answered. A front calls it when a run ends without its pending calls being answered, an abandoned conversation or a sub-agent that was cancelled, so the engine does not hold them for the life of the process. Engine.Release and Engine.Answers forget the calls they answer on their own, so a run answered through either needs no Forget.
func (*Engine) Grant ¶
Grant adds an allow rule for the rest of the process, as "always allow" does in an approval prompt that the default raised. The grant is journaled through the observer with a Verdict whose Action is Allow and whose Rule is the new rule. A rule with a specifier and no matcher is refused with ErrNoMatcher. A rule whose name is an alias is expanded as Build expands one, so the grant adds a rule per tool the name governs and journals each. A grant never beats a deny rule, and never outranks an ask rule; use Engine.GrantOver to answer a prompt an ask rule raised. A persisted grant is the product's: it writes the rule into its settings and rebuilds the engine at the next start.
func (*Engine) GrantOver ¶
GrantOver answers the prompt behind v with "always allow". It adds r as an allow rule and, when an ask rule produced v, keeps that rule from firing for the calls r matches, since precedence alone would let it ask again: a grant with a specifier appends the carve-out "<tool>(!<spec>)" beside the ask rule, under the grant's own source, and a bare grant removes the ask rule. The carve-out is the grant's because the grant is, so a product that persists its own source's rules writes a personal "always allow" into its own settings and not into the file the team shares; a carve-out cancels a rule of any source that does not outrank it, which is what makes it reach the rule it answers. Engine.PolicyOf is what a product persists. The grant is journaled as Engine.Grant journals one.
It reports why it cannot, so a front drops the "always" option and offers a one-time approval instead: v did not defer the call, a deny rule produced it, the ask rule comes from a source that outranks r's, the ask rule is no longer in the policy, r does not name the ask rule's tool, r is a carve-out, or r has a specifier and no matcher. The reason is stable text a front can show.
func (*Engine) GrantSet ¶ added in v0.0.3
GrantSet activates a rule set under its source, as a skill's allowed-tools grants the tools the skill was written to run. It is the grant a verdict cannot answer: there is no prompt yet, so Engine.GrantOver has nothing to grant over, and an appended allow rule loses to any ask rule naming the tool, which is why a team whose settings ask before every bash was prompted for every command of the commit skill they wrote to avoid it.
A set's allow rules therefore shadow the ask rules they cover: while the grant is active, an ask rule from another source whose rank is at or below the set's is not consulted for a subject the set allows. A skill from a trusted source is a grant the user made by installing it; an untrusted source's allow rules are withheld, as Merge withholds them, and reported by Engine.Withheld. The set's deny and ask rules apply at once, trusted or not, since they only restrict. A grant never beats a deny.
A rule is refused, and not activated, when it could never fire (no matcher, a carve-out, a tool-name glob in an allow list), when its source is untrusted, when a deny rule with no specifier names its tool, and when an ask rule with no specifier from a source that outranks the set names its tool, which is the rank check Engine.GrantOver makes. An ask rule with a specifier is left to the decision, where the rank test is made against the subject.
Every rule is stamped with the set's source, as Merge stamps one, so the verdict that fires names where the permission came from.
The set is keyed by its source name: activating a second set under the same name replaces the first, and Engine.Revoke removes it, which is what a product calls at the turn boundary for a grant that lasts one turn. Rules a name governs are expanded as Build expands them. Every grant and every refusal is a Verdict through the observer.
A product with several skills activates each as its own source rather than merging them all into the policy before the run, so the tools of a skill the model never opened are never granted and Merge never has to be given two sources with one name.
func (*Engine) Grants ¶ added in v0.0.3
Grants returns the rule sets in force beside the policy, in the order they were activated, as Engine.GrantSet kept them: the rules it refused are not here, and an untrusted source's allow rules are on Engine.Withheld instead.
func (*Engine) Policy ¶
Policy returns a copy of the policy to persist: the one built, with every grant Engine.Grant and Engine.GrantOver made applied. A grant appends an allow rule; a grant over an ask rule also appends a carve-out beside that rule, or removes it, so what a product writes back into its settings is all here.
The scoped rule sets Engine.GrantSet activated are not here, and Engine.Grants reports those: they last as long as the source that carries them, a skill in use rather than a settings file, and an engine rebuilt from this policy decides as this one does once they are revoked.
func (*Engine) PolicyOf ¶ added in v0.0.3
PolicyOf returns the rules of one source, as a RuleSet carrying that source, which is what a product writes back into that source's settings file. The whole policy is not: it holds the rules of every file that was merged, so a product that persisted it would copy the managed and project files into its own local settings and freeze a snapshot of somebody else's.
The rules are the ones in force, so a grant this source made is here, carve-out and all, and a rule another source contributed is not. A source with no rules gives an empty set whose Source is named from Engine.Sources when the merge listed it. The scoped sets Engine.GrantSet activated are not here: they are not a product's to persist.
func (*Engine) Release ¶ added in v0.0.2
func (e *Engine) Release(ctx context.Context, end *agentturn.RunEnd, answers ...agentturn.Answer) ([]agentturn.Answer, error)
Release answers the calls end left pending that the engine held for an ask, given the answers to the calls it asked about, and returns every answer for agentturn's Resume in the order of end.Pending, the model's order, which is the order a sequential batch runs in. The policy allowed a held call, so it is approved and runs with the batch, unless an answer ends the run, one built with agentturn.Refuse, in which case the held call is answered with a refusal the model reads, since the turn is over. A held call the given answers already cover keeps its answer, and an answer for a call that is not pending is returned after the rest. Every release is a Verdict through the observer with Held set: Allow with the rule that allowed the call, or Block when the turn was stopped.
A pending call neither the caller nor the engine answers is ErrUnanswered, returned with the answers and naming the call, since Resume would otherwise fail with the loop's own error and nothing would say which call was missed. It is what a front sees when it answers the wrong run, or when another hook deferred a call the engine never saw.
The calls it answers are forgotten: the run's entry is dropped as its last deferred call is answered, so an engine shared by several agents does not grow with the runs they finish.
func (*Engine) Removes ¶ added in v0.0.3
Removes reports the deny rule that takes the tool out of the request, and whether one does. A deny rule with no specifier names the tool itself and so denies every call of it, which both references answer by never offering the tool: the model does not see it, plans nothing around it and spends no tokens being refused. A deny rule with a specifier denies some calls and leaves the tool offered.
It is the engine's own tool-name test, so a front that lists what a policy withholds and the list the model is offered cannot disagree.
func (*Engine) ResetReviews ¶
func (e *Engine) ResetReviews()
ResetReviews forgets the refusals the denial bound counts, as Codex does at each new user message. A front calls it before the prompt that starts a new turn.
func (*Engine) Revoke ¶ added in v0.0.3
Revoke removes the rules a source granted and returns how many rules it removed, so a product ends a turn-scoped grant at the turn boundary, as a skill's allowed-tools "clears when you send your next message". It touches neither the policy Build was given nor the rules Engine.Grant and Engine.GrantOver added, which are the always-allow journal and outlive a turn.
func (*Engine) Runs ¶ added in v0.0.3
Runs returns the runs the engine is holding deferred calls for, so a product can see what it has not answered.
func (*Engine) SetPolicy ¶ added in v0.0.3
SetPolicy replaces the policy the engine decides with, validated as Build validates one and expanded through the same aliases. The matchers are not rebuilt, since they are the expensive half and they do not change, and the hook values the loop holds keep working, so a product re-derives its rules without replacing its config, which agentturn refuses while a run is active.
It is what a tool list that changes needs. An engine is built from a snapshot of that list, so a tool an MCP server announces mid-session is a tool the policy never heard of: an Ask() default defers every call to it and an unattended run parks on an approval nobody will give. A product that cannot re-derive a policy writes rules that cover the tools it has not seen instead: a bare name, or a tool-name glob in the deny or ask list, "mcp__*", governs a tool that appears later, where an allow rule needs the tool's own name.
The policy in force changes for the next call decided, not for the calls already decided: a run that ended on an ask is answered under the rules that deferred it, and a batch being decided as the policy changes can see both, so a product that must not straddle one replaces the policy between turns. The scoped grants Engine.GrantSet activated, the deferred calls and the review log are untouched. The replacement is not journalled; a product records it where it derives the policy.
func (*Engine) Sources ¶
Sources returns the sources the policy was merged from, as Merge listed them, so the session can name the policy in force.
func (*Engine) ToolProvider ¶ added in v0.0.3
func (e *Engine) ToolProvider(base func(context.Context) []agenttool.Tool) func(context.Context) []agenttool.Tool
ToolProvider returns the hook value for agentturn.Config. ToolProvider: base's tools with the ones the policy removes taken out. The loop consults it once per turn, before the model call, so a deny added by Engine.SetPolicy or a grant takes the tool away on the next turn and a tool a server announces mid-session is filtered as it appears.
base is a provider rather than a list because the list is what changes; a product whose list is fixed passes one that returns it, or filters it once with Engine.Filter and sets Config.Tools.
func (*Engine) Withheld ¶ added in v0.0.3
Withheld returns the allow rules withheld from the untrusted sources: those the merge withheld, and those of the rule sets Engine.GrantSet activated from a source the user has not trusted. The engine never consults them: they are what a front shows when it asks whether to trust a folder or a skill, so the user reads what trusting it would allow rather than agreeing blind. Trusting a source means merging or granting again with Source.Trusted set.
type Matcher ¶
type Matcher func(spec string, args json.RawMessage) bool
Matcher decides whether a specifier matches one subject's arguments. A product registers one per tool that takes specifiers; a rule with a specifier for a tool without a matcher is an error at Build, not a silent non-match.
func PrefixMatcher ¶
PrefixMatcher is the common case: the specifier is "<prefix>:*", matched as a prefix of one string field of the arguments, or any other value, matched exactly. A missing field, or one that is not a string, matches nothing.
type Option ¶
type Option func(*Engine)
Option configures an Engine.
func WithAliases ¶ added in v0.0.3
WithAliases names the tools a rule name governs, for the rules a product does not write itself: a settings file copied out of the reference's documentation, and a skill's allowed-tools, are spelled with the reference's tool names, "Bash", "Read", "Edit", and a product's tools are named whatever the product named them. The reference also has one rule name govern several tools, Read reaching its search tools as well as its file one, which nothing else here can express.
agentpolicy.WithAliases(map[string][]string{
"Bash": {"bash"},
"Read": {"read", "grep", "glob"},
"Edit": {"edit", "write"},
})
Build expands a rule whose name has an entry into one rule per tool it names, keeping the specifier and the source, so the expansion happens once, where the matchers already are, and Engine.Policy reports the rules as they are evaluated. A name with no entry is a tool's own name and still fails closed: a rule with a specifier for a tool with no matcher does not build. Expansion is one level: a tool an alias names is not itself expanded. Nothing folds case, and an entry that names no tool does not build.
func WithDenialBound ¶
func WithDenialBound(b DenialBound) Option
WithDenialBound replaces DefaultDenialBound.
func WithObserver ¶
WithObserver registers fn to receive every verdict the engine produces: each decision, each grant and each reviewer answer, exactly once, outside the engine's lock. A hook cannot append to the transcript, so this is how a verdict reaches the session.
type Outcome ¶
type Outcome int
Outcome is a reviewer's answer to a deferred call. The zero value is Refused, so a Review left unset refuses the call.
const ( // Refused answers the call with a refusal the model reads. Refused Outcome = iota // Approved runs the call, with Review.Args in place of the model's // arguments when set. Approved // TimedOut means the reviewer gave no answer in time. The call does // not run, and the record says why, distinct from a refusal. TimedOut )
type Policy ¶
type Policy struct {
Allow []Rule
Deny []Rule
Ask []Rule
// Default applies to a subject no rule matches. It must be set.
Default Default
// Sources lists where the rules came from, as [Merge] fills it,
// so the session can name the policy in force. A policy built by
// hand may leave it nil.
Sources []Source
// Withheld are the allow rules of the untrusted sources, stamped
// with their source, in the order [Merge] saw them. They are not
// evaluated: nothing in the engine reads this list, and a rebuild
// of the policy with the source trusted is what applies them. A
// front reads them to tell the user what trusting a folder would
// allow, which is the reference's whole workflow around trust.
Withheld []Rule
}
Policy is the rule set: what is allowed, what is denied, what needs approval, and what happens otherwise. Precedence between the lists is fixed: deny, then ask, then allow, then Default.
func AutoEdit ¶
AutoEdit approximates Codex's middle mode: reads and edits run, commands ask, and so does a tool the split does not name. The reference confines the edits to the workspace, which is the product's to arrange.
func FullAuto ¶
FullAuto approximates Codex's unattended mode: every tool the split names runs without asking, and the confinement is the sandbox's, which is the product's to arrange. A tool the split does not name still asks, so a tool added later by a plugin does not run unseen.
func Merge ¶
Merge unions the lists of several sources into one Policy, as the settings files of an organisation, a repository and a user combine. The sets are ordered by Rank, highest first, so the most authoritative rule is the one a verdict names; every rule is stamped with its set's Source, which is what scopes a carve-out; and the allow rules of an untrusted source are withheld, since they grant capability, while its deny and ask rules apply at once, since they only restrict. Precedence between the lists does not depend on the source. Default is left unset for the product to choose, and every source is listed on the policy, trusted or not.
A withheld allow rule is kept on Policy.Withheld rather than dropped, so a front can tell the user what trusting a folder would allow. Nothing evaluates that list.
Each source must be named, and no two may share a name, since the name is what scopes a grant and names the file a product persists its own rules into. A product merging several skills names each one, "skill:commit", and the error says so; a skill's rules more often belong in Engine.GrantSet, which keys them by source and takes them back at the turn boundary, than in the policy every run is built from.
type Refusal ¶ added in v0.0.3
Refusal is one rule a grant could not activate, with the stable text that says why, so a front can show the user what a skill asked for and what it did not get, as it drops the "always allow" option when Engine.GrantOver refuses.
type Review ¶
type Review struct {
Outcome Outcome
// Reason is shown to the model on a refusal and recorded on every
// outcome.
Reason string
// Args, for an approval, replaces the arguments the tool receives.
// nil keeps the call's own.
Args json.RawMessage
// Note is what the reviewer tells the model with the result, on an
// approval or a refusal: the loop appends it after the outputs as a
// user message, so the model reads the result and the note
// together, as a user's note on an approval reaches it.
Note string
// By names who answered, for the record: [ByAgent] for a model,
// which is what the classify package's reviewer sets, [ByHuman]
// for a person a reviewer asked on the front's behalf. Empty is
// read as ByAgent, since a Reviewer answers where a human would.
// [Engine.Answers] puts it on the verdict, and it is what the
// answer itself will carry once agentturn's Answer names a
// decider; the engine's own fail-closed answers, a timeout and a
// review that failed, are [ByPolicy].
By string
}
Review is a reviewer's answer.
type Reviewer ¶
type Reviewer interface {
Review(ctx context.Context, info agentturn.ToolCallInfo, v Verdict) (Review, error)
}
Reviewer answers a deferred call without a human: Codex's auto-review, a rule of the product's, a front that asks a person, or a test double. It receives the call as the hook saw it and the verdict that deferred it, and says who answered through Review.By. A model-backed one lives in the classify package; the engine never calls a model itself.
type ReviewerFunc ¶
ReviewerFunc adapts a function to Reviewer.
func (ReviewerFunc) Review ¶
func (f ReviewerFunc) Review(ctx context.Context, info agentturn.ToolCallInfo, v Verdict) (Review, error)
Review calls f.
type Rule ¶
type Rule struct {
Tool string
// Spec is the text inside the parentheses, "" for a bare name.
Spec string
// Source is where the rule came from. The zero Source is a rule
// the product built itself.
Source Source
}
Rule is one token of the grammar: a tool name, or a name with a specifier in parentheses, "Bash(git:*)". It is the grammar of a skill's allowed-tools, so agentskill.ToolRule maps onto it field for field without either module importing the other. What a specifier means belongs to the tool's Matcher; the grammar knows one thing about it, that a specifier beginning with "!" is a carve-out.
func ParseRules ¶
ParseRules parses the grammar: tokens separated by whitespace, each a tool name or a name followed by a specifier in parentheses. Only whitespace outside parentheses separates tokens, so a specifier may contain spaces, "Bash(git status:*)", and parentheses inside a specifier are literal as long as they balance, "Edit(./Finance (2024)/**)". An empty string yields no rules and no error. The rules have no Source.
func (Rule) Bare ¶
Bare reports whether the rule names a tool without a specifier, so it matches every call of the tool.
func (Rule) CarveOut ¶
CarveOut returns the pattern of a carve-out, a specifier beginning with "!", and whether the rule is one. A carve-out never matches on its own; it cancels a match of another rule in the same list, for the same tool, from a source it does not rank below, when its pattern matches the subject. The pattern is the tool's to interpret, as any specifier is.
func (Rule) Glob ¶ added in v0.0.3
Glob reports whether the rule's tool name is a glob over tool names rather than one tool's own name, "mcp__*" for every tool of every MCP server. A glob is honoured in the deny and ask lists, where both references honour one, and Build refuses it in the allow list and refuses it with a specifier, since a glob names no matcher.
func (Rule) MatchesTool ¶ added in v0.0.3
MatchesTool reports whether the rule's tool name names the tool: the same name, or a glob that matches it, where "*" stands for any run of characters at any position. Nothing folds case: the reference documents case sensitivity for almost nothing, so guessing here would replace a loud failure with a quiet one. It is the engine's own test, exported so a product that offers the model a tool list does not write a second one that can disagree.
type Source ¶
Source is where a set of rules came from: a settings file, a skill, the session. Name identifies the source, keys a scoped grant and names the file a product persists its own rules into. Path and Hash let a session name the policy in force. Trusted false withholds the source's allow rules in Merge, as a repository's settings wait for the user to trust the folder while its deny and ask rules apply at once; a Source built by hand is untrusted until it says otherwise. Rank orders sources by authority, higher first: a grant may answer an ask rule only from a source of equal or lower rank, a carve-out cancels a rule only of a source it does not rank below, and a grant set's allow rules shadow only the ask rules it does not rank below. Rank never affects precedence between lists; deny before ask before allow holds whatever the sources.
type Subject ¶
type Subject struct {
// Args is the subject as the matchers see it.
Args json.RawMessage
// Tool, when set, evaluates the subject against another tool's
// rules, as a shell redirect target is checked against the file
// tool's rules. Empty means the tool that was called.
Tool string
// Text is what a prompt shows for this subject.
Text string
}
Subject is one thing the policy is evaluated against. A call is one subject, its own arguments, unless the tool's Subjects splits it.
type Subjects ¶
type Subjects func(args json.RawMessage) ([]Subject, error)
Subjects splits one call into its subjects, after normalisation. A shell tool splits a command on its operators and strips wrappers; a file tool resolves its path; most tools are one subject, the call itself, and register no splitter. The splitter is the product's; the root package holds no shell parser. An error fails closed: the call is blocked with the error as the reason.
type ToolMatcher ¶
type ToolMatcher struct {
Match Matcher
// Subjects is nil for a tool that is one subject per call.
Subjects Subjects
}
ToolMatcher is what a product registers for a tool: how a specifier is matched and, when one call is several subjects, how it splits.
type Tools ¶
Tools is a tool set split the way the presets need it: the tools that read, the tools that edit, and the tools that execute. The split is the product's; the presets only name the tools.
type Verdict ¶
type Verdict struct {
RunID string
Turn int
// CallID and Tool name the call decided. They are empty for a
// content verdict; a grant names the rule's tool.
CallID string
Tool string
// Guard names the guard that decided content; empty otherwise.
Guard string
// Action is Allow, Block or Defer.
Action agentturn.ToolAction
// Rule is the rule that fired, or the rule granted. It is nil when
// the default applied and for a guard's or a reviewer's verdict.
Rule *Rule
// Reason is the stable text behind the action, the same text the
// model reads when the action blocks a call.
Reason string
// By names who decided, in the session format's words: [ByPolicy]
// for a rule the engine evaluated on its own, [ByAgent] for a
// reviewer that is a model, [ByHuman] for a person a front asked.
// It is what a product writes as the decision's decider when it
// records the verdict, and it is empty for a guard's verdict,
// whose decider is the guard that Guard names.
By string
// Held is set on a Defer for a call the policy allowed but holds
// because another call of its batch asks, and on the verdict that
// releases or refuses it once the ask is answered.
Held bool
// Subject is the Text of the subject whose verdict the fold kept,
// the half of a compound command that raised the question, as the
// tool's Subjects splitter wrote it: a prompt says which part of a
// command line it is asking about with it, and the reason names
// the rule, the two read together. It is empty for a call no
// splitter split, since the splitter is what writes the text, and
// for a splitter that writes none.
Subject string
}
Verdict is what was decided and why, for recording: the engine's decision on a call, a hold and its release, a grant, a guard's verdict on content, or a reviewer's answer to a deferred call. The loop's recorder writes the decision the hook returned as the call's decision entry; the verdict carries what that entry cannot, the rule above all, and a product writes it beside the decision as a custom entry under agentpolicy.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package classify backs a guard and a reviewer with a model: the subject, content or a deferred tool call, is sent to the model with a rubric, and the model's structured answer becomes the verdict.
|
Package classify backs a guard and a reviewer with a model: the subject, content or a deferred tool call, is sent to the model with a rubric, and the model's structured answer becomes the verdict. |
|
Package guard decides what may enter or leave an agent's window: a contract for a check over content, deterministic checks that ship here, and the hook values that run them, BeforeModelCall over the request's input, OutputGuard over each assistant message as the stream completes it and ShouldStopAfterTurn over a finished turn.
|
Package guard decides what may enter or leave an agent's window: a contract for a check over content, deterministic checks that ship here, and the hook values that run them, BeforeModelCall over the request's input, OutputGuard over each assistant message as the stream completes it and ShouldStopAfterTurn over a finished turn. |