Skip to main content
Policy decides which effects an agent may perform. Each agent has its own rule bundle, evaluated the first time a step is submitted. A rule returns allow, deny, or require_approval. A replayed step returns its recorded outcome without consulting policy again, so editing a bundle does not re-gate work an execution has already done. Attach a bundle inline with policy: or from a file with policy_file: in a provisioning manifest. Set one or the other, not both, and write the policy_file path relative to the manifest. You can also load a bundle over the admin API with POST /v0/policies/{agent_id}. The kernel compiles a bundle before storing it, so a malformed one is rejected with a validation error at registration rather than failing later at a step. An agent with no bundle is unrestricted, in production as much as in dev.

Bundle format

Evaluation

Rules are evaluated top to bottom in the order they appear in the bundle. The first rule whose when block matches wins, so put the specific rules above the broad ones. If nothing matches, default_action applies, and any value other than allow means deny. Every rule needs an id, unique within the bundle. Unknown keys are rejected, so a bundle carrying a key no rule field claims will not load. default_action covers tool_call and llm_call steps. An unmatched local step is allowed whatever default_action says. To govern one, match it with a rule on target or step_kind: local.

Match conditions

All fields present in a rule’s when block must match (AND). Omitted fields are not checked.

Argument predicates

arguments is a map of argument key to predicate. The key must be present in the call’s arguments, and every constraint listed under it must pass. Values are compared as strings. A predicate needs at least one constraint. An empty one (command: {}, or command: {equals: ""}) is rejected at load, because a constraint that matches any value would silently widen the rule.

Rule decisions

A rule’s then block carries the decision and its options: Every policy decision event (step.allowed, step.denied, step.awaiting_approval) carries the matched rule_id in its payload, which is what makes the log auditable. rule_id is the matched rule’s own id and is not settable from the bundle. A decision that came from somewhere other than a rule gets a fixed id. The default_action fallthrough is default, an unmatched local step is local, an agent with no bundle is permissive, and a stored bundle that will not compile is bundle-error.

Approvals

When a rule returns require_approval, the kernel records step.awaiting_approval and approval.requested, creates an approval, and moves the execution to blocked. A human grants or denies it through the approvals API and the execution resumes. See Events and Approvals.
approvers is a guardrail rather than access control. decided_by is a string in the request body, and the bearer token is shared and carries no identity, so the check stops the wrong person deciding by accident but not someone willing to type another person’s name. Enforcing it properly needs decided_by to come from an authenticated principal. Do not rely on approvers to keep a decision away from a caller who already holds the API token.

Rate limits

A rule can cap how often it fires. The bucket is keyed on the rule’s rule_id and the scope in per_what, so two rules never share one.
A step over the limit comes back as rate_limited rather than a policy denial. Put a hard ceiling in a deny or require_approval rule instead. With max_wait set, a limited step parks. The submit returns blocked, the execution stays running, and the kernel re-dispatches once the bucket refills. An execution parks only once. If the step is still over the limit on the retry, or the wait would run longer than max_wait, it is refused.

Token budgets

A rule can cap the LLM tokens an execution spends. The cap applies only to a rule that decides allow.
The meter sums the input and output tokens recorded on the execution’s steps, so an effect counts once however many times it was attempted. Only llm_call steps record usage, so nothing else moves the meter. The check runs before the call, so the step that crosses the limit still runs. A response with no parseable usage never advances the meter, most often an OpenAI-style stream requested without stream_options.include_usage, and rebuno_llm_usage_missing_total counts those. If the kernel cannot read the execution’s usage at all, it lets the step through.

Testing a bundle

rebuno policy test evaluates a bundle against cases and exits non-zero when a decision does not match what the case expects.
Cases live beside the bundle, shell.yaml with shell.policytest.yaml, or wherever --cases points.
target is required and kind defaults to tool_call. expect is the decision the case must produce and expect_rule the rule that must make it, which catches a case still decided correctly but by the wrong rule. Omit both to assert nothing. Unknown keys are rejected. A run also lists the rules no case reached, which is how a rule shadowed by a broader one above it shows up. --target <name> --args '{...}' probes a single input instead of running cases, printing its decision and rule. A finished execution is the other source of cases. Replaying one feeds its recorded steps back through a bundle and fails the steps a change would now decide differently. The steps come from a running kernel, so replay names an agent with --agent-id and reaches a kernel the way every command does (CLI), or calls POST /v0/policies/{agent_id}/test directly.

Examples

Deny by default, allow only known tools:
Allow safe shell commands, gate the rest on approval:
See examples/policies/shell.yaml and examples/rebuno.dev.yaml for working bundles.