# Identity and permissions for agents

Understand what your credential can do, why a call was denied, and how to get an agent its own scoped identity. This page is written for an AI agent. It extends the deployment protocol at `https://app.tokay.io/llms.txt`. Everything here is an escape hatch. The happy path needs none of it, so act on an explicit user request or a named readiness action, not on your own initiative.

## The authority model

- There are two kinds of tokens, and they do not mix. Platform credentials (person sessions, API tokens, bot tokens) authenticate against the Tokay API. Machine tokens authenticate against a deployed app's own URL and never work on the API. See the access reference for machine tokens.
- Effective authority is an intersection. A token acts within the live memberships of its owning principal, and a `GRANT_LIMITED` token is further narrowed to its explicit grant actions. Holding a credential never adds memberships.
- Authority is checked on every request. Revoking a token or removing a membership changes what the very next call can do, with no session to expire.
- Secrets are write only through the API. A credential that can set secret values still cannot read them back, which keeps your context and logs from becoming a credential store.
- Every material action is attributed to the principal and the specific credential that performed it, so prefer a dedicated agent identity over borrowing a person's token for anything durable.

## What to call

| To | Use |
|---|---|
| Create an agent identity with memberships and a token | `createBot` |
| Issue another token for an existing bot | `createBotToken` |
| See the action implication map | `permissionActionImplications` |
| Rename or revoke a platform token | `renameToken` / `revokeToken` |
| Remove an agent identity | `removeBot` |

## Create an agent identity

One call creates the bot, its memberships, and its first token:

```
mutation Bot($workspace: ID!, $project: ID!) {
  createBot(input: {
    workspace: $workspace,
    displayName: "deploy-agent",
    tokenLabel: "deploy-agent-token",
    tokenScopeMode: GRANT_LIMITED,
    grants: [{ action: PROJECT_STATUS }, { action: PROJECT_LOGS }, { action: PROJECT_DEPLOY }],
    projectMemberships: [{ project: $project, role: MEMBER }]
  }) {
    botCreation { token tokenLabel tokenScopeMode expiresAt bot { id } }
  }
}
```

The `token` is shown exactly once. `tokenScopeMode: FULL_MEMBERSHIP` skips grants and gives the token everything the bot's memberships allow. `GRANT_LIMITED` requires a matching grant for every action, which is the right default for automation. `repoMemberships` take `{ repo, role }` the same way, `expiresAt` bounds the token's lifetime, and `canMintTokens` decides whether the bot can issue further tokens. Use `createBotToken(input: { bot, tokenLabel, tokenScopeMode, grants })` to rotate or add tokens later.

## Grant actions

Grants use the `PermissionActionType` enum. The names are the same strings that appear in denial errors.

| Action | Covers |
|---|---|
| `WORKSPACE_READ` | Discover and read workspace context |
| `WORKSPACE_CREATE_PROJECT`, `WORKSPACE_CREATE_REPO` | Create projects and repositories |
| `WORKSPACE_MANAGE_ACCESS` | Members, bots, domains, workspace connections |
| `PROJECT_STATUS` | Read project and service state |
| `PROJECT_LOGS` | Logs, request payloads, runtime output |
| `PROJECT_QUERY` | Read only managed resource queries |
| `PROJECT_EXPORT` | Data exports and service file downloads |
| `PROJECT_WRITE` | Operate and configure services, trash and restore |
| `PROJECT_DEPLOY` | Deploy services and enable auto-deploy |
| `PROJECT_FILES_WRITE` | Service file uploads and rescue copies |
| `PROJECT_INVOKE` | Run app tasks, scheduled jobs, and test events |
| `PROJECT_MANAGE_RESOURCES` | Backing resources, outputs, resource restore |
| `PROJECT_WRITE_SECRETS` | Set and rotate secrets and service overrides |
| `PROJECT_MANAGE_ACCESS` | Access policy, visitors, machine tokens, endpoints, hosts |
| `PROJECT_DESTROY` | Purges, storage wipes, snapshot restore, destructive migration confirms, loosening release safety |
| `REPO_READ`, `REPO_WRITE`, `REPO_MANAGE_ACCESS`, `REPO_DESTROY` | The repository equivalents |

Grant the minimum that covers the task. An agent that deploys and verifies typically needs `PROJECT_STATUS`, `PROJECT_LOGS`, `PROJECT_DEPLOY`, and `PROJECT_WRITE_SECRETS`, and no workspace actions.

## Read a denial

Denied calls fail with a precise, parseable message, `Not authorized for PROJECT_DEPLOY on SERVICE <uuid>`. The named action is the grant to request, and the target tells you whether the gap is a missing grant or a missing membership. Do not retry a denied call unchanged, and do not escalate by asking for `FULL_MEMBERSHIP` when one named grant would do. Report the exact action and target to the user.

Some actions imply others, so a single grant can carry its prerequisites. The map is queryable:

```
query { permissionActionImplications(first: 50) { nodes { action impliedAction } } }
```

## Token hygiene

- Present platform tokens as `Authorization: Bearer <token>` on every API request.
- Never write a token or secret value into source, project secrets, or task commands. Attribution already records who did what.
- `revokeToken` cuts a platform token on the next request. `removeBot` retires the identity and everything it can act as.
- When a user asks you to act beyond your grants, name the exact missing action rather than requesting a broader credential.

## Error shapes

| You see | It means | Do this |
|---|---|---|
| "Not authorized for ACTION on TYPE id" | Missing grant or membership for that target | Report the named action and target to the user |
| A read returns null for an id you were given | The database hides rows your credential cannot see | Treat it as not yours. Confirm the id and your memberships |
| A machine token fails on `/graphql` | Machine tokens work only against the app | Use a platform credential for the API |
| A token you just revoked still sits in your context | Nothing. Authority is already gone | Stop using it. The next call fails |
