---
slug: platform/agent-access
title: Agent access
kind: reference
surface: platform
summary: Reference for what an agent's key may do in each workspace, where it lives and what it can access, its level in each workspace, the three levels, protected collections, the secrets switch, a move, the plan-limit answer, the lines written into its log, and every error code.
sources:
  - aetherfy-control-plane:docs/AGENT_ACCESS_LEVELS.md
  - aetherfy-control-plane:shared/agent_grants.py
  - aetherfy-control-plane:shared/plan_validator.py
  - aetherfy-control-plane:api/routes/agent_access.py
  - aetherfy-control-plane:api/middleware/agent_scope.py
  - vectordb:backend/services/agentAccess.js
  - vectordb:backend/services/collectionProtection.js
  - vectordb:backend/services/agentSystemLog.js
  - vectordb:backend/utils/agentRoutePolicy.js
  - vectordb:backend/utils/planLimit.js
  - dashboard/migrations/183_agent_access_levels.sql
---

# Agent access

An agent **lives in** one workspace: the one where it runs, which decides its
deploy regions, its secrets, where its new collections go
(`AETHERFY_WORKSPACE_ID`) and its starting level. It **can access** the
collections of other workspaces you grant it, each at a level. The level decides
what the agent's code may do there. This page is the reference for the levels,
protected collections and the switch that lets an agent write its own secrets.
Only you can change any of them, never the agent.

## Who the rules apply to

The levels and protection apply to **an agent's key** only: the key an agent
receives when it runs. Your own API key, whether you use it from the SDKs, the
CLI or plain HTTP, and the dashboard are never limited by a level or by
protection. You can do everything an agent is refused.

The checks add no database query beyond the ones a request already makes. Protection
rides the lookup the edge makes to resolve the collection, which is answered from a
Redis-backed cache that holds the collection's flag for up to 60 seconds (a change of
protection tells the cache to forget it at once). A request that is forwarded to
another region re-reads the collection by its id there. The agent's levels are an
in-memory copy refreshed within 60 seconds. The one place a check reads the database
again is changing an alias, a rare administrative action, where an agent's change
reads the alias's target once more.

## The model: lives in, can access

- **Lives in:** exactly one workspace. Where it lives is not a grant, so it
  cannot be removed; to change it, move the agent.
- **Can access:** any other workspace you grant it. A grant belongs to the agent,
  names a workspace and carries a level. A workspace you did not grant is refused
  to the agent (`403 AGENT_KEY_WORKSPACE_FORBIDDEN`), and so is one that does not
  exist, so an agent cannot find out which workspace names exist.

## The three access levels

In words (the dashboard and the CLI say the same):

- **Read access:** can search, query, retrieve and count points, and read a collection's information and schema.
- **Read and write access:** everything read access can do, plus add, change and delete points, and create collections and indexes.
- **Full access:** everything read and write access can do, plus delete collections, delete indexes and change their structure (schema, aliases), which changes them for everyone who uses them.

A request needs one level and any higher level passes. The next table lists
every operation and the level it needs.

| Operation | Needs |
|---|---|
| Search, query, scroll, retrieve and count points; read a collection, its schema and its aliases; list collections and aliases; analyze a schema | Read access |
| Add or update points (upsert), set a payload, overwrite a payload, update vectors | Read and write access |
| Delete points, delete vectors, delete payload fields or clear a payload, by id or by filter | Read and write access, and refused on a protected collection |
| Create a collection | Read and write access |
| Create a payload index | Read and write access |
| Delete a payload index | Full access |
| Set or delete a schema | Full access |
| Change a collection's description | Full access |
| Create, rename or delete an alias | Full access, and refused when the alias points at a protected collection |
| Delete a collection | Full access, and refused on a protected collection |
| Rename a collection | Never an agent: a person's change (`403 AUTH_AGENT_KEY_OUT_OF_SCOPE`) |

Creating adds something and removes nothing, which is why creating a collection
or an index needs read and write access while deleting either does not.

## How an agent's level in a workspace is decided

An agent holds one level in each workspace it reaches, decided in this order:

1. In the workspace where it **lives**: the agent's **own setting** if it has one,
   otherwise that **workspace's default** for the agents living there. The
   default is read and write access.
