Skip to content

Agent access and scoped tokens

View as Markdown

An agent access token represents one user in one workspace. That association selects identity and row authority; it is not an independently configurable OAuth-style list of action scopes.

On every request, project auth reloads the token’s current user/workspace membership and builds abilities from its current roles. Removing the membership or changing its roles therefore changes later requests. The browser session’s active workspace is irrelevant to a bearer token.

sapporta endpoints list and sapporta endpoints show read the application contract at /api/openapi.json. The generated .env.development sets SAPPORTA_OPENAPI_POLICY=public, so both work against a local development server with no credential, and their output carries each route’s parameters, request body, and response schemas.

The commands that read or write workspace data need a token: rows, tables, sql, and api. The CLI takes the token from --api-token, then from SAPPORTA_API_TOKEN, then from the project’s .env.agent, described below. A deployment leaves SAPPORTA_OPENAPI_POLICY unset, which keeps the contract behind sign-in, so endpoint discovery there needs a token as well.

Only a signed-in person creates a token. A freshly scaffolded project has no user until someone signs up, and the data commands are unavailable until then.

Create the token in the intended workspace

Section titled “Create the token in the intended workspace”

An agent access token carries one user and workspace boundary into the CLI, so the setup begins in the browser rather than the terminal. Open /account/profile in an interactive browser session, switch to the workspace the caller should represent, and create an agent access token. Give it a name that identifies the caller and purpose, and add an expiry when the access is temporary.

After creation, the dialog offers two ways to finish. If a coding agent will work from this checkout, open the agent at the project root and choose Copy setup prompt. The prompt already contains the application’s CLI base URL and the one-time token, so paste it into that trusted agent before closing the dialog. If you prefer to configure the environment yourself, choose Copy token and use the manual variables below. Neither route is automatic, and the plaintext token is not available again after the dialog closes.

Later token-list requests return metadata such as the token’s name, workspace, creation time, expiry, last use, and revocation time. The project database stores a hash of the secret rather than the raw token.

The read, create, and delete abilities on agent_access_token authorize these interactive token-management actions. Bearer credentials may call ordinary app APIs, but token-management endpoints reject them with 403 forbidden.

Hand the setup prompt to one trusted agent

Section titled “Hand the setup prompt to one trusted agent”

The setup prompt has the agent read the project’s agent docs first, such as AGENTS.md, for an authenticated Sapporta command and token location they already give. When the docs give one, the agent sets up for that command and leaves the docs alone. Otherwise it uses the CLI a workspace package already provides: pnpm exec finds the sapporta bin only from inside the package that declares sapporta or @sapporta/server, for example pnpm --filter ./packages/api exec sapporta. It installs the CLI only when no package provides it.

To supply SAPPORTA_API_URL and SAPPORTA_API_TOKEN, the agent reuses the project’s existing directory environment tooling, such as mise, direnv, or a dotenv runner, unless the project’s docs already name where the token goes. If the project has no such tool, the fallback is a private, gitignored local wrapper rather than a newly installed environment manager. When a project’s agent docs name .env.agent as the token’s location, the agent writes the token there instead, and the CLI reads it with no environment tooling.

When the project’s docs give no authenticated command, the agent adds the exact invocation to AGENTS.md; it does not edit agent docs that already give one. Either way it proves the connection with a read-only command. In a sandboxed agent, that final check may require explicit network permission for the application URL.

Against a local development server, endpoints list succeeds without a credential and therefore reports reachability rather than the token. Read the auth context to confirm the token itself:

Terminal window
pnpm exec sapporta api get '/api/auth-context'

The generated setup prompt contains the raw credential by design, so treat the whole prompt as a secret-bearing handoff. Paste it only into the intended agent session. The resulting AGENTS.md instruction may name an environment tool or wrapper, but it must never contain the token itself. Keep the credential out of source files, committed configuration, screenshots, shell history, and later task prompts.

The CLI reads SAPPORTA_API_URL and SAPPORTA_API_TOKEN. The URL is the deployment base before /api. The generated prompt includes the base URL derived from the running application; the agent then places it in the project’s private environment tooling. A local development server needs no URL, because the CLI reads the project’s own SAPPORTA_API_PORT from .env.development.

Terminal window
export SAPPORTA_API_URL="https://tasks.example.com"
read -s SAPPORTA_API_TOKEN
export SAPPORTA_API_TOKEN
pnpm exec sapporta endpoints show "GET /api/reports/project-progress"
pnpm exec sapporta rows list tasks --output json

Keep the token in the project’s .env.agent

Section titled “Keep the token in the project’s .env.agent”

A project can keep its agents’ token in .env.agent, a dotenv file in the project root:

.env.agent
SAPPORTA_API_TOKEN=spat_<token-id>_<secret>

The CLI reads the file when neither --api-token nor SAPPORTA_API_TOKEN sets a token, from any directory inside the project. It reads only SAPPORTA_API_TOKEN from the file, and a blank value counts as no token. pnpm dev loads only .env.development, so the API process’s environment never holds the agents’ token.

The file is a credential:

  • The generated .gitignore ignores .env*, so the file is not committed.
  • The generated .dockerignore ignores .env* at the project root, so the file is not sent to a Docker build. A project scaffolded earlier keeps its own .dockerignore; replace its .env lines with .env*.
  • Make the file readable only by its owner, for example with chmod 600 .env.agent.

The token in .env.agent is sent only to the project’s own API: the URL the CLI reads from .env.development, or the built-in default. When --api-url or SAPPORTA_API_URL chooses the URL, the CLI withholds the file’s token and an authentication failure says so. To reach another deployment, supply that deployment’s token with SAPPORTA_API_TOKEN.

If .env.agent exists but cannot be read, for example because another user owns it, every CLI command stops with an error that names the file.

An application that provisions its agents’ token writes the file with writeProjectApiToken(projectRoot, token) from @sapporta/server and reads it back with readProjectApiToken(projectRoot). The writer creates an owner-only file and renames it into place. It throws on a token that would not read back as written, such as a blank one or one that contains # or quotes.

If you call HTTP directly instead, send a normal bearer header:

GET /api/tables/tasks HTTP/1.1
Host: tasks.example.com
Authorization: Bearer spat_<token-id>_<secret>

The token-bound workspace supplies request authority. A query parameter, request body, or workspace_id field cannot switch or widen it.

Protected apps require credentials to retrieve their OpenAPI/endpoint inventory. The inventory proves which operation is mounted and describes its HTTP shape; it does not encode application abilities or row policy.

Choose the narrowest mounted operation that owns the change, make the request, and then read the affected record through an authoritative scoped operation. A successful transport response alone does not prove that the intended row changed.

Use a small fixture with one record in Workspace A and a different record in Workspace B. A Workspace A token should see only Workspace A rows. Automation for Workspace B gets a separate token created while Workspace B is active.

Then exercise each recovery branch:

ConditionStable responseNext step
Revoked token401 token_revokedReplace or remove the credential
Expired token401 token_expiredCreate a new time-bounded token
Membership removed403 workspace_requiredRestore membership or choose another workspace
Bearer calls token-management route403 forbiddenUse an interactive session
Revoke target belongs to another workspace/user404 not_foundRecheck the active browser workspace and token ID

Status and code are the contract. Message wording is illustrative.

Revoking the credential invalidates the next API request. Removing a local environment variable or deleting .env.agent does not revoke it:

Terminal window
unset SAPPORTA_API_TOKEN
rm .env.agent