プラグイン:ディレクトリ約束と発見
責務
プラグイン (plugin) は OpenClaw の拡張パッケージ——ディレクトリ約束に従って配置された npm パッケージやソースディレクトリで,自身が提供するツール、スキル、チャネル (channel)、provider、コマンド、フック (hook)、RPC メソッドを宣言します。システムは起動時に複数のルートディレクトリを走査して候補プラグインを発見 (discovery) し,読み込み (loading) 後に PluginRegistry にまとめます。以降のメインループ、ゲートウェイ、agent session はすべてこの登録表から拡張能力を取り出します。
プラグインシステム自体はビジネスを「実行」せず,3 件事だけを行います:走査で複数のルートから候補を探し,解析でマニフェスト (manifest) と bundle 形式を読み込み,登録で統一 registry にまとめます。本当に仕事をするのはプラグインが登録したツール、provider、チャネル——これらの能力は PluginRegistry に詰め込まれた後,他のソース(ビルトイン、SDK カスタム)の能力と対等になります。
OpenClaw は複数の bundle 形式を同時にサポートします:.codex-plugin/plugin.json(Codex スタイル)、.claude-plugin/plugin.json(Claude スタイル)、.cursor-plugin/plugin.json(Cursor スタイル),およびネイティブの plugin.json。これにより OpenClaw は他エコシステムのプラグインパッケージを直接再利用でき,作者に再パッケージを強いません。
設計動機
なぜ明示的 import ではなくディレクトリ約束を使うのか?プラグインのソースが多様だからです:npm パッケージもあれば,git clone したソースディレクトリ,bundled された dist 産物,ユーザが ~/.openclaw/plugins/ に手動で置いたフォルダもあります。明示的 import はパスとモジュールシステムが就緒していることを要求しますが,ディレクトリ約束は「このディレクトリにマニフェストファイルがある」だけで識別できます。これにより発見 (discovery) 段階は純ファイルシステム操作になり,モジュールを読み込む必要がありません——モジュール読み込みはコストが高く(Node ESM 解析を走り,場合によっては tsx コンパイルが必要),識別と読み込みを分離するとコールドスタートを大幅に高速化できます。
bundle-manifest.ts(bundle-manifest.ts 相対パス定数:L21-L24) は 3 種の bundle 形式の相対パスを定義します:
/** Relative manifest path for Codex-style plugin bundles. */
export const CODEX_BUNDLE_MANIFEST_RELATIVE_PATH = ".codex-plugin/plugin.json";
export const CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH = ".claude-plugin/plugin.json";
export const CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH = ".cursor-plugin/plugin.json";これらはすべてディレクトリ内の隠しサブディレクトリ + plugin.json であることに注意——これによりプラグインパッケージ自身は普通の npm パッケージで,package.json、README.md、ソースを持ちつつ,.codex-plugin/plugin.json で OpenClaw が関心のある部分(skills/hooks/capabilities)を宣言できます。
BundlePluginManifest 型(src/plugins/bundle-manifest.ts:L27-L39) は複数形式を正規化します:
export type BundlePluginManifest = {
id: string;
name?: string;
description?: string;
version?: string;
skills: string[];
settingsFiles?: string[];
hooks: string[];
bundleFormat: PluginBundleFormat;
activation?: PluginManifestActivation;
capabilities: string[];
};bundleFormat フィールドがソース形式をマークし,skills / hooks / capabilities が OpenClaw が本当に気にする能力宣言——これらはプラグインルートからの相対パス配列です。この「正規化」により以降の loader はプラグインがどのエコシステム由来かを気にする必要がありません。
読み込み段階 loadOpenClawPlugins(loader.ts loadOpenClawPlugins:L1821-L1895) にはいくつかの重要設計があります:キャッシュ再利用(同じ cacheKey なら読み込み済み registry を直接返し,モジュールの繰り返し読み込みを回避)、アクティベーション状態分離(shouldActivate が前回のランタイム状態を空にするか制御,非アクティベーション読み込みは読むだけで副作用を書かない)、モジュールローダー遅延作成(すべてのプラグインが無効なら module loader を作らず,コールドスタートコストを節約)。
PluginRegistry(registry-types.ts PluginRegistry:L434-L484) は大きなオブジェクトで,各能力ごとに配列:
export type PluginRegistry = {
plugins: PluginRecord[];
tools: PluginToolRegistration[];
hooks: PluginHookRegistration[];
typedHooks: TypedPluginHookRegistration[];
channels: PluginChannelRegistration[];
channelSetups: PluginChannelSetupRegistration[];
providers: PluginProviderRegistration[];
modelCatalogProviders: PluginModelCatalogProviderRegistration[];
// ... 他にも十数個の provider 型と登録項
gatewayHandlers: GatewayRequestHandlers;
httpRoutes: PluginHttpRouteRegistration[];
cliRegistrars: PluginCliRegistration[];
commands: PluginCommandRegistration[];
// ...
diagnostics: PluginDiagnostic[];
};これがMap ではなく,能力別バケット配列オブジェクトであることに注意。理由は各能力の登録次元が異なるからです:ツールは name+executor,チャネルは channelId+pluginId,provider は api+providerId——無理に Map<string, T> にすると疑似 key をでっち上げる必要があります。配列+diagnostics により発見/読み込み過程で発生した警告やエラーをランタイムまで運べます,「読み込み失敗したら捨てる」ではなく。
主要ファイル
bundle-manifest.ts 3 種形式:L21-L24— Codex/Claude/Cursor bundle 相対パス定数。bundle-manifest.ts BundlePluginManifest:L27-L39— 正規化後の bundle マニフェスト型。discovery.ts discoverOpenClawPlugins:L1432-L1510— 発見メインエントリ,複数ルート + extraPaths を走査。discovery.ts workspace 約束:L1484-L1505— workspace 自動発見を extensions root に制限,ワークスペース全体の誤走査を回避。discovery.ts discoverFromPath:L1210-L1259— 単一パス発見:ファイル/ディレクトリ分岐処理。loader.ts PluginLoadOptions:L181-L232— 読み込みオプション全集,cache/onlyPluginIds/mode 等を含む。loader.ts loadOpenClawPlugins:L1821-L1903— 読み込みメインエントリ,キャッシュ再利用 + アクティベーション状態分離。registry-types.ts PluginRegistry:L434-L484— 登録表型,能力別バケット。registry-types.ts PluginToolRegistration:L76-L86— ツール登録項の例。discovery.ts bundled 別名拒否:L1459-L1470— extraPaths が bundled root を指すときは warn だけで重複走査しない。
データフロー
発見は複数のルートから始まります(discovery.ts discoverOpenClawPlugins:L1432-L1510):
export function discoverOpenClawPlugins(params: {
workspaceDir?: string;
extraPaths?: string[];
installRecords?: Record<string, PluginInstallRecord>;
ownershipUid?: number | null;
env?: NodeJS.ProcessEnv;
}): PluginDiscoveryResult {
const env = params.env ?? process.env;
const workspaceDir = normalizeOptionalString(params.workspaceDir);
const workspaceRoot = workspaceDir ? resolveUserPath(workspaceDir, env) : undefined;
const roots = resolvePluginSourceRoots({ workspaceDir: workspaceRoot, env });
// ...
for (const extraPath of extra) {
// ...
const bundledAlias = resolvePackagedBundledLoadPathAlias({
bundledRoot: roots.stock,
loadPath: resolveUserPath(trimmed, env),
});
if (bundledAlias) {
result.diagnostics.push({
level: "warn",
source: trimmed,
message: `ignored plugins.load.paths entry that points at OpenClaw's ${bundledAlias.kind} bundled plugin directory; remove this redundant path or run openclaw doctor --fix`,
});
continue;
}
discoverFromPath({ rawPath: trimmed, origin: "config", /* ... */ });
}bundled 別名検出に注意——ユーザが plugins.load.paths で OpenClaw ビルトイン bundled プラグインディレクトリを指した場合,warn だけで重複走査しません。そのディレクトリは本来走査範囲にあるからです。これは「同じプラグインを二重に読み込んで重名エラーになる」のを予防します。
workspace 自動発見には重要なコメントがあります(src/plugins/discovery.ts:L1489-L1505):
if (roots.workspace && workspaceRoot && !workspaceMatchesBundledRoot) {
// Keep workspace auto-discovery constrained to the OpenClaw extensions root.
// Recursively scanning the full workspace treats arbitrary project folders as
// plugin candidates and causes noisy "plugin manifest not found" validation failures.
discoverInDirectory({
dir: roots.workspace,
origin: "workspace",
// ...
});
}これはかつて踏んだ穴です:ワークスペース全体を再帰走査すると,普通の node_modules サブディレクトリやソースフォルダがプラグイン候補として扱われ,「plugin manifest not found」が報告されます——ノイズが真のエラーを埋没させます。だから workspace 発見は extensions root 内に制限されます。
読み込み段階(loader.ts キャッシュ+アクティベーション:L1862-L1903) はキャッシュ再利用 + アクティベーション状態分離を走ります:
const cacheEnabled = options.cache !== false && options.resolveRawConfigEnvVars !== true;
if (cacheEnabled) {
const cached = getReusableCachedPluginRegistry({
cacheKey,
onlyPluginIds,
runtimeSubagentMode,
options,
});
if (cached) {
if (shouldActivate) {
restoreRegisteredAgentHarnesses(cached.state.agentHarnesses);
restorePluginCommands(cached.state.commands ?? []);
// ... 一連のランタイム状態を復元
activatePluginRegistry(
cached.state.registry,
cached.cacheKey,
cached.runtimeSubagentMode,
options.workspaceDir,
);
}
return cached.state.registry;
}
}
pluginLoaderCacheState.beginLoad(cacheKey);
try {
if (shouldActivate) {
clearActivatedPluginRuntimeState();
}
const loadPluginModule = createPluginModuleLoader({
devSourceRoot,
pluginSdkResolution: options.pluginSdkResolution,
});キャッシュ再利用で復元するのは registry 本体だけでなく,agentHarnesses、commands、compactionProviders、interactiveHandlers、embeddingProviders などの多数のグローバル副作用——プラグイン読み込み時これらの副作用はグローバル表に登録されるため,再アクティベーション時に対応位置に復元しなければなりません。clearActivatedPluginRuntimeState は新規読み込み前に旧状態を空にし,前回登録したツール/コマンドが今回に「漏れる」のを避けます。
bundle マニフェスト解析は bundle-manifest.ts ファイル読み込み:L92-L110 です:readRootStructuredFileSync で root 境界内で JSON を読み,失敗は { ok: false, error, manifestPath } に転換しスローしません。
境界と失敗
- 3 種の bundle 形式が共存:
.codex-plugin/.claude-plugin/.cursor-pluginはすべて隠しサブディレクトリ +plugin.json(src/plugins/bundle-manifest.ts:L21-L24)。1 つのプラグインが複数形式を同時に宣言でき,loader は最初にマッチした形式を選んでBundlePluginManifestに正規化します。 - workspace 発見範囲は制限:自動発見は extensions root だけを走査し,ワークスペース全体を再帰しません(
src/plugins/discovery.ts:L1489-L1505)。さもなくば「plugin manifest not found」ノイズが爆発します。 - bundled ディレクトリは重複走査しない:ユーザが
plugins.load.pathsで bundled root を指すと warn だけで重複走査しません(src/plugins/discovery.ts:L1459-L1470)。openclaw doctor --fixの実行で冗長設定を自動クリーンすることを推奨。 - キャッシュ再利用で副作用を復元:キャッシュヒット時に registry を返すだけでなく,
restoreRegisteredAgentHarnesses/restorePluginCommands/restoreRegisteredEmbeddingProvidersなど十数個のグローバル復元を行います(src/plugins/loader.ts:L1872-L1885)。1 つでも漏れるとツール/コマンドが「見えなく」なります。 - アクティベーション状態分離:
shouldActivateが false のときは前回登録したランタイム状態を空にしません——これはスナップショット読み込み(validate モード / parallel discovery)のためで,他プラグインのコマンドを誤って空にするのを避けます。 - モジュール遅延読み込み:すべてのプラグインが無効のとき,
createPluginModuleLoaderは呼ばれません(src/plugins/loader.ts:L1904-L1908)——単体テストでよくあるシナリオで,コールドスタートコストを節約。 - diagnostics は捨てない:
PluginDiscoveryResultとPluginRegistryは両方ともdiagnostics: PluginDiagnostic[]配列を持ち,発見/読み込み過程の warn/error をランタイムまで運びます。openclaw doctorがこれらの diagnostics を読んで修正提案を出します。 - プラグインはツールではない:プラグインはツールを登録できますが,プラグイン自体はパッケージです。
PluginRegistry.toolsはツール配列で,各PluginToolRegistrationはプラグイン ID + ツール定義を参照します——これらのツールは最終的に ツールシステム の Map に合流します。同様にプラグインはskills/SKILL.mdフィールドを携帯できます(スキル 参照)が,プラグイン自体はディレクトリ約束で発見される拡張パッケージで,スキル自体ではありません。
まとめ
プラグインはディレクトリ約束で発見される拡張パッケージです:.codex-plugin/plugin.json などの隠しサブディレクトリが能力を宣言し,discovery が複数ルート + extraPaths を走査して候補を探し,loader が PluginRegistry に正規化して能力別バケットにまとめます。キャッシュ再利用で十数個のグローバル副作用を復元し,アクティベーション状態分離でスナップショット読み込みが前回のランタイム状態を空にしないようにします。3 種の bundle 形式で OpenClaw は Codex/Claude/Cursor エコシステムのプラグインを直接取り込めます。ツール との違い:ツールは Map 内の関数,プラグインはツールを産出するソースの一つ。スキル との違い:スキルは Markdown ガイド,プラグインはコンテンツとしてスキルを携帯できます。チャネル登録と RPC メソッド登録はどちらも チャネル登録表 の経路を走ります。
公式資料:Plugins ドキュメント · README。