Approvals are real end to end on this surface, because the approvals table is
already keyed on (company, tenant_user) — which is exactly the address these
routes use.
List pending approvals
next_cursor is always null here. This list takes a limit, not a cursor. If
you have more pending approvals than your limit, raise the limit (up to 200) —
paging past that is not available on this address yet, and the response says so
rather than minting a cursor the next call could not honour.
Decide
Body
One write path, two addresses
This route calls the same service the existing
/v1/users/{user_id}/approvals/{id}/approve
and /deny routes call. It is a second address
for one write path, not a second write path — so these two rules hold identically
here:
- A human must resolve. An agent-authored resolution is rejected. Approving
with an API key that authenticates an agent does not work.
- No self-approval. The actor that requested an approval cannot resolve it.
Approving replays the frozen payload captured when the approval was created.
It does not re-evaluate the request against current policy.
One rule that does NOT hold identically, measured. The legacy approval
addresses re-check that the calling agent profile is still active before a
mutating request; this address does not. A credential that has been revoked can
still reach POST …/decide here after it has stopped working at
/v1/users/{user_id}/approvals/….This is the same class of gap that
GET …/plan reports as
revocation_on_reads.enforced: false, except that here it extends to a write. If
you revoke an operator for a security reason, rotate the key — do not rely on
revocation alone to close this surface.
Why grant is null
A grant in the durable-runtime design is argument-bound, single-use and pinned to a
policy snapshot. Nothing here mints one: approving replays a stored payload. The
field is null with the reason beside it rather than absent, so a client written
against the eventual shape does not have to guess whether the key is missing or the
value is.
decision_id and trace_id are null for the same reason — there is no decision
ledger and no trace column in this build.