Execution Decision Model

ADK governs arbitrary executable actions — a ROS 2 cmd_vel publish, an AI agent tool call, a Kubernetes scale operation — with one primitive. The domain lives in the signed manifest and in pluggable providers; the core runtime is domain-neutral.

Pipeline

Proposed Action
      ↓
Policy Evaluation            (Rego → Outcome + optional verdict/authorized_action/transformation)
      ↓
Manifest-defined Verdict     (deployment vocabulary: CONSTRAIN, SANITIZE, CAP, …)
      ↓
Runtime-defined Effect       (finite: pass_through | replace | reject)
      ↓
Provider Action Transform    (optional, deterministic, domain-owned)
      ↓
Authorized Action            (exactly one, for an executable decision)
      ↓
Enforcement Adapter          (mechanical — no discretion)
      ↓
Execution
      ↓
Enactment Receipt            (proposed → authorized → enacted; faithfulness verified)

Three layers of authority

  • Verdict vocabulary is deployment-defined (the signed manifest). The runtime never hardcodes domain verdict names — ALLOW/CONSTRAIN/AVOID/HOLD/EXECUTE/SANITIZE/CAP are all just examples an operator declares.

  • Effects are runtime-defined and finite. Every verdict maps to exactly one effect:

    • pass_through — the proposed action is authorized unchanged. (Outcome must be Allow; no authorized action, no transformation.)

    • replace — the proposed action is not authorized; exactly one replacement is produced, from either a policy-supplied authorized_action OR a runtime-owned transformation. An old adapter that reads only decision fails closed on the original; an upgraded adapter enacts the replacement.

    • reject — nothing is authorized (deny, no alternative).

    • Approval-style suspend is the existing Defer outcome; a manifest/provider-defined safe output is expressed as replace + a provider safe transform (e.g. stop_motion), never a hardcoded zero value in the core.

  • Transformations are provider-defined and domain-isolated. A Provider (e.g. builtin.ros2_twist, builtin.kubernetes) owns the transform math. The generic runtime resolves the named provider from the manifest, verifies its id/version and that it Supports the action kind + input schema, calls it deterministically, and validates the authorized action. The core never imports a provider package or understands linear/angular/replicas.

Decision precedence

A verdict transforms an already-allowed action; it can never elevate a denied or deferred one. On any non-Allow outcome the verdict and reason codes are recorded as audit metadata only and no authorized action is surfaced.

Constraint ceilings

The operator’s manifest is the outer safety envelope: it caps the inputs a policy passes to a transformation (action_transformations.constraint_ceilings). Because a policy can compute an input from live data, the cap is enforced at decision time (where the concrete value exists); a literal over-ceiling input in the policy is additionally rejected at bundle load. Ceilings are constraint-kind-aware — a field is capped only where the provider declares it a numeric maximum (ConstraintMax); unsupported kinds (a floor like a clearance threshold, an enum, an unknown field) fail closed rather than blindly applying min(requested, ceiling). Capped fields are recorded on the decision (transformation_constraints_applied) for audit.

Audit chain

Every enforced decision records: proposed action + digest → verdict → effect → selected transformation + provider id/version + inputs + constraints applied → authorized action + digest → enacted action + digest → faithfulness result / deviation → policy ref + manifest ref. This proves proposed → authorized → enacted end-to-end, across any domain.

Faithfulness is verified by comparing the canonical authorized action against the adapter’s reported enacted action — generic, domain-free. An adapter that can only report a transport- serialized payload (e.g. the ROS bridge’s CDR bytes) is decoded via a provider-registered action codec (actiontransform.Codec — CodecID/Version/Supports(schema)/Encode/Decode), so the encode + enactment paths serialize/deserialize without importing any domain transport package (INV-EDM-3); an unknown schema yields an unverifiable-but-recorded receipt rather than a false verdict on decode, and fails closed (ErrNoCodec) on a serialization-requiring encode. The codec is one extensibility seam (#1347): a deployment governs a new actuator message type on the typed path by contributing a Go codec, with no change to the generic runtime.

Actuator schemas (v1.9)

The optional actuator_schemas manifest block lets an operator declare, per actuator message schema, the runtime’s knowledge of that message type — closing two follow-ups without an ADK code change:

  • Field layout (#1347) — an ordered list of CDR fields ({field, type}, dotted paths + fixed-size primitives). A domain-neutral layout codec (runtime/layoutcodec) encodes/decodes the exact RTPS CDR wire shape the hand-written codecs use (4-byte LE encapsulation header + size-aligned fields), so a message type ADK ships no Go codec for is still decodable on the faithfulness path. The enactment verifier consults a declared layout when no hand-written codec owns the reported bytes’ type, keeping the same decoded / malformed / not-owned contract. This removes the hardcoded {Twist, PoseStamped} limit: an operator adds a fixed-layout message (Ackermann, a custom Twist variant) by declaration alone.

  • Validation contract (#1341) — required / allowed field paths + numeric ranges. The runtime validates the authorized actuator command against this contract (not merely for JSON-serializability): a policy cannot authorize an out-of-range or malformed command. The check runs fail-closed for both provider-generated (against the transformation’s output_schema) and policy-direct authorized actions (keyed off the command’s message type).

Both are gated on an explicit schema_version ≥ 1.9 (an older runtime that can’t enforce a declared contract / decode a declared layout must refuse the bundle, not silently ignore it).

Invariants

  • INV-EDM-1 — Verdict vocabulary is manifest-defined; the runtime hardcodes no domain verdict names.

  • INV-EDM-2 — Enforcement effects are runtime-defined and finite.

  • INV-EDM-3 — Transformation implementations are provider-defined and domain-isolated; the generic core imports no provider.

  • INV-EDM-4 — A transformation may only narrow or replace an already-allowed action.

  • INV-EDM-5 — A denied or deferred action is never elevated by a verdict or transformation.

  • INV-EDM-6 — Exactly one authorized action exists for an executable decision.

  • INV-EDM-7 — Adapters enact exactly the authorized action and have no transformation discretion.

  • INV-EDM-8 — Unknown verdicts, effects, transformations, providers, schemas, or malformed provider output fail closed.

  • INV-EDM-9 — The signed manifest binds the decision vocabulary, effects, providers, versions, schemas, and constraints.

  • INV-EDM-10 — Audit data proves proposed action → authorized action → enacted action.

Compatibility

The graded-verdict / maneuver naming that preceded this model never shipped to a release (it lived only on the #1310 epic branch), so the generic model is a clean rename with no legacy aliases — the manifest carries execution_decisions + action_transformations, not graded_verdicts / maneuvers, and there is no dual-form parsing to maintain.

Providers

  • builtin.ros2_twist — transforms geometry_msgs/msg/Twist actuator commands (clamp_velocity, stop_motion, avoid_obstacle). Owns all ROS-specific shape/encoding knowledge.

  • builtin.kubernetes — remediation transforms (cap_replicas). Demonstrates that a non-robotics domain plugs in without touching generic runtime decision code.