Un manifiesto de agente de Copilot es una frontera de seguridad. Aquí está el verificador.
Todas las propiedades de ámbito de un manifiesto de agente de Microsoft 365 Copilot son opcionales y, en seis casos, omitir una da el ámbito más amplio en lugar del más estrecho. Un validador de JSON Schema aprueba esos manifiestos, porque ninguno tiene nada inválido: falta algo que era opcional. Así que escribimos la comprobación que sí los detecta. Encuentra seis problemas en un manifiesto que pasa todas las demás pruebas.
Un agente declarativo para Microsoft 365 Copilot no es código. Es un manifiesto JSON que le dice al modelo para qué sirve, qué puede leer y qué puede hacer. El modelo, la orquestación y el recorte por permisos los pone Copilot. Las fronteras las pone usted.
Casi todas las propiedades que definen esas fronteras son opcionales. En seis casos, dejar una fuera no restringe al agente. Le da el ámbito más amplio que existe.
La línea que lo concede todo
Un manifiesto necesita tres campos: name, description e instructions. Todo lo demás son capacidades, y esta es la que más pesa.
{
"capabilities": [
{
"name": "OneDriveAndSharePoint",
"items_by_url": [
{ "url": "https://tenant.sharepoint.com/sites/Tenders/Documents/Live" }
]
}
]
}
Ahora borre el array items_by_url. La documentación es explícita: sin items_by_url ni items_by_sharepoint_ids, el agente pasa a poder acceder a todas las fuentes de OneDrive y SharePoint de la organización.
No a algunas. A todas. Y la versión equivocada no da ningún error: produce un agente que funciona, responde preguntas y fundamenta las respuestas en todos los sitios a los que tenga acceso la persona que habla con él.
Copilot sigue aplicando el recorte de seguridad de esa persona, así que el agente no lee nada que ella misma no pudiera abrir. Esa garantía es real, y escribimos sobre la misma frontera en el asistente de IA que solo lee lo que el usuario puede leer. Pero recortar no es delimitar. Quien tenga acceso amplio se queda con un agente de licitaciones respondiendo desde archivos de recursos humanos y actas del consejo, porque nada le dijo que no.
La misma regla aparece seis veces:
| Capacidad | Omitir esto | Y el agente obtiene |
|---|---|---|
OneDriveAndSharePoint |
items_by_url, items_by_sharepoint_ids |
Todas las fuentes de SharePoint y OneDrive de la organización |
GraphConnectors |
connections |
Todos los conectores de Copilot de la organización |
TeamsMessages |
urls |
Todos los canales, reuniones y conversaciones |
Meetings |
items_by_id |
Todas las reuniones |
WebSearch |
sites |
La web abierta |
Email |
folders |
El buzón entero |
Es una opción defendible: un agente que por defecto no leyera nada sería inútil recién instalado. Pero invierte el hábito de quien escribe configuración de seguridad, donde una regla que falta suele significar denegado. Aquí, el silencio significa sí.
La trampa que sobrevive a conocer el patrón
El esquema 1.8 añadió dos capacidades de escritura, EmailActions y MeetingActions. La primera cubre triaje, envío supervisado, eliminación, reglas de bandeja de entrada, respuesta automática y gestión de carpetas.
La documentación dice que EmailActions funciona con independencia de Email y que las restricciones de ámbito definidas en Email, incluidas folders, shared_mailbox y group_mailboxes, no se le aplican. Por eso este manifiesto no hace lo que se pretendía:
{
"capabilities": [
{ "name": "Email", "folders": [ { "folder_id": "inbox" } ] },
{ "name": "EmailActions" }
]
}
La lectura queda limitada a la bandeja de entrada. Borrar, mover y crear reglas no queda limitado a nada. No hay manera de delimitar EmailActions: el objeto tiene exactamente una propiedad, su nombre. La decisión es binaria y hay que tomarla a propósito.
Ese manifiesto es obra de alguien que estaba teniendo cuidado. Tener cuidado en el sitio equivocado da un archivo que parece prudente y no lo es.
La comprobación que sí lo detecta
Un validador de JSON Schema aprueba todos los manifiestos equivocados de aquí atrás, porque ninguno tiene nada inválido. Falta algo que era opcional, que es distinto, y es lo único que aquí importa.
O sea, la revisión tiene que buscar ausencias, y eso es trabajo mecánico, lo que significa que no debería recaer en una persona leyendo con atención a las cinco de la tarde.
# Each capability, the properties that scope it, and what happens without them.
SCOPE = {
"OneDriveAndSharePoint": (("items_by_url", "items_by_sharepoint_ids"),
"every SharePoint and OneDrive source"),
"GraphConnectors": (("connections",), "every Copilot connector"),
"TeamsMessages": (("urls",), "every channel, meeting and chat"),
"Meetings": (("items_by_id",), "every meeting"),
"WebSearch": (("sites",), "the open web"),
"Email": (("folders", "shared_mailbox", "group_mailboxes"),
"the whole mailbox"),
}
# Write capabilities. There is no way to scope them: the choice is binary.
WRITES = {"EmailActions": "delete, move and create inbox rules",
"MeetingActions": "book meetings and change the calendar"}
def check(manifest: dict) -> list[tuple[str, str]]:
found = []
caps = {c.get("name"): c for c in manifest.get("capabilities", [])
if isinstance(c, dict)}
for name, cap in caps.items():
if name in SCOPE:
props, everything = SCOPE[name]
if not any(cap.get(p) for p in props):
found.append(("OPEN", f"{name} without {' or '.join(props)}:"
f" reads {everything}"))
if name in WRITES:
found.append(("WRITES", f"{name} can {WRITES[name]}"))
# Email is scoped, EmailActions is not, and the file gives no sign of it.
if "EmailActions" in caps and any(caps.get("Email", {}).get(p)
for p in SCOPE["Email"][0]):
found.append(("TRAP", "Email is scoped but EmailActions ignores that limit"))
if not manifest.get("behavior_overrides", {}).get(
"special_instructions", {}).get("discourage_model_knowledge"):
found.append(("INVENTS", "answers from model knowledge when sources are silent"))
if not manifest.get("disclaimer", {}).get("text"):
found.append(("NO NOTICE", "the reader is not told where answers come from"))
return found
Añádale veinte líneas que leen un archivo y devuelven un código de salida distinto de cero, y ejecútelo contra un manifiesto que todas las demás herramientas aprueban:
$ python check_agent.py tender-desk.json
Tender Desk schema v1.8
OPEN OneDriveAndSharePoint without items_by_url or items_by_sharepoint_ids:
reads every SharePoint and OneDrive source
WRITES EmailActions can delete, move and create inbox rules
OPEN WebSearch without sites: reads the open web
TRAP Email is scoped but EmailActions ignores that limit
INVENTS answers from model knowledge when sources are silent
NO NOTICE the reader is not told where answers come from
6 to review
$ echo $?
1
Delimite bien ese mismo agente y se calla y sale con cero. Eso es lo que le permite frenar una publicación, en vez de quedarse en un documento que nadie abre.
La línea TRAP es la que justifica el script entero. Solo salta cuando alguien ha delimitado Email y después ha puesto EmailActions al lado, que es precisamente el manifiesto que escribe una persona cuidadosa.
Lo ejecutamos contra un manifiesto realista para nuestro propio tenant, con el sitio de SharePoint bien delimitado, y aun así tuvo algo que decir: TeamsMessages había entrado sin un array urls, así que un agente pensado para una biblioteca de documentos podía leer todos los canales y conversaciones a los que pertenece el usuario. Aquello no lo decidió nadie. Fue una línea que nunca estuvo.
Lo que no detecta
Conviene decirlo con claridad, porque una comprobación que exagera lo que cubre es peor que ninguna.
Lee un archivo. No sabe si la URL de SharePoint apunta a una carpeta con doce documentos o a un sitio con cuarenta mil, y esa diferencia pesa más que todo lo que el script mide. No puede decir si las instrucciones son buenas. Y no dice nada sobre si las personas que usan el agente deberían siquiera tener acceso a ese contenido, que es una cuestión de permisos anterior al agente y que le sobrevive.
Lo que se ahorra es la parte de la revisión en la que una persona es peor: reparar en algo que no está.
Por qué esto importa
El atractivo de un agente declarativo es que no hay código que mantener. Un manifiesto, unas cuantas fuentes de conocimiento, y algo genuinamente útil para un equipo que nunca tendría una aplicación hecha a medida.
Es cierto, y es también por eso que el ámbito pesa más aquí que en una aplicación. Nadie revisa un archivo JSON como revisa un pull request. No hay batería de pruebas. No hay compilador que avise de que el array que quedó fuera significa el tenant entero.
Ahora hay, al menos, un script.


