Aller au contenu

Garde-fous — une politique unique pour chaque action qui modifie l’état

Les agents de Control Center peuvent supprimer des fichiers, pousser des branches, publier des pull requests et appeler des API externes. Chacune de ces actions relève d’un type de risque différent, et pendant longtemps une seule d’entre elles — les commandes shell — disposait d’une politique. Les garde-fous existent pour répondre uniformément à une seule question, pour chaque action qui modifie l’état qu’un agent peut entreprendre, quel que soit l’exécuteur ou l’outil qui la propose : cette action est-elle autorisée, nécessite-t-elle une approbation, ou est-elle refusée ?

Il existe exactement un moteur de politiques. Il est consulté à chaque point où une action peut quitter le système : le registre d’outils du harnais intégré, le verrou du répartiteur MCP côté serveur et les mutations d’opérations de dépôt. Une règle écrite une fois s’applique donc aussi bien à l’exécuteur intégré qu’aux CLI d’agents externes — il n’existe pas de second chemin, plus discret, pour la contourner. L’ancienne politique de commandes limitée à bash fonctionne toujours : une règle peut cibler un préfixe de commande plutôt qu’une classe d’action, et les deux formes coexistent dans le même magasin.

Le moteur ne remplace toutefois pas le bac à sable du système d’exploitation, et le bac à sable ne le remplace pas non plus. Ils répondent à des questions différentes : les garde-fous décident si un appel que Control Center peut voir a le droit de se produire, tandis que le bac à sable délimite la portée du processus qui en résulte. cc_server applique le bac à sable lorsque l’hôte propose un backend — mais Windows n’en propose aucun et Linux a besoin de bwrap et socat. Sur ces hôtes, les garde-fous constituent la couche de contrôle la plus externe plutôt qu’une politique posée au-dessus d’un socle, ce qu’il est utile de savoir lorsque vous décidez du degré de permissivité à adopter.

ActionClass : une taxonomie volontairement fermée

Section intitulée « ActionClass : une taxonomie volontairement fermée »

Chaque outil qui modifie l’état déclare les ActionClasses auxquelles il appartient, parmi un ensemble fermé de treize classes d’effet. Cinq d’entre elles déclenchent une demande d’approbation par défaut ; les autres autorisent, et seule une règle peut changer cela.

Classe Valeur par défaut intégrée Ce qu’elle couvre
fileDelete Demander d’abord Supprimer un fichier (une écriture de type suppression, rm)
gitPush Demander d’abord Pousser vers un dépôt git distant
prCreate Demander d’abord Créer une pull request
prPublish Demander d’abord Publier une revue, fusionner une PR
vendorSyncWrite Demander d’abord Écrire dans un outil de suivi de tickets externe (Linear, Jira, synchronisation GitHub)
fileWriteOutsideWorktree Autoriser Écrire un fichier en dehors de l’arbre de travail isolé
gitCommit Autoriser Créer un commit git
networkEgress Autoriser Sortie réseau (récupération web, HTTP arbitraire)
secretAccess Autoriser Lire un secret ou un identifiant
packageInstall Autoriser Installer un paquet (npm, pip, pub, brew)
processSpawn Autoriser Lancer un processus (le shell et tout ce qui passe par le shell)
workspaceMutation Autoriser Modifier la structure de l’espace de travail (dépôts, espaces, agents)
enclosureControl Autoriser Piloter une enceinte (rig) : démarrer une VM, lui envoyer des entrées

Les huit classes autorisées par défaut ont été choisies en partant du principe que le socle du bac à sable s’applique en dessous. C’est le cas là où l’hôte propose un backend, mais pas sous Windows ni sur une machine Linux dépourvue de bwrap et socat — si vous voulez y soumettre fileWriteOutsideWorktree ou packageInstall à un contrôle, c’est une règle que vous devez écrire.

Cet ensemble est volontairement fermé. Un nouvel outil qui modifie l’état sans déclarer de classe échoue au test à cliquet, de sorte que la taxonomie ne peut s’étendre que par une décision explicite et argumentée — « la prolifération de la taxonomie est la mort de cette fonctionnalité ». Treize classes suffisent à exprimer les politiques que les gens écrivent réellement (ne jamais pousser sans demander, ne jamais supprimer en dehors d’un arbre de travail, toujours autoriser les lectures) et sont assez peu nombreuses pour que l’interface des paramètres reste compréhensible.

Une règle associe (scope, ActionClass | command prefix) → allow | prompt | deny. Les portées s’imbriquent de la plus spécifique à la moins spécifique :

espace > agent > espace de travail > préréglage de mode > valeur par défaut intégrée

La résolution suit l’ordre spécificité d’abord : la première portée dotée d’une règle correspondante décide, et la résolution s’arrête là. Le caractère le plus restrictif ne sert que d’arbitrage — entre deux règles de spécificité égale au sein d’une même portée, et lors de la combinaison des différentes classes que déclare une même action. Au sein d’une portée, une règle de commande correspond au préfixe de commande le plus long. « Autoriser » ne l’emporte sur « Refuser » nulle part.

