The shape of the flow
The executor verifying the cosignature is the part that makes this enforcement rather than advice. A decision stored only in our database binds an executor that chooses to ask us, every time. A signature binds anything that checks it.Evaluation order
Three intrinsic rules always run first. They cannot be disabled and they are not part of any policy you write:session_inactive: the budget must beactive.runtime_scope: the agent’s key must be scoped for this action type.session_budget: the spend must fit the remaining budget.
allowlist-mode policy that matched no allow rule denies with not_allowlisted. Each
allowlist policy is an independent gate, so attaching two means both must be satisfied.
Outcomes and states are different vocabularies
Two fields describe adjacent facts and they overlap without meaning the same thing.decision.outcome is what the policy engine answered. data.state is where the intent sits in
its lifecycle.
An allowed payment lands in
pending exactly like an allowed action does. Kordio executes
nothing, so there is no state in which it has already spent on your behalf. pending means you
hold the authority and have not yet said what you did with it.
An intent leaves pending only when you report the outcome. So requires_approval and denied
appear in both vocabularies and mean the same thing; every other value belongs to one or the
other.
Full state sets:
- Action intent:
pending,requires_approval,completed,failed,denied. - Payment intent:
pending,requires_approval,executed,failed,denied.
Why a budget and not just a cap
A per-transaction cap stops one big mistake. It does nothing about the same reasonable-looking action happening nineteen times. A budget is the answer to that. You open one per run, every authorized action commits against it, and it is what the loop eventually runs into. Reservations are held while an action is pending or waiting on a person, and released when it fails or is denied. A budget is claimed with a single conditional update rather than a lock, so parallel sub-agents under one agent contend at the row instead of queueing behind each other.A budget has no expiry and no close endpoint. It stops spend by running out, not by ending, so
an action nobody reports as
complete or fail holds its reservation for good. Open a budget
per run rather than reusing one.Headroom
Every decision carriesheadroom, reporting what each limit had left when the decision was
made, reduced to the tightest value per key.
Read it as before, not after. Headroom is computed ahead of the reservation, so on an allowed
decision session_remaining_cents still counts the amount you were just authorized to spend.
Subtract cost_cents yourself if you want the figure after this action.
Give it to your agent either way. An agent that knows it has $380 left on the budget picks a
cheaper vendor; an agent that only knows it was refused retries into the same wall.
Policy snapshot
Every decision also carriespolicy_snapshot: the active policies exactly as they read at the
instant the decision was made, each with its typed columns, its mode, its imports and its
rules array.
That is what makes a decision auditable after the fact. Widen a cap next month and the decision
recorded today still shows the narrower one, so “why was this allowed” has an answer that does
not depend on the policy still existing.
Each entry also carries imported_rules: the rules of every module the policy imports, resolved
at decision time. The imports array names the modules, imported_rules records what they said,
so editing a shared module next month does not quietly rewrite what today’s decision was judged by.
It is [] in exactly three cases, and each one means no policy ran: the agent has no active
policy (no_policy), a spend token refused before the engine started, or the budget was claimed
out from under the request between evaluation and reservation. An allowed decision always
carries at least one policy.
The remedy
A refusal that only says what broke leaves the agent to guess, and agents guess by retrying into the same wall. Every denial and every hold therefore carries aremedy: what would have
to change for the call to go through.
kind in code, show summary to a person. The amount in max_cents is the real
one: for a monthly cap it is what is left after this month’s spend, not the cap itself, and for
a budget it is what the budget has left rather than what was asked for.
Three kinds an agent can act on alone:
The rest need a person:
raise_budget, reopen_budget, allow_resource, allow_action,
grant_scope, create_policy, new_spend_token, and approve on a hold. On approve,
max_cents is the amount that would have cleared without anyone being asked.
remedy is null on an allow, and on the rare denial with no mechanical cause, such as a
free-form condition that matched. It is stored with the decision rather than recomputed, so it
records the fix as it stood at the time even if the policy has moved on since.
Policy version and frame hash
Alongside the snapshot, every decision carries two digests.policy_version is a content digest of that snapshot. The same rules always hash to the same
version and any edit produces a different one, so decisions group by the ruleset that judged them
and the same request under the same version replays to the same outcome. It reads pv_none when
no policy ran.
frame_hash is a SHA-256 over the request frame: action type, resource, cost, currency, agent,
budget, workspace and the whole metadata object. It binds the decision to the exact request that
produced it, including metadata that no response echoes back. Unlike the snapshot, it is present
on every decision, including the denials that never reached the policy engine.
Both are repeated in the cosignature claims, so whoever executes the action can pin the decision
without calling back. See Cosignatures.
Payments work like actions
POST /control/v1/agent/payment_intents is a separate resource from POST /control/v1/agent/actions, but it
behaves the same way: Kordio authorizes, you execute, you report back. It holds no funds and
moves no money, so there is nothing it could execute on your behalf.
One difference remains, and it will bite you if you assume symmetry.
The terminal success state differs in name only: an action ends
completed, a payment ends
executed. Both mean the same thing, that you told us the work went through.
A payment is still evaluated as the action type payment.create with the counterparty as its
resource, so an agent’s caps, windows and allowlists apply across payments and other actions
together. Spend on one consumes headroom for the other.
Test and live
An agent istest or live. Both run the same engine, evaluate the same policies, and return
the same shapes, including cosignatures. The difference is that live agents authorize real
money, and creating one needs a plan with live mode on it.
You can therefore prove the entire integration, including cosignature verification inside your
own executor, before anything is at stake.
Next steps
Writing a policy
Every rule kind, and the condition language.
Holding spend for a person
What a
202 means and how it resolves.What Kordio guarantees
The properties this ordering is meant to give you.
Where the ledger fits
What the projection mirrors, and what it does not promise.