mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(qqbot): keep private commands off framework surface [AI] (#77212)
* fix: keep private qqbot commands off framework surface * addressing codex review * docs: add changelog entry for PR merge
This commit is contained in:
committed by
GitHub
parent
d5edeae6ee
commit
e3364ae3bd
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
|
||||
- Memory/wiki: preserve representation from both corpora in `corpus=all` searches while backfilling unused result capacity, so memory hits are not starved by numerically higher wiki integer scores. Fixes #77337. Thanks @hclsys.
|
||||
- Telegram: clean up tool-only draft previews after assistant message boundaries so transient `Surfacing...` tool-status bubbles do not linger when no matching final preview arrives. Thanks @BunsDev.
|
||||
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* Register all `requireAuth: true` slash commands with the framework via
|
||||
* Register slash commands that are allowed on the framework surface via
|
||||
* `api.registerCommand`.
|
||||
*
|
||||
* Routing through the framework lets `resolveCommandAuthorization()` apply
|
||||
* `commands.allowFrom.qqbot` precedence and the `qqbot:` prefix normalization
|
||||
* before any QQBot command handler runs.
|
||||
*
|
||||
* This module is intentionally thin: it wires the engine-side command
|
||||
* registry (`getFrameworkCommands`) to the framework registration surface via
|
||||
* the three single-responsibility helpers in this directory.
|
||||
* This module is intentionally thin: it wires the engine-side command registry
|
||||
* (`getFrameworkCommands`) to the framework registration surface via the three
|
||||
* single-responsibility helpers in this directory.
|
||||
*/
|
||||
|
||||
import type { OpenClawPluginApi, PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
* "qqbot:<id>" in channel.allowFrom matches the inbound event.senderId "<id>".
|
||||
* Verified against the normalization logic in the gateway.ts inbound path.
|
||||
*
|
||||
* Note: commands.allowFrom.qqbot precedence over channel allowFrom is enforced
|
||||
* by the framework's resolveCommandAuthorization(). QQBot routes requireAuth:true
|
||||
* commands through the framework (api.registerCommand), so that behavior is
|
||||
* covered by the framework's own tests rather than duplicated here.
|
||||
* Note: framework command authorization precedence is covered by the
|
||||
* framework's own tests rather than duplicated here.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { resolveQQBotCommandsAllowFrom, resolveSlashCommandAuth } from "./slash-command-auth.js";
|
||||
import { getWrittenQQBotConfig, installCommandRuntime } from "./slash-command-test-support.js";
|
||||
import { getFrameworkCommands, matchSlashCommand } from "./slash-commands-impl.js";
|
||||
import type { SlashCommandContext } from "./slash-commands.js";
|
||||
import { SlashCommandRegistry, type SlashCommandContext } from "./slash-commands.js";
|
||||
|
||||
function createStreamingContext(overrides: Partial<SlashCommandContext> = {}): SlashCommandContext {
|
||||
return {
|
||||
@@ -29,8 +29,39 @@ function createStreamingContext(overrides: Partial<SlashCommandContext> = {}): S
|
||||
}
|
||||
|
||||
describe("QQBot framework slash commands", () => {
|
||||
it("routes bot-approve through the auth-gated framework registry", () => {
|
||||
expect(getFrameworkCommands().map((command) => command.name)).toContain("bot-approve");
|
||||
it("exposes private-only admin commands with private-chat metadata", () => {
|
||||
const commands = getFrameworkCommands();
|
||||
const names = commands.map((command) => command.name);
|
||||
|
||||
expect(names).toEqual(
|
||||
expect.arrayContaining(["bot-approve", "bot-clear-storage", "bot-logs", "bot-streaming"]),
|
||||
);
|
||||
for (const commandName of ["bot-approve", "bot-clear-storage", "bot-logs", "bot-streaming"]) {
|
||||
expect(commands.find((command) => command.name === commandName)?.c2cOnly).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves private-only auth metadata for framework registration", () => {
|
||||
const registry = new SlashCommandRegistry();
|
||||
registry.register({
|
||||
name: "private-admin",
|
||||
description: "private admin command",
|
||||
requireAuth: true,
|
||||
c2cOnly: true,
|
||||
handler: () => "ok",
|
||||
});
|
||||
registry.register({
|
||||
name: "shared-admin",
|
||||
description: "shared admin command",
|
||||
requireAuth: true,
|
||||
handler: () => "ok",
|
||||
});
|
||||
|
||||
const commands = registry.getFrameworkCommands();
|
||||
|
||||
expect(commands.map((command) => command.name)).toEqual(["private-admin", "shared-admin"]);
|
||||
expect(commands.find((command) => command.name === "private-admin")?.c2cOnly).toBe(true);
|
||||
expect(commands.find((command) => command.name === "shared-admin")?.c2cOnly).toBeUndefined();
|
||||
});
|
||||
|
||||
it("routes bot-streaming through the auth-gated framework registry", () => {
|
||||
|
||||
@@ -32,8 +32,8 @@ export function initCommands(port: CommandsPort): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all commands that require authorization, for registration with the
|
||||
* framework via api.registerCommand() in registerFull().
|
||||
* Return commands that may be registered with the framework via
|
||||
* api.registerCommand() in registerFull().
|
||||
*/
|
||||
export function getFrameworkCommands(): QQBotFrameworkCommand[] {
|
||||
return registry.getFrameworkCommands();
|
||||
|
||||
@@ -100,8 +100,8 @@ function lc(s: string): string {
|
||||
* Slash command registry.
|
||||
*
|
||||
* Maintains two maps:
|
||||
* - `commands` — pre-dispatch commands (requireAuth: false)
|
||||
* - `frameworkCommands` — auth-gated commands (requireAuth: true)
|
||||
* - `commands` — QQBot message-flow commands
|
||||
* - `frameworkCommands` — auth-gated commands that are safe on the framework surface
|
||||
*/
|
||||
export class SlashCommandRegistry {
|
||||
private readonly commands = new Map<string, SlashCommand>();
|
||||
@@ -113,14 +113,15 @@ export class SlashCommandRegistry {
|
||||
// Always register in the pre-dispatch map so QQ message-flow slash
|
||||
// commands can match and execute directly (with requireAuth gating).
|
||||
this.commands.set(key, cmd);
|
||||
// Auth-gated commands are additionally exposed to the framework command
|
||||
// surface (api.registerCommand) for CLI / control-plane invocation.
|
||||
// Auth-gated commands are exposed to the framework command surface.
|
||||
// Private-chat-only metadata is preserved so the bridge can enforce the
|
||||
// same routing restriction before dispatching handlers.
|
||||
if (cmd.requireAuth) {
|
||||
this.frameworkCommands.set(key, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return all auth-gated commands for framework registration. */
|
||||
/** Return all commands that may be registered on the framework surface. */
|
||||
getFrameworkCommands(): QQBotFrameworkCommand[] {
|
||||
return Array.from(this.frameworkCommands.values()).map((cmd) => ({
|
||||
name: cmd.name,
|
||||
|
||||
Reference in New Issue
Block a user