Skip to content

Action Envelope

The Action Envelope is the portable contract between any adapter and the engine. An adapter's whole job is to normalize a backend action into this shape; reeflex-core reasons only about the envelope and knows nothing about WordPress, Postgres, or S3. The canonical, versioned definition lives in reeflex-spec/SPEC.md §2, with machine-readable JSON schemas alongside it — this page is the reader's tour.

Shape

{
  "agent":     { "id": "agent:demo", "session_id": "sess_01H…" },
  "action":    { "namespace": "store", "verb": "delete", "ability": "store/bulk-delete-products" },
  "target":    { "environment": "production" },
  "magnitude": { "count": 200 },
  "axes":      { "reversibility": "irreversible", "blast_radius": "broad", "externality": "internal" },
  "approval":  { "present": false }
}

Fields

Field Meaning
agent.id Who is acting.
agent.session_id Ties actions into one trajectory. R5 tracks cumulative deletes per session, so splitting a big action into many small ones (fragmentation) buys nothing.
action.namespace The backend domain (e.g. store, infra).
action.verb The normalized verb — read, create, update, delete, … — not the backend's raw method name. Normalization is the adapter's responsibility (SPEC §3). Five verbs for physical equipment — set, mode, override, actuate, flash — are described below.
action.ability The specific backend capability being invoked.
target.environment production, staging, or dev. Some rules only arm in production.
target.safety_function Optional, default false. true when the target is a safety function — see below.
magnitude.count How many items the action touches.
magnitude.physical Optional. The declared size of a physical change — see below.
axes The three risk axes (below) — the heart of the model.
approval { "present": false } on a first attempt; on a resubmission after a hold is approved, carries the approval so core can bind it to the exact envelope.

The three risk axes

Reeflex prices an action on impact, not on identity. Every action is scored on three axes:

Axis Values Question
reversibility reversible · recoverable · irreversible Can this be undone?
blast_radius single · scoped · broad · systemic How much does it touch?
externality internal · outbound · physical Does its effect leave the system?

The base policy packs (open, Apache-2.0) reason over these axes plus cumulative session state. For example, irreversible + broad + production → require_approval; irreversible + systemic + production → deny (refused even with approval). See the policy guide and what the base policy does not catch.

Physical-impact fields

Requires a core that carries RFX-427

These fields and verbs are additive and optional: an envelope without them is decided exactly as before. They are read by rules R8–R11 and two budgets, added to the base pack under RFX-427. reeflex-core v0.2.3 and earlier do not read them — on those versions set is treated as update, the other four verbs fall to core's unknown-verb default, and safety_function / magnitude.physical are ignored.

axes.externality: physical has always existed in the model ("SCADA, robotics, energy"). These fields give the policy something to read about a physical action beyond that one value.

{
  "action":    { "verb": "set" },
  "target":    { "environment": "production", "safety_function": false },
  "magnitude": { "count": 1,
                 "physical": { "envelope_fraction": 0.4, "unit": "degC",
                               "requested": 48.0, "range": [20.0, 90.0] } },
  "axes":      { "externality": "physical" }
}
Field Meaning
target.safety_function true when the target is a safety function, interlock, E-stop, alarm limit, SIL-rated logic, or a safety-PLC parameter. Read by R8.
magnitude.physical.envelope_fraction The size of the requested change relative to the declared safe operating range, >= 0. 1.0 is a full-range step; above 1.0 means the requested value lies outside the declared safe range. Read by R10a and summed per session by the physical_envelope budget. How it is computed from a raw request is the adapter mapping's job — see the MCP gateway.
magnitude.physical.unit, .requested, .range Informational. Carried into the decision record so a reviewer can see what the fraction was computed from; no rule reads them.

Core reads the number; it does not compute it. With the declared safe range [min, max] and span = max − min, the adapter computes it as follows (the rules in dev-2's RFX-428 mapping work, which correct the first draft's single abs(requested − current) / span):

Case envelope_fraction
requested inside the range, current known abs(requested − current) / span, capped at 1.0 — a move back into range from a drifted plant is not read as out of range
requested inside the range, current not supplied the worst-case step, max(requested − min, max − requested) / span — never below the true step. A midpoint guess would read a min→max move as 0.5
requested outside the range 1.0 + distance_outside / span — always above 1.0, whatever current is

Example, range [20, 90]: from 85 to 92 is 1.0 + 2/70 ≈ 1.03 (R10a), not abs(92 − 85) / 70 = 0.1. The worst-case default has a cost: every write without current reads at least 0.5, so the default physical_envelope budget of 1.0 holds the third such write in a session.

Verb Meaning Typical raw actions
set write a setpoint / parameter / recipe value OPC-UA write, Modbus write register, set_temperature, write_tag
mode change operating mode auto↔manual, maintenance, bypass, arm/disarm, start/stop a unit
override bypass or alter a safety function disable interlock, mute an alarm limit, override E-stop, change a SIL parameter
actuate cause a physical, non-parametric effect open valve, move axis, dose, dispense, launch, release, cut
flash replace firmware / control logic firmware update, PLC program download, controller config load

The five verbs are canonical only on an envelope that declares axes.externality: physical, and only spelled whole: a set on an internal or outbound envelope is still read as update, and set_temperature is not set. An adapter that cannot classify a physical action maps it to update with axes.externality: physical and the most restrictive axes.

These are declarations. safety_function and envelope_fraction are what the adapter's mapping says, not something core measures — a wrong safe range in a mapping is a wrong verdict, so mapping files are controlled documents. For where this sits relative to the safety PLC, and what it does not claim, see Reeflex above the safety PLC.

The hold binding covers these fields: an approval granted for a 0.4 step does not release a 0.9 step, and an approval for a non-safety target does not release the same verb on a safety target.

The Decision

/v1/decide returns one of three decisions with total precedence (deny > require_approval > allow):

  • allow — the action proceeds.
  • require_approval — a hold; it waits for an approver you trust (a human, or an agent you trust — HIL / AIL).
  • deny — blocked, with a reason the agent can read.

The engine fails closed: if OPA is unreachable or a policy is ambiguous, the answer is deny, never allow. The full decision shape — including decision_id, obligations, and (for holds) hold_id / expires_ts — is in the REST API reference.