Grant an App access to a connection¶
You have connected a mailbox, calendar or drive. An App — a headless assistant, a scheduling agent — needs to act on part of it. This page issues that access, checks it, and revokes it.
You must own the connection. There is no org-admin path: an administrator cannot grant on your connection for you, and cannot revoke a grant you made.
Step 1 — Find the connection¶
The grant commands take a reference to a connection you already have, and there is no owner-scoped way to list connections — get the id from the portal, where you connected it.
That is a prerequisite, not a step of the task: granting, checking and revoking all happen on the CLI or the API, which is what this page is badged for.
The API listing is an admin operation, not the owner's route
exchangeConnections(orgId:) exists, but listing and deleting connections
are org ADMIN management operations. Operating a mailbox is owner-only;
enumerating an org's connections is administration. So unless you are also
an org ADMIN, that query will not return your own connection, and the
portal is the way in.
Step 2 — Decide the scopes¶
A grant carries one or more scopes, and each operation an agent runs maps to exactly one:
| Scope | What it lets the grantee do |
|---|---|
mail.read |
List messages and folders |
mail.send |
Reply to a message |
calendar.freebusy |
Ask whether you are busy — not what you are doing |
calendar.read |
List calendars and read events, including their contents |
drive.read |
List, search, fetch, export and download files |
Grant the narrowest set that does the job, and note that
calendar.freebusy is not a weaker calendar.read — it answers a different
question, and is the right scope for anything that only needs to schedule.
Replying is the only write a grant can carry. Every other mutation is owner-only — nothing a grant holds reaches:
- mail — deleting, moving, marking read, flagging, categorizing
- calendar — creating, updating, cancelling or responding to events
- drive — creating documents
If an agent needs one of those, delegation is not the mechanism. The pattern is worth holding rather than the list: a grantee reads, and answers mail. Anything that changes your data in any other way stays with you.
Drafts: treat as owner-only until confirmed
The draft operations (save, update, send) sit in an unresolved spot.
The operation→scope table maps them to mail.send, while the GraphQL
mutations that perform them (emailSaveDraft, emailUpdateDraft,
emailSendDraft) are each documented "Owner-only."
Those two describe different layers and may both be right, but until that is settled this page will not tell you a grantee can drive drafts. Plan for owner-only — the stricter reading is the safe one to be wrong about.
Step 3 — Create the grant¶
hadron connection grant create \
--connection <connection-id> \
--app hrn:app:acme.com:scheduling-assistant \
--scopes calendar.freebusy
--app takes an App id or URN. Without --expires-at the grant lasts until
you revoke it. To bound it, pass an ISO-8601 timestamp in the future —
computed, so the example cannot go stale:
# 90 days out; the first form is BSD/macOS, the fallback is GNU/Linux
EXPIRY=$(date -u -v+90d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|| date -u -d '+90 days' +%Y-%m-%dT%H:%M:%SZ)
hadron connection grant create \
--connection <connection-id> \
--app hrn:app:acme.com:scheduling-assistant \
--scopes calendar.freebusy \
--expires-at "$EXPIRY"
Prefer an expiry when you know the end date
A contractor engagement or a trial has a date. An expiry you set once beats a revocation you have to remember, and it fails in the safe direction if you forget.
Adding a scope later does not mean editing this grant. Multiple live grants on the same connection and App are valid and their scopes union, so you can issue a second grant for the extra scope and revoke it independently.
Step 4 — Check what you have granted¶
Run it without --connection to see every grant you have issued. This is the
review surface — the answer to "what can that assistant currently see?" — so
it is worth a look after any change, and periodically after none.
Step 5 — Revoke¶
Revocation takes effect immediately: liveness is re-checked on every operation rather than cached, so there is no window in which a revoked grant still works. It is a soft delete, so the record of what was granted survives for audit.
Revoking a grant does nothing to the connection. It stays connected and everything else keeps working.
Doing it through the API¶
mutation($conn: String!, $app: String!, $scopes: [String!]!) {
createConnectionGrant(connectionRef: $conn, appRef: $app, scopes: $scopes) {
id scopes expiresAt
}
}
connectionGrants(connectionRef:) lists them and revokeConnectionGrant(ref:)
takes one back. Same gate as the CLI: owner-only, both ways.
When something refuses¶
| What you see | What it means |
|---|---|
| Refused on create, and you are not the connection's owner | Correct. Owner-only, with no admin bypass. Ask the owner to issue it. |
| An unrecognised scope is rejected | The scope list is a fixed allow-list — check the spelling against the table above. |
| The agent still cannot perform an operation it has the scope for | Check the operation is delegable at all: the owner-only set above is refused for every grantee, whatever scopes they hold. |
A grant you expected is missing from grant list |
It expired or it was revoked — two different things. Revocation is a soft delete (the row is marked deleted); expiry just means expiresAt has passed, and the row is untouched. Neither is an edit, so the fix is a new grant, not an undo. |
Related¶
- Connections and delegated access — why the grantee is an App install rather than an Agent, and why an unmapped operation is permanently owner-only.
hadronCLI reference — the command surface.- Authorization — the org-level roles and grants this sits beside, and does not overlap with.