Plugins

Les implémentations de modèles, les outils, les skills de runtime, les destinations d'audit, les connecteurs et les services en arrière-plan utilisent un seul protocole RuntimePlugin et un PluginHost par Harness. Ce sont des plugins d'architecture du code, pas des serveurs MCP. Un plugin peut apporter plusieurs capacités ; le système n'impose pas de catégories de plugins mutuellement exclusives.

La boucle d'agent, la Session, les permissions et les registres restent dans le moteur. Les bundles déclaratifs oharness.plugin.json existants restent pris en charge et ne sont jamais importés automatiquement comme modules exécutables.

Démarrage rapide

Lancez la démonstration à quatre plugins, sans identifiants ni appels réseau :

npm run demo:plugins
npm run test:plugins

Les exemples de script/fixtures/runtime-plugins.ts implémentent un modèle scripté, un outil local, un observateur d'audit et un connecteur simulé. Ce sont des démonstrations. Le connecteur Cloud hébergé est enregistré par défaut sous connector-cloud en dehors des instances éphémères. Il enregistre deux outils sans accès réseau au démarrage ; une connexion ou une configuration serveur manquante devient une erreur d'outil, pas un échec de démarrage. Désactivez-le avec { "id": "connector-cloud", "enabled": false } dans les runtimePlugins de plugins.json. Les applications qui embarquent le moteur peuvent définir cloudConnectors: false. L'adaptateur Composio direct reste une alternative facultative pour les développeurs ; ne l'activez pas dans les clients hébergés. Pipedream n'est pas implémenté.

Le plugin Memory intégré, d'identifiant memory, gère désormais l'outil de mémoire, l'index du prompt par tour, la page Web et le cycle de vie de la synchronisation en arrière-plan. Il est actif même dans les instances éphémères de Harness (comme l'ancien comportement de la mémoire locale). Désactivez-le dans plugins.json pour retirer toutes ces contributions. C'est distinct de la désactivation de la synchronisation cloud avec conservation de la mémoire locale.

Écrire un plugin

import type { RuntimePlugin } from "@oharness/engine";

export default {
  id: "hello-tools",
  version: "1.0.0",
  apiVersion: 1,
  parseConfig(raw) {
    if (raw !== undefined) throw new Error("This plugin takes no options");
    return {};
  },
  setup(ctx) {
    ctx.tools.register({
      name: "hello_greet",
      description: "Return a greeting",
      inputSchema: { type: "object", properties: {} },
      readOnly: true,
      async execute() {
        return { content: [{ type: "text", text: "Hello" }] };
      },
    });
  },
} satisfies RuntimePlugin;

setup s'exécute une fois par instance de Harness. Gardez-y les clients, les minuteurs et l'état mutable, pas au niveau du module. Les identifiants de plugin utilisent des lettres minuscules, des chiffres, des points, des tirets bas ou des traits d'union. La version 1 de l'API est vérifiée indépendamment de la version propre du plugin.

Charger et configurer

Une application qui embarque le moteur peut fournir explicitement des définitions de plugins de confiance :

import { Harness } from "@oharness/engine";
import greeting from "./hello-plugin.js";

const harness = await Harness.create({
  runtimePlugins: [{ plugin: greeting, required: true }],
});
try {
  console.log(harness.runtimePluginStatus());
  // Create sessions / agents as usual.
} finally {
  await harness.close();
}

Vous pouvez aussi ajouter runtimePlugins au fichier de plugins utilisateur (plugins.json dans OHARNESS_HOME, normalement ~/.oharness). Un chemin de module absolu ou un package installé résolvable depuis ce répertoire est accepté ; rien n'est téléchargé. Les modules doivent exporter par défaut une définition de plugin. Préférez du JavaScript compilé pour la distribution ; du TypeScript source nécessite le sous-ensemble de suppression de types pris en charge par Node.

{
  "runtimePlugins": [
    {
      "id": "hello-tools",
      "module": "/absolute/path/to/hello-plugin.js",
      "enabled": true,
      "required": true
    }
  ]
}

config est transmis à parseConfig. Une entrée configurée avec le même identifiant peut configurer ou désactiver un plugin fourni en ligne, mais ne peut pas remplacer son implémentation par un autre module. Les identifiants en double dans l'une ou l'autre liste sont rejetés. enabled: false évite entièrement l'import du module, l'analyse de la configuration et le setup. required vaut true par défaut ; les échecs facultatifs sont visibles dans l'état sans interrompre la création du Harness. La liste de configuration remplace une liste précédente au lieu de fusionner en profondeur les options des plugins. Les modifications s'appliquent à la prochaine création de Harness, pas aux sessions actives.

