Authoring policies in YAML¶
This tutorial walks through the multi-modal policy authoring path landed by epic #747. You’ll author a policy in YAML, compile it to the Rego the runtime evaluator loads, build a deployable bundle, and inspect which rule fired through the telemetry WAL.
What you’ll learn¶
The Phase 1 YAML DSL schema — six predicate operators (
eq,ne,in,and,or,not) over a closed Rule shape.How a YAML rule list compiles to Rego that the existing
policy/evaluator.goloads unchanged.How
Decision.MatchedRulesurfaces the matched rule’s ID + source file:line through the WAL, and howwal inspectlets operators see it.
Prereqs¶
autonomyCLI installed (autonomy --versionshould report a build).A working directory you can write files into.
Step 1 — Author the YAML¶
Save the following as policy.yaml:
version: "1.0.0"
default: deny
rules:
- id: REQ-PASS-001
effect: allow
tool: tool.echo
when:
op: eq
field: preflight.outcome
value: pass
This is the canonical preflight-gate shape from
INV-MM-8:
allow tool.echo only when the action carries a ContractRefine
preflight binding whose outcome is pass. The rule’s id is
operator-supplied; it becomes the explainability anchor downstream.
Schema at a glance¶
Field |
Type |
Required |
Notes |
|---|---|---|---|
|
string |
yes |
Must be |
|
string |
yes |
|
|
string |
yes |
Must be unique across the file. |
|
string |
yes |
|
|
string |
yes |
The tool kind this rule targets. |
|
predicate |
no |
Optional. Absent ⇒ unconditional match for |
The when predicate is a closed tree over eq / ne / in / and /
or / not. See
policy-ir-taxonomy for the
full taxonomy.
Step 2 — Compile to Rego¶
mkdir -p compiled
autonomy policy compile-yaml policy.yaml --out compiled/policy.rego
Print the result:
cat compiled/policy.rego
You’ll see the canonical autonomy package the runtime loads:
package autonomy
import rego.v1
default allow := false
allow := true if {
# Rule REQ-PASS-001 (allow)
input.kind == "tool.echo"
input.preflight.outcome == "pass"
}
matched_rule_id := "REQ-PASS-001" if {
input.kind == "tool.echo"
input.preflight.outcome == "pass"
}
rules_meta := {
"REQ-PASS-001": {"file": "policy.yaml", "line": 4, "column": 5}
}
Two things worth noting:
The first match wins. The compiler emits a Rego
elsechain so rules are evaluated in document order — a leading deny rule shadows a later allow rule that the same input also matches.The
matched_rule_idchain andrules_metamap are the C05 explainability metadata. The runtime evaluator queries them alongsideallowand populatesDecision.MatchedRulewith the rule ID and source position.
Step 3 — Build a bundle with frontend provenance¶
autonomy policy build \
--in compiled \
--out bundle.tar.gz \
--version 1.0.0 \
--name yaml-authored-demo \
--policy-frontend yaml-dsl
--policy-frontend yaml-dsl stamps the inner manifest.json with
"policy_frontend": "yaml-dsl" so downstream provenance audits can
distinguish hand-authored Rego from YAML-compiled Rego. Omit the flag
(or pass --policy-frontend rego) for hand-authored bundles. The
build output names the value it wrote:
policy build: OK
version: 1.0.0
hash: sha256:...
runtime-version:
policy_frontend: yaml-dsl
output: bundle.tar.gz
The matching outer-manifest field lives at schema_version: "1.6" —
see bundle/manifest.go SchemaVersion16.
The cross-check helper bundle.CrossCheckPolicyFrontend(outer, inner)
catches tamper or build-pipeline drift between the two layers.
Step 4 — Evaluate an action against the bundle¶
autonomy policy eval runs an action through the bundle’s evaluator
in-process — no daemon, no network. It’s the operator-friendly way to
see the policy decision (and its explainability metadata) without
spinning up the runtime server.
Evaluate an action that should be denied — tool.echo with no
preflight binding fails the input.preflight.outcome == "pass" gate:
autonomy policy eval --bundle bundle.tar.gz --kind tool.echo
{"outcome":"deny","reason":"policy: deny","kind":"tool.echo","params":{}}
Exit code 1 (deny). No explainability fields — the default outcome
fired because no rule matched. A deny with a populated matched_rule_id
came from an explicit deny rule; this deny came from the fall-through.
Now construct an action that DOES match the rule. The rule’s when
reads input.preflight.outcome — we’d need a real preflight binding
to satisfy it through policy eval. For the purposes of this tutorial,
swap the rule body to a params check that’s reachable via --params:
# policy.yaml — adjusted to use a params field rather than preflight
version: "1.0.0"
default: deny
rules:
- id: REQ-PASS-001
effect: allow
tool: tool.echo
when:
op: eq
field: params.tier
value: pro
Recompile, rebuild, and re-evaluate:
autonomy policy compile-yaml policy.yaml --out compiled/policy.rego
autonomy policy build --in compiled --out bundle.tar.gz --version 1.0.0 \
--policy-frontend yaml-dsl
autonomy policy eval --bundle bundle.tar.gz --kind tool.echo \
--params '{"tier": "pro"}'
{
"outcome": "allow",
"reason": "policy: allow",
"kind": "tool.echo",
"params": {"tier": "pro"},
"matched_rule_id": "REQ-PASS-001",
"matched_rule_file": "policy.yaml",
"matched_rule_line": 4,
"matched_rule_column": 5,
"evaluation_trace": [
"rule \"REQ-PASS-001\" matched on input.kind=\"tool.echo\" (source: policy.yaml:4)"
]
}
The C05 MatchedRule (id + file + line) and C06 evaluation_trace
flow through policy eval’s JSON output verbatim. Operators get
“which rule fired and where it lives in source” without leaving the
CLI.
Step 5 — Trigger an explicit deny and read the remediation hint¶
A deny that comes from an explicit rule (not the default fall-through)
carries a remediation_hint. Add a deny rule and exercise it:
version: "1.0.0"
default: allow
rules:
- id: BLOCK-SHELL
effect: deny
tool: tool.shell
when:
op: eq
field: kind
value: tool.shell
autonomy policy compile-yaml policy.yaml --out compiled/policy.rego
autonomy policy build --in compiled --out bundle.tar.gz --version 1.0.0 \
--policy-frontend yaml-dsl
autonomy policy eval --bundle bundle.tar.gz --kind tool.shell
{
"outcome": "deny",
"reason": "policy: deny",
"kind": "tool.shell",
"params": {},
"matched_rule_id": "BLOCK-SHELL",
"matched_rule_file": "policy.yaml",
"matched_rule_line": 4,
"evaluation_trace": [
"rule \"BLOCK-SHELL\" matched on input.kind=\"tool.shell\" (source: policy.yaml:4)"
],
"remediation_hint": "denied by rule \"BLOCK-SHELL\" at policy.yaml:4"
}
The hint names the rule + file:line so the operator can jump straight to the offending rule. Compare this to the Step 4 default-fallthrough deny which had no hint at all — that distinction is the audit signal.
What’s next¶
Production runtime path: when the bundle is loaded into the runtime server (
autonomy policy load→ in-process activation), the same explainability fields surface on every WAL decision entry the running runtime emits. Inspect them with:autonomy wal inspect --kind autonomy.decision --json
Each JSON line carries the same
matched_rule_id/matched_rule_file/matched_rule_line/evaluation_trace/remediation_hintattrs thepolicy evaloutput above shows.The taxonomy reference: policy-ir-taxonomy.
The invariants the Phase 1 substrate holds: INV-MM-1 through INV-MM-8.
The matching
--with-explainabilityflag onautonomy audit queryis a follow-up; today operators get explainability viapolicy eval’s JSON output (this tutorial) or the WAL entries the runtime emits.
Troubleshooting¶
policy compile-yaml: duplicate rule id “X”: Rule IDs must be unique across the file. The error names both indices so you can locate the typo.policy evaloutput has nomatched_rule_*keys: either the bundle is hand-authored Rego (norules_metablock) or the evaluation hit the default outcome (no rule matched). Confirm--policy-frontend yaml-dslwas passed topolicy buildand the--kind+--paramsactually satisfy a rule’swhen.Deny outcome with empty
remediation_hint: the deny came from the default outcome — there’s no rule to point at. Add an explicit deny rule if you want the hint populated.