feat(plugins): expose the selected runtime entry (#144818)

This commit is contained in:
Peter Steinberger 2026-09-11 02:58:13 -07:00 • committed by GitHub
parent fe1c715de7
commit f0e841aed3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 95 additions and 0 deletions

View file

@ -85,6 +85,7 @@ and external URLs. Registering another provider replaces the current provider.
| `api.version` | `string?` | Plugin version (optional) |
| `api.description` | `string?` | Plugin description (optional) |
| `api.source` | `string` | Plugin source path |
| `api.runtimeSource` | `string?` | Selected runtime entrypoint path, when the loader has selected a runtime artifact |
| `api.rootDir` | `string?` | Plugin root directory (optional) |
| `api.config` | `OpenClawConfig` | Current config snapshot (active in-memory runtime snapshot when available) |
| `api.pluginConfig` | `Record<string, unknown>` | Plugin-specific config from `plugins.entries.<id>.config` |
@ -93,6 +94,14 @@ and external URLs. Registering another provider replaces the current provider.
| `api.registrationMode` | `PluginRegistrationMode` | Current load mode; `"setup-runtime"` is the lightweight setup flow with runtime available |
| `api.resolvePath(input)` | `(string) => string` | Resolve path relative to plugin root |
Use `api.runtimeSource` to locate private modules beside the selected runtime
entrypoint. It records the loader's source, standalone package, or bundled
artifact choice and always identifies the main runtime entry, even during
setup registration. `api.source` and `api.rootDir` retain discovery identity;
they can differ from the selected artifact. `runtimeSource` is absent when no
runtime artifact has been selected, including metadata-only APIs. This path is
a location fact, not authorization to invoke a retired plugin.
## Where each section moved
Every section of the single-page version now lives on this page or on one of

View file

@ -10,6 +10,7 @@ type BuildPluginApiParams = {
version?: string;
description?: string;
source: string;
runtimeSource?: string;
rootDir?: string;
registrationMode: OpenClawPluginApi["registrationMode"];
config: OpenClawConfig;
@ -131,6 +132,7 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
version: params.version,
description: params.description,
source: params.source,
runtimeSource: params.runtimeSource,
rootDir: params.rootDir,
registrationMode: params.registrationMode,
config: params.config,

View file

@ -183,6 +183,8 @@ export type OpenClawPluginApi = {
version?: string;
description?: string;
source: string;
/** Selected runtime entrypoint, independent of setup; absent without runtime artifact selection. */
readonly runtimeSource?: string;
rootDir?: string;
registrationMode: PluginRegistrationMode;
config: OpenClawConfig;

View file

@ -54,6 +54,10 @@ export function hasCompletedPluginRuntimeRegistration(record: ArtifactBoundRecor
);
}
export function getPluginRuntimeEntrySource(record: ArtifactBoundRecord): string | undefined {
return record[RUNTIME_ARTIFACT_SELECTION]?.runtimeEntry.source;
}
export function matchesPluginRuntimeArtifactSelection(
record: ArtifactBoundRecord,
params: RuntimeArtifactSelectionInput & { rootDir: string; source: string },

View file

@ -79,6 +79,45 @@ afterEach(() => {
});
describe("resolvePluginRuntimeArtifact", () => {
it.each(["source", "package-local", "root-bundled"])(
"exposes the selected %s runtime entry to registration",
(layout) => {
const fixture = createBundledPluginFixture();
const entry =
layout === "source"
? fixture.source
: layout === "package-local"
? path.join(fixture.rootDir, "dist", "index.js")
: fixture.builtSource;
fs.mkdirSync(path.dirname(entry), { recursive: true });
fs.writeFileSync(
entry,
`export default {
id: "fixture",
register(api) {
api.registerService({ id: api.runtimeSource ?? "missing runtime source", start() {} });
}
};\n`,
);
const registry = withEnv(
{
OPENCLAW_BUNDLED_PLUGINS_DIR: path.dirname(fixture.rootDir),
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
},
() =>
loadOpenClawPlugins({
cache: false,
config: { plugins: { allow: ["fixture"], entries: { fixture: { enabled: true } } } },
onlyPluginIds: ["fixture"],
preferBuiltPluginArtifacts: layout !== "source",
}),
);
expect(registry.services.map(({ service }) => service.id)).toEqual([entry]);
expect(registry.plugins[0]?.source).toBe(fixture.source);
},
);
it.each(["missing", "present", "staging-symlink", "canonical-directory-symlink"])(
"keeps the execution entry and boundary together for a %s canonical entry",
(layout) => {

View file

@ -15,6 +15,7 @@ import {
schedulePluginSessionTurn,
unschedulePluginSessionTurnsByTag,
} from "./host-hook-scheduled-turns.js";
import { getPluginRuntimeEntrySource } from "./plugin-runtime-artifact-binding.js";
import { isPluginRegistryActivated, isPluginRegistryRetired } from "./registry-lifecycle.js";
import type { PluginRegistrars } from "./registry-registrars.js";
import type { PluginRuntimeResolver } from "./registry-runtime.js";
@ -130,6 +131,7 @@ export function createPluginApiFactory(
version: record.version,
description: record.description,
source: record.source,
runtimeSource: getPluginRuntimeEntrySource(record),
rootDir: record.rootDir,
registrationMode,
config: params.config,

View file

@ -0,0 +1,37 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createPluginRecord } from "./loader-records.js";
import { bindPluginRuntimeArtifactSelection } from "./plugin-runtime-artifact-binding.js";
import { createPluginRegistry } from "./registry.js";
import { createPluginRuntime } from "./runtime/index.js";
describe("plugin API runtime entrypoint", () => {
it("preserves the selected main entry during setup registration without guessing an unselected entry", () => {
const builder = createPluginRegistry({
logger: console,
runtime: createPluginRuntime(),
activateGlobalSideEffects: false,
});
const rootDir = path.resolve("plugins/fixture");
const source = path.join(rootDir, "index.ts");
const record = createPluginRecord({
id: "fixture",
source,
rootDir,
origin: "global",
enabled: true,
configSchema: false,
});
expect(builder.createApi(record, { config: {} }).runtimeSource).toBeUndefined();
const runtimeSource = path.join(rootDir, "dist/index.js");
bindPluginRuntimeArtifactSelection(record, {
runtimeEntry: { source: runtimeSource, rootDir },
setupEntry: { source: path.join(rootDir, "dist/setup.js"), rootDir },
});
const api = builder.createApi(record, { config: {}, registrationMode: "setup-runtime" });
expect(api.runtimeSource).toBe(runtimeSource);
expect(api.source).toBe(source);
expect(api.rootDir).toBe(rootDir);
});
});