La configuration de projet ne peut ni introduire ni reconfigurer de plugins de runtime. Fournir HarnessOptions.config est une décision de confiance explicite de l'application qui embarque le moteur. Ne mettez pas de secrets dans la configuration de projet ni dans les métadonnées des plugins. Chargez-les via votre intégration ou votre environnement d'identifiants approuvé, et ne les journalisez jamais.

Interfaces de capacités

InterfaceContribution
ctx.tools.register(tool)Définition et implémentation d'un outil ; le pipeline de permissions existant s'applique toujours
ctx.skills.register(skill)Skill en mémoire disponible pendant la durée de vie de ce plugin ; les noms en double sont rejetés
ctx.models.register(provider, models)Implémentation de modèle et ses définitions de modèles, validées de façon atomique
ctx.auth.register(provider)Implémentation d'authentification utilisant le contrat AuthProvider existant
ctx.hooks.on(event, handler)Hook de cycle de vie typé existant, avec désabonnement automatique
ctx.audit.registerSink(sink)Observateur des événements de permission et d'exécution, au mieux (best-effort)
ctx.prompts.register(source)Section asynchrone du prompt système, actualisée à chaque tour d'agent
ctx.pages.register(page)Descripteur de page rendue par l'hôte et gestionnaires de lecture et d'action
ctx.onStart(fn)Travail en arrière-plan, démarré une fois tous les setups de plugins terminés
ctx.onDispose(fn)Ressources à fermer, dans l'ordre inverse de leur acquisition

Toutes les contributions doivent être enregistrées pendant setup. Elles sont suivies par l'hôte et annulées automatiquement. L'enregistrement par défaut rejette les conflits ; les plugins ne peuvent pas remplacer en silence les éléments intégrés ni les enregistrements les uns des autres. Les API de registre fluides existantes restent compatibles. Retirer une implémentation d'authentification n'efface pas les identifiants. Les fournisseurs de modèles doivent apporter leurs propres identifiants, et non tenter d'écraser les entrées existantes du catalogue. Sélectionnez un modèle de plugin avec la configuration model habituelle ou l'API de sélection de modèle.

Un skill de runtime fournit name, description et body, plus un directory absolu facultatif lorsque son corps fait référence à des scripts ou à d'autres fichiers. Il utilise le même outil skill à divulgation progressive que les skills persistés, apparaît avec la source runtime et est retiré automatiquement à l'arrêt de son plugin. Les skills de runtime ne peuvent pas remplacer les skills intégrés, utilisateur, de projet, configurés ou d'autres skills de runtime portant le même nom.

ctx.signal annonce l'arrêt du plugin. Les outils et les flux de modèle reçoivent aussi le signal d'annulation combiné de l'hôte et de l'appel. Gardez le travail en arrière-plan coopératif ; enregistrez le nettoyage d'une ressource juste après son acquisition, avant des await ultérieurs susceptibles d'échouer. onStart doit se résoudre après avoir lancé le travail, et non attendre une boucle de travail infinie. Conservez plutôt sa promesse d'achèvement et attendez-la dans onDispose.

Plugin d'audit

Le moteur fournit jsonlAuditPlugin (id: "audit-jsonl"), qui utilise ce même contrat. Passez-lui { path: "/absolute/path/to/events.jsonl" } comme configuration, soit via l'API d'intégration, soit via une entrée de module pointant vers packages/engine/dist/runtime-plugins/builtin/jsonl-audit.js. Il crée les répertoires parents, ouvre le fichier en mode ajout (0600 pour un fichier nouvellement créé) et vide sa file d'attente avant de fermer. Il ne fait pas de rotation des fichiers et ne modifie pas les permissions des fichiers existants.

Une destination d'audit reçoit des RuntimeAuditEvent :

  • permission : les enregistrements de décision et de réponse existants du PermissionEngine, y compris les décisions des sous-agents relayées par l'hôte.
  • tool_execution : les appels terminés, y compris les erreurs levées par les outils et converties en résultats d'erreur ; inclut la durée, l'identité de l'outil et le plugin de runtime propriétaire.

Les événements d'exécution omettent les corps d'entrée et de sortie ainsi que les identifiants. Les enregistrements de permission conservent les champs rule/principal existants, qui peuvent contenir des chemins ou des arguments de commande : les destinations doivent tenir compte de leur sensibilité. Ce n'est ni un rejeu complet des événements ni un journal de conformité ; dans cette implémentation, les appels bloqués avant le PermissionEngine n'obtiennent pas d'enregistrements de décision supplémentaires.

