The gate
Ask before you act: an agent declares a registered action and its input, and your organisation's policy answers allow, refuse, or a person decides.
The other requests ask a named person to look at something. The gate asks your organisation's policy, before the action rather than after it. The agent says what it is about to do and with what arguments; rules an administrator wrote answer. Most calls are settled by a rule and never reach anybody, and when no rule settles one it becomes an ordinary approval.
What a gate does, and what it does not. Three different things are true of a gate, and they are worth keeping apart. Which one applies depends on who carries out the action.
- Approval coordination
- Any action Deliverd does not carry out itself — a payment, a deployment, a message. The agent asks before it acts, a rule or a person answers, and the agent acts on the answer. The decision is advisory to the agent's runtime: Deliverd records the question and the answer, and does not see the act. Where the code that performs the act is yours, it can check the signed decision token against the input before acting, and turn away one nobody agreed to.
- Enforcement
- Only where Deliverd itself performs the act. Publishing: publishing policy is applied on the server to every publish and every share, and an agent cannot make a report public. And a staged report release, POST /api/v1/gates/{id}/release, where Deliverd makes the approved version live itself.
- Execution evidence
- Every receipt an agent reports after acting. Each receipt says how much was checked: verified when Deliverd did it, hash_matched when the reported artifact's hash matched the approved one, and reported when it is the agent's word. A receipt whose hash, destination or uses differ from the permission is refused.
Mediated execution for arbitrary actions, and stronger attestation of what an agent did, are on the roadmap and not built.
- 1
Register the actions
Your deploy names the consequential things your software does —
finance.refund— with a schema for their input. - 2
Write the policy
An administrator writes rules against those actions. See Agent policy.
- 3
Gate the call
Before it acts, the agent calls the gate with the action and the input it will use, and branches on
allowed.
Registering actions
An action is a stable name for something consequential, registered once per organisation so a rule can be written about it. Register from your deploy with deliverd actions push (see CLI) or the API (actions:write, with a key belonging to an owner or administrator):
curl -X POST "$DELIVERD_URL/api/v1/actions" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"actionId": "finance.refund",
"name": "Issue customer refund",
"displayTitle": "Refund {{amount}} {{currency}} to {{customer_id}}",
"editableFields": ["amount"],
"inputSchema": {
"type": "object",
"required": ["customer_id", "amount", "currency"],
"properties": {
"customer_id": { "type": "string" },
"amount": { "type": "number", "minimum": 0 },
"currency": { "type": "string", "enum": ["GBP", "EUR", "USD"] }
}
}
}'- It is an upsert: registering the same id again is the normal case, because it runs on every deploy. An administrator's title, risk category and per-field sensitivity labels are kept separately and survive it.
- The schema is a small subset of JSON Schema:
type,propertiesandrequired, with string, number, integer or boolean fields andenum,minimum,maximum,minLength,maxLengthandpattern. An unknown keyword is refused (400 invalid_definition) rather than ignored. editableFieldsnames what an approver may correct if the gate sends the request to a person.- An agent key is refused (
403 agent_not_permitted). An agent that could name actions could name its way around the rule that governs it. Register with a key that belongs to a person.
Calling the gate
TypeScript
const decision = await deliverd.gate({
action: "finance.refund",
externalId: `refund:${order.id}`,
input: {
customer_id: order.customerId,
amount: 1240,
currency: "GBP",
},
});
if (!decision.allowed) return;
await stripe.refunds.create(decision.input);gate() waits by default: a rule's answer returns at once, and a request that went to a person waits until they decide. Pass wait: false to return immediately with a pending decision, or use deliverd.gates.create() in a handler that must not hold a connection open. In Python the call is deliverd.gate(action=…, external_id=…, input=…). Over MCP it is check_gate, with get_gate to read a pending decision back.
Over REST it is POST /api/v1/gates (gates:write), then GET /api/v1/gates/{id} (gates:read) while a person decides:
curl -X POST "$DELIVERD_URL/api/v1/gates" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"actionId": "finance.refund",
"externalId": "refund:ord_8812",
"input": { "customer_id": "cus_1", "amount": 1240, "currency": "GBP" },
"reason": "Customer reported a duplicate charge"
}'A first ask answers 201. environment is optional and lets rules tell staging from production; reason is shown to whoever is asked to decide.
The answer
Branch on canExecute: authorised, inside its window, with a use left, not revoked. allowed says a rule or a person agreed at some point; status says why:
status | allowed | What happened | What the agent does |
|---|---|---|---|
allowed | true | A rule permitted it, and none of your ethics rules asked for a person. | The agent carries on. Nobody was interrupted, and the decision is still written down. |
denied | false | A rule refused it — a policy rule, or one of your ethics rules. | The agent stops, and reads which rule and why. A refusal is an answer, not an error — it arrives as a 200 rather than down the failure path. |
pending | false | No rule settled it, or an ethics rule asked for a person to weigh it. | It became an ordinary approval: a named person decides on the page they already use, and the call can wait for them. |
approved | true | A person agreed to it. | Carries on, with the input that came back. |
rejected, expired, cancelled | false | Nobody said yes. | Does not act. |
changes_requested | false | A person sent it back with a note. | Revises, then asks again with supersedesGateId. |
revoked | false | Somebody withdrew the permission before it was used. | Does not act. |
used | false | The permission has been spent. | Asks again for the next act. |
reason and rule name the rule that answered, basis says whether a rule or a person decided, approvalUrl is where a pending request is waiting, and changes lists anything the approver corrected.
Four things to know
- Two scopes
- gates:write raises a decision, gates:read polls one. Both are in the default set a user key gets, so a key you already have can try it.
- A refusal is a 200
- Policy saying no is a result you branch on, not an exception. There is no gate_denied error code and there deliberately never was — routing the single most ordinary outcome down every SDK's failure path would make the normal case look like a bug.
- An agent key may call it
- The only new surface of which that is true. Registering actions and writing policy both refuse an agent principal, because an agent that names the actions or writes the rules is deciding for itself. Submitting to a rule is the opposite of that — an agent that cannot ask can only act without asking.
- externalId is required
- Which no other POST in this API is. A random per-attempt key protects a retried request; it does not protect a retried decision, and a crashed agent re-running its refund step would otherwise raise a second approval for the same money. Only you know which two calls are the same unit of work.
With the same externalId, a retry by the same caller, for the same act in the same environment, re-reads its decision and answers 200 with reused: true instead of raising a second one. The same externalId from another caller, environment, risk or platform is refused as a conflict.
What a permission covers
Name what the act touches when you ask: destination ({ type, id }), classification, risk, and the artifact with its sha256 — or a Deliverd report and version, whose hash Deliverd reads itself. Policy can match on all of them, and whether the destination is inside your organisation is worked out by Deliverd from your directory, never taken from the request. The permission is bound to exactly that: scope in the answer says what, and authorisation says until when and how many uses (one unless the policy says otherwise).
After acting
- Report what you did:
POST /api/v1/gates/{id}/execution(gates:execute), MCPrecord_execution, orgate.recordExecution(). Send the sameartifactSha256anddestination. A different hash is refused asapproval_invalidated, another destination asdestination_mismatch, a second use asgate_used— and nothing is recorded. phase: "started"claims the use before acting, which is the safe order for an agent that may be retried: the second copy is told the permission is spent before it acts. Finish it withcompletedorfailedand theclaimId.- Each receipt says how much Deliverd checked:
verifiedwhen Deliverd carried the act out itself (POST /api/v1/gates/{id}/releasemakes a staged report version live),hash_matchedwhen the reported hash matched the approved one,reportedotherwise. POST /api/v1/gates/{id}/cancelwithdraws a request, or a permission you have not used. An administrator, or somebody who approved it, can revoke an unused permission from its page under Agents → Agent governance.GET /api/v1/gates/{id}/evidenceis the whole record: the policy, who approved under what authority, the scope they saw, the receipts, and what was not checked.
It fails closed
- An unregistered action is refused with
404 unknown_action; input the action's schema does not describe is400 invalid_input, with aproblemslist. Neither proceeds. - An action no rule mentions goes to a person. This is the opposite of most permission systems, and deliberate: an action nobody has written a rule about is one nobody has decided about.
- Gate policy rules are applied on the Business Starter, Business and Enterprise plans. On other plans they are stored but not applied, so every gated action goes to a person.
- When your organisation's ethics rules flag a gated action, it goes to a person even if a rule would have allowed it; a rule that refuses turns the answer into
denied.
Checking the yes where the action happens
When the code that performs the action is yours, it does not have to take the agent's word. A gate that may be acted on carries decisionToken: the yes, signed with your deployment's Ed25519 key, binding the organisation, the gate, the action, a SHA-256 of the approved input, the destination and artifact hash when the gate named them, and an expiry of at most fifteen minutes. The agent passes it along with the request; the service that issues the refund checks it before it does:
TypeScript
// In the refund service, before it refunds anything
const check = await deliverd.decisionTokens.verify(req.headers["deliverd-decision"], {
action: "finance.refund",
input: req.body, // what this service is about to do
});
if (!check.ok) return res.status(403).json({ error: check.reason });- It turns away a token signed by any other key, one issued by another deployment, an expired one, one for another action, and one agreed for a different input: a yes for £1200 does not cover £1240. Key order in the input does not matter; the hash is over canonical JSON.
- The TypeScript SDK (
deliverd.decisionTokens.verify) and the Python SDK (client.decision_tokens.verify) check locally with the public key at/.well-known/deliverd-evidence-key, fetched once from the deployment the client talks to. Anything else can ask:POST /api/v1/gates/verify-token(gates:read) answersvalidand areason, and only for your own organisation's tokens. - A token does not count uses. Report the execution to spend the use, and a second report is refused as
gate_used.
The decision is advisory
Your agent reports what it intends to do, policy answers, and your agent then performs the action itself. Deliverd holds what the agent reports doing to what was agreed — the artifact's hash, the destination, the uses left — and refuses a report that differs, but it does not see the act, except a report release it carries out itself. An agent that misreports is misreporting to its own runtime, which is the trust boundary every client library already has — but it is a boundary, and this is where it sits. What the gate gives you is a question asked before the act, answered by somebody other than the caller, and written down either way, and a signed answer your own code can check where the act happens. The platform your agent runs on keeps its own confirmations and safety checks; a permission here never replaces them.
Try it on your own agent.
Free to start, no card. Sign up with Google, Microsoft or GitHub, and the first page after hands you an API key.