2. In a workspace it **can access**: the **grant's level**, and nothing else. The
   agent's own setting and the home workspace's default never apply there. A new
   grant is read access.

Worked examples. Two workspaces: `support`, whose default for its agents is read
and write access, and `research`, whose default is read access.

| Agent | Lives in | Own setting | Grants | Level in `support` | Level in `research` |
|---|---|---|---|---|---|
| `support-bot` | `support` | none | `research`: read | Read and write (the default of `support`) | Read (the grant) |
| `analyst` | `research` | full | none | Not reachable | Full (its own setting; the default of `research` is read) |
| `triage` | `support` | read | `research`: full | Read (its own setting is lower than the default, and wins) | Full (the grant is higher than its level at home, and applies only there) |
| `nightly` | `support` | none | `research`: read and write | Read and write (follows the default) | Read and write (the grant) |

Two more rules that follow:

- **Changing a workspace's default changes every agent living there that has no
  setting of its own**, and leaves an agent with a setting of its own unchanged.
- **Full at home never carries over.** An agent at full access in its own
  workspace is not at full access in a workspace it can only access: there it
  holds the level of its grant. A high level where an agent lives never raises
  what it can do somewhere else.

An agent's key cannot change a level, a grant or a default, its own or another's,
and `aetherfy.yaml` has no field for one.

### What a change does and how fast

A change takes effect within 60 seconds on the Aetherfy vector API. So does
lowering a level or removing a grant. When you lower a level or revoke a grant the
dashboard and the CLI say it plainly and do not ask for a confirmation:

> Takes effect within 60 seconds; requests above the new level are refused.

A change that lets the agent's code do more asks first: raising a level, adding a
grant, turning the secrets switch on, turning protection off.

## What happens when an agent tries more than its level

A request above the agent's level is refused `403 AGENT_ACCESS_LEVEL_INSUFFICIENT`
and nothing is applied. An agent at read access that tries to write gets:

```json
{
  "error": {
    "code": "AGENT_ACCESS_LEVEL_INSUFFICIENT",
    "message": "This agent's key holds read access in workspace \"research\", and this request needs read and write access. Ask the account owner to raise this agent's access there (the agent's ACCESS tab in the dashboard, or afy access).",
    "workspace": "research",
    "level": "read",
    "required_level": "read_write",
    "documentation_url": "https://docs.aetherfy.com/platform/agent-access"
  }
}
```

The message says the levels in words; `level` and `required_level` carry the
machine values (`read`, `read_write`, `full`). It is decided on the workspace the
request addresses, before anything is forwarded or read, and it is not retryable
until you change the level.

## What the agent's own log shows

A refusal is also written into the agent's own log, so the person reading the
agent's logs sees what its key was refused without reading a response body. Each
refusal of an agent's request (its access, a protected collection or the plan's
collection limit) and each time the agent deleted a collection or points is one
line on the `system` stream, in the same plain sentences as the refusal itself
and never the raw level names:

```bash
afy logs invoice-bot --stream system --json
```

Every line has `stream` `system` and `level` `SYSTEM`. Examples:

| What happened | The line |
|---|---|
| The agent tried to delete a collection with read and write access | `Refused DELETE /api/v1/workspaces/research/collections/kb: This agent's key holds read and write access in workspace "research", and this request needs full access. …` |
| The agent tried to delete points of a protected collection, three times | `Refused POST /api/v1/workspaces/research/collections/kb/points/delete: Collection "kb" is protected: … (3 times)` |
| The agent created a collection past the plan's limit | `Refused POST /api/v1/workspaces/research/collections: Collection limit reached. Your free plan allows 3 collections.` |
| The agent deleted 12 points | `Deleted 12 points from collection "kb" in workspace "research".` |
| The agent deleted a collection | `Deleted collection "kb" in workspace "research".` |

A delete says how many points it removed; a delete by filter counts the points the
filter selected. The same refusal of the same kind of request repeated within a
minute is **one** line ending `(3 times)`, and deletes from one collection within
a minute add up to one line (`(4 requests)`). An agent that refuses itself in a
loop cannot flood its own log: past 50 distinct lines in a minute, one further line
says how many were not listed. The lines appear within about a minute of the
event, from any region, in the order of the logs. Reading data, and anything your
own key does, writes no line. In the dashboard they are on the agent's LOGS tab
with the stream `system`. The full list of streams is in
[runs and logs](/agents/runs-and-logs).

