Explain
Answering "what can this agent do" with interbolt explain instead of reading every rule by hand.
Explain
Once rules condition on agent.id/agent.groups (see
Per-agent carve-outs and
Group membership),
answering "what can support-agent actually do" means reading every rule in
every sink and mentally resolving group membership against first-match-wins
order. interbolt explain does that resolution for you.
It is a static command, on the same footing as validate: it loads and
compiles a policy file, imports no consumer code, executes no agent, and
makes no network call. Unlike validate, it never fails a build: it always
exits 0, since it answers a question rather than gating one.
interbolt explain --agent
interbolt explain policy.yaml --agent billing-agentBinds agent.id to the given value and agent.groups to that agent's
declared membership (an id absent from the policy's agents: section
resolves to no groups, not an error, the same open-world rule
agent.groups follows everywhere else), then partially evaluates every rule
in every sink against that one binding. Given:
agents:
billing-agent:
groups: [payer, internal]
sinks:
payments.send_payment:
rules:
- name: payers_need_approval
when: agent.groups.exists(g, g == "payer")
action: require_approval
- name: billing_agent_blocked
when: agent.id == "billing-agent"
action: block
- name: taint_gate
when: taint.exists(t, t.trust == "untrusted")
action: require_approvalinterbolt explain policy.yaml --agent billing-agent --show-eliminated prints:
billing-agent (groups: internal, payer)
payments.send_payment
require_approval payers_need_approval (unconditional)
billing_agent_blocked (eliminated, shadowed by 'payers_need_approval' ('billing-agent' is a member of group 'payer'))
taint_gate (eliminated, shadowed by 'payers_need_approval' ('billing-agent' is a member of group 'payer'))
default: blockEvery rule resolves to one of three outcomes:
- Unconditional. The identity condition is the entire rule and it is
true for this agent: the rule always fires, and (first-match-wins) every
rule after it in that sink is dead for this agent.
payers_need_approvalabove is unconditional forbilling-agent, since it is a declared member ofpayer. - Eliminated. The rule's own identity condition is false for this agent,
or an earlier unconditional rule already shadows it. Hidden by default
(this is usually the bulk of a large policy); pass
--show-eliminatedto see them, dimmed, with the shadowing rule and the membership fact that causes it named, the same shape Identity shadowing's own message uses. - Conditional. Something the command cannot resolve remains (
taint,args,run, or an unrecognized shape), printed as the residual condition. Identity combined into a disjunction with one of these is reported conditional, not eliminated, even when the identity leg alone would settle it: the other side of the||might still hold, so the command never over-claims reachability.
The effective default for each sink (defaults.sink_action, or whatever
a sink falls through to) is always printed, since that is what actually
applies once every declared rule is accounted for.
An agent id with no entry in agents: is a valid, useful query, not an
error. It is exactly what shows a misconfigured or newly deployed agent's
real access:
$ interbolt explain policy.yaml --agent some-typo
some-typo (groups: none)
...Partial shadowing: why this exists alongside the lint
Identity shadowing
proves a rule unreachable across the entire declared agent universe, and
runs in validate/CI. It is restricted to rules built purely from
agent.id/agent.groups, and it has a stated blind spot: partial
shadowing, where a group rule shadows an id rule for some members of the
group and not others. That is not provable unreachability (it is often
intentional), so the lint leaves it out rather than widening itself into
heuristics.
explain --agent is what answers it: the same policy can report a rule
eliminated for one member and conditional/unconditional for another,
since it resolves reachability for one concrete agent rather than proving it
once for everyone. That is the case the lint does not, and will not, cover.
interbolt explain --group
interbolt explain policy.yaml --group payerBinds agent.groups to contain the given group and leaves agent.id
unbound, the more common review question once groups are in place, since
capabilities are usually discussed by group rather than by named agent.
A rule keyed on a specific id cannot be resolved either way without knowing
which member of the group is acting, so it comes back conditional with a
label distinct from an ordinary runtime-conditional rule. Given
billing_agent_blocked (agent.id == "billing-agent") ordered before
payers_need_approval in the same sink:
group payer
payments.send_payment
block billing_agent_blocked (depends on which member: agent.id == "billing-agent")
require_approval payers_need_approval (unconditional)
default: block"Depends on which member" is not the same "conditional" a genuine
taint/args rule gets. Those are two different kinds of "cannot resolve
this": one resolves the moment you pick a concrete --agent, the other never
resolves no matter which agent you pick. The label keeps them from reading
as equally vague.
interbolt explain --tool
interbolt explain policy.yaml --tool payments.send_paymentThe inverse question ("who can reach this sink") without attempting reachability analysis. The output opens with the tool's declared capabilities, or an explicit statement that none are declared, since an absent declaration means no trifecta legs are counted for that tool. It then lists every agent id and group literally mentioned anywhere in the sink's rules, plus the sink's effective default, with a reminder that an agent mentioned nowhere still falls through to that default:
payments.send_payment
capabilities: reaches_external
require_approval payers_need_approval (groups: payer)
block billing_agent_blocked (agent ids: billing-agent)
require_approval taint_gate (no identity reference)
default: block (undeclared agents fall through to this)A tool whose entry declares no capabilities: key, or declares an empty
list, prints capabilities: none declared in that first line instead.
Golden files in CI
Since explain always exits 0, it is not a gate on its own, but its
output is diffable, and that diff is the only reviewable signal for a
class of change groups introduce: adding an agent to a group changes what
fires for it with no rule edit at all, so a plain git diff on the policy
file no longer shows the behavior change. Commit explain's output per
agent as a golden file, and fail the build when it changes without review:
interbolt explain policy.yaml --agent billing-agent > golden/billing-agent.txt
git diff --exit-code golden/Pair this with interbolt validate in the same job: validate gates
on the policy being well-formed, the golden-file diff gates on a specific
agent's behavior actually staying reviewed.
Limits
Only the recognized agent.id ==/!= and agent.groups.exists(...) shapes
(and their negations, combined with &&/||) are resolved, the same six
shapes Identity shadowing
recognizes. Anything else is symbolic. Identity inside a disjunction that
does not resolve true is always reported conditional, never eliminated, since
the command is conservative by design: it would rather under-claim
reachability than risk telling you a rule is dead when it might still fire.