ContractRefine Demo Runbook¶
Self-contained, demo-neutral walkthrough that exercises the full ContractRefine loop introduced in epic #1020:
POST /v1/preflight → returns a passing PreflightDecision with id
POST /v1/tool → with X-Autonomy-Preflight-Id: <id>; 200 allow
GET /v1/audit/{id} → returns BOTH preflight + decision frames
(chronological INV-CR-8 join)
The embedded policy bundle declares requires_preflight: true, so a
/v1/tool call WITHOUT the header is denied at the runtime binding
layer (contract: preflight_required). The Rego rule additionally
requires input.preflight.outcome == "pass", so repair_required
and deny verdicts also fail at the policy layer. Both gates fire
under mode=enforce.
No Docker, external registry, or control-plane connection is needed.
What the demo proves¶
Three independent claims, each verified by a separate step:
Step |
Claim |
How |
|---|---|---|
1/3 |
The verifier returns a deterministic verdict for a well-formed plan |
POST /v1/preflight; outcome must be |
2/3 |
The runtime binding gate honors a valid |
POST /v1/tool with the header; decision must be |
3/3 |
The audit surface joins the two frames under one |
GET /v1/audit/{audit_id}; |
A pass on all three is the “verify → bind → execute → audit join” loop. A failure on any of them is a runbook-reproducibility regression — see the failure drills below.
Run the demo¶
autonomy demo contract-refine
Expected output (abbreviated):
──────────────────────────────────────────────────────
AutonomyOps / ContractRefine / governance demo
──────────────────────────────────────────────────────
policy : contract-refine-demo v1.0.0
mode : enforce (requires_preflight=true)
audit : tamper-evident WAL (local)
──────────────────────────────────────────────────────
Step 1/3 POST /v1/preflight
outcome : pass
preflight_id : <uuid>
audit_id : <uuid>
Step 2/3 POST /v1/tool (with X-Autonomy-Preflight-Id)
decision : allow
output : contract-refine demo: verify → bind → execute → audit join
audit_id : <uuid>
Step 3/3 GET /v1/audit/{audit_id}
frames : 2 (chronological)
[0] kind=autonomy.preflight timestamp=<HH:MM:SS.mmm>
[1] kind=autonomy.decision timestamp=<HH:MM:SS.mmm>
──────────────────────────────────────────────────────
PASS 1 — preflight verdict was pass
ALLOW 1 — /v1/tool admitted under verified binding
JOIN 2 — preflight + decision frames present in audit
WAL 7 events — binding + decision durable
──────────────────────────────────────────────────────
✓ PASS — verify → bind → execute → audit join
The ✓ PASS line on the final row is the load-bearing signal — the
smoke test in CI (TestDemoContractRefine_HappyPath) pins it.
Footprint control¶
By default the WAL directory is a temp directory removed on exit.
--keeppreserves the auto-generated directory.AUTONOMY_DEMO_WAL_DIR=<path>pins the WAL to a known path (created if missing, never auto-removed).
The footprint panel at the end of the run prints the actual paths and byte counts so the operator can inspect them.
Failure drills¶
Each drill modifies one step’s input and asserts the documented deny
path fires. These are the runbook’s reproducibility pins for the
four contract:preflight_* reason codes from C08 (#1028).
Drill 1 — missing header ⇒ contract: preflight_required¶
The bundle’s requires_preflight=true flag is what makes this gate
fire. Omit the header on /v1/tool:
curl -s -X POST http://localhost:7777/v1/tool \
-H 'Content-Type: application/json' \
-d '{"kind":"tool.echo","params":{"text":"hi"}}' | jq .
Expected:
{
"decision": "deny",
"reason": "contract: preflight_required",
"audit_id": "<uuid>"
}
Drill 2 — kind mismatch ⇒ contract: preflight_binding_mismatch¶
Preflight a request bound to tool.echo, then call /v1/tool with
tool.shell:
# Step 1: get a preflight bound to tool.echo (the demo helper does this).
PFID=$(autonomy preflight verify --json - <<'EOF' | jq -r .preflight_id
{
"intent":{"kind":"mission.start"},
"actor":{"node_id":"demo-node"},
"capability":"tool.echo",
"context":{"zone":"default"},
"plan":[{"kind":"tool.echo"}]
}
EOF
)
# Step 2: try to use it for a different kind.
curl -s -X POST http://localhost:7777/v1/tool \
-H "X-Autonomy-Preflight-Id: $PFID" \
-H 'Content-Type: application/json' \
-d '{"kind":"tool.shell","params":{"cmd":"true"}}'
Expected: reason: "contract: preflight_binding_mismatch".
Drill 3 — replay ⇒ contract: preflight_replay¶
Use the same preflight_id twice. The first call returns allow;
the second 403s with preflight_replay — the durable WAL marker
(EventKindPreflightConsumed) makes this hold even if the in-memory
cache evicts between calls. See
runtime/server_preflight_binding.go
for the marker mechanism.
PFID=<from Drill 2>
curl -s -X POST http://localhost:7777/v1/tool \
-H "X-Autonomy-Preflight-Id: $PFID" \
-H 'Content-Type: application/json' \
-d '{"kind":"tool.echo","params":{"text":"hi"}}'
# → allow
curl -s -X POST http://localhost:7777/v1/tool \
-H "X-Autonomy-Preflight-Id: $PFID" \
-H 'Content-Type: application/json' \
-d '{"kind":"tool.echo","params":{"text":"hi again"}}'
# → deny, reason "contract: preflight_replay"
Drill 4 — expired ⇒ contract: preflight_expired¶
The verifier’s default BindingWindow is 5 minutes. Wait past it
(or pin a shorter window via DefaultVerifier.BindingWindow in a
custom build) and re-use the id:
sleep 301
curl -s -X POST http://localhost:7777/v1/tool \
-H "X-Autonomy-Preflight-Id: $PFID" ...
# → deny, reason "contract: preflight_expired"
In CI the 5-minute sleep is impractical; the test surface for this
deny path lives in runtime/server_preflight_binding_test.go
(TestBinding_ExpiresAtInPast_DenyExpired), which stamps a past
ExpiresAt directly.
Architecture reference¶
The contract this demo exercises is documented in docs/architecture/contract-refine.md. The nine INV-CR-* invariants the loop satisfies are listed in docs/architecture/invariants.md.
Cross-epic¶
This demo is the reference shape Isaac and ROSOrin extend:
Isaac: replaces the in-process
DefaultVerifierwith one backed by the governance-labmock_policy_service; the rest of the loop (binding consumption, audit join) is unchanged.ROSOrin: the same loop runs against a real DDS bridge; the per-message governed bridge router consults the same
/v1/preflightendpoint before forwarding to the real domain.
Neither integration changes the runbook claims above — they reuse
the same verify → bind → execute → audit join shape.
Where the smoke test lives¶
TestDemoContractRefine_HappyPath in
cmd/autonomy/commands/demo_contract_refine_test.go
pins the runbook’s headline pass signal end-to-end.