Skip to main content
A decision is a pure function of the request. Kordio calls nothing while it decides, reads nothing but the fields you sent and the limits already on record, and answers in the same request: allowed, needs a person, or denied.

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:
  1. session_inactive: the budget must be active.
  2. runtime_scope: the agent’s key must be scoped for this action type.
  3. session_budget: the spend must fit the remaining budget.
Then the rules compiled from each active policy run, oldest policy first, with imported modules compiled in ahead of a policy’s own rules. The first denial wins, immediately. An approval requirement only stands if nothing denies. That ordering matters: a rule that would hold something for a human never overrides a rule that refuses it outright.
An agent with no active policy denies everything, with the rule no_policy. The intrinsic rules do not count as a policy. This is fail-closed by design: an agent nobody has written rules for has no authority rather than unlimited authority. Two ways in: creating the agent with skip_starter_policy: true, or disabling the last policy it has. A new agent otherwise arrives with an active starter policy, so no_policy is a state you choose rather than one you land in.
An 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 carries headroom, 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 carries policy_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 a remedy: what would have to change for the call to go through.
Branch on 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 is test 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.