Interbolt
Guides

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-agent

Binds 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_approval

interbolt 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: block

Every 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_approval above is unconditional for billing-agent, since it is a declared member of payer.
  • 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-eliminated to 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 payer

Binds 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_payment

The 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.

On this page