Un agente custom de Copilot en VS Code es, al final, un fichero markdown en .github/agents/. Lo abres, editas el system prompt, lo invocas, y responde. Cómodo. El problema aparece cuando alguien pregunta lo aburrido: ¿qué prompt se ejecutó de verdad la última vez? ¿el que está en git, o el que ese desarrollador tenía sin commitear en su disco? Y de ahí sale todo lo demás: versiones, drift, y quién corrió qué.

Así que hice lo de siempre: en vez de discutirlo con un diagrama, lo compilé. Este repo captura la telemetría completa de los agentes custom de Copilot Chat vía OpenTelemetry, la manda a Jaeger, y sobre esos traces monta un informe de auditoría y drift.

La tesis, en una frase

El trace sabe qué se ejecutó. Git sabe qué debería haberse ejecutado. El drift es la resta.

No hace falta un campo version: en el frontmatter del agente. No hace falta un registro aparte. Todo el diseño cabe en dos hechos que ya tienes delante:

  • Cada trace lleva copilot_chat.repo.head_commit_hash — el commit exacto del workspace en el momento de la invocación.
  • Cada trace lleva gen_ai.input.messages — el system prompt que VS Code mandó de verdad al modelo, no el que ahora mismo hay en disco.

El primero te dice qué definición tocaba correr. El segundo te dice qué se ejecutó realmente. Compara uno contra otro y el drift deja de ser una sospecha para convertirse en un aserto.

Git como fuente de verdad

El versionado se apoya en git y en nada más. Cada hash de commit identifica con precisión qué definición del agente estaba en el repo cuando se invocó:

trace A  →  commit abc123  →  summary.agent.md en ese commit = definición V1
trace B  →  commit def456  →  summary.agent.md en ese commit = definición V2

¿Quieres saber qué instrucciones usó un trace de hace tres semanas? git show <commit>:.github/agents/summary.agent.md. No hay memoria tribal, no hay «creo que entonces el prompt decía…». El SHA es la versión.

Y aquí está el matiz que evita falsos positivos: el informe distingue dos columnas de commit. RepoCommit es el HEAD del repo (cambia con cualquier commit a cualquier fichero); AgentVer es el último commit que tocó ese .agent.md. Si el repo avanzó pero el agente no cambió, AgentVer se queda quieto y el drift report marca [OK]. Sin ruido.

Los cuatro estados del informe

El verificador lee los traces de Jaeger, reconstruye la tabla de invocaciones y emite un informe con cuatro veredictos. El más interesante no es el [OK]:

[DIRTY] summary  — 2 invocación(es) con cambios locales sin commitear
          El system prompt ejecutado no coincide con lo que hay en git.

[DIRTY] es la joya. El script saca el system prompt real del trace (gen_ai.input.messages) y lo compara contra git show <AgentVer>:.github/agents/<agente>.agent.md. Si el cuerpo en git no aparece en el prompt ejecutado, alguien corrió con cambios locales sin subir. Requiere captureContent: true, y sin él estás ciego a esto. Los otros tres — [DRIFT] (dos versiones distintas en poco tiempo), [NEW] (agente que no existía en el commit base), [OK] (verificado contra git) — completan el cuadro.

Lo que ganas: auditoría gratis

Esto es lo que de verdad me interesa. Si toda invocación deja un trace ordenado con quién, qué commit, qué prompt, qué herramientas y cuántos tokens, ese trace ya es la traza de auditoría. No hay que instrumentar nada aparte. La identidad del usuario (enduser.id, enduser.name, enduser.github.login) se inyecta sola al arrancar, sin hardcodear a nadie: cualquiera que clone el repo y ejecute el script queda identificado en el panel Process de Jaeger. Coste por agente, por versión, por usuario — todo sale de la misma propiedad que te da la trazabilidad.

La parte incómoda: auditar no es prevenir

No vendo humo, así que lo digo claro: esto te da visibilidad forense, no prevención. Puedes reconstruir con precisión qué pasó, pero no puedes impedir que pase. Un desarrollador edita su .agent.md local, lo invoca las veces que quiera, y solo lo verás después, en el trace. Peor: puede abrir cualquier workspace en cualquier carpeta, crear su propio .github/agents/ y correr lo que le dé la gana — VS Code no restringe de qué rutas carga agentes.

Aquí es donde la mayoría de las opciones se caen. Bloquear ficheros con icacls, CODEOWNERS, submódulos read-only, sync por MDM… todas protegen la copia, y todas se saltan desde la máquina del desarrollador. La única que no se puede evitar vive en la frontera de red: un API Gateway con inspección TLS que mire el gen_ai.input.messages[system] de la llamada real al LLM y lo compare contra un registro de cuerpos autorizados. Da igual de dónde salió el fichero: el contenido ejecutado viaja en esa petición HTTP, y el gateway lo ve. Combínalo con un check server-side en CI/CD (que el desarrollador no puede modificar) y tienes puerta de commit y puerta de runtime, ninguna de las dos sorteable desde local.

Mi punto de vista personal

Llevamos meses tratando los agentes custom como ficheros de configuración inofensivos. No lo son: un .agent.md es un system prompt que gobierna cómo se comporta la IA sobre tu código, y en un entorno regulado eso es superficie de supply chain como cualquier dependencia. La diferencia es que a una dependencia le exiges un hash y una versión, y al agente no le exiges nada.

Lo bonito de apoyarse en git y en el trace es que la trazabilidad sale casi gratis. Lo honesto es reconocer que la trazabilidad, sola, es forense: te cuenta la película después de rodarla. Si de verdad necesitas prevenir — no solo saber —, el sitio para hacerlo no es el disco del desarrollador, es la red. Este repo es la capa de evidencia; el gateway es la de control. Las dos hacen falta, y confundirlas es lo que separa «tengo telemetría» de «gobierno mis agentes».