Une même commande d’automatisation de l’interface fonctionne parfaitement lorsqu’elle est lancée manuellement en SSH, mais ne parvient plus à effectuer une capture d’écran, à cliquer dans une fenêtre ou à envoyer des événements à une autre application depuis un runner CI. Relancer le job ne résout généralement rien : la défaillance ne vient pas de la logique du script, mais du mécanisme macOS de transparence, de consentement et de contrôle, ou TCC. Celui-ci ne vérifie pas seulement « quel utilisateur exécute la commande » ; il tient également compte de la session graphique concernée, du processus responsable et d’un éventuel changement d’identité du code.
Vérifier qu’il s’agit bien d’un échec TCC
Les problèmes TCC sont souvent confondus avec un pilote défaillant, une fenêtre qui ne s’est pas ouverte ou une commande arrivée à expiration. Commencez par séparer le job en deux catégories : les étapes strictement exécutées en ligne de commande et celles qui nécessitent des autorisations graphiques. Si la compilation, la lecture du dépôt et les requêtes réseau ordinaires fonctionnent, mais que les captures d’écran, le contrôle via l’accessibilité ou les événements d’automatisation échouent séparément, il devient pertinent d’examiner TCC.
Commencez par consigner le contexte d’exécution :
id
whoami
printf 'console_user=%s
' "$(stat -f '%Su' /dev/console)"
printf 'uid=%s
' "$(id -u)"
launchctl print "gui/$(id -u)" >/tmp/gui-domain.txt 2>&1
ps -axo user,pid,ppid,command | grep -E 'runner|xcodebuild|osascript' | grep -v grep
Si l’utilisateur associé à /dev/console n’est pas celui du runner, ou si le domaine gui/<uid> n’existe pas, le job ne dispose d’aucun domaine de connexion graphique utilisable. Réinitialiser les autorisations à répétition ne servira alors à rien : il faut d’abord corriger le contexte depuis lequel le job est lancé.
Le fait qu’une commande « fonctionne en SSH » prouve uniquement que le shell, les chemins et les autorisations de fichiers sont valides. Cela ne prouve pas que le processus dispose des autorisations nécessaires pour enregistrer l’écran, utiliser les fonctions d’accessibilité ou automatiser des applications.
Identifier l’origine du refus dans les journaux système
Immédiatement après avoir reproduit l’échec, consultez les journaux sur une période courte afin de ne pas noyer les informations utiles parmi des entrées sans rapport :
log show --last 5m \
--predicate 'subsystem == "com.apple.TCC"' \
--style compact
Examinez en priorité le service demandé, le chemin du client, le processus responsable et le résultat du refus. Ne recherchez pas uniquement le nom du script. Les scripts shell sont souvent lancés par un terminal, un runner, osascript ou un processus hôte de test. Le composant qui doit réellement recevoir l’autorisation peut donc être un processus parent qui héberge leur exécution.
Établir une référence pour les quatre éléments d’identité
Chaque Mac cloud doit conserver une référence des autorisations ne contenant aucune clé. Celle-ci doit au minimum inclure l’utilisateur d’exécution, le domaine de lancement, le chemin réel de l’exécutable et l’identité de signature du code. Après la mise à niveau du runner ou le remplacement d’un binaire, comparez de nouveau ces éléments avec la référence.
RUNNER="/opt/ci/bin/runner"
ls -l "$RUNNER"
realpath "$RUNNER"
codesign -dv --verbose=4 "$RUNNER" 2>&1
codesign -dr - "$RUNNER" 2>&1
spctl --assess --type execute --verbose=4 "$RUNNER"
Un chemin identique ne garantit pas une identité identique. Remplacer un fichier sur place, télécharger temporairement un outil non signé ou faire pointer un même lien symbolique vers plusieurs versions peut modifier le client que TCC identifie. Une méthode plus fiable consiste à placer chaque version dans un répertoire distinct, puis à mettre à jour le point d’entrée au moyen d’un basculement contrôlé. Les vérifications de signature doivent être relancées après chaque basculement.
Il faut également déterminer clairement quel est le « processus responsable ». Par exemple, si le runner lance un shell, qui lance ensuite osascript, l’objet à autoriser lors du contrôle final de l’application graphique n’est pas nécessairement le script stocké dans le dépôt. Pour diagnostiquer le problème, remontez la chaîne des PPID au lieu d’élargir simultanément les autorisations de plusieurs outils sans rapport.
Exécuter le job dans la bonne session graphique
Un job qui doit interagir avec le bureau ne doit pas être exécuté comme LaunchDaemon. Un LaunchDaemon appartient au domaine système : le démarrer sous un utilisateur précis ne le fait pas pour autant entrer dans la session Aqua de cet utilisateur. Il est préférable d’enregistrer le runner comme LaunchAgent de l’utilisateur concerné, puis de le charger après l’ouverture de la session graphique.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.ci-runner</string>
<key>ProgramArguments</key>
<array>
<string>/opt/ci/bin/runner</string>
<string>run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>LimitLoadToSessionType</key>
<string>Aqua</string>
<key>StandardOutPath</key>
<string>/Users/ci/Library/Logs/ci-runner.log</string>
<key>StandardErrorPath</key>
<string>/Users/ci/Library/Logs/ci-runner-error.log</string>
</dict>
</plist>
Validez d’abord le fichier avec plutil -lint, puis chargez-le sous l’utilisateur cible :
plutil -lint "$HOME/Library/LaunchAgents/com.example.ci-runner.plist"
launchctl bootstrap "gui/$(id -u)" \
"$HOME/Library/LaunchAgents/com.example.ci-runner.plist"
launchctl print "gui/$(id -u)/com.example.ci-runner"
Si bootstrap indique que le domaine est introuvable, vérifiez d’abord que l’utilisateur dispose déjà d’une session graphique active. Ne forcez pas le job à entrer dans la session d’un autre utilisateur et ne comptez pas sur un appel ponctuel à sudo pour masquer une erreur d’appartenance.
Ne réinitialiser que les autorisations nécessaires
N’envisagez une réinitialisation qu’après avoir vérifié l’utilisateur, la session et l’identité du processus. Commencez par arrêter le runner afin qu’il ne continue pas à envoyer des demandes pendant la réinitialisation et ne rende pas le diagnostic plus confus. L’utilisateur concerné doit ensuite réinitialiser uniquement les services visés :
launchctl bootout \
"gui/$(id -u)/com.example.ci-runner"
tccutil reset Accessibility
tccutil reset ScreenCapture
tccutil reset AppleEvents
Ne considérez pas ces trois commandes comme un ensemble systématique. Si le job nécessite uniquement l’enregistrement de l’écran, ne traitez que ScreenCapture. Ajoutez Accessibility lorsqu’il doit piloter l’interface, et AppleEvents seulement lorsqu’il doit envoyer des événements d’automatisation à une application précise. Après la réinitialisation, déclenchez un test minimal dans une session graphique valide, accordez les autorisations requises, puis redémarrez le runner.
Après une modification de l’autorisation d’enregistrement de l’écran, les anciens processus peuvent conserver l’état précédent. Il faut quitter complètement le processus responsable et le relancer, au lieu de simplement réexécuter le script du dépôt. Les autorisations d’automatisation peuvent aussi être enregistrées séparément pour chaque application cible. Pouvoir contrôler l’application A ne signifie donc pas que l’application B peut également être contrôlée.
Supprimer directement la base de données TCC du répertoire utilisateur ou modifier ses enregistrements SQLite ne constitue pas une méthode de réparation fiable. Cette opération efface les décisions concernant davantage d’applications et rend le processus de récupération difficile à auditer. Les Mac cloud d’OrbVPS sont des nœuds physiques dédiés, mais cette exclusivité ne change pas le modèle d’autorisations de macOS. Le principe du moindre privilège doit toujours s’appliquer au job réel et au processus responsable.
Intégrer la procédure de récupération aux critères de validation
Une fois le système rétabli, ne réactivez pas immédiatement l’ensemble du pipeline. Exécutez d’abord une sonde limitée à une seule capture d’écran ou à un seul événement d’automatisation, puis rétablissez progressivement les tâches de test. Il est recommandé d’inscrire les résultats suivants dans le rapport de validation du nœud :
- L’utilisateur du runner est identique à l’utilisateur de la console.
- Le domaine
gui/<uid>existe et l’état du LaunchAgent est running. - Le chemin réel du runner correspond à la version attendue.
- La sortie de
codesigncorrespond à la référence. - Les journaux TCC ne contiennent plus de refus pour le service visé.
- La sonde minimale réussit toujours après le redémarrage du runner.
- Aucun processus hôte de test ou processus d’automatisation ne subsiste après la fin du job complet.
Enfin, simulez une récupération après le redémarrage du nœud. Tant que la session graphique n’est pas établie, le runner doit rester indisponible pour de nouveaux jobs, plutôt que d’en accepter un qui restera bloqué lors d’une capture d’écran ou d’une interaction avec une fenêtre. Vous pouvez ajouter un contrôle de session au point d’entrée du job afin d’afficher un diagnostic explicite et de quitter en cas d’échec :
uid="$(id -u)"
if ! launchctl print "gui/$uid" >/dev/null 2>&1; then
printf '%s
' "No active GUI session for CI user"
exit 75
fi
La difficulté principale d’un incident TCC ne réside pas dans le bouton d’autorisation, mais dans l’identification du processus auquel cette autorisation doit appartenir. En fixant l’utilisateur d’exécution, le domaine de lancement du LaunchAgent, le chemin de l’exécutable et l’identité de signature, les problèmes d’autorisations cessent d’être des incidents aléatoires et deviennent un ensemble de conditions d’exécution vérifiables et réversibles.
Questions fréquentes
Pourquoi une commande réussit-elle en SSH mais est-elle refusée par TCC dans le service CI ?
Les deux exécutions peuvent appartenir à des sessions, domaines launchd ou processus responsables différents. TCC n’accorde pas automatiquement les mêmes droits au seul motif que le chemin du script est identique.
Faut-il supprimer directement la base de données TCC pour réparer les autorisations ?
Non. Arrêtez les tâches concernées, exécutez tccutil reset pour le service précis avec l’utilisateur affecté, puis accordez de nouveau l’autorisation dans une session graphique active.
Lancez vos builds sur des nœuds physiques Apple Silicon dédiés
Configurez votre nœud selon le modèle, la région et la durée de location. La disponibilité réelle est indiquée en temps réel dans le tableau de bord.