# Autenticação de agentes na ExploriGen (agent auth)

Você é um agente. Este documento segue o roteiro do perfil auth.md da WorkOS — discover, pick a method, register, claim, exchange, use, errors, revocation — e diz exatamente o que a ExploriGen implementa e o que, de propósito, não implementa.

Resumo honesto: quase tudo aqui é aberto. As páginas em markdown, o llms.txt, os dois servidores MCP, o /ask, o índice `GET https://explorigen.io/api` e o sandbox (test mode) não pedem credencial nenhuma. Só a API REST v1 (`https://explorigen.io/api/v1/`) pede um bearer, e qualquer agente obtém um sozinho, de graça, em duas chamadas, sem conta, sem e-mail e sem pessoa no meio (registro autônomo, self-serve API key). O bearer identifica a sessão do agente (`registration_id`) e é a metade "recurso protegido" do perfil; os mesmos dados estão abertos nas outras superfícies. Não há chave de parceiro nem tier pago. O início rápido (quickstart) com os três `curl` está em https://explorigen.io/agentes.

## Discover

A descoberta tem dois saltos, como no perfil. Se você chegou aqui por um 401, ele veio com o apontador:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://explorigen.io/.well-known/oauth-protected-resource", scope="site.read prontidao.read"
Content-Type: application/json; charset=utf-8

{ "error": "missing_token", "error_description": "…", "auth": "…/auth.md", "sandbox": "https://explorigen.io/api/v1/sandbox/" }
```

O 401 é emitido em `GET https://explorigen.io/api/v1/` e em toda rota `https://explorigen.io/api/v1/…` sem bearer válido. O índice `GET https://explorigen.io/api` responde 200 sem token, de propósito: ele é a porta pública.

1a. Metadata do recurso protegido (RFC 9728): `GET https://explorigen.io/.well-known/oauth-protected-resource` — também servido no caminho relativo `/.well-known/oauth-protected-resource/api/v1`, o caminho exato da RFC para o recurso `https://explorigen.io/api/v1/`. Campos: `resource` = `https://explorigen.io/api/v1/` (use como `aud`), `authorization_servers` = a própria origem (site, API e servidor de autorização moram no mesmo host, decisão registrada), `scopes_supported` = `site.read prontidao.read`, `bearer_methods_supported` = `["header"]`, `sandbox`.

1b. Metadata do servidor de autorização (RFC 8414): `GET https://explorigen.io/.well-known/oauth-authorization-server`. Traz `issuer`, `token_endpoint` (= `https://explorigen.io/agent/token`), `grant_types_supported` (só `urn:ietf:params:oauth:grant-type:jwt-bearer`), `token_endpoint_auth_methods_supported` = `["none"]`, `response_types_supported` = `[]` (não há fluxo de navegador: nenhum authorization-code, nenhum `authorization_endpoint`) e o bloco `agent_auth`:

- `agent_auth.skill` — este documento.
- `agent_auth.identity_endpoint` — `https://explorigen.io/agent/identity`, onde você se registra (Register).
- `agent_auth.identity_types_supported` — `["anonymous"]`. Só isso.
- `agent_auth.events_supported` — `[]`.
- Não existem `agent_auth.claim_endpoint`, `agent_auth.events_endpoint`, `agent_auth.identity_assertion` (logo, nenhum `assertion_types_supported`) nem `revocation_endpoint`. Se você procurar esses campos e não achar, está certo: não há endpoint atrás deles.

## Pick a method

O perfil oferece três métodos; a ExploriGen aceita um:

1. `identity_assertion` com ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`) — **não suportado**. Não há lista de provedores confiáveis nem contas de usuário para vincular; a resposta é `400 issuer_not_enabled`. Não gaste tempo cunhando um id-jag.
2. `service_auth` (por e-mail do usuário, com cerimônia de claim) — **não suportado**; resposta `400 service_auth_not_enabled`.
3. `anonymous` — **o único método**. Serve para qualquer agente, com ou sem usuário humano por trás, porque não há dado privado do outro lado: a API v1 é somente leitura (read-only) sobre conteúdo público.

Se você tem um usuário e uma sessão, ainda assim use `anonymous`: nada aqui exige, guarda ou devolve identidade humana. Não há consentimento a colher porque nenhuma identidade é assertada ao serviço.

## Register

```http
POST https://explorigen.io/agent/identity
Content-Type: application/json

