Execution Decision Model¶
Audience: operators and policy authors enabling graded execution decisions — #1310. A graded policy returns more than allow/deny: it can authorize a modified action (“yes, but at 0.08 m/s near an obstacle”, “yes, but capped at 8 replicas”) and record both what was proposed and what was authorized.
The model is domain-neutral — it governs any executable action (a ROS 2 Twist, an AI-agent tool call, a Kubernetes scale) through one pipeline:
Proposed Action → Policy → Verdict → Effect → (Transformation) → Authorized Action → Adapter → Enactment Receipt
The worked walkthrough (Steps 1–3) uses ROS 2 Twist; the cross-domain examples below show the identical contract governing an AI-agent tool call and a Kubernetes scale — only the provider and action shape differ.
Model¶
A graded decision has these parts on the /v1/tool response, all optional
(an ungraded decision omits them, so a consumer that reads only decision
is unaffected):
Field |
Meaning |
|---|---|
|
The verdict the policy assigned (a name from the declared vocabulary). |
|
The concrete, complete action the runtime authorized. |
|
Machine-readable reason enums. |
Three layers own three things, and no layer reaches into another’s:
Verdict vocabulary is manifest-defined. The runtime hardcodes no verdict names; a deployment declares its verdicts in the signed manifest, so an operator adds or retunes them per deployment without recompiling.
Effects are runtime-defined and finite. Each verdict maps to one effect — the runtime’s fixed set of enforcement behaviors. The runtime never infers behavior from a verdict name.
Transformations are provider-defined. Computing an authorized action (clamp a velocity, cap a replica count) is owned by a domain provider bound in the manifest; the generic runtime resolves and calls it but never understands its action shape.
Effects¶
Effect |
Meaning |
|---|---|
|
The original proposed action is authorized as-is; the verdict rides the response as metadata. |
|
The original action is not authorized; exactly one replacement — an |
|
Nothing is authorized (deny, no alternative). |
Fail-closed by construction: a policy that returns a verdict on a
deployment that declares no vocabulary, a verdict outside the
declared set, a verdict whose effect maps to an unknown value, a
replace verdict carrying zero or more than one replacement, or a
pass_through/reject verdict carrying a replacement, is denied and its
graded fields dropped. A graded policy can never actuate on a deployment
that has not explicitly enabled and defined its vocabulary.
Step 1 — Declare the vocabulary (deployment manifest, schema v1.8)¶
Add an execution_decisions block (verdict → effect) and, if the policy
selects transformations, an action_transformations block binding each
transformation to a provider. Both require an explicit schema_version: "1.8" — a runtime too old to enforce the model refuses the bundle rather
than silently ignoring it.
schema_version: "1.8"
execution_decisions:
verdicts:
- { name: ALLOW, effect: pass_through }
- { name: CONSTRAIN, effect: replace }
- { name: AVOID, effect: replace }
- { name: HOLD, effect: replace }
- { name: DENY, effect: reject }
action_transformations:
providers:
- { id: builtin.ros2_twist, version: "1" }
enabled:
- name: clamp_velocity
provider: builtin.ros2_twist
provider_version: "1"
action_kind: tool.ros2.topic.publish
input_schema: ros2://geometry_msgs/msg/Twist
output_schema: ros2://geometry_msgs/msg/Twist
Verdict names are operator-defined — ALLOW/CONSTRAIN/AVOID/HOLD/DENY
above are an example, not a runtime default. A replace effect determines
the replacement contract on its own, so there is no separate “requires an
authorized action” flag. The manifest reaches the runtime via the signed
release lock; see the runtime start --deployment-manifest /
release activate --deployment-manifest flows.
Cross-domain examples¶
The same three-layer contract — manifest-defined verdicts, runtime-defined effects, provider- or policy-computed authorized actions — governs any executable action. The runtime code does not change between these; only the manifest, the policy, and the enforcement adapter differ.
1. ROS 2 Twist (robotics)¶
The walkthrough above: a CONSTRAIN (replace) verdict selects the
builtin.ros2_twist clamp_velocity transformation, which caps a proposed Twist
to the policy’s max_linear/max_angular; the authorized command rides the
response as authorized_action + authorized_payload_b64 (CDR bytes), and the
governed bridge publishes those bytes verbatim.
2. OpenClaw tool call (AI agent)¶
A policy governs an agent’s tool call and, when it can’t be allowed as-is,
authorizes a sanitized replacement it computes itself — no provider needed,
because the replacement is expressed directly as authorized_action:
schema_version: "1.8"
execution_decisions:
verdicts:
- { name: EXECUTE, effect: pass_through }
- { name: SANITIZE, effect: replace }
- { name: BLOCK, effect: reject }
verdict := "SANITIZE" if risky_flag
# strip the dangerous flag; authorize the cleaned command directly.
authorized_action := object.remove(input.params, ["force"]) if risky_flag
The runtime denies the original on the wire and surfaces the sanitized
authorized_action; the agent’s tool-executor adapter runs that instead. An
approval-required verdict is the runtime’s existing Defer outcome
(require_approval), not a replace.
3. Kubernetes scale (remediation)¶
A CAP (replace) verdict selects the builtin.kubernetes cap_replicas
transformation, capping a scale action’s replica count; the operator’s manifest
ceiling caps how high the policy may set the limit:
schema_version: "1.8"
execution_decisions:
verdicts:
- { name: PROCEED, effect: pass_through }
- { name: CAP, effect: replace }
- { name: REJECT, effect: reject }
action_transformations:
providers: [ { id: builtin.kubernetes, version: "1" } ]
enabled:
- name: cap_replicas
provider: builtin.kubernetes
provider_version: "1"
action_kind: tool.k8s.scale
input_schema: k8s://apps/v1/Scale
output_schema: k8s://apps/v1/Scale
constraint_ceilings:
cap_replicas: { max_replicas: 8 }
verdict := "CAP" if over_budget
transformation := {"name": "cap_replicas", "inputs": {"max_replicas": 8}} if over_budget
The authorized action is the complete scale command — the target
deployment rides along with the capped replicas — so the enactment receipt
proves which deployment was scaled, verified by the generic canonical-action
comparison (no ROS/CDR decoding on this path).
Step 3 — Verify¶
POST /v1/tool a command that trips the caution band and inspect the
response:
{
"decision": "deny",
"reason": "runtime execution-decision: CONSTRAIN blocks the proposed action; authorized replacement provided",
"verdict": "CONSTRAIN",
"authorized_action": { "linear_x": 0.08, "angular_z": 0.2 },
"reason_codes": ["obstacle_caution_band", "speed_limit"],
"audit_id": "..."
}
decision: "deny"— thereplaceeffect drops the original fail-closed for un-upgraded adapters.verdict+authorized_action— the upgraded adapter enacts the replacement instead.
The WAL autonomy.decision frame for that audit_id records both
proposed_action (the original request) and authorized_action plus their
content digests (proposed_digest, authorized_digest), so the audit trail
shows the proposed-vs-authorized diff. Inspect it with autonomy wal inspect
or GET /v1/audit/{audit_id}.
Enactment receipts¶
Surfacing the authorized action isn’t enough to prove the adapter enacted it. After an enforcement adapter acts, it reports back what it actually enacted:
POST /v1/audit/{audit_id}/enactment
{
"enacted_action": { "linear_x": 0.08 },
"enforcer_id": "governed_ros2_bridge:node_a",
"authorized_digest": "sha256:…" // echoed from the /v1/tool response
}
The runtime correlates the receipt by audit_id, computes the digest of the
enacted action, and compares it to what it authorized (authorized_action
for a replace, else the original proposed_action):
{ "audit_id": "…", "verifiable": true, "faithful": true, "enacted_digest": "sha256:…" }
A faithful: false response — and a deviation on the recorded
autonomy.enactment_receipt WAL frame — means the adapter enacted something
other than what ADK authorized (or the authorized action was altered in
transit, caught via the echoed authorized_digest). This is what makes “the
enforcement adapter has no independent decision authority” a checkable claim.
GET /v1/audit/{audit_id} surfaces the receipt and an enactment summary
(faithful, enforcer_id, deviation).
Failure modes¶
Symptom |
Cause |
Fix |
|---|---|---|
|
Policy returns a verdict but the manifest has no |
Declare the vocabulary (Step 1). |
|
Policy returned a verdict name that isn’t declared (often a typo). |
Add the verdict, or fix the policy’s verdict string. |
|
A |
Return exactly one. |
|
A |
Drop the replacement, or change the verdict’s effect. |
|
Policy selected a transformation the manifest doesn’t enable. |
Add it to |
|
The policy’s |
Omit |
|
Bad inputs, an unresolvable/version-mismatched provider, or an action the transformation can’t operate on. |
Fix the inputs; confirm the provider + version are registered and the |
Manifest rejected: |
A block is on too-old a manifest. |
Set |