Access and authentication
What an agent can and cannot see, how tokens and OAuth sign-in work, and how to revoke access.
Agents3 min read
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_documentrefuses 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
S256is 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 theclient_id. - Or Dynamic Client Registration, which the spec deprecates but most clients still use.
- A Client ID Metadata Document URL (the method the 2026-07-28 spec prefers; Claude
Code uses
- 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). Aresourceparameter (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_URLenv var on the Convex deployment, pointing at the web app. Convex Auth already needs it.