SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-21

DeepSeek Harness用dshプラグインの作り方

空のフォルダーからdshプラグインを作成します。package.jsonの重要な項目、マウントするpatchファイル、実際に動くtool、必要な2つのhookを確認できます。

dsh プラグインとは

dsh プラグインは、apply 関数をエクスポートし、DeepSeek Harness に読み込ませるための小さな YAML ファイルを 1 つ含む npm パッケージです。先に学ぶべき専用のプラグイン SDK はありません。dsh は Cordis アプリケーションであり、「すべてがプラグイン」という説明は文字どおりです。ツールレジストリ、エージェントループ、セッションストア、Web サーバーはすべて同じプラグインツリーに属する要素であり、あなたのパッケージもそこに加わります。

Cordis は汎用の構成フレームワークです。独立して開発され、Koishi chatbot framework の基盤として長年使われています。プラグインの読み込みとアンロードを処理し、プラグイン間の依存関係を解決します。エージェントについては何も知りません。エージェントに関する機能はすべて、その上に組み合わせられた harness パッケージから提供されます。そのため、以下のプラグイン構造は非常に小さく見えます。利用できる機能の大部分は継承されます。

プラグインには 2 つの部分があります。ホスト側は Node で実行され、ツールとイベントリスナーを登録し、独自のサービスも提供できます。ブラウザー側は Web UI 内で実行され、インターフェーススロットを登録します。最初のプラグインはほぼ常にホスト側だけで構成します。ブラウザー側は必要になるまで任意のものとして扱ってください。

このガイドは、2026 年 8 月 19 日時点の @deepseek-ai/dsh version 0.1.0-rc.7、つまり npm の latest tag を基に作成しています。dsh は developer preview であり、README 自身にも互換性を壊す変更が入ると記載されています。以下の各キー名は、その時点の上流ドキュメントとリポジトリで確認したものです。preview API では release candidate 間でフィールド名が変更されるため、依存する前にもう一度確認してください。harness がまだ実行されていない場合は、まず VPS 上の DeepSeek Harnessdsh API key と model configuration を設定してから、ここに戻ってきてください。

パッケージ化する前に、まず一時ファイルを1つ読み込む

最初にパッケージ化すると、理解するまでに時間がかかります。最初にファイルを1つ読み込み、ランタイムがコードを呼び出すことを確認してから、パッケージ化します。

harness のチェックアウト外にフォルダーを作成し、その中にファイルを1つ置きます。

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 は、診断時にプラグインを識別するためのメタデータです。apply は契約全体です。Cordis はこれを1回呼び出し、プラグイン用にスコープされたコンテキストを渡します。そのコンテキストに登録した内容は、プラグインが破棄されるときに自動的に解除されます。

その隣に cordis.yml を記述します。

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

次に、そのファイルを上に重ねたプロファイルを起動します。

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

dsh が PATH にない場合は、npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml でも同じことができます。この npx 経由では、このガイドが説明するバージョンではなく、キャッシュされた古いリリース候補が渡されることがあります。そのため、harness が文書化されたフラグを明確に拒否する場合は、自分のファイルを疑う前に、dsh のインストールとバージョンエラーの対処法を確認してください。dsh を起動したターミナルに [hello-plugin] plugin loaded と表示されるはずです。何も表示されない場合、その行は解決されていません。

name フィールドには、npm パッケージ名またはファイルシステムパスを指定します。上流のドキュメントでは、パスは絶対パスでなければならないと説明されています。一時プラグインが出力を生成しない場合、最初に確認するのは相対パスの ./hello.ts です。次に確認するのはファイル拡張子です。文書化されたループは、harness リポジトリのクローンから pnpm dsh web --patch ... として実行されます。この場合、TypeScript のエントリは tsx 経由で読み込まれます。dsh を npm から入手した場合は、行の参照先を通常の JavaScript にするか、先にファイルをビルドしてください。

--patch はランチャーのフラグです。そのオーバーレイは、すべてのバンドルと独自のプロファイルパッチの後に適用されます。そのため、一時オーバーレイは常に優先されます。反復して調整する間は、これが望ましい動作です。

役に立つ最小のツールを作成する

