Authoring approval-chain policies as graphs¶
This tutorial walks through the graph frontend landed by epic
#1079, the third
authoring modality in the multi-modal policy stack after the
YAML DSL and JSON Rules. You’ll author
a branched approval chain in graph YAML, compile it to the
require_approval IR rules the runtime evaluator promotes to
runtime.Defer, build a
deployable bundle, and inspect which branch fired through the
telemetry WAL.
When to use the graph frontend¶
YAML DSL and JSON Rules already let you write a single
require_approval rule. The graph frontend earns its keep when:
The same tool kind has multiple branches with different approver requirements per environment / region / tier.
The branches share structure but differ in
approvers/require: all|any.A reviewer wants to read the approval chain as a hierarchy, not as N independent rules scattered through a flat list.
The graph frontend lowers each branch to exactly one IR rule with
effect: require_approval, so the runtime semantics are identical
to what you’d get from hand-authoring the rules in YAML — but the
authoring document captures the operator’s intent (the chain)
explicitly.
Prereqs¶
autonomyCLI installed (autonomy --versionshould report a build).A working directory you can write files into.
Step 1 — Author the approval chain¶
Save the following as chains.graph.yaml:
version: "1.0.0"
chains:
- id: prod-shell-approval
tool: tool.shell
branches:
- id: production-strict
when:
op: eq
field: params.env
value: production
approvers:
- on-call
- security
require: all
- id: staging-relaxed
when:
op: eq
field: params.env
value: staging
approvers:
- on-call
require: all
This is one chain (prod-shell-approval) gating tool.shell with
two branches: production-strict requires on-call AND security to
approve when env=production; staging-relaxed requires only
on-call when env=staging. Anything else falls through to the
program-level default (deny) — see
Scope decision for why
graph files compile to standalone programs.
Schema at a glance¶
Field |
Required |
Notes |
|---|---|---|
|
yes |
Must be |
|
yes (≥1) |
One chain per (operator-conceptual) approval policy |
|
yes |
Unique within the document; MUST NOT contain |
|
yes |
Tool kind this chain gates ( |
|
yes (≥1) |
Each branch becomes one IR rule |
|
yes |
Unique within the chain; MUST NOT contain |
|
no |
IR-shape predicate (same op set as YAML DSL: |
|
no |
List of approver labels (operator intent — metadata today) |
|
no |
|
Approvers + Require are operator intent today; future workflow tooling will consume them once an IR Approval block lands. The graph parser validates their shape so typos surface at compile time instead of being silently lost.
Step 2 — Compile to Rego¶
mkdir -p compiled
autonomy policy compile-graph chains.graph.yaml --out compiled/policy.rego
You should see:
policy compile-graph: wrote compiled/policy.rego (2 rules, <N> bytes)
Two rules — one per branch. The compiled IR Rule IDs are
<chain.id>/<branch.id>:
prod-shell-approval/production-strictprod-shell-approval/staging-relaxed
The generated Rego is the standard data.autonomy package the
runtime evaluator loads. Each rule carries effect: require_approval
in the rules_meta block, so the evaluator maps a match to
runtime.Defer (which runtime.Decision.IsAllowed() returns
false for — the action blocks).
Step 3 — Build a bundle¶
autonomy policy build \
--in compiled \
--out bundle.tar.gz \
--version 1.0.0 \
--name prod-shell-approval \
--policy-frontend graph
Expected output:
policy build: OK
version: 1.0.0
hash: sha256:<digest>...
runtime-version:
policy_frontend: graph
output: bundle.tar.gz
The --policy-frontend graph flag stamps the inner manifest’s
policy_frontend field, so downstream provenance audits can
distinguish graph-authored bundles from hand-written Rego /
yaml-dsl / json-rules. See
INV-MM-5
for the cross-check contract this enables.
Step 4 — Evaluate a request¶
autonomy policy eval \
--bundle bundle.tar.gz \
--kind tool.shell \
--params '{"env": "production"}'
A single JSON line on stdout:
{"outcome":"defer","reason":"...","kind":"tool.shell","params":{"env":"production"},"matched_rule_id":"prod-shell-approval/production-strict","matched_rule_file":"chains.graph.yaml","matched_rule_line":6,...}
The load-bearing fields:
outcome: defer— the action is blocked pending approval.policy eval’s exit code is1(any non-allow outcome).matched_rule_id— the compiled Rule.ID, i.e.<chain.id>/<branch.id>. The graph-frontend convention makes the operator-authored chain + branch immediately recoverable from the runtime decision.matched_rule_file+matched_rule_line— the explainability anchor (C05+C06 path from epic #747). The line points to the branch in the original graph YAML.
Try the staging branch:
autonomy policy eval \
--bundle bundle.tar.gz \
--kind tool.shell \
--params '{"env": "staging"}'
{"outcome":"defer",...,"matched_rule_id":"prod-shell-approval/staging-relaxed","matched_rule_line":15,...}
Different branch fired; line number points at the staging-relaxed branch in the source. Try a non-matching env:
autonomy policy eval \
--bundle bundle.tar.gz \
--kind tool.shell \
--params '{"env": "dev"}'
{"outcome":"deny","reason":"default deny","kind":"tool.shell","params":{"env":"dev"}}
outcome: deny — no branch’s when matched, so the program-level
default (deny) fired. No matched_rule_id because no rule fired.
Step 5 — Load + observe in the WAL¶
When the bundle is loaded into a running runtime server, each decision lands in the WAL with the same explainability fields:
autonomy wal inspect --kind autonomy.decision --json
Each line is a JSON object whose attrs carry matched_rule_id,
matched_rule_file, and matched_rule_line for every
graph-authored decision, identically to the explainability fields
the YAML authoring tutorial documents
— the IR makes both frontends emit the same downstream shape.
Validation rules to be aware of¶
The graph parser is fail-closed and rejects common authoring mistakes at compile time:
Unknown top-level / chain / branch key → reject.
Duplicate keys at any level → reject (a common copy-paste mistake that would otherwise silently “second-wins”).
Chain ID containing
/→ reject. The/is reserved for the compiled-Rule.ID separator; without this rule two valid chains could collapse to the same Rule.ID. Use.,-, or_for hierarchical naming (e.g.team.security.critical-shell).Duplicate chain.id across the document → reject.
Duplicate branch.id within a chain → reject.
Predicate semantic errors (unknown op, leaf without
field, compound withfield,notwith multiple children) → reject. The graph parser delegates predicate validation to the IR validator (INV-MM-7).Non-string scalars where the schema says string (
tool: false,id: 42,require: true) → reject with a hint to quote the value.
What’s NOT in scope today¶
Approver tokens / multi-stage chains. The runtime today blocks on
require_approvaland waits for an upstream system to decide. A future epic could add aninput.approval_tokensbucket so chains can advance through stages; that needs workflow-substrate work not in scope here.Approver-metadata-in-IR. Branch
approvers:+require:are captured + validated for shape but NOT propagated into the compiled IR Rule. Future workflow tooling needs anir.Rule.Approvalfield that this epic doesn’t add.Visual / graphical editor. Text format only.
See Scope decision for the full deferred-scope list and the asymmetry-test rationale.
Related¶
Authoring policies in YAML — the sibling tutorial covering the YAML DSL frontend.