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.
A key with dots is a path into nested objects, so config.environment reads environment inside the config object. The rule does not match when a segment is missing or its value is not an object; arrays are not traversed. An argument whose own name contains a dot takes precedence over the path it spells.
A value that is not a string, such as a number or an object, is compared as compact JSON text: {"force": true} compares as {"force":true}.

History conditions

history is a list of clauses about the steps policy allowed earlier: steps that started, whether they succeeded, failed, or are still running. Every clause must hold.
The call being decided counts toward a clause it matches, unless the clause has result. A clause with at_most: 3 on its own target allows the third call and refuses the fourth, and a sum includes the amount being requested. Steps still running count, so parallel calls cannot pass a cap before earlier ones finish. Denied steps and steps awaiting approval do not count. A clause with the default bound requires that something happened; at_most: 0 requires that it did not. Either can carry the decision:
A selected step whose sum argument is missing or not a number fails the clause. Write a cap as an allow rule above a stricter one, so such a step falls through to the stricter rule:
If the kernel cannot read history, the step is denied with reason history_unavailable under the rule being evaluated. The client names the session, so a new session starts with empty history. A session-scoped count or sum therefore limits a cooperating client; for a hard ceiling, use a rate limit with per_what: agent or global. A clause that requires something to have happened denies in a new session.

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.
decided_by is the authenticated caller: admin for the deployment token, service:<key id> for an API key, or user:<claim> for an OIDC user. A caller holding the approvals:delegate scope may name the approver instead, and the approval records that caller in asserted_by. approvers entries match decided_by exactly, and a group:<name> entry matches an OIDC user in that group. A name supplied through approvals:delegate matches only exact entries.

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.
per_what: session shares the bucket across a session’s executions, and falls back to the execution for one without a session. per_what: subject shares it across every execution acting for one subject, and falls back the same way. 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 or a session 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. With scope: session it sums every execution in the session, including failed ones, and falls back to the execution for one without a session. 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.

Judge

A rule that decides judge hands the call to a decision model, which picks allow, deny, or require_approval. It suits tools whose calls are too varied to list as predicates, such as a shell. Text in the arguments can steer the model, so put deny rules for commands known to be dangerous above it.
Without question, the model answers: “Should tool_call run automatically, be blocked, or wait for a human to approve it?” The default criteria are: The model is any HTTP API that returns a probability for each decision. A YAML file named by REBUNO_JUDGE_CONFIG (Deployment) describes the request and where the probabilities are in the response. Without it, judge rules deny.
The body template receives: examples/judges holds provider configs.

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. subject and subject_groups set the execution’s subject for subjects conditions; replayed executions supply their own. 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. history lists the allowed steps a case sees in every scope. Each takes target, kind, args, result, and ago, a Go duration before the decision:
--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. Each replayed step sees the history of its execution and session as it stood when it was decided.
--session, --since, and --limit replay many of the agent’s executions at once, newest first, 100 by default and at most 1000:

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.