Identity and permissions for agents

Understand what your Platform credential can do, why a call was denied, and how to give automation its own durable Bot identity. This page extends https://app.tokay.io/llms.txt. Act only on an explicit user request or named readiness action.

Last updated

AI agent? Start with llms.txt.

The authority model

  • You act as a durable Person or Bot. An AI agent is the tool, not another identity that carries authority.
  • Current Workspace, Project, and repository memberships define which resources the principal can reach.
  • Every API, MCP, Git HTTPS, and SSH credential carries explicit action grants that can only narrow that membership authority. Browser sessions remain the recovery and account management surface.
  • API and MCP use distinct credential types and exact audiences. API credentials work at GraphQL and API REST. MCP credentials work only at the official MCP endpoint. Git credentials work only for Git transport.
  • Platform and Tokay Access credentials never cross. A tokay_machine_... credential opens one deployed app Project and cannot manage Tokay.
  • Revocation and membership changes are checked on every protected Platform operation.

viewer is the acting Principal, currentPerson is the human Person when one exists, and currentCredential is the exact credential admitted on this request. Use currentCredential { id credentialType destination grants(first: 50) { nodes { action actionDomain } } } to inspect the request's credential authority without supplying an ID.

Use each credential collection for a different scope. Principal.platformCredentials and Principal.platformOauthConnections show credentials and OAuth connections acting as that Principal. viewer.authorizedPlatformOauthConnections shows connections the current Principal approved, including connections acting as an authorized Bot. manageablePlatformCredentials is the cross actor root for credentials the current request may administer, including OAuth roots. The current request credential may be a web session and therefore need not appear in the owner inventory.

Handle Platform request errors

GraphQL request errors use errors[].extensions. REST uses a flat JSON body. Both carry the same contract. code is the broad transport class, reason is the stable operation-specific spelling, message is safe display text, and correlationId identifies this request for support. Branch on code first, branch on a documented reason only when needed, and never parse message.

Use Retry-After when it is present. Do not assume every rejection is retryable. BAD_USER_INPUT means correct the request or its documented precondition, UNAUTHENTICATED means replace or reacquire the credential, FORBIDDEN means the credential lacks authority, NOT_FOUND means the addressed resource is unavailable, CONFLICT means refresh relevant state before deciding, RATE_LIMITED and UNAVAILABLE may be retried only when Retry-After is present, and INTERNAL_SERVER_ERROR means stop and report the correlation ID. A durable failure { action code message supportRef } belongs to accepted background work and is a different contract.

New workspaces are created with funding. If an existing workspace later loses funding, service creation can return BAD_USER_INPUT with reason = WORKSPACE_FUNDING_REQUIRED_TO_START. Restoring funding requires an authorized Person with WORKSPACE_MANAGE_BILLING. Once restored, retry the unchanged service submission with the same idempotency key.

Create credentials only with explicit grants

Each destination has its own creation grant.

Grant May issue
CREDENTIAL_CREATE_API Opaque token for the API audience
CREDENTIAL_CREATE_MCP Opaque token for the MCP audience
CREDENTIAL_CREATE_GIT Git HTTPS password or SSH public key registration

Creation grants are never implied or preselected. For ordinary creation, a bearer creates a narrower credential for the same Person or Bot. The new credential cannot carry a creation grant. Git credentials cannot create anything.

API and MCP leaves default to one hour, must expire within 24 hours, and cannot outlive a finite root. Git HTTPS and SSH leaves have optional expiry instead. They may be unbounded beneath an unbounded root, while a finite root caps them at its own expiry. All leaves die when their root and its lineage are revoked.

Creating a credential for another destination requires the exact creation grant, and its confirmation says so. In particular, an API credential created from MCP works directly at the API, outside MCP. People and Bots can use either Git encoding.

Git grants may be wildcard, pinned to one Workspace, or pinned to one repository. A grant must name at most one target. SSH public key, grants, owner, lineage, and access expiry are immutable after registration. renamePlatformCredential changes only the label.

Connect an outside client with OAuth

Use Platform OAuth when a CLI, desktop agent, or MCP client needs approval without copying a token.

Destination Resource
GraphQL and API REST https://api.tokay.io
Official Tokay MCP https://mcp.tokay.io/mcp

Discovery starts at the resource metadata endpoint for protected resources. The client publishes an HTTPS Client ID Metadata Document and uses authorization code with S256 PKCE. Scope names must match the exact PermissionActionType casing.

