fix(plugins): trust official Codex package commands

This commit is contained in:
Vincent Koc
2026-05-04 03:25:26 -07:00
parent 89a15fddaf
commit 4c40686f9e
5 changed files with 121 additions and 3 deletions

View File

@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins/commands: allow the official ClawHub Codex plugin package to keep reserved `/codex` command ownership, matching the existing npm-managed Codex package behavior. Thanks @vincentkoc.
- Plugins/commands: scope QQBot framework slash commands to the QQBot channel so `/bot-*` command handlers and native specs do not leak onto unrelated chat surfaces. Thanks @vincentkoc.
- fix: harden backend message action gateway routing [AI]. (#76374) Thanks @pgondhi987.
- Gate QQBot streaming command auth [AI]. (#76375) Thanks @pgondhi987.

View File

@@ -161,6 +161,110 @@ describe("host-hook fixture plugin contract", () => {
);
});
it("allows the official ClawHub Codex plugin to keep /codex command ownership", () => {
const { config, registry } = createPluginRegistryFixture();
const codexRoot = path.join("/tmp", ".openclaw", "extensions", "codex");
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "codex",
name: "Codex",
packageName: "@openclaw/codex",
origin: "global",
rootDir: codexRoot,
source: path.join(codexRoot, "dist", "index.js"),
}),
register(api) {
api.registerCommand({
name: "codex",
description: "Official ClawHub Codex command",
ownership: "reserved",
handler: async () => ({ text: "ok" }),
});
},
});
expect(registry.registry.commands.map((entry) => entry.command.name)).toEqual(["codex"]);
expect(registry.registry.diagnostics).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
pluginId: "codex",
message: expect.stringContaining("only bundled plugins can claim reserved command"),
}),
]),
);
});
it("rejects non-official global Codex plugins from /codex command ownership", () => {
const { config, registry } = createPluginRegistryFixture();
const codexRoot = path.join("/tmp", ".openclaw", "extensions", "codex");
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "codex",
name: "Codex",
origin: "global",
rootDir: codexRoot,
source: path.join(codexRoot, "dist", "index.js"),
}),
register(api) {
api.registerCommand({
name: "codex",
description: "Impostor Codex command",
ownership: "reserved",
handler: async () => ({ text: "no" }),
});
},
});
expect(registry.registry.commands).toHaveLength(0);
expect(registry.registry.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
pluginId: "codex",
message: expect.stringContaining("only bundled plugins can claim reserved command"),
}),
]),
);
});
it("rejects workspace Codex plugins that spoof the official package name", () => {
const { config, registry } = createPluginRegistryFixture();
const codexRoot = path.join("/tmp", "workspace", "codex");
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "codex",
name: "Codex",
packageName: "@openclaw/codex",
origin: "workspace",
rootDir: codexRoot,
source: path.join(codexRoot, "dist", "index.js"),
}),
register(api) {
api.registerCommand({
name: "codex",
description: "Workspace Codex command",
ownership: "reserved",
handler: async () => ({ text: "no" }),
});
},
});
expect(registry.registry.commands).toHaveLength(0);
expect(registry.registry.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
pluginId: "codex",
message: expect.stringContaining("only bundled plugins can claim reserved command"),
}),
]),
);
});
it("rejects reserved command ownership for non-reserved bundled command names", () => {
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({

View File

@@ -1731,6 +1731,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
name: manifestRecord.name ?? pluginId,
description: manifestRecord.description,
version: manifestRecord.version,
packageName: manifestRecord.packageName,
format: manifestRecord.format,
bundleFormat: manifestRecord.bundleFormat,
bundleCapabilities: manifestRecord.bundleCapabilities,
@@ -1768,6 +1769,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
name: manifestRecord.name ?? pluginId,
description: manifestRecord.description,
version: manifestRecord.version,
packageName: manifestRecord.packageName,
format: manifestRecord.format,
bundleFormat: manifestRecord.bundleFormat,
bundleCapabilities: manifestRecord.bundleCapabilities,
@@ -2553,6 +2555,7 @@ export async function loadOpenClawPluginCliRegistry(
name: manifestRecord.name ?? pluginId,
description: manifestRecord.description,
version: manifestRecord.version,
packageName: manifestRecord.packageName,
format: manifestRecord.format,
bundleFormat: manifestRecord.bundleFormat,
bundleCapabilities: manifestRecord.bundleCapabilities,
@@ -2590,6 +2593,7 @@ export async function loadOpenClawPluginCliRegistry(
name: manifestRecord.name ?? pluginId,
description: manifestRecord.description,
version: manifestRecord.version,
packageName: manifestRecord.packageName,
format: manifestRecord.format,
bundleFormat: manifestRecord.bundleFormat,
bundleCapabilities: manifestRecord.bundleCapabilities,

View File

@@ -327,6 +327,7 @@ export type PluginRecord = {
id: string;
name: string;
version?: string;
packageName?: string;
description?: string;
format?: PluginFormat;
bundleFormat?: PluginBundleFormat;

View File

@@ -271,10 +271,18 @@ export function resolvePluginPath(input: string, rootDir: string | undefined): s
return rootDir ? path.resolve(rootDir, trimmed) : resolveUserPath(input);
}
function isOfficialNpmCodexPluginRecord(record: Pick<PluginRecord, "id" | "rootDir" | "source">) {
function isOfficialCodexPluginRecord(
record: Pick<PluginRecord, "id" | "origin" | "packageName" | "rootDir" | "source">,
) {
if (record.id !== "codex") {
return false;
}
if (record.origin !== "global") {
return false;
}
if (record.packageName === "@openclaw/codex") {
return true;
}
const sourcePath = path
.normalize(record.rootDir ?? record.source)
.split(path.sep)
@@ -283,9 +291,9 @@ function isOfficialNpmCodexPluginRecord(record: Pick<PluginRecord, "id" | "rootD
}
function canClaimReservedCommandOwnership(
record: Pick<PluginRecord, "id" | "origin" | "rootDir" | "source">,
record: Pick<PluginRecord, "id" | "origin" | "packageName" | "rootDir" | "source">,
) {
return record.origin === "bundled" || isOfficialNpmCodexPluginRecord(record);
return record.origin === "bundled" || isOfficialCodexPluginRecord(record);
}
const ACTIVE_PLUGIN_HOOK_REGISTRATIONS_KEY = Symbol.for("openclaw.activePluginHookRegistrations");