Aller au contenu

Bac à sable et sécurité

Un agent lit des fichiers, exécute des commandes de shell et appelle des API externes pour votre compte. La défaillance qui vous préoccupe est rarement de la malveillance — c’est un agent qui se trompe de chemin et réécrit votre copie de travail, ou qui pousse une branche que vous n’aviez pas fini d’examiner. La question à laquelle le produit doit donc répondre n’est pas « l’agent est-il digne de confiance », mais « quel est le rayon d’impact quand il se trompe ».

Control Center y répond par couches, et ces couches sont indépendantes à dessein. Si l’une manque, les autres tiennent quand même. C’est important, parce que la couche la plus externe — le bac à sable du système d’exploitation — est la seule conditionnelle : elle dépend de l’hôte qui offre un backend, et sous Windows aucun hôte n’en offre.

Cinq éléments bornent l’exécution d’un agent sur tous les hôtes, avec ou sans bac à sable. Aucun d’eux n’est une frontière du noyau; tous sont réels. Là où l’hôte offre un backend, le bac à sable du système d’exploitation se place au-dessus d’eux, et la section « Ce que le bac à sable ajoute et où il s’arrête » ci-dessous explique ce que cela change.

Son propre arbre de travail en copie sur écriture. Chaque conversation provisionne sa propre copie de travail de chaque dépôt lié, clonée en copie sur écriture par l’implémentation native rift fournie plutôt que copiée octet par octet, de sorte que l’isolation est assez peu coûteuse pour être la valeur par défaut. Le dépôt enregistré que vous avez ajouté à l’espace de travail n’est pas le répertoire dans lequel travaille l’agent — deux conversations qui touchent le même dépôt ne peuvent pas voir les modifications de l’autre, et ni l’une ni l’autre ne modifie votre copie de travail.

Le grain mérite d’être précisé : l’arbre de travail est par conversation, pas par agent. Deux agents d’une même conversation obtiennent des répertoires de travail superposés distincts qui partagent le repos/ de cette conversation — c’est précisément le but, puisqu’ils collaborent à une même modification — tandis que leurs répertoires d’agent privés restent séparés. La copie sur écriture est le seul backend partout où rift est livré : un système de fichiers incapable de reflink, ou un répertoire de données situé sur un autre volume que le dépôt, fait échouer le provisionnement plutôt que de se rabattre sur autre chose. Windows est l’exception — il n’y a aucun backend de copie sur écriture là-bas, donc git worktree est le backend, et non une dégradation silencieuse.

Le confinement des chemins dans les outils de fichiers du harnais intégré. Les outils read, write, edit, apply_patch, search, find et search_files du harnais résolvent chaque chemin par rapport au répertoire de travail de l’exécution, plus des racines partagées — le répertoire repos/ de la conversation, où vivent les arbres de travail, et le répertoire des pièces jointes de l’espace lorsqu’il existe — et refusent tout ce qui aboutit à l’extérieur. Les chemins absolus ne sont acceptés que s’ils se trouvaient déjà à l’intérieur. Un lien symbolique situé dans l’arbre de travail et pointant hors de celui-ci est résolu puis refusé lui aussi. C’est pourquoi un agent ne peut pas faire un write dans ~/.ssh, même sans bac à sable en jeu.

La surface d’outils du mode. Le mode d’une conversation décide quels outils sont matérialisés pour l’exécution, point. Dans les modes en lecture seule (plan, review, orchestrate), les outils de mutation ne figurent tout simplement pas dans le registre, et la politique de commandes de shell de ces modes refuse carrément toute commande de mutation — git commit, git push, npm install, docker et le reste — plutôt que de demander une approbation. Un outil absent ne peut pas être appelé, ce qui est une garantie plus forte qu’un outil présent mais contrôlé.

Les garde-fous d’actions unifiés. Chaque outil de mutation déclare les classes d’effets qu’il peut produire, et le moteur de politiques résout chacune d’elles en autorisation, en demande d’approbation ou en refus avant que l’appel s’exécute. Une décision prompt sans approbateur connecté se résout en refus — une question que personne ne peut répondre n’est jamais un oui. Voir Garde-fous pour le modèle de résolution.

Des identifiants délivrés par le courtier et un environnement assaini. Les identifiants sont émis à chaque lancement et révoqués à la fin de l’exécution, et le jeton GitHub d’une exécution peut lire, mais pas pousser — voir « Les poussées passent par Control Center » plus bas. ACP, le bash du harnais et Claude Code sous l’enveloppe native reçoivent un environnement dépourvu des vecteurs d’injection de l’éditeur de liens dynamique et de l’interpréteur (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND et leurs semblables) avant le démarrage du processus. Claude Code par l’intermédiaire de NoSandboxAdapter hérite de l’environnement de l’hôte.

