SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-21

Créer un plugin dsh pour DeepSeek Harness

Créez un plugin dsh depuis un dossier vide : package.json, fichier YAML de chargement, outil réel et deux hooks indispensables, sans SDK distinct à apprendre.

Ce qu’est réellement un plugin dsh

Un plugin dsh est un paquet npm qui exporte une fonction apply et fournit un petit fichier YAML indiquant à DeepSeek Harness de le charger. Il n’existe pas de SDK de plugins distinct à apprendre au préalable. dsh est une application Cordis, et l’affirmation « tout est un plugin » est littérale : le registre des outils, la boucle de l’agent, le stockage des sessions et le serveur web sont tous des éléments du même arbre de plugins que votre paquet rejoint.

Cordis est un framework général de composition, développé indépendamment et utilisé depuis des années comme base du framework de chatbot Koishi. Il gère le chargement et le déchargement, et résout les dépendances entre les plugins. Il ne sait rien des agents. Tout ce qui concerne les agents provient des paquets du harness ajoutés par-dessus. C’est pourquoi la structure d’un plugin présentée ci-dessous est si réduite. La plupart des fonctionnalités sont héritées.

Un plugin comporte deux parties. La partie hôte s’exécute dans Node, enregistre les outils et les listeners d’événements, et peut fournir ses propres services. La partie navigateur s’exécute dans l’interface web et enregistre des emplacements d’interface. Un premier plugin est presque toujours limité à la partie hôte. Considérez donc la partie navigateur comme facultative jusqu’à ce que vous en ayez besoin.

Ce guide a été rédigé pour la version @deepseek-ai/dsh 0.1.0-rc.7, le tag npm latest du 19 août 2026. dsh est une developer preview, et son propre README indique que des changements incompatibles sont prévus. Chaque nom de clé ci-dessous a été vérifié dans la documentation upstream et le dépôt à cette date. Vérifiez-les de nouveau avant d’en dépendre, car une preview API peut renommer des champs entre deux release candidates. Si le harness ne fonctionne pas encore, configurez-le avec DeepSeek Harness sur un VPS et la clé API et la configuration du modèle dsh, puis revenez ici.

Charger un seul fichier de test avant de créer un package

Commencer par créer le package est la méthode la plus lente pour comprendre ce fonctionnement. Chargez un seul fichier, vérifiez que le runtime appelle votre code, puis créez le package.

Créez un dossier en dehors du checkout du harness et placez-y un fichier.

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  console.log('[hello-plugin] plugin loaded')
}

export const name correspond aux métadonnées utilisées pour identifier le plugin dans les diagnostics. apply constitue l’intégralité du contrat : Cordis l’appelle une fois et lui transmet un contexte limité à votre plugin. Tout ce que vous enregistrez sur ce contexte est annulé automatiquement lorsque le plugin est supprimé.

À côté, écrivez cordis.yml.

- insert:
    - id: hello
      name: '/absolute/path/to/scratch-plugin/hello.ts'

Démarrez maintenant un profil avec ce fichier ajouté par-dessus.

dsh web --patch ./scratch-plugin/cordis.yml

Si dsh ne se trouve pas dans votre PATH, npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml fait la même chose. Cette commande via npx peut fournir une ancienne release candidate mise en cache au lieu de la version décrite dans ce guide. Si le harness rejette directement un flag documenté, consultez les correctifs pour les erreurs d’installation et de version de dsh avant de mettre votre propre fichier en cause. Vous devriez voir [hello-plugin] plugin loaded dans le terminal depuis lequel dsh a été démarré. Si rien ne s’affiche, la ligne n’a pas été résolue.

Le champ name accepte le nom d’un package npm ou un chemin de système de fichiers. La documentation amont précise que le chemin doit être absolu. Un ./hello.ts relatif est le premier élément à vérifier lorsqu’un plugin de test ne produit aucune sortie. Le deuxième est l’extension du fichier. La procédure documentée s’exécute avec pnpm dsh web --patch ... depuis un clone du dépôt du harness, où les entrées TypeScript sont chargées via tsx. Si votre dsh provient de npm, indiquez un fichier JavaScript standard ou construisez d’abord le fichier.

