restore extension-api backward compatibility with migration warning

This commit is contained in:
Tak Hoffman 2026-03-20 12:41:19 -05:00
parent e4d0fdcc15
commit 16e055c083
No known key found for this signature in database
10 changed files with 217 additions and 15 deletions

View File

@ -166,6 +166,11 @@ my-plugin/
Always import from specific `openclaw/plugin-sdk/\<subpath\>` paths. The old
monolithic import is deprecated (see [SDK Migration](/plugins/sdk-migration)).
If older plugin code still imports `openclaw/extension-api`, treat that as a
temporary compatibility bridge only. New code should use injected runtime
helpers such as `api.runtime.agent.*` instead of importing host-side agent
helpers directly.
```typescript
// Correct: focused subpaths
import { definePluginEntry } from "openclaw/plugin-sdk/core";
@ -174,6 +179,9 @@ my-plugin/
// Wrong: monolithic root (lint will reject this)
import { ... } from "openclaw/plugin-sdk";
// Deprecated: legacy host bridge
import { runEmbeddedPiAgent } from "openclaw/extension-api";
```
<Accordion title="Common subpaths reference">
@ -302,7 +310,7 @@ patterns is strongly recommended.
## Related
- [Plugin SDK Migration](/plugins/sdk-migration) — migrating from the deprecated compat import
- [Plugin SDK Migration](/plugins/sdk-migration) — migrating from deprecated compat surfaces
- [Plugin Architecture](/plugins/architecture) — internals and capability model
- [Plugin Manifest](/plugins/manifest) — full manifest schema
- [Plugin Agent Tools](/plugins/agent-tools) — adding agent tools in a plugin

View File