{ "type": "anonymous" }
```

Resposta 200:

```json
{
  "registration_id": "reg_…",
  "registration_type": "anonymous",
  "identity_assertion": "<JWT assinado pelo serviço>",
  "assertion_expires": "<ISO 8601, 24 h depois>",
  "pre_claim_scopes": ["site.read", "prontidao.read"],
  "post_claim_scopes": ["site.read", "prontidao.read"],
  "scopes": ["site.read", "prontidao.read"]
}
```

O `registration_id` é `reg_` mais 20 caracteres base64url aleatórios; ele vira o `sub` de todo token derivado. A `identity_assertion` é um JWT compacto (HS256, assinado pelo serviço) com `iss` = origem, `aud` = `https://explorigen.io/api/v1/`, `typ` = `assertion`, validade de 24 horas. Guarde-a: ela troca por quantos access_tokens você precisar até expirar. Corpo só em JSON; corpo acima de 4 KB → `413`; `type` ausente ou desconhecido → `400 invalid_request`. `GET` nesse endpoint responde `405` com `Allow: POST, OPTIONS` (nunca 404), e `OPTIONS` responde `204`.

## Claim

Não há cerimônia de claim. A resposta do registro anônimo não traz `claim_url` nem `claim_token`, o bloco `agent_auth` não tem `claim_endpoint`, e o `token_endpoint` não aceita o grant de polling `urn:workos:agent-auth:grant-type:claim` (responde `400 unsupported_grant_type`). Repare que `pre_claim_scopes` e `post_claim_scopes` são iguais: não existe escopo que um humano desbloqueie ao "tomar posse" do agente, porque não existem contas para tomar posse de nada. Pule direto para Exchange. Se o seu cliente exige uma etapa de claim para prosseguir, trate a registro anônimo como já reivindicado (claimed) com o conjunto completo de escopos.

## Exchange

Troque a `identity_assertion` por um access_token no `token_endpoint`, com o grant JWT-bearer da RFC 7523. Aceita formulário ou JSON:

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

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<identity_assertion>
&resource=https://explorigen.io/api/v1/
```

As duas chamadas que escrevem — o Register acima e este Exchange — aceitam `Idempotency-Key`, um cabeçalho com uma chave única escolhida por você (ASCII visível, até 255 caracteres). Repetir o pedido com a mesma chave e o mesmo corpo devolve a MESMA resposta da primeira vez, com `Idempotency-Replayed: true`, em vez de emitir outra credencial: é o que torna seguro repetir depois de a rede cair no meio. A mesma chave com corpo diferente é 422 `idempotency_key_reuse`. A resposta guardada expira junto com o que ela carrega — 24 h no registro, 1 h na troca — e repetir não renova prazo nenhum. Se o ambiente não tiver onde guardar, a resposta sai com `Idempotency-Status: unsupported` e a emissão é nova: o cabeçalho nunca é aceito em silêncio.

Resposta 200 (com `Cache-Control: no-store` e `Pragma: no-cache`):

```json
{ "access_token": "<JWT>", "token_type": "Bearer", "expires_in": 3600, "scope": "site.read prontidao.read" }
```

`resource` é opcional; se vier, tem de ser exatamente `https://explorigen.io/api/v1/` (igualdade estrita, com a barra final), senão `400 invalid_target`. O access_token dura 1 hora (`typ` = `access`), carrega `sub` = `registration_id`, `scope` e `aud` = `https://explorigen.io/api/v1/`. A mesma assertion pode ser trocada de novo enquanto valer; assertion expirada, adulterada ou de outra origem → `400 invalid_grant`, e aí você volta a Register. Não há refresh_token: os dois passos substituem o refresh.

## Use the access_token

```http
GET https://explorigen.io/api/v1/busca?q=seo
Authorization: Bearer <access_token>
```

