# Auth.md — Locus agent authentication discovery

Canonical skill: `https://paywithlocus.com/SKILL.md`

This reference documents who creates the Locus Pro account and how each
integration authenticates: native MCP OAuth, the generated Locus CLI, and the
plain HTTP API. Follow the integration selected by the canonical skill: honor
an explicit request, reuse a healthy integration, or select from the runtime's
actual native OAuth/plugin and approved CLI support, with HTTP only when
neither fits. Neither adapter is the default, and
shell availability alone does not select the CLI. Ask once only when both
paths are equally supported and no native host signal resolves the choice.
Once selected, do not install, probe, or authenticate through the other path as
a prerequisite or silent fallback.

Generic HTTP access is not native MCP support. Never recreate MCP discovery,
registration, token exchange, callback handling, or polling with `curl`,
`urllib`, or custom HTTP code. The agent signup, agent-credential approval, and
AgentID requests documented here are separate HTTP calls; the approval calls
only complete a request started by a waiting native MCP client.

## Who creates the account

Reuse a healthy login first. Otherwise ask the human once:

> Do you want to sign up for Locus Pro yourself, or should I create the account
> for you? If I create it, I'll attach it to your email so you can claim it.

Leave the choice to them. If they sign up themselves, follow
[Human-owned signup](#human-owned-signup). If you create it, follow
[Agent signup](#agent-signup-with-the-humans-email). An agent that already has
an AgentID and a live agent inbox may use [AgentID](#agentid-optional) instead.

## Agent signup with the human's email

The human gives you only their email address. If you already know it, confirm
it with them. Never ask for their password.

- Contract: `GET https://api.paywithlocus.com/api/credits/agent/onboarding`
- Signup: `POST https://api.paywithlocus.com/api/credits/agent/signup`
- Availability: `compatibility.signup`. While `invitation_only` is true,
  signup returns `403`; ask the human to sign up through an invitation.

1. Generate a registration token: 24 cryptographically random bytes encoded
   as unpadded base64url (32 characters). Store it in the approved secret store
   before sending it. Retrying signup with the same token, email, and name
   returns the same account instead of creating another.
2. Send it with no `Authorization` header:

   ```http
   POST https://api.paywithlocus.com/api/credits/agent/signup
   Content-Type: application/json

   {"email": "jane@example.com", "name": "Jane's research agent", "registrationToken": "<32 characters>"}
   ```

3. Store `account.connection.credential` (an `lcac_` key, valid for a year) in
   the approved secret store. It works at once. Delete the stored registration
   token only after the credential is stored.
4. Send the human `account.ownerHandoff.message` verbatim. Locus emails them a
   one-time link to claim the account. When several agents named the same
   address, the link lists each account and the human picks theirs. Claiming
   makes them an owner and creates their sign-in if they have none. Declining suspends
   the workspace and revokes your access. If they lose the email, send
   `POST /api/credits/agent/claim/resend` with your credential. If they say
   the address is wrong, send the same request with `{"email": "<correct>"}`
   before anyone claims; the old link stops working. The address can be
   corrected at most three times.

Every signup creates a new workspace with every tool on and no credits. Until
a person claims it, a tool that is off turns on when you request access to it;
after the claim, the owner decides, and a request returns an approval link for
them. Use a plain name: a name
that contains a web or email address is refused because it appears in the
owner email. Until the owner claims the account, it can receive at most $50
in credits, and every funding message shows the owner email so the payer can
catch a typo. An unclaimed account that is never funded or used for 30 days is
suspended. One network can create a limited number of accounts per day, and
an address gets at most one claim email every ten minutes and three a day. `409` means the token is already bound to different details;
generate a new one only if you are sure no account was created with it.

Rotate the credential before it expires with
`POST /api/credits/agent/credential/rotate` and a new `registrationToken`.

### Use the credential with your adapter

- **CLI.** `locus auth signup --email <email> --name <name>` does steps 1–3
  and saves the credential in the CLI's own store.
- **HTTP.** Send `Authorization: Bearer <credential>`; see the
  [API guide](https://paywithlocus.com/agent/api.md).
- **MCP.** Your MCP client still uses OAuth. Approve its pending request with
  the credential instead of a browser sign-in:
  1. Start the client's normal Locus OAuth login. Request the authorization
     URL it opens without following redirects. The `Location` is the Locus
     authorization page; its `request` parameter is a handle starting with
     `lmo_req_`.
  2. Approve it:

     ```http
     POST https://api.paywithlocus.com/api/credits/mcp/oauth/authorize/agent-credential
     Authorization: Bearer <credential>
     Content-Type: application/json

     {"request": "lmo_req_..."}
     ```

  3. Deliver the returned `redirectUrl` to the waiting MCP client exactly as
     returned: request it on the machine running the client, or use the
     client's documented manual-callback input. Keep the client's login
     process running until it stores its tokens.

  Only the credential returned by signup can approve this way; a key a person
  created in the dashboard cannot. The MCP grant ends when that credential is
  revoked or the owner declines the claim.

## CLI path: Locus OAuth or API key

Follow this section when the user explicitly selected the Locus CLI, or when
the canonical selection rules chose it for a shell-capable runtime without
native MCP support, and the human signs in themselves. For an account you
create, use `locus auth signup` as above. First require `compatibility.cli.available: true`. If it is
false, report its `reason`. Do not switch to native MCP OAuth unless the user
asks. Install only from `compatibility.cli.install_url`, then follow the CLI's
bundled `locus-cli` skill and command help. Each environment has its own CLI,
which calls that environment's API and keeps its own login; it needs no
`--base-url`.

`locus auth login` signs in with the standard Locus OAuth server: Authorization
Code with PKCE through a loopback callback, or the device flow with `--device`
on a headless machine. The CLI then trades that token at
`POST /api/credits/cli/credential` for a native `lcac_` credential bound to
this installation. It keeps the refresh token and renews the credential before
it expires. Repeating the login rotates the credential in place under the same
connection, so it does not fail with a conflict. Neither command configures
the MCP server.

The other option is an existing API key. Pipe an `lcr_` or `lcac_` key on
stdin with `printf %s "$LOCUS_API_KEY" | locus auth login --with-token`; never
put it in a positional argument. The CLI checks the key with one free call
before it saves it.

The CLI stores its login in its own config directory with owner-only file
permissions and never prints it. Never copy that credential into a project
file, chat or static MCP header. Check the result with
`locus auth status --json`.

If Locus provisions a Locus account and an agents workspace during sign-in, it
emails the person a set-password link and the login output shows
`password_setup_pending: true`. The CLI works immediately. Revoking the CLI
connection in the dashboard blocks that installation until the person signs in
again.

## AgentID (optional)

AgentID remains supported for agents that already have an AgentID identity and
a live agent inbox. It is not the recommended path for new agents; use
[Agent signup](#agent-signup-with-the-humans-email) instead. An AgentID is bound
to exactly one agent workspace: registration, MCP sign-in, and dashboard
sign-in with that AgentID all land in it.

- Registration contract: `GET https://api.paywithlocus.com/api/credits/agent/onboarding`
- Registration: `POST https://api.paywithlocus.com/api/credits/agent/register`
- Identity provider: AgentID OpenID Connect
- Issuer: `https://auth.agentid.com`
- Client descriptor: `GET https://api.paywithlocus.com/api/credits/agent/identity/agentid`
- Callback: `GET https://api.paywithlocus.com/api/credits/agent/identity/agentid/callback`
- Required OIDC controls: Authorization Code, PKCE S256, nonce, issuer and
  audience verification, stable `sub`, `email_verified: true`
- Signup result: scoped `lcac_` compatibility credential for the setup and
  funding REST calls in the canonical skill
- MCP authentication: standard Locus MCP OAuth, with AgentID as the
  resource-owner login; 15-minute access tokens and rotating refresh tokens
  stay in the MCP client's native token store

Random registration entropy creates only a 30-minute pending row. Locus does
not create a tenant, provider, ledger account, or bearer credential until the
AgentID callback verifies.

The MCP OAuth authorization screen is public. An agent-owned account chooses
AgentID and proves the same provider/subject bound during signup. A human-owned
account signs into Locus instead. Both paths return the same resource-bound
OAuth token format. There is no limit on connected apps.

Re-authorizing an agent-owned account with the same AgentID revokes that
identity's previous OAuth token family. It cannot replace a connection owned by
a different identity.

## Browserless AgentID approval

This is the complete approval contract for both Locus signup and MCP OAuth. It
is intentionally included here so an agent does not need to discover an
identity protocol from search results.

### 1. Create an inbox and signing identity once

If the agent does not control an inbox, follow `https://agent.email/skill.md`
first. Generate an extractable P-256 (`ES256`) key pair locally. Persist the
private JWK only in the runtime's approved secret store. Retain the public
JWK's `kty`, `crv`, `x`, and `y` fields.

Register the public key with AgentMail. Authenticate this request with the
AgentMail API key, never with the AgentID signing key:

```http
POST https://api.agentmail.to/v0/api-keys/public-keys
Authorization: Bearer $AGENTMAIL_API_KEY
Content-Type: application/json

{
  "name": "agentid-sign-in",
  "scope": {"type": "inbox", "id": "agent@example.agentmail.to"},
  "public_key": {"kty": "EC", "crv": "P-256", "x": "...", "y": "..."}
}
```

Store the returned `api_key_id` next to the private JWK. It is the `kid` used
below. One way to generate the key material with `jose` is:

```js
import { exportJWK, generateKeyPair } from 'jose';

const { privateKey, publicKey } = await generateKeyPair('ES256', { extractable: true });
const privateJwk = await exportJWK(privateKey); // move directly into the secret store
const { kty, crv, x, y } = await exportJWK(publicKey);
const publicJwk = { kty, crv, x, y };
```

Never send the AgentMail API key to AgentID. Never send the private JWK to
AgentMail, AgentID, or Locus.

### 2. Start and read the authorization transaction

Fetch the AgentID authorization URL returned by Locus. Enable an HTTP cookie
jar before the first request and keep it in memory or an owner-only temporary
file. Follow redirects only until the AgentID waiting page. Read the exact
22-character request ID displayed by that page.

The cookie and request ID belong to one transaction. Starting another GET may
create a new request ID. The continue request in step 4 must use the cookie from
the GET that produced the approved ID.

### 3. Sign and submit the exact approval

Create a compact ES256 JWS. Do not add issuer, audience, timestamps, or other
claims. The protected header and payload are exactly:

```json
{"alg":"ES256","typ":"agentid-approval+jwt","kid":"<api_key_id>"}
```

```json
{"jti":"<22-character-request-id>","inbox_id":"agent@example.agentmail.to"}
```

For example, after loading the private JWK and key ID from the secret store:

```js
import { CompactSign, importJWK } from 'jose';

const key = await importJWK(privateJwk, 'ES256');
const assertion = await new CompactSign(
  new TextEncoder().encode(JSON.stringify({ jti: requestId, inbox_id: inboxId })),
)
  .setProtectedHeader({ alg: 'ES256', typ: 'agentid-approval+jwt', kid: apiKeyId })
  .sign(key);
```

Submit it without an `Authorization` header:

```http
POST https://api.auth.agentid.com/v0/authorize/approve
Content-Type: application/json

{"assertion":"<compact JWS>","inbox_id":"agent@example.agentmail.to"}
```

Success is `204 No Content`. A non-204 response is not approval; stop and
inspect the response without logging secrets.

### 4. Continue with the same cookie

Using the same cookie jar from step 2, fetch and follow redirects from:

```text
https://auth.agentid.com/v0/authorize/continue?jti=<22-character-request-id>
```

Without that cookie AgentID returns `401`, even when the signature was valid.
Follow the redirect back to Locus exactly as returned. The standard OAuth
authorization response can include `code`, `state`, and
`iss=https://auth.agentid.com`; do not remove, rewrite, or reorder parameters.
Delete any temporary cookie jar after the callback completes.

For agent signup, replay the exact registration request after the Locus
callback succeeds. For MCP OAuth, keep the MCP client's login process running
so the final registered redirect URI can deliver its authorization code.

## Human-owned signup

- Signup: `https://platform.paywithlocus.com/signup`
- Authentication: verified email/password or the dashboard's supported social
  providers
- MCP: native OAuth through the metadata advertised by the Locus MCP resource

When MCP authorization starts first, use the sign-up link on the Locus login
page. The pending authorization survives email verification, MFA, and supported
social-provider redirects. For a loopback callback, use **Copy response to
agent** and paste those OAuth response parameters into the waiting MCP client.
Never ask the
human to reveal their password, email verification code, OAuth token, or Locus
API credential to the agent.

## Funding boundary

No signup path grants credits. Only when the task needs credits or the human
asks, an agent credential can create a Stripe Checkout link that lasts 24 hours
(`locus fund`, the MCP `add_credits` tool, or
`POST /api/credits/agent/funding-sessions`). Send its `humanHandoff.message` to
the human verbatim. The payer funds prepaid usage but does not become the
agent account's identity or receive its credential. Never ask for card details
in chat.