Consent chooses whether the connection acts as the approving Person or an authorized Bot. Choosing a Bot records the Person's permission to use that Bot. Removing that permission or the Person's Workspace membership disconnects the connection. A later admin demotion does not silently remove the permission.

Every authorization creates an independent connection. Platform OAuth returns no refresh token or expires_in. The connection stays active until disconnected, revoked, or its principal loses authority. OAuth roots do not rotate. Disconnect and complete OAuth again. API and MCP credentials cannot be exchanged.

Create a Bot identity

Creating a Bot establishes identity and memberships only.

mutation CreateBot($workspace: ID!, $project: ID!) {
  createBot(input: {
    workspace: $workspace
    displayName: "deploy-bot"
    projectMemberships: [{ project: $project, role: MEMBER }]
    repoMemberships: []
  }) {
    bot { id principal { id displayName } }
  }
}

The Bot cannot authenticate until an authorized Person separately provisions a root credential.

mutation ProvisionBot($bot: ID!) {
  provisionBotCredential(input: {
    bot: $bot
    label: "deploy-bot API"
    credentialType: API_TOKEN
    grants: [
      { action: PROJECT_READ }
      { action: PROJECT_READ_LOGS }
      { action: PROJECT_DEPLOY }
    ]
  }) {
    platformCredentialCreation {
      token
      platformCredential { id credentialType destination isLeaf expiresAt }
    }
  }
}

token is shown once. Provisioning creates a durable handoff. The Bot root survives the issuing credential's expiry, revocation, and OAuth disconnect. The Bot's memberships remain its resource boundary.

Create a narrower worker credential

An API or MCP root with the exact target creation grant can call issuePlatformCredential. The child cannot contain CREDENTIAL_CREATE_* and cannot exceed the root's ordinary grants.

mutation IssueLeaf {
  issuePlatformCredential(input: {
    label: "one deploy"
    credentialType: API_TOKEN
    grants: [{ action: PROJECT_READ }, { action: PROJECT_DEPLOY }]
  }) {
    platformCredentialCreation {
      token
      platformCredential { id destination isLeaf expiresAt }
    }
  }
}

Creating, rotating, and revoking credentials confirm first, through the API and MCP alike. The first call changes nothing and fails with CONFIRMATION_REQUIRED; its extensions.confirmation.impact states the destination, grants, expiry, and whether the credential works outside the issuing destination or outlives you. Repeat the unchanged call with confirmationToken added to the input to create it and receive the token once.

Each Person or Bot has separate limits of 100 API roots, 100 MCP roots, and 100 independent Git credentials total across HTTPS and SSH. Each API/MCP root has its own 100-leaf counter across API, MCP, Git HTTPS, and SSH leaves, so a leaf never consumes a root slot or another root's capacity.

Rotate a manual API or MCP root

A live API or MCP root that was not created through OAuth can replace only itself. Rotation preserves its principal, label, destination, grants, and root position, then atomically revokes the predecessor and every direct leaf. Omitting expiresAt preserves the old expiry. A finite root may be shortened but not extended.

mutation RotateRoot {
  rotatePlatformCredential(input: {}) {
    platformCredentialCreation { token credentialType destination expiresAt }
  }
}

The successor token appears once. If the committed response is lost, the old root is already dead and the successor cannot be recovered. Its confirmation states how many direct leaves it revokes and how many independent credentials survive. OAuth roots must disconnect and authorize again.

Register an SSH Git credential

An API or MCP root with CREDENTIAL_CREATE_GIT can register a narrower SSH credential for the same Person or Bot through registerSshKey. Supply explicit Git grants. The registration may have an access expiry and is revoked with its root. The key authenticates Git only and is never a Workspace VM login credential.

mutation RegisterKey($workspace: ID!) {
  registerSshKey(input: {
    label: "deploy workstation"
    publicKey: "ssh-ed25519 AAAA..."
    grants: [
      { action: REPO_READ, workspace: $workspace }
      { action: REPO_WRITE, workspace: $workspace }
    ]
  }) {
    platformCredential {
      id
      label
      sshFingerprint
      expiresAt
      grants(first: 10) { nodes { action workspace { id name } repo { id name } } }
    }
  }
}

Use provisionBotSshKey when a Person deliberately creates an independent SSH credential for a Bot. That handoff requires authority to manage the Bot, and its confirmation states that it survives the provisioner's credential.

Revocation

