Compare commits

...

3 Commits

Author SHA1 Message Date
Josh Lehman
c6f8e83f16 fix: scope Slack plugin native command lookup 2026-04-13 11:18:17 -07:00
rafaelreis-r
7eb9874553 fix: address review feedback on plugin command gate
- Deny unknown/unloaded providers by default (!channelPlugin → return [])
- Add Slack to test registry with capabilities.nativeCommands to validate
  the intended code path instead of null-fallback
- Consolidate duplicate getPluginCommandSpecs import in slash.ts
- Regenerate plugin-sdk API baseline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 11:18:16 -07:00
rafaelreis-r
0c9440d882 fix: allow plugin commands on Slack when channel supports native commands
The `getPluginCommandSpecs` gate only checked `nativeCommandsAutoEnabled`,
which Slack sets to `false`. This caused plugin-registered slash commands
(e.g. /lcm, /lossless from lossless-claw) to be silently excluded from
Slack, even though Slack declares `capabilities.nativeCommands: true` and
the user explicitly enables native commands via config.

The fix widens the gate to also pass when the channel plugin declares
`capabilities.nativeCommands === true`, and adds the
`getPluginCommandSpecs` integration to the Slack slash command setup so
plugin commands are merged into the native command list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 11:18:16 -07:00
8 changed files with 68 additions and 3 deletions

View File

