Files
openclaw/experiments/plugin-sdk-namespaces-plan.md
2026-03-25 14:13:58 +01:00

14 KiB

Plugin SDK Namespaces Plan

Plain-Language TL;DR

OpenClaw should introduce a few clear SDK namespaces like plugin, channel, and provider, instead of keeping so much of the public surface flat.

The safe way to do that is:

  • add thin ESM facade entrypoints, not TypeScript namespace
  • keep the root openclaw/plugin-sdk surface small
  • add namespaced aliases to OpenClawPluginApi
  • keep old flat APIs as compatibility aliases during migration
  • forbid leaf modules from importing back through namespace facades

That gives plugin authors a cleaner SDK that feels closer to VS Code, without turning the SDK into a giant barrel or creating circular import problems.

Goal

Introduce public namespaces to the OpenClaw Plugin SDK so the surface feels closer to the VS Code extension API, while keeping the implementation tight, isolated, and resistant to circular imports.

This plan is about the public SDK shape. It is not a proposal to merge everything into one giant barrel.

Why This Is Worth Doing

Today the Plugin SDK has three visible problems:

  • The public package export surface is large and mostly flat.
  • src/plugin-sdk/core.ts and src/plugin-sdk/index.ts carry too many unrelated meanings.
  • OpenClawPluginApi is still a flat registration API even though api.runtime already proves grouped namespaces work well.

The result is harder docs, harder discovery, and too many helper names that look equally important even when they are not.

Current Facts In The Repo

  • Package exports are generated from a flat entrypoint list in src/plugin-sdk/entrypoints.ts and scripts/lib/plugin-sdk-entrypoints.json.
  • The root openclaw/plugin-sdk entry is intentionally tiny in src/plugin-sdk/index.ts.
  • api.runtime is already a successful namespace model. It groups behavior as agent, subagent, media, imageGeneration, webSearch, tools, channel, events, logging, state, tts, mediaUnderstanding, and modelAuth in src/plugins/runtime/index.ts.
  • The main plugin registration API is still flat in OpenClawPluginApi in src/plugins/types.ts.
  • The concrete API object is assembled in src/plugins/registry.ts, and a second partial copy exists in src/plugins/captured-registration.ts.

Those facts suggest a path that is low-risk:

  • keep leaf modules as the source of truth
  • add namespace facades on top
  • move docs and examples to the namespace model
  • keep flat compatibility aliases during migration

Design Principles

1. Do Not Use TypeScript namespace

Use normal ESM modules and package exports.

The SDK already ships as package export subpaths. The namespace model should be implemented as public facade modules, not TypeScript namespace syntax.

2. Keep The Root Tiny

Do not turn openclaw/plugin-sdk into a giant VS Code-style monolith.

The closest safe equivalent is:

  • a tiny root for shared types and a few universal values
  • a small number of explicit namespace entrypoints
  • optional ergonomic aggregation only after the namespace surfaces settle

3. Namespace Facades Must Be Thin

Namespace entrypoints should contain no real business logic.

They should only:

  • re-export stable leaves
  • assemble small namespace objects
  • provide compatibility aliases

That keeps cycles and accidental coupling down.

4. Types Stay Direct And Easy To Import

Like VS Code, namespaces should mostly group behavior. Common types should stay directly importable from the root or the owning domain surface.

Examples:

  • ChannelPlugin
  • ProviderPlugin
  • OpenClawPluginApi
  • PluginRuntime

5. Do Not Namespace Everything At Once

Only namespace areas that already have a clear public identity.

Phase 1 should focus on:

  • plugin
  • channel
  • provider

runtime already has a good public namespace shape on api.runtime and should not be reopened as a giant package-export family in the first pass.

Proposed Public Model

Namespace Entry Points

Canonical public entrypoints:

  • openclaw/plugin-sdk/plugin
  • openclaw/plugin-sdk/channel
  • openclaw/plugin-sdk/provider
  • openclaw/plugin-sdk/runtime
  • openclaw/plugin-sdk/testing

What each should mean:

  • plugin
    • plugin entry helpers
    • shared plugin definition helpers
    • plugin-facing config schema helpers that are truly universal
  • channel
    • channel entry helpers
    • chat-channel builders
    • stable channel-facing contracts and helpers
  • provider
    • provider entry helpers
    • auth, catalog, models, onboard, stream, usage, and provider registration helpers
  • runtime
    • the existing api.runtime story and runtime-related public helpers that are truly stable
  • testing
    • plugin author testing helpers

