Multi-Modal Policy — IR Taxonomy¶
Note
Status — Phase 1 design proposal (epic #747)
This page documents the planned Phase 1 IR shape. The
policy/ir/ package, ir.schema.json, and the property-test suite
referenced below do not exist in the repository yet — child PR
C02 lands them; C03 lands the YAML DSL parser + IR → Rego compiler.
Treat every package path, file name, and test name on this page as a
target to land, not a present-day fact.
This page exists in C01 so subsequent PRs have a single design contract to implement against and a single doc to update if the contract changes during implementation.
This page catalogs the planned Phase 1 shape of the policy intermediate representation (IR). The IR is the contract between authoring modality and runtime evaluation — every non-Rego frontend will produce it; the Rego backend will read it. See the parent Architecture for the design discipline that bounds catalog growth.
What the IR represents¶
A compiled IR program is a closed list of Rules plus a top-level
default outcome. Each Rule has an effect (allow or deny in
Phase 1; Phase 2 adds audit_only / require_approval /
rate_limited / sandboxed), a target tool kind, and a when predicate.
The runtime evaluator iterates the rules in document order; the first
match wins; an empty match-set falls through to the default outcome.
Match precedence is deliberately first-match, not most-specific. The catalog page on the IR test suite pins it.
The Phase 1 IR shape¶
C02 will define the IR in a new policy/ir/ package as a closed set
of Go types. The JSON schema at policy/ir/ir.schema.json will be
the canonical wire form; the Go types will be the consumer surface.
Neither exists today — the shape below is the contract C02’s PR will
implement against.
Program
├── default: Outcome — "allow" | "deny" (Phase 1)
├── version: string — IR schema version, e.g. "1.0.0"
└── rules: []Rule
Rule
├── id: string — operator-supplied; surfaces in matched_rule.id
├── effect: Outcome
├── tool: string — required: the tool.kind this rule targets
├── when: Predicate — optional; absent ⇒ unconditional match
└── source: SourceRef — {file, line, column} for the frontend's view
Predicate
├── op: PredicateOp — "eq" | "ne" | "in" | "and" | "or" | "not"
├── field: string — for leaf ops; references a known input field
├── value: any — leaf comparand
└── children: []Predicate — for compound ops (and/or/not)
Phase 1 predicate operators¶
|
Arity |
Meaning |
|---|---|---|
|
leaf |
|
|
leaf |
|
|
leaf |
|
|
compound |
every child matches |
|
compound |
some child matches |
|
compound |
single child does not match |
field is a dotted path into the Rego input document — the same
shape the existing Rego evaluator already exposes today. The IR does
not introduce a separate input schema; it READS the input the runtime
adapter populates. Concretely:
field: "kind"readsinput.kind— the tool kind.field: "params.altitude_m"readsinput.params.altitude_m— a caller-supplied param.field: "preflight.outcome"readsinput.preflight.outcome— the ContractRefine (C07 #1027) resolved preflight verdict, present when a binding has been resolved. When the binding is absent,input.preflightis undefined and the leaf predicate evaluates tofalse(the rule body fails — same shape asinput.preflight.outcome == "pass"failing in Rego). This is the C07 “absent ≠ true” contract pinned byTestEval_PreflightAbsent_RuleRequiringItDenies. An operator who instead wants to allow when the binding is absent (the inverse case) writesnot input.preflightin Rego directly; Phase 1 of the IR does not provide a sugar for thenot-presentcase (a Phase 2 predicate op would land it).
The path traversal MUST match Rego’s input.<dot>.<dot> access
semantics so a YAML/JSON-authored rule expressing
input.preflight.outcome == "pass" compiles to a Rego predicate
identical (modulo formatting) to the hand-written equivalent. The
C03 property test that pins IR → Rego equivalence covers
preflight-bound paths in its fixture set.
The set is deliberately small. Phase 1 ships predicates that map 1:1 to
Rego primitives the existing evaluator already understands — no string
prefix matching, no regex, no numeric comparisons. Phase 2 widens the set
when the context-aware inputs require it (e.g. classification ordering
needs lt / lte).
What the IR is NOT¶
No imperative control flow. No loops, no recursion, no conditionals beyond the predicate tree.
No mutable state. The IR is a pure expression tree.
No external calls. Predicates read only from the input. Producers (HTTP fetches, file reads, OPA built-ins beyond comparison) are explicitly forbidden — the runtime already wraps Rego with the same contract.
No frontend leakage. A Rule’s
sourcecarries provenance for explainability but the IR does not encode the originating syntax. A YAML rule and a JSON rule that produce identical IR compile to identical Rego.
How frontends compile into the IR¶
Each Phase 1 frontend produces IR via a closed compile path. The compilers are pure functions: same input ⇒ same IR, no clock, no randomness.
YAML DSL → IR (Phase 1)¶
A YAML rule list:
default: deny
version: "1.0.0"
rules:
- id: NET-001
effect: deny
tool: shell.exec
when:
op: eq
field: network
value: public
- id: ROB-007
effect: allow
tool: robot.move
Compiles into the IR Program directly. The compiler:
Validates the
versionfield againstpolicy/ir.SupportedVersions.Resolves every
toolto a non-empty string.Validates the predicate tree shape (op + arity).
Stamps
source.file+source.lineon every Rule from the YAML document position.
Canonical preflight gate (ContractRefine integration)¶
The ContractRefine epic #1020 ships
policy.BundleManifest.RequiresPreflight
(manifest-layer binding gate) plus input.preflight (Rego rules read
the verified PreflightDecision). YAML-authored bundles MUST be able to
express the same input.preflight.outcome == "pass" gating Rego
authors write today. The canonical YAML form:
default: deny
version: "1.0.0"
rules:
- id: REQ-PASS-001
effect: allow
tool: tool.echo
when:
op: eq
field: preflight.outcome
value: pass
The IR compiler lowers this to a Rego predicate equivalent to:
allow if {
input.kind == "tool.echo"
input.preflight.outcome == "pass"
}
The same bundle’s policy.BundleManifest can carry
requires_preflight: true and the runtime gates /v1/tool
identically to a Rego-authored bundle — see
INV-MM-8.
JSON Rules → IR (Phase 1)¶
The JSON shape is the IR schema verbatim (machine-generated bundles emit
the IR JSON directly). The compiler validates against
policy/ir/ir.schema.json and stamps source.file from the input
filename; source.line is left zero for machine-generated bundles.
Natural language → IR (Phase 3)¶
Out of Phase 1 scope. The Phase 3 path produces a draft IR program that
a Rego-fluent owner reviews and approves before it reaches policy build.
Graph / workflow → IR (Phase 3)¶
Out of Phase 1 scope. The graph shape compiles approval-chain semantics
into require_approval rules (a Phase 2 outcome), so the Graph frontend
naturally sequences after the Phase 2 outcome work.
How the IR compiles to Rego (Phase 1 backend)¶
The IR → Rego compiler is the Phase 1 backend. It emits a single
autonomy.rego file whose data.autonomy.allow predicate evaluates to
the same boolean a hand-written equivalent would. The generated Rego
shape is:
package autonomy
import rego.v1
default allow := false # mirrors Program.default
allow if {
# one block per allow-effect Rule, in IR order; first match wins
...
}
deny-effect rules become explicit not allow if { ... } guards so the
runtime’s existing fail-closed contract (any non-true → Deny) catches
them via the unchanged
policy/evaluator.go
path.
Determinism¶
The compiler MUST walk the IR program in document order. Generated
Rego identifier names MUST derive from Rule.id (or a deterministic
hash of the rule’s position when id is empty). The output MUST be
byte-identical across runs for the same input — C03 will land a
property test (TestIRToRego_Deterministic in
policy/ir/compiler_test.go) pinning this.
Explainability annotations¶
C03 will emit a structured comment block above each rule body encoding
{rule_id, source_file, source_line}. C05 will widen runtime.Decision
with MatchedRule; the evaluator (via the OPA Rego v1 metadata
annotation surface) will populate it from these comments. See
Architecture § Explainability widening.
Adding to the IR (v2 path)¶
New IR operators or rule fields land via the following discipline:
One operator (or one field) per PR. The op, its semantics, its precedence, and its Rego lowering are reviewed together.
A property test pins the lowering: random IR program containing the new op compiles to Rego that evaluates identically to a hand-written predicate.
A taxonomy table row on this page.
An INV-MM entry on Invariants iff the addition asserts a new safety or correctness property (the predicate-set additions in Phase 2 do, since they widen the field shape input contract).
Catalog growth is bounded by deliberate review pressure — operators ask “what new authoring surface does this enable?” before “is the code clean?”. An operator that already has a Rego analog is an implementation detail of the IR, not a new entry.