Steps 7 and 8 of the request flow, written as small functions over the
executor's request state so PhoenixKit.API.Executor.execute/4 stays a plain chain:
... Flow.merge_header -> validate -> Flow.split_control ->
Flow.idempotency -> Flow.gate -> runEach step returns {:ok, state} to go on, {:error, code, detail, context, state}
to refuse (the executor answers a problem document), or {:done, state} when the
answer is already in state.body (a preview, a replay): the executor then responds
without running the handler. finish/1 and release/1 are called when the
response is sent: they store the answer under the idempotency claim, or give the
claim up.
The request state is the executor's map. Besides its own keys the steps read and set
control, dry_run?, input_hash, claim, replayed?, held_again?, store?,
outcome, confirmation, preview and result (the answer as a decoded map, for a
transport that wants the map rather than the text). The executor resolves
request_context once, before the gate, for every context the steps build.
Summary
Functions
Called when the answer is sent. Stores a 200 body under the claim when it may be
stored (Idempotency.storable?/2, given the tool's own credential fields), otherwise gives
the claim up. Never raises: the handler has already run, so a store that fails (a database
hiccup) is logged and the answer is sent anyway; the claim then runs out with its lease.
Step 8. A read tool passes. dry_run: true runs the handler with ctx.dry_run? == true
and answers the preview (no code, no approval). A write of a person's live session
(kind: :user) never runs on this call, dangerous or not: it is held for the person to
confirm (session_flow/1). Any other write call of a key that requires approval goes to
the approval queue, dangerous or not, and a confirm_token from such a key is refused.
For every other caller a dangerous call (dangerous?/1) goes through the confirmation
flow, and any other call passes.
Claims the request's Idempotency-Key, if it has one. A dry run never claims.
A public call without a client token has no scope to claim in and ignores the key.
The Idempotency-Key header of a write call becomes the idempotency_key field, so
the schema validates it like the field. Both given and different, or the header sent
twice, is a validation error. A read tool ignores the header, and so does a request
that does not read it (idempotency_header?: false, the MCP transport: its key is the
argument), and a write tool that cannot replay (ControlFields.replay_tool?/1).
The person confirms the write their session's call was held for: the step that takes
the place of gate/1 in PhoenixKit.API.Executor.redeem_as/6. The state has passed the
whole request path again, so this checks only the row: it must be the person's
(Confirm.lookup_for_user/2), for the same tool and arguments, still waiting
(Confirm.redeemable/4); then the handler runs as a dry run for a fresh fingerprint and
Confirm.claim_token/3 spends the row, atomically. {:ok, state} goes on to run the
handler.
Called when the request is refused: gives the idempotency claim up. Never raises.
Takes the control fields out of the validated input. The handler gets what is left;
input_hash is computed from it, so a confirming call hashes like its preview.
Functions
Called when the answer is sent. Stores a 200 body under the claim when it may be
stored (Idempotency.storable?/2, given the tool's own credential fields), otherwise gives
the claim up. Never raises: the handler has already run, so a store that fails (a database
hiccup) is logged and the answer is sent anyway; the claim then runs out with its lease.
Step 8. A read tool passes. dry_run: true runs the handler with ctx.dry_run? == true
and answers the preview (no code, no approval). A write of a person's live session
(kind: :user) never runs on this call, dangerous or not: it is held for the person to
confirm (session_flow/1). Any other write call of a key that requires approval goes to
the approval queue, dangerous or not, and a confirm_token from such a key is refused.
For every other caller a dangerous call (dangerous?/1) goes through the confirmation
flow, and any other call passes.
Claims the request's Idempotency-Key, if it has one. A dry run never claims.
A public call without a client token has no scope to claim in and ignores the key.
The Idempotency-Key header of a write call becomes the idempotency_key field, so
the schema validates it like the field. Both given and different, or the header sent
twice, is a validation error. A read tool ignores the header, and so does a request
that does not read it (idempotency_header?: false, the MCP transport: its key is the
argument), and a write tool that cannot replay (ControlFields.replay_tool?/1).
The person confirms the write their session's call was held for: the step that takes
the place of gate/1 in PhoenixKit.API.Executor.redeem_as/6. The state has passed the
whole request path again, so this checks only the row: it must be the person's
(Confirm.lookup_for_user/2), for the same tool and arguments, still waiting
(Confirm.redeemable/4); then the handler runs as a dry run for a fresh fingerprint and
Confirm.claim_token/3 spends the row, atomically. {:ok, state} goes on to run the
handler.
When the values changed since the preview, nothing runs: the row is replaced by a new
one with the fresh preview (Confirm.reissue_for_user/7) and the answer is the prompt of
the new row with "reason" => "changed". Any other refusal is 409 confirm_token_invalid with context.reason invalid (unknown or somebody else's),
other_arguments, used, rejected or expired, without a preview.
Called when the request is refused: gives the idempotency claim up. Never raises.
Takes the control fields out of the validated input. The handler gets what is left;
input_hash is computed from it, so a confirming call hashes like its preview.
The input of a write tool must not hold a NUL character (JSON \u0000): a code or an
approval keeps the input and its preview in jsonb, which cannot store one, so the call
is refused here, before anything is claimed or written.