[{"data":1,"prerenderedAt":58},["ShallowReactive",2],{"doc:fr:plugins\u002Fapi":3},{"title":4,"lede":5,"html":6,"headings":7},"Le protocole","Un plugin qui a un programme parle à Escouade en JSON-RPC 2.0 : dans l’app, en WASM, s’il est en JS, en TypeScript ou en Rust ; sur son entrée et sa sortie standard s’il est un process. Cette page décrit les messages et le process.","\u003Cp>Un plugin \u003Ccode class=\"key\">run.js\u003C\u002Fcode> ou \u003Ccode class=\"key\">run.wasm\u003C\u002Fcode> parle le même protocole : \u003Ca href=\"#un-programme-en-wasm\">ce qui change en WASM\u003C\u002Fa> vient plus bas, et \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F\">les appels\u003C\u002Fa> donnent un exemple en TS et en Rust pour chaque méthode.\u003C\u002Fp>\n\u003Ch2 id=\"quand-le-programme-demarre\">Quand le programme démarre\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>À sa première activation, jamais avant.\u003C\u002Fstrong> Escouade lance le programme de \u003Ccode class=\"key\">run\u003C\u002Fcode> quand un fait de sa liste \u003Ccode class=\"key\">activation\u003C\u002Fcode> se produit (\u003Ccode class=\"key\">event:ticket.column\u003C\u002Fcode>), ou au premier appel qu’il lui fait. Un plugin sans \u003Ccode class=\"key\">activation\u003C\u002Fcode> ne tourne pas ; un plugin sans \u003Ccode class=\"key\">run\u003C\u002Fcode> est déclaratif : rien ne s’exécute.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Un process par plugin.\u003C\u002Fstrong> Il sert tous les projets et tous les agents où le plugin est actif : Escouade distingue ses appels par leur \u003Ccode class=\"key\">id\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Dans le dossier du plugin\u003C\u002Fstrong>, avec trois variables : \u003Ccode class=\"key\">ESCOUADE_PLUGIN_ID\u003C\u002Fcode> (son id), \u003Ccode class=\"key\">ESCOUADE_PLUGIN_API\u003C\u002Fcode> (la version du protocole, \u003Ccode class=\"key\">1\u003C\u002Fcode>) et \u003Ccode class=\"key\">ESCOUADE_VERSION\u003C\u002Fcode> (celle d’Escouade).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Avec tout ce qu’il lance.\u003C\u002Fstrong> Sous Windows, le process est dans un Job object ; sous macOS et Linux, il mène son groupe de processus. Quand Escouade l’arrête, ce qu’il a lancé s’arrête avec lui ; sous Windows, même si Escouade plante.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"les-messages\">Les messages\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>Du JSON-RPC 2.0, \u003Cstrong>un message par ligne\u003C\u002Fstrong> : du JSON en UTF-8, sans retour à la ligne, terminé par \u003Ccode class=\"key\">\\n\u003C\u002Fcode>. La sortie standard ne sert qu’au protocole ; écris tes traces sur la sortie d’erreur, qui va au \u003Ca href=\"#le-journal\">journal\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Des lots dans les deux sens\u003C\u002Fstrong> : un tableau JSON de messages, sur une ligne. Réponds à un lot par un lot ; les réponses peuvent venir dans n’importe quel ordre, chacune avec l’\u003Ccode class=\"key\">id\u003C\u002Fcode> de sa requête.\u003C\u002Fli>\n\u003Cli>Une notification n’a pas d’\u003Ccode class=\"key\">id\u003C\u002Fcode> et n’attend pas de réponse.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode class=\"language-json\">{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:7,&quot;method&quot;:&quot;echo&quot;,&quot;params&quot;:{&quot;a&quot;:1}}\n[{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:8,&quot;method&quot;:&quot;echo&quot;,&quot;params&quot;:1},{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;log&quot;,&quot;params&quot;:{&quot;level&quot;:&quot;info&quot;,&quot;message&quot;:&quot;prêt&quot;}}]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"la-poignee-de-main\">La poignée de main\u003C\u002Fh2>\n\u003Cp>Le premier message d’Escouade est \u003Ccode class=\"key\">initialize\u003C\u002Fcode>. Il donne la version du protocole qu’il parle et ce qu’il sait faire :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;jsonrpc&quot;: &quot;2.0&quot;,\n  &quot;id&quot;: 1,\n  &quot;method&quot;: &quot;initialize&quot;,\n  &quot;params&quot;: {\n    &quot;protocolVersion&quot;: 1,\n    &quot;escouade&quot;: { &quot;version&quot;: &quot;2.1.1&quot; },\n    &quot;plugin&quot;: { &quot;id&quot;: &quot;dev.escouade.slack&quot;, &quot;version&quot;: &quot;1.1.0&quot; },\n    &quot;lang&quot;: &quot;fr&quot;,\n    &quot;capabilities&quot;: { &quot;batch&quot;: true, &quot;secrets&quot;: true, &quot;settings&quot;: true, &quot;log&quot;: true, &quot;attach&quot;: true }\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Le plugin répond la version qu’il parle et ses propres capacités :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;result&quot;:{&quot;protocolVersion&quot;:1,&quot;capabilities&quot;:{&quot;batch&quot;:true}}}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Ccode class=\"key\">lang\u003C\u002Fcode> est la langue de l’interface (\u003Ccode class=\"key\">fr\u003C\u002Fcode>, \u003Ccode class=\"key\">en\u003C\u002Fcode>), pour ce que le plugin écrit à l’utilisateur.\u003C\u002Fli>\n\u003Cli>Une autre version, ou une réponse sans version, arrête le plugin, avec la raison : « it speaks version 2 of the protocol; this Escouade speaks version 1 ».\u003C\u002Fli>\n\u003Cli>Sans réponse en 10 secondes, le plugin est arrêté aussi.\u003C\u002Fli>\n\u003Cli>Rien d’autre ne lui est envoyé avant sa réponse : ce qui arrive entre-temps attend dans une file.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"les-delais\">Les délais\u003C\u002Fh2>\n\u003Cp>Chaque appel d’Escouade a un délai. Passé ce délai, Escouade n’attend plus : \u003Cstrong>sa réponse par défaut s’applique\u003C\u002Fstrong> (une demande d’autorisation va à l’utilisateur, comme sans plugin), et une réponse tardive est ignorée.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Méthode\u003C\u002Fth>\n\u003Cth>Délai\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">initialize\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>10 s\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">shutdown\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>1 s\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">permissions\u002F…\u003C\u002Fcode> (une décision qui fait attendre un agent)\u003C\u002Ftd>\n\u003Ctd>500 ms\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">tickets\u002F…\u003C\u002Fcode>, \u003Ccode class=\"key\">forge\u002F…\u003C\u002Fcode>, \u003Ccode class=\"key\">workflow\u002F…\u003C\u002Fcode>, \u003Ccode class=\"key\">engine\u002FoneShot\u003C\u002Fcode> (un import, un appel à un service)\u003C\u002Ftd>\n\u003Ctd>60 s\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Les autres\u003C\u002Ftd>\n\u003Ctd>5 s\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Chaque délai dépassé est noté au journal : « test\u002Fdo: no answer within 300 ms ».\u003C\u002Fp>\n\u003Ch2 id=\"ce-qu-escouade-sert-au-plugin\">Ce qu’Escouade sert au plugin\u003C\u002Fh2>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Méthode\u003C\u002Fth>\n\u003Cth>Paramètres\u003C\u002Fth>\n\u003Cth>Réponse\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ &quot;key&quot;: &quot;webhook&quot; }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Le secret, ou \u003Ccode class=\"key\">null\u003C\u002Fcode> si aucun n’est gardé sous cette clé.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">settings\u002Fget\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">{}\u003C\u002Fcode>, ou \u003Ccode>{ &quot;project&quot;: &quot;&lt;id&gt;&quot; }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Ses réglages, tels que l’utilisateur les a choisis dans l’onglet Réglages de sa page : un objet par clé de \u003Ccode class=\"key\">contributes.settings\u003C\u002Fcode>, sans celles laissées à leur valeur par défaut. Avec un projet (le \u003Ccode class=\"key\">project.id\u003C\u002Fcode> d’un fait), ceux que son \u003Ca href=\"\u002Fdocs\u002Fescouade-yml\u002F\">\u003Ccode class=\"key\">.escouade.yml\u003C\u002Fcode>\u003C\u002Fa> lui donne sous \u003Ccode class=\"key\">plugins.&lt;id&gt;\u003C\u002Fcode> passent devant, s’il est activé dans ce projet : seulement les clés qu’il déclare, d’une valeur de leur type, jamais un secret.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">log\u003C\u002Fcode> (notification)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ &quot;level&quot;: &quot;info&quot;, &quot;message&quot;: &quot;…&quot; }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Aucune : la ligne va au journal du plugin.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">files\u002Fread\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ &quot;path&quot;: &quot;data\u002Fwords.txt&quot; }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">&quot;…&quot;\u003C\u002Fcode>, le texte d’un fichier de son dossier, en texte UTF-8, 256 Ko au plus, aux deux transports. Refusé (\u003Ccode class=\"key\">-32003\u003C\u002Fcode>, gardé au journal) : un chemin absolu, avec \u003Ccode class=\"key\">..\u003C\u002Fcode> ou un lecteur, un lien qui mène hors du dossier, un nom caché (\u003Ccode class=\"key\">.env\u003C\u002Fcode>, \u003Ccode class=\"key\">.git\u003C\u002Fcode>), le manifeste. \u003Ccode class=\"key\">-32005\u003C\u002Fcode> pour un fichier absent, \u003Ccode class=\"key\">-32004\u003C\u002Fcode> pour un fichier trop gros ou qui n’est pas du texte.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">net\u002Ffetch\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ &quot;url&quot;: &quot;https:\u002F\u002F…&quot;, &quot;method&quot;: &quot;POST&quot;, &quot;headers&quot;: {}, &quot;body&quot;: &quot;…&quot; }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ &quot;status&quot;: 200, &quot;headers&quot;: {}, &quot;body&quot;: &quot;…&quot; }\u003C\u002Fcode>, pour un plugin en WASM seulement : voir \u003Ca href=\"#le-reseau-par-escouade\">le réseau\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">net\u002Fstream\u003C\u002Fcode>, \u003Ccode class=\"key\">ws\u002F*\u003C\u002Fcode>, \u003Ccode class=\"key\">timer\u002F*\u003C\u002Fcode>, \u003Ccode class=\"key\">fs\u002F*\u003C\u002Fcode>, \u003Ccode class=\"key\">proc\u002F*\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F\">les appels\u003C\u002Fa>.\u003C\u002Ftd>\n\u003Ctd>Pour un plugin en WASM seulement ; un process reçoit \u003Ccode class=\"key\">-32601\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Une autre méthode répond l’erreur \u003Ccode class=\"key\">-32601\u003C\u002Fcode> et le note au journal. Les erreurs propres à Escouade : \u003Ccode class=\"key\">-32001\u003C\u002Fcode> pour un secret qui n’est pas celui du plugin, \u003Ccode class=\"key\">-32002\u003C\u002Fcode> pour un trousseau injoignable, \u003Ccode class=\"key\">-32003\u003C\u002Fcode> pour ce que le plugin ne peut pas atteindre (un hôte, un fichier), \u003Ccode class=\"key\">-32004\u003C\u002Fcode> pour une requête qui échoue en route, \u003Ccode class=\"key\">-32005\u003C\u002Fcode> pour un fichier absent, \u003Ccode class=\"key\">-32602\u003C\u002Fcode> pour des paramètres qui ne tiennent pas.\u003C\u002Fp>\n\u003Ch2 id=\"les-secrets\">Les secrets\u003C\u002Fh2>\n\u003Cp>Les secrets d’un plugin sont dans le trousseau du système, sous un nom à lui : \u003Ccode class=\"key\">plugin:&lt;id&gt;:&lt;clé&gt;\u003C\u002Fcode>. Il les demande par \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode>, au moment où il en a besoin.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Seulement les siens.\u003C\u002Fstrong> La clé doit être l’une de ses \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode> ; un motif finissant par \u003Ccode class=\"key\">*\u003C\u002Fcode> en prend plusieurs (\u003Ccode class=\"key\">account:*\u003C\u002Fcode>). Tout autre clé est refusée par \u003Ccode class=\"key\">-32001\u003C\u002Fcode> et notée au journal, même celle d’un autre plugin écrite en entier : \u003Ccode>plugin:dev.autre:token\u003C\u002Fcode> n’est pas une clé de ses permissions.\u003C\u002Fli>\n\u003Cli>Un plugin n’obtient jamais le secret d’un compte, d’un harness ou d’une intégration d’Escouade.\u003C\u002Fli>\n\u003Cli>Un secret donné au plugin est masqué (\u003Ccode class=\"key\">••••\u003C\u002Fcode>) partout dans son journal, sa sortie d’erreur comprise.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"les-plantages-et-les-relances\">Les plantages et les relances\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Un plantage relance le plugin\u003C\u002Fstrong>, trois fois en cinq minutes. Au quatrième, il est arrêté : « stopped after 4 crashes within 5 minutes ». Il ne redémarre plus seul ; le bouton « Relancer » de son journal, dans la vue Plugins, le relance, ses plantages oubliés.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Les appels en cours\u003C\u002Fstrong> quand il plante échouent : leur réponse par défaut s’applique.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Pendant la relance\u003C\u002Fstrong>, ce qu’Escouade lui envoie attend dans une file de 256 messages, puis part dans l’ordre une fois \u003Ccode class=\"key\">initialize\u003C\u002Fcode> répondu. Une file pleine perd son plus vieux message, et le journal le dit.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ce que chaque agent lui avait attaché\u003C\u002Fstrong> (pour un harness, \u003Ccode class=\"key\">agent\u002Fattach\u003C\u002Fcode> : sa session, son mode, son modèle) est redonné au nouveau process avant tout autre message.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"la-memoire\">La mémoire\u003C\u002Fh2>\n\u003Cp>Escouade lit la mémoire du plugin et de tout ce qu’il a lancé toutes les deux secondes, comme celle des agents. Au-delà de 1 Go, le plugin est arrêté, sans relance : « stopped: 1100 MB of memory, past the limit of 1024 MB ».\u003C\u002Fp>\n\u003Ch2 id=\"la-fermeture-d-escouade\">La fermeture d’Escouade\u003C\u002Fh2>\n\u003Cp>Quand Escouade se ferme, il envoie \u003Ccode class=\"key\">shutdown\u003C\u002Fcode> à chaque plugin qui tourne, attend sa réponse une seconde au plus pour tous, puis arrête son process et tout ce qu’il a lancé. Réponds à \u003Ccode class=\"key\">shutdown\u003C\u002Fcode>, puis termine-toi : ce que tu n’as pas écrit avant est perdu.\u003C\u002Fp>\n\u003Ch2 id=\"le-journal\">Le journal\u003C\u002Fh2>\n\u003Cp>Chaque plugin a son journal, dans \u003Ccode>~\u002F.escouade\u002Fplugin-logs\u002F&lt;id&gt;.log\u003C\u002Fcode> ; au-delà de 1 Mo, il devient \u003Ccode class=\"key\">&lt;id&gt;.log.1\u003C\u002Fcode> et repart de zéro. Ses lignes sont en anglais, comme le journal de l’app, et notent :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>ses démarrages, sa réponse à \u003Ccode class=\"key\">initialize\u003C\u002Fcode>, ses arrêts et leur raison ;\u003C\u002Fli>\n\u003Cli>ses plantages, avec leur code de sortie et ses dernières lignes de sortie d’erreur ;\u003C\u002Fli>\n\u003Cli>chaque ligne de sa sortie d’erreur ;\u003C\u002Fli>\n\u003Cli>ses messages : ses \u003Ccode class=\"key\">log\u003C\u002Fcode>, les secrets qu’il a demandés (jamais leur valeur) ;\u003C\u002Fli>\n\u003Cli>les appels restés sans réponse, ce qu’il a demandé et qu’on lui a refusé, les messages perdus d’une file pleine.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Les 500 dernières lignes se lisent dans l’onglet Journal de sa page, dans la vue Plugins (\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Finstaller\u002F#le-journal\">installer un plugin\u003C\u002Fa>).\u003C\u002Fp>\n\u003Cp>Ses méthodes, leurs paramètres, leurs réponses et leurs délais sont décrits par un schéma, \u003Ca href=\"\u002Fschemas\u002Fplugin-protocol-1.json\">\u003Ccode>plugin-protocol-1.json\u003C\u002Fcode>\u003C\u002Fa>, d’où viennent les types du \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fsdk\u002F\">SDK en TypeScript\u003C\u002Fa>. Pour vérifier qu’un plugin tient ce protocole sans lancer l’app, joue \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">le banc de conformité\u003C\u002Fa> : \u003Ccode>escouade-plugin test &lt;dossier&gt;\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"un-programme-en-wasm\">Un programme en WASM\u003C\u002Fh2>\n\u003Cp>Un plugin dont le manifeste dit \u003Ccode>&quot;run&quot;: { &quot;js&quot;: &quot;dist\u002Fplugin.mjs&quot; }\u003C\u002Fcode> ou \u003Ccode>&quot;run&quot;: { &quot;wasm&quot;: &quot;Cargo.toml&quot; }\u003C\u002Fcode> ne lance pas de process : Escouade fait tourner son module \u003Cstrong>dans l’app, en WASM\u003C\u002Fstrong>, sans Node sur la machine. Une instance par plugin, sur un fil à elle, qui garde son état d’un appel à l’autre comme un process. Le protocole est le même : les mêmes messages, portés par l’entrée et la sortie standard du module, la même poignée de main, les mêmes délais, le même journal et les mêmes relances ; \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fsdk\u002F\">le SDK\u003C\u002Fa> et \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Frust\u002F\">le kit Rust\u003C\u002Fa> le parlent pour toi.\u003C\u002Fp>\n\u003Cp>Le module est compilé une fois : par le store pour un plugin du store (l’app le télécharge et vérifie son empreinte contre \u003Ccode class=\"key\">version.json\u003C\u002Fcode>), par l’app pour un \u003Ccode class=\"key\">run.js\u003C\u002Fcode> installé depuis un dossier ou un dépôt git. Un \u003Ccode class=\"key\">run.wasm\u003C\u002Fcode> hors du store vient avec son \u003Ccode class=\"key\">plugin.wasm\u003C\u002Fcode>, à côté du manifeste. L’app le précompile à l’installation, le garde dans son cache et le précompile à nouveau après une mise à jour d’Escouade. Il lui faut Escouade 2.2.0 ou plus récent.\u003C\u002Fp>\n\u003Ch3 id=\"le-reseau-par-escouade\">Le réseau, par Escouade\u003C\u002Fh3>\n\u003Cp>Un plugin en WASM n’ouvre pas de connexion : tout passe par Escouade, par \u003Ccode class=\"key\">net\u002Ffetch\u003C\u002Fcode>, \u003Ccode class=\"key\">net\u002Fstream\u003C\u002Fcode> et \u003Ccode class=\"key\">ws\u002Fopen\u003C\u002Fcode>, que seul un plugin en WASM reçoit (un process a son propre réseau, et ces méthodes lui répondent \u003Ccode class=\"key\">-32601\u003C\u002Fcode>). \u003Ccode class=\"key\">fetch\u003C\u002Fcode> en JS et le client du kit en Rust y mènent. Escouade envoie la requête :\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>en https seulement\u003C\u002Fstrong>, vers un hôte de \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode> ou l’un de ses sous-domaines (\u003Ccode class=\"key\">slack.com\u003C\u002Fcode> couvre \u003Ccode class=\"key\">api.slack.com\u003C\u002Fcode>) ; jamais vers une adresse IP écrite dans l’URL, que \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode> ne nomme pas ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>sans suivre de redirection\u003C\u002Fstrong> : le plugin reçoit la réponse 3xx ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>en 10 s, pour 1 Mio de réponse au plus\u003C\u002Fstrong>, sauf si \u003Ccode class=\"key\">limits\u003C\u002Fcode> en demande plus ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>jamais vers une adresse de la machine ou du réseau local\u003C\u002Fstrong>, même pour un hôte déclaré : bouclage, privée, de lien local, non spécifiée, de multidiffusion ou de diffusion, en IPv4 comme en IPv6. Escouade juge les adresses que la connexion emploie elle-même, après la résolution du nom, et non une résolution à part que le DNS pourrait contredire ;\u003C\u002Fli>\n\u003Cli>\u003Cstrong>par le proxy de Réglages › Réseau\u003C\u002Fstrong>, s’il y en a un : seul le nom est alors jugé, et le journal le dit.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Ccode class=\"key\">{account}\u003C\u002Fcode>, l’hôte d’un compte que l’utilisateur a saisi, et \u003Ccode class=\"key\">local:shared\u003C\u002Fcode>, le serveur local d’un harness, ouvrent http et le réseau local pour ces hôtes seuls : voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcapabilities\u002F#le-reseau\">les capacités de l’hôte\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>Un appel refusé répond \u003Ccode class=\"key\">-32003\u003C\u002Fcode> et le journal le garde (« slack.com resolves to 127.0.0.1, an address of this machine or of the local network », « example.org is not in permissions.network of … ») ; un appel qui échoue en route (pas de réponse à temps, une réponse trop grande, un hôte injoignable) répond \u003Ccode class=\"key\">-32004\u003C\u002Fcode>. Le plugin voit un \u003Ccode class=\"key\">fetch\u003C\u002Fcode> qui rejette.\u003C\u002Fp>\n\u003Ch3 id=\"les-fichiers-de-son-dossier\">Les fichiers de son dossier\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Les fichiers JSON de sa racine\u003C\u002Fstrong> (\u003Ccode class=\"key\">rules.json\u003C\u002Fcode>, \u003Ccode class=\"key\">channels.json\u003C\u002Fcode>) lui sont donnés au démarrage : 256 Ko chacun, 1 Mo en tout, jamais son manifeste. \u003Ccode class=\"key\">readFileSync\u003C\u002Fcode> de \u003Ccode class=\"key\">node:fs\u003C\u002Fcode> les lit, comme sous Node (\u003Ccode>readFileSync(new URL('..\u002Frules.json', globalThis._importMeta_.url), 'utf8')\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Les autres\u003C\u002Fstrong> se lisent par \u003Ccode class=\"key\">files\u002Fread\u003C\u002Fcode> (\u003Ccode class=\"key\">p.file()\u003C\u002Fcode> du SDK), en texte UTF-8, 256 Ko au plus, un sous-dossier compris (\u003Ccode class=\"key\">data\u002Fwords.txt\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Le reste\u003C\u002Fstrong> passe par \u003Ccode class=\"key\">fs\u002F*\u003C\u002Fcode>, sous les racines de \u003Ccode class=\"key\">permissions.files\u003C\u002Fcode> : voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcapabilities\u002F#les-fichiers-files\">les fichiers\u003C\u002Fa>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"les-limites\">Les limites\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>\u003Cstrong>64 Mo de mémoire, 2 s par tour de travail\u003C\u002Fstrong>, 500 ms quand il tient une décision.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Une boucle sans fin, une mémoire qui grossit ou une exception non rattrapée\u003C\u002Fstrong> finissent l’instance, et le journal dit pourquoi (« a turn ran past 2000 ms: Escouade stopped its instance », « its instance crashed: … ») ; une nouvelle est faite, comme après le plantage d’un process, avec les mêmes trois relances en cinq minutes, puis le plugin est arrêté.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Des minuteurs\u003C\u002Fstrong> : \u003Ccode class=\"key\">setTimeout\u003C\u002Fcode> et \u003Ccode class=\"key\">clearTimeout\u003C\u002Fcode> existent en JS, \u003Ccode class=\"key\">sleep\u003C\u002Fcode> et \u003Ccode class=\"key\">timeout\u003C\u002Fcode> dans le kit, 10 ms au moins, 64 à la fois.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Un bundle fait avec le SDK 0.2\u003C\u002Fstrong> ne tourne pas en WASM : le plugin s’arrête et son journal dit de le reconstruire avec \u003Ccode class=\"key\">@escouade-dev\u002Fplugin\u003C\u002Fcode> 0.3 ou plus récent.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"ce-qu-un-plugin-en-wasm-ne-peut-pas-faire\">Ce qu’un plugin en WASM ne peut pas faire\u003C\u002Fh3>\n\u003Cp>Rien de ce qu’il ne déclare pas : ni réseau, ni fichier hors de son dossier, ni programme. Il ne lit pas l’environnement et ne charge aucun module natif ou distant. Un bundle qui importe autre chose que \u003Ccode class=\"key\">node:fs\u003C\u002Fcode> et \u003Ccode class=\"key\">node:readline\u003C\u002Fcode>, ou qui a des \u003Ccode class=\"key\">export\u003C\u002Fcode>, n’est pas compilé, et le plugin s’arrête avec la raison. Ses secrets et ses réglages sont les siens seulement, comme pour un process. C’est pourquoi la fenêtre d’installation dit « Tourne dans Escouade, isolé », puis chaque \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcapabilities\u002F\">capacité\u003C\u002Fa> qu’il demande, et non « Lance un programme sur ton ordinateur, avec tes droits ».\u003C\u002Fp>\n\u003Ch2 id=\"ce-que-l-utilisateur-change\">Ce que l’utilisateur change\u003C\u002Fh2>\n\u003Cp>Un plugin désactivé, une permission retirée ou rendue, une mise à jour installée : son process est arrêté, et le suivant reçoit le manifeste et les permissions de maintenant. Une permission de secret retirée fait répondre \u003Ccode class=\"key\">-32001\u003C\u002Fcode> à \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode> pour cette clé, comme pour un secret qui n’est pas le sien.\u003C\u002Fp>\n",[8,12,15,18,21,24,27,30,33,36,39,42,46,49,52,55],{"id":9,"text":10,"depth":11},"quand-le-programme-demarre","Quand le programme démarre",2,{"id":13,"text":14,"depth":11},"les-messages","Les messages",{"id":16,"text":17,"depth":11},"la-poignee-de-main","La poignée de main",{"id":19,"text":20,"depth":11},"les-delais","Les délais",{"id":22,"text":23,"depth":11},"ce-qu-escouade-sert-au-plugin","Ce qu’Escouade sert au plugin",{"id":25,"text":26,"depth":11},"les-secrets","Les secrets",{"id":28,"text":29,"depth":11},"les-plantages-et-les-relances","Les plantages et les relances",{"id":31,"text":32,"depth":11},"la-memoire","La mémoire",{"id":34,"text":35,"depth":11},"la-fermeture-d-escouade","La fermeture d’Escouade",{"id":37,"text":38,"depth":11},"le-journal","Le journal",{"id":40,"text":41,"depth":11},"un-programme-en-wasm","Un programme en WASM",{"id":43,"text":44,"depth":45},"le-reseau-par-escouade","Le réseau, par Escouade",3,{"id":47,"text":48,"depth":45},"les-fichiers-de-son-dossier","Les fichiers de son dossier",{"id":50,"text":51,"depth":45},"les-limites","Les limites",{"id":53,"text":54,"depth":45},"ce-qu-un-plugin-en-wasm-ne-peut-pas-faire","Ce qu’un plugin en WASM ne peut pas faire",{"id":56,"text":57,"depth":11},"ce-que-l-utilisateur-change","Ce que l’utilisateur change",1793889379491]