0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【#4】 OpenClaw を読み解く — コアに能力を注ぐ、たった一つの窓口

0
Last updated at Posted at 2026-06-27

【#4】 OpenClaw を読み解く — コアに能力を注ぐ、たった一つの窓口

本記事のコード参照は OpenClaw maincee2aca409(version 2026.6.10)時点。行番号は更新でズレ得ます。

#01 で「コアはプラグイン非依存」という最重要原則を見ました。今回はそれを実際に成立させている仕組み——マニフェスト、ローダー、capability レジストリ、そして 300 を超えるサブパスエントリポイントを持つプラグイン SDK——を src/plugins/src/plugin-sdk/ から読み解きます。

すべては宣言から始まる — マニフェストという名乗り

各ネイティブプラグインのルートには openclaw.plugin.jsonPLUGIN_MANIFEST_FILENAME, src/plugins/manifest.ts:26)が必須です。中身を表す型 PluginManifestsrc/plugins/manifest.ts:297)が、プラグインが何を提供するかを宣言します。

export type PluginManifest = {
  id: string;
  configSchema: JsonSchemaObject;
  requiresPlugins?: string[];
  enabledByDefault?: boolean;
  channels?: string[];        // 例: ["telegram"]
  providers?: string[];       // 例: ["anthropic"]
  modelSupport?: PluginManifestModelSupport;
  contracts?: PluginManifestContracts;  // capability の静的所有スナップショット
  activation?: PluginManifestActivation;
  setup?: PluginManifestSetup;
  providerAuthChoices?: PluginManifestProviderAuthChoice[];
  // ... 20 以上のフィールド
};

特に効いているのが contractsmanifest.ts:406)です。

export type PluginManifestContracts = {
  embeddingProviders?: string[];
  speechProviders?: string[];
  imageGenerationProviders?: string[];
  webFetchProviders?: string[];
  webSearchProviders?: string[];
  tools?: string[];
  // ... さらに capability カテゴリが続く
};

これは「このプラグインは speechProviders として cloud-tts を持つ」といった能力の静的目録です。コアはこの目録を読むだけで「誰が何を提供できるか」を、プラグインのコードを実行せずに把握できます。

二つの面 — メタデータの面と、実行の面

src/plugins/AGENTS.md が要に据えるのが、control plane と runtime plane の分離です。

Keep control-plane and runtime-plane concerns separate: discovery, manifest parsing, config validation, setup/onboarding hints, and activation planning belong to the control plane; actual plugin execution belongs to runtime resolution.

ローダーの流れ(公開エントリ loadOpenClawPlugins(), src/plugins/loader.ts:1821)はこの2面で構成されます。

  1. Discovery scan(control plane) — プラグインのルートを探し、openclaw.plugin.jsonpackage.jsonコードを実行せずに読む。
  2. Manifest registry(control plane) — スキーマ・環境変数依存・認証メタデータを検証。
  3. Registry assembly(runtime) — ここで初めてプラグインのモジュールを読み込み、登録フックを呼び、capability レジストリを組み立てる。

「メタデータだけで動く部分」と「実行が必要な部分」を厳密に分けることで、openclaw --help のような軽い操作でプラグイン本体を読み込まずに済む——#02 の fast-path とまさに同じ思想です。

プラグインの出自も型で区別されます(PluginCandidate, src/plugins/discovery.ts:69)。originPluginOrigin, src/plugins/plugin-origin.types.ts:2)は 'bundled' | 'global' | 'workspace' | 'config'formatPluginFormat, src/plugins/manifest-types.ts:12)は 'openclaw' | 'bundle'。バンドル形式(PluginBundleFormat, manifest-types.ts:15'codex' | 'claude' | 'cursor')も識別され、コア配布物に同梱される内部プラグインと、外部プラグインを正しく扱い分けます。

コアが名前を知らずに済む理由 — 汎用レジストリの妙

src/plugins/registry.ts:407createPluginRegistry() が肝です。ポイントは、レジストリが capability 駆動であってプラグイン駆動ではないこと。

export function createPluginRegistry(registryParams: PluginRegistryParams) {
  const registry = createEmptyPluginRegistry();
  const { registerModelCatalogProvider, registerSpeechProvider, /* ... */ } =
    createModelCatalogRegistrationHandlers({ registry, pushDiagnostic });
  // コアは "openai" や "anthropic" を誰が所有するか一切知らない。
  // 汎用ハンドラを呼ぶだけ:
  return api.registerSpeechProvider({ id: "cloud-tts", ...speechProvider });
}

コードに "telegram""anthropic" という分岐は現れません。プラグインが汎用の registerSpeechProvider(...) のような capability 登録 API を呼び、コアは「cloud-tts という ID の speech provider が登録された」とだけ知る。だからプラグインを差し替え・無効化・上書きしても、コアのコードは一切変わりません。これが #01 の「No bundled ids/defaults/policy in core」の実装です。

