Bundled Capabilities

Deep Dive

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.

📚 What is this & why does it matter?

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.

💡 Real-world analogy

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.

Contents

Overview

Primary Role

Bootstrap and register capabilities from bundled plugins at runtime.

Main Inputs

Plugin IDs array, load options, environment variables (VITEST flag).

Key Decisions

How to build loader config with enablement compat, whether to apply Vitest shims, how to resolve SDK aliases.

Outputs

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.

How It Works

Step 1 — Build Runtime Config

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.

Step 2 — Enablement Compatibility

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.

Step 3 — Vitest Detection

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"),
  };
}
Why shims? The full SDK runtime imports database clients, HTTP servers, and crypto modules. Under Vitest, plugins only need type-compatible stubs so that register() calls succeed without side effects. The shims are zero-dependency modules that satisfy the type contracts.

Step 4 — Build Alias Map

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.

Step 5 — Create Jiti Loader

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.

Step 6 — Import Plugin Modules

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.

Step 7 — Captured Registration

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.

Behavior State Machine

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.

Annotated Code Walkthrough

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"),
  };
}
Shim boundary: Only modules with heavy side effects (runtime, config, logger) are shimmed. Type-only modules like 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);
}
Proxy pattern: Using a Proxy for the capturing API means new register*() methods added to the SDK are automatically captured without updating the capability loader. Only the register prefix convention matters.

Edge Cases & Error Handling

ScenarioBehaviorLocation
Plugin module has no register exportresolvePluginModuleExport() throws; error is caught, logged, and the plugin is skippedresolvePluginModuleExport()
Plugin import fails (syntax error, missing dependency)Jiti throws; error is caught and logged with the plugin ID; other plugins continue loadingImport phase
VITEST mode needs smaller dependency graphHeavy SDK modules (runtime, config, logger) are replaced with zero-dependency shimsapplyVitestCapabilityAliasOverrides()
SDK alias resolution with dist vs sourceAlias map checks for dist/ paths first, falls back to src/ for dev-mode compatibilitybuildPluginLoaderAliasMap()
Multiple plugins with conflicting registrationsLast registration wins for tools/providers with duplicate names; warning logged to aid debuggingPluginRegistry.capture()

See also:

Directory Map

Source files for bundled capability loading
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