Nested Leaves

Under those namespaces, the long-term canonical leaves should become nested:

  • openclaw/plugin-sdk/channel/setup

  • openclaw/plugin-sdk/channel/pairing

  • openclaw/plugin-sdk/channel/reply-pipeline

  • openclaw/plugin-sdk/channel/contract

  • openclaw/plugin-sdk/channel/targets

  • openclaw/plugin-sdk/channel/actions

  • openclaw/plugin-sdk/channel/inbound

  • openclaw/plugin-sdk/channel/lifecycle

  • openclaw/plugin-sdk/channel/policy

  • openclaw/plugin-sdk/channel/feedback

  • openclaw/plugin-sdk/channel/config-schema

  • openclaw/plugin-sdk/channel/config-helpers

  • openclaw/plugin-sdk/provider/auth

  • openclaw/plugin-sdk/provider/catalog

  • openclaw/plugin-sdk/provider/models

  • openclaw/plugin-sdk/provider/onboard

  • openclaw/plugin-sdk/provider/stream

  • openclaw/plugin-sdk/provider/usage

  • openclaw/plugin-sdk/provider/web-search

Not every current flat subpath needs a namespaced replacement. The goal is to promote the stable public domains, not to preserve every current export forever.

What Happens To core

core is overloaded today. In a namespace model it should shrink, not grow.

Target split:

  • plugin-wide entry helpers move toward plugin
  • channel builders and channel-oriented shared helpers move toward channel
  • core remains as a migration surface and compatibility alias for one release cycle

Rule: no new public API should be added to core once namespace entrypoints exist.

Proposed OpenClawPluginApi Shape

Keep context fields flat:

  • id
  • name
  • version
  • description
  • source
  • rootDir
  • registrationMode
  • config
  • pluginConfig
  • runtime
  • logger
  • resolvePath

Move registration behavior behind namespaces:

Current flat method Proposed namespace alias
registerTool api.tool.register
registerHook api.hook.register
on api.hook.on
registerHttpRoute api.http.registerRoute
registerChannel api.channel.register
registerProvider api.provider.register
registerSpeechProvider api.provider.registerSpeech
registerMediaUnderstandingProvider api.provider.registerMediaUnderstanding
registerImageGenerationProvider api.provider.registerImageGeneration
registerWebSearchProvider api.provider.registerWebSearch
registerGatewayMethod api.gateway.registerMethod
registerCli api.cli.register
registerService api.service.register
registerInteractiveHandler api.interactive.register
registerCommand api.command.register
registerContextEngine api.contextEngine.register
registerMemoryPromptSection api.memory.registerPromptSection

Keep the flat methods as direct compatibility aliases during migration.

That gives plugin authors a clearer public shape without forcing immediate rewrites across the repo.

Example Public Usage

Proposed style:

import { definePluginEntry } from "openclaw/plugin-sdk/plugin";
import { channel } from "openclaw/plugin-sdk/channel";
import { provider } from "openclaw/plugin-sdk/provider";
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";

const chatPlugin: ChannelPlugin = channel.createChatPlugin({
  id: "demo",
  /* ... */
});

export default definePluginEntry({
  id: "demo",
  register(api: OpenClawPluginApi) {
    api.channel.register(chatPlugin);
    api.command.register({
      name: "status",
      description: "Show plugin status",
      run: async () => ({ text: "ok" }),
    });
  },
});

This is close to the VS Code mental model:

  • grouped behavior
  • direct types
  • obvious public areas

without requiring a single monolithic root import.

Optional Ergonomic Surface

If the project later wants the closest possible VS Code feel, add a dedicated opt-in facade such as openclaw/plugin-sdk/sdk.

That facade can assemble:

  • plugin
  • channel
  • provider
  • runtime
  • testing

It should not be phase 1.

Why:

  • it is the highest-risk barrel from a cycle and weight perspective
  • it is easier to add once the namespace surfaces already exist
  • it preserves the root openclaw/plugin-sdk entry as a small type-oriented surface

Internal Implementation Rules

These rules are the important part. Without them, namespaces will rot into barrels and cycles.

Rule 1: Namespace Facades Are One-Way

Namespace entrypoints may import leaf modules.