ログ行が出れば、プラグインが読み込まれたことは確認できます。ツールが動作すれば、プラグインがエージェントの一部になったことを確認できます。

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'] は省略されやすい行です。Cordis の設定内のエントリは同時に起動するため、ファイル内の行位置から読み込み順序は決まりません。順序は宣言した依存関係によって決まります。inject によって、Cordis は ctx.tools が存在するまで待機してから apply を呼び出します。これがないと、登録先となるレジストリがまだ存在しない時点でコードが実行される可能性があります。

オブジェクトの残りの部分は、モデルに見える契約を定義します。parameters は引数のスキーマで、execute にはそのスキーマに基づいて解析済みの引数が渡されます。output.schemaexecute が返す値を説明し、render はその値をモデルが読み取るコンテンツブロックに変換します。この2つを分離することで、インターフェースには一方を表示しながら、モデルにはもう一方を読み取らせることができます。

プロファイルを起動し、アシスタントに名前で誰かへ挨拶するよう依頼します。応答は execute を通じて返されます。ctx による登録は可逆的であるため、プラグインを破棄するとツールの登録も自動的に解除されます。ソケットやファイルハンドルなど、Cordis が管理できないリソースについては、ctx.effect() を呼び出して disposer を渡します。

最初のプラグインが実際に触れる2つの拡張ポイント

拡張ポイントの全体像は長くなります。最初のプラグインでは、そのほとんどを2つでカバーできます。

Conversation events は、永続化されるログ付きのストリームです。名前は session/eventturn/startturn/endstep/startstep/enduser/messageassistant/messageassistant/chunktool/calltool/result です。通常の listener をアタッチします。

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

まず payload を1回出力して読み取ります。このガイドも含め、どのガイドからも payload のフィールド名をコピーしないでください。preview API では、payload の形が最も頻繁に変わる部分だからです。

