Saltar para o conteúdo
Blume
Português
Esc
navegarabrir⌘Jpré-visualizar
Nesta página

Descoberta de agentes

Como os agentes encontram sua superfície legível por máquina sem adivinhar — o manifesto de legibilidade para agentes, cabeçalhos Link, o catálogo de APIs da RFC 9727, WebMCP, skills publicadas, descoberta via DNS e chaves de Web Bot Auth.

Publicar llms.txt, espelhos em Markdown, uma API JSON e um servidor MCP é só metade do trabalho — um agente ainda precisa encontrar tudo isso. O Blume anuncia a superfície inteira pelas convenções que os agentes de fato sondam: um manifesto na raiz do site, cabeçalhos Link e tags <link>, arquivos well-known e o próprio model context do navegador. Tudo aqui vem ligado por padrão e é derivado do que você já habilitou.

Legibilidade para agentes

O Blume escreve um manifesto /agent-readability.json na raiz do seu site que indexa a superfície voltada a agentes descrita ao longo desta seção — assim um agente consegue descobri-la com uma única requisição, em vez de adivinhar convenções ou raspar HTML. Como o llms.txt, ele vem ligado por padrão:

seo: {
  agentReadability: true,
}

O manifesto lista apenas o que você habilitou — o padrão de espelho em Markdown bruto, a API JSON e sua descrição OpenAPI, llms.txt e llms-full.txt, o servidor MCP e seu documento de descoberta, o endpoint do Ask AI, o sitemap e os feeds RSS — junto com o nome do seu site, a descrição, o repositório de origem e a política de uso do content-signal. As URLs são absolutas quando deployment.site está definido, e relativas à raiz caso contrário:

{
  "artifacts": {
    "markdown": {
      "contentNegotiation": "text/markdown",
      "pattern": "https://docs.example.com/{route}.md"
    },
    "api": {
      "openapi": "https://docs.example.com/openapi.json",
      "pages": "https://docs.example.com/api/docs/pages.json",
      "search": "https://docs.example.com/api/docs/search"
    },
    "llmsFullTxt": "https://docs.example.com/llms-full.txt",
    "llmsTxt": "https://docs.example.com/llms.txt",
    "mcp": {
      "discovery": "https://docs.example.com/.well-known/mcp.json",
      "url": "https://docs.example.com/mcp"
    }
  },
  "description": "Docs for the Acme API.",
  "generator": "blume@1.0.0",
  "name": "Acme Docs",
  "site": "https://docs.example.com",
  "contentUsage": { "search": true, "ai-input": true, "ai-train": true },
  "repository": "https://github.com/acme/docs"
}

O campo contentNegotiation aparece apenas quando o site publicado realmente respeita o cabeçalho Accept: text/markdown — veja negociação de conteúdo; em todo outro tipo de deploy, o manifesto anuncia somente o padrão de espelho .md.

Defina seo.agentReadability como false para pular isso, ou publique seu próprio public/agent-readability.json para assumir o controle — o Blume nunca sobrescreve um arquivo que você coloca em public/.

Agentes que sondam um site não sabem que devem procurar pelo manifesto — então o Blume também o anuncia em um cabeçalho de resposta Link da RFC 8288 na página inicial, usando tipos de relação registrados na IANA:

Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json",
  </openapi.json>; rel="service-desc"; type="application/json",
  </agent-readability.json>; rel="describedby"; type="application/json",
  </llms.txt>; rel="describedby"; type="text/plain",
  </index.md>; rel="alternate"; type="text/markdown"

Cada entrada aparece somente quando o recurso correspondente está ativo. O link alternate aponta para o espelho em Markdown da página inicial — o Markdown bruto da própria página quando a rota inicial é uma página de conteúdo, ou o fallback sintetizado do llms.txt quando é uma landing page. O link service-desc (RFC 8631) aponta para a descrição OpenAPI da API JSON, e o api-catalog para o catálogo de APIs gerado. O cabeçalho acompanha todas as superfícies que o Blume controla: o servidor de desenvolvimento (confira com curl -I localhost:4321), builds estáticos via o arquivo _headers emitido (Netlify e Cloudflare) e builds de servidor na Vercel via as regras de roteamento do deploy.

