mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 04:20:43 +00:00
fix(plugins): use built code for tool discovery
This commit is contained in:
@@ -215,9 +215,9 @@ describe("resolveBundledPluginsDir", () => {
|
||||
},
|
||||
],
|
||||
[
|
||||
"prefers source extensions during tsx-driven source execution",
|
||||
"still prefers built bundled plugins during tsx-driven source execution",
|
||||
{
|
||||
prefix: "openclaw-bundled-dir-tsx-",
|
||||
prefix: "openclaw-bundled-dir-tsx-built-",
|
||||
hasExtensions: true,
|
||||
hasSrc: true,
|
||||
hasDistRuntimeExtensions: true,
|
||||
@@ -225,7 +225,7 @@ describe("resolveBundledPluginsDir", () => {
|
||||
hasGitCheckout: true,
|
||||
},
|
||||
{
|
||||
expectedRelativeDir: "extensions",
|
||||
expectedRelativeDir: path.join("dist-runtime", "extensions"),
|
||||
execArgv: ["--import", "tsx"],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -121,50 +121,17 @@ function overrideResolvesUnderPackageBundledRoot(params: {
|
||||
.some((trustedRoot) => pathContains(trustedRoot, realOverride));
|
||||
}
|
||||
|
||||
function runningSourceTypeScriptProcess(): boolean {
|
||||
const argv1 = process.argv[1]?.toLowerCase();
|
||||
if (
|
||||
argv1?.endsWith(".ts") ||
|
||||
argv1?.endsWith(".tsx") ||
|
||||
argv1?.endsWith(".mts") ||
|
||||
argv1?.endsWith(".cts")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let index = 0; index < process.execArgv.length; index += 1) {
|
||||
const arg = process.execArgv[index]?.toLowerCase();
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
if (arg === "tsx" || arg.includes("tsx/register")) {
|
||||
return true;
|
||||
}
|
||||
if ((arg === "--import" || arg === "--loader") && process.execArgv[index + 1]) {
|
||||
const next = process.execArgv[index + 1].toLowerCase();
|
||||
if (next === "tsx" || next.includes("tsx/")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveBundledDirFromPackageRoot(
|
||||
packageRoot: string,
|
||||
preferSourceCheckout: boolean,
|
||||
): string | undefined {
|
||||
function resolveBundledDirFromPackageRoot(packageRoot: string): string | undefined {
|
||||
const sourceExtensionsDir = path.join(packageRoot, "extensions");
|
||||
const builtExtensionsDir = path.join(packageRoot, "dist", "extensions");
|
||||
const sourceCheckout = isSourceCheckoutRoot(packageRoot);
|
||||
const hasUsableSourceTree = sourceCheckout && hasUsableBundledPluginTree(sourceExtensionsDir);
|
||||
if (preferSourceCheckout && hasUsableSourceTree) {
|
||||
return sourceExtensionsDir;
|
||||
}
|
||||
// Local source checkouts stage a runtime-complete bundled plugin tree under
|
||||
// dist-runtime/. Prefer that over source extensions only when the paired
|
||||
// dist/ tree exists; otherwise wrappers can drift ahead of the last build.
|
||||
// Even when OpenClaw itself runs from TypeScript, bundled plugins should use
|
||||
// compiled JavaScript whenever it is available. Source plugin entries force
|
||||
// jiti onto hot runtime paths such as per-run tool construction.
|
||||
const runtimeExtensionsDir = path.join(packageRoot, "dist-runtime", "extensions");
|
||||
const hasUsableRuntimeTree = sourceCheckout
|
||||
? hasUsableBundledPluginTree(runtimeExtensionsDir)
|
||||
@@ -209,8 +176,6 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env):
|
||||
}
|
||||
}
|
||||
|
||||
const preferSourceCheckout = runningSourceTypeScriptProcess();
|
||||
|
||||
try {
|
||||
const argvRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1] });
|
||||
const rejectedOverrideUsesArgvRoot = Boolean(
|
||||
@@ -227,7 +192,7 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env):
|
||||
(entry, index, all): entry is string => Boolean(entry) && all.indexOf(entry) === index,
|
||||
);
|
||||
for (const packageRoot of packageRoots) {
|
||||
const bundledDir = resolveBundledDirFromPackageRoot(packageRoot, preferSourceCheckout);
|
||||
const bundledDir = resolveBundledDirFromPackageRoot(packageRoot);
|
||||
if (bundledDir) {
|
||||
return bundledDir;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/plugin-test-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getRegisteredMemoryEmbeddingProvider } from "../memory-embedding-providers.js";
|
||||
import { createPluginRecord } from "../status.test-helpers.js";
|
||||
|
||||
describe("memory embedding provider registration", () => {
|
||||
it("rejects non-memory plugins that did not declare the capability contract", () => {
|
||||
@@ -81,4 +82,41 @@ describe("memory embedding provider registration", () => {
|
||||
ownerPluginId: "memory-core",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps companion embedding providers available during tool discovery", () => {
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
const record = createPluginRecord({
|
||||
id: "tool-discovery-memory",
|
||||
name: "Tool Discovery Memory",
|
||||
kind: "memory",
|
||||
});
|
||||
registry.registry.plugins.push(record);
|
||||
const api = registry.createApi(record, {
|
||||
config,
|
||||
registrationMode: "tool-discovery",
|
||||
});
|
||||
|
||||
api.registerMemoryEmbeddingProvider({
|
||||
id: "tool-discovery-embedding",
|
||||
create: async () => ({ provider: null }),
|
||||
});
|
||||
api.registerTool({
|
||||
name: "memory_recall",
|
||||
label: "Memory Recall",
|
||||
description: "Recall memory",
|
||||
parameters: {},
|
||||
execute: async () => ({ content: [], details: {} }),
|
||||
});
|
||||
|
||||
expect(getRegisteredMemoryEmbeddingProvider("tool-discovery-embedding")).toEqual({
|
||||
adapter: expect.objectContaining({ id: "tool-discovery-embedding" }),
|
||||
ownerPluginId: "tool-discovery-memory",
|
||||
});
|
||||
expect(registry.registry.tools).toEqual([
|
||||
expect.objectContaining({
|
||||
pluginId: "tool-discovery-memory",
|
||||
names: ["memory_recall"],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
const second = getCachedPluginJitiLoader(params);
|
||||
|
||||
expect(second).toBe(first);
|
||||
first("/repo/extensions/demo/index.ts");
|
||||
expect(createJiti).toHaveBeenCalledTimes(1);
|
||||
expect(cache.size).toBe(1);
|
||||
});
|
||||
@@ -70,6 +71,8 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
});
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
first("/repo/dist/extensions/demo/api.ts");
|
||||
second("/repo/dist/extensions/demo/api.ts");
|
||||
expect(createJiti).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"file:///repo/src/plugins/public-surface-loader.ts",
|
||||
@@ -119,6 +122,7 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
});
|
||||
|
||||
expect(second).toBe(first);
|
||||
first("/repo/extensions/demo/index.ts");
|
||||
expect(createJiti).toHaveBeenCalledTimes(1);
|
||||
expect(createJiti).toHaveBeenCalledWith(
|
||||
"file:///repo/src/plugins/loader.ts",
|
||||
@@ -161,6 +165,7 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
});
|
||||
|
||||
expect(second).toBe(first);
|
||||
second("/repo/dist/extensions/demo-b/api.js");
|
||||
expect(createJiti).toHaveBeenCalledTimes(1);
|
||||
expect(cache.size).toBe(1);
|
||||
});
|
||||
@@ -193,6 +198,29 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
tryNative: false,
|
||||
});
|
||||
|
||||
getCachedPluginJitiLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo-a/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "/repo/extensions/demo-a/index.ts",
|
||||
aliasMap: {
|
||||
alpha: "/repo/alpha",
|
||||
beta: "alpha/sub",
|
||||
},
|
||||
tryNative: false,
|
||||
})("/repo/extensions/demo-a/index.ts");
|
||||
getCachedPluginJitiLoader({
|
||||
cache,
|
||||
modulePath: "/repo/extensions/demo-b/index.ts",
|
||||
importerUrl: "file:///repo/src/plugins/loader.ts",
|
||||
jitiFilename: "/repo/extensions/demo-b/index.ts",
|
||||
aliasMap: {
|
||||
beta: "alpha/sub",
|
||||
alpha: "/repo/alpha",
|
||||
},
|
||||
tryNative: false,
|
||||
})("/repo/extensions/demo-b/index.ts");
|
||||
|
||||
const marker = Symbol.for("pathe:normalizedAlias");
|
||||
const firstAlias = (createJiti.mock.calls[0]?.[1] as { alias?: Record<string, string> }).alias;
|
||||
const secondAlias = (createJiti.mock.calls[1]?.[1] as { alias?: Record<string, string> }).alias;
|
||||
@@ -231,8 +259,9 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { loadedFrom: string };
|
||||
expect(result.loadedFrom).toBe("/repo/dist/extensions/demo/api.js");
|
||||
// jiti is created eagerly, but its loader must NOT be invoked for .js
|
||||
// targets that `tryNativeRequireJavaScriptModule` resolves.
|
||||
// Jiti should not be constructed or invoked for .js targets that
|
||||
// `tryNativeRequireJavaScriptModule` resolves.
|
||||
expect(createJiti).not.toHaveBeenCalled();
|
||||
expect(jitiLoader).not.toHaveBeenCalled();
|
||||
// allowWindows must be passed so the native fast path works on Windows too.
|
||||
expect(nativeStub).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", {
|
||||
|
||||
@@ -76,26 +76,41 @@ export function getCachedPluginJitiLoader(params: {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const jitiLoader = (params.createLoader ?? createJiti)(jitiFilename, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
const loadWithJiti = new Proxy(jitiLoader, {
|
||||
apply(target, thisArg, argArray) {
|
||||
const [first, ...rest] = argArray as [unknown, ...unknown[]];
|
||||
if (typeof first === "string") {
|
||||
return Reflect.apply(target, thisArg, [toSafeImportPath(first), ...rest] as never) as never;
|
||||
}
|
||||
return Reflect.apply(target, thisArg, argArray as never) as never;
|
||||
},
|
||||
});
|
||||
let loadWithJiti: PluginJitiLoader | undefined;
|
||||
const getLoadWithJiti = (): PluginJitiLoader => {
|
||||
if (loadWithJiti) {
|
||||
return loadWithJiti;
|
||||
}
|
||||
const jitiLoader = (params.createLoader ?? createJiti)(jitiFilename, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
loadWithJiti = new Proxy(jitiLoader, {
|
||||
apply(target, thisArg, argArray) {
|
||||
const [first, ...rest] = argArray as [unknown, ...unknown[]];
|
||||
if (typeof first === "string") {
|
||||
return Reflect.apply(target, thisArg, [
|
||||
toSafeImportPath(first),
|
||||
...rest,
|
||||
] as never) as never;
|
||||
}
|
||||
return Reflect.apply(target, thisArg, argArray as never) as never;
|
||||
},
|
||||
});
|
||||
return loadWithJiti;
|
||||
};
|
||||
// When the caller has explicitly opted out of native loading (for example
|
||||
// `bundled-capability-runtime` in Vitest+dist mode, which depends on
|
||||
// jiti's alias rewriting to surface a narrow SDK slice), route every
|
||||
// target through jiti so those alias rewrites still apply.
|
||||
if (!tryNative) {
|
||||
params.cache.set(scopedCacheKey, loadWithJiti);
|
||||
return loadWithJiti;
|
||||
const loader = ((target: string, ...rest: unknown[]) =>
|
||||
(getLoadWithJiti() as (t: string, ...a: unknown[]) => unknown)(
|
||||
target,
|
||||
...rest,
|
||||
)) as PluginJitiLoader;
|
||||
params.cache.set(scopedCacheKey, loader);
|
||||
return loader;
|
||||
}
|
||||
// Otherwise prefer native require() for already-compiled JS artifacts
|
||||
// (the bundled plugin public surfaces shipped in dist/). jiti's transform
|
||||
@@ -109,7 +124,7 @@ export function getCachedPluginJitiLoader(params: {
|
||||
if (native.ok) {
|
||||
return native.moduleExport;
|
||||
}
|
||||
return (loadWithJiti as (t: string, ...a: unknown[]) => unknown)(target, ...rest);
|
||||
return (getLoadWithJiti() as (t: string, ...a: unknown[]) => unknown)(target, ...rest);
|
||||
}) as PluginJitiLoader;
|
||||
params.cache.set(scopedCacheKey, loader);
|
||||
return loader;
|
||||
|
||||
@@ -183,6 +183,7 @@ export type PluginLoadOptions = {
|
||||
* via package metadata because their setup entry covers the pre-listen startup surface.
|
||||
*/
|
||||
preferSetupRuntimeForChannelPlugins?: boolean;
|
||||
toolDiscovery?: boolean;
|
||||
activate?: boolean;
|
||||
loadModules?: boolean;
|
||||
installBundledRuntimeDeps?: boolean;
|
||||
@@ -621,6 +622,7 @@ function buildCacheKey(params: {
|
||||
forceSetupOnlyChannelPlugins?: boolean;
|
||||
requireSetupEntryForSetupOnlyChannelPlugins?: boolean;
|
||||
preferSetupRuntimeForChannelPlugins?: boolean;
|
||||
toolDiscovery?: boolean;
|
||||
loadModules?: boolean;
|
||||
installBundledRuntimeDeps?: boolean;
|
||||
runtimeSubagentMode?: "default" | "explicit" | "gateway-bindable";
|
||||
@@ -661,6 +663,7 @@ function buildCacheKey(params: {
|
||||
const startupChannelMode =
|
||||
params.preferSetupRuntimeForChannelPlugins === true ? "prefer-setup" : "full";
|
||||
const moduleLoadMode = params.loadModules === false ? "manifest-only" : "load-modules";
|
||||
const discoveryMode = params.toolDiscovery === true ? "tool-discovery" : "default-discovery";
|
||||
const bundledRuntimeDepsMode =
|
||||
params.installBundledRuntimeDeps === false ? "skip-runtime-deps" : "install-runtime-deps";
|
||||
const runtimeSubagentMode = params.runtimeSubagentMode ?? "default";
|
||||
@@ -672,7 +675,7 @@ function buildCacheKey(params: {
|
||||
installs,
|
||||
loadPaths,
|
||||
activationMetadataKey: params.activationMetadataKey ?? "",
|
||||
})}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${moduleLoadMode}::${bundledRuntimeDepsMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}::${activationMode}`;
|
||||
})}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${moduleLoadMode}::${discoveryMode}::${bundledRuntimeDepsMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}::${activationMode}`;
|
||||
}
|
||||
|
||||
function matchesScopedPluginRequest(params: {
|
||||
@@ -804,6 +807,7 @@ function resolvePluginRegistrationPlan(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
preferSetupRuntimeForChannelPlugins: boolean;
|
||||
toolDiscovery: boolean;
|
||||
}): PluginRegistrationPlan | null {
|
||||
if (params.canLoadScopedSetupOnlyChannelPlugin) {
|
||||
return {
|
||||
@@ -823,6 +827,15 @@ function resolvePluginRegistrationPlan(params: {
|
||||
if (!params.enableStateEnabled) {
|
||||
return null;
|
||||
}
|
||||
if (params.toolDiscovery) {
|
||||
return {
|
||||
mode: "tool-discovery",
|
||||
loadSetupEntry: false,
|
||||
loadSetupRuntimeEntry: false,
|
||||
runRuntimeCapabilityPolicy: true,
|
||||
runFullActivationOnlyRegistrations: false,
|
||||
};
|
||||
}
|
||||
const loadSetupRuntimeEntry =
|
||||
params.shouldLoadModules &&
|
||||
!params.validateOnly &&
|
||||
@@ -901,6 +914,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
|
||||
forceSetupOnlyChannelPlugins,
|
||||
requireSetupEntryForSetupOnlyChannelPlugins,
|
||||
preferSetupRuntimeForChannelPlugins,
|
||||
toolDiscovery: options.toolDiscovery,
|
||||
loadModules: options.loadModules,
|
||||
installBundledRuntimeDeps: options.installBundledRuntimeDeps,
|
||||
runtimeSubagentMode,
|
||||
@@ -1530,6 +1544,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
cfg,
|
||||
env,
|
||||
preferSetupRuntimeForChannelPlugins,
|
||||
toolDiscovery: options.toolDiscovery === true,
|
||||
});
|
||||
|
||||
if (!registrationPlan) {
|
||||
|
||||
@@ -256,7 +256,7 @@ type HookRollbackEntry = { name: string; previousRegistrations: HookRegistration
|
||||
type PluginRegistrationCapabilities = {
|
||||
/** Broad registry writes that discovery and live activation both need. */
|
||||
capabilityHandlers: boolean;
|
||||
/** Runtime channel registration is suppressed for setup-only metadata loads. */
|
||||
/** Runtime channel registration is suppressed for setup-only and tool discovery loads. */
|
||||
runtimeChannel: boolean;
|
||||
};
|
||||
|
||||
@@ -268,9 +268,10 @@ type PluginRegistrationCapabilities = {
|
||||
function resolvePluginRegistrationCapabilities(
|
||||
mode: PluginRegistrationMode,
|
||||
): PluginRegistrationCapabilities {
|
||||
const capabilityHandlers = mode === "full" || mode === "discovery" || mode === "tool-discovery";
|
||||
return {
|
||||
capabilityHandlers: mode === "full" || mode === "discovery",
|
||||
runtimeChannel: mode !== "setup-only",
|
||||
capabilityHandlers,
|
||||
runtimeChannel: mode !== "setup-only" && mode !== "tool-discovery",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,8 @@ export function resolvePluginTools(params: {
|
||||
: undefined;
|
||||
const loadOptions = buildPluginRuntimeLoadOptions(context, {
|
||||
installBundledRuntimeDeps: false,
|
||||
activate: false,
|
||||
toolDiscovery: true,
|
||||
runtimeOptions,
|
||||
});
|
||||
const registry = resolvePluginToolRegistry({
|
||||
|
||||
@@ -2217,6 +2217,7 @@ export type OpenClawPluginModule = OpenClawPluginDefinition | ((api: OpenClawPlu
|
||||
*
|
||||
* - `full`: live runtime activation; long-lived side effects may start.
|
||||
* - `discovery`: read-only capability discovery; skip sockets/workers/clients.
|
||||
* - `tool-discovery`: capability discovery for executable tools; skip channel runtime hydration.
|
||||
* - `setup-only`: lightweight channel setup entry only.
|
||||
* - `setup-runtime`: setup flow that also needs the runtime channel entry.
|
||||
* - `cli-metadata`: CLI command metadata collection.
|
||||
@@ -2224,6 +2225,7 @@ export type OpenClawPluginModule = OpenClawPluginDefinition | ((api: OpenClawPlu
|
||||
export type PluginRegistrationMode =
|
||||
| "full"
|
||||
| "discovery"
|
||||
| "tool-discovery"
|
||||
| "setup-only"
|
||||
| "setup-runtime"
|
||||
| "cli-metadata";
|
||||
|
||||
Reference in New Issue
Block a user