@ -1,17 +1,24 @@
---
title: "Plugin SDK Migration"
sidebarTitle: "SDK Migration"
summary: "Migrate from the deprecated openclaw/plugin-sdk/compat import to focused subpath imports"
summary: "Migrate from legacy compat surfaces to focused plugin-sdk subpaths and injected runtime helpers"
read_when:
- You see the OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED warning
- You are updating a plugin from the monolithic import to scoped subpaths
- You see the OPENCLAW_EXTENSION_API_DEPRECATED warning
- You are updating a plugin from the monolithic plugin-sdk import to scoped subpaths
- You are updating a plugin away from openclaw/extension-api
- You maintain an external OpenClaw plugin
---
# Plugin SDK Migration
The `openclaw/plugin-sdk/compat` import is deprecated. All plugins should use
**focused subpath imports** (`openclaw/plugin-sdk/\<subpath\>`) instead.
OpenClaw is migrating from broad compatibility surfaces to narrower, documented
contracts:
- `openclaw/plugin-sdk/compat` -> focused `openclaw/plugin-sdk/<subpath>` imports
- `openclaw/extension-api` -> injected runtime helpers such as `api.runtime.agent.*`
This page explains what changed, why, and how to migrate.
<Info>
The compat import still works at runtime. This is a deprecation warning, not
@ -32,19 +39,21 @@ with a clear purpose.
<Steps>
<Step title="Find deprecated imports">
Search your plugin for imports from the compat path:
Search your plugin for imports from either deprecated surface:
```bash
grep -r "plugin-sdk/compat" my-plugin/
grep -r "openclaw/extension-api" extensions/my-plugin/
```
</Step>
<Step title="Replace with focused subpaths">
Each export maps to a specific subpath. Replace the import source:
<Step title="Replace with focused subpaths or runtime injection">
Each export from compat maps to a specific subpath. Replace the import
source:
```typescript
// Before (deprecated)
// Before (compat entry)
import {
createChannelReplyPipeline,
createPluginRuntimeStore,
@ -57,14 +66,60 @@ with a clear purpose.
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
```
See the [subpath reference](#subpath-reference) below for the full mapping.
If your plugin imports from `openclaw/extension-api`, you will now see:
```text
[OPENCLAW_EXTENSION_API_DEPRECATED] Warning: openclaw/extension-api is deprecated.
Migrate to api.runtime.agent.* or focused openclaw/plugin-sdk/<subpath> imports.
```
That bridge also still works at runtime today. It exists to preserve older
plugins while they migrate to the injected plugin runtime.
Move host-side helpers onto the injected plugin runtime instead of
importing them directly:
```typescript
// Before (deprecated extension-api bridge)
import { runEmbeddedPiAgent } from "openclaw/extension-api";
const result = await runEmbeddedPiAgent({
sessionId,
sessionFile,
workspaceDir,
prompt,
timeoutMs,
});
// After (preferred injected runtime)
const result = await api.runtime.agent.runEmbeddedPiAgent({
sessionId,
sessionFile,
workspaceDir,
prompt,
timeoutMs,
});
```
The same pattern applies to the other legacy `extension-api` helpers:
- `resolveAgentDir` -> `api.runtime.agent.resolveAgentDir`
- `resolveAgentWorkspaceDir` -> `api.runtime.agent.resolveAgentWorkspaceDir`
- `resolveAgentIdentity` -> `api.runtime.agent.resolveAgentIdentity`
- `resolveThinkingDefault` -> `api.runtime.agent.resolveThinkingDefault`
- `resolveAgentTimeoutMs` -> `api.runtime.agent.resolveAgentTimeoutMs`
- `ensureAgentWorkspace` -> `api.runtime.agent.ensureAgentWorkspace`
- session store helpers -> `api.runtime.agent.session.*`
See the [subpath reference](#subpath-reference) below for the scoped import
mapping.
</Step>
<Step title="Build and test">
```bash
pnpm build
pnpm test -- my-plugin/
pnpm test -- extensions/my-plugin/
```
</Step>
</Steps>
@ -101,10 +156,10 @@ check the source at `src/plugin-sdk/` or ask in Discord.
## Removal timeline
| When | What happens |
| ---------------------- | --------------------------------------------------------------- |
| **Now** | Compat import emits a runtime deprecation warning |
| **Next major release** | Compat import will be removed; plugins still using it will fail |
| When | What happens |
| --- | --- |
| **Now** | Compat import and `openclaw/extension-api` emit runtime warnings |
| **Next major release** | These legacy bridges may be removed; plugins still using them will fail |
All core plugins have already been migrated. External plugins should migrate
before the next major release.
@ -115,6 +170,7 @@ Set this environment variable while you work on migrating:
```bash
OPENCLAW_SUPPRESS_PLUGIN_SDK_COMPAT_WARNING=1 openclaw gateway run
OPENCLAW_SUPPRESS_EXTENSION_API_WARNING=1 openclaw gateway run
```
This is a temporary escape hatch, not a permanent solution.

View File

@ -513,6 +513,7 @@
"types": "./dist/plugin-sdk/tool-send.d.ts",
"default": "./dist/plugin-sdk/tool-send.js"
},
"./extension-api": "./dist/extensionAPI.js",
"./cli-entry": "./openclaw.mjs"
},
"scripts": {

21
src/extensionAPI.test.ts Normal file
View File

@ -0,0 +1,21 @@
import * as extensionApi from "openclaw/extension-api";
import { describe, expect, it } from "vitest";
describe("extension-api compat surface", () => {
it("keeps legacy agent helpers importable", () => {
expect(typeof extensionApi.runEmbeddedPiAgent).toBe("function");
expect(typeof extensionApi.resolveAgentDir).toBe("function");
expect(typeof extensionApi.resolveAgentWorkspaceDir).toBe("function");
expect(typeof extensionApi.resolveAgentTimeoutMs).toBe("function");
expect(typeof extensionApi.ensureAgentWorkspace).toBe("function");
});
it("keeps legacy defaults and session helpers importable", () => {
expect(typeof extensionApi.DEFAULT_MODEL).toBe("string");
expect(typeof extensionApi.DEFAULT_PROVIDER).toBe("string");
expect(typeof extensionApi.resolveStorePath).toBe("function");
expect(typeof extensionApi.loadSessionStore).toBe("function");
expect(typeof extensionApi.saveSessionStore).toBe("function");
expect(typeof extensionApi.resolveSessionFilePath).toBe("function");
});
});

32
src/extensionAPI.ts Normal file
View File

@ -0,0 +1,32 @@
// Legacy compat surface for plugins that still import openclaw/extension-api.
// Keep this file intentionally narrow and forward-only.
const shouldWarnExtensionApiImport =
process.env.VITEST !== "true" &&
process.env.NODE_ENV !== "test" &&
process.env.OPENCLAW_SUPPRESS_EXTENSION_API_WARNING !== "1";
if (shouldWarnExtensionApiImport) {
process.emitWarning(
"openclaw/extension-api is deprecated. Migrate to api.runtime.agent.* or focused openclaw/plugin-sdk/<subpath> imports. See https://docs.openclaw.ai/plugins/sdk-migration",
{
code: "OPENCLAW_EXTENSION_API_DEPRECATED",
detail:
"This compatibility bridge is temporary. Bundled plugins should use the injected plugin runtime instead of importing host-side agent helpers directly. Migration guide: https://docs.openclaw.ai/plugins/sdk-migration",
},
);
}
export { resolveAgentDir, resolveAgentWorkspaceDir } from "./agents/agent-scope.js";
export { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./agents/defaults.js";
export { resolveAgentIdentity } from "./agents/identity.js";
export { resolveThinkingDefault } from "./agents/model-selection.js";
export { runEmbeddedPiAgent } from "./agents/pi-embedded.js";
export { resolveAgentTimeoutMs } from "./agents/timeout.js";
export { ensureAgentWorkspace } from "./agents/workspace.js";
export {
resolveStorePath,
loadSessionStore,
saveSessionStore,
resolveSessionFilePath,
} from "./config/sessions.js";

View File

@ -358,6 +358,23 @@ function createPluginSdkAliasFixture(params?: {
return { root, srcFile, distFile };
}
function createExtensionApiAliasFixture(params?: { srcBody?: string; distBody?: string }) {
const root = makeTempDir();
const srcFile = path.join(root, "src", "extensionAPI.ts");
const distFile = path.join(root, "dist", "extensionAPI.js");
mkdirSafe(path.dirname(srcFile));
mkdirSafe(path.dirname(distFile));
fs.writeFileSync(
path.join(root, "package.json"),
JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
"utf-8",
);
fs.writeFileSync(path.join(root, "openclaw.mjs"), "export {};\n", "utf-8");
fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8");
fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8");
return { root, srcFile, distFile };
}
function createPluginRuntimeAliasFixture(params?: { srcBody?: string; distBody?: string }) {
const root = makeTempDir();
const srcFile = path.join(root, "src", "plugins", "runtime", "index.ts");
@ -3354,6 +3371,36 @@ module.exports = {
expect(resolved).toBe(expected === "dist" ? fixture.distFile : fixture.srcFile);
});
it.each([
{
name: "prefers dist extension-api alias when loader runs from dist",
modulePath: (root: string) => path.join(root, "dist", "plugins", "loader.js"),
expected: "dist" as const,
},
{
name: "prefers src extension-api alias when loader runs from src in non-production",
modulePath: (root: string) => path.join(root, "src", "plugins", "loader.ts"),
env: { NODE_ENV: undefined },
expected: "src" as const,
},
{
name: "resolves extension-api alias from package root when loader runs from transpiler cache path",
modulePath: () => "/tmp/tsx-cache/openclaw-loader.js",
argv1: (root: string) => path.join(root, "openclaw.mjs"),
env: { NODE_ENV: undefined },
expected: "src" as const,
},
])("$name", ({ modulePath, argv1, env, expected }) => {
const fixture = createExtensionApiAliasFixture();
const resolved = withEnv(env ?? {}, () =>
__testing.resolveExtensionApiAlias({
modulePath: modulePath(fixture.root),
argv1: argv1?.(fixture.root),
}),
);
expect(resolved).toBe(expected === "dist" ? fixture.distFile : fixture.srcFile);
});
it.each([
{
name: "prefers dist candidates first for production src runtime",

View File

@ -130,12 +130,42 @@ const resolvePluginSdkAlias = (params: LoaderModuleResolveParams = {}): string |
function buildPluginLoaderAliasMap(modulePath: string): Record<string, string> {
const pluginSdkAlias = resolvePluginSdkAlias({ modulePath });
const extensionApiAlias = resolveExtensionApiAlias({ modulePath });
return {
...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}),
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...resolvePluginSdkScopedAliasMap({ modulePath }),
};
}
const resolveExtensionApiAlias = (params: LoaderModuleResolveParams = {}): string | null => {
try {
const modulePath = resolveLoaderModulePath(params);
const packageRoot = resolveLoaderPackageRoot({ ...params, modulePath });
if (!packageRoot) {
return null;
}
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
modulePath,
isProduction: process.env.NODE_ENV === "production",
});
const candidateMap = {
src: path.join(packageRoot, "src", "extensionAPI.ts"),
dist: path.join(packageRoot, "dist", "extensionAPI.js"),
} as const;
for (const kind of orderedKinds) {
const candidate = candidateMap[kind];
if (fs.existsSync(candidate)) {
return candidate;
}
}
} catch {
// ignore
}
return null;
};
function resolvePluginRuntimeModulePath(params: LoaderModuleResolveParams = {}): string | null {
try {
const modulePath = resolveLoaderModulePath(params);
@ -170,6 +200,7 @@ export const __testing = {
buildPluginLoaderAliasMap,
listPluginSdkAliasCandidates,
listPluginSdkExportedSubpaths,
resolveExtensionApiAlias,
resolvePluginSdkScopedAliasMap,
resolvePluginSdkAliasCandidateOrder,
resolvePluginSdkAliasFile,

View File

@ -18,6 +18,7 @@
"target": "es2023",
"useDefineForClassFields": false,
"paths": {
"openclaw/extension-api": ["./src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"],
"openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"],
"openclaw/plugin-sdk/account-id": ["./src/plugin-sdk/account-id.ts"]

View File

@ -169,6 +169,7 @@ function buildCoreDistEntries(): Record<string, string> {
entry: "src/entry.ts",
// Ensure this module is bundled as an entry so legacy CLI shims can resolve its exports.
"cli/daemon-cli": "src/cli/daemon-cli.ts",
extensionAPI: "src/extensionAPI.ts",
"infra/warning-filter": "src/infra/warning-filter.ts",
"telegram/audit": "extensions/telegram/src/audit.ts",
"telegram/token": "extensions/telegram/src/token.ts",

View File

@ -13,6 +13,10 @@ export default defineConfig({
resolve: {
// Keep this ordered: the base `openclaw/plugin-sdk` alias is a prefix match.
alias: [
{
find: "openclaw/extension-api",
replacement: path.join(repoRoot, "src", "extensionAPI.ts"),
},
...pluginSdkSubpaths.map((subpath) => ({
find: `openclaw/plugin-sdk/${subpath}`,
replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`),