Nem todo agente entra pela raiz, no entanto — um que siga um resultado de busca ou um link compartilhado cai numa página profunda e nunca vê o cabeçalho da página inicial. Por isso, toda página renderizada também carrega os mesmos links de descoberta no seu <head> HTML, usando as mesmas relações registradas na IANA:

<link
  rel="describedby"
  href="/agent-readability.json"
  type="application/json"
/>
<link rel="describedby" href="/llms.txt" type="text/plain" />
<link rel="alternate" href="/docs/example.md" type="text/markdown" />

Aqui o link alternate aponta para o espelho em Markdown bruto daquela página em si, para que o agente possa pular direto do HTML em que caiu para a versão eficiente em tokens. Como os links do head viajam junto com o HTML pré-renderizado, eles também funcionam em hosts que ignoram _headers e não conseguem enviar cabeçalhos de resposta personalizados (GitHub Pages, S3) — não importa em qual página o agente entre.

Catálogo de APIs

Quando o site publica APIs, o Blume gera um catálogo de APIs da RFC 9727 em /.well-known/api-catalog — um linkset que permite aos agentes enumerar suas APIs a partir apenas do domínio, servido com seu media type registrado application/linkset+json em todas as superfícies de build. Não há nada para configurar: o catálogo é derivado do que já está no blume.config.ts. Cada referência OpenAPI ou AsyncAPI vira uma entrada ancorada na sua rota de docs renderizada, com service-doc apontando para essa documentação e service-desc para a spec quando ela está em uma URL acessível; a própria API JSON do site vira uma entrada descrita por seu /openapi.json; e o servidor MCP vira uma entrada com seu documento de descoberta como descrição do serviço:

{
  "linkset": [
    {
      "anchor": "https://docs.example.com/reference",
      "service-doc": [
        { "href": "https://docs.example.com/reference", "type": "text/html" }
      ],
      "service-desc": [{ "href": "https://api.example.com/openapi.json" }]
    },
    {
      "anchor": "https://docs.example.com/api/docs",
      "service-desc": [
        {
          "href": "https://docs.example.com/openapi.json",
          "type": "application/json"
        }
      ],
      "service-doc": [
        { "href": "https://docs.example.com/", "type": "text/html" }
      ]
    },
    {
      "anchor": "https://docs.example.com/mcp",
      "service-desc": [
        {
          "href": "https://docs.example.com/.well-known/mcp.json",
          "type": "application/json"
        }
      ],
      "service-doc": [
        { "href": "https://docs.example.com/", "type": "text/html" }
      ]
    }
  ]
}

Um site sem referências de API, sem servidor MCP e com a API JSON desligada não emite catálogo algum — não haveria nada nele. Como em todo lugar, um arquivo public/.well-known/api-catalog que você mesmo publique vence o gerado.

WebMCP

O WebMCP é uma API de navegador emergente que permite a uma página registrar ferramentas diretamente com um navegador agêntico — sem necessidade de conexão a um servidor separado. Toda página do Blume registra a superfície somente-leitura da documentação no model context da página: search_docs (busca no site), get_page (o Markdown bruto de uma página) e list_pages (o índice do llms.txt). O script é minúsculo, não carrega nenhuma maquinaria de busca até que uma ferramenta seja de fato chamada, e vira um no-op silencioso em todo navegador sem a API — o que hoje significa todos, fora o preview inicial do Chrome. Ele se registra em qualquer superfície que a spec (ainda em fluxo) exponha (navigator.modelContext ou document.modelContext), via provideContext ou registerTool por ferramenta.

Vem ligado por padrão; defina webmcp: false para desativar:

ai: {
  webmcp: false,
}

Descoberta de skills

Se o seu projeto publica agent skillso próprio repositório do Blume publica —, aponte ai.skills para o diretório que as contém, e o build as publica para descoberta conforme a RFC de Agent Skills Discovery:

ai: {
  skills: "./skills",
}

O caminho é resolvido a partir da raiz do seu projeto, e cada subdiretório com um SKILL.md vira uma skill publicada. Uma skill que é apenas um SKILL.md solitário é copiada literalmente para /.well-known/agent-skills/<name>/SKILL.md (type: "skill-md"); uma skill com recursos de apoio (scripts/, references/, assets/) é empacotada em um .tar.gz determinístico (type: "archive") para que suas referências relativas resolvam após a extração, com os bits de execução dos scripts preservados. O índice de descoberta em /.well-known/agent-skills/index.json traz o $schema v0.2.0 e, por skill, seu nome, tipo, descrição (do frontmatter do SKILL.md), URL do artefato e o digest SHA-256 que os clientes usam para verificar os downloads.

