---
title: "Agent Skills Discovery: publicar skills en .well-known para que los agentes las encuentren"
excerpt: "El RFC de Cloudflare define un índice en /.well-known/agent-skills/index.json para que un agente descubra qué skills publica un dominio. Qué dice la spec, leída del original, y cómo la implementé en este sitio con Next.js."
date: "2026-08-28T11:30:00.000Z"
category: "Inteligencia Artificial"
tech_article: true
author:
  name: "angel cruz"
  picture: "/images/me/angel-cruz.png"
ogImage:
  url: "/images/open-graph/cloudflare-monetization-gateway.png"
seo_title: "Agent Skills Discovery RFC: qué es y cómo publicar /.well-known/agent-skills"
seo_description: "Guía del Agent Skills Discovery RFC de Cloudflare: el índice index.json, los campos type, url y digest, la divulgación progresiva y una implementación real en Next.js."
---

**Agent Skills Discovery es un RFC de Cloudflare que define un sitio fijo donde un dominio publica sus [Agent Skills](https://agentskills.io/): `/.well-known/agent-skills/index.json`.** Un agente pide esa URL, recibe la lista de skills con su nombre, su descripción y el digest de cada una, y ya puede descargar solo la que necesita. Sin configuración previa y sin que nadie le pase un enlace.

Lo implementé en este sitio esta semana y me equivoqué tres veces por el camino, así que además de contarte qué dice la spec te cuento en qué me estrellé. Todo lo que sigue sale del [README del RFC](https://github.com/cloudflare/agent-skills-discovery-rfc), que leí entero, no de resúmenes.

## Qué problema resuelve

Hoy las skills están desperdigadas. El propio RFC lista dónde hay que ir a buscarlas: repos de GitHub, documentación de cada fabricante, enlaces que alguien compartió en redes, y configuración manual del usuario final.

Y formula la pregunta que nadie sabía responder de forma estándar:

> There is no standard way to answer: "What skills does example.com publish?"

La solución es deliberadamente aburrida, que es lo mejor que se puede decir de una spec de descubrimiento: registrar `agent-skills` como sufijo de URI well-known, según el [RFC 8615](https://datatracker.ietf.org/doc/html/rfc8615), el mismo mecanismo que usan `robots.txt` o `security.txt`. Una ubicación predecible y nada más.

Si vienes de [content negotiation para agentes de IA](/post/content-negotiation-agentes-ia) o de [DNS-AID](/post/dns-aid-descubrimiento-agentes-ia-dns), esto es otra pieza de la misma capa: llms.txt te dice qué contenido hay, DNS-AID resuelve dónde está el agente antes del primer HTTP, y esto responde qué capacidades publica el dominio.

## Cómo es el índice

El índice va obligatoriamente en `/.well-known/agent-skills/index.json`. La spec usa las palabras clave del [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119), así que en lo que sigue, cuando digo obligatorio es un MUST y cuando digo recomendado es un SHOULD. Este es el formato completo:

```json
{
  "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
  "skills": [
    {
      "name": "code-review",
      "type": "skill-md",
      "description": "Review code for bugs, security issues, and best practices.",
      "url": "/.well-known/agent-skills/code-review/SKILL.md",
      "digest": "sha256:c4d5e6f7..."
    }
  ]
}
```

Los cinco campos de cada entrada son obligatorios, y cada uno tiene su letra pequeña:

| Campo | Qué es | La trampa |
|---|---|---|
| `name` | Identificador de la skill | De 1 a 64 caracteres, minúsculas, números y guiones. Sin guion al principio ni al final, y sin dos seguidos |
| `type` | Tipo de distribución | Solo `"skill-md"` o `"archive"`. **Cualquier otro valor y el cliente se salta la entrada** |
| `description` | Qué hace y cuándo usarla | Máximo 1024 caracteres, y debería coincidir con la del frontmatter del SKILL.md |
| `url` | Dónde está el artefacto | Se resuelve con la URL del índice como base, según [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-5). Vale absoluta, relativa a la raíz o relativa al directorio |
| `digest` | SHA-256 del artefacto | Formato `sha256:{hex}`, con 64 caracteres hexadecimales en minúscula |

El `url` no tiene que vivir en tu dominio. La spec lo dice explícitamente: la convención es alojar las skills bajo `/.well-known/agent-skills/`, pero el campo permite servirlas desde un CDN o desde una ruta versionada.

## El `$schema` es opaco, y ahí me estrellé

Es el campo que peor se lee si vienes de JSON Schema normal.

El `$schema` de primer nivel es obligatorio. Y la spec dice esto:

> The `$schema` URI is an **opaque identifier**. Clients MUST match it against known schema URIs to determine how to process the index. The URI does not need to be resolvable.

O sea: no es un enlace que el cliente vaya a abrir. Es una cadena que compara para saber qué versión del formato está leyendo. De hecho, al escribir esto, `schemas.agentskills.io` ni siquiera resuelve por DNS.

Yo hice justo lo contrario. Mi archivo llevaba una URI equivocada, apuntando a un dominio de documentación en vez de a la del RFC, comprobé que daba 404, y de ahí deduje que la convención no existía, así que la quité. El 404 no probaba nada: por diseño esa URI puede no resolver nunca.

Y quitarla tiene consecuencias, porque la spec también define qué pasa cuando falta:

> If `$schema` is absent, clients SHOULD treat the index as v0.1.0 for backward compatibility.

La v0.1.0 usaba un array `files` de rutas sin digests. No es compatible. Así que un índice sin `$schema` se lee entero con las reglas equivocadas.

## `skill-md` o `archive`

Solo hay dos formas de distribuir una skill, y la spec es clara sobre cuándo usar cada una.

**`skill-md`** es un único archivo `SKILL.md`. Es lo recomendado para cualquier skill que no necesite nada más. El `digest` es el SHA-256 de los bytes de ese archivo.

**`archive`** es un `.tar.gz` o un `.zip` con el directorio completo, para skills que traen scripts, referencias o assets. El cliente está obligado a soportar los dos formatos. El `SKILL.md` va en la raíz del archivo, no dentro de una carpeta envoltorio.

Si eliges archive, heredas todos los problemas clásicos de descomprimir algo que te bajaste de internet, y la spec los enumera como obligaciones del cliente: rechazar rutas con `..` o absolutas, rechazar enlaces simbólicos que apunten fuera del directorio de la skill, y poner un tope al tamaño descomprimido para que nadie te tumbe con una bomba de descompresión.

## Los tres niveles de carga

Esta es la parte que más me gustó del RFC, porque es donde se ve que está pensado para el coste real de los tokens y no solo para que el JSON valide.

| Nivel | Qué se carga | Cuándo | Coste |
|---|---|---|---|
| 1 | `name` y `description` del índice | Al arrancar o al sondear | Unos 100 tokens por skill |
| 2 | El cuerpo del `SKILL.md` | Cuando la skill se activa | Menos de 5k tokens, recomendado |
| 3 | Archivos referenciados (scripts, referencias, assets) | Bajo demanda | Sin límite |

El ejemplo del RFC lo explica mejor que cualquier tabla: una skill de PDFs cuyo `SKILL.md` enlaza a `references/FORMS.md` y a `references/TABLES.md`. Un agente que solo tiene que extraer texto carga el `SKILL.md` y para ahí. Uno que tiene que rellenar un formulario sigue el enlace a `FORMS.md`. El script de tablas no se descarga nunca si nadie pide tablas.

Es decir: una skill puede traer material de referencia extenso sin cobrarte contexto por adelantado.

## Los digests no son decorativos

Todos los digests son SHA-256 sobre los bytes crudos del artefacto, en formato `sha256:{hex}`. Sirven para dos cosas distintas, y conviene no confundirlas.

La primera es **caché**: comparas el digest del índice con el que tienes guardado, y si coincide te ahorras la descarga.

La segunda es **integridad**, y ahí la spec no deja margen:

> Clients MUST verify downloaded content against the `digest` in the index. A mismatch indicates the content is corrupted or has been tampered with; clients MUST NOT use unverified content.

Esto tiene una implicación de implementación que es fácil pasar por alto: si el archivo que sirves y el digest que publicas los genera código distinto, el día que cambies una línea del `SKILL.md` y se te olvide regenerar el índice, un cliente conforme rechaza la skill entera. El fallo no es ruidoso: la skill deja de servir sin que nada proteste.

## Cómo lo implementé en Next.js

Mi solución a ese problema es la única decisión de diseño que creo que merece copiarse: **un único builder del que salen las dos cosas**.

El cuerpo de cada skill vive en un módulo de datos, `lib/agent-skills.ts`:

```ts
export const AGENT_SKILLS: readonly AgentSkill[] = [
  {
    name: "angelcruz-dev-index",
    description:
      "Orientarse en angelcruz.dev antes de buscar nada concreto: qué artículos, guías y herramientas existen, y en qué URL vive cada uno...",
    body: (base) => `# Índice de angelcruz.dev

## Cómo usarla

Pide el índice:

    GET ${base}/llms.txt
...`,
  },
];

export function buildSkillMd(skill: AgentSkill): string {
  return `---
name: ${skill.name}
description: ${JSON.stringify(skill.description)}
---

${skill.body(siteConfig.url)}`;
}
```

El route handler que sirve el artefacto llama a `buildSkillMd`. El que genera el índice llama a `buildSkillMd` **y le calcula el hash**:

```ts
function sha256Digest(input: string): string {
  return `sha256:${crypto.createHash("sha256").update(input).digest("hex")}`;
}

export async function GET() {
  const document = {
    $schema: SCHEMA_URI,
    skills: AGENT_SKILLS.map((skill) => ({
      name: skill.name,
      type: "skill-md",
      description: skill.description,
      url: `${siteConfig.url}${skillUrl(skill.name)}`,
      digest: sha256Digest(buildSkillMd(skill)),
    })),
  };

  return new Response(JSON.stringify(document, null, 2), {
    headers: {
      "Content-Type": "application/json; charset=utf-8",
      "Cache-Control": "public, s-maxage=86400, stale-while-revalidate",
      "Access-Control-Allow-Origin": "*",
    },
  });
}
```

Los bytes servidos y los bytes hasheados salen de la misma llamada. No se pueden desincronizar.

Para servir cada `SKILL.md` uso un segmento dinámico con `dynamicParams = false`, que hace que cualquier nombre que no esté en `generateStaticParams` caiga en el 404 del framework en lugar de devolver un 200 vacío. La spec pide exactamente eso: devolver 404 para skills que no existen.

```ts
export const dynamic = "force-static";
export const dynamicParams = false;

export function generateStaticParams() {
  return AGENT_SKILLS.map((skill) => ({ name: skill.name }));
}
```

Las cabeceras también están en la spec, en su sección de consideraciones HTTP. El índice va como `application/json` y el `SKILL.md` como `text/markdown` o `text/plain`. El CORS abierto es recomendado, no obligatorio, por si el cliente corre en un navegador.

### Verifica el digest contra los bytes reales

Que compile no prueba nada aquí. Lo que hay que comprobar es que el hash publicado coincide con el archivo que sale por el cable:

```bash
shasum -a 256 .next/server/app/.well-known/agent-skills/mi-skill/SKILL.md.body
```

Y comparar con el `digest` del índice generado. Si no cuadra, tienes una skill que ningún cliente conforme va a usar.

## Un detalle de YAML que casi me come

El `SKILL.md` lleva frontmatter YAML con `name` y `description`. Mi descripción decía esto:

> Orientarse en angelcruz.dev antes de buscar nada concreto: qué artículos, guías y herramientas existen

Fíjate en los dos puntos seguidos de espacio en medio de la frase. Un escalar YAML sin comillas **no puede contener** `": "`. El parser corta ahí y te suelta un `mapping values are not allowed in this context`, y el frontmatter entero deja de leerse.

La solución es comillar la descripción. Como YAML es superconjunto de JSON para cadenas, `JSON.stringify` produce un escalar válido y de paso escapa lo que haga falta:

```ts
description: ${JSON.stringify(skill.description)}
```

Lo cacé validando el archivo generado con `gray-matter` en lugar de mirarlo y darlo por bueno. Recomiendo el hábito: el frontmatter es de las pocas cosas que se rompen en silencio.

## El error más caro: un `type` inventado

El más grave de los tres, y el que llevaba más tiempo publicado.

Mi índice declaraba `"type": "knowledge"`, porque lo que publico no son skills de procedimiento sino acceso a un corpus, y "knowledge" describía bien la intención. El problema es que la intención no es un campo. `type` solo admite `"skill-md"` o `"archive"`, y la spec dice qué hace el cliente ante cualquier otra cosa:

> Clients encountering an unrecognized `type` value SHOULD skip that skill entry and MAY warn the user.

Un índice que devolvía 200, con JSON bien formado y digests correctos, y del que un cliente conforme se llevaba cero skills. Ninguna comprobación de salud lo habría detectado.

Para arreglarlo tuve que cambiar lo que publico, no cómo lo etiquetaba. Ahora publico dos `SKILL.md` de verdad que **enseñan** a consumir el corpus, en vez de apuntar directamente a él: `angelcruz-dev-index` explica cómo orientarse con `llms.txt` y cómo pedir cualquier página con `Accept: text/markdown`, y `angelcruz-dev-corpus` explica cuándo tirar del corpus completo y cómo usar el digest para no volver a descargarlo. Cumplen el formato y describen mejor lo que ofrezco.

## Preguntas frecuentes

### ¿Qué es el Agent Skills Discovery RFC?

Una propuesta de Cloudflare para registrar `agent-skills` como sufijo de URI well-known según el RFC 8615, de modo que cualquier dominio publique sus skills en `/.well-known/agent-skills/index.json` y los agentes las descubran sin configuración previa. El repositorio es público, con licencia Apache-2.0.

### ¿Es un estándar oficial del IETF?

No. Es un RFC en el sentido de "petición de comentarios", publicado como repositorio de GitHub, no un documento del IETF con número asignado. Usa las palabras clave del RFC 2119 y se apoya en RFCs reales (8615 para el well-known, 3986 para resolver URLs), pero el documento en sí es una propuesta abierta. Al escribir esto acumula 338 estrellas y su último cambio es de abril de 2026.

### ¿Dónde va el archivo index.json?

En `/.well-known/agent-skills/index.json`, en la raíz del dominio. La ruta es obligatoria y no admite variantes: es todo el sentido de una URI well-known.

### ¿Qué diferencia hay entre skill-md y archive?

`skill-md` sirve un único archivo `SKILL.md` y es lo recomendado para skills sencillas. `archive` sirve un `.tar.gz` o `.zip` con el directorio completo, y es para skills que traen scripts, referencias o assets, donde una sola descarga preserva los permisos y la estructura.

### ¿Para qué sirve el campo digest?

Para dos cosas: detectar si una skill cambió sin volver a descargarla, y verificar la integridad de lo que descargas. El cliente está obligado a comprobarlo, y si no coincide no puede usar el contenido.

### ¿Tengo que alojar las skills en mi propio dominio?

No. El índice sí, pero el campo `url` de cada entrada admite URLs absolutas, así que las skills pueden vivir en un CDN o en una ruta versionada de otro origen.

### ¿Esto reemplaza a llms.txt?

No, resuelven cosas distintas. `llms.txt` describe **contenido** para que un modelo lo lea. Agent Skills Discovery publica **capacidades**: instrucciones sobre cómo hacer algo, con su propio ciclo de carga progresiva y verificación por digest. Este sitio publica los dos, y mis skills enseñan justamente a usar el llms.txt.

## Referencias

- [Agent Skills Discovery via Well-Known URIs](https://github.com/cloudflare/agent-skills-discovery-rfc), el RFC completo de Cloudflare
- [Agent Skills](https://agentskills.io/specification), la especificación del formato de una skill
- [RFC 8615](https://datatracker.ietf.org/doc/html/rfc8615), Well-Known URIs
- [RFC 3986, sección 5](https://datatracker.ietf.org/doc/html/rfc3986#section-5), resolución de referencias URI
- [El índice de este sitio](/.well-known/agent-skills/index.json), por si quieres ver uno funcionando

Si quieres comprobar el mío, pide el índice, quédate con el digest de una skill, descarga su `SKILL.md` y hazle el SHA-256. Tienen que coincidir. Si algún día no coinciden, es que se me olvidó lo que acabo de contarte.

---

## Sitemap

Índice completo del sitio: [/sitemap.md](https://www.angelcruz.dev/sitemap.md)

Canónico HTML: [https://www.angelcruz.dev/post/agent-skills-discovery-well-known](https://www.angelcruz.dev/post/agent-skills-discovery-well-known)
