mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(plugins): require declared runtime setup entries
This commit is contained in:
@@ -1024,7 +1024,7 @@ Important examples:
|
||||
| `openclaw.extensions` | Declares native plugin entrypoints. Must stay inside the plugin package directory. |
|
||||
| `openclaw.runtimeExtensions` | Declares built JavaScript runtime entrypoints for installed packages. Must stay inside the plugin package directory. |
|
||||
| `openclaw.setupEntry` | Lightweight setup-only entrypoint used during onboarding, deferred channel startup, and read-only channel status/SecretRef discovery. Must stay inside the plugin package directory. |
|
||||
| `openclaw.runtimeSetupEntry` | Declares the built JavaScript setup entrypoint for installed packages. Must stay inside the plugin package directory. |
|
||||
| `openclaw.runtimeSetupEntry` | Declares the built JavaScript setup entrypoint for installed packages. Requires `setupEntry`, must exist, and must stay inside the plugin package directory. |
|
||||
| `openclaw.channel` | Cheap channel catalog metadata like labels, docs paths, aliases, and selection copy. |
|
||||
| `openclaw.channel.commands` | Static native command and native skill auto-default metadata used by config, audit, and command-list surfaces before channel runtime loads. |
|
||||
| `openclaw.channel.configuredState` | Lightweight configured-state checker metadata that can answer "does env-only setup already exist?" without loading the full channel runtime. |
|
||||
|
||||
@@ -28,9 +28,12 @@ JavaScript when available:
|
||||
`extensions` and `setupEntry` remain valid source entries for workspace and git
|
||||
checkout development. `runtimeExtensions` and `runtimeSetupEntry` are preferred
|
||||
when OpenClaw loads an installed package and let npm packages avoid runtime
|
||||
TypeScript compilation. If an installed package only declares a TypeScript
|
||||
source entry, OpenClaw will use a matching built `dist/*.js` peer when one
|
||||
exists, then fall back to the TypeScript source.
|
||||
TypeScript compilation. Explicit runtime entries are required: `runtimeSetupEntry`
|
||||
requires `setupEntry`, and missing `runtimeExtensions` or `runtimeSetupEntry`
|
||||
artifacts fail install/discovery instead of silently falling back to source. If
|
||||
an installed package only declares a TypeScript source entry, OpenClaw will use a
|
||||
matching built `dist/*.js` peer when one exists, then fall back to the TypeScript
|
||||
source.
|
||||
|
||||
All entry paths must stay inside the plugin package directory. Runtime entries
|
||||
and inferred built JavaScript peers do not make an escaping `extensions` or
|
||||
|
||||
@@ -130,7 +130,9 @@ peer such as `src/index.ts` to `dist/index.js`.
|
||||
Use `openclaw.runtimeExtensions` when published runtime files do not live at the
|
||||
same paths as the source entries. When present, `runtimeExtensions` must contain
|
||||
exactly one entry for every `extensions` entry. Mismatched lists fail install and
|
||||
plugin discovery rather than silently falling back to source paths.
|
||||
plugin discovery rather than silently falling back to source paths. If you also
|
||||
publish `openclaw.setupEntry`, use `openclaw.runtimeSetupEntry` for its built
|
||||
JavaScript peer; that file is required when declared.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -791,6 +791,37 @@ describe("discoverOpenClawPlugins", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects missing explicit runtime setup entries for installed package plugins", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "extensions", "missing-runtime-setup-pack");
|
||||
mkdirSafe(path.join(pluginDir, "src"));
|
||||
mkdirSafe(path.join(pluginDir, "dist"));
|
||||
|
||||
writePluginPackageManifest({
|
||||
packageDir: pluginDir,
|
||||
packageName: "@openclaw/missing-runtime-setup-pack",
|
||||
extensions: ["./dist/index.js"],
|
||||
setupEntry: "./src/setup-entry.ts",
|
||||
runtimeSetupEntry: "./dist/setup-entry.js",
|
||||
});
|
||||
writePluginEntry(path.join(pluginDir, "dist", "index.js"));
|
||||
writePluginEntry(path.join(pluginDir, "src", "setup-entry.ts"));
|
||||
|
||||
const result = await discoverWithStateDir(stateDir, {});
|
||||
const candidate = findCandidateById(result.candidates, "missing-runtime-setup-pack");
|
||||
|
||||
expect(candidate).toBeDefined();
|
||||
expect(candidate?.setupSource).toBeUndefined();
|
||||
expect(
|
||||
result.diagnostics.some(
|
||||
(entry) =>
|
||||
entry.level === "error" &&
|
||||
entry.message.includes("runtime setup entry not found") &&
|
||||
entry.message.includes("./dist/setup-entry.js"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects package runtimeExtensions that do not match extension entries", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "extensions", "runtime-mismatch-pack");
|
||||
|
||||
@@ -920,6 +920,38 @@ describe("installPluginFromArchive", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects package installs when runtimeSetupEntry is missing", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
fs.mkdirSync(path.join(pluginDir, "src"), { recursive: true });
|
||||
fs.mkdirSync(path.join(pluginDir, "dist"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "missing-runtime-setup-plugin",
|
||||
version: "1.0.0",
|
||||
openclaw: {
|
||||
extensions: ["./dist/index.js"],
|
||||
setupEntry: "./src/setup-entry.ts",
|
||||
runtimeSetupEntry: "./dist/setup-entry.js",
|
||||
},
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(path.join(pluginDir, "dist", "index.js"), "export {};\n");
|
||||
fs.writeFileSync(path.join(pluginDir, "src", "setup-entry.ts"), "export {};\n");
|
||||
|
||||
const result = await installPluginFromDir({
|
||||
dirPath: pluginDir,
|
||||
extensionsDir,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.INVALID_OPENCLAW_EXTENSIONS);
|
||||
expect(result.error).toContain("runtime setup entry not found");
|
||||
expect(result.error).toContain("./dist/setup-entry.js");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects package installs when an extension entry is a symlink escape", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const outsideDir = path.join(path.dirname(pluginDir), "outside-symlink");
|
||||
|
||||
@@ -168,6 +168,64 @@ export async function validatePackageExtensionEntriesForInstall(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const packageManifest = getPackageManifestMetadata(params.manifest);
|
||||
const setupEntry = normalizeOptionalString(packageManifest?.setupEntry);
|
||||
const runtimeSetupEntry = normalizeOptionalString(packageManifest?.runtimeSetupEntry);
|
||||
if (runtimeSetupEntry && !setupEntry) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "package.json openclaw.runtimeSetupEntry requires openclaw.setupEntry",
|
||||
};
|
||||
}
|
||||
if (setupEntry) {
|
||||
const sourceEntry = await validatePackageExtensionEntry({
|
||||
packageDir: params.packageDir,
|
||||
entry: setupEntry,
|
||||
label: "setup entry",
|
||||
requireExisting: false,
|
||||
});
|
||||
if (!sourceEntry.ok) {
|
||||
return sourceEntry;
|
||||
}
|
||||
|
||||
if (runtimeSetupEntry) {
|
||||
const runtimeResult = await validatePackageExtensionEntry({
|
||||
packageDir: params.packageDir,
|
||||
entry: runtimeSetupEntry,
|
||||
label: "runtime setup entry",
|
||||
requireExisting: true,
|
||||
});
|
||||
if (!runtimeResult.ok) {
|
||||
return runtimeResult;
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
if (sourceEntry.exists) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
let foundBuiltSetupEntry = false;
|
||||
for (const builtEntry of listBuiltRuntimeEntryCandidates(setupEntry)) {
|
||||
const builtResult = await validatePackageExtensionEntry({
|
||||
packageDir: params.packageDir,
|
||||
entry: builtEntry,
|
||||
label: "inferred runtime setup entry",
|
||||
requireExisting: false,
|
||||
});
|
||||
if (!builtResult.ok) {
|
||||
return builtResult;
|
||||
}
|
||||
if (builtResult.exists) {
|
||||
foundBuiltSetupEntry = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundBuiltSetupEntry) {
|
||||
return { ok: false, error: `setup entry not found: ${setupEntry}` };
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -307,6 +365,7 @@ function resolvePackageRuntimeEntrySource(params: {
|
||||
packageRootRealPath?: string;
|
||||
entryPath: string;
|
||||
runtimeEntryPath?: string;
|
||||
runtimeEntryLabel?: string;
|
||||
origin: PluginOrigin;
|
||||
sourceLabel: string;
|
||||
diagnostics: PluginDiagnostic[];
|
||||
@@ -340,6 +399,12 @@ function resolvePackageRuntimeEntrySource(params: {
|
||||
if (runtimeSource) {
|
||||
return runtimeSource;
|
||||
}
|
||||
params.diagnostics.push({
|
||||
level: "error",
|
||||
message: `${params.runtimeEntryLabel ?? "runtime entry"} not found: ${params.runtimeEntryPath}`,
|
||||
source: params.sourceLabel,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (shouldInferBuiltRuntimeEntry(params.origin)) {
|
||||
@@ -397,6 +462,7 @@ export function resolvePackageSetupSource(params: {
|
||||
: {}),
|
||||
entryPath: setupEntryPath,
|
||||
runtimeEntryPath: normalizeOptionalString(packageManifest?.runtimeSetupEntry),
|
||||
runtimeEntryLabel: "runtime setup entry",
|
||||
origin: params.origin,
|
||||
sourceLabel: params.sourceLabel,
|
||||
diagnostics: params.diagnostics,
|
||||
@@ -435,6 +501,7 @@ export function resolvePackageRuntimeExtensionSources(params: {
|
||||
: {}),
|
||||
entryPath,
|
||||
runtimeEntryPath: runtimeResolution.runtimeExtensions[index],
|
||||
runtimeEntryLabel: "runtime extension entry",
|
||||
origin: params.origin,
|
||||
sourceLabel: params.sourceLabel,
|
||||
diagnostics: params.diagnostics,
|
||||
|
||||
Reference in New Issue
Block a user