Trusted Action Fields
A policy is only as trustworthy as the facts it reads. Keel exposes two kinds of policy field, and the difference between them matters more than any individual field name:
keel_derived— Keel established this fact itself, from the governed action.caller_asserted— the caller supplied this fact on the request.
The agent your policy is meant to constrain is normally the caller. This page
documents the context._keel.action_envelope.* fields Keel supports as an
authoring contract, which of them are Stable, and — for the three that are not —
exactly where the value comes from. It also states what Keel does and does not
do to the record of those facts once a decision is made.
Changed: financial.amount_usd_micros is no longer Stable. It was published
here as a keel_derived Stable path. On Keel’s canonical payment rail Keel does
not establish it — it multiplies two caller-supplied values, and the fact it
emits there is stamped request_bound, not keel_derived. The authoring
catalog now labels the field caller_asserted. Both .value and .state move
to Preview.
If you have a spend cap keyed on this field, read
amount_usd_micros before your
next policy review. The threshold still evaluates and still fires; on the
canonical rail it constrains a number whose inputs the caller chose.
That leaves connector.identity.value and connector.tool_name.value as the
entire Stable set. financial.currency.value and financial.operation.value
were demoted earlier for the same reason. Also corrected on this page: the
availability table (no field on this page is available on realtime), the
recommended absence guard, which did not fire on the canonical payment rail, and
how these facts are recorded, which now states
plainly that the permit signature does not cover them. See
what is in the contract and
known limitations.
For condition syntax and the operator catalog, see Policy Conditions. For rule actions and evaluation order, see Policy Reference.
Provenance: who established the fact
Every authorable field carries a provenance label in the policy-authoring catalog. Four values exist:
| Provenance | Who established the value | Sound to gate enforcement on, against the caller? |
|---|---|---|
keel_derived | Keel, from the governed action and its own registries | Yes |
connector_asserted | The downstream connector, relayed through Keel | Only with the connector inside your trust boundary |
caller_asserted | The caller, on the request | No |
custom | Free-form values the caller places under context.* | No |
“Sound to gate enforcement on” is a claim about where the value came from, not
about what Keel does to the record afterwards. A keel_derived fact is one the
caller could not choose; it is not a signed attestation, and this page’s
recording section says exactly what protects it
once the decision is made.
Provenance is coarser than the fact’s own trust label
Provenance is a property of the field, fixed across every request. Each fact
in the envelope also carries its own per-request trust label, and that label
has a different and finer vocabulary:
trust | What it means for that one fact |
|---|---|
keel_verified | Read back from a Keel-verified record, such as the MCP decision trace or server registry. |
keel_derived | Keel computed it from the governed action. |
request_bound | The value came from the request. Keel bound it to the decision and will not accept a different value at dispatch, but Keel did not independently establish it. |
caller_declared | The caller stated it and Keel neither verified nor bound it. |
unavailable | The fact is not present; there is no value. |
The two axes do not always agree, and a field carries one provenance for every
surface it appears on. financial.amount_usd_micros emits request_bound on
the canonical payment rail and keel_derived on an MCP payment connector;
financial.currency does the same. The catalog resolves that split by declaring
the weaker reading — caller_asserted — so an author who assumes the caller
supplied the value is never wrong, and one who assumes Keel established it is
wrong only on the surface where money moves. The field reference below spells
out which path gives which for
amount_usd_micros,
currency, and
operation.
Where the two disagree, the fact’s own trust label is the one that describes
what happened on that request. Read it in the decision snapshot before you
weight a value as evidence against the caller — and read
how these facts are recorded for what that
snapshot is and is not.
trust is reported on the fact, but it is not an authorable policy path —
you cannot branch on it in a rule. It is there for you to inspect when
deciding, at authoring time, how much weight a field deserves.
Do not rely on a caller_asserted field alone when the caller is inside your
threat model. An agent that can choose the value of a field can choose the
branch of your policy that field selects. attrs.* paths, action.name, and
everything under context.* other than context._keel.* are caller-asserted:
useful for describing intent, logging, and advisory rules, but not for proving
what an action will do.
Keel enforces part of this for you. The policy authoring API attaches a
caller_asserted_field warning to any condition that reads a caller-asserted
field, and it rejects outright — with identity_field_not_enforceable — any
hard allow or deny rule keyed on a caller-asserted identity claim. The
warning is advisory; the rule below is not automated, and is yours to apply.
Two properties make context._keel.* different from the rest of the request:
- Keel authors the entire
_keelnamespace during evaluation. A caller-supplied_keelobject on the request is discarded before any policy runs, so a caller cannot inject or overwrite a trusted fact. - Every fact in the action envelope is a fixed-shape object rather than a bare value, so an unavailable fact is explicitly unavailable rather than silently missing. That is what makes the absence rule below possible.
What is in the contract
Two tiers exist for these fields. There is no third.
| Tier | Meaning |
|---|---|
| Stable | Keel supports this path, type, and meaning as an authoring contract. Breaking changes follow the deprecation policy in Surface Maturity. |
| Preview | Visible, possibly authorable, and subject to change without notice. Do not build enforcement on it. |
The Stable set is exactly these two paths:
| Path | Type | Provenance | Tier |
|---|---|---|---|
context._keel.action_envelope.connector.identity.value | string | keel_derived | Stable |
context._keel.action_envelope.connector.tool_name.value | string | keel_derived | Stable |
Both were chosen because each is an extracted fact — something Keel read off the governed action or resolved from its own registry — rather than a Keel judgment about that action, and because Keel establishes it on every surface where it is available at all. Extracted facts have a meaning that does not move when Keel’s classifiers improve.
Two paths is the whole set, and it contains nothing financial. That is the
honest size of the contract today, not an omission from this page. Every
money-shaped field in the envelope — the amount, its normalization, the
currency, the operation — is caller_asserted in the authoring catalog because
on Keel’s canonical payment rail the caller supplied the inputs. You can still
author against them, and the sections below say exactly what each one is worth
on each rail. What you cannot do is treat one as something Keel established.
financial.amount_usd_micros.value and .state were previously published on
this page as Stable, with keel_derived provenance. They are not, and both are
now Preview. On the canonical payment rail Keel computes the value as
requested_amount × 1,000,000 — arithmetic over two caller-supplied action
attributes — and stamps the resulting fact request_bound. A derivation cannot
establish more than its inputs, so the authoring catalog now declares the field
caller_asserted. On an MCP payment connector Keel really does derive it, from
a schema-validated tool argument; the single catalog label takes the weaker of
the two readings, which is the one that holds where real money moves.
.state follows .value out of the Stable set. Keel does establish the state —
the catalog still declares it keel_derived, correctly, because whether an
adapter produced an amount is Keel’s own answer — but a stability tier on the
availability signal for a Preview value promises a contract Keel is not holding.
The pair moves together. Both paths keep working and keep their meaning; neither
is covered by the deprecation policy. See
amount_usd_micros.
financial.currency.value and financial.operation.value were previously
published on this page as Stable. They are not, and both are now Preview.
Neither held the property a Stable tier promises. On Keel’s canonical payment
rail currency is taken verbatim from a caller-supplied request attribute, and
operation carries three disjoint value spaces depending on which code path
produced it — including, on MCP, whatever string an upstream server chose to
name its tool. Policies that already read these paths keep working; they are no
longer covered by the deprecation policy, and the two field-reference sections
below describe what each actually does. If you gate enforcement on either one,
read currency and
operation before your next policy review.
Discoverable does not mean supported
Any context._keel.action_envelope.* path not listed above is Preview, even
if you can see it. The envelope is a fixed-shape object, so unlisted paths
appear in API responses, in the policy-authoring catalog, and in decision
snapshots. Some are authorable. None of that makes them a contract: their
meaning may be refined, and a refinement would silently change what your policy
does. Treat the table above as the allowlist.
The same applies in reverse. Fields outside the envelope — including the flat
context._keel.action_access_* family documented in
Read/Write Governance — have their own
support status and are not governed by this page. The flat family is
keel_derived but carries no per-fact availability state, so the absence rule
below cannot be expressed with it.
Absence is not zero
Every fact in the envelope has a .state alongside its .value:
| State | Meaning |
|---|---|
present | Keel established the fact. .value is meaningful. |
absent | The fact does not exist for this action. .value is null. |
unknown | The fact may exist, but Keel could not establish it. .value is null. |
not_applicable | The fact is meaningless for this action. .value is null. |
A fallible fact is one whose .state can be something other than present.
amount_usd_micros is fallible: a non-USD payment, an untyped financial action,
and a non-financial action all produce a null value.
A policy that gates on .value without defining behaviour for the
non-present states can fail open. Two mechanics combine to cause this:
.valueis always a resolvable path. The envelope emits the key with anullvalue when the fact is unavailable, so{"op": "exists", "value": true}on a.valuepath is true whether or not the fact is available.existstells you the envelope has the slot, not that Keel established the fact.- A numeric comparison against
nullis not applicable, and inapplicable comparisons resolve to false. So a threshold rule such asamount_usd_micros.value gt 50000000simply does not match when the amount is unavailable.
Rules are evaluated in array order and the first terminal match wins, while
allow is non-terminal. A threshold rule that does not match is therefore not a
denial — it is a rule that stayed silent, leaving whatever allow preceded it
standing. The unpriced payment is exactly the one you wanted reviewed.
The correct pattern
Pair every threshold on a fallible fact with a companion rule that decides what
happens when the fact is unavailable. Gate the threshold on .state, then
handle the remaining states explicitly.
The companion rule needs a way to say “this action moves money” that does not
depend on the amount it is trying to guard. Use
financial.moves_customer_value — Keel’s own value-movement verdict, which is
keel_derived on every surface and is present on the canonical payment rail:
{
"name": "payment-review-with-absence-guard",
"rules": [
{
"if": {
"all": [
{
"field": "context._keel.action_envelope.financial.amount_usd_micros.state",
"op": "eq",
"value": "present"
},
{
"field": "context._keel.action_envelope.financial.amount_usd_micros.value",
"op": "gt",
"value": 50000000
}
]
},
"action": "require_human_review"
},
{
"if": {
"all": [
{
"field": "context._keel.action_envelope.financial.moves_customer_value.value",
"op": "eq",
"value": true
},
{
"field": "context._keel.action_envelope.financial.amount_usd_micros.state",
"op": "neq",
"value": "present"
}
]
},
"action": "require_human_review"
}
]
}The first rule reviews payments above $50.00. The second reviews any action Keel
has classified as value movement whose amount it could not establish — the case
the first rule cannot see. Use deny in place of require_human_review when an
unpriced payment should be blocked outright.
Evaluated against the real envelope, the second rule fires on an unpriced
payment on the canonical rail and on an unpriced payment through an MCP
payments connector, and stays silent on priced payments and on non-financial
actions:
| Request | moves_customer_value.value | amount_usd_micros.state | Guard fires |
|---|---|---|---|
| EUR payment on the canonical rail | true | not_applicable | yes |
| USD payment on the canonical rail | true | present | no |
EUR payment via MCP payments | true | not_applicable | yes |
USD payment via MCP payments | true | present | no |
| Non-financial read | null (unknown) | unknown | no |
A previous version of this page used
connector.identity.value in ["stripe", "payments"] as the companion rule’s
scope. That guard does not fire on the canonical payment rail — the rail’s
provider is stripe_mpp, which is not an MCP connector, so connector.identity
is not_applicable and its .value is null there. The all never held, and
the remediation missed the surface the hazard lives on. If you copied that
pattern, replace the connector.identity condition as shown above.
Every path in this pattern is Preview. amount_usd_micros is Preview
because on the canonical rail Keel derives it from caller-supplied inputs;
financial.moves_customer_value is Preview because it is a Keel classifier
judgment rather than an extracted fact, so its meaning can be refined as
classification improves. The Stable set — connector.identity.value and
connector.tool_name.value — cannot express this control at all: neither
reaches present on the canonical payment rail, so a guard written only from
Stable paths cannot be scoped to the surface the hazard lives on.
That is the real state of the contract, and it is the reason to write the
pattern this way rather than to avoid it. moves_customer_value is
keel_derived on every surface, and the guard’s job is to route the cases Keel
could not price into human review — which is the outcome you want whether or
not the number that was missing would have been trustworthy. Pin the behaviour
you depend on with a test, and see known limitations.
The last row deserves a note. A non-financial action reports
moves_customer_value.state as unknown, not present/false, so the guard
above stays silent on it. If you also want to stop everything Keel could not
classify, add moves_customer_value.state neq present as a third rule
rather than widening the second — it is a much broader control, and it will
review ordinary reads.
The .state gate in the first rule is belt-and-braces: the numeric comparison
would already fail closed on its own. Write it anyway. It makes the rule’s
intent legible, and it survives being copied into an any branch or negated,
where the implicit behaviour would not.
Field reference
connector.identity.value
context._keel.action_envelope.connector.identity.value
| Property | Value |
|---|---|
| Type | string |
| Provenance | keel_derived |
| Tier | Stable |
| Operators | eq, neq, in, not_in, contains, starts_with, ends_with, matches_regex, exists |
| Available on | permit and MCP surfaces (see availability) |
Meaning. The Keel-registered identity of the connector that will carry out
the action — for example github, slack, stripe, payments, crm,
cloud, or a scoped database identity such as postgres.readonly.
How Keel derives it. When Keel resolves a verified connector binding for the request, the identity comes from Keel’s own MCP server registry, not from anything on the request. Two properties give it its trust:
- It is never the MCP server slug. The slug is customer-renamable; the connector identity is a registry value. Renaming a server in the dashboard cannot change this field, and because unmapped tools default to review, a rename can only make classification stricter.
- A managed connector identity with no Keel connector contract is rejected rather than passed through, and a binding whose stored contract hash, contract version, or semantic-adapter version has drifted from the current contract does not produce facts — it challenges.
States. present when a verified binding exists. unknown when the request
runs through MCP but no verified connector identity was resolved — which is
always the case on realtime. not_applicable when the action does not execute
through a connector at all, such as a direct model call or a payment on the
canonical rail, whose provider (stripe_mpp, x402) is a payment rail rather
than an MCP connector.
Security caveats. An identity proves which connector, not what the call
does. It does not prove the connector version is pinned — version pinning is
tracked separately and is frequently unknown. Tool names are only unique within
a connector, so pair this field with tool_name rather than matching a tool
name alone.
{
"field": "context._keel.action_envelope.connector.identity.value",
"op": "in",
"value": ["stripe", "payments"]
}connector.tool_name.value
context._keel.action_envelope.connector.tool_name.value
| Property | Value |
|---|---|
| Type | string |
| Provenance | keel_derived |
| Tier | Stable |
| Operators | eq, neq, in, not_in, contains, starts_with, ends_with, matches_regex, exists |
| Available on | permit and MCP surfaces (see availability) |
Meaning. The exact tool name Keel bound to this decision, verbatim.
Tool names are not lower-cased, and matching on them is case-sensitive.
Keel normalizes a bound tool name by trimming surrounding whitespace and
nothing else, so a server that advertises Send-Money-NOW publishes
Send-Money-NOW in this field. An eq or in match against
send-money-now will not fire.
This is asymmetric with the permit’s requested_action, which is lower-cased
during normalization. Do not assume the two are comparable case-insensitively,
and do not assume a lower-case allowlist covers a mixed-case tool. Enumerate
tool names exactly as the connector advertises them — check the tool contract
rather than guessing the casing.
How Keel derives it. The name comes from the connector’s own tools/list
response, pinned into a tool contract, and read back from the verified decision
trace — not from a label on the request. Before any fact is produced, Keel
re-hashes the pinned input schema and compares it against both the stored
contract hash and the observed hash; any mismatch challenges the call. A
contract that has not been re-verified within 24 hours challenges as stale, and
call arguments that do not validate against the pinned schema are denied.
Matching is on exact names only, and exact includes exact case. A newly introduced tool never inherits a meaning from a name prefix or a caller-supplied label.
States. present when a verified binding exists. unknown when the request
runs through MCP but no verified tool name was recorded — which is always the
case on realtime. not_applicable on provider execution that does not involve a
tool call.
Security caveats. This field is available on the MCP surface and on the
permit surface when the permit cites an MCP decision; it is never available
on realtime, where a policy matching on it can never match. A tool
name is an identifier, not a capability claim — an unmapped name still
classifies as unknown access, and knowing the name does not tell you what the
tool does. Because the value space is open-ended, prefer positive matches
(eq, in) over negative ones: a not_in list silently admits every tool
nobody has thought of yet.
{
"all": [
{
"field": "context._keel.action_envelope.connector.identity.value",
"op": "eq",
"value": "github"
},
{
"field": "context._keel.action_envelope.connector.tool_name.value",
"op": "in",
"value": ["merge_pull_request", "create_repository"]
}
]
}financial.amount_usd_micros.value and .state
context._keel.action_envelope.financial.amount_usd_micros.value
context._keel.action_envelope.financial.amount_usd_micros.state
Preview — demoted from Stable. This is the field a spend cap is most likely
to be written against, so read this section before relying on one. On the
canonical payment rail the number is Keel’s arithmetic over two caller-supplied
values, and the fact is stamped request_bound. Keel will not accept a
different amount at dispatch than the one it decided on, but it never checked
the amount against anything outside the request. On an MCP payment connector it
is genuinely keel_derived. See
what the amount actually is below.
| Property | Value |
|---|---|
| Type | .value integer, .state enum |
| Catalog provenance | .value caller_asserted; .state keel_derived |
Fact trust (.value) | request_bound on the canonical rail, keel_derived on MCP |
| Tier | Preview (both paths) |
| Operators | .value: eq, neq, gt, gte, lt, lte, exists. .state: eq, neq, in, not_in, exists |
| Units | USD micros. 1 USD = 1,000,000. $50.00 is 50000000. |
| Available on | permit and MCP surfaces (see availability) |
Meaning. The amount of customer value the action moves, normalized to USD
micros. This is a payment amount, not a model cost. Inference spend is a
separate concept with its own public field, estimated_cost_usd_micros; the two
are never interchangeable and should never be summed.
What amount_usd_micros actually is
How Keel derives it is path-dependent, and the two paths differ in how much they are worth. Two typed adapters produce it, and nothing else does:
- MCP payment connectors. For the
paymentsandstripeconnector identities, Keel’s semantic adapter reads the amount out of a schema-validated tool argument: the argument must be declared in the pinned tool schema and present in the call, must be a non-negative integer, and is read in minor units, then multiplied by 10,000. On this path the fact’strustiskeel_derived— Keel read the number out of a verified, schema-validated tool call. - Canonical payment rails. When the request matches one of Keel’s canonical
payment rails — the action name, resource type, operation, provider, and model
must all line up — Keel takes the caller’s
requested_amountaction attribute, which is expressed in whole USD units on this path, and multiplies by 1,000,000. The result ispresentonly when the caller’srequested_currencyisusd. On this path the fact’strustisrequest_bound: both inputs to the multiplication are caller-supplied attributes, stampedrequest_boundthemselves.
In both cases the number is extracted from the action Keel is authorizing, not from a side channel the caller controls independently, and on both Keel binds it to the decision — it will not accept a different amount at dispatch than the one it decided on.
This is why the field is no longer Stable. A keel_derived label implied
Keel established the number. On the canonical rail it did not: it did the
arithmetic over values the caller chose. Keel doing the multiplication earns the
fact’s source — keel.payment_adapter — not its trust. A derived fact
carries the weakest trust among the inputs it passes through, so
amount_usd_micros inherits request_bound from amount and currency, and
the authoring catalog declares the field caller_asserted.
What that does and does not cost you:
- It does not mean the threshold is decorative. An agent that inflates
requested_amountto get a bigger payment trips agtrule harder, and the bound value is the one that must be dispatched. - It does mean the number is not a floor. An agent that wants to stay under
a cap can declare a small
requested_amount, or a non-USDrequested_currencythat makes the fieldnot_applicableentirely. On the canonical rail a spend cap keyed on this field constrains what the caller declared, not what Keel measured. - The remedy is the absence rule: pair the threshold
with a companion rule that fires when the amount is not
present, so side-stepping the normalization routes to review rather than to silence.
A previous version of this page claimed Keel requires the permit’s action name
to equal the bound tool name before it will build typed payment facts at all.
It does not, and you should not rely on that equality as a control. One
projection — the one that carries verified MCP financial facts onto a permit —
does enforce it. But when that check fails, Keel falls back to reading the
amount and currency straight off the verified connector facts, and that
fallback performs no such comparison. A permit whose requested_action does
not match the bound tool name still yields a present
amount_usd_micros. Treat the amount as bound to the tool call Keel verified,
not to the action name the caller wrote on the permit. If you need the two to
agree, compare connector.tool_name.value yourself in the policy.
States. present when an adapter produced an amount. not_applicable when
the payment is not in USD — there is no conversion step, so a non-USD payment
has no USD normalization — or when the action is not classified as value
movement at all. unknown when the action may move value but no typed adapter
could produce an amount — including every realtime turn.
Security caveats. This is the fallible field the absence rule exists for; read that section before writing a threshold. Three specific traps:
- On the canonical payment rail this is not evidence against the caller.
The caller chose both inputs. A cap of
gt 50000000is a control on the declared amount; it is not a statement that Keel measured $50.00. Treat it as a tripwire on what the agent asked for, and put the enforcement weight on the companion rule that catches a non-presentamount. - Non-USD payments are
not_applicable, not zero. A USD-only threshold rule ignores a €10,000 transfer entirely, and on the canonical rail the caller picks the currency that decides this. If your control is about size, pair the threshold with a rule that reviews or denies non-presentstates, as in the correct pattern. Do not reach for acurrencyallowlist instead: on that rail the currency is caller-supplied too, so an agent can choose the branch that allowlist selects. existsdoes not mean available.{"op": "exists", "value": true}on the.valuepath is true even when the amount isnull. Compare.statetopresentinstead.
{
"all": [
{
"field": "context._keel.action_envelope.financial.amount_usd_micros.state",
"op": "eq",
"value": "present"
},
{
"field": "context._keel.action_envelope.financial.amount_usd_micros.value",
"op": "gt",
"value": 50000000
}
]
}financial.currency.value
context._keel.action_envelope.financial.currency.value
Preview — demoted from Stable. On the canonical payment rail this value is taken verbatim from a caller-supplied request attribute and is not validated there. Do not use it as evidence against the caller on that path. See what it actually is below.
| Property | Value |
|---|---|
| Type | string, lower case |
| Catalog provenance | caller_asserted |
Fact trust | request_bound on the canonical rail, keel_derived on MCP |
| Tier | Preview |
| Operators | eq, neq, in, not_in, contains, starts_with, ends_with, matches_regex, exists |
| Available on | permit and MCP surfaces |
Meaning. The currency the financial action is denominated in — for example
usd or usdc. Always lower case in the envelope.
What currency actually is
How Keel derives it is path-dependent, and the two paths differ in how much they are worth.
- MCP payment connectors. Keel’s semantic adapter reads a schema-validated
currencyargument off the tool call, requires it to match a three-to-twelve-letter code, and normalizes it to lower case. Because the pattern is not restricted to ISO 4217, stablecoin and token symbols are representable alongside fiat codes. On this path the fact’strustiskeel_derived— Keel read it out of a verified, schema-validated tool call. - Canonical payment rails. The value is the caller’s
requested_currencyaction attribute, trimmed and lower-cased. On this path the fact’strustisrequest_bound, and Keel applies no format validation at that site — no ISO check, no length check, no allowlist. Any non-empty string the caller sends is published verbatim as apresentfact. A request carryingrequested_currency: "zzz"yieldscurrency.value == "zzz"withstate == "present".
Keel binds the rail value to the decision — it will not accept a different currency at dispatch than the one it decided on — so the field is meaningful as a request-binding. It is not an observation Keel made of the world, and the agent your policy constrains is normally the one who chose it.
This is why the field is no longer Stable. A keel_derived label would imply
Keel established the value. On the canonical rail it did not; it bound a value
the caller supplied. The catalog therefore reports the weaker cross-surface
classification, caller_asserted. Read the fact’s own trust label in the
decision snapshot when deciding how much weight to give a particular event.
States. present when an adapter produced a currency. unknown when the
action may move value but no typed adapter produced one. not_applicable when
the action is not value movement. Never present on realtime.
Security caveats. Currency is a label on the amount, never a magnitude — do
not infer size from it. It is also not a conversion: usd here does not imply
that a USD amount is available, and amount_usd_micros can still be
not_applicable. Check the amount’s .state separately.
Above all: on the canonical payment rail, a currency allowlist is not a
control against the caller. An agent that picks the currency picks which
branch of your policy runs. If you want to constrain value movement by
denomination, gate on amount_usd_micros.state and treat a non-present amount
as the thing to review, as in the correct pattern. Keel
does establish that state — it is Keel’s own answer about whether an adapter
produced an amount — but be precise about what it establishes: present means
Keel produced a USD figure, not that Keel checked the figure. On the canonical
rail present is reached because the caller declared a USD amount. Use
currency to describe a decision, not to authorize one.
{
"all": [
{
"field": "context._keel.action_envelope.financial.currency.value",
"op": "eq",
"value": "usd"
},
{
"field": "context._keel.action_envelope.financial.amount_usd_micros.state",
"op": "eq",
"value": "present"
}
]
}financial.operation.value
context._keel.action_envelope.financial.operation.value
Preview — demoted from Stable. This path carries three disjoint value spaces depending on which derivation produced it, and on MCP the value is whatever string an upstream server chose to name its tool. A field whose value space depends on the caller cannot be a stable contract.
| Property | Value |
|---|---|
| Type | string |
| Catalog provenance | caller_asserted |
Fact trust | varies by path — request_bound, keel_verified, or keel_derived |
| Tier | Preview |
| Operators | eq, neq, in, not_in, contains, starts_with, ends_with, matches_regex, exists |
| Available on | permit and MCP surfaces |
Meaning. What kind of financial operation this is — but what kind of string you get depends on which of three derivations ran.
How Keel derives it. Three paths, each producing a different flavour of value, and the three value spaces do not overlap:
| Path | Value | Fact trust | Value space |
|---|---|---|---|
| Canonical payment rails | the literal payment.execute | request_bound | exactly one constant |
| Registry-derived | the literal refund | keel_derived | exactly one constant |
| MCP payment connectors | the verified tool name | keel_verified | open-ended — any tool name |
- Canonical payment rails. Keel sets the literal
payment.executewhen the request matches a canonical rail. The value is a constant Keel chose, though the fact is labelledrequest_bound. - Registry-derived. When no typed payment adapter applies but Keel’s action
registry tags the action as value movement and as a refund, the operation is
the literal
refund. This path emits no other value; anything else the registry knows resolves tounknown. - MCP payment connectors. The verified tool name from the decision trace, verbatim and case-sensitive, with no equality check against the permit’s action name. This is the open-ended one.
The MCP value space collides with the constants. Nothing stops an upstream
MCP server naming a tool payment.execute or refund. If it does, this field
publishes that name and an allowlist such as
operation in ["payment.execute", "refund"] matches a third-party tool call
as though it were Keel’s own canonical rail or a registry-typed refund. The
allowlist cannot tell the two apart, because at this path the value is the
tool name.
Pair the operation with connector.identity.value whenever you match on it, so
a rule meant for the canonical rail cannot be satisfied by a connector tool that
happens to share the name.
States. present when one of those paths produced a value. unknown when
the action may be financial but no typed operation could be established.
not_applicable when the action is not value movement. Never present on
realtime.
Security caveats. This is not a closed enum. One derivation path returns
a connector tool name, so the value space grows with the tools your project
connects. That has two hard consequences. First, never build an allow-shaped
rule on a negative match: a not_in or neq gate admits every operation string
that does not yet exist, including the next money-moving tool somebody adds.
Enumerate the operations you intend to permit with in, and treat anything else
— including unknown — as needing review. Second, because matching is
case-sensitive and the MCP path is verbatim, an allowlist entry that differs
only in case will not match.
{
"all": [
{
"field": "context._keel.action_envelope.connector.identity.value",
"op": "in",
"value": ["stripe", "payments"]
},
{
"field": "context._keel.action_envelope.financial.operation.value",
"op": "in",
"value": ["payment.execute", "refund"]
},
{
"field": "context._keel.action_envelope.financial.amount_usd_micros.state",
"op": "eq",
"value": "present"
}
]
}Availability by surface
Availability is not the same as trust, and the two are independent. A field
that is not_applicable on a surface keeps whatever provenance it has; it
simply has no value to give there. Nothing in this table promotes a
caller_asserted field.
Generated from the committed Keel API contract 4daab88ced66610d3e8ae9c47caecc75842d5eb7
using artifacts/keel-api/4daab88ced66610d3e8ae9c47caecc75842d5eb7/action-envelope-availability.json (SHA-256 872256a1ab3696850283da50dca2ee8ec300dc1490ad22e005b63f1730a72355). Do not edit this table by hand.
| Field | Permit | MCP | Realtime |
|---|---|---|---|
action.access_level.value | yes | yes | yes |
action.risk_tags.value | yes | yes | yes |
connector.identity.value | yes | yes | no |
connector.tool_name.value | yes | yes | no |
connector.tool_schema_hash.state | yes | yes | yes |
financial.moves_customer_value.state | yes | yes | yes |
financial.moves_customer_value.value | yes | yes | yes |
financial.operation.value | yes | yes | no |
financial.amount.state | yes | yes | yes |
financial.amount.value | yes | yes | no |
financial.amount_usd_micros.state | yes | yes | yes |
financial.amount_usd_micros.value | yes | yes | no |
financial.currency.value | yes | yes | no |
financial.destination_digest.state | yes | yes | yes |
financial.destination_digest.value | yes | yes | no |
authority.is_mutation.state | yes | yes | yes |
authority.is_mutation.value | yes | yes | yes |
authority.compute_lineage_only.value | yes | yes | yes |
pricing.estimated_cost_usd_micros.state | yes | yes | yes |
pricing.estimated_cost_usd_micros.value | yes | no | yes |
Availability is per leaf. In particular,
financial.amount_usd_micros.state is matchable on Realtime because it resolves to
unknown or not_applicable, while financial.amount_usd_micros.value cannot reach
present there. The two rows are intentionally different.
A field marked available on a surface can still resolve to unknown or
not_applicable for any individual request. Availability describes the
contract; .state describes the request.
How these facts are recorded
Provenance answers where a value came from. This section answers a different question that policy authors and auditors ask next: once Keel decides, what protects the record of the facts it decided on?
Keel records these facts and protects them against ordinary database writes. The permit signature does not cover them. That is the whole classification: recorded, with local write protection. Not attested by the permit signature, and not independently anchored. The rest of this section is the detail behind that sentence.
Where the envelope goes
The envelope is built during evaluation and lands in two places:
- The policy evaluation context, at
context._keel.action_envelope— the paths this page documents, which your rules read. - The decision snapshot, at
GovernanceDecisionSnapshot.engine_context_json.trusted_action_envelope, alongside a frozen capture of the authorizer input.
It does not land in the permit’s resource_attributes_json.
What the permit signature covers
A v6 permit binds resource_attributes_canonical_hash — an RFC 8785 digest of
resource_attributes_json — into the signed preimage. Because the envelope is
not in that object, that digest does not cover it. The v7 required field set is
resource_attributes_canonical_hash, authority_chain_digest,
quota_reservation_id, subject_id, subject_type, account_id, and
org_id: no envelope field, and no decision-snapshot hash.
The signed preimage does carry policy_snapshot_hash, and it is easy to read
that as covering the evaluation. It does not. It hashes the policy identity and
version, the outcome, the reason, the constraints, and the budgets — the
decision and the policy that produced it. Evaluation input is not in it. Its
purpose is to make a post-hoc policy edit break re-verification, not to bind the
facts the policy read.
So: a verifier that checks a permit signature has checked the decision, the
policy that produced it, and the request attributes hashed into
resource_attributes_json. It has not checked a single field on this page.
What does protect the record
The decision snapshot carries its own snapshot_hash over its stored content,
and the row is write-protected at two layers:
- ORM guards.
before_updateandbefore_deletehooks onGovernanceDecisionSnapshotraise rather than let application code modify or remove a snapshot. - Database triggers. PostgreSQL triggers raise on
UPDATEand onDELETEagainstgovernance_decision_snapshots. The update trigger permits exactly one narrow case — a foreign-keySET NULLthat blanksproject_idwhile every other column stays byte-identical — and rejects everything else.
That is real protection, and it is the reason this page keeps telling you to
read the fact’s trust label in the decision snapshot: the snapshot is the
record of what actually happened, and ordinary writes cannot rewrite it.
What it is not
The snapshot_hash is a local integrity hash over stored content. It is not
signed, not hash-chained, and not externally anchored. It is not in the permit
preimage, so a permit signature does not vouch for it, and nothing else signs it
either. Its guarantee is scoped to Keel’s own database: it detects a snapshot
that no longer matches its own hash, and the triggers stop the writes that would
cause that. It says nothing to a party who does not trust that database.
Keel does run a signed, Rekor-anchored integrity checkpoint — but over the governance event chain, not over decision snapshots. The trusted action envelope is not in that chain, so the anchoring that exists elsewhere in the product does not reach these facts.
What this means in practice. Treat the envelope as high-quality operational
evidence, and cite it as such: it is durable, write-protected, and it records
what Keel saw. Do not describe it to an auditor, a counterparty, or a customer
as signed or attested, and do not build a dispute-resolution story that depends
on a third party verifying these facts from a permit signature. If that is what
you need, the material that is signed is the permit’s own decision fields and
resource_attributes_json — see Threat Model for what the
signature is designed to establish.
Known limitations
These are open inaccuracies in the product, not in this page. They are listed
here so a policy author can plan around them. Each was re-checked against
main when this page was last revised.
Resolved since the previous revision. The catalog used to report
financial.currency.value and financial.operation.value as keel_derived
while the envelope stamped them request_bound on the canonical rail. Both are
now declared caller_asserted, and so is financial.amount_usd_micros.value.
The catalog and the envelope agree on these three; the limitation is gone and
its entry has been removed rather than left standing.
Still open:
- On the MCP surface the catalog understates provenance for five financial
fields.
financial.amount.value,amount_usd_micros.value,currency.value,operation.value, anddestination_digest.valueare all declaredcaller_asserted, but on a connector-bound MCP call Keel’s semantic adapter genuinely derives them and the envelope stampskeel_derived. A field carries one provenance across every surface, so the catalog declares the weaker reading deliberately. The consequence is only ever conservative: these fields are worth more on MCP than the label admits, never less. Keel’s provenance check treats an overclaim as a build failure and an underclaim as a recorded contradiction; there are currently five underclaims and no overclaims. - The Stable set contains no surface-independent “is this value movement?”
predicate — and, since the amount was demoted, nothing financial at all.
financial.moves_customer_valueis the right field for the job and iskeel_derivedon every surface, but it is a classifier judgment rather than an extracted fact, so it sits in Preview. The absence guard in the correct pattern uses it deliberately and marks the trade-off. - The trusted action envelope is not covered by the permit signature. It is recorded in the decision snapshot and write-protected there, but it is not signed, not hash-chained, and not externally anchored. See how these facts are recorded — this is a property of the design as it stands, not a defect awaiting a fix, and it is listed here because it is the assumption most often made in the other direction.
- Bound MCP tool names are case-preserving while permit
requested_actionis lower-cased. The asymmetry is deliberate at this point: changing normalization would alter matching for every existing policy at once. Author tool-name matches with the exact casing the connector advertises.
Related pages
- Policy Conditions — operators, path resolution, and fail-closed semantics
- Policy Reference — rule actions and evaluation order
- Read/Write Governance — the flat
context._keel.action_access_*family - Decision Model — how policies combine
- Surface Maturity — what a stability tier commits Keel to
- Threat Model — what Keel does and does not defend against