# Access and authentication

> What an agent can and cannot see, how tokens and OAuth sign-in work, and how to revoke access.

## Rules agents inherit

An agent follows the same rules as a person in the web app, and a few of its own.

- **Access is identical.** Every tool resolves the workspace role, per-folder and
  per-document grants, and locks through `lib/access.ts` (CONCEPT.md §23b). A document
  you cannot open does not exist to the agent: not in search, not in listings, and not
  as the far end of a relationship.
- **Denied reads as not found.** The agent never learns that a restricted item exists.
- **One workspace per connection.** IDs from another workspace, even one you belong to,
  come back as not found.
- **Notes outrank summaries.** A note is written by a person. Tools return notes
  separately from AI summaries, and a folder's note is included with everything inside it.
- **Agent-drawn links are `origin: "ai"`.** A model decided the edge, even though your
  credentials carried the request (§9). They show up as AI suggestions in the web app,
  and a pair someone dismissed cannot be proposed again through MCP.
- **Edits are versioned.** Each change creates a document version that people can restore,
  and it re-runs the AI pipeline.
- **The live editor wins.** If someone has the document open in the collaborative editor,
  `edit_document` refuses and tells the agent who. Otherwise it clears the editor's CRDT
  state, and the next editor to open loads the new markdown.

## Authentication

Both ways of connecting produce the same thing: a row in the `agentTokens` table.

| Field                      | Meaning                                        |
| -------------------------- | ---------------------------------------------- |
| `userId`                   | Who the agent acts as                          |
| `workspaceId`              | The one workspace it can reach                 |
| `scope`                    | `read` or `write`                              |
| `kind`                     | `personal` (token) or `oauth` (sign-in)        |
| `tokenHash`, `refreshHash` | SHA-256 of the tokens. No plaintext is stored. |

On every request the gateway hashes the bearer token, looks up the row, checks expiry, and
**re-checks workspace membership**. Removing someone from a workspace cuts off their
agents without anyone revoking tokens. Revoking in Settings → Agents deletes the row, and
the next request is refused. Resetting your password deletes every token and connection you
hold, in every workspace.

Each tool call is recorded with its tool name, ids, outcome and time, kept for 30 days. What
an agent writes or asks (search queries, document text, notes, reasons, names) is not kept.

Anonymous requests get `401` with
`WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp"`,
which starts OAuth discovery in clients that support it.

### Personal access tokens

- Format `ctx_pat_…`, 256 bits of randomness, minted in an action (not a mutation, which
  replays deterministically).
- Expiry: 30 days, 90 days, 1 year, or never.
- A viewer cannot create a write-scoped token.

### OAuth 2.1

Implemented in `convex/oauth.ts` as Convex HTTP actions. It is not a separate auth
service.

| Endpoint                                               | Standard  |
| ------------------------------------------------------ | --------- |
| `/.well-known/oauth-protected-resource[/mcp]`          | RFC 9728  |
| `/.well-known/oauth-authorization-server[/mcp]`        | RFC 8414  |
| `/oauth/register`                                      | RFC 7591  |
| `/oauth/authorize` → web app `/oauth/consent`          | OAuth 2.1 |
| `/oauth/token` (`authorization_code`, `refresh_token`) | OAuth 2.1 |
| `/oauth/revoke`                                        | RFC 7009  |

- **Public clients only.** PKCE with `S256` is required. No client secrets, and no
  implicit or password grant.
- **Client identity:**
  - A Client ID Metadata Document URL (the method the 2026-07-28 spec prefers; Claude
    Code uses `https://claude.ai/oauth/claude-code-client-metadata`). The server fetches
    it, and the document must name itself as the `client_id`.
  - Or Dynamic Client Registration, which the spec deprecates but most clients still use.
- **Redirect URIs:** matched exactly, except that loopback addresses may use any port
  (RFC 8252 §7.3). Native clients bind whatever port is free, and their metadata cannot
  know it in advance.
- **Authorization responses include `iss`** (RFC 9207). A `resource` parameter
  (RFC 8707) must name this server.
- **Consent** happens in the web app, where your session already exists. Everything shown
  about the client is re-derived on the server from its id, never read from the URL. The
  page sends `frame-ancestors 'none'`, so it cannot be framed to steal a click.
- **Lifetimes:** code 5 minutes, single use. Access token 1 hour. Refresh token 90 days,
  rotated on every use (the old refresh and access tokens stop working).
- Requires the `SITE_URL` env var on the Convex deployment, pointing at the web app. Convex
  Auth already needs it.
