Um manifesto de agente do Copilot é uma fronteira de segurança. Aqui fica o verificador.
Todas as propriedades de âmbito de um manifesto de agente do Microsoft 365 Copilot são opcionais e, em seis casos, omitir uma dá o âmbito mais largo em vez do mais estreito. Um validador de JSON Schema aprova esses manifestos, porque nenhum deles tem nada de inválido: falta uma coisa que era opcional. Por isso escrevemos a verificação que os apanha. Encontra seis problemas num manifesto que passa em todos os outros testes.
Um agente declarativo para o Microsoft 365 Copilot não é código. É um manifesto JSON que diz ao modelo para que serve, o que pode ler e o que pode fazer. O modelo, a orquestração e o recorte por permissões são do Copilot. As fronteiras são consigo.
Quase todas as propriedades que definem essas fronteiras são opcionais. Em seis casos, deixar uma de fora não restringe o agente. Dá-lhe o âmbito mais largo que existe.
A linha que dá tudo
Um manifesto precisa de três campos: name, description e instructions. Tudo o resto são capacidades, e esta é a que mais pesa.
{
"capabilities": [
{
"name": "OneDriveAndSharePoint",
"items_by_url": [
{ "url": "https://tenant.sharepoint.com/sites/Tenders/Documents/Live" }
]
}
]
}
Agora apague o array items_by_url. A documentação é explícita: sem items_by_url nem items_by_sharepoint_ids, o agente passa a poder aceder a todas as fontes de OneDrive e SharePoint da organização.
Não a algumas. A todas. E a versão errada não dá erro nenhum: produz um agente que funciona, responde a perguntas e fundamenta as respostas em todos os sites a que a pessoa que fala com ele tiver acesso.
O Copilot continua a aplicar o recorte de segurança dessa pessoa, portanto o agente não lê nada que ela própria não pudesse abrir. Essa garantia é real, e escrevemos sobre a mesma fronteira em o assistente de IA que só lê o que o utilizador pode ler. Mas recortar não é delimitar. Quem tiver acesso largo fica com um agente de concursos a responder a partir de ficheiros de recursos humanos e de atas da administração, porque nada lhe disse que não.
A mesma regra aparece seis vezes:
| Capacidade | Omitir isto | E o agente fica com |
|---|---|---|
OneDriveAndSharePoint |
items_by_url, items_by_sharepoint_ids |
Todas as fontes de SharePoint e OneDrive da organização |
GraphConnectors |
connections |
Todos os conectores do Copilot da organização |
TeamsMessages |
urls |
Todos os canais, reuniões e conversas |
Meetings |
items_by_id |
Todas as reuniões |
WebSearch |
sites |
A web aberta |
Email |
folders |
A caixa de correio inteira |
É uma opção defensável: um agente que por omissão não lesse nada seria inútil acabado de instalar. Mas inverte o hábito de quem escreve configuração de segurança, onde uma regra que falta costuma querer dizer negado. Aqui, o silêncio quer dizer sim.
A armadilha que sobrevive a conhecer o padrão
O esquema 1.8 acrescentou duas capacidades de escrita, EmailActions e MeetingActions. A primeira cobre triagem, envio supervisionado, eliminação, regras da caixa de entrada, resposta automática e gestão de pastas.
A documentação diz que EmailActions funciona independentemente de Email e que as restrições de âmbito definidas em Email, incluindo folders, shared_mailbox e group_mailboxes, não se lhe aplicam. Por isso este manifesto não faz o que se queria:
{
"capabilities": [
{ "name": "Email", "folders": [ { "folder_id": "inbox" } ] },
{ "name": "EmailActions" }
]
}
A leitura fica limitada à caixa de entrada. Apagar, mover e criar regras não fica limitado a coisa nenhuma. Não há maneira de delimitar EmailActions: o objeto tem exatamente uma propriedade, o nome. A decisão é binária e tem de ser tomada de propósito.
Aquele manifesto é obra de alguém que estava a ter cuidado. Ter cuidado no sítio errado dá um ficheiro que parece prudente e não é.
A verificação que apanha isto
Um validador de JSON Schema aprova todos os manifestos errados que estão aqui atrás, porque nenhum deles tem nada de inválido. Falta uma coisa que era opcional, que é diferente, e é a única que aqui interessa.
Ou seja, a revisão tem de procurar ausências, e isso é trabalho mecânico, o que quer dizer que não devia caber a uma pessoa a ler com atenção às cinco da 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
Junte-lhe vinte linhas que leem um ficheiro e devolvem um código de saída diferente de zero, e corra-o contra um manifesto que todas as outras ferramentas aprovam:
$ 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 o mesmo agente como deve ser e ele cala-se e sai com zero. É isso que lhe permite travar uma publicação, em vez de ficar num documento que ninguém abre.
A linha TRAP é a que justifica o script inteiro. Só dispara quando alguém delimitou Email e a seguir pôs EmailActions ao lado, que é precisamente o manifesto que uma pessoa cuidadosa escreve.
Corremo-lo contra um manifesto realista para o nosso próprio tenant, com o site de SharePoint devidamente delimitado, e ainda assim teve alguma coisa a dizer: TeamsMessages tinha entrado sem um array urls, portanto um agente pensado para uma biblioteca de documentos podia ler todos os canais e conversas a que o utilizador pertence. Ninguém decidiu aquilo. Foi uma linha que nunca lá esteve.
O que não apanha
Vale a pena dizê-lo com clareza, porque uma verificação que exagera aquilo que cobre é pior do que nenhuma.
Lê um ficheiro. Não sabe se o URL de SharePoint aponta para uma pasta com doze documentos ou para um site com quarenta mil, e essa diferença pesa mais do que tudo o que o script mede. Não consegue dizer se as instruções são boas. E não diz nada sobre se as pessoas que usam o agente deviam sequer ter acesso àquele conteúdo, que é uma questão de permissões anterior ao agente e que lhe sobrevive.
O que ele dispensa é a parte da revisão em que uma pessoa é pior: dar por uma coisa que não está lá.
Porque é que isto importa
O apelo de um agente declarativo é não haver código para manter. Um manifesto, algumas fontes de conhecimento, e algo genuinamente útil para uma equipa que nunca teria uma aplicação feita à medida.
É verdade, e é também por isso que o âmbito pesa mais aqui do que numa aplicação. Ninguém revê um ficheiro JSON como revê um pull request. Não há bateria de testes. Não há compilador que avise que o array que ficou de fora quer dizer o tenant inteiro.
Agora há, pelo menos, um script.