Chaque destination reçoit son propre instantané et sa propre file ordonnée. Une destination qui lève une exception ne bloque pas les autres et ne change pas la décision d'un outil. Les files sont plafonnées à 1 000 événements en attente par destination ; au-delà, un avertissement est émis et les événements excédentaires sont abandonnés. L'arrêt vide les files avant de fermer les ressources des destinations, dans la limite du délai de nettoyage. audit: false désactive les événements d'audit du runtime ainsi que la trace de permissions existante.

Exemple de destination fichier (dans un plugin) :

const file = await open(config.path, "a", 0o600); // node:fs/promises
ctx.onDispose(() => file.close());
ctx.audit.registerSink(async (event) => {
  await file.appendFile(JSON.stringify(event) + "\n");
});

Cela conserve la sémantique d'audit au mieux (best-effort) existante du moteur. Rendre un plugin d'audit obligatoire garantit le démarrage, pas des écritures avec fermeture par défaut (fail-closed). Un audit durable obligatoire nécessiterait une politique d'exécution avec attente conçue séparément.

Plugin de connecteur

Utilisez createConnectorPlugin comme fabrique pratique, ou implémentez directement RuntimePlugin. La fabrique renvoie un RuntimePlugin ordinaire, pas un autre mécanisme :

const plugin = createConnectorPlugin({
  id: "my-connector",
  version: "1.0.0",
  parseConfig: validateConnectorConfig,
  async connect(config, { signal, logger }) {
    // Wrap your selected SDK. Bind a trusted account here, not from model input.
    return createMyConnectorClient(config, { signal, logger });
  },
});

ConnectorClient expose tools(): Promise<readonly Tool[]> et close(). Si la découverte des outils échoue, le client connecté est quand même fermé. L'assistant force readOnly et sessionLocal à false pour les outils externes : les métadonnées du fournisseur ne peuvent donc pas contourner en silence l'approbation ou les restrictions du mode plan. N'exposez que des outils approuvés, utilisez des espaces de noms, validez les arguments et propagez le signal de chaque appel au SDK. N'envoyez pas tout le catalogue d'un fournisseur dans chaque prompt du modèle.

L'hôte n'implémente ni l'OAuth des fournisseurs, ni la liaison de comptes, ni les webhooks, ni le stockage par locataire. Le connecteur Cloud fournit la création de liens hébergés, l'interaction Web et la liaison vérifiée ; il ne modifie pas le protocole de l'hôte. L'identité des comptes multi-utilisateurs doit provenir d'un contexte applicatif de confiance, jamais d'un identifiant de compte choisi par le modèle. Le PluginHost lui-même ne nécessite aucune modification de l'infrastructure cloud.

Cycle de vie et compatibilité

  1. Résoudre les entrées activées et valider le contrat et la configuration.
  2. Exécuter les setups dans l'ordre configuré ; enregistrer la propriété des enregistrements et des ressources.
  3. Exécuter les démarrages une fois tous les setups terminés ; exposer l'état actif.
  4. En cas d'échec, annuler le plugin en échec ; tout interrompre et annuler s'il est obligatoire.
  5. À la fermeture du Harness, empêcher les nouvelles exécutions de l'hôte, annuler ou vider les exécutions en cours, interrompre les appels de plugins, désenregistrer les contributions, puis fermer les ressources dans l'ordre inverse.

Le nettoyage est idempotent et continue après les échecs individuels. Le vidage de l'agent et chaque vidage/nettoyage de plugin sont limités à cinq secondes. Les délais dépassés sont signalés ; ils ne peuvent pas arrêter de force du JavaScript non coopératif, et du travail peut subsister après un délai dépassé. Un outil de plugin cloné et périmé rejette les appels après l'arrêt de son plugin.

/plugins dans la CLI et /context dans le Web exposent l'état du runtime à côté des bundles déclaratifs. Les erreurs omettent les messages d'exception fournis par les plugins pour éviter de divulguer des secrets de configuration. Ajoutez au besoin des diagnostics nettoyés avec le logger propre au plugin.

La V1 n'a volontairement ni déchargement à chaud, ni graphe de dépendances ou de services entre plugins, ni installateur de packages distants, ni bac à sable, ni chargeur arbitraire de modules frontend, ni cœur Agent/Session remplaçable. Les plugins installés dans le processus disposent des droits du processus : l'API de contexte est une frontière de maintenabilité, pas un bac à sable de sécurité.

Consultez les limites de configuration pour migrer l'ancienne configuration en ligne. Les fichiers de plugins séparés sont une configuration utilisateur de confiance, jamais chargée depuis un répertoire de projet arbitraire.