さらに src/plugins/manifest-contract-runtime.ts:16resolveManifestContractRuntimePluginResolution() は、capability(例: speechProviders)にマッチするプラグイン ID を、ランタイムを起こさずにスナップショットから解決します。「まずメタデータで解く、実行は最後」の徹底ぶりがうかがえます。

なぜ入口は 300 を超えて細かいのか — 規律としての分割

SDK の入口はとにかく細かく分かれています。実測では scripts/lib/plugin-sdk-entrypoints.json のエントリポイント目録が 340 件、ルート package.jsonexports が 325 件、スタンドアロンの packages/plugin-sdk/package.jsonexports が 63 件。./plugin-sdk/reply-runtime, ./plugin-sdk/agent-runtime, ./plugin-sdk/ssrf-policy …と #01 で package.json を覗いたときの圧巻のリストです。これには明確な設計理由があります。src/plugin-sdk/AGENTS.md から。

Prefer a small versioned host/kernel seam plus narrow documented SDK entrypoints over broad convenience barrels. Keep public SDK entrypoints cheap at module load. If a helper is only needed on async paths such as send, monitor, probe, directory-live, login, or setup, prefer a narrow *.runtime subpath over re-exporting it through a broad SDK barrel that hot channel entrypoints import on startup.

噛み砕くと、「大きな便利バレル」を避け、用途別の細いエントリに割るということ。理由は3つです。

  1. 遅延ロード境界: チャネルの起動 hot path は ./core./channel-runtime だけを import すれば済み、重いポリシーモジュールを巻き込まない。
  2. プラグイン非依存の貫徹: config / auth / state など各ランタイムサービスを独立した契約にし、プラグインが必要な能力ひとつだけに依存できる。
  3. バンドルプラグインのファサード: ./discord./lmstudio のような一部サブパスはバンドルプラグインのモジュールへのエイリアスで、外部プラグインからは import できないようビルド時にガードされる(supportedBundledFacadeSdkEntrypoints)。

*.runtime という命名規約(例: time-runtime, text-runtime)が「非同期パスでのみ必要な重い API」を表し、./core が「全チャネルが起動時に eager import する軽い契約」を表す——この粒度設計が、巨大 SDK を「起動が遅くならない」状態に保っています。

越えてはならぬ一線 — 境界の鉄則

src/plugin-sdk/AGENTS.mdsrc/plugins/AGENTS.md から、プラグイン作者・コア開発者双方が守るべき鉄則を抜き出します。

  • ホストは内部に手を伸ばさせない: 「Host loads plugins; plugins should not reach through the SDK into arbitrary host internals.」src/channels/**, src/agents/**, src/plugins/** の実装都合を、意図的な公開契約でない限り SDK に晒さない。
  • エントリは module load 時に安いこと: 起動 hot path が重い import を引かないよう、send/monitor/probe/login/setup 用のヘルパは *.runtime サブパスへ。
  • 同一ランタイム面で static と dynamic import を混ぜない(#12 のビルド回で扱う [INEFFECTIVE_DYNAMIC_IMPORT] 検査に直結)。
  • plugin-owned を core-owned に滲ませない: 「plugins.entries.<id>.config を無関係なコア経路から直接読む」ことを禁止。汎用ヘルパ・プラグインランタイムフック・マニフェストメタデータを使う。

一枚に畳む — プラグインがコアに届くまでの道のり

openclaw.plugin.json (宣言)
   │  discovery scan ── コードを実行せず読む(control plane)
   ▼
manifest registry ── スキーマ/env/auth を検証(control plane)
   │  必要になって初めて…
   ▼
registry assembly ── プラグイン本体を読み込み register フック実行(runtime)
   │  汎用ハンドラ registerXxxProvider(id, impl) を呼ぶ
   ▼
capability registry ── コアは「ID と能力」だけを知る(plugin-agnostic)

まとめ — 無秩序ではなく、規律の産物

  • プラグインは openclaw.plugin.json能力を宣言し、コアはその目録(contracts)をコード実行前に読む。
  • ローダーは control plane(メタデータ)/ runtime plane(実行)を分離し、laziness を守る。
  • レジストリは capability 駆動の汎用ハンドラで、コアにプラグイン名の分岐が現れない。
  • SDK の細いサブパス群は「起動を軽く保つための遅延ロード境界」であり、無秩序ではなく規律の産物。

次回予告 — 運ぶことに徹するチャネルへ

#05 は、もっとも数の多いプラグイン種別、**チャネル層(transport-only)**を読み解きます。20 以上のメッセージングサービスを「商品ロジックを持たない純粋な配送層」としてどう抽象化しているのか。受信イベントの正規化、ドラフトストリーミング、そして「チャネルにコマンドを推測させない」という設計則を、extensions/telegram を題材に追います。

図1.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?