Les outils de fichiers du harnais s’exécutent dans le processus du serveur, et non dans un processus enfant, de sorte qu’aucun profil de bac à sable ne pourrait les contraindre même si on en attachait un — seul bash passe par le lanceur de commandes isolé. Leur confinement repose sur la vérification de chemin décrite ci-dessus, et cette vérification est donc porteuse plutôt que d’être une deuxième ligne de défense.

L’outil bash est l’autre endroit où il faut être précis. Ses commandes sont évaluées en fonction de la politique de commandes du mode, puis lancées avec un environnement assaini et l’arbre de travail comme répertoire de travail. Là où un backend existe, ce lancement est enveloppé, de sorte que le bac à sable est ce qui empêche une commande de shell d’écrire à l’extérieur de l’arbre de travail; là où il n’y en a pas — Windows, ou Linux sans bwrap et socat — la politique de commandes est la seule chose entre la commande et le reste du disque. En mode chat, la politique refuse catégoriquement une liste fixe (élévation de privilèges, destruction du système et du disque, évasion de conteneur et d’espace de noms) et demande une approbation pour les mutations de l’état distant (poussées git push, gh pr create, publications et installations de paquets). Tout ce qui ne figure sur aucune des deux listes s’exécute. Les pipelines, les chaînes et les sous-shells sont analysés et chaque sous-commande est évaluée séparément, la décision la plus restrictive l’emportant.

Claude Code, lorsqu’il sert d’adaptateur, est le cas où le bac à sable pèse le plus lourd. Control Center le lance avec --dangerously-skip-permissions, parce qu’un claude -p non interactif bloquerait autrement indéfiniment sur sa propre invite d’approbation. Ses appels de lecture, d’écriture et de modification se produisent à l’intérieur de son propre processus et Control Center ne les voit jamais. Ses appels Bash sont envoyés, avant leur exécution, à un hook sur le serveur qui classe la ligne de commande et consulte la règle correspondante, et les outils mcp__* que Control Center sert passent le point de contrôle comme d’habitude. Là où un backend existe, ces appels invisibles se produisent quand même à l’intérieur de l’enveloppe du bac à sable, de sorte que le système d’exploitation est la frontière autour d’eux. Là où aucun n’existe, rien ne l’est. Chaque adaptateur l’énonce dans sa matrice d’application — voir la matrice d’honnêteté des adaptateurs.

Il n’existe aucun indicateur d’autorisation par agent. Le droit pour un agent de créer un commit, de pousser, d’ouvrir une pull request, d’écrire dans un outil de suivi ou d’atteindre le réseau relève des garde-fous — autoriser, demander d’abord ou refuser — et les mêmes règles s’appliquent quel que soit l’adaptateur qui exécute l’agent.

