第 5 課:設定と公開:設定可能、配布可能
一言でいうと:プラグインが自分用に動くだけでは終わりではありません。「デプロイごとに異なる可能性がある」パラメータをすべて設定可能なスキーマとして宣言し、プラグインをインストール可能なバンドルにパッケージ化して公開すれば、他の人は1 コマンドでインストールし、設定で必要に応じて調整できるようになります。この課で「設定可能、配布可能」を学び終えれば、あなたのプラグインは正式に「一人前」です。
1. ユーザーストーリー:「自分用」から「使えて、変えられる」へ
D くんは前の 3 課で学んだスキルを使って「リポジトリ要約」プラグインを書きました。リポジトリのパスを渡すと、agent が自動的に README を読み、コード量を集計し、要約を生成します。自分のマシンでは快調に動いていました。
金曜の午後、同僚の H くんがやってきました。「その要約プラグイン、すごく便利だね。俺にもインストールしてよ!」
D くんはすぐに 3 つの問題に気づきました。
- H くんは使っているモデルもタイムアウト時間も違います——しかし
TIMEOUT = 30000はソースコードにハードコードされていて、変更できません。 - ソースフォルダ全体をコピーして渡して、その後バグを直すたびに手動で同期し直すわけにはいきません。
- H くんは自分でパラメータを調整したいが、コアロジックは絶対に壊したくありません。
従来のフレームワークの答えは「ソースをコピーしてコードを書き換える」——フォークして、パラメータを書き換え、それぞれが勝手に直し、アップグレード時には苦労してマージする、というものです。DSH の答えは 2 つの言葉です:設定可能と配布可能。
- 設定可能:「デプロイごとに異なる値が必要になる可能性がある」パラメータをすべて設定フィールドとして宣言し、ユーザーが設定で値を渡します——H くんがタイムアウトを変えたければ、設定を書き換えるだけで、コードには触れません(第 2 節)。
- 配布可能:プラグインを標準的なバンドルとしてパッケージ化して公開し、他の人が 1 コマンドでインストールし、設定で組み立てられるようにします(第 3、4 節)。
🎁 たとえ話:前の課であなたが作ったのは「使いやすいドライバー 1 本」です。この課で作るのは「そのドライバーを標準の工具箱に入れられて、買った人がグリップを交換できる」ものです——設定がグリップ、公開が箱詰めです。
2. プラグインに設定を追加する:スキーマ、デフォルト値、組み立て
Config 型を定義し、デフォルト値はスキーマに書く
Cordis の約束事はこうです。プラグインから Config 型をエクスポートし、さらに同名の Schemastery スキーマを用意します——デフォルト値はスキーマに直接書きます(出典:docs/user/develop/basic/config.zh.md):
import type { Context } from 'cordis'
import Schema from 'schemastery'
export const name = 'my-plugin'
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // User value or schema default.
}
部分ごとに読み解きましょう。
export interface Config:プラグインが必要とする設定がどんな形かを宣言します——TypeScript の型なので、コードを書くときに補完とヒントが効きます。export const Config = Schema.object({ ... }):同名のスキーマで、各フィールドの型を記述すると同時にデフォルト値(.default(...))も与えます。apply(ctx, config):第 2 引数が組み立て後の設定です——ユーザーが値を渡せばその値を、渡さなければスキーマのデフォルト値を使います。
⚠️ 普通のオブジェクトを
Configとしてエクスポートしないでください。Cordis が要求する Standard Schema インターフェースを満たさず、プラグインが検証できません。型とスキーマを同名にするのは Cordis の約束事です。2 つの名前に分けないでください。
組み立て時に config を渡す
設定はどこに書くのでしょうか? あのおなじみの cordis.yml です。プラグインエントリに config キーを追加します(出典:docs/user/develop/basic/config.zh.md):
- insert:
- id: hello
name: './src/my-plugin.ts'
config:
greeting: 'Hi there'
maxRetries: 5
プラグインの読み込み時に、Cordis はエクスポートされたスキーマでこの設定を検証し、提供されなかったフィールドにデフォルト値を補填します——ここでは verbose が書かれていないので false になります。検証に通らなかったら? 「設定エラーは明確に」:スキーマはプラグイン読み込み時に検証を実行し、設定が不正ならプラグインは読み込みに失敗し、明確なエラーメッセージを出します。壊れた設定のまま動き続けることはありません。
config 変更 → 増分リロード
ユーザーが設定を変更した後は? プログラム全体を再起動する必要はありません。ドキュメントの原文です(出典:docs/user/develop/basic/config.zh.md「HMR との連携」):
設定の変更はプラグインのホットスワップをトリガーします。
cordis.yml内のあるプラグインのconfigを変更すると、フレームワークは古いインスタンスをアンロードして新しいインスタンスを読み込みます。登録はすべて effect であり自動的にクリーンアップされるため、置き換え後に古いインスタンスの登録が残ることはありません。
これこそ第 2 章の論文にある「時間的合成可能性」の現場です。古いインスタンスのアンロード = そのエフェクトをすべてロールバック、新しいインスタンスの読み込み = 再登録。変更されたプラグインだけがこの再組み立てを経験し、他のプラグインには一切影響しません——これが図に書かれた「増分リロード」です。フィールド単位で調整し、動かすべきものだけを動かします。
2 つの設計原則
公式ドキュメントより(出典:docs/user/develop/basic/config.zh.md「設計原則」):
- ハードコードされた調整可能パラメータを持たない:デプロイごとに異なる値が必要になる可能性があるパラメータは、すべて設定フィールドとして定義しなければなりません。検証基準はたった 1 文です——コードを変更せずに
cordis.ymlでこの値を変えられるか? できなければ、設定に引き上げます。 - 設定エラーは明確に:スキーマで自己完結した制約を表現し、無効な設定はプラグイン読み込み時に失敗させます。間違った値でこっそり動き始めることのないように。
3. 公開:プラグインをインストール可能な「バンドル」にする
プラグインが設定可能になりました。他の人にインストールしてもらうには? まず 2 つの概念を区別しましょう(出典:docs/user/develop/basic/publish.zh.md):
- バンドル(bundle):設定レイヤーを同梱した npm パッケージ。マニフェストは
dsh.bundleを宣言し、「このパッケージは何を提供するか?」に答えます——プラグイン行を挿入または上書きする patch ファイル 1 枚です。 - profile:
$DSH_HOME/profiles/name配下にある、起動可能な組み立てを記述するディレクトリ。マニフェストはdsh.profileを宣言し、「この構成はどのバンドルをどんな順序で組み合わせたものか?」に答えます。
1 文で覚えてください。バンドルはあなたが書いて配布するもの、profile はユーザーが起動するもの。両方を兼ねるものは存在しません。
パッケージ構造:3 点セット
バンドルは通常こんな形です(出典:docs/user/develop/basic/publish.zh.md):
hello-plugin/
├── package.json # declares dsh.bundle
├── cordis.patch.yml # the layer applied when a profile lists this bundle
└── index.js # plugin modules the patch rows reference
この package.json は dsh.bundle で自分がバンドルであることを宣言します。
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
patch ファイルの形は、以前書いた --patch overlay と同じです——patch エントリの YAML 配列です——ただしプラグイン行は相対ソースパスではなくパッケージ名でこのパッケージを参照します。こうすれば Node のモジュール解決でインストール済みのコードが見つかります(出典:docs/user/develop/basic/publish.zh.md):
- insert:
- id: hello
name: dsh-hello-plugin
ビルドと 3 つの配布経路
公開前にビルドします。build スクリプト(tsdown など)が TypeScript を lib/ 成果物にコンパイルします。配布経路は 3 つあります(出典:docs/user/develop/basic/publish.zh.md):
| 経路 | コマンド | ユーザーが受け取るもの |
|---|---|---|
| npm に公開 | pnpm publish(公開時に lib/ をビルド済みにする) | プリビルド済みコード。dsh plugin add your-package で直接インストール |
| tarball を渡す | pnpm pack | hello-plugin-0.1.0.tgz ファイル 1 個 |
| GitHub から直接インストール | git リポジトリにプッシュ | ソースコード——下記の落とし穴を参照 |
⚠️ git インストールという関門:git インストールが取得するのはソースコードであり、ビルド成果物ではありません——どの段階でもあなたの
buildスクリプトは実行されないため、TypeScript パッケージはlib/出力なしで届き、読み込みに失敗します。そこで両側がそれぞれ 1 つずつやるべきことがあります(出典:docs/user/develop/basic/publish.zh.md):
- 作者:
prepareスクリプトを提供する——pnpm は git インストール後にこれを実行してソースから公開エントリをビルドします。これは自己完結していなければなりません(開発環境にしか存在しないコンテキスト、例えば隣に monorepo のチェックアウトがあることなどを前提にしてはいけません)。- ユーザー:ビルドを承認する。pnpm ≥ 10 は明示的に許可されるまで git 依存の
prepareスクリプトの実行を拒否するため、最初のaddは失敗します。dshが直し方を教えてくれます——pnpm が表示した正確なパッケージキーを、その profile のpnpm-workspace.yamlにコピーします:
allowBuilds:
dsh-hello-plugin: true
この承認は正確に受け止めてください。これは、そのパッケージのコードがインストール時にあなたのマシン上で実行されることを許可するものであり、agent が動くどのサンドボックスの外でもありません。ソースを信頼できるパッケージにだけ承認を与え、コミットを固定してください(github:you/hello-plugin#sha)。そうすれば後のプッシュで実際に実行される内容がこっそり変わることはありません。ユーザーにこの承認をさせたくないですか? ならビルド成果物を配布しましょう——npm でも tarball でもビルド権限は一切不要です。
バージョン管理:配布可能であることの土台
package.json の version はセマンティックバージョニング(SemVer)です。0.1.0 = メジャー.マイナー.パッチ。アップグレードにはルールがあります——破壊的変更はメジャーを上げ、機能追加はマイナーを上げ、バグ修正はパッチを上げます。なぜこんなに重要なのでしょうか? バージョンは「発見と互換性」の礎だからです。これは第 4 節ですぐに扱います。誰かが 0.1.0 をインストールした後で、あなたがこっそりインターフェースを変えたら、結果はインターフェースドリフトです。
发布即分发:别人一条命令装上你的插件,配置变化自动协调
公開すれば配布完了です。他の人は 1 コマンドであなたのプラグインをインストールし、設定の変更は自動的に調整されます。
4. 他の人の使い方:インストール、組み立て、オンデマンド設定、そして命名と発見
1 コマンドで profile にインストール
相手はあなたのパッケージ(またはチェックアウト)を手に入れ、自分のマシンで次を実行します(出典:docs/user/develop/basic/publish.zh.md):
cd hello-plugin
dsh plugin --profile demo add .
このコマンドを分解してみましょう。
dsh plugin --profile demo add .は profile ディレクトリ内で pnpm に転送されるため、pnpm のすべてのサブコマンドが使えます。- 初回使用時には profile を初期化します——
@deepseek-ai/dsh-baseが最初のバンドルになります。 - あなたのパッケージは
dsh.bundleを宣言しているので、dshはそれをdsh.profile.bundlesに追加します:
{
"name": "dsh-profile-demo",
"private": true,
"dependencies": {
"dsh-hello-plugin": "link:/path/to/hello-plugin"
},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"dsh-hello-plugin"
]
}
}
}
起動せずにこのレイヤーだけを検証してから、起動します。
dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer
dsh --profile demo
外したいですか? dsh plugin --profile demo remove dsh-hello-plugin で依存と対応するレイヤーの両方が削除されます。
組み立てとオンデマンド設定:後のレイヤーが前のレイヤーを上書き
有効な設定は、空のルートの上に順番にレイヤーを重ねて構成されます(出典:docs/user/develop/basic/publish.zh.md「読み込み順」):
- profile の
dsh.profile.bundlesリストに列挙された各バンドルの patch。リスト順。 - profile 自身の
cordis.patch.yml。 - home レベルの
$DSH_HOME/cordis.patch.yml(各 profile で共有されるマシンローカルの好み)。 - 各
--patchoverlay。argv 順。 - ランチャーのフラグ patch(例:
dsh web --port)。
後に適用されたレイヤーが行単位で勝ちます。ここからバンドル作者には 2 つの帰結があります。
- あなたの patch は
idで前のレイヤーの行を上書きできますが、patch は対象行のconfig値全体を置き換え、キーの深いマージはしません——上書き時にはその行に必要なすべてのキーを書き直す必要があります。変更したいキーだけ書いてはいけません。 - ユーザーは自分の profile の
cordis.patch.ymlで、あなたのパッケージに触れることなくあなたの行を上書きできます——だから公開時には「ユーザーがそのまま使う可能性の高い設定のデフォルト値を先に提供し、残りはスキーマに任せる」のです。
言い換えると、あなたは使いやすいデフォルト値をスキーマに書き込み、選択権をユーザーの設定レイヤーに渡します——これが「設定可能、配布可能」が合体した姿です。
命名と発見:バージョン互換性とインターフェースドリフト(論文 5.5 に呼応)
プラグインが見つけられ、インストールされ、長く使われ続けるには、公開できるだけでは足りません。「発見」の関門も通らなければなりません。第 2 章の第 13 課で扱った論文第 5 章を覚えていますか? その 5.5 節は 2 つの落とし穴を特別に警告しています。
| 問題 | 何か | 結果 |
|---|---|---|
| インターフェースドリフト | 提供者が改版時にキー k に関連付けられたインターフェースを変更した(フィールド追加、メソッドシグネチャ変更、振る舞い契約の変更)のに、古いインターフェース向けにコンパイルされた消費者が同じキー k を宣言し続けている | 残余エフェクトのレベルでは依存は「満たされ」ているが、実行時の値が期待に合わない:型エラー、メソッドが見つからない、暗黙の振る舞いの乖離 |
| キー衝突 | 独立に開発された 2 つの提供者が、まったく無関係のインターフェースに同じキー名 k を使った | 消費者は互換性チェックなしに別の提供者の値を受け入れ、障害は予測不能で診断も困難 |
論文は 3 つの補救策を提示しています。キーの名前空間化(キーの同一性にインターフェースを定義するパッケージの識別子を含め、構造的にキー衝突をなくす)、ピア依存(Cordis が現在採用——ホスト言語のパッケージマネージャーでバージョン制約を宣言し、バージョン非互換をインストール時に発見する。実行時の障害に引きずり込まない。代償は、提供者がセマンティックバージョニングの約束を自主的に守ることに依存し、強制できないこと)、構造的互換性(インターフェースの構造が消費者の期待を包含するかで判断するが、振る舞い契約は複雑)。あなたの日常に落とし込むと:
- パッケージ名は一意に:npm に公開するときは名前空間をうまく使います(プラットフォームの慣例は
@deepseek-ai/dsh-*のようなプレフィックス)。 - バージョンのルールを守る:セマンティックバージョニングの約束に従い、こっそりインターフェースを変えない。破壊的変更はメジャーを上げ、変更履歴を書く。
- 新しい機能には新しいキーを:サービスやツールに登録キーを付けるときは既存プラグインのキー名を避け、「インターフェースドリフト」を公開の前に食い止める。
要点の振り返り
- 設定可能:プラグインは
Config型 + 同名スキーマをエクスポートし、デフォルト値はスキーマに書く。ユーザーはcordis.ymlのconfigキーで値を渡し、Cordis が検証してデフォルト値を補填する。「コードを変えずに設定でこの値を変えられるか」がハードコードの検証基準。 - 増分リロード:config の変更はプラグインのホットスワップをトリガーする——古いインスタンスをアンロードし、新しいインスタンスを読み込む。登録は effect なので自動的にクリーンアップされる。論文の「時間的合成可能性」に呼応。
- バンドルと profile:バンドル(
dsh.bundle)は作者が配布するもの、profile(dsh.profile)はユーザーが起動する組み立て。配布経路は npm、tarball、GitHub の 3 つで、git でソースをインストールするにはprepareスクリプト +allowBuilds承認が必要。 - インストールと組み立て:
dsh plugin --profile demo add .で profile にインストール。有効な設定はレイヤーで構成され、後のレイヤーが前のレイヤーを上書き。patch はconfigを行全体で置き換え、深いマージはしない。 - 命名と発見:パッケージ名の一意性、セマンティックバージョニング、インターフェースドリフトとキー衝突の回避——これは論文 5.5 節が公開者に渡す 3 枚の「違反切符免除カード」です。
🚀 次の課「実戦応用:LLM アダプターと自己言及ツール」では、現実世界のプラグインがどんな姿かを見ます——エージェントに新しいモデルプロバイダーを接続する方法、そして「自分自身を呼び出せる」プラグインがどんな体験か。
セルフテスト · 設定と公開
回答を終えたら「解答を送信」をクリックすると、正誤と解説を確認できます。