@@ -261,6 +261,7 @@ Docs: https://docs.openclaw.ai
- Daemon/launchd: keep `openclaw gateway stop` persistent without uninstalling the macOS LaunchAgent, re-enable it on explicit restart or repair, and harden launchd label handling. (#64447) Thanks @ngutman.
- Plugins/context engines: preserve `plugins.slots.contextEngine` through normalization and keep explicitly selected workspace context-engine plugins enabled, so loader diagnostics and plugin activation stop dropping that slot selection. (#64192) Thanks @hclsys.
- Heartbeat: stop top-level `interval:` and `prompt:` fields outside the `tasks:` block from bleeding into the last parsed heartbeat task. (#64488) Thanks @Rahulkumar070.
- Slack/plugin commands: include plugin-registered slash commands in Slack native command registration when Slack native commands are enabled. (#64578) Thanks @rafaelreis-r.
- Agents/OpenAI replay: preserve malformed function-call arguments in stored assistant history, avoid double-encoding preserved raw strings on replay, and coerce replayed string args back to objects at Anthropic and Google provider boundaries. (#61956) Thanks @100yenadmin.
- Heartbeat/config: accept and honor `agents.defaults.heartbeat.timeoutSeconds` and per-agent heartbeat timeout overrides for heartbeat agent turns. (#64491) Thanks @cedillarack.
- CLI/devices: make implicit `openclaw devices approve` selection preview-only and require approving the exact request ID, preventing latest-request races during device pairing. (#64160) Thanks @coygeek.

View File

@@ -1,2 +1,2 @@
600f05b14825fa01eb9d63ab6cab5f33c74ff44a48cab5c65457ab08e5b0e91a plugin-sdk-api-baseline.json
99d649a86a30756b18b91686f3683e6e829c5e316e1370266ec4fee344bc55cb plugin-sdk-api-baseline.jsonl
42a93d8368fd40f6bbe3045ba89b84a28e1131c700d4e57580febd3e773b23a4 plugin-sdk-api-baseline.json
515333c277b725abaccf4fd5ab8c5e58b2de39b26e1fe4738f31852fcf789c96 plugin-sdk-api-baseline.jsonl

View File

@@ -3,6 +3,7 @@ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pi
import {
resolveCommandAuthorizedFromAuthorizers,
resolveNativeCommandSessionTargets,
listProviderPluginCommandSpecs,
} from "openclaw/plugin-sdk/command-auth";
import { type ChatCommandDefinition, type CommandArgs } from "openclaw/plugin-sdk/command-auth";
import {
@@ -670,6 +671,17 @@ export async function registerSlackMonitorSlashCommands(params: {
skillCommands,
provider: "slack",
});
const existingNativeNames = new Set(
nativeCommands.map((c) => normalizeLowercaseStringOrEmpty(c.name)).filter(Boolean),
);
for (const pluginCommand of listProviderPluginCommandSpecs("slack")) {
const normalizedName = normalizeLowercaseStringOrEmpty(pluginCommand.name);
if (!normalizedName || existingNativeNames.has(normalizedName)) {
continue;
}
existingNativeNames.add(normalizedName);
nativeCommands.push(pluginCommand);
}
}
if (nativeCommands.length > 0) {

View File

@@ -76,6 +76,10 @@ export {
listSkillCommandsForWorkspace,
resolveSkillCommandInvocation,
} from "../auto-reply/skill-commands.js";
export {
getPluginCommandSpecs,
listProviderPluginCommandSpecs,
} from "../plugins/command-registration.js";
export type { SkillCommandSpec } from "../agents/skills.js";
export {
buildModelsProviderData,

View File

@@ -8,6 +8,7 @@ import {
clearPluginCommandsForPlugin,
getPluginCommandSpecs,
isPluginCommandRegistryLocked,
listProviderPluginCommandSpecs,
pluginCommands,
type RegisteredPluginCommand,
} from "./command-registry-state.js";
@@ -196,5 +197,10 @@ export function registerPluginCommand(
return { ok: true };
}
export { clearPluginCommands, clearPluginCommandsForPlugin, getPluginCommandSpecs };
export {
clearPluginCommands,
clearPluginCommandsForPlugin,
getPluginCommandSpecs,
listProviderPluginCommandSpecs,
};
export type { RegisteredPluginCommand };

View File

@@ -79,6 +79,15 @@ export function getPluginCommandSpecs(provider?: string): Array<{
) {
return [];
}
return listProviderPluginCommandSpecs(provider);
}
/** Resolve plugin command specs for a provider's native naming surface without support gating. */
export function listProviderPluginCommandSpecs(provider?: string): Array<{
name: string;
description: string;
acceptsArgs: boolean;
}> {
return Array.from(pluginCommands.values()).map((cmd) => ({
name: resolvePluginNativeName(cmd, provider),
description: cmd.description,

View File

@@ -5,6 +5,7 @@ import {
clearPluginCommands,
executePluginCommand,
getPluginCommandSpecs,
listProviderPluginCommandSpecs,
listPluginCommands,
matchPluginCommand,
registerPluginCommand,
@@ -202,6 +203,17 @@ beforeEach(() => {
},
},
},
{
pluginId: "slack",
source: "test",
plugin: {
...createChannelTestPluginBase({
id: "slack",
label: "Slack",
capabilities: { nativeCommands: true, chatTypes: ["direct", "group"] },
}),
},
},
]),
);
});
@@ -300,6 +312,25 @@ describe("registerPluginCommand", () => {
]);
});
it("allows Slack to resolve provider-native plugin specs without changing shared native gating", () => {
const result = registerVoiceCommandForTest({
nativeNames: {
default: "talkvoice",
discord: "discordvoice",
},
description: "Demo command",
});
expect(result).toEqual({ ok: true });
expect(listProviderPluginCommandSpecs("slack")).toEqual([
{
name: "talkvoice",
description: "Demo command",
acceptsArgs: false,
},
]);
});
it("accepts native progress metadata on plugin commands", () => {
const result = registerVoiceCommandForTest({
nativeProgressMessages: { telegram: "Running voice command..." },

View File

@@ -14,6 +14,7 @@ import {
clearPluginCommandsForPlugin,
getPluginCommandSpecs,
listPluginInvocationKeys,
listProviderPluginCommandSpecs,
registerPluginCommand,
validateCommandName,
validatePluginCommandDefinition,
@@ -42,6 +43,7 @@ export {
clearPluginCommands,
clearPluginCommandsForPlugin,
getPluginCommandSpecs,
listProviderPluginCommandSpecs,
registerPluginCommand,
validateCommandName,
validatePluginCommandDefinition,