## Memories and collections follow the same rules

A memory namespace is a collection, so the levels apply to it exactly as to any
other collection. This is the whole mapping, for the Python and the JavaScript
memory SDKs alike:

| Memory operation | What it does on the vector API | The level it needs |
|---|---|---|
| Search, get, list | Search, query, retrieve, scroll | Read access |
| Add, update | Upsert points | Read and write access |
| Delete by ids, by filter, and delete metadata keys | Delete points, delete payload fields | Read and write access, and refused on a protected collection |
| Create a namespace | Create a collection | Read and write access |
| Delete a namespace, `clear()` on a namespace | Delete the collection | Full access, and refused on a protected collection |
| `clear()` on a thread, delete a thread | Delete the thread's points by filter | Read and write access, and refused on a protected collection |
| The first thread in a workspace | Creates the shared threads collection and two payload indexes on it | Read and write access: creating a collection or an index adds and removes nothing |
| Set or delete a schema | Change the collection's schema | Full access |

At each level, memory behaves like this:

| Level | An agent using the memory SDK can |
|---|---|
| Read access | Search, get and list memories and threads |
| Read and write access (the default) | Everything above, and create namespaces and threads, add, update and delete memories, and clear a thread |
| Full access | Everything above, and delete or clear a namespace and set a schema |

At the default level, memory works out of the box. Deleting or clearing a
namespace needs full access, and the refusal says so and how to raise it.

## Why the default is read and write

An agent that reads, writes and creates can do its job, and cannot delete a
collection until a person raises it to full access. That keeps a mistake that
destroys gigabytes out of reach by default, without taking memory away: creating
a namespace creates a collection and its indexes, which read and write access
allows.

To raise or lower it, change the workspace's default (every agent that follows
it), or one agent's own setting. Both are in [Changing the levels](#changing-the-levels).

## Protected collections

A collection can be **protected**. Protected means **no agent removes data from
the collection**. On a protected collection, at any level, **no agent's key**
may:

- delete the collection;
- delete points, delete vectors or delete payload fields, by id or by filter,
  including clearing a payload;
- delete, rename or repoint an alias that points at it.

Each is refused `403 COLLECTION_PROTECTED`, with `collection_name`.

**Protection stops an agent from deleting the collection or deleting data from it.
An agent with write access can still change or overwrite points.** An agent that
may write can still add points, upsert an existing point id, set or overwrite a
payload and update vectors on a protected collection. To stop all changes, give
the agent read access.

You, with your own API key, the dashboard or the CLI, can still do all of it, and
only you can turn protection off. An agent's key cannot.

**How fast it applies.** Protection applies within a minute: at once when the
change reaches the region that answers the agent, and within 60 seconds at the
latest everywhere. If the vector service cannot confirm the change at once, the
dashboard and the CLI say it is saved and may take up to a minute to apply (the
request answers `503 SERVICE_UNAVAILABLE` with a `Retry-After`); sending the same
request again applies it. If Aetherfy cannot read a collection's protection, an
agent's request that removes data is refused `503` rather than assumed to be
unprotected.

### Two layers

The level and protection answer different questions. The **level** is what an
agent may do in a workspace, for all of its collections. **Protection** is about
one collection, whatever the agent's level: it holds even for an agent at full
access. A request meets the level first, then protection. Neither is a backup:
they stop an agent from removing data, and do not bring it back once a person has.

### Turning protection on and off