Revoke the smallest credential set that contains the problem.

  • Self revokes one credential without replacement.
  • Lineage revokes a root and the direct leaves created for the same Person or Bot.
  • Revoking a Bot root always includes its leaves.
  • Passive expiry never sweeps descendants.

A lineage may have provisioned independent credentials for another principal. Revocation preview returns one review set and lets the user explicitly include any credential they can currently manage. Selected API/MCP roots bring their direct leaves. Routine revocation never crosses that boundary silently.

Read previewPlatformCredentialRevocation(platformCredential, mode) for the exact Self or Lineage targets and the independent review set, and show it to the user. Then call revokePlatformCredential with the credentials they selected; it confirms like any credential change, and the token binds that selection.

Grant actions

Grants use exact PermissionActionType names. This is the complete catalog; OAuth scopes are these same names:

Action Covers
WORKSPACE_READ Discover and read Workspace context
WORKSPACE_CREATE_PROJECT, WORKSPACE_CREATE_REPO Create Projects and repositories
WORKSPACE_MANAGE Workspace members, Bots, custom domains, audit, and Workspace connections
WORKSPACE_MANAGE_BILLING Create Workspaces and attach, replace, change, or end Workspace funding
PROJECT_READ Project status, topology, and sanitized runtime state
PROJECT_READ_LOGS Logs, captured request payloads, and runtime output
PROJECT_QUERY Read only managed database queries
PROJECT_EXPORT Managed database exports and service file downloads
PROJECT_WRITE Operate and configure Services, nonsecret Config, and restoring deleted Projects and Services. Also app access, meaning who can open Services, Groups, Machines and their credentials, public routes, custom hosts, and visitor activity
PROJECT_DEPLOY Deploy Services and turn on auto-deploy
PROJECT_WRITE_SECRETS Set and rotate Secret values without reading them back
PROJECT_INVOKE Run app tasks, scheduled jobs, and test events
PROJECT_WRITE_FILES Create and overwrite durable service files
PROJECT_MANAGE_RESOURCES Backing resources, resource outputs, and restoring deleted databases
PROJECT_MANAGE_MEMBERS The Project member list only. Who can open a Service is PROJECT_WRITE
PROJECT_DESTROY Purges, storage wipes, snapshot restores, confirming destructive migrations, and loosening release safety
REPO_READ Committed source, source analysis, and readable Project Config
REPO_WRITE Source changes, and deleting and restoring a repository
REPO_MANAGE The repository roster and its external source connections
REPO_DESTROY Permanently delete repository data
CREDENTIAL_CREATE_API, CREDENTIAL_CREATE_MCP, CREDENTIAL_CREATE_GIT Create credentials for that destination (see the credential table above)

REPO_READ includes committed source and readable Project Config for API/MCP credentials. Git grants pinned to a Workspace or repository authorize only Git transport. Secrets remain write only.

Read a denial

A denial answers code: FORBIDDEN with reason set to the action plus _REQUIRED, such as PROJECT_DEPLOY_REQUIRED, and a message such as Not authorized for PROJECT_DEPLOY on SERVICE. Credential creation denials read CREDENTIAL_CREATE_API_REQUIRED, CREDENTIAL_CREATE_MCP_REQUIRED, or CREDENTIAL_CREATE_GIT_REQUIRED. A denial also carries targetId, the Node ID of the resource it concerns when it has one, and never a raw UUID.

requiredAction appears only when the presented credential lacks that grant. Then REST also answers WWW-Authenticate: Bearer error="insufficient_scope" naming the same scope. Ask the user for that one action. An OAuth client requests a new connection with the union of its current actions and that action. A denial without requiredAction is about membership or a browser only step, and no new grant changes it. Do not retry unchanged or ask for broad authority when one named grant is enough.

A missing credential answers code: UNAUTHENTICATED with reason: TOKEN_REQUIRED, and a malformed, revoked, or wrong surface credential answers TOKEN_INVALID, on GraphQL, REST, and MCP alike.

Credential hygiene

  • Send API credentials as Authorization: Bearer <token> only to the API resource.
  • Never write a credential into source, Config, Secrets, task commands, or logs.
  • Prefer one distinct leaf per worker or subagent instead of sharing a bearer. This preserves attribution and independent revocation.
  • Discard a credential after 401 invalid_token. OAuth connections reauthorize rather than refresh.
  • Remove a Bot only when you intend to end the entire identity, memberships, connections, operator permissions, roots, and leaves.