diff --git a/docs/plugins/building-plugins.md b/docs/plugins/building-plugins.md index 026ac4492de..eea080fb406 100644 --- a/docs/plugins/building-plugins.md +++ b/docs/plugins/building-plugins.md @@ -166,6 +166,11 @@ my-plugin/ Always import from specific `openclaw/plugin-sdk/\` 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"; ``` @@ -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 diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 83970720578..53ac7d71750 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -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/\`) instead. +OpenClaw is migrating from broad compatibility surfaces to narrower, documented +contracts: + +- `openclaw/plugin-sdk/compat` -> focused `openclaw/plugin-sdk/` imports +- `openclaw/extension-api` -> injected runtime helpers such as `api.runtime.agent.*` + +This page explains what changed, why, and how to migrate. The compat import still works at runtime. This is a deprecation warning, not @@ -32,19 +39,21 @@ with a clear purpose. - 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/ ``` - - Each export maps to a specific subpath. Replace the import source: + + 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/ 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. ```bash pnpm build - pnpm test -- my-plugin/ + pnpm test -- extensions/my-plugin/ ``` @@ -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. diff --git a/package.json b/package.json index 646027a2cb5..8c8572581f9 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/extensionAPI.test.ts b/src/extensionAPI.test.ts new file mode 100644 index 00000000000..d2d9bf657a0 --- /dev/null +++ b/src/extensionAPI.test.ts @@ -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"); + }); +}); diff --git a/src/extensionAPI.ts b/src/extensionAPI.ts new file mode 100644 index 00000000000..267ba27ab3c --- /dev/null +++ b/src/extensionAPI.ts @@ -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/ 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"; diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index a4bf12fad15..8d50d1148c8 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -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", diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 03a1b0810ff..6f5900f8334 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -130,12 +130,42 @@ const resolvePluginSdkAlias = (params: LoaderModuleResolveParams = {}): string | function buildPluginLoaderAliasMap(modulePath: string): Record { 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, diff --git a/tsconfig.json b/tsconfig.json index bc6439e921f..e2f9e4ff97e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] diff --git a/tsdown.config.ts b/tsdown.config.ts index 98dd9e3d341..304f781d91d 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -169,6 +169,7 @@ function buildCoreDistEntries(): Record { 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", diff --git a/vitest.config.ts b/vitest.config.ts index f254bcdf0a7..568f5dd03e6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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`),