Deux décisions offrent des garanties fortes :

  • Demandes refusées par défaut. Un prompt sans approbateur connecté se résout en refus. Une question sans réponse n’est jamais un oui.
  • Combinaison la plus restrictive. Une action qui couvre plusieurs classes les combine de la façon la plus restrictive ; lorsque plusieurs classes demandent une approbation, une seule confirmation les énumère toutes.

Il n’existe délibérément aucun verrou par tour. Chaque appel d’outil réévalue la politique depuis le début, de sorte que la répétition des demandes pour une même action est bornée par le « mémoriser cette décision » de l’opérateur lui-même, et non par quoi que ce soit lié au tour.

Une règle peut encadrer les ARGUMENTS d’une action, et pas seulement son verbe. « Peut pousser » et « peut pousser vers feature/* » sont deux affirmations différentes, et seule la seconde constitue un contrôle — un filtre de capacité qui n’autorise que le verbe est le schéma à l’origine de la plupart des incidents d’agents publiés.

Une contrainte peut viser :

  • chemins — des motifs glob (repos/**, !**/.env)
  • références — des références git, avec négation ! (['**', '!main'] se lit « n’importe quelle branche sauf main »)
  • hôtes — des destinations réseau, avec jokers de sous-domaine (*.internal)
  • commandes — des préfixes de commande, mis en correspondance sur une limite de mot
  • plafonds — maxCount / maxCents, ce qui permet d’exprimer « supprimer jusqu’à 50 fichiers »

Trois propriétés découlent du maintien d’une grammaire fermée et sans boucle, et toutes trois constituent précisément l’objectif : chaque règle se lit comme une phrase que vous pouvez relire, chaque refus peut nommer la contrainte qui a correspondu, et l’ensemble de la politique peut être énuméré — vous pouvez voir ce qu’un agent a le droit de faire avant qu’il ne s’exécute.

Trois règles qui comptent :

  • Une règle contrainte est plus spécifique qu’une règle non contrainte à la même portée, de sorte que « refuser le push vers main » l’emporte sur « autoriser le push » sans que personne n’ait à réfléchir à l’ordre.
  • Une règle restrictive (Refuser / Demander d’abord) s’applique lorsque la requête touche l’une quelconque des valeurs qu’elle nomme ; une règle permissive (Autoriser) ne s’applique que si la requête est entièrement comprise dans son périmètre. Les lire de la même façon permettrait à un agent de blanchir un chemin interdit en le plaçant à côté d’un chemin anodin.
  • Une règle qui nomme une facette sur laquelle la requête ne dit rien n’est ni appliquée ni ignorée : elle déclenche une demande d’approbation. L’ignorer silencieusement rendrait toute règle de branche protégée inopérante face à un outil qui ne déclare pas sa référence ; refuser silencieusement reviendrait à rejeter tout push que l’extracteur ne sait pas décrire. Sans approbateur connecté, la règle de refus par défaut déjà en place transforme cette escalade en refus.

Ce qu’une règle de commande peut intercepter ou non

Section intitulée « Ce qu’une règle de commande peut intercepter ou non »

Une règle commandPrefix correspond sur une limite de mot : git push couvre git push origin main et pas git pushx. Elle ne normalise pas le shell. git push (deux espaces), cd sub && git push et GIT_DIR=… git push sont des chaînes différentes, et aucune ne correspond à une règle portant sur git push.

C’est une limite réelle, et elle est énoncée plutôt que masquée : les règles de commande sont une commodité offerte à l’opérateur pour façonner le comportement ordinaire des agents, et non une frontière contre un agent adversaire. Les frontières sont les classes d’effet (qu’un outil déclare et ne peut pas reformuler), le filet de commandes du mode situé sous le magasin de politiques, et le bac à sable du système d’exploitation sous ces deux éléments. Une règle de commande qui se résout en allow ne relâche jamais ni l’un ni l’autre — le filet du mode s’exécute après, et son deny est définitif.

