ContractRefine — Pre-execution Contract Verification¶
ContractRefine is a semantic preflight layer above the AutonomyOps ADK runtime’s existing mechanical enforcement stack. Mechanical layers answer “is this call shape-valid, identity-valid, and allowed?”. ContractRefine answers a different question: “is the planned action internally consistent with the governance configuration the operator agreed to?”.
It returns one of three top-level outcomes on the
PreflightDecision.outcome field:
Outcome |
Meaning |
Execution? |
|---|---|---|
|
No blocking contract failed. |
May proceed to |
|
A blocking contract failed AND a deterministic, bounded repair hint exists. |
Caller may retry once with corrections; not allowed as-is. |
|
A blocking contract failed with no repair path. |
Execution is blocked. |
The outcome field is always one of those three values — it is the
operator’s primary decision signal and the wire-format enum is closed.
There is no fourth “advisory” outcome.
Where advisory signals live¶
A contract may fail at severity: advisory rather than severity: blocking. Advisory failures are recorded inside the response —
PreflightDecision.contract_results[] carries one entry per evaluated
contract, each with its own outcome (pass | fail | not_applicable)
and severity (info | blocking | advisory). The top-level
outcome aggregates only severity: blocking failures; advisory
failures surface in contract_results and in the WAL but do not change
the top-level verdict.
The canonical advisory case in Phase 1 is chain_unverifiable_offline
(Contract Taxonomy), which records that the
verifier could not prove decision-chain completeness because evaluating
it would require substrate the edge node cannot reach offline. The
operator chose disconnected operation and accepts the residual risk;
the WAL preserves the signal for audit. The top-level outcome for
this case is pass (no blocking contract failed), and /v1/tool
proceeds normally.
Clients implementing automated retry-on-repair logic SHOULD switch on
the top-level outcome only. Clients implementing audit dashboards
or operator-explain UIs SHOULD additionally surface any
contract_results entry whose severity is advisory so the
operator sees what the verifier could not verify.
Every preflight emits an autonomy.preflight WAL event correlated to
the eventual /v1/tool decision under a shared audit_id.
Where it sits in the stack¶
ContractRefine is additive. It does not bypass or replace any existing layer; it adds an earlier checkpoint that the operator can opt into.
[caller / agent / planner]
│
│ POST /v1/preflight {intent, plan, context}
▼
┌─────────────────────────────────────────────────────────────────┐
│ runtime/server.go : handlePreflight │
│ │
│ runtime/contract.Verifier │
│ ├─ resolve actor / entitlement / attestation / policyctx │
│ ├─ run contract checks (deterministic) │
│ └─ produce PreflightDecision │
│ │
│ ── WAL emit(autonomy.preflight, attrs{audit_id,...}) │
└─────────────────────────────────────────────────────────────────┘
│
│ caller may then proceed with X-Autonomy-Preflight-Id header
▼
┌─────────────────────────────────────────────────────────────────┐
│ runtime/server.go : handleTool (unchanged today) │
│ │
│ ── pre-policy admission gates ── (mavlink, ros 2 mediator) │
│ ── policy.Evaluator.Eval ── (Rego, data.autonomy.allow)│
│ ── attestation.Gate ── (enrollment / rollout) │
│ ── executeTool ── (allowlist / egress-DLP) │
│ │
│ ── WAL emit(autonomy.decision, attrs{audit_id,...}) │
└─────────────────────────────────────────────────────────────────┘
The two WAL events join under a shared audit_id, producing a
forward-traceable record: intent stated → preflight verdict → tool
decision → tool outcome, all under one correlation key.
The same ServerOptions wiring pattern used today by the MAVLink supervisor
(runtime/mavlink_gate.go) and the ROS 2
mediator (runtime/ros2/) is reused: a nil ContractVerifier ⇒ preflight is
disabled, /v1/preflight returns 503, and /v1/tool keeps its current
semantics verbatim. This is the backward-compatibility door (see
INV-CR-1).
Why a new endpoint, not a wrapper around /v1/tool¶
/v1/tool is execution-bound, single-call, post-decision. A preflight
evaluates a plan (one or more tool calls), and may legitimately return
repair_required without firing any execution. Reusing /v1/tool would
muddy its semantics. The existing
runtime/server.go dual-emit pattern —
policy frame + runtime-enforcement frame under one audit_id — already
demonstrates the discipline ContractRefine extends forward in time, not
backward.
Core architectural commitments¶
Verifier is interface-driven.
contract.Verifieris a Go interface inruntime/contract/types.go; the default implementation is the exportedcontract.DefaultVerifierinruntime/contract/verifier.go. Wired intoruntime.ServerOptions.ContractVerifierthe same wayMavlinkSupervisorandROS2Mediatorare wired today.No global state. The verifier resolves its inputs from the request body plus injected resolvers (entitlement, attestation snapshot, policy context). No singletons; no module-level mutable state.
Deterministic. Same
PreflightRequestbytes + same resolvers + same policy bundle ⇒ samePreflightDecisionbytes (modulo the freshly generatedpreflight_idandaudit_id). See INV-CR-5.Same fail-closed discipline as
/v1/tool. Verifier unavailable, resolver error, unknown contract kind ⇒deny. Neverpasson error. See INV-CR-2.Reason-shape contract reused. A new layer prefix
contract:matches the existing regex enforced byruntime/reasons_shape_test.go. See INV-CR-4.Same WAL discipline. A new
EventKindis added totelemetry/emitter.go(the file that defines theEventKindtype and its constants;telemetry/events.goholds the per-kind builder helpers). Same Entry envelope, same fsync + safe_seq durability, sameaudit_idcorrelation. See INV-CR-8.
Backward compatibility¶
ContractRefine is gated by a single config knob,
runtime.contract_refine.mode, with three positions that mirror the
existing attestation.mode ladder:
Mode |
|
|
|
Default |
|---|---|---|---|---|
|
503 — verifier wired but disabled |
Header ignored; no binding check runs |
Field is parsed but never enforced — bundle behaves identically to one without the field |
Explicit opt-out only |
|
Returns a decision |
Binding check runs; any failure emits an |
Missing header emits an |
Default (post-C12) |
|
Returns a decision |
Binding check runs; mismatch ⇒ deny with |
Bundle’s |
GA after future soak |
INV-CR-3 (Invariants) is authoritative on the binding-check ladder; the table above is the operator-facing summary. The two MUST agree — if a future change updates one, the other follows.
Existing deployments are unaffected:
Old clients that never send
X-Autonomy-Preflight-Idcontinue to work because the header is optional. In the post-C12 default (advisory), a missing header on arequires_preflight: truebundle emits a deny decision frame to the WAL but the request still reaches the policy layer — no wire-level 403, no behavioral change relative to a pre-#1020 deployment serving the same client.Policy bundles without
requires_preflightare loaded unchanged and behave exactly as they did pre-#1020. The absent field is treated asfalse; the bundle loader MUST NOT reject for the field’s absence under any mode (see INV-CR-1 enforcement).WAL consumers ignore unknown event kinds; the
autonomy.preflightaddition is non-breaking.Operators who want the pre-C12 silent posture set
runtime.contract_refine.mode: disabledexplicitly; the zero-value default is no longerdisabled(C12 #1032).
What this is NOT¶
ContractRefine is deliberately narrow. Operators reading the architecture sometimes ask whether the layer also does the following — it does not:
Not an LLM rubric. No model-in-the-loop scoring, no “LLM judge”. Contracts are deterministic predicates over a typed input. See INV-CR-7.
Not an unbounded refinement loop. At most one repair attempt per preflight session by default; configurable up to a small constant. Beyond that,
deny. See INV-CR-6.Not a planner. ContractRefine does not generate plans; it accepts them.
Not a replacement for policy. Rego policy still runs at
/v1/tool. ContractRefine produces an additional signal that policy can read; it does not bypass policy. See INV-CR-9.Not Isaac-aware, not ROSOrin-aware. No domain-specific contract kinds, no NVIDIA APIs, no robot-specific assumptions. Isaac (#849) and ROSOrin (#793) consume ContractRefine; they do not extend it.
Integration surface¶
The contract Isaac (#849) and ROSOrin (#793) consume. This section is the single source of truth for what downstream consumers code against — endpoints, headers, contract names, eligibility shape, and the reason-stacking rule. Anything not listed here is internal to the verifier and may change without notice.
Endpoints¶
Method |
Path |
Purpose |
Lands in PR |
|---|---|---|---|
|
|
Submit a |
C05 (#1025), C09 (#1029) dry-run |
|
|
Retrieve a stored decision from the WAL. |
C05 (#1025) |
|
|
Execute an action. Reads |
C08 (#1028) |
|
|
Returns the chronological INV-CR-8 join of every |
C08 (#1028) |
Mode (runtime.contract_refine.mode) gates the endpoints:
disabled ⇒ /v1/preflight returns 503 and the /v1/tool binding
check is bypassed; advisory and enforce differ only on whether a
binding-check failure denies vs records. See the
Backward compatibility table for the full
matrix.
Header¶
X-Autonomy-Preflight-Id: <preflight_id>
Consumers set this header on /v1/tool to bind an action to a
previously-issued preflight decision. The runtime resolves the id
against the WAL, verifies the binding (kind in BoundToolKinds,
ExpiresAt in the future, not previously consumed), and either
attaches the resolved PreflightDecision to runtime.Action.Preflight
(for Rego rules) or denies with one of the four binding reason codes
(see below).
The dry-run path returns preflight_id = "" and audit_id = "" so a
dry-run decision is unambiguously non-bindable (the empty string is
rejected at header-validation time before any cache lookup).
Contract catalog¶
The Phase 1 catalog is enumerated on the Contract Taxonomy page and is the source of truth for what the verifier evaluates. The seven contracts:
plan_shape · argument_provenance · capability_in_zone ·
rollback_contract · expected_evidence · attestation_freshness ·
decision_chain_complete
Consumer epics MUST NOT depend on a contract that isn’t in this list. Domain-specific contracts (MAVLink obligations, mission-gate semantics, restricted-zone primitives) layer ON TOP of the neutral catalog in their own epics — they do not extend the catalog itself. See What the catalog is NOT on the taxonomy page.
ExecutionEligibility shape¶
A passing PreflightDecision carries an ExecutionEligibility block
that names what /v1/tool will accept under the returned
preflight_id. The wire shape (runtime/contract/types.go):
{
"allowed": true,
"required_rollback": "rb.cancel.mission",
"bound_tool_kinds": ["tool.echo", "tool.shell"],
"expires_at": "2026-06-15T12:05:00Z"
}
Field semantics: allowed is false on deny / repair_required;
required_rollback is optional (only when the request named one);
bound_tool_kinds is optional and load-bearing for the /v1/tool
binding check; expires_at is the verifier’s now + BindingWindow,
RFC3339 UTC.
bound_tool_kinds is the load-bearing field for the binding check:
the /v1/tool call’s kind MUST be in this list. expires_at
bounds the binding lifetime (INV-CR-3);
the verifier’s DefaultBindingWindow constant (defined in
runtime/contract/verifier.go)
is 5 minutes, configurable per-deployment via the verifier’s
BindingWindow field.
The runtime separately enforces an in-memory binding-cache TTL ceiling
of 60 seconds (runtime.PreflightBindingCacheCeiling). When the
cache entry evicts, a subsequent /v1/tool call rehydrates from the
WAL and (if a matching autonomy.preflight_consumed marker exists)
resolves to contract: preflight_replay. The 60-second ceiling
bounds runtime memory regardless of how long an operator’s
BindingWindow is set to; it does NOT shorten the binding’s
operator-visible lifetime, only the hot-lookup window.
Reason codes¶
Two layers of reason codes consumer epics MUST handle, both in the
canonical <layer>: <subreason> shape that
runtime/reasons_shape_test.go
pins. Source of truth: runtime/contract/reasons.go.
Verifier-emitted (returned in PreflightDecision.Reason):
Code |
When |
|---|---|
|
Every blocking contract passed. |
|
At least one blocking contract failed but EVERY blocking failure produced ≥1 bounded hint. |
|
The session’s bounded-repair budget (INV-CR-6) was already consumed. |
|
A |
|
One of the seven Phase 1 contracts failed — e.g. |
|
Fail-closed paths owned by INV-CR-2. |
Binding-layer (emitted at /v1/tool under the same audit_id):
Code |
When |
|---|---|
|
Header absent when the bundle declares |
|
Header resolves but the action’s |
|
|
|
The id was already consumed — INV-CR-3 single-purpose binding. Durable via the |
Reason-stacking rule¶
When multiple verifier failures could fire on one preflight, the
rule is most-specific-blocking-failure first, advisory failures
stacked under it. The verifier composes a single top-level
Outcome + Reason by walking ContractResults in deterministic
catalog order:
If any blocking failure exists AND every blocking failure contributed ≥1 bounded hint → top-level is
repair_requiredwithReason = contract: repair_required.Otherwise if any blocking failure exists → top-level is
denywithReason = the first blocking failure's per-contract reason.Otherwise → top-level is
passwithReason = contract: passed.
Advisory failures (severity advisory) stay in ContractResults
under their own per-contract row but do NOT influence the top-level
Outcome. The
Where advisory signals live section
above is the operator-facing summary of this rule; the implementation
lives in
runtime/contract/verifier.go
composeOutcome.
Consumer epics that need a finer-grained view than the top-level
Outcome walk ContractResults themselves. The catalog order
(taxonomy page) is the
deterministic iteration order; tests pin it.
What consumers MAY rely on across PRs¶
A small surface that ContractRefine treats as a frozen API once published — any future change goes through a deprecation cycle, not a silent break:
Endpoint paths (
/v1/preflight,/v1/preflight/{id},/v1/tool,/v1/audit/{id}).The
X-Autonomy-Preflight-Idheader name.The seven Phase 1 contract names.
The reason codes enumerated above and on the taxonomy page.
PreflightDecision,PreflightRequest,ExecutionEligibility,ResultJSON tags.The chronological INV-CR-8 join shape on
/v1/audit—Frames[].kind ∈ {"autonomy.preflight", "autonomy.decision"}, sorted bytimestampascending.
What consumers MAY NOT rely on: verifier internals (resolver
construction, attempt counter shape, repair_hints content beyond
the {field, suggestion, bounded} tuple, evidence-map keys outside
the catalog).
Reference demo¶
The walk-through that exercises the full integration surface end-to-
end (POST /v1/preflight → POST /v1/tool with header → GET
/v1/audit chronological join) is the
ContractRefine Demo Runbook.
Headline command: autonomy demo contract-refine. Isaac and ROSOrin
extend the same shape — see the Cross-epic section of the runbook.