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:
Pavan Kumar Gondhi
2026-05-04 20:23:51 +05:30
committed by GitHub
parent d5edeae6ee
commit e3364ae3bd
6 changed files with 49 additions and 18 deletions

View File

@@ -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.

View File

@@ -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";

View File

@@ -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";

View File

@@ -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", () => {

View File

@@ -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();

View File

@@ -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,