Un push est l’action qui doit tenir même face à un agent qui cherche à contourner une règle : il est donc appliqué par ce que l’agent détient plutôt que par ce qu’il exécute. Avec une GitHub App configurée, une exécution dans un arbre de travail GitHub reçoit un jeton d’installation limité au dépôt, qui peut lire le contenu et travailler avec les pull requests mais pas pousser. La configuration git de l’exécution réécrit chaque URL de push GitHub (https://github.com/, git@github.com:, ssh://git@github.com/) vers la passerelle d’exécution d’agent, un point de terminaison en boucle locale sur cc_server. La passerelle lit les refs que le push met à jour, interroge la règle Pousser vers un dépôt distant — une règle limitée à des branches, comme « refuser les push vers main », s’applique donc — et ne transmet le push à GitHub qu’ensuite, avec un jeton d’écriture qu’elle émet à cette fin et ne remet jamais à l’agent. Un push refusé apparaît dans la sortie de git elle-même :

! [remote rejected] main -> main (Control Center policy: …)

La passerelle ne transmet les push qu’au dépôt propre à l’arbre de travail ou à un dépôt GitHub lié à l’espace de travail. Les fetch vont directement à GitHub avec le jeton de lecture.

Les autres commandes shell rencontrent une barrière plus souple. L’outil bash du harnais et, via un hook, l’outil Bash de Claude Code classent chaque ligne de commande et interrogent la règle dont elle relève : git commit, merge, rebase, cherry-pick, revert et am relèvent de Créer un commit ; gh pr create de Ouvrir une pull request ; gh pr merge, review, comment et close de Publier une revue ou fusionner ; gh issue create, edit, comment et close de Écrire dans un outil de suivi externe ; les installations de paquets de Installer un paquet ; curl et wget de Accéder au réseau. Cela reconnaît l’écriture habituelle d’une action, et un agent déterminé à en cacher une (eval, un fichier de script, un appel d’API direct) y parvient. Les push n’en dépendent pas.

Aucune clé d’API de tickets n’est placée dans un bac à sable. Les agents écrivent les tickets via les outils MCP de Control Center, qui interrogent Écrire dans un outil de suivi externe.

La règle Accéder au réseau est lue une seule fois, au démarrage de l’exécution. Le réseau d’un bac à sable est soit activé, soit désactivé : un refus le désactive, tandis qu’autoriser et demander d’abord le laissent activé ; les outils web_fetch et web_search du harnais continuent de demander à chaque appel. Les identifiants sont révoqués au démontage de l’exécution, avec tout jeton d’écriture émis par la passerelle, si bien qu’aucune référence ne survit à l’exécution.

Ce que le bac à sable ajoute et où il s’arrête

Section intitulée “Ce que le bac à sable ajoute et où il s’arrête”

L’énumération SandboxBackend comporte trois valeurs — native (Seatbelt sur macOS, bubblewrap sur Linux et WSL2), none et microvm. L’enveloppement des commandes d’agent n’utilise que les deux premières : microvm est le backend de machine virtuelle cloisonnée pour les terminaux interactifs et les rigs, et non une mise à niveau silencieuse d’une enveloppe bash. Comprendre la forme native explique ce que vous gagnez là où une enveloppe est attachée, et ce qu’elle n’allait jamais vous donner.

cc_server sonde l’hôte une fois au démarrage et, là où un backend répond, il alimente les deux jointures du chemin de lancement, parce que les transports n’en partagent pas une seule. La première (SandboxPort) enveloppe l’interface de ligne de commande de Claude Code. La seconde (SandboxManager) enveloppe les adaptateurs ACP et l’outil bash du harnais intégré. N’en alimenter qu’une laisserait les autres se lancer sans enveloppe, ce qui se produisait auparavant. Sur un hôte sans backend, ou sous --sandbox off, ils se lancent tous sans enveloppe et le serveur indique dans quelle situation vous êtes sur sa ligne de démarrage. ACP et le bash du harnais assainissent quand même l’environnement; Claude Code par l’intermédiaire de NoSandboxAdapter hérite de l’environnement de l’hôte.

Une surface s’y soustrait délibérément : le terminal hébergé sur le serveur accepte un argument de backend par session, et cc_server le construit en prenant par défaut le shell de l’hôte, de sorte qu’un terminal que vous ouvrez depuis l’application s’exécute sans bac à sable, à moins qu’un client ne nomme native explicitement. Il s’agit d’un vrai shell sur l’hôte du serveur, et non d’une exécution d’agent.

Sous Linux, bubblewrap construit un espace de noms de montage, de sorte que l’agent ne voit réellement que ce qui y a été lié. Sous macOS, sandbox-exec applique un profil Seatbelt qui est permissif par défaut — un profil entièrement fermé par défaut rend un shell interactif inutilisable, parce que le nombre d’appels dyld, XPC et Mach à énumérer est impraticable. Le profil macOS part donc de (allow default) et y découpe des refus : lectures de chemins secrets, politique d’écriture réinitialisée en refus avec des autorisations explicites par-dessus, blocage de la suppression (unlink) sur chaque chemin refusé et ses ancêtres pour qu’un renommage ne puisse pas contourner un refus d’écriture, refus d’exécution pour les binaires toujours dangereux et pour tout ce qui est copié dans un répertoire accessible en écriture, et réseau limité aux ports du proxy de bouclage plus DNS.

La conséquence pratique : sous Linux, la vue qu’a l’agent du système de fichiers est rétrécie; sous macOS, l’agent peut encore lire la majeure partie de l’hôte et vos copies de travail enregistrées sont protégées par des refus explicites d’écriture et de renommage plutôt que par l’invisibilité. Les deux sont une isolation au niveau de l’espace de noms, pas une frontière de sécurité du noyau — les backends le disent dans leurs propres notes de capacités.

La série de montages par liaison est l’endroit où la frontière entre agents est tracée. Le lancement en superposition (Claude Code et ACP) monte jusqu’à quatre chemins, résolus par exécution : le répertoire de travail superposé de l’agent en lecture-écriture, le répertoire de configuration de l’agent en lecture seule, le répertoire repos/ partagé de la conversation en lecture-écriture lorsqu’il existe, et le répertoire des pièces jointes de l’espace en lecture seule lorsqu’il existe. Le répertoire d’un agent voisin n’est jamais monté. L’enveloppe bash du harnais est plus étroite : elle ne monte par liaison que le répertoire de travail de l’exécution. C’est la même forme que la vérification de chemin intraprocessus du harnais applique en logiciel, de sorte que les deux concordent — mais seule la vérification de chemin couvre les outils de fichiers du harnais lui-même, qui n’atteignent jamais un espace de noms de montage.

Vos copies de travail enregistrées rejoignent l’ensemble de refus d’écriture par-dessus, dans tous les modes et sur les trois chemins enveloppés (Claude Code, ACP, bash du harnais), et chaque règle est émise à la fois sous sa forme littérale et sous sa forme résolue par lien symbolique. La seconde forme n’est pas un simple raffinement : le noyau compare une règle de refus au chemin résolu et macOS fait de la résolution le cas courant plutôt que l’exception — /tmp est en réalité /private/tmp, /var/folders/… est en réalité /private/var/folders/… — de sorte qu’une copie de travail atteinte par l’une ou l’autre forme aurait été accessible en écriture alors que le profil semblait l’interdire.

Le réseau, sous un bac à sable, est une liste d’autorisation de domaines par session, appliquée par des proxys de bouclage intraprocessus. Le trafic sortant est refusé par défaut : lorsque le réseau est désactivé, les proxys démarrent quand même, mais leur liste d’autorisation est vide, de sorte que toute connexion sortante est refusée. Lorsqu’il est activé, le résolveur construit la liste d’autorisation à partir d’une base de référence établie avec soin — fournisseurs d’API de LLM, hébergement Git, registres de paquets, catalogue de modèles — plus les domaines de GitHub, et il compare l’hôte cible de chaque connexion à cette liste. Les caractères génériques fonctionnent (*.example.com correspond aux sous-domaines, pas au domaine racine). Une liste distincte de refus permanents l’emporte sur la liste d’autorisation et couvre les points de terminaison de métadonnées infonuagiques et les collecteurs de télémétrie. Ce qui n’est pas configurable, c’est la liste elle-même : la spécification comporte un champ egressAllowlist pour des domaines supplémentaires, mais rien dans le chemin de lancement ne le remplit, de sorte qu’en pratique chaque exécution obtient la base de référence plus GitHub, et rien de plus.

Les refus, sous macOS, sont observables : un moniteur écoute le journal système à la recherche des refus de Sandbox, filtre le bruit connu, déduplique les tempêtes de nouvelles tentatives et expose chacun d’eux comme un SandboxViolation qui nomme ce qui a été tenté et ce qui a été bloqué. Sous Linux, le chemin d’enveloppement analyse stderr à la recherche de EPERM, Operation not permitted et Permission denied et émet le même type d’événement — il n’y a aucune écoute du journal système. Ces éléments arrivent sur le propre flux d’événements de l’exécution, de sorte qu’un appel système refusé apparaît dans la transcription plutôt que seulement dans un journal. La télémétrie n’existe que pendant qu’un bac à sable natif enveloppe réellement le processus — sur un hôte sans backend, il n’y a rien à observer. Elle est aussi sans rapport avec les garde-fous : une violation provient d’un appel système refusé, jamais d’une consultation de règle.

Les confirmations viennent des garde-fous, pas du bac à sable

Section intitulée “Les confirmations viennent des garde-fous, pas du bac à sable”

Le bac à sable n’a aucun chemin d’approbation. Il démarre, exécute, émet des événements et se démonte; il n’y a nulle part en lui où un humain peut répondre à une question. Quand vous voyez une confirmation dans un espace — approuver cette poussée, approuver cette suppression — elle a été soulevée par le moteur de garde-fous (ou par la politique de commandes du harnais) avant que l’action n’atteigne le moteur d’exécution.

Cet ordre est délibéré. Une approbation qui surviendrait après l’appel système serait une trace d’audit, pas un point de contrôle. C’est aussi pourquoi la règle du refus par défaut vit dans les garde-fous : sans approbateur connecté, la décision est prise avant que quoi que ce soit s’exécute, et c’est un refus.

Exécuter les diagnostics d’un agent est le seul endroit qui énonce ce qui peut être vérifié à la main.

  • Rigs et enceintes : l’échelon au-dessus du bac à sable — quand le travail exige une machine jetable entière avec une frontière de noyau, pas un processus enveloppé
  • Garde-fous : la couche de politiques au-dessus du plancher du système d’exploitation et l’origine réelle des approbations
  • Cycle de vie du lancement d’un agent : où l’isolation s’insère dans le flux de lancement
  • Modes : comment un mode rétrécit la surface d’outils et la politique de commandes
  • Référence des backends de bac à sable : les backends et leur configuration