| Where | How |
|---|---|
| Dashboard | The collection's page, its settings: the protection switch. Turning it off asks you to type the collection's name |
| CLI | `afy collections protect <name>`, `afy collections unprotect <name>`; `afy collections list` shows which collections are protected |
| Aetherfy agents API | `PATCH /api/v1/collections/{id}` with `{"protected": true}`, sent on its own (`400 COLLECTION_PROTECTED_COMBINED` when it comes with `regions`, or with a `workspace_id` other than the collection's own) |

Turning protection off asks first, since it lets agents delete again:
"Unprotecting a collection lets agents with read and write access delete its
points, and agents with full access delete the collection."

## Where an agent's secrets come from

An agent receives the secrets of the workspace it lives in, plus its own. A grant
never brings a granted workspace's secrets. A grant means "you may use this data";
it does not hand over the keys that workspace's agents run with. If a task needs
a credential, give it to the agent explicitly, on the agent or in the workspace it
lives in.

## The secrets switch

Each agent has a switch, **off by default**. While it is on, the agent's own key
may create, update, rotate and delete **its own secrets**, and nothing else. It
never reaches the secrets of its workspace (shared by every agent living there:
a switched-on agent could redirect its siblings or break them, so workspace
secrets are written by people only), of a workspace it was granted, or of another
agent. It cannot list secrets, so a secret's value is never read back by the
agent. Names beginning `AETHERFY_` stay reserved, and **a value is at most 32 KiB**
(32 768 UTF-8 bytes; one byte more is refused with `422 SECRET_VALIDATION_ERROR`).

The use case is a scheduled task that rotates a credential, such as an OAuth
refresh token, and saves it for its next run. A secret the agent writes is stored
at once and read when the agent starts: a service sees the new value after its
next deployment (a redeploy, `afy deploy`, a push to a linked repository, or a
rollback); a task sees it on its next run, which is why the switch suits tasks. A
running machine never changes its environment.

The switch is read on every request, so turning it off closes the routes on the
agent's next request, with no redeploy. Moving the agent turns it off. Turn it on
in the dashboard (the agent's ACCESS tab, which asks first), with
`afy access <agent> --secrets on`, or `PUT /api/v1/agents/{agent}/access/secrets`
with `{"enabled": true}`. With the switch off, the secret routes answer
`403 AUTH_AGENT_KEY_OUT_OF_SCOPE`, and the message says the account owner has not
turned the switch on.

## What a move resets

Moving an agent to another workspace (`afy update <name> --workspace <workspace>`, or
MOVE on its ACCESS tab) changes where it lives, and so:

- **Its own level is cleared.** It follows the new home's default, because a level
  chosen for one workspace is not carried into another whose owner chose a
  different default.
- **A grant to the new home is dropped:** the agent lives there now, and the grant
  would otherwise come back to life if it moved again. Its other grants stay.
- **Its secrets switch is turned off.** The switch was your decision about the
  agent in the place it lived.
- **The old home is no longer reachable** unless you grant it, and protection is
  not affected: a protected collection stays protected.

### The confirmation lists what the move changes

Both surfaces ask before they move an agent (`afy update --workspace` asks you to
answer `y`; MOVE on the ACCESS tab opens a dialog), and both list what the move
does to this agent, in these sentences. A line appears only when it applies.

| Line | Shown when |
|---|---|
| `Its own level in '<old>' (<level>) is cleared; in '<new>' it holds that workspace's default.` | the agent has its own level set |
| `Its secrets switch is turned off.` | the switch is on |
| `Its grant to '<new>' is dropped: it is its home now.` | the agent holds a grant to the new home |
| `It keeps its other grants: '<workspace>' (<level>), '<workspace>' (<level>).` | it has other grants; each is named with its level, separated by commas |
| `It has no other grants.` | it has none |
| `It no longer reaches '<old>' unless it is granted there.` | always |

`--yes` skips the question, not the list: the CLI prints the same lines to
stderr, and a script without `--yes` and without a terminal is refused (exit 2)
before anything is sent. If the lines cannot be read, the CLI says so and still
asks; the move itself is never refused for it. An agent already in the target
workspace has nothing to confirm.

## What happens when a collection is deleted

The collection's record goes at once, and the name is free to use again. The
physical removal of its data in each region runs in the background, with retries.
Nothing refuses the delete for being in use. Before `afy collections delete` asks
you to confirm, it tells you which agents used the collection in the last 35 days
("Used by agent invoice-bot in the last 35 days."), and the delete is your call.
The collection's page in the dashboard shows the same agents in its USED BY list
(the dashboard deletes points, not collections). The 35 days is how far back Aetherfy records
which agent used which collection, so an agent whose only trigger is a monthly
scheduled task is named too. It is not a retention promise, and the delete is
never refused for it. What stops an agent deleting a collection is its level (the
default cannot delete one) and protection (no agent can, at any level).

## When the plan's limit is hit

Creating more collections than the plan allows is refused `403 PLAN_LIMIT_EXCEEDED`
by the vector API, with the same body the agents API sends for its own plan limits:
`code`, `message`, `upgrade_url`, `limit` (the limit that fired), `allowed` (what
your plan allows) and `current` (what you hold now). `allowed` is a number, or
`false` for a feature the plan lacks; a feature that is simply on or off has no
`current`.

```json
{
  "error": {
    "code": "PLAN_LIMIT_EXCEEDED",
    "message": "Collection limit reached. Your free plan allows 3 collections.",
    "upgrade_url": "https://app.aetherfy.com/dashboard/settings/billing",
    "limit": "max_collections",
    "allowed": 3,
    "current": 3
  }
}
```

```json
{
  "detail": {
    "code": "PLAN_LIMIT_EXCEEDED",
    "message": "Your plan does not support always-on mode. Upgrade to enable it.",
    "upgrade_url": "https://app.aetherfy.com/dashboard/settings/billing",
    "limit": "allows_always_on",
    "allowed": false
  }
}
```

The agents API carries the same fields inside `detail`, the vector API inside
`error`. `allowed` is `null` for a limit your plan sets no cap on; a refusal is
only sent when there is a cap to reach, so a client normally sees a number or
`false`. A memory size the schedule does not price is not a plan limit: it is
`422 MEMORY_SIZE_UNSUPPORTED`, with the supported sizes in its body.

## Spawning does not pass access on

Levels are not transitive through spawn. An agent at read access can spawn a worker
listed in its `allowed_workers`, and that worker runs with **its own** level, which
may be full access. Listing a worker is a decision you take, so give a worker the
level you want it to hold and list it deliberately.

## Changing the levels

| Where | How |
|---|---|
| Dashboard | The agent's page, ACCESS tab: the workspaces it can access, each with a level, set when granting and changeable after, and the level where it lives. The workspace's page, SETTINGS tab: its default level. The workspace's page, AGENTS & ACCESS tab: which agents live there or can access it, each with its level |
| CLI | `afy access <agent> --add <workspace> --level read_write`; `afy access <agent> --own-level read` (or `default`); `afy workspaces update <workspace> --agent-access-level read` |
| Aetherfy agents API | `POST /api/v1/agents/{agent}/access/workspaces` with `level`; `PUT /api/v1/agents/{agent}/access/own-workspace` with `level` (`null` follows the workspace's default); `PATCH /api/v1/workspaces/{workspace}` with `agent_access_level` |

A change that lets the agent's code do more asks first, in the dashboard and in
the CLI (`--yes` skips the question, and is required without a terminal; a "no"
exits 1 and changes nothing). A change that changes nothing asks nothing, and a
change that narrows asks nothing; it only says when it takes effect.

An agent can read its own access, never another's:
`GET /api/v1/agents/{agent}/access` with its injected key answers the levels it
holds and whether its secrets switch is on.

## Error codes

| Code | Status | Meaning, and the way out |
|---|---|---|
| `AGENT_ACCESS_LEVEL_INSUFFICIENT` | 403 | The request needs more than the level the agent holds in that workspace. Raise its level in that workspace, or stop the request |
| `AGENT_KEY_WORKSPACE_FORBIDDEN` | 403 | The workspace is not the agent's and was not granted to it, or does not exist. Grant it, or use a workspace it reaches |
| `AUTH_AGENT_KEY_OUT_OF_SCOPE` | 403 | A route the agent's key may not call at all (an account route, renaming a collection), or a secret route while the switch is off. Use your own key, or turn the switch on |
| `COLLECTION_PROTECTED` | 403 | An agent's key tried to delete a protected collection, remove data from it, or delete, rename or repoint an alias that points at it. Turn protection off first, or do it with your own key |
| `COLLECTION_PROTECTED_COMBINED` | 400 | `protected` was sent with `regions`, or with a `workspace_id` other than the collection's own, in one request. Send it on its own |
| `AGENT_ACCESS_UNAVAILABLE` | 503 | Aetherfy could not read the agent's levels; nothing was applied. Retry |
| `COLLECTION_LOOKUP_UNAVAILABLE` | 503 | Aetherfy could not look the collection up just now, which includes its protection when the request removes data. Retry |
| `SERVICE_UNAVAILABLE` | 503 | On a protection change: it is saved but the vector service did not confirm it yet, so it may take up to a minute to apply. Send the same request again to apply it now |
| `PLAN_LIMIT_EXCEEDED` | 403 | The plan's collection limit is reached; the answer names `limit`, `allowed` and `current`. Delete a collection or upgrade |

What an agent catches for each (the full mapping is on
[errors](/vectors/errors#how-the-aetherfy-sdks-map-an-error-to-an-exception)):

| Code | Python and JavaScript raise | What it carries (Python / JavaScript) |
|---|---|---|
| `AGENT_KEY_WORKSPACE_FORBIDDEN` | `AgentWorkspaceForbiddenError` | `.workspace` |
| `AGENT_ACCESS_LEVEL_INSUFFICIENT` | `AccessLevelInsufficientError` | `.workspace`, `.level`, `.required_level` / `.requiredLevel`, each one of the machine values `read`, `read_write`, `full` |
| `COLLECTION_PROTECTED` | `CollectionProtectedError` | `.collection_name` / `.collectionName` |
| `AUTH_AGENT_KEY_OUT_OF_SCOPE` | `AgentKeyOutOfScopeError` | `.agent_id` / `.agentId`, `.required_scope` / `.requiredScope` |
| `PLAN_LIMIT_EXCEEDED` | `QuotaExceededError` | `.limit`, `.allowed`, `.current`, `.upgrade_url` / `.upgradeUrl` |

The first four are kinds of `PermissionDeniedError`, so one handler for
`PermissionDeniedError` catches every refusal of a valid key. A plan limit is not
one of them: the agent may do the thing, the account is at its cap.

## Frequently asked questions

### Does this limit my own key or the dashboard?

No. Levels and protection apply to an agent's key only. Your own API key and the
dashboard can do everything an agent is refused.

### Where does an agent live, and what is a grant?

An agent lives in exactly one workspace. A grant gives one agent access to another
workspace's collections, never its secrets. Grants belong to agents, not to
workspaces: a workspace has a default level for the agents living in it, and each
agent that can access it from elsewhere holds the level of its own grant.

### How do the levels combine?

In the workspace where it lives, an agent holds its own setting, or the
workspace's default when it has none. In a workspace it can access, it holds the
grant's level. The home level never carries over: see the worked examples above.

### What happens when an agent at read access tries to write?

It is refused with `403 AGENT_ACCESS_LEVEL_INSUFFICIENT`, naming the workspace,
the level held and the level needed, and a line is written into its log. The full
answer is shown above.

### Is it enough to protect a collection?

Protection and levels are two layers. Protection is about one collection and
holds at any level; the level is about everything an agent may do in a
workspace. Protection does not stop an agent with write access from changing or
overwriting points. To stop all changes, give the agent read access.

### How do schema changes relate to levels?

Reading needs read access; changing a schema, deleting an index or changing an
alias needs full access. Creating an index needs read and write access.

### Why is the default read and write? How do I raise or lower it?

See "Why the default is read and write" above: agents read, write and create,
including memory namespaces, and cannot delete a collection until a person raises
them to full access. Raise or lower the workspace's default, or one agent's own
setting, as described under "Changing the levels".

### Where does an agent's configuration come from?

Its secrets come only from where it lives: its own and its workspace's. A grant
never brings secrets. The secrets switch lets the agent write its own secrets
only, and a new value takes effect at its next run or deploy, which is why it is
for tasks.

### Why doesn't a grant bring the other workspace's secrets?

A grant means "you may use this data". It should not silently hand over the keys
that workspace's agents run with. If a task needs a credential, give it to the
agent explicitly, on the agent or in the workspace it lives in.

### Do memories follow the same rules as collections?

Yes: a namespace is a collection. See the table under "Memories and collections
follow the same rules".

### What happens when a collection is deleted?

The record goes at once; the physical removal in each region runs in the
background with retries. You are warned which agents used it in the last 35 days.

### How fast does a change take effect?

Within a minute on the vector API: 60 seconds at most. Lowering a level or removing a
grant takes effect within the same window: requests above the new level are
refused.

### Can an agent change its own access?

No. Its key cannot change a level, a grant, a default or a collection's
protection, and `aetherfy.yaml` has no field for them.

### Does an agent pass its access on when it spawns another?

No. See "Spawning does not pass access on".