Leaf modules may not import their namespace entrypoint.

Examples:

  • allowed: src/plugin-sdk/channel.ts importing ./channel-setup.ts
  • forbidden: src/plugin-sdk/channel-setup.ts importing ./channel.ts

Rule 2: No Public-Specifier Self-Imports Inside The SDK

Files inside src/plugin-sdk/** should never import from openclaw/plugin-sdk/....

They should import local source files directly.

Rule 3: Shared Code Lives In Shared Leaves

If channel and provider need the same implementation detail, move that code to a shared leaf instead of importing one namespace from the other.

Good shared homes:

  • a narrowed core during migration
  • a dedicated internal shared leaf
  • existing domain-neutral helpers

Bad pattern:

  • provider/* importing from channel/index
  • channel/* importing from provider/index

Rule 4: Assemble The API Surface Once

OpenClawPluginApi should be built by one canonical factory.

src/plugins/registry.ts and src/plugins/captured-registration.ts should stop hand-building separate versions of the API object.

That factory can expose:

  • flat methods
  • namespace aliases

from the same underlying implementation.

Rule 5: Namespace Entry Files Stay Small

Namespace facades should stay close to pure exports. If a namespace file grows real orchestration logic, split that logic back into leaf modules.

Migration Strategy

Phase 1: Add Namespace Aliases To OpenClawPluginApi

Do this first.

Why:

  • lowest migration risk
  • no package export churn required yet
  • plugin authors immediately get the better public shape
  • docs can start using namespaces without moving leaf files

Implementation:

  • extend OpenClawPluginApi in src/plugins/types.ts
  • assemble namespace aliases in the canonical API builder
  • keep all existing flat methods

Phase 2: Add Canonical Namespace Entrypoints

Add:

  • plugin
  • channel
  • provider

as thin public facades over existing flat leaves.

Implementation detail:

  • the first pass can re-export current flat files
  • do not move source layout and package exports in the same commit if it can be avoided

Examples:

  • src/plugin-sdk/channel/setup.ts can initially re-export from ../channel-setup.js
  • src/plugin-sdk/provider/auth.ts can initially re-export from ../provider-auth.js

This lets the public namespace story land before the internal source move.

Phase 3: Move The Canonical Docs And Templates

Once aliases exist:

  • docs prefer namespaced entrypoints
  • templates prefer namespaced imports
  • new SDK additions land under namespaces first

At this point the old flat leaves still work but stop being the preferred story.

Phase 4: Deprecate Flat Leaves

After one release cycle of overlap:

  • mark flat leaves as compatibility aliases
  • keep the highest-value ones for longer if third-party plugin breakage risk is high
  • stop documenting them as first-class API

What Should Not Be Namespaced In Phase 1

To keep the refactor tight, do not force these into the first milestone:

  • every *-runtime helper subpath
  • extension-branded public subpaths
  • one-off utilities that do not yet have a stable domain home
  • the root openclaw/plugin-sdk barrel

If a subpath is only public because history leaked it, namespace work should not promote it.

Guardrails And Validation

The namespace rollout should ship with explicit checks.

Existing Checks To Reuse

  • src/plugin-sdk/subpaths.test.ts
  • src/plugin-sdk/runtime-api-guardrails.test.ts
  • pnpm build for [CIRCULAR_REEXPORT] warnings
  • pnpm plugin-sdk:api:check

New Checks To Add

  • namespace facade files may only re-export or compose approved leaves
  • leaf files under a namespace may not import their parent index facade
  • no new API should be added to core once namespace facades exist
  • compatibility aliases must stay type-equivalent to canonical namespaced leaves

The elegant end state is:

  • a tiny root
  • a few first-class namespaces
  • direct types
  • a grouped api registration surface
  • stable leaves under each namespace
  • no reverse imports from leaves back into namespace facades

That gives OpenClaw a VS Code-like feel where the public SDK has clear domains, but still respects the repo's existing build, lazy-loading, and package-boundary constraints.

Short Recommendation

If this work starts soon, the first implementation step should be:

  1. extract one canonical OpenClawPluginApi builder
  2. add namespace aliases there
  3. add plugin, channel, and provider facade entrypoints
  4. move docs and examples to those names
  5. only then decide which flat leaves deserve long-term compatibility

That sequence keeps the refactor elegant and minimizes the chance that namespaces become another layer of accidental coupling.