# auth.md: how an agent gets access to Comment.io

You are an agent. Comment.io is a shared workspace of Markdown documents for people and their agents. Every credential here acts as one agent in one workspace, and a human member of that workspace grants it: there is no self-service registration. Follow the steps in order.

Comment.io serves three agent endpoints on `https://comment.io`, each with its own credential:

| Endpoint | What it is | Credential |
| --- | --- | --- |
| `https://comment.io/mcp` | MCP server, code mode: a `run` tool that takes any command | OAuth access token for this resource |
| `https://comment.io/mcp/tools` | MCP server, compat mode: one typed tool per command, for clients already connected to it; connect new clients to `/mcp` | OAuth access token for this resource |
| `https://comment.io/api/run` | HTTP API: `POST {"command": "..."}` ([OpenAPI](https://comment.io/openapi.json)) | API token |

A token works only at its own endpoint, except that a Microsoft Copilot grant for `/mcp` also works at `/mcp/tools`. An agent with a shell can use SSH instead; the [agent guide](https://comment.io/llms.txt) covers it and everything after authentication.

## Step 1 — Discover

A call without a token answers `401` with a `WWW-Authenticate` header naming the endpoint's protected-resource metadata (RFC 9728):

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://comment.io/.well-known/oauth-protected-resource/mcp"
```

Fetch it. The MCP servers' metadata lists the authorization server; `/api/run`'s lists none, because its token is not issued over OAuth:

- `https://comment.io/.well-known/oauth-protected-resource/mcp`
- `https://comment.io/.well-known/oauth-protected-resource/mcp/tools`
- `https://comment.io/.well-known/oauth-protected-resource/api/run`

The authorization server's metadata (RFC 8414) is at `https://comment.io/.well-known/oauth-authorization-server`. It declares `authorization_endpoint`, `token_endpoint`, PKCE `S256`, `client_id_metadata_document_supported: true` and `registration_endpoint`. It has no `agent_auth` block, and there are no scopes: a grant is for one agent in one workspace, at one MCP server.

## Step 2 — Pick a method

1. **You are an MCP client** → OAuth 2.1 authorization code, with PKCE required for public clients and optional for configured confidential clients. Steps 3 to 5.
2. **You can send HTTP requests but not use MCP or SSH** → an API token for `/api/run`. Use an approved AgentID sign-in where offered, or ask your human for one (Step 3) and skip to Step 6.

Comment.io does not offer the WorkOS agent-registration methods: no `identity_endpoint`, no `identity_assertion` with an ID-JAG (`id-jag`), no `service_auth` and no anonymous registration. A request for one of them has nowhere to go.

## Step 3 — Register

Registering an OAuth client grants nothing: only the human's consent in Step 4 does.

- **OAuth, public clients.** Any MCP client may connect, with no secret, in one of three ways:
  - **Client ID Metadata Document.** The `client_id` is the HTTPS URL of a document listing its redirect URIs. Comment.io fetches it, on port 443 and without redirects, and `redirect_uri` must be one of them (a loopback one on any port).
  - **Dynamic client registration (RFC 7591).** `POST https://comment.io/oauth/register` with `Content-Type: application/json` and at most 8 KiB of client metadata, including `redirect_uris`. The `201` answer has a new `client_id`, the redirect URIs that qualify (HTTPS, or HTTP on a loopback address, without a fragment; others, such as a custom scheme, are dropped) and `token_endpoint_auth_method: none`. Nothing is stored. Limit: 30 registrations a minute per address.
  - **Any other `client_id`** of 1 to 255 letters, digits, `-`, `.`, `_` or `~` (never a URL) is accepted as is, registered or not, with any qualifying `redirect_uri`.

  Consent names a document's client by the document's host (Grok Build, which Comment.io knows, by name), and the other two only by the host `redirect_uri` returns to.
  The IDs of the known clients below and of Comment.io's own credentials (`docbox-builtin`, `docbox-api-token`) are reserved: no public client may use them.
- **OAuth, configured known clients.** `docbox-microsoft` and `docbox-meta` use fixed client IDs and secrets, with allowlisted redirect URIs; they cannot register themselves, and their IDs never pass as another client's.
- **API token.** A human member can create one in the workspace’s Settings, under Agents. Where AgentID is offered, an agent can instead sign in at `https://comment.io/auth/agentid?workspace=WORKSPACE&agent=AGENT`. The first sign-in requires a workspace member’s approval; subsequent approved sign-ins replace that agent’s existing API token. Follow the [HTTP API guide](https://comment.io/llms/http-api.md) for setup and recovery. Save the token in a private header file, never in chat, logs or a repository.

## Step 4 — Claim

The human's consent is the claim. Send their browser to the authorization endpoint:

```http
GET https://comment.io/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&code_challenge=CHALLENGE&code_challenge_method=S256&state=STATE&resource=RESOURCE
```

Each value is URL-encoded. `CHALLENGE` is the base64url SHA-256 of your PKCE verifier; public clients must send it with `code_challenge_method=S256`. Configured known confidential clients may omit both, but a challenge they send must use S256 and is checked at exchange. `RESOURCE` names the MCP server, exactly `https://comment.io/mcp` or `https://comment.io/mcp/tools`; without it the grant is for `/mcp`, the single-tool server, for Microsoft Copilot's and Meta Muse's known clients too. Meta Muse's rejects `/mcp/tools` for new authorizations. A Microsoft grant for `/mcp` also works at `/mcp/tools`, so a Copilot connector set up there keeps working; a Microsoft grant for `/mcp/tools`, as every one approved before Microsoft's default changed is, stays there, even after refresh. The two are separate grants with separate consent. An existing Meta compat grant stays bound to `/mcp/tools`, even after refresh; reconnect and approve again for `/mcp`. The human signs in, picks a workspace, then one of its agents or a new one, and approves. Their browser returns to `redirect_uri` with `code`, `state` and `iss`; check that `iss` is `https://comment.io`. The code is valid for one minute. A refusal returns `error=access_denied`.

## Step 5 — Exchange

```http
POST https://comment.io/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=CODE&redirect_uri=REDIRECT_URI&client_id=CLIENT_ID&code_verifier=VERIFIER
```

This example is for a public client. A configured known confidential client instead authenticates this exchange and every refresh with its issued secret, using either `client_secret=SECRET` in the form or HTTP Basic client authentication (not both). It sends `code_verifier` only if it supplied an S256 challenge.

The answer:

```json
{"access_token": "...", "token_type": "Bearer", "expires_in": 7776000, "refresh_token": "..."}
```

The access token expires after `expires_in` seconds. Refresh with `grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=CLIENT_ID`. Each refresh answers a new refresh token and the old one stops working. A `resource` parameter, if sent, must be the grant's own.

## Step 6 — Use the access_token

Send it to the endpoint it was granted for:

```http
POST https://comment.io/mcp
Authorization: Bearer ACCESS_TOKEN
```

An API token is used the same way, and only at `/api/run`:

```sh
curl -sS -H @"$HOME/.config/commentio/WORKSPACE.AGENT.header" -H 'content-type: application/json' \
  --data '{"command":"help"}' https://comment.io/api/run
```

Run `help` first: it starts with "You are AGENT in workspace WORKSPACE." The [agent guide](https://comment.io/llms.txt) explains the commands. Check the JSON `exitCode`, not only the HTTP status: a command failure can still answer HTTP 200. An API token does not expire.

## Errors

| Code | Where | What to do |
| --- | --- | --- |
| `invalid_client_metadata`, `invalid_redirect_uri` | `/oauth/register` | The body is not JSON, is over 8 KiB or is not sent as `application/json`; or no redirect URI qualifies. |
| `invalid_client`, `invalid_redirect_uri`, `invalid_authorization_request` | `/oauth/authorize` (shown on the page) | The public client's metadata document is unavailable, a known client is not configured, `redirect_uri` is not allowlisted or does not qualify, or the query is too long or repeats a parameter. For a registered or other opaque `client_id`, any other request error is also shown here as `invalid_authorization_request`. Nothing is sent back. |
| `invalid_request` | redirect, `/oauth/token` | A parameter is missing or malformed, or required public-client PKCE is missing. Fix the request. |
| `unsupported_response_type` | redirect | Use `response_type=code`. |
| `invalid_target` | redirect, `/oauth/token` | `resource` is neither MCP server, or at authorization names the other server than a confidential client's pinned one. |
| `invalid_client` (401) | `/oauth/token` | A confidential client's secret is missing or wrong, its known client is not configured, or a public client sent a secret. |
| `invalid_grant` | `/oauth/token` | `resource` names the other MCP server than the grant's: fix it and retry. Otherwise the code expired or was used, the verifier does not match, or the refresh token was replaced or revoked: start again at Step 4. |
| `unsupported_grant_type` | `/oauth/token` | Use `authorization_code` or `refresh_token`. |
| `access_denied` | redirect | The human declined. |
| `rate_limited` (429) | `/oauth/token`, `/oauth/register` | Back off and retry after `Retry-After`. |
| `unauthorized` (401) | `/mcp`, `/mcp/tools`, `/api/run` | No token, or one that ended (`error="invalid_token"` in `WWW-Authenticate`). For OAuth, refresh once, then start again at Step 4. For an API token, repeat an approved AgentID sign-in where offered, or ask your human for a new one. |
| `origin_forbidden` (403) | `/mcp`, `/mcp/tools`, `/api/run` | A browser page on another origin sent the call. Call from your server. |
| `server_busy`, `unavailable` (503) | `/mcp`, `/mcp/tools`, `/api/run` | Nothing ran. Retry after `Retry-After`. |

Retry only explicit pre-execution `server_busy` or `unavailable` refusals after `Retry-After`. A lost response, a deadline or a nonzero command exit may follow committed writes: reread state before deciding whether to repeat a mutation. Do not retry a 4xx unchanged.

## Revocation

There is no `revocation_endpoint`. A human ends access in the workspace's Settings, under Agents:

- **Disconnect** on an agent ends all of its credentials at once: OAuth grants, API tokens, SSH keys and AgentID approval.
- **Roll** on an API token ends the old token at once.
- Access also ends when the human who approved the grant or made the token leaves the workspace, or when the workspace is deleted.

The next call then answers `401`. Ending access does not recall output you already received.
