Plugins: Verzeichniskonvention und Entdeckung
Verantwortung
Ein Plugin (plugin) ist das Erweiterungspaket von OpenClaw — ein npm-Paket oder ein Source-Verzeichnis nach Verzeichniskonvention, das deklariert, welche Werkzeuge, Fertigkeiten, Kanäle (channel), Provider, Befehle, Hooks, RPC-Methoden es bietet. Beim Start scannt das System mehrere Wurzelverzeichnisse, entdeckt (discovery) Kandidaten-Plugins, lädt (loading) und sammelt sie in einer PluginRegistry; die Hauptschleife, das Gateway und die Agent-Sitzung nehmen aus dieser Registry ihre Erweiterungsfähigkeiten.
Das Plugin-System selbst „führt" keine Geschäftslogik aus, es macht nur drei Dinge: Scannen mehrerer Wurzelverzeichnisse nach Kandidaten, Parsen von Manifest (manifest) und Bundle-Format, Registrieren in einer einheitlichen Registry. Die eigentliche Arbeit machen Werkzeuge, Provider und Kanäle, die vom Plugin registriert wurden — sobald diese Fähigkeiten in der PluginRegistry sind, stehen sie gleichberechtigt neben Fähigkeiten aus anderen Quellen (eingebaut, SDK-eigen).
OpenClaw unterstützt mehrere Bundle-Formate: .codex-plugin/plugin.json (Codex-Stil), .claude-plugin/plugin.json (Claude-Stil), .cursor-plugin/plugin.json (Cursor-Stil) sowie natives plugin.json. So kann OpenClaw Plugin-Pakete anderer Ökosysteme direkt weiterverwenden, ohne Paketautoren zum Neupacken zu zwingen.
Designmotivation
Warum Verzeichniskonvention statt explizitem Import? Weil Plugin-Quellen sehr unterschiedlich sind: man sind npm-Pakete, man sind per git clone gezogene Source-Verzeichnisse, man sind gebaute Dist-Artefakte, man hat ein Nutzer manuell unter ~/.openclaw/plugins/ abgelegt. Expliziter Import verlangt, dass Pfad und Modulsystem bereit sind; eine Verzeichniskonvention erkennt bereits „in diesem Verzeichnis liegt eine Manifest-Datei". So kann die Entdeckung (discovery) als reine Dateisystemoperation ablaufen, ohne Module zu laden — Modulladen ist teuer (Node-ESM-Auflösung, möglicher tsx-Compile); Trennung von Erkennen und Laden beschleunigt den Kaltstart erheblich.
bundle-manifest.ts (bundle-manifest.ts relative Pfad-Konstanten:L21-L24) definiert die relativen Pfade der drei Bundle-Formate:
/** 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";Beachten Sie, dass alles versteckte Unterverzeichnisse + plugin.json sind — so kann ein Plugin-Paket selbst ein normales npm-Paket sein mit package.json, README.md, Source, und gleichzeitig eine .codex-plugin/plugin.json-Datei tragen, die den OpenClaw-relevanten Teil (skills/hooks/capabilities) deklariert.
Der Typ BundlePluginManifest (src/plugins/bundle-manifest.ts:L27-L39) normalisiert mehrere Formate:
export type BundlePluginManifest = {
id: string;
name?: string;
description?: string;
version?: string;
skills: string[];
settingsFiles?: string[];
hooks: string[];
bundleFormat: PluginBundleFormat;
activation?: PluginManifestActivation;
capabilities: string[];
};Das Feld bundleFormat markiert das Quellformat; skills / hooks / capabilities sind die von OpenClaw wirklich genutzten Fähigkeitsdeklarationen — Pfad-Arrays relativ zum Plugin-Root. Diese „Normalisierung" erlaubt es dem nachfolgenden Loader, das Herkunfts-Ökosystem des Plugins zu ignorieren.
Die Ladephase loadOpenClawPlugins (loader.ts loadOpenClawPlugins:L1821-L1895) hat mehrere Schlüsseldesigns: Cache-Wiederverwendung (gleicher cacheKey liefert bereits geladene Registry zurück, verhindert wiederholtes Modulladen), Aktivzustands-Trennung (shouldActivate steuert, ob der vorherige Runtime-Zustand geleert wird; inaktives Laden liest nur, schreibt keine Seiteneffekte), Lazy-Erstellung des Modulladers (wenn alle Plugins deaktiviert sind, wird kein Modullader erzeugt, spart Kaltstartkosten).
PluginRegistry (registry-types.ts PluginRegistry:L434-L484) ist ein großes Objekt mit einem Array pro Fähigkeit:
export type PluginRegistry = {
plugins: PluginRecord[];
tools: PluginToolRegistration[];
hooks: PluginHookRegistration[];
typedHooks: TypedPluginHookRegistration[];
channels: PluginChannelRegistration[];
channelSetups: PluginChannelSetupRegistration[];
providers: PluginProviderRegistration[];
modelCatalogProviders: PluginModelCatalogProviderRegistration[];
// ... noch über zehn Provider-Typen und Registrierungen
gatewayHandlers: GatewayRequestHandlers;
httpRoutes: PluginHttpRouteRegistration[];
cliRegistrars: PluginCliRegistration[];
commands: PluginCommandRegistration[];
// ...
diagnostics: PluginDiagnostic[];
};Beachten Sie: Es ist keine Map, sondern ein nach Fähigkeiten getrenntes Array-Objekt. Der Grund: Die Registrierungsdimensionen unterscheiden sich pro Fähigkeit — Werkzeuge haben name+executor, Kanäle channelId+pluginId, Provider api+providerId. Sie in Map<string, T> zu quetschen würde bedeuten, einen künstlichen Key zu erfinden. Array+diagnostics erlauben es, Warnungen und Fehler aus Entdeckung/Ladung bis in die Runtime mitzunehmen, anstatt „Laden fehlgeschlagen, verwerfen".
Schlüsseldateien
bundle-manifest.ts drei Formate:L21-L24— Codex/Claude/Cursor Bundle relative Pfad-Konstanten.bundle-manifest.ts BundlePluginManifest:L27-L39— Normalisiertes Bundle-Manifest-Typ.discovery.ts discoverOpenClawPlugins:L1432-L1510— Entdeckungs-Haupteinstieg, scannt mehrere Wurzeln + extraPaths.discovery.ts Workspace-Einschränkung:L1484-L1505— Workspace Auto-Discovery ist auf extensions root beschränkt, verhindert versehentliches Scannen des gesamten Arbeitsbereichs.discovery.ts discoverFromPath:L1210-L1259— Entdeckung auf einem Pfad: Datei/Verzeichnis-Zweigbehandlung.loader.ts PluginLoadOptions:L181-L232— Vollständige Lade-Optionen, inkl. cache/onlyPluginIds/mode.loader.ts loadOpenClawPlugins:L1821-L1903— Lade-Haupteinstieg, Cache-Wiederverwendung + Aktivzustands-Trennung.registry-types.ts PluginRegistry:L434-L484— Registry-Typ, nach Fähigkeiten getrennt.registry-types.ts PluginToolRegistration:L76-L86— Beispiel für Werkzeug-Registrierung.discovery.ts bundled-Alias-Ablehnung:L1459-L1470— Wenn extraPaths auf bundled root zeigt, nur warn, nicht doppelt scannen.
Datenfluss
Die Entdeckung startet von mehreren Wurzeln (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", /* ... */ });
}Beachten Sie die bundled-Alias-Erkennung — wenn der Nutzer in plugins.load.paths auf das OpenClaw-interne bundled-Plugin-Verzeichnis zeigt, wird nur gewarnt, nicht doppelt gescannt, da dieses Verzeichnis bereits im Scan-Bereich liegt. Das verhindert „doppeltes Laden desselben Plugins führt zu Namenskonflikt".
Die Workspace Auto-Discovery hat einen kritischen Kommentar (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",
// ...
});
}Das ist eine bereits durchlaufene Falle: Scanne rekursiv den gesamten Workspace, wird jeder gewöhnliche node_modules-Unterordner oder Source-Ordner als Plugin-Kandidat betrachtet und wirft „plugin manifest not found" — dieses Rauschen überdeckt echte Fehler. Deshalb ist die Workspace-Entdeckung auf extensions root beschränkt.
Die Ladephase (loader.ts Cache+Aktivierung:L1862-L1903) nutzt Cache-Wiederverwendung + Aktivzustands-Trennung:
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 ?? []);
// ... über ein Dutzend Runtime-Zustände wiederherstellen
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,
});Cache-Wiederverwendung stellt nicht nur die Registry wieder her, sondern auch agentHarnesses, commands, compactionProviders, interactiveHandlers, embeddingProviders und über ein Dutzend weiterer globaler Seiteneffekte —因为这些 Seiteneffekte wurden beim Laden des Plugins in globale Tabellen eingetragen; bei Reaktivierung müssen sie an die richtige Position zurückgestellt werden. clearActivatedPluginRuntimeState leert vor dem Neuladen den alten Zustand, damit im vorherigen Lauf registrierte Werkzeuge/Befehle nicht in diesen Lauf „durchsickern".
Bundle-Manifest-Parsing in bundle-manifest.ts Datei laden:L92-L110: Nutzt readRootStructuredFileSync zum Lesen von JSON an der Root-Grenze; bei Misserfolg { ok: false, error, manifestPath }, kein Throw.
Grenzen und Fehler
- Drei Bundle-Formate parallel:
.codex-plugin/.claude-plugin/.cursor-pluginsind alles versteckte Unterverzeichnisse +plugin.json(src/plugins/bundle-manifest.ts:L21-L24). Ein Plugin kann mehrere Formate gleichzeitig deklarieren — der Loader wählt das erste passende Format und normalisiert es zuBundlePluginManifest. - Workspace-Entdeckung ist begrenzt: Auto-Discovery scannt nur extensions root, nicht rekursiv den gesamten Workspace (
src/plugins/discovery.ts:L1489-L1505). Sonst explodiert „plugin manifest not found"-Rauschen. - Bundled-Verzeichnis wird nicht doppelt gescannt: Wenn der Nutzer in
plugins.load.pathsauf bundled root zeigt, nur warn, nicht doppelt scannen (src/plugins/discovery.ts:L1459-L1470). Empfohlen,openclaw doctor --fixauszuführen, um redundante Konfiguration zu bereinigen. - Cache-Wiederverwendung stellt Seiteneffekte wieder her: Bei Cache-Treffer wird nicht nur die Registry zurückgegeben, sondern auch
restoreRegisteredAgentHarnesses/restorePluginCommands/restoreRegisteredEmbeddingProvidersund über ein Dutzend weiterer globaler Wiederherstellungen (src/plugins/loader.ts:L1872-L1885). Fehlt einer, werden Werkzeuge/Befehle „unsichtbar". - Aktivzustands-Trennung:
shouldActivatefalse leert den vorherigen registrierten Runtime-Zustand nicht — das ist für Snapshot-Laden (validate-Modus / parallel discovery) gedacht, um versehentliches Löschen der Befehle anderer Plugins zu vermeiden. - Lazy-Modulladen: Wenn alle Plugins deaktiviert sind, wird
createPluginModuleLoadergar nicht aufgerufen (src/plugins/loader.ts:L1904-L1908) — häufiger Fall in Unit-Tests, spart Kaltstartkosten. - Diagnostics gehen nicht verloren:
PluginDiscoveryResultundPluginRegistrytragen beide ein Arraydiagnostics: PluginDiagnostic[]; Warnungen/Fehler aus Entdeckung/Ladung werden bis in die Runtime mitgeführt.openclaw doctorliest diese Diagnostics und gibt Reparaturhinweise. - Plugin ist nicht Werkzeug: Ein Plugin kann Werkzeuge registrieren, aber das Plugin selbst ist ein Paket.
PluginRegistry.toolsist ein Array von Werkzeugen; jedePluginToolRegistrationreferenziert Plugin-ID + Werkzeugdefinition — diese Werkzeuge werden letztlich in die Map des Werkzeugsystems eingespeist. Ebenso kann ein Plugin einskills/SKILL.md-Feld tragen (siehe Fertigkeiten), ist aber selbst ein nach Verzeichniskonvention entdecktes Erweiterungspaket, nicht die Fertigkeit selbst.
Zusammenfassung
Plugins sind nach Verzeichniskonvention entdeckte Erweiterungspakete: Versteckte Unterverzeichnisse wie .codex-plugin/plugin.json deklarieren Fähigkeiten; discovery scannt mehrere Wurzeln + extraPaths nach Kandidaten; der Loader normalisiert in eine PluginRegistry getrennt nach Fähigkeiten. Cache-Wiederverwendung stellt über ein Dutzend globale Seiteneffekte wieder her; Aktivzustands-Trennung verhindert, dass Snapshot-Laden den vorherigen Runtime-Zustand leert. Drei Bundle-Formate erlauben OpenClaw, Plugins aus dem Codex/Claude/Cursor-Ökosystem direkt zu konsumieren. Unterschied zu Werkzeugen: Werkzeuge sind Funktionen in der Map, Plugins sind eine Quelle, die Werkzeuge produziert; Unterschied zu Fertigkeiten: Fertigkeiten sind Markdown-Anleitungen, Plugins können Fertigkeiten als Inhalt tragen. Kanal-Registrierung und RPC-Methoden-Registrierung laufen über die Kanal-Registry.
Vergleich mit offiziellen Ressourcen: Plugins-Doku · README.