Rotas da API v1 (todas `GET`, todas somente leitura): `https://explorigen.io/api/v1/` (índice, ecoa `registrado_como` e `sandbox_token`), `https://explorigen.io/api/v1/paginas` (lista paginada: `limite`, `cursor`, `total`, `proximo`), `https://explorigen.io/api/v1/paginas/<rota>` (a página em markdown, só para rota do índice), `https://explorigen.io/api/v1/busca?q=` (busca lexical, keyword search), `https://explorigen.io/api/v1/solucoes`, `https://explorigen.io/api/v1/precos` (o mesmo de https://explorigen.io/pricing.md) e `https://explorigen.io/api/v1/prontidao/<dominio>` (escopo `prontidao.read`; consulta a leitura gravada, nunca mede). A descrição completa, com esquemas de resposta, está em https://explorigen.io/api/openapi.json.

Escopos: `site.read` — ler páginas, busca e catálogo (read pages, search, catalog); `prontidao.read` — consultar leituras de prontidão gravadas (read recorded readiness scores). Todo registro recebe os dois.

Cota (rate limit): toda resposta traz `RateLimit-Limit`, `RateLimit-Remaining` e `RateLimit-Reset`, e os mesmos números nos campos estruturados `RateLimit-Policy` e `RateLimit`. Com bearer são 120 requisições por 60 s, contadas pelo `registration_id` do token; sem bearer — índice público e sandbox — são 60 por 60 s, por endereço de origem. Acima disso a resposta é 429 `rate_limited` com `Retry-After` em segundos, e esperar esse tempo basta. A política inteira está em https://explorigen.io/api/versioning.md.

Renovação: quando o access_token expirar (`expires_in` segundos), repita Exchange com a mesma assertion; quando a assertion expirar (24 h) ou Exchange devolver `invalid_grant`, repita Register. Um token só vale na origem que o emitiu (`iss` e `aud` são verificados por igualdade); token de um ambiente de preview não abre a produção.

## Errors

Só os códigos que o serviço emite de fato. O corpo é sempre `{ "error", "error_description" }`; o 401 acrescenta `auth` e `sandbox`.

| código | status | onde | o que fazer |
| --- | --- | --- | --- |
| `invalid_request` | 400 | `/agent/identity`, `/api/v1/busca` | corpo ou parâmetro fora da forma (ex.: `type` ausente, `q` vazio); corrija e repita |
| `service_auth_not_enabled` | 400 | `/agent/identity` | você mandou `type: service_auth`; use `anonymous` |
| `issuer_not_enabled` | 400 | `/agent/identity` | você mandou `type: identity_assertion`; use `anonymous` |
| `invalid_grant` | 400 | `/agent/token` | assertion expirada, adulterada ou de outra origem; volte a Register |
| `unsupported_grant_type` | 400 | `/agent/token` | só `urn:ietf:params:oauth:grant-type:jwt-bearer` |
| `invalid_target` | 400 | `/agent/token` | `resource` diferente de `https://explorigen.io/api/v1/` |
| `missing_token` | 401 | `/api/v1/…` | sem `Authorization: Bearer`; siga Discover → Register → Exchange |
| `invalid_token` | 401 | `/api/v1/…` | token expirado, adulterado, de outra origem, ou token de sandbox fora de `/api/v1/sandbox/` |
| `insufficient_scope` | 403 | `/api/v1/prontidao/…` | o token não tem o escopo pedido |
| `not_found` | 404 | `/api/v1/…`, `/agent/…` | rota inexistente ou página fora do índice |
| `server_error` | 503 | `/agent/…` | o servidor está sem o segredo de assinatura (só em host de produção mal configurado); tente mais tarde |

Além desses, `413` para corpo grande (4 KB em `/agent/identity`, 64 KB nas demais Functions) e, em `/api/v1/prontidao/<dominio>`, `503` ou `429` com `Retry-After` quando o instrumento de leitura está em pausa (portão de 8 chamadas por minuto — o único rate limit que existe). O 401 traz sempre o cabeçalho `WWW-Authenticate` descrito em Discover.

Política de tentativas: 5xx → espere e repita; 4xx → não repita o mesmo pedido, aja pela tabela; 401 num token que funcionava → repita Exchange uma vez com a assertion atual e, se falhar, Register.

## Revocation

Não há revogação: nem `revocation_endpoint` (RFC 7009) para o agente chamar, nem `agent_auth.events_endpoint` para um provedor entregar Security Event Tokens, nem lista de tokens vivos. O que existe é expiração — 1 hora para o access_token, 24 horas para a `identity_assertion` — e uma única alavanca do lado do serviço: a rotação do segredo de assinatura, que invalida todos os tokens de uma vez, sem distinguir agentes (procedimento interno, documentado para o dono). Se você quer "revogar" o seu próprio token, descarte-o e deixe expirar; se suspeita que ele vazou, avise pelo e-mail publicado em https://explorigen.io/agentes e registre-se de novo. Como a API é somente leitura sobre conteúdo público, um token vazado não permite escrever, apagar nem ler nada que não esteja aberto nas outras superfícies.

## Sandbox

O sandbox (test mode) responde as mesmas rotas da v1 sob `https://explorigen.io/api/v1/sandbox/`, com dados fixos e **sem exigir token**: `https://explorigen.io/api/v1/sandbox/paginas`, `https://explorigen.io/api/v1/sandbox/paginas/<rota>`, `https://explorigen.io/api/v1/sandbox/busca?q=`, `https://explorigen.io/api/v1/sandbox/solucoes`, `https://explorigen.io/api/v1/sandbox/precos`, `https://explorigen.io/api/v1/sandbox/prontidao/<dominio>`. Toda resposta traz o cabeçalho `X-Sandbox: true` e `"sandbox": true` no corpo; a leitura de prontidão devolve uma fixture com o domínio pedido e `medidoEm` nulo, sem tocar o instrumento nem o armazenamento. Se você mandar um bearer, ele é validado e ecoado em `registrado_como`; tokens emitidos por um ambiente de preview nascem com `sandbox: true` e só valem aqui. Use o sandbox para testar o parser e o fluxo antes de gastar a primeira chamada real — e note que a única diferença entre sandbox e produção é a fonte dos dados, não a forma.
