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 whosewhen 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’swhen 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’sthen 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 returnsrequire_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’srule_id
and the scope in per_what, so two rules never share one.
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 decidesallow.
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.
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:examples/policies/shell.yaml and
examples/rebuno.dev.yaml for working bundles.