Building plugins
Hooks de Plugin
Les hooks de Plugin sont des points d’extension intégrés au processus pour les plugins OpenClaw : ils permettent d’inspecter ou de modifier les exécutions d’agents, les appels d’outils, le flux de messages, le cycle de vie des sessions, le routage des sous-agents, les installations ou le démarrage du Gateway.
Utilisez plutôt les hooks internes pour un petit script
HOOK.md installé par un opérateur et réagissant aux événements de commande et du Gateway tels que /new,
/reset, /stop, agent:bootstrap ou gateway:startup.
Démarrage rapide
Enregistrez des hooks typés avec api.on(...) depuis le point d’entrée du plugin :
export default definePluginEntry({ id: "tool-preflight", name: "Tool Preflight", register(api) { api.on( "before_tool_call", async (event) => { if (event.toolName !== "web_search") { return; } return { requireApproval: { title: "Exécuter la recherche web", description: `Autoriser la requête de recherche : ${String(event.params.query ?? "")}`, severity: "info", timeoutMs: 60_000, }, }; }, { priority: 50 }, ); },});Les gestionnaires pouvant renvoyer des décisions ou des modifications s’exécutent séquentiellement par
priority décroissante ; les gestionnaires de même priorité conservent leur ordre d’enregistrement.
Les gestionnaires d’observation uniquement s’exécutent en parallèle, et les envois d’observation
sans attente peuvent se chevaucher avec des événements ultérieurs. N’utilisez pas la priorité pour ordonner
les effets secondaires d’observation.
api.on(name, handler, opts?) accepte :
| Option | Effet |
|---|---|
priority |
Ordre d’exécution ; la valeur la plus élevée s’exécute en premier. |
timeoutMs |
Délai d’attente maximal par hook. À son expiration, OpenClaw cesse d’attendre ce gestionnaire et poursuit l’exécution. Cela n’annule ni le gestionnaire ni ses effets secondaires. Omettez cette option pour utiliser le délai d’attente par hook par défaut de l’exécuteur. |
Les opérateurs peuvent définir les délais des hooks sans modifier le code du plugin :
{ "plugins": { "entries": { "my-plugin": { "hooks": { "timeoutMs": 30000, "timeouts": { "before_prompt_build": 90000, "agent_end": 60000 } } } } }}hooks.timeouts.<hookName> remplace hooks.timeoutMs, qui remplace la valeur
api.on(..., { timeoutMs }) définie par le plugin. Chaque valeur doit être un
entier positif inférieur ou égal à 600000 ms. Privilégiez les remplacements par hook pour les
hooks connus comme étant lents, afin qu’un plugin ne bénéficie pas partout d’un délai plus long.
La promesse d’un gestionnaire ayant dépassé son délai continue de s’exécuter, car les rappels de hook ne reçoivent pas de signal d’annulation. L’envoi du hook peut libérer son admission au Gateway alors que le travail de ce plugin est encore en cours. Les plugins qui gèrent des tâches de longue durée doivent fournir leur propre cycle de vie d’annulation et d’arrêt.
Les hooks de modification sortants message_sending et reply_payload_sending utilisent un
délai par défaut de 15 secondes par gestionnaire. Si l’un d’eux dépasse ce délai, OpenClaw journalise l’erreur du plugin
et poursuit avec la charge utile la plus récente afin que la file de livraison sérialisée puisse
se terminer. Définissez un délai par hook plus long pour les plugins qui effectuent intentionnellement un travail plus lent
avant la livraison.
Les plugins de canal qui utilisent createReplyDispatcher peuvent également déclarer un délai
positif plus élevé par étape avec beforeDeliverOptions: { timeoutMs }, ou lors de
l’ajout d’une tâche avec dispatcher.appendBeforeDeliver(handler, { timeoutMs }).
Sans délai déclaré par le propriétaire, ces rappels utilisent le même délai par défaut de 15 secondes,
afin qu’un rappel bloqué ne puisse pas retenir la file de livraison sérialisée.
Chaque hook reçoit event.context.pluginConfig, la configuration résolue du
plugin qui a enregistré ce gestionnaire. OpenClaw l’injecte pour chaque gestionnaire sans
modifier l’objet d’événement partagé visible par les autres plugins.
Catalogue des hooks
Les hooks sont regroupés selon la surface qu’ils étendent. Les noms en gras acceptent un résultat de décision (blocage, annulation, remplacement ou demande d’approbation) ; les autres servent uniquement à l’observation.
Tour de l’agent
| Hook | Objectif |
|---|---|
before_model_resolve |
Remplacer le fournisseur ou le modèle avant le chargement des messages de session |
agent_turn_prepare |
Consommer les injections de tour mises en file d’attente par les plugins et ajouter du contexte au tour en cours avant les hooks de prompt |
before_prompt_build |
Ajouter du contexte dynamique ou du texte au prompt système avant l’appel du modèle |
before_agent_start |
Phase combinée réservée à la compatibilité ; privilégiez les deux hooks précédents |
before_agent_run |
Inspecter le prompt final et les messages de session avant leur soumission au modèle ; peut bloquer l’exécution |
before_agent_reply |
Court-circuiter le tour du modèle avec une réponse synthétique ou un silence |
before_agent_finalize |
Inspecter la réponse finale naturelle et demander un passage supplémentaire du modèle |
agent_end |
Observer les messages finaux, l’état de réussite et la durée d’exécution |
heartbeat_prompt_contribution |
Ajouter du contexte réservé au Heartbeat pour les plugins de surveillance en arrière-plan et de cycle de vie |
Observation de la conversation
| Hook | Objectif |
|---|---|
model_call_started / model_call_ended |
Métadonnées épurées des appels au fournisseur/modèle : temporisation, résultat et hachages bornés des identifiants de requête. Aucun contenu de prompt ou de réponse. |
llm_input |
Entrée du fournisseur : prompt système, prompt, historique |
llm_output |
Sortie du fournisseur, utilisation et contextTokenBudget résolu lorsqu’il est disponible |
Outils
| Hook | Objectif |
|---|---|
before_tool_call |
Réécrire les paramètres de l’outil, bloquer l’exécution ou exiger une approbation |
after_tool_call |
Observer les résultats de l’outil, les erreurs et la durée |
resolve_exec_env |
Fournir à exec des variables d’environnement détenues par le plugin |
tool_result_persist |
Réécrire le message de l’assistant produit à partir du résultat d’un outil |
before_message_write |
Inspecter ou bloquer l’écriture d’un message en cours (rare) |
Messages et livraison
| Hook | Objectif |
|---|---|
inbound_claim |
Revendiquer un message entrant avant le routage vers l’agent (réponses synthétiques) |
channel_pairing_requested |
Observer les nouvelles demandes d’association par message privé |
message_received |
Observer le contenu entrant, l’expéditeur, le fil et les métadonnées |
message_sending |
Réécrire le contenu sortant ou annuler la livraison |
reply_payload_sending |
Modifier ou annuler les charges utiles de réponse normalisées avant la livraison |
message_sent |
Observer la réussite ou l’échec de la livraison sortante |
before_dispatch |
Inspecter ou réécrire un envoi sortant avant son transfert au canal |
reply_dispatch |
Participer au pipeline final d’envoi des réponses |
Sessions et Compaction
| Hook | Objectif |
|---|---|
session_start / session_end |
Suivre les limites du cycle de vie des sessions. reason vaut new, reset, idle, daily, compaction, deleted, shutdown, restart ou unknown. shutdown/restart sont déclenchés par le finaliseur d’arrêt du Gateway lorsque le processus s’arrête ou redémarre avec des sessions actives, afin que les plugins (mémoire, magasins de transcriptions) puissent finaliser les lignes fantômes au lieu de les laisser ouvertes entre les redémarrages. Le finaliseur est soumis à une durée maximale afin qu’un plugin lent ne puisse pas bloquer SIGTERM/SIGINT. |
before_compaction / after_compaction |
Observer ou annoter les cycles de Compaction |
before_reset |
Observer les événements de réinitialisation de session (/reset, réinitialisations programmatiques) |
Sous-agents
subagent_spawned/subagent_ended- observent le lancement et l’achèvement des sous-agents.subagent_delivery_target- hook de compatibilité pour la livraison de l’achèvement lorsqu’aucune liaison de session du cœur ne peut projeter une route.subagent_spawning- hook de compatibilité obsolète. Le cœur prépare désormais les liaisons de sous-agentsthread: trueau moyen des adaptateurs de liaison de session du canal avant le déclenchement desubagent_spawned.subagent_spawnedinclutresolvedModeletresolvedProviderlorsqu’OpenClaw a résolu le modèle natif de la session enfant avant le lancement.subagent_endedcontienttargetSessionKey(identité — correspond àsubagent_spawned.childSessionKey),targetKind("subagent"ou"acp"),reason, l’élément facultatifoutcome("ok","error","timeout","killed","reset"ou"deleted"), l’élément facultatiferror,runId,endedAt,accountIdetsendFarewell. Il n’inclut pasagentIdnichildSessionKey; utiliseztargetSessionKeypour établir la corrélation avec l’événementsubagent_spawnedcorrespondant.
Cycle de vie
| Hook | Objectif |
|---|---|
gateway_start / gateway_stop |
Démarrer ou arrêter les services appartenant au plugin avec le Gateway |
deactivate |
Alias de compatibilité obsolète de gateway_stop ; utilisez gateway_stop dans les nouveaux plugins |
cron_reconciled |
Effectuer la réconciliation avec l’état Cron complet du Gateway après le démarrage ou le rechargement |
cron_changed |
Observer les changements de cycle de vie Cron gérés par le Gateway (ajouté, mis à jour, supprimé, démarré, terminé, planifié) |
before_install |
Inspecter les éléments d’installation de skill ou de plugin préparés depuis un runtime de plugin chargé |
Demandes d’association de canal
Utilisez channel_pairing_requested lorsqu’un plugin doit avertir un opérateur ou
écrire une entrée d’audit après que l’expéditeur d’un message privé non associé a créé une demande
d’association en attente. Le hook est déclenché lors de la création de la demande ; la remise par le canal de
la réponse d’association n’est pas retardée par des gestionnaires de hook lents ou défaillants.
api.on("channel_pairing_requested", async (event) => { await notifyOperator({ text: `Nouvelle demande d’association ${event.channel} de ${event.senderId} : ${event.code}`, });});Le hook sert uniquement à l’observation. Il n’approuve, ne rejette, ne supprime ni ne réécrit
la réponse d’association. La charge utile inclut le canal, le champ facultatif accountId,
le senderId limité au canal, le code d’association et les métadonnées du canal. Traitez le
code d’association comme un identifiant d’approbation actif à usage unique et transmettez-le uniquement à une
destination de confiance destinée aux opérateurs. Traitez metadata comme du texte d’identité non fiable
fourni par l’expéditeur. Le hook n’inclut ni le corps du message entrant ni les médias.
Hooks de débogage du runtime
Utilisez before_model_resolve pour changer de fournisseur ou de modèle pour un tour d’agent : il
s’exécute avant la résolution du modèle. llm_output ne s’exécute qu’après qu’une tentative du modèle
a produit une sortie d’assistant.
Pour vérifier le modèle de session effectif, inspectez les enregistrements du runtime, puis
utilisez openclaw sessions ou les surfaces de session/état du Gateway. Pour déboguer
les charges utiles du fournisseur, démarrez le Gateway avec --raw-stream et
--raw-stream-path <path> afin d’écrire les événements bruts du flux du modèle dans un fichier jsonl.
Politique d’appel des outils
before_tool_call reçoit :
event.toolNameevent.params- les champs facultatifs
event.toolKindetevent.toolInputKind, des discriminateurs faisant autorité côté hôte pour les outils qui partagent intentionnellement un nom ; par exemple, les appelsexecexternes en mode code utilisenttoolKind: "code_mode_exec"et incluenttoolInputKind: "javascript" | "typescript"lorsque le langage d’entrée est connu - le champ facultatif
event.derivedPaths, des indications de chemins cibles dérivées par l’hôte au mieux de ses possibilités pour les enveloppes d’outils connues telles queapply_patch; ces chemins peuvent être incomplets ou surestimer ce que l’outil modifiera réellement (par exemple, avec des entrées incorrectes ou partielles) - le champ facultatif
event.runId - le champ facultatif
event.toolCallId - des champs de contexte tels que
ctx.agentId,ctx.sessionKey,ctx.sessionId,ctx.runId,ctx.toolKind,ctx.toolInputKindet le diagnosticctx.trace
Il peut renvoyer :
type BeforeToolCallResult = { params?: Record<string, unknown>; block?: boolean; blockReason?: string; requireApproval?: { title: string; description: string; severity?: "info" | "warning" | "critical"; timeoutMs?: number; /** @deprecated Les approbations non résolues sont toujours refusées. */ timeoutBehavior?: "allow" | "deny"; allowedDecisions?: Array<"allow-once" | "allow-always" | "deny">; pluginId?: string; onResolution?: ( decision: "allow-once" | "allow-always" | "deny" | "timeout" | "cancelled", ) => Promise<void> | void; };};Comportement de protection des hooks de cycle de vie typés :
block: trueest définitif et ignore les gestionnaires de priorité inférieure.block: falseest traité comme une absence de décision.paramsréécrit les paramètres de l’outil pour l’exécution.requireApprovalmet en pause l’exécution de l’agent et interroge l’utilisateur par l’intermédiaire des approbations de plugin./approvepeut approuver les exécutions et les approbations de plugin. Dans les relaisPreToolUsenatifs en mode rapport du serveur d’application Codex, cette action est déléguée à la demande d’approbation correspondante du serveur d’application ; consultez Runtime du harnais Codex.- Un
block: truede priorité inférieure peut toujours bloquer après qu’un hook de priorité supérieure a demandé une approbation. onResolutionreçoit la décision résolue :allow-once,allow-always,deny,timeoutoucancelled.
Consultez Demandes d’autorisation des plugins pour
le routage des approbations, le comportement des décisions et les cas où utiliser requireApproval plutôt
que des outils facultatifs ou des approbations d’exécution.
Les plugins qui nécessitent une politique au niveau de l’hôte peuvent enregistrer des politiques d’outils de confiance avec
api.registerTrustedToolPolicy(...). Celles-ci s’exécutent avant les hooks
before_tool_call ordinaires et avant les décisions normales des hooks. Les politiques de confiance
intégrées s’exécutent en premier ; les politiques de confiance des plugins installés s’exécutent ensuite dans l’ordre
de chargement des plugins ; les hooks before_tool_call ordinaires s’exécutent après elles. Les plugins intégrés conservent
le chemin existant des politiques de confiance. Les plugins installés doivent être explicitement activés
et déclarer chaque identifiant de politique dans contracts.trustedToolPolicies ; les identifiants non déclarés
sont rejetés avant l’enregistrement. Les identifiants de politique sont limités au plugin qui les enregistre,
de sorte que différents plugins peuvent réutiliser le même identifiant local. Utilisez ce niveau uniquement
pour les contrôles approuvés par l’hôte, tels que la politique de l’espace de travail, l’application du budget ou
la sécurité des workflows réservés.
Hook d’environnement d’exécution
resolve_exec_env permet aux plugins de fournir des variables d’environnement aux appels de l’outil exec
avant l’exécution de la commande. Il reçoit :
event.sessionKeyevent.toolName, actuellement toujours"exec"event.host, l’une des valeurs"gateway","sandbox"ou"node"- des champs de contexte tels que
ctx.agentId,ctx.sessionKey,ctx.messageProvideretctx.channelId
Renvoyez un Record<string, string> à fusionner dans l’environnement d’exécution. Les gestionnaires
s’exécutent par ordre de priorité ; les résultats ultérieurs remplacent les résultats antérieurs pour la même
clé.
La sortie du hook est filtrée par la politique de clés d’environnement d’exécution de l’hôte avant
la fusion. PATH est toujours supprimé (la résolution des commandes et les vérifications des binaires sûrs
en dépendent). Les clés non valides et les clés dangereuses remplaçant les valeurs de l’hôte, telles que LD_*,
DYLD_*, NODE_OPTIONS, les variables de proxy (HTTP_PROXY, HTTPS_PROXY,
ALL_PROXY, NO_PROXY) et les variables de remplacement TLS (NODE_TLS_REJECT_UNAUTHORIZED,
SSL_CERT_FILE et similaires), sont supprimées. L’environnement du plugin filtré est inclus
dans les métadonnées d’approbation et d’audit du Gateway et transmis aux demandes
d’exécution sur l’hôte Node.
Persistance des résultats d’outils
Les résultats d’outils peuvent inclure des details structurés pour le rendu de l’interface utilisateur, les diagnostics,
le routage des médias ou les métadonnées appartenant au plugin. Traitez details comme des métadonnées du runtime,
et non comme du contenu d’invite :
- OpenClaw supprime
toolResult.detailsavant la relecture par le fournisseur et l’entrée de Compaction, afin que les métadonnées ne deviennent pas du contexte pour le modèle. - Les entrées de session persistantes ne conservent que des
detailsde taille limitée. Les détails trop volumineux sont remplacés par un résumé compact etpersistedDetailsTruncated: true. tool_result_persistetbefore_message_writes’exécutent avant l’application de la limite finale de persistance. Conservez desdetailsde petite taille et évitez de placer du texte pertinent pour l’invite uniquement dansdetails; placez la sortie de l’outil visible par le modèle danscontent.
Hooks d’invite et de modèle
Utilisez les hooks propres à chaque phase pour les nouveaux plugins :
before_model_resolve: reçoit uniquement l’invite actuelle et les métadonnées des pièces jointes. RenvoyezproviderOverrideoumodelOverride.agent_turn_prepare: reçoit l’invite actuelle, les messages de session préparés et toutes les injections mises en file d’attente exactement une fois qui ont été extraites pour cette session. RenvoyezprependContextouappendContext.before_prompt_build: reçoit l’invite actuelle et les messages de session. RenvoyezprependContext,appendContext,systemPrompt,prependSystemContextouappendSystemContext.heartbeat_prompt_contribution: s’exécute uniquement pour les tours de Heartbeat et renvoieprependContextouappendContext. Destiné aux moniteurs en arrière-plan qui doivent résumer l’état actuel sans modifier les tours initiés par l’utilisateur.
before_agent_start est conservé à des fins de compatibilité. Préférez les hooks explicites
ci-dessus afin que le plugin ne dépende pas d’une ancienne phase combinée.
before_agent_run s’exécute après la construction de l’invite et avant toute entrée du modèle,
y compris le chargement des images locales de l’invite et l’observation llm_input. Il reçoit
l’entrée utilisateur actuelle dans prompt, ainsi que l’historique de session chargé dans messages
et l’invite système active. Renvoyez { outcome: "block", reason, message? }
pour arrêter l’exécution avant que le modèle ne lise l’invite. reason est interne ;
message est le remplacement présenté à l’utilisateur. Seuls les résultats pass et block sont
pris en charge ; les structures de décision non prises en charge entraînent un refus par défaut.
Lorsqu’une exécution est bloquée, OpenClaw stocke uniquement le texte de remplacement dans
message.content, ainsi que des métadonnées de blocage non sensibles telles que l’identifiant du
plugin bloquant et l’horodatage. Le texte utilisateur d’origine n’est pas conservé dans la transcription
ni dans le contexte futur. Les motifs de blocage internes sont considérés comme sensibles et
exclus des charges utiles de transcription, d’historique, de diffusion, de journal et de diagnostic.
L’observabilité doit utiliser des champs assainis tels que l’identifiant du bloqueur, le résultat,
l’horodatage ou une catégorie sûre.
before_agent_start et agent_end incluent event.runId lorsqu’OpenClaw peut
identifier l’exécution active ; la même valeur figure également dans ctx.runId. Les exécutions pilotées par
Cron exposent aussi ctx.jobId (l’identifiant de la tâche Cron d’origine) dans le contexte du tour
d’agent, afin que les hooks puissent limiter les métriques, les effets secondaires ou l’état à une tâche
planifiée particulière. ctx.jobId ne fait pas partie du contexte d’outil before_tool_call.
Pour les exécutions provenant d’un canal, ctx.channel et ctx.messageProvider identifient
la surface du fournisseur, telle que discord ou telegram, tandis que ctx.channelId est
l’identifiant de la conversation cible lorsqu’OpenClaw peut le déduire de la clé
de session ou des métadonnées de remise.
Lorsque l’identité de l’expéditeur est disponible, les contextes des hooks d’agent incluent également :
ctx.senderId- identifiant de l’expéditeur limité au canal (par exemple,open_idde Feishu, identifiant utilisateur Discord). Renseigné lorsque l’exécution provient d’un message utilisateur dont les métadonnées d’expéditeur sont connues.ctx.chatId- identifiant de conversation propre au transport (par exemple,chat_idde Feishu,chat_idde Telegram). Renseigné lorsque le canal d’origine fournit un identifiant de conversation natif.ctx.channelContext.sender.id- le même identifiant d’expéditeur quectx.senderId, dans un objet appartenant au canal que les plugins peuvent étendre avec des champs propres au canal.ctx.channelContext.chat.id- le même identifiant de conversation quectx.chatId, dans un objet appartenant au canal que les plugins peuvent étendre avec des champs propres au canal.
Le cœur définit uniquement les champs id imbriqués. Les plugins de canal qui transmettent des
métadonnées d’expéditeur ou de conversation plus riches par l’intermédiaire de l’assistant entrant peuvent enrichir
PluginHookChannelSenderContext ou PluginHookChannelChatContext depuis
openclaw/plugin-sdk/channel-inbound :
declare module "openclaw/plugin-sdk/channel-inbound" { interface PluginHookChannelSenderContext { unionId?: string; userId?: string; }}Les plugins de canal transmettent ces champs par l’intermédiaire de l’assistant du SDK entrant :
buildChannelInboundEventContext({ // ... channelContext: { sender: { id: senderOpenId, unionId, userId }, chat: { id: chatId }, },});Ces champs sont facultatifs et absents pour les exécutions provenant du système (Heartbeat, Cron, événement d’exécution).
ctx.senderExternalId reste disponible comme champ de compatibilité source obsolète pour
les anciens plugins. Le cœur ne le renseigne pas ; les nouvelles identités d’expéditeur
propres au canal doivent se trouver sous ctx.channelContext.sender par augmentation
de module.
agent_end est un hook d’observation. Les chemins du Gateway et des harnais persistants
l’exécutent sans attendre après le tour, tandis que les chemins CLI ponctuels et éphémères attendent
la promesse du hook avant le nettoyage du processus afin que les plugins de confiance puissent vider
la télémétrie terminale ou capturer l’état. L’exécuteur de hooks applique un délai d’expiration de 30 secondes
afin qu’un plugin bloqué ou un point de terminaison d’intégration ne puisse pas laisser la promesse du hook
en attente indéfiniment. Une expiration est consignée et OpenClaw continue ; elle
n’annule pas les opérations réseau appartenant au plugin, sauf si celui-ci utilise également son propre signal
d’abandon.
Utilisez model_call_started et model_call_ended pour la télémétrie des appels au fournisseur
qui ne doit pas recevoir les prompts bruts, l’historique, les réponses, les en-têtes, les corps de
requête ni les ID de requête du fournisseur. Ces hooks incluent des métadonnées stables telles que
runId, callId, provider, model, les champs facultatifs api/transport, les champs terminaux
durationMs/outcome, ainsi que upstreamRequestIdHash lorsqu’OpenClaw peut produire un
hachage borné de l’ID de requête du fournisseur. Lorsque l’environnement d’exécution a résolu
les métadonnées de la fenêtre de contexte, l’événement et le contexte du hook incluent également
contextTokenBudget, le budget effectif de jetons après application des plafonds du modèle, de la configuration et de l’agent,
ainsi que contextWindowSource et contextWindowReferenceTokens lorsqu’un
plafond inférieur a été appliqué.
before_agent_finalize s’exécute uniquement lorsqu’un harnais est sur le point d’accepter une réponse finale
naturelle de l’assistant. Il ne correspond pas au chemin d’annulation /stop et ne
s’exécute pas lorsque l’utilisateur abandonne un tour. Renvoyez { action: "revise", reason } pour demander
au harnais une passe supplémentaire du modèle avant la finalisation, { action: "finalize", reason? } pour forcer la finalisation, ou omettez le résultat pour continuer.
Les gestionnaires disposent d’un budget par défaut de 15s ; en cas d’expiration, OpenClaw consigne l’échec et
continue avec la réponse finale d’origine.
Les hooks Stop natifs de Codex sont relayés vers ce hook sous forme de décisions OpenClaw
before_agent_finalize.
Lorsqu’ils renvoient action: "revise", les plugins peuvent inclure des métadonnées retry afin
que la passe supplémentaire du modèle soit bornée et sûre en cas de réexécution :
type BeforeAgentFinalizeRetry = { instruction: string; idempotencyKey?: string; maxAttempts?: number;};instruction est ajoutée à la raison de révision envoyée au harnais.
idempotencyKey permet à l’hôte de compter les nouvelles tentatives pour une même requête de plugin
parmi des décisions de finalisation équivalentes, et maxAttempts limite le nombre de passes
supplémentaires que l’hôte autorisera avant de poursuivre avec la réponse finale naturelle.
Les plugins non intégrés qui ont besoin de hooks de conversation brute (before_model_resolve,
before_agent_reply, llm_input, llm_output, before_agent_finalize,
agent_end ou before_agent_run) doivent définir :
{ "plugins": { "entries": { "my-plugin": { "hooks": { "allowConversationAccess": true } } } }}Les hooks qui modifient le prompt et les injections persistantes au tour suivant peuvent être désactivés pour chaque
plugin avec plugins.entries.<id>.hooks.allowPromptInjection=false.
Extensions de session et injections au tour suivant
Les plugins de workflow peuvent conserver un petit état de session compatible avec JSON à l’aide de
api.session.state.registerSessionExtension(...) et le mettre à jour au moyen de la
méthode Gateway sessions.pluginPatch. Les lignes de session projettent l’état des
extensions enregistrées par l’intermédiaire de pluginExtensions, ce qui permet à Control UI et aux autres
clients d’afficher l’état appartenant au plugin sans connaître ses mécanismes internes.
api.registerSessionExtension(...) fonctionne toujours, mais est déconseillée au profit de
l’espace de noms api.session.state.
Utilisez api.session.workflow.enqueueNextTurnInjection(...) lorsqu’un plugin doit
transmettre exactement une fois un contexte persistant au prochain tour du modèle (la fonction de niveau supérieur
api.enqueueNextTurnInjection(...) est un alias déconseillé présentant le même
comportement). OpenClaw vide la file des injections avant les hooks de prompt, supprime
les injections expirées et déduplique par idempotencyKey pour chaque plugin. Il s’agit
du point d’intégration approprié pour les reprises après approbation, les résumés de politiques, les écarts des moniteurs
en arrière-plan et les continuations de commandes qui doivent être visibles par le modèle au
tour suivant, mais ne doivent pas devenir du texte permanent du prompt système.
La sémantique de nettoyage fait partie du contrat. Les rappels de nettoyage des extensions de session et
du cycle de vie de l’environnement d’exécution reçoivent reset, delete, disable ou
restart. L’hôte supprime l’état persistant des extensions de session appartenant au plugin
et les injections en attente pour le tour suivant lors de reset/delete/disable ; restart
conserve l’état persistant de la session, tandis que les rappels de nettoyage permettent aux plugins de libérer
les tâches du planificateur, le contexte d’exécution et les autres ressources hors bande de l’ancienne
génération de l’environnement d’exécution.
Hooks de message
Utilisez les hooks de message pour les politiques de routage et de distribution au niveau du canal :
message_received: observe le contenu entrant, l’expéditeur,threadId,messageId,senderId, la corrélation facultative avec l’exécution ou la session, ainsi que les métadonnées.message_sending: réécritcontentou renvoie{ cancel: true }.reply_payload_sending: réécrit les objetsReplyPayloadnormalisés (notammentpresentation,delivery, les références aux médias et le texte) ou renvoie{ cancel: true }.message_sent: observe la réussite ou l’échec final.
Pour les réponses TTS uniquement audio, content peut contenir la transcription vocale
masquée même lorsque la charge utile du canal ne comporte aucun texte ni aucune légende visible.
La réécriture de ce content met uniquement à jour la transcription visible par le hook ; elle n’est pas
affichée comme légende du média.
Les événements reply_payload_sending peuvent inclure usageState, un instantané en direct
fourni au mieux pour chaque tour concernant le modèle, l’utilisation et le contexte. La distribution persistante, la réexécution récupérée et
les réponses sans corrélation exacte avec l’exécution l’omettent.
Les contextes des hooks de message exposent des champs de corrélation stables lorsqu’ils sont disponibles :
ctx.sessionKey, ctx.runId, ctx.messageId, ctx.senderId, ctx.trace,
ctx.traceId, ctx.spanId, ctx.parentSpanId et ctx.callDepth. Les contextes entrants
et before_dispatch exposent également les métadonnées de réponse lorsque le canal
dispose des données du message cité filtrées selon la visibilité : replyToId, replyToIdFull,
replyToBody, replyToSender et replyToIsQuote. Privilégiez ces
champs de premier ordre avant de lire les métadonnées héritées.
Privilégiez les champs typés threadId et replyToId avant d’utiliser des métadonnées
propres au canal.
Règles de décision :
message_sendingaveccancel: trueest terminal.message_sendingaveccancel: falseest traité comme une absence de décision.- Le
contentréécrit continue vers les hooks de priorité inférieure, sauf si un hook ultérieur annule la distribution. reply_payload_sendings’exécute après la normalisation de la charge utile et avant la distribution sur le canal, y compris pour les réponses redirigées vers le canal d’origine. Les gestionnaires s’exécutent séquentiellement et chacun voit la dernière charge utile produite par les gestionnaires de priorité supérieure.- Les charges utiles de
reply_payload_sendingn’exposent pas les marqueurs de confiance de l’environnement d’exécution tels quetrustedLocalMedia; les plugins peuvent modifier la structure de la charge utile, mais ne peuvent pas accorder la confiance aux médias locaux. message_sendingpeut renvoyercancelReasonet desmetadatabornées avec une annulation. Les nouvelles API de cycle de vie des messages exposent cela comme un résultat de distribution supprimée avec la raisoncancelled_by_message_sending_hook; la distribution directe héritée continue de renvoyer un tableau de résultats vide par compatibilité.message_sentest uniquement destiné à l’observation. Les échecs des gestionnaires sont consignés et ne modifient pas le résultat de la distribution.
Hooks d’installation
Utilisez security.installPolicy pour les décisions d’autorisation ou de blocage appartenant à l’opérateur. Cette
politique s’exécute à partir de la configuration OpenClaw, couvre les chemins d’installation et de mise à jour de la CLI, et
échoue en mode fermé lorsqu’elle est activée mais indisponible.
before_install est un hook de cycle de vie de l’environnement d’exécution des plugins. Il s’exécute après
security.installPolicy uniquement dans le processus OpenClaw où les hooks de plugins ont
déjà été chargés, par exemple dans les flux d’installation adossés au Gateway. Il est utile pour
les observations, avertissements et vérifications de compatibilité appartenant au plugin, mais ne constitue pas
la principale frontière de sécurité de l’entreprise ou de l’hôte pour les installations. Le champ
builtinScan reste présent dans la charge utile de l’événement par compatibilité, mais
OpenClaw n’effectue plus de blocage intégré du code dangereux au moment de l’installation ; il
s’agit donc d’un résultat ok vide. Renvoyez des constatations supplémentaires ou
{ block: true, blockReason } pour arrêter l’installation dans ce processus.
block: true est terminal. block: false est traité comme une absence de décision. Les échecs des gestionnaires
bloquent l’installation en mode fermé.
Cycle de vie du Gateway
Utilisez gateway_start pour démarrer les services généraux des plugins et gateway_stop pour
nettoyer les ressources de longue durée. Le planificateur Cron peut encore être en cours de chargement lorsque
gateway_start s’exécute ; ne l’utilisez donc pas comme signal de référence pour une projection
Cron externe.
Ne vous appuyez pas sur le hook interne gateway:startup pour les services de l’environnement d’exécution
appartenant aux plugins.
cron_reconciled est déclenché après que le planificateur Cron du Gateway et ses observateurs de sortie
ont réconcilié leur état persistant. Il est déclenché aussi bien lors du
démarrage initial que lors du remplacement du planificateur pendant le rechargement de la configuration. L’événement indique
reason (startup ou reload) et l’état enabled effectif. Un Cron désactivé
émet tout de même un événement avec enabled: false, ce qui permet à une projection externe de
supprimer les réveils obsolètes. Utilisez ctx.getCron?.() pour obtenir l’instance exacte du planificateur qui
a terminé la réconciliation ; un rechargement ultérieur ne redirige pas ce rappel.
ctx.abortSignal appartient au même instantané du planificateur. Le Gateway l’abandonne dès
qu’un planificateur plus récent est armé ou que l’arrêt commence. Transmettez-le à chaque
effet secondaire persistant et n’acceptez pas l’instantané après son abandon.
Il s’agit d’un signal de cycle de vie du planificateur, et non d’un signal d’activation de plugin : un
rechargement à chaud limité au plugin ne le redéclenche pas. Un consommateur nouvellement activé reçoit
sa première référence lors du prochain remplacement du planificateur ou démarrage du Gateway.
Comme les autres hooks d’observation, les rappels gateway_start et cron_reconciled
peuvent se chevaucher. Si les deux gestionnaires partagent l’initialisation du plugin, coordonnez-les
avec une promesse locale au plugin indiquant qu’il est prêt, plutôt qu’en vous appuyant sur l’ordre des rappels.
cron_changed est déclenché pour les événements de cycle de vie Cron appartenant au Gateway avec une charge utile
d’événement typée couvrant les raisons added, updated, removed, started, finished
et scheduled. L’événement transporte un instantané PluginHookGatewayCronJob
(notamment state.nextRunAtMs, state.lastRunStatus et
state.lastError lorsqu’ils sont présents), ainsi qu’un PluginHookGatewayCronDeliveryStatus
valant not-requested | delivered | not-delivered | unknown. Les événements de suppression
sont postérieurs à la validation : ils ne sont déclenchés qu’après la réussite de la suppression persistante et transportent toujours
l’instantané de la tâche supprimée afin que les planificateurs externes puissent réconcilier l’état.
Un événement scheduled est postérieur à la validation : il n’est déclenché qu’après qu’une écriture persistante réussie
a modifié la valeur nextRunAtMs effective d’une tâche existante, à l’exclusion de l’événement de cycle de vie
explicite added, updated ou removed de cette tâche. Le champ de niveau supérieur
event.nextRunAtMs correspond au prochain réveil validé ; lorsqu’il est absent, la tâche
n’a aucun prochain réveil. Traitez ces événements comme des indications de réconciliation, et non comme un journal ordonné
des différences. Utilisez-les comme des indications pouvant être regroupées pour relire le dernier planificateur capturé par
cron_reconciled ; n’adoptez pas le planificateur depuis un contexte cron_changed.
Conservez OpenClaw comme source de vérité pour la vérification des échéances et l’exécution.
Projection Cron externe sûre
Projetez un instantané complet des réveils au lieu de transmettre les différences des événements Cron. L’opération
replaceAll de l’adaptateur externe doit être atomique et idempotente, et elle
ne doit se résoudre qu’après que l’hôte a accepté durablement l’instantané. Elle doit
également respecter le signal d’abandon fourni : si le signal est abandonné avant l’acceptation
persistante, l’adaptateur ne doit pas accepter cet instantané.
Ce modèle maintient un seul worker du dernier état en cours. Seul cron_reconciled
adopte une instance du planificateur ; cron_changed demande simplement à ce worker de relire
l’instance faisant autorité, afin qu’une indication tardive ne puisse pas restaurer un planificateur plus ancien.
Une révision plus récente abandonne la tentative active de l’hôte avant qu’elle puisse accepter un instantané
obsolète.
type ExternalWake = { jobId: string; runAtMs: number }; type ExternalWakeHost = { replaceAll(wakes: readonly ExternalWake[], options: { signal: AbortSignal }): Promise<void>; close(): Promise<void>;}; type CronReader = { list(options: { includeDisabled: true }): Promise< Array<{ id: string; enabled?: boolean; state?: { nextRunAtMs?: number }; }> >;}; export function registerCronProjection(api: OpenClawPluginApi, host: ExternalWakeHost) { const lifecycle = new AbortController(); let cron: CronReader | undefined; let enabled = false; let hasBaseline = false; let reconciliationSignal: AbortSignal | undefined; let requestedRevision = 0; let appliedRevision = 0; let worker = Promise.resolve(); let activeAttempt: AbortController | undefined; const projectLatest = async () => { let retryMs = 1_000; while (!lifecycle.signal.aborted && appliedRevision < requestedRevision) { const ownerSignal = reconciliationSignal; if (!ownerSignal || ownerSignal.aborted) { return; } const targetRevision = requestedRevision; const attempt = new AbortController(); const signal = AbortSignal.any([lifecycle.signal, ownerSignal, attempt.signal]); activeAttempt = attempt; try { const jobs = enabled && cron ? await cron.list({ includeDisabled: true }) : []; if (signal.aborted || targetRevision !== requestedRevision) { continue; } const wakes = jobs .flatMap((job): ExternalWake[] => { const runAtMs = job.enabled === false ? undefined : job.state?.nextRunAtMs; return runAtMs === undefined ? [] : [{ jobId: job.id, runAtMs }]; }) .sort((a, b) => a.runAtMs - b.runAtMs || a.jobId.localeCompare(b.jobId)); await host.replaceAll(wakes, { signal }); if (signal.aborted || targetRevision !== requestedRevision) { continue; } appliedRevision = targetRevision; retryMs = 1_000; } catch { if (lifecycle.signal.aborted || ownerSignal.aborted) { return; } if (attempt.signal.aborted) { continue; } api.logger.warn(`external cron projection failed; retrying in ${retryMs}ms`); try { await sleep(retryMs, undefined, { signal }); } catch { if (lifecycle.signal.aborted) { return; } if (attempt.signal.aborted) { continue; } } retryMs = Math.min(retryMs * 2, 30_000); } finally { if (activeAttempt === attempt) { activeAttempt = undefined; } } } }; const requestProjection = () => { const targetRevision = ++requestedRevision; activeAttempt?.abort(); worker = worker.then(async () => { if (!lifecycle.signal.aborted && appliedRevision < targetRevision) { await projectLatest(); } }); return worker; }; api.on("cron_reconciled", (event, ctx) => { const reconciledCron = ctx.getCron?.(); if (event.enabled && !reconciledCron) { api.logger.warn("cron reconciliation did not expose a scheduler"); return; } cron = reconciledCron; enabled = event.enabled; hasBaseline = true; reconciliationSignal = ctx.abortSignal; return requestProjection(); }); api.on("cron_changed", () => { if (hasBaseline) { return requestProjection(); } }); api.on("gateway_stop", async () => { lifecycle.abort(); await worker; await host.close(); });}Lorsque cron_reconciled signale enabled: false, le même chemin appelle
replaceAll([]) et supprime les réveils externes obsolètes. Dans cet exemple,
les nouvelles tentatives et l’attente exponentielle sont locales au processus
et considèrent les échecs de l’adaptateur d’exécution comme transitoires ;
validez la configuration non récupérable avant l’enregistrement. OpenClaw ne
fournit pas de boîte d’envoi pour les effets des hooks de Plugin. Si le processus
se termine avant l’acceptation durable, le prochain démarrage du Gateway émet un
nouvel instantané cron_reconciled faisant autorité. gateway_stop interrompt
le travail de l’hôte en cours, attend que le worker se termine, puis ferme
l’adaptateur.
Dépréciations à venir
Quelques surfaces associées aux hooks sont dépréciées, mais restent prises en charge. Effectuez la migration avant la prochaine version majeure :
- Enveloppes de canal en texte brut dans les gestionnaires
inbound_claimetmessage_received. LisezBodyForAgentet les blocs structurés de contexte utilisateur au lieu d’analyser le texte d’enveloppe à plat. Voir Enveloppes de canal en texte brut → BodyForAgent. before_agent_startest conservé à des fins de compatibilité. Les nouveaux plugins doivent utiliserbefore_model_resolveetbefore_prompt_buildau lieu de la phase combinée.subagent_spawningest conservé pour assurer la compatibilité avec les anciens plugins, mais les nouveaux plugins ne doivent pas renvoyer de routage de fil de discussion depuis celui-ci. Le cœur prépare les liaisons de sous-agentthread: trueau moyen des adaptateurs de liaison de session de canal avant le déclenchement desubagent_spawned.deactivatereste un alias de compatibilité déprécié pour le nettoyage jusqu’après le 2026-08-16. Les nouveaux plugins doivent utilisergateway_stop.onResolutiondansbefore_tool_callutilise désormais l’union typéePluginApprovalResolution(allow-once/allow-always/deny/timeout/cancelled) au lieu d’une valeurstringlibre.api.registerSessionExtension/api.enqueueNextTurnInjectionrestent des alias de compatibilité de premier niveau. Les nouveaux plugins doivent utiliserapi.session.state.registerSessionExtension(...)etapi.session.workflow.enqueueNextTurnInjection(...).
Pour la liste complète — enregistrement des capacités de mémoire, profil de
raisonnement du fournisseur, fournisseurs d’authentification externes, types de
découverte de fournisseurs, accesseurs d’exécution des tâches et renommage de
command-auth en command-status — consultez
Migration du SDK de Plugin → Dépréciations actives.
Pages connexes
- Migration du SDK de Plugin — dépréciations actives et calendrier de suppression
- Création de plugins
- Présentation du SDK de Plugin
- Points d’entrée des plugins
- Hooks internes
- Fonctionnement interne de l’architecture des plugins