2つ目の拡張ポイントは waterfall です。agent/pre-stepagent/requestagent/request-errorllm/streamtools/* events は waterfall であり、waterfall listener には異なるシグネチャがあります。listener は next callback を受け取り、callback を呼び出した場合にだけチェーンが続きます。

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
})

await next() を忘れると、hook を追加したことにはなりません。model call を何も返さない処理に置き換えたことになり、agent はそこで停止します。これは、リクエストを意図的に拒否する gateway plugin では、short circuit が設計どおりの動作だからです。この違いが、最初のプラグインで多くの混乱を招きます。周囲の処理を書く前に、next() call を記述してください。

agent/request は model call 自体をラップします。その payload には、call を実行する agent、現在開いている turn の番号、リクエストが属する step、そしてその turn の abort signal が含まれます。そのため、request logger や rate limiter に適した接点です。tools/* waterfalls は、1つ下の層で同じ形を取ります。tools/pre-execute は dispatch の前に許可、拒否、または承認要求を行います。tools/execute は dispatch をラップします。tools/post-execute は正規化された結果を置き換えるか、ブロックできます。tools/result は確定した結果を監視するだけです。

他のユーザーがインストールできるバンドルとしてパッケージ化する

バンドルは、package.json にパッチファイルを指す dsh.bundle フィールドを定義した npm package です。この定義が、作業用ファイルとインストール可能な package の唯一の違いです。

{
  "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"
  }
}

その横にある cordis.patch.yml は短い内容です。

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

name の行は package 名なので、これら2つの文字列は一致している必要があります。id の行は、ユーザーが設定を上書きするときに後続のレイヤーが対象にする値です。安定した値を選び、別の plugin で再利用しないでください。

files には cordis.patch.yml を含める必要があります。省略すると、公開した tarball には、パッケージに含まれていないファイルを指す dsh.bundle.patch が入ります。そのため、package はインストールされてもツリーに何も追加しません。

plugin フォルダーを含むディレクトリから、profile にインストールします。

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

dsh plugin --profile <name> は、残りの引数をその profile ディレクトリ内の pnpm に渡します。そのため、addremove は pnpm と同じように動作します。dsh plugin --profile demo remove dsh-plugin-hello でアンインストールします。webheadless の profile は、初回の使用時に同梱されたテンプレートから自動的に作成されます。その他の profile 名は dsh plugin で作成する必要があります。

合成ツリーに行が表示されない理由

合成は、空のエントリ一覧から始まり、固定された順序でレイヤーを積み重ねます。まず、プロファイルの dsh.profile.bundles に記載された各バンドルを、記載順に適用します。次に、プロファイル自身の cordis.patch.yml を適用します。その後に $DSH_HOME/cordis.patch.yml を適用し、最後にコマンドラインから指定した --patch オーバーレイを適用します。後のレイヤーにある行は、id が同じ先行レイヤーの行を置き換えます。

プロファイルは $DSH_HOME/profiles/<name> 以下に配置されます。プロファイルディレクトリには、順序付きの bundles リストを含む dsh.profile マニフェストを格納した package.json と、ユーザー独自のパッチファイルがあります。バンドル名は、まず dsh のインストール先から解決され、次にプロファイルの node_modules から解決されます。pnpm がツリー外のプラグインを配置するのはこの場所です。

dsh --profile demo --dump-config は、何も起動せずに完全に合成されたツリーを出力します。この出力が、デバッグ時の切り分けの基準になります。行の id が存在しない場合、問題は合成にあります。解決できない名前か、パッチファイルがパッケージ化されていないことが原因です。行が存在するのに何も起きない場合、問題はコードにあります。最初にこの点を確認すれば、推測による調査の大半を省けます。

実際に読み込みエラーが現れる場所

apply 内で発生したエラーは明確に現れます。プロセスはその例外によって終了し、自分のコード行を指すスタックトレースが表示されます。

解決に失敗しても、エラーは目立ちません。loader は解決できないモジュールをクラッシュさせず、Cordis logger を通じて報告します。上流のチュートリアルでは、起動時にこれらのメッセージが失われたように見える場合があると説明しています。console exporter が接続される前に出力されるためです。そのため、パスの誤りは、読み込まれたものの何もしない plugin とまったく同じように見えます。コードを読む前に、上記の --dump-config check を実行する価値があるのはこのためです。

開発中は、apply の最初の文として console.log を記述しておきます。これがない場合、問題がどちら側にあるかを判断できます。後で削除してもコストはかかりません。サーバーでは、試行錯誤の間は service manager の下ではなく harness を foreground で実行してください。そうすれば、loader の出力が確認しに行く必要のある journal ではなく、端末に表示されます。

再起動を伴わずに反復する

現時点で、ホスト側についての正直な答えは再起動です。Web アプリケーションバンドルでは、共有 hot module reload の仕組みが無効になっています。また、その仕組みは reload のライフサイクルをテストした後に再度有効化すると記載されています。クライアント側の reload チェーンは常にマウントされていますが、rebuild watcher がクライアントバンドルを書き換えるまで待機します。そのため、Node 側にも作用しません。

まだ利用できない reload を追うのではなく、再起動にかかるコストを下げてください。プラグインは 1 つのファイルにまとめます。プロファイルにインストールするのではなく、--patch で読み込みます。これにより、編集から実行までの間に build step や pnpm step が入りません。すべてを ctx 経由で登録すれば、再起動時に重複した tool や古い listener が残りません。自分で確保したものは、実際の disposer を指定した ctx.effect() で囲んでください。disposer がない場合、通常は 2 回目の実行が失敗します。1 回目の実行がポートを保持したままになるためです。

ラップトップではなくサーバー上で実行する harness に対して開発する場合も、上記は変わりません。ただし、Web UI の bind 設定は重要です。ポート 3080 の loopback bind では、ページが自動的に開かない理由と対処方法を説明しています。

ブラウザー側と、信頼できる範囲

プラグイン独自のインターフェースが必要な場合にのみ追加します。これはバンドルと同じ dsh フィールドで宣言します。

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

"platform": "web" は必須です。パッケージに ./client エクスポートがない場合、スキャナーがエラーをスローします。そのため、エクスポートマップは単なる利便機能ではなく、マニフェストの一部です。クライアントエントリには、クライアントランタイム型で拡張された Cordis の Context が渡されます。すべての登録は apply 内で ctx.slots.register を介して行います。そこでモジュールレベルの副作用は許可されません。

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)
}

開始する前に、2 つの点を理解しておく必要があります。クライアントマニフェストの inject はスケジューリングではなくドキュメントです。パッケージレベルの依存関係エッジを記録しますが、有効化の順序は制御しません。external では、ベースライン外のモジュール要求を宣言します。これにより、プラグインが要求する前にそれらが実体化されます。ここはプレビューで最も変更の速い部分です。そのため、コードを書く当日に、ガイドを読んだ日ではなく、ハーネスリポジトリの packages/client/AGENTS.md を確認してください。

公開し、プラグインが変更する範囲を明示する

GitHub リポジトリに dsh-plugin トピックを追加すると、利用者がプラグインを探すときに閲覧する一覧に表示されます。これは見知らぬ利用者からの信頼を得る行為であり、責任を伴います。その責任は、インストール前に dsh プラグインを審査するためのガイドで読者に確認を促している内容の裏返しです。そのチェックリストに沿って記述することが、要件を満たす最も簡単な方法です。

  • 依存関係を固定します。推移的依存関係に caret 付きの範囲指定を使うと、先週は安全だったパッケージが今週は異なるコードを実行する可能性があります。これは、サーバーを狙う npm サプライチェーン攻撃の正確な仕組みです。
  • マニフェストに変更対象を記載します。inject の一覧は、どの harness サービスを利用するかを正直かつ機械可読にまとめたものです。レビュー担当者は数秒で確認し、その内容から評価します。
  • ネットワーク呼び出しを黙って行わないでください。ツールが API を呼び出す場合は、README にホスト名を記載し、エンドポイントを設定可能にします。記載のないサーバーへ接続するプラグインは、こうした点を監査する担当者によって一覧から削除されます。
  • files を最小限に保ちます。作業フォルダー全体を公開すると、残っていた認証情報ファイルがレジストリに到達する可能性があります。
  • git インストーラー向けに、開発時専用の前提に依存せずビルドできる prepare スクリプトを用意します。また、そのビルドをプロファイルの pnpm-workspace.yaml で allowlist に追加する必要があることを README に記載します。
  • ビルドおよびテストしたリリース候補に合わせて README に日付を記載します。プレビュー API の利用者は、どのリリース候補を使用したかを把握する必要があります。

完成したプラグインが外部からどのように見えるかを確認するには、インストールする価値のある dsh プラグインを読み、インストール前に各 README が何を説明しているかに注目してください。別の agent 用の拡張機能を書いた経験がある場合は、Claude Code プラグインの構成方法が有用な比較対象になります。harness はライブなオブジェクトグラフと、元に戻せる登録機構を提供します。これはファイルのマニフェストより強力である一方、それに応じた責任も伴います。

FAQ

dsh プラグインを作成するには npm に公開する必要がありますか?

いいえ。cordis.yml overlay 内のファイルシステムパスを dsh web --patch ./scratch-plugin/cordis.yml で読み込めば、harness 内で独自のコードを実行できます。パスは絶対パスでなければなりません。パッケージ化が必要になるのは、他のユーザーがプラグインをインストールする場合だけです。その場合でも、dsh plugin --profile demo add ./my-plugin を使ってローカルフォルダーをインストールすれば、registry に接続せずにパッケージ化した形式をテストできます。

プラグインは読み込まれるのに、ツールが表示されないのはなぜですか?

まず dsh --profile demo --dump-config を実行します。その出力に row id がない場合、プラグインは mount されていません。原因はコードではなく composition にあります。row が存在する場合は、export const inject = ['tools'] を確認します。Cordis の設定内のエントリは同時に開始されるため、ファイルの順序で load order は決まりません。この宣言がないと Cordis は tool registry の準備を待たず、applyctx.tools を利用できないタイミングで実行され、登録できないことがあります。

cordis.yml と cordis.patch.yml の違いは何ですか?

cordis.yml は完全なエントリ一覧です。cordis.patch.yml はその上に適用するレイヤーで、id で row を指定して新しい row を挿入したり、既存の設定を置き換えたりします。bundle は package.json 内の dsh.bundle.patch を通じて独自の patch file を指定します。レイヤーは固定された順序で適用されます。profile に記載された順序ですべての bundle、次に profile の patch file、続いて $DSH_HOME/cordis.patch.yml、最後に任意の --patch overlay が適用されます。後のレイヤーが優先されます。

agent の実行中に dsh プラグインを hot reload できますか?

web profile の host 側については、0.1.0-rc.7 時点ではできません。この bundle では shared hot module reload row が無効になっており、reload lifecycle のテストが完了したら再び有効にするという注記がファイルにあります。代わりに、迅速な再起動を前提に設計してください。1 ファイルを --patch で build step なしに読み込み、すべての登録を ctx 経由で行うことで、実行間でリソースが残らないようにします。Cordis が自動的に cleanup できないリソースには、disposer を指定した ctx.effect() を使用します。