Skills com name/description ausentes ou inválidos segundo a spec são ignoradas com um aviso de build, em vez de publicadas quebradas, e um public/.well-known/agent-skills/index.json que você mesmo publique assume o controle de toda a superfície. As skills publicadas também são listadas no llms.txt.

Descoberta via DNS (DNS-AID)

O DNS for AI Discovery é um draft emergente da IETF que permite aos agentes descobrir a superfície de IA de um site antes de fazer uma única requisição HTTP, consultando registros SVCB/HTTPS em ServiceMode em um ponto de entrada DNS well-known. Registros DNS vivem na sua zona, não no build, então essa é a única superfície de descoberta que o Blume não pode publicar por você — em vez disso, adicione um registro no seu provedor de DNS:

_index._agents.docs.example.com. 3600 IN HTTPS 1 docs.example.com. alpn=h2

Use o tipo de registro HTTPS se o seu provedor oferecer (o Vercel DNS oferece; ele não suporta o tipo SVCB puro), ou um registro SVCB em ServiceMode com os parâmetros alpn e port caso contrário. O draft também recomenda assinar a zona com DNSSEC para que resolvedores validadores retornem respostas autenticadas — provedores como a Cloudflare habilitam isso com um clique, enquanto alguns (incluindo o Vercel DNS) simplesmente não suportam.

O blume audit --url <origin> verifica isso por você: quando deployment.site está definido, a camada de rede consulta o ponto de entrada via DNS-over-HTTPS e informa o registro exato a publicar caso não exista nenhum, além de dizer se as respostas são autenticadas por DNSSEC. Defina BLUME_DOH_URL para apontar a consulta ao seu próprio resolvedor se a sua rede bloquear os públicos (Google, Cloudflare).

Web Bot Auth

O Web Bot Auth funciona na direção oposta: não se trata de agentes lendo a sua documentação, mas de os agentes da sua organização se identificarem ao fazer requisições em outros lugares. Seus agentes assinam as requisições com HTTP Message Signatures, e os sites receptores as verificam contra um diretório de chaves públicas publicado no seu domínio. Se a sua organização opera agentes e o seu site Blume vive no domínio com que eles se identificam, publique as chaves públicas deles:

ai: {
  webBotAuth: {
    keys: [{ kty: "OKP", crv: "Ed25519", x: "JrQLj5P_89iXES9-vFgrIy29c…" }],
  },
}

O Blume então serve o JWKS em /.well-known/http-message-signatures-directory com seu media type registrado em todas as superfícies de build. O diretório é público por definição, então a configuração só admite chaves públicas — um JWK contendo material privado (d, p, q, …) falha na validação com um erro em vez de publicar uma credencial vazada. Gere um par Ed25519 com:

node -e 'const { generateKeyPairSync } = require("node:crypto"); const { publicKey, privateKey } = generateKeyPairSync("ed25519"); console.log("public: ", JSON.stringify(publicKey.export({ format: "jwk" }))); console.log("private:", JSON.stringify(privateKey.export({ format: "jwk" })))'

O JWK público vai na configuração acima; o privado vai onde quer que o seu agente de assinatura rode (um gerenciador de segredos, nunca o repositório). Se a sua organização não opera agentes, pule isso — um diretório vazio não anuncia nada que valha a pena verificar.

Como o blume.config.ts é executado em tempo de build, a chave não precisa ficar hardcoded — carregue-a de uma variável de ambiente de build para manter a configuração livre de blobs de chave e rotacionar sem um commit:

const webBotAuthKey = process.env.WEB_BOT_AUTH_PUBLIC_JWK;

export default defineConfig({
  ai: {
    webBotAuth: {
      keys: webBotAuthKey ? [JSON.parse(webBotAuthKey)] : [],
    },
  },
});

Ambientes sem a variável não publicam diretório algum, e uma chave carregada dessa forma é validada exatamente como uma inline — incluindo a checagem de material privado. (A chave pública não é um segredo, então commitá-la inline é igualmente válido; a variável de ambiente é uma escolha de ergonomia, não de segurança.)

Esta página foi útil?