> ## Documentation Index
> Fetch the complete documentation index at: https://cadenya.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# API key scopes

> The permission model for API keys, the full scope reference, and the rules for managing keys with keys.

Every API key carries a list of scopes, and every endpoint declares the scope it requires. A request succeeds when the key holds that scope. There is no implicit access: a key with no scopes can call `Whoami` and nothing else.

Set scopes when you create a key, in the dashboard or through the [API Keys](/docs/api-reference) endpoints. Dashboard sessions use the same scope vocabulary, resolved from your role's permissions, so a rule you read here applies to humans and keys alike.

## How scopes work

A scope is a `resource:verb` string, like `agents:read` or `secrets:manage`.

* **`manage` implies `read`.** A key with `agents:manage` calls every agent endpoint, read or write. Stored scope sets are normalized: `agents:read` is dropped when `agents:manage` is present, and duplicates collapse.
* **`*` is full access.** It is the only way to hold everything, including scope families added in the future. A set that contains `*` normalizes to `*` alone.
* **Two families are manage-only.** Reads of `secrets` and `account` are as sensitive as writes, so no `:read` verb exists for them.

## Scope reference

| Scope               | Grants access to                                                                                                 |
| ------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `agents:read`       | Read agents, variations, schedules, and models                                                                   |
| `agents:manage`     | Create, update, publish, archive, and delete agents, variations, and schedules; enable, disable, and swap models |
| `objectives:read`   | Read objectives, events and event streams, tasks, context windows, diagnostics, feedback, and uploads            |
| `objectives:manage` | Create and continue objectives, approve or deny tool calls, submit feedback, compact, and create uploads         |
| `tools:read`        | Read tool sets and tools, fetch consumed OpenAPI specs, and search                                               |
| `tools:manage`      | Create, update, sync, omit, restore, archive, and delete tool sets and tools                                     |
| `memory:read`       | Read memory layers and entries                                                                                   |
| `memory:manage`     | Create, update, and delete memory layers and entries                                                             |
| `api_keys:read`     | Read a workspace's API keys and the global key                                                                   |
| `api_keys:manage`   | Create, update, rotate, disable, enable, and delete API keys                                                     |
| `workspaces:read`   | List workspaces, members, and profiles; read workspace details                                                   |
| `workspaces:manage` | Create, update, and archive workspaces; add and remove members                                                   |
| `secrets:manage`    | Workspace secrets, tool set secrets, and AI provider keys, reads included                                        |
| `account:manage`    | Read the account; rotate the webhook signing key and challenge token                                             |
| `*`                 | Everything, now and in the future                                                                                |

One endpoint is scope-free: `Whoami` answers for any valid credential, so a key can always identify itself.

## Deny by default

A key's scope set is the whole story. There is no grandfathering and no fallback.

* A key with an **empty scope set** is a dead key: it authenticates, it can call `Whoami`, and every other endpoint denies it.
* A **missing scope** denies the request even for reads. `agents:manage` does not help you list tool sets.
* Full access is always an **explicit grant**: `*` on the key, or every scope spelled out (see the next section).

## Managing keys with keys

API keys can mint other API keys, so the API enforces one rule to stop privilege escalation: **a key can never produce access it does not hold.**

* **Downscope only.** Creating or updating a key with scopes the caller does not hold fails with `PermissionDenied`. Updates check this rule only when the scope set changes, so a narrow key can still rename a broader one.
* **Rotation requires dominance.** Rotating a key returns that key's fresh token, which is equivalent to minting it. The caller's scopes must cover the target's.
* **Granting `*` requires full access.** Holding `*`, or holding the `manage` verb of every scope family, qualifies. Anything less does not, even if it covers most of the vocabulary.
* **Granting nothing is always allowed.** Any caller can mint a dead key.

## Disabled keys

An API key is either enabled or disabled. Disable a key with `POST /v1/workspaces/{workspaceId}/api_keys/{id}:disable` and its token stops authenticating on every endpoint, `Whoami` included, until you enable it again. The key, its scopes, and its token survive the round trip, which makes disable the right kill switch when you suspect a leak but are not ready to rotate.

## Reading a denial

Scope denials return `PermissionDenied` with a `google.rpc.ErrorInfo` detail you can branch on. The `domain` is always `api.cadenya.com`.

| Reason             | Meaning                                                                       | Metadata                           |
| ------------------ | ----------------------------------------------------------------------------- | ---------------------------------- |
| `SCOPE_MISSING`    | The caller does not hold the endpoint's required scope                        | `required_scope`, `granted_scopes` |
| `SCOPE_NOT_HELD`   | A key operation would grant, update, or rotate beyond the caller's own scopes | `granted_scopes`                   |
| `API_KEY_DISABLED` | The presented token belongs to a disabled key                                 | `api_key_id`                       |

`granted_scopes` echoes the caller's own scopes back as a comma-separated list. It reveals nothing the caller does not already hold, and it turns "why is this 403ing" into a one-glance diff:

```json theme={null}
{
  "code": 7,
  "message": "caller does not hold the \"agents:read\" scope required by this endpoint",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.ErrorInfo",
      "reason": "SCOPE_MISSING",
      "domain": "api.cadenya.com",
      "metadata": {
        "required_scope": "agents:read",
        "granted_scopes": "memory:read,tools:read"
      }
    }
  ]
}
```

## Scopes gate endpoints, not embedded fields

A scope controls which endpoints a key can call. It does not filter the responses of endpoints the key can call. When a response embeds a summary of a related resource (an agent's `info.createdBy` profile, a tool set's tool counts), that summary hydrates whether or not the key holds the related family's scope. If a key can read the resource, it can read the whole resource.
