Primary Role
Bootstrap and register capabilities from bundled plugins at runtime.
Runtime capability registration for bundled plugins with Vitest compatibility
Bundled Capabilities bridges the gap between static plugin manifests and live runtime behavior. It takes a list of plugin IDs, constructs a loader configuration with SDK alias resolution and optional Vitest shims, dynamically imports each plugin module, and captures all registration calls into a unified PluginRegistry.
After a plugin manifest has been parsed and its commands discovered, the actual plugin code still needs to be loaded and executed. Bundled Capabilities handles the runtime bootstrap: it resolves import paths so plugins can reference the SDK, applies test-mode shims when running under Vitest, and intercepts all registerTool(), registerHook(), etc. calls to build a complete PluginRegistry.
Imagine a theater stage manager preparing for opening night. The script (manifest) lists all the actors (plugins) and their roles (capabilities). The stage manager (Bundled Capabilities) has to actually bring each actor on stage, hand them their props (SDK aliases), possibly swap in understudies for rehearsal (Vitest shims), and record which actor registered for which scene (captured registration). Only after all actors have checked in does the show (PluginRegistry) go live.
Bootstrap and register capabilities from bundled plugins at runtime.
Plugin IDs array, load options, environment variables (VITEST flag).
How to build loader config with enablement compat, whether to apply Vitest shims, how to resolve SDK aliases.
PluginRegistry with all bundled plugin capabilities registered.
flowchart LR
IDs["Plugin IDs[]"] --> Config["buildBundledCapabilityRuntimeConfig()"]
Config --> Compat["withBundledPluginEnablementCompat()"]
Compat --> Vitest{"VITEST?"}
Vitest -->|Yes| Shims["Apply Vitest Shims"]
Vitest -->|No| Jiti["createJiti() + alias map"]
Shims --> Jiti
Jiti --> Load["Import plugin modules"]
Load --> Register["capturedPluginRegistration()"]
Register --> Registry["PluginRegistry"]
The pipeline starts with a list of plugin IDs and progressively builds a loader environment: first applying enablement compatibility rules, then branching for Vitest when needed, constructing a Jiti-based TypeScript loader with SDK path aliases, importing each plugin module, and finally capturing all registration calls into the plugin registry.
buildBundledCapabilityRuntimeConfig() takes the list of plugin IDs and load options and constructs a config object describing which plugins to load and how to configure them.
withBundledPluginEnablementCompat() wraps the config to ensure that plugins which were previously enabled/disabled maintain their state across format migrations. This is critical when upgrading from an older bundle format.
If the VITEST environment variable is set, applyVitestCapabilityAliasOverrides() replaces heavy SDK module paths with lightweight shims. This avoids pulling in the full OpenClaw runtime during unit tests, keeping the test dependency graph small and fast.
function applyVitestCapabilityAliasOverrides(
aliasMap: Record<string, string>
): Record<string, string> {
if (!process.env.VITEST) return aliasMap;
return {
...aliasMap,
// Replace heavy SDK modules with test-safe stubs
"openclaw/plugin-sdk/runtime": vitestShimPath("runtime"),
"openclaw/plugin-sdk/config": vitestShimPath("config"),
"openclaw/plugin-sdk/logger": vitestShimPath("logger"),
};
}
register() calls succeed without side effects. The shims are zero-dependency modules that satisfy the type contracts.
buildPluginLoaderAliasMap() creates a mapping from openclaw/plugin-sdk/* import specifiers to their actual file system paths. This allows plugins to use clean import paths without knowing the monorepo layout.
A Jiti runtime TypeScript loader is created with the alias map injected. Jiti enables importing .ts files at runtime without a prior compile step, using on-the-fly transpilation.
Each plugin module is imported via Jiti. The loader resolves the entry point from the manifest, imports it, and extracts the default export. resolvePluginModuleExport() handles both ESM default exports and CJS module.exports patterns.
createCapturedPluginRegistration() wraps a PluginApi instance that records every registerTool(), registerHook(), registerProvider() call into the PluginRegistry. The plugin’s register() function is called with this capturing API.
stateDiagram-v2 [*] --> BuildConfig : buildBundledCapabilityRuntimeConfig() BuildConfig --> CheckVitest : Check VITEST env CheckVitest --> ApplyShims : VITEST=true CheckVitest --> BuildAliases : VITEST=false ApplyShims --> BuildAliases BuildAliases --> CreateJiti : createJiti(options) CreateJiti --> ImportPlugins : Import each plugin ImportPlugins --> ResolveExport : resolvePluginModuleExport() ResolveExport --> CaptureReg : createCapturedPluginRegistration() CaptureReg --> Done : PluginRegistry Done --> [*]
The state machine is linear with one branch: the Vitest check determines whether shim overrides are applied before building the alias map. After that, the pipeline proceeds sequentially through Jiti creation, module import, export resolution, and registration capture.
Step through applyVitestCapabilityAliasOverrides() and the surrounding loader setup:
Build the base alias map that maps SDK import specifiers to real file paths:
function buildPluginLoaderAliasMap(
sdkRoot: string
): Record<string, string> {
return {
"openclaw/plugin-sdk/plugin-entry": join(sdkRoot, "plugin-entry.ts"),
"openclaw/plugin-sdk/runtime": join(sdkRoot, "runtime.ts"),
"openclaw/plugin-sdk/config": join(sdkRoot, "config.ts"),
"openclaw/plugin-sdk/logger": join(sdkRoot, "logger.ts"),
"openclaw/plugin-sdk/types": join(sdkRoot, "types.ts"),
"openclaw/plugin-sdk/core": join(sdkRoot, "core.ts"),
};
}
Apply Vitest overrides when running under the test framework — swap heavy modules for lightweight stubs:
function applyVitestCapabilityAliasOverrides(
aliasMap: Record<string, string>
): Record<string, string> {
if (!process.env.VITEST) return aliasMap;
const shimRoot = join(__dirname, "__vitest-shims__");
return {
...aliasMap,
"openclaw/plugin-sdk/runtime": join(shimRoot, "runtime.ts"),
"openclaw/plugin-sdk/config": join(shimRoot, "config.ts"),
"openclaw/plugin-sdk/logger": join(shimRoot, "logger.ts"),
};
}
types.ts and core.ts use the real paths even in test mode, ensuring type compatibility is verified at test time.
Create the Jiti loader with the final alias map and import the plugin module:
import { createJiti } from "jiti";
const aliases = applyVitestCapabilityAliasOverrides(
buildPluginLoaderAliasMap(sdkRoot)
);
const jiti = createJiti(__filename, {
alias: aliases,
interopDefault: true,
});
const mod = await jiti.import(pluginEntryPath);
const entry = resolvePluginModuleExport(mod);
Resolve the module export, handling both ESM default and CJS patterns:
function resolvePluginModuleExport(
mod: unknown
): DefinedPluginEntry {
if (mod && typeof mod === "object") {
const m = mod as Record<string, unknown>;
// ESM: { default: DefinedPluginEntry }
if (m.default && typeof (m.default as any).register === "function") {
return m.default as DefinedPluginEntry;
}
// CJS: module.exports = DefinedPluginEntry
if (typeof (m as any).register === "function") {
return m as unknown as DefinedPluginEntry;
}
}
throw new Error("Plugin module has no register export");
}
Execute the plugin’s registration via the capturing API that records all calls:
function createCapturedPluginRegistration(
entry: DefinedPluginEntry,
registry: PluginRegistry,
config: OpenClawConfig
): void {
const api = createScopedPluginApi({
id: entry.id,
name: entry.name,
config,
logger: createLogger(entry.id),
});
// Wrap each register method to capture into registry
const captured = new Proxy(api, {
get(target, prop) {
if (typeof prop === "string" && prop.startsWith("register")) {
return (...args: unknown[]) => {
registry.capture(entry.id, prop, args);
return (target as any)[prop](...args);
};
}
return (target as any)[prop];
},
});
entry.register(captured);
}
register*() methods added to the SDK are automatically captured without updating the capability loader. Only the register prefix convention matters.
| Scenario | Behavior | Location |
|---|---|---|
Plugin module has no register export | resolvePluginModuleExport() throws; error is caught, logged, and the plugin is skipped | resolvePluginModuleExport() |
| Plugin import fails (syntax error, missing dependency) | Jiti throws; error is caught and logged with the plugin ID; other plugins continue loading | Import phase |
| VITEST mode needs smaller dependency graph | Heavy SDK modules (runtime, config, logger) are replaced with zero-dependency shims | applyVitestCapabilityAliasOverrides() |
| SDK alias resolution with dist vs source | Alias map checks for dist/ paths first, falls back to src/ for dev-mode compatibility | buildPluginLoaderAliasMap() |
| Multiple plugins with conflicting registrations | Last registration wins for tools/providers with duplicate names; warning logged to aid debugging | PluginRegistry.capture() |
See also:
src/plugins/
├── bundled-capability-runtime.ts # Main entry: buildBundledCapabilityRuntimeConfig()
├── bundled-capability-types.ts # PluginRegistry, CapturedRegistration types
├── bundled-capability-aliases.ts # buildPluginLoaderAliasMap(), alias resolution
├── bundled-capability-vitest.ts # applyVitestCapabilityAliasOverrides()
├── bundled-capability-loader.ts # Jiti loader creation, resolvePluginModuleExport()
├── bundled-capability-capture.ts # createCapturedPluginRegistration(), Proxy wrapping
└── __vitest-shims__/
├── runtime.ts # Lightweight runtime stub for tests
├── config.ts # No-op config stub
└── logger.ts # Console-based logger stub