Répondre à une demande par « mémoriser cette décision » écrit une véritable règle de politique : limitée à une portée (espace / agent / espace de travail), contrainte sur ses arguments (approuver un push vers feature/login accorde feature/**, et non main) et auto-révocable au terme d’un TTL explicite. Dire oui pour les prochaines heures n’équivaut pas à réécrire discrètement votre politique permanente, et les règles ainsi écrites atterrissent dans le même magasin, avec la provenance remembered et une date d’expiration, de sorte qu’elles peuvent être révoquées avant terme.

Chaque règle en porte un :

  • consultatif — autorise, mais consigne que la règle a correspondu. C’est le chemin d’adoption : déployez une règle stricte en mode consultatif, consultez le journal d’audit pendant une semaine pour voir exactement ce qu’elle aurait bloqué, puis promouvez-la. Sans ce palier, les politiques strictes ne sont jamais activées.
  • souple — refuse, avec possibilité de dérogation par une personne disposant de la permission de dérogation, accompagnée d’une justification consignée. La dérogation est elle-même un événement audité.
  • strict (valeur par défaut) — refuse. Rien ne permet d’y déroger.

Un opérateur d’installation peut épingler une politique qu’aucun administrateur d’espace de travail ne peut assouplir. Les règles gérées résident côté serveur et sont fusionnées de la façon la plus restrictive avec la chaîne propre à chaque espace de travail — elles ne constituent pas une portée en tête de cette chaîne, car un allow géré placé en tête permettrait à l’installation de passer outre un refus d’espace de travail, soit l’inverse de ce à quoi sert un plafond. Une règle gérée ne peut donc jamais que resserrer.

Définir CC_SERVER_MANAGED_POLICY sur un fichier JSON prime entièrement sur les règles enregistrées, ce qui permet à un opérateur d’épingler une posture qu’aucune interface d’administration ne peut inverser — la réponse à « puis-je empêcher mes développeurs de désactiver les contrôles de sécurité ? »

Le cadran d’autonomie est un profil, pas un système parallèle

Section intitulée « Le cadran d’autonomie est un profil, pas un système parallèle »

Le cadran d’autonomie propre à chaque espace est un profil nommé appliqué à ce même magasin de politiques, et non un second mécanisme. Ses trois réglages sont stockés et transmis sous la forme proposeOnly, actWithApproval et actFreely (les formes avec traits d’union ne sont que du texte d’interface, et autonomy.setForSpace rejette tout le reste). Ne pas le définir équivaut à actWithApproval.

Ce que chacun fait à une décision résolue :

  • proposeOnly refuse d’emblée tout outil soumis à un contrôle. L’agent peut toujours raisonner et répondre ; il ne peut simplement pas agir, et son message en explique la raison.
  • actWithApproval (et l’absence de réglage) correspond à la porte d’approbation à refus par défaut décrite plus haut.
  • actFreely pré-approuve tout ce qui ne s’est pas résolu en un deny strict.

Ce dernier mérite d’être énoncé sans détour plutôt qu’adouci : sous actFreely, une décision prompt n’est pas transmise à un approbateur — elle est autorisée et personne n’est sollicité. Seule une règle deny explicite survit au cadran. actFreely est donc une concession délibérée d’autonomie, et non un réglage de confort, et la règle de refus par défaut protège précisément le cas où cela compte (actWithApproval, la valeur par défaut).

La délégation entre agents est protégée au point de passage delegate_task par quatre garde-fous : un plafond de profondeur (3 par défaut), la détection des cycles, un plafond d’autonomie (un agent délégué ne peut jamais agir avec plus d’autonomie que celui qui l’a délégué — un privilège ne peut pas être blanchi en confiant le travail à un agent plus libre) et une enveloppe budgétaire (la délégation est imputée au budget restant du délégateur et ne peut pas en créer davantage). Chacun est refusé bruyamment, la raison du garde-fou étant renvoyée mot pour mot à l’agent.

Tous les exécuteurs ne permettent pas à Control Center d’intercepter chaque action. Les CLI d’agents externes exécutent certains outils nativement, dans leur propre processus, là où aucun verrou ne peut s’interposer. Plutôt que de prétendre le contraire, chaque adaptateur porte une matrice d’honnêteté : pour chaque adaptateur, si Control Center filtre la surface d’outils, intercepte les appels d’outils, observe le contrat d’achèvement, peut voir les outils natifs de l’exécuteur et si les outils exécutés dans le processus sont couverts par un profil de bac à sable.

Deux entrées méritent d’être lues avant de faire confiance à une configuration. Le harnais intégré déclare que ses outils de fichiers exécutés dans son propre processus ne sont pas placés dans un bac à sable — la surface d’outils et ce moteur de politiques constituent sa seule frontière en matière de système de fichiers. Claude Code déclare que ses propres appels d’outils ne sont pas interceptables, car il est lancé avec --dangerously-skip-permissions ; Control Center voit ses appels mcp__* et, via un hook, ses lignes de commande Bash, tandis que ses propres outils de lecture, d’écriture et d’édition s’exécutent sans être vus.

L’écran des paramètres affiche cette matrice, car le seul échec impardonnable pour un système de garde-fous est de revendiquer une couverture qui n’existe pas.

Les garde-fous se trouvent dans Paramètres → Espace de travail → Autorisations des agents (/workspaces/<id>/settings/workspace/permissions). Cet écran regroupe la matrice de politiques où les règles s’écrivent, des modèles de politique, une sonde « et si ? », le journal d’audit des autorisations et la matrice d’honnêteté des adaptateurs.

La sonde prend une classe d’action (ou une commande) et une portée, puis indique quelle règle a décidé et pourquoi. Comme la résolution est pure et déterministe — même action, même portée, même décision — la réponse de la sonde est celle que l’agent obtiendra.