--patch est un flag du launcher. Sa surcharge est appliquée en dernier, après chaque bundle et après votre propre patch de profil. Une surcharge de test est donc toujours prioritaire, ce qui est exactement ce qu’il faut pendant les itérations.

Écrire l’outil le plus simple qui soit utile

Une ligne de journal prouve que le plugin est chargé. Un outil prouve que le plugin fait partie de l’agent.

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

export const inject = ['tools'] est la ligne souvent oubliée. Les entrées d’une configuration Cordis démarrent en parallèle. La position d’une ligne dans le fichier ne garantit donc aucun ordre de chargement. Cet ordre vient des dépendances déclarées. inject indique à Cordis d’attendre que ctx.tools existe avant d’appeler votre apply. Sans cette dépendance, votre code peut s’exécuter alors que le registre n’est pas encore disponible pour l’enregistrement.

Le reste de l’objet définit le contrat présenté au modèle. parameters est le schéma des arguments, et execute reçoit les arguments déjà validés selon ce schéma. output.schema décrit la valeur renvoyée par execute, tandis que render convertit cette valeur en blocs de contenu lus par le modèle. Séparer ces deux éléments permet à l’interface d’afficher une chose alors que le modèle en lit une autre.

Démarrez le profil et demandez à l’assistant de saluer une personne par son nom. La réponse revient via votre execute. L’enregistrement avec ctx est réversible : la suppression du plugin désenregistre donc automatiquement l’outil. Pour les ressources que Cordis ne peut pas connaître, comme un socket ou un descripteur de fichier, appelez ctx.effect() et transmettez-lui une fonction de nettoyage.

Les deux points d’extension qu’un premier plugin utilise vraiment

La liste complète des points d’extension est longue. Deux d’entre eux couvrent presque tous les premiers plugins.

Les événements de conversation constituent le flux durable et journalisé. Leurs noms sont session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call et tool/result. Vous leur associez un listener ordinaire.

ctx.on('tool/call', (payload) => {
  console.log('[my-plugin] tool/call', JSON.stringify(payload))
})

Affichez le payload une fois, puis lisez-le. Ne recopiez pas les noms de champs du payload depuis un guide, y compris celui-ci, car la structure du payload est la partie d’une preview API qui évolue le plus.

Le deuxième point d’extension est le waterfall. agent/pre-step, agent/request, agent/request-error, llm/stream et les événements tools/* sont des waterfalls, et un listener de waterfall utilise une signature différente. Il reçoit un callback next, et la chaîne continue uniquement s’il l’appelle.

ctx.on('agent/request', async (payload, next) => {
  const startedAt = Date.now()
  const downstream = await next()
  console.log('[my-plugin] model request took', Date.now() - startedAt, 'ms')
  return downstream
})

Si vous oubliez await next(), vous n’avez pas ajouté de hook. Vous avez remplacé l’appel au modèle par rien, et l’agent s’arrête à ce stade, car le court-circuit est le comportement prévu pour un plugin de gateway qui refuse volontairement une requête. Cette différence est à l’origine de la plupart des confusions avec un premier plugin. Écrivez l’appel next() avant d’écrire quoi que ce soit autour.

agent/request encapsule l’appel au modèle lui-même. Son payload contient l’agent qui effectue l’appel, le numéro du tour ouvert, l’étape à laquelle la requête appartient et le signal d’abandon de ce tour. C’est donc le point d’extension adapté à un logger de requêtes ou à un limiteur de débit. Les waterfalls tools/* ont la même structure un niveau plus bas. tools/pre-execute autorise, refuse ou demande une approbation avant l’envoi. tools/execute encapsule l’envoi. tools/post-execute peut remplacer ou bloquer le résultat normalisé. tools/result observe uniquement le résultat figé.

Empaquetez-le pour que d’autres personnes puissent l’installer

Un bundle est un package npm dont le champ package.json déclare un champ dsh.bundle qui pointe vers son fichier de patch. Cette déclaration est toute la différence entre un fichier temporaire et un élément installable.

{
  "name": "dsh-plugin-hello",
  "version": "0.1.0",
  "type": "module",
  "main": "lib/index.js",
  "files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
  "engines": { "node": "^22.19 || >=24", "dsh": ">=0.1.0-rc.6" },
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
  "keywords": ["dsh-plugin", "deepseek-harness"],
  "scripts": { "build": "tsdown", "prepare": "pnpm run build" },
  "exports": {
    ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
    "./cordis.patch.yml": "./cordis.patch.yml",
    "./package.json": "./package.json"
  }
}

Le cordis.patch.yml placé à côté est court.

- insert:
    - id: dsh-plugin-hello
      name: dsh-plugin-hello

La ligne name correspond au nom du package. Ces deux chaînes doivent donc correspondre. La ligne id est la cible utilisée par une couche ultérieure lorsqu’un utilisateur remplace votre configuration. Choisissez une valeur stable et ne la réutilisez jamais pour un autre plugin.

files doit lister cordis.patch.yml. Si vous l’omettez, l’archive tarball publiée contient un dsh.bundle.patch qui pointe vers un fichier jamais inclus dans le package. Le package s’installe alors, mais n’ajoute rien à l’arbre.

Installez-le dans un profile depuis le répertoire qui contient le dossier de votre plugin.

dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demo

dsh plugin --profile <name> transmet le reste de ses arguments à pnpm dans le répertoire de ce profile. add et remove se comportent donc comme avec pnpm. Désinstallez-le avec dsh plugin --profile demo remove dsh-plugin-hello. Les profiles web et headless sont créés à partir des templates fournis lors de leur première utilisation. Tout autre nom de profile doit être créé avec dsh plugin.

Pourquoi votre ligne est absente de l’arbre composé

La composition part d’une liste d’entrées vide et empile les couches dans un ordre fixe. D’abord, chaque bundle indiqué dans le dsh.profile.bundles du profil, dans l’ordre de la liste. Ensuite, le cordis.patch.yml propre au profil. Puis le $DSH_HOME/cordis.patch.yml. Enfin, tout overlay --patch fourni sur la ligne de commande. Les couches ajoutées ensuite remplacent les lignes précédentes portant le même id.

Les profils se trouvent sous $DSH_HOME/profiles/<name>. Un répertoire de profil contient un package.json qui porte le manifeste dsh.profile avec sa liste bundles ordonnée, ainsi que le fichier de patch de l’utilisateur. Les noms de bundles sont d’abord résolus depuis l’installation de dsh, puis depuis le node_modules du profil, où pnpm place un plugin externe à l’arborescence.

dsh --profile demo --dump-config affiche l’arbre entièrement composé sans démarrer quoi que ce soit. Cette sortie permet de distinguer les deux axes du diagnostic. Si l’id de votre ligne est absent, le problème vient de la composition : un nom ne se résout pas ou un fichier de patch n’a jamais été packagé. Si la ligne est présente mais que rien ne se produit, le problème vient de votre code. Commencez par répondre à cette question pour éviter la plupart des recherches à l’aveugle.

Où les erreurs de chargement apparaissent réellement

Une erreur levée dans apply est explicite. Le processus se termine avec cette exception et vous obtenez une trace de la pile qui pointe vers votre propre ligne.

Les échecs de résolution sont silencieux. Le loader signale via le logger de Cordis qu’il ne parvient pas à résoudre un module, au lieu de s’arrêter brutalement. Le tutoriel amont avertit que ces messages peuvent sembler absents au démarrage, car ils sont émis avant l’ajout des exporters de console. Une faute de frappe dans un chemin ressemble donc exactement à un plugin qui s’est chargé mais n’a rien fait. C’est pourquoi il est utile d’exécuter la vérification --dump-config ci-dessus avant de lire du code.

Conservez un console.log comme première instruction dans apply pendant le développement. Son absence vous indique quelle partie du problème est en cause, et vous pourrez le supprimer plus tard sans conséquence. Sur un serveur, exécutez le harness au premier plan pendant vos essais, plutôt que sous le contrôle d’un service manager. La sortie du loader s’affichera ainsi dans votre terminal, au lieu d’être écrite dans un journal que vous devrez consulter.

Itérer sans redémarrer tout l’environnement

La réponse honnête, pour la partie hôte aujourd’hui, est de redémarrer. Le bundle de l’application web est livré avec son mécanisme partagé de hot module reload désactivé. Le fichier indique qu’il sera réactivé une fois le cycle de rechargement testé. La chaîne de rechargement côté client est toujours montée, mais elle reste inactive tant qu’un watcher de rebuild ne réécrit pas les bundles client. Elle ne fait donc rien non plus pour votre partie Node.

Rendez le redémarrage rapide au lieu de chercher un rechargement qui n’existe pas encore. Conservez le plugin dans un seul fichier. Chargez-le avec --patch plutôt que de l’installer dans un profil. Ainsi, aucune étape de build ni étape pnpm ne s’intercale entre une modification et une exécution. Enregistrez tout via ctx afin qu’un redémarrage ne laisse ni outil en double ni listener obsolète. Encapsulez tout ce que vous allouez vous-même dans ctx.effect() avec un disposer réel. Le symptôme habituel d’un disposer manquant est que la deuxième exécution échoue parce que le premier processus détient encore le port.

Si vous développez avec un harness exécuté sur un serveur plutôt que sur votre ordinateur portable, rien de ce qui précède ne change. En revanche, le binding de la Web UI est important. Le binding loopback sur le port 3080 explique pourquoi la page ne s’ouvre pas automatiquement et comment y remédier.

La partie navigateur et le niveau de confiance à lui accorder

Ajoutez-la uniquement si votre plugin a besoin de sa propre interface. Elle se déclare dans le même champ dsh que le bundle.

{
  "dsh": {
    "client": {
      "platform": "web",
      "inject": [],
      "external": [],
      "immediately": false
    }
  },
  "exports": {
    ".": "./src/index.ts",
    "./client": "./src/client/apply.ts",
    "./package.json": "./package.json"
  }
}

"platform": "web" est obligatoire, et le scanner échoue si le package n’exporte pas ./client. La map d’exports fait donc partie du manifest, et n’est pas une simple commodité. Le point d’entrée client reçoit le Context de Cordis, élargi avec le type du runtime client. Chaque enregistrement s’effectue dans apply via ctx.slots.register. Les effets de bord au niveau du module ne sont pas autorisés à cet endroit.

import type { Context } from 'cordis'
import type { DshClientContext } from '@deepseek-ai/dsh-client-runtime'

export async function apply(ctx: Context & DshClientContext) {
  ctx.slots.register({ name: 'domain.entry.slot' }, MyComponent)
}

Deux détails sont importants avant de commencer. Dans le manifest client, inject sert à documenter les dépendances, et non à planifier leur activation : il enregistre les dépendances du package et ne contrôle pas l’ordre d’activation. external permet de déclarer les requêtes de modules qui ne font pas partie de la base minimale, afin qu’elles soient matérialisées avant que votre plugin ne les demande. C’est la partie qui évolue le plus rapidement dans cette preview. Consultez donc packages/client/AGENTS.md dans le dépôt du harness le jour où vous écrivez le code, et non le jour où vous lisez un guide à ce sujet.

Publier et indiquer ce que votre plugin touche

Ajouter le sujet dsh-plugin à un dépôt GitHub le fait apparaître dans la liste que les utilisateurs consultent lorsqu’ils recherchent des plugins. Cela engage la confiance de personnes que vous ne connaissez pas et crée des obligations. Ces obligations sont le miroir de ce que notre guide de vérification d’un plugin dsh avant son installation demande aux lecteurs de contrôler. Rédiger votre plugin en suivant cette checklist est donc le moyen le plus simple de la respecter.

  • Épinglez vos dépendances. Une plage de versions avec caret sur une dépendance transitive peut faire exécuter cette semaine un code différent par un package qui était sûr la semaine dernière. C’est précisément le mécanisme utilisé dans les attaques de supply chain npm contre un serveur.
  • Indiquez dans le manifest ce que vous touchez. Votre liste inject fournit un résumé honnête et lisible par machine des services du harness auxquels vous accédez. Un reviewer la lit en quelques secondes et se fait une opinion à partir de ces informations.
  • Aucun appel réseau silencieux. Si un outil appelle une API, indiquez l’hôte dans le README et rendez l’endpoint configurable. Les personnes qui auditent ces plugins retireront de la liste un plugin qui contacte un serveur sans l’avoir indiqué.
  • Limitez files au strict nécessaire. Publier un dossier de travail complet est le meilleur moyen qu’un fichier contenant des credentials soit envoyé au registry.
  • Fournissez aux installateurs git un script prepare qui se construit sans supposer la présence de dépendances réservées au développement. Indiquez aussi dans le README qu’ils doivent allowlister cette build dans leur pnpm-workspace.yaml de profil.
  • Datez le README en fonction de la release candidate que vous avez construite et testée. Les utilisateurs d’une preview API doivent savoir laquelle vous avez utilisée.

Pour voir à quoi ressemble un plugin terminé depuis l’extérieur, consultez les plugins dsh qui valent la peine d’être installés et observez ce que chaque README indique avant l’installation. Si vous avez écrit des extensions pour un autre agent, la structure des plugins Claude Code fournit un point de comparaison utile. Le harness vous remet un object graph actif et permet une registration réversible. Cela offre davantage de puissance qu’un manifest de fichiers et implique davantage de responsabilités.

FAQ

Dois-je publier sur npm pour écrire un plugin dsh ?

Non. Un chemin de système de fichiers dans un overlay cordis.yml, chargé avec dsh web --patch ./scratch-plugin/cordis.yml, suffit pour exécuter votre propre code dans le harness. Le chemin doit être absolu. Le packaging ne devient nécessaire que lorsqu’une autre personne installe le plugin. Même dans ce cas, vous pouvez installer un dossier local avec dsh plugin --profile demo add ./my-plugin pour tester la forme packagée sans utiliser de registry.

Pourquoi mon plugin se charge-t-il, mais l’outil n’apparaît-il jamais ?

Exécutez d’abord dsh --profile demo --dump-config. Si l’identifiant de votre ligne n’apparaît pas dans cette sortie, le plugin n’a jamais été monté. La cause vient alors de la composition, pas du code. Si la ligne est présente, vérifiez la présence de export const inject = ['tools']. Les entrées d’une configuration Cordis démarrent en parallèle. L’ordre des fichiers ne détermine donc pas l’ordre de chargement. Sans cette déclaration, Cordis n’attend pas le registre des outils. Votre apply peut alors s’exécuter avant que ctx.tools soit disponible pour y effectuer l’enregistrement.

Quelle est la différence entre cordis.yml et cordis.patch.yml ?

cordis.yml contient la liste complète des entrées. cordis.patch.yml est une couche appliquée par-dessus cette liste. Elle cible les lignes par leur identifiant afin d’en insérer de nouvelles ou de remplacer une configuration existante. Un bundle référence son propre fichier de patch via dsh.bundle.patch dans package.json. Les couches sont appliquées dans un ordre fixe : tous les bundles dans l’ordre indiqué par le profil, puis le fichier de patch du profil, puis $DSH_HOME/cordis.patch.yml, puis tout overlay --patch. Les couches appliquées en dernier sont prioritaires.

Puis-je recharger à chaud un plugin dsh pendant que l’agent s’exécute ?

Pas pour la partie hôte dans le profil web, à partir de 0.1.0-rc.7. Ce bundle fournit la ligne partagée de rechargement à chaud des modules désactivée. Une note dans le fichier indique qu’elle sera réactivée une fois son cycle de rechargement testé. Concevez plutôt le plugin pour permettre un redémarrage rapide : un seul fichier, chargé via --patch sans étape de build, et chaque enregistrement effectué via ctx afin qu’aucune ressource ne fuie d’une exécution à la suivante. Utilisez ctx.effect() avec un disposer pour les ressources que Cordis ne peut pas nettoyer seul.