Grants
A release gate checks that evidence was presented with an action. A grant gate checks who cleared it. A GRANT-v1 record is an authorisation signed by a designated approver key: a named tool scope, an optional resource and environment, a validity window, and a use budget. A tool under a require_grant rule clears only when a valid grant is presented with the action, and the requester cannot mint its own authority. A grant signed by any key other than a designated approver is refused, and the refusal says so by name.
The decision is signed either way. A refused action leaves an ACTION-v1 record that shows the gate operated, checkable offline against the published key.
The rule
Add a require_grant rule to an active policy. The value is a tool pattern, a colon, and one or more approver public keys (Ed25519, base64url), comma-separated:
json{ "name": "press-cell-authority", "rules": [ { "type": "require_grant", "value": "press.*:<approver public key, base64url>" } ] }
press.* matches every tool whose name starts with press.; an exact name works too. Tools outside the pattern are untouched by this rule.
The grant record
A grant is a JSON object carrying these fields, signed by the approver over their canonical form:
| Field | Meaning |
|---|---|
v | Format version |
grant_id | Unique id of this grant |
intent_ref | Optional. Names the user authorisation this grant serves, so a reissue for a retry inherits the spent budget instead of resetting it |
org_id | The organisation the grant was issued for |
approver | The approver's public key |
signing_mode | How the approver key is held |
scope_tool | The tool, or tool pattern, the grant covers |
scope_resource, scope_environment | Optional. The declared action must match them when set |
max_uses | Decimal string; empty means unlimited |
not_before, not_after | Validity window, ISO 8601 |
require_args | Optional. Evidence keys the action must carry |
Presenting it
Put the grant under args.aqta_grant when authorising the action:
bashcurl -X POST https://api.aqta.ai/v1/actions/authorize \ -H "Authorization: Bearer $SEAL_API_KEY" -H "Content-Type: application/json" \ -d '{ "tool": "press.cycle", "agent": "scheduler/2.1", "args": { "setpoint_kn": 180, "aqta_grant": { "v": "grant-1", "grant_id": "g-7f3a…", "scope_tool": "press.*", "max_uses": "1", "…": "…" } } }'
With a valid grant the response is "outcome": "ALLOWED" and a signed record. When the grant has a use budget the response carries grant_use (which use this was, and how many remain). It also carries args_bound: the exact object args_hash commits to, with the grant's content hash bound into it in place of the grant itself. Store it beside the grant record in the evidence pack; a reviewer holding both can recompute the hash and confirm this action was cleared under exactly that authority.
Refusals, by name
Each refusal is a signed BLOCKED record whose reason names the cause:
- No grant:
tool 'press.cycle' requires a grant from a designated approver and none authorises this action: no grant was presented under args.aqta_grant - Self-signed:
… grant is not signed by a designated approver key - Wrong scope:
… action 'press.cycle' is outside the granted scope 'press.calibrate' - Expired or early:
… grant has expired,… grant is not yet valid - Missing evidence:
… grant requires evidence not presented: approved_by
Authority is not necessarily reusable
A grant may be scoped to a particular action, an expiry window, and a number of uses. Seal fails closed when the permitted uses have been exhausted. Four behaviours you can observe at the boundary:
max_useson the grant sets the budget; empty means unlimited.- Each cleared action returns
grant_usein the response:{"use_index": 2, "max_uses": 3, "remaining": 1}. - The use after the last is refused:
Blocked: the grant presented does not have a use left. grant already used 3 of 3 times. - Two concurrent attempts on the last use: exactly one clears; the other is refused, and that refusal is signed.
Single use, claimed atomically
max_uses is enforced after policy has cleared the action, by an atomic claim against a unique index, so one use cannot be spent twice even under concurrent requests. The loser of a race is refused, and that refusal is signed like any other decision. A refused action never spends a use.
Counting is keyed on the authorisation, not the grant. When the approver sets intent_ref, every grant issued for that intent shares one budget: an agent that retries, replans or resumes after a crash and asks again gets a fresh grant, and the fresh grant inherits the uses already spent. That closes the semantic-replay case where every grant was correctly single-use but the user's single authorisation ran twice. Without an intent_ref, uses count against the grant alone. An intent reference cannot collide with a grant id, and it is covered by the signature.
What it proves, and where it stops
- It proves that a designated key authorised this scope at this time, that the action fell inside it, and that the boundary counted the use.
- It does not prove the approver was entitled to approve, that the key was held by the person you expect, or what ran after clearance unless the boundary performed the action itself.
Where this runs
Every refusal path above has a test. The same rule governs a physical action in the edge signer's press-cell demonstration: a press cycle needs the plant approver's grant, the scheduler's own grant is refused by name, and power is released only through a single-use permit. See Release gates for the evidence half of the boundary.