$decision->niveau_autorise = 'ecriture_complete'; — cette ligne ne devrait jamais pouvoir s’exécuter une fois qu’une décision d’autorisation a été calculée pour un agent, et pourtant rien n’empêche techniquement une fonction intermédiaire mal écrite de le faire si cette décision circule sous forme de tableau PHP classique entre plusieurs appels d’ability enchaînés.
Un agent IA qui exécute une tâche complexe appelle rarement une seule ability isolée : il enchaîne souvent plusieurs appels successifs qui partagent un même contexte d’autorisation, calculé une fois au début de la chaîne. Si ce contexte est transporté sous une forme modifiable, une erreur de programmation dans une des étapes intermédiaires peut, par inadvertance ou par exploitation délibérée, élargir la portée de ce qui avait été initialement autorisé.
Le problème du tableau associatif partagé
Une implémentation naïve calcule une décision d’autorisation sous forme de tableau, puis la transmet de fonction en fonction au fil de la chaîne d’appels d’ability :
$decision = [
'niveau_autorise' => 'lecture_seule',
'utilisateur_id' => 42,
'expire_a' => time() + 300,
];
executer_premiere_ability($decision);
executer_seconde_ability($decision); // rien n'empêche une modification entre-temps
Rien, dans ce code, n’empêche une fonction intermédiaire de modifier $decision['niveau_autorise'] avant de le transmettre à l’étape suivante. Ce risque reste théorique tant que le code est écrit par une seule équipe attentive, mais il devient bien réel dès qu’une extension tierce ou un module communautaire s’insère dans cette chaîne d’appels.
Remplacer le tableau par une classe readonly
Depuis PHP 8.1, une classe peut déclarer ses propriétés en lecture seule : une fois initialisées dans le constructeur, ces propriétés ne peuvent plus être réassignées, sous peine d’une erreur fatale immédiate.

final class DecisionAutorisationAgent
{
public function __construct(
public readonly string $niveau_autorise,
public readonly int $utilisateur_id,
public readonly int $expire_a,
) {
}
public function estExpiree(): bool
{
return time() > $this->expire_a;
}
}
Une tentative de modification d’une propriété readonly après construction déclenche une Error immédiate et explicite, ce qui transforme une altération silencieuse en une panne visible et facile à diagnostiquer, plutôt qu’en un élargissement de portée passé inaperçu.
$decision = new DecisionAutorisationAgent('lecture_seule', 42, time() + 300);
// Cette ligne lève une Error : Cannot modify readonly property
$decision->niveau_autorise = 'ecriture_complete';
Ce que ce mécanisme ne résout pas
Une classe readonly garantit l’intégrité de la décision une fois qu’elle a été calculée ; elle ne garantit en rien que cette décision initiale était elle-même correcte. Le calcul des règles d’autorisation — quel niveau accorder à quel utilisateur, pour quelle durée — reste une logique distincte, à valider séparément, en amont de la construction de cet objet.
Composer avec une nouvelle instance plutôt que muter
Si une étape de la chaîne doit restreindre davantage la décision (par exemple, passer d’un niveau large à un niveau plus restreint pour une sous-tâche spécifique), la bonne pratique consiste à créer une nouvelle instance dérivée, jamais à contourner le caractère readonly par une astuce de réflexion PHP.
function restreindre(DecisionAutorisationAgent $decision): DecisionAutorisationAgent
{
return new DecisionAutorisationAgent(
'lecture_seule', // toujours plus restrictif, jamais plus large
$decision->utilisateur_id,
$decision->expire_a,
);
}
- Une nouvelle instance ne peut que restreindre, jamais élargir, le niveau initial calculé
- Chaque étape de la chaîne reçoit une décision qu’elle ne peut techniquement pas altérer
- Une expiration courte, vérifiée à chaque appel via
estExpiree(), limite la fenêtre d’exploitation en cas de fuite du contexte
Une décision d’autorisation qui peut être modifiée après son calcul n’est pas vraiment une décision : c’est une suggestion que le reste du code est libre d’ignorer.
En résumé
Transporter une décision d’autorisation sous forme d’objet readonly plutôt que de tableau modifiable ferme une classe de bugs et d’exploitations qui reste invisible tant qu’aucune étape intermédiaire ne cherche à en abuser. Ce mécanisme, disponible nativement depuis PHP 8.1, ne dispense évidemment pas de valider correctement les règles d’autorisation en amont : il garantit seulement que la décision, une fois prise, ne peut plus être détournée en cours de chaîne.