test(plugin-sdk): tighten ACP command dispatch guards

This commit is contained in:
Peter Steinberger
2026-04-06 02:42:48 +01:00
parent 7b47d27d0a
commit bf269e7b67
4 changed files with 148 additions and 5 deletions

View File

@@ -0,0 +1,111 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { shouldBypassAcpDispatchForCommand } from "./dispatch-acp-command-bypass.js";
import { buildTestCtx } from "./test-ctx.js";
describe("shouldBypassAcpDispatchForCommand", () => {
it("returns false for plain-text ACP turns", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
BodyForCommands: "write a test",
BodyForAgent: "write a test",
});
expect(shouldBypassAcpDispatchForCommand(ctx, {} as OpenClawConfig)).toBe(false);
});
it("returns false for ACP slash commands", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
CommandBody: "/acp cancel",
BodyForCommands: "/acp cancel",
BodyForAgent: "/acp cancel",
});
expect(shouldBypassAcpDispatchForCommand(ctx, {} as OpenClawConfig)).toBe(false);
});
it("returns false for ACP reset-tail slash commands", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
CommandSource: "native",
CommandBody: "/new continue with deployment",
BodyForCommands: "/new continue with deployment",
BodyForAgent: "/new continue with deployment",
});
expect(shouldBypassAcpDispatchForCommand(ctx, {} as OpenClawConfig)).toBe(false);
});
it("returns false for slash commands when text commands are disabled", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
CommandBody: "/acp cancel",
BodyForCommands: "/acp cancel",
BodyForAgent: "/acp cancel",
CommandSource: "text",
});
const cfg = {
commands: {
text: false,
},
} as OpenClawConfig;
expect(shouldBypassAcpDispatchForCommand(ctx, cfg)).toBe(false);
});
it("returns false for unauthorized bang-prefixed commands", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
CommandBody: "!poll",
BodyForCommands: "!poll",
BodyForAgent: "!poll",
CommandAuthorized: false,
});
expect(shouldBypassAcpDispatchForCommand(ctx, {} as OpenClawConfig)).toBe(false);
});
it("returns false for bang-prefixed commands when text commands are disabled", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
CommandBody: "!poll",
BodyForCommands: "!poll",
BodyForAgent: "!poll",
CommandAuthorized: true,
CommandSource: "text",
});
const cfg = {
commands: {
text: false,
},
} as OpenClawConfig;
expect(shouldBypassAcpDispatchForCommand(ctx, cfg)).toBe(false);
});
it("returns true for authorized bang-prefixed commands when text commands are enabled", () => {
const ctx = buildTestCtx({
Provider: "discord",
Surface: "discord",
CommandBody: "!poll",
BodyForCommands: "!poll",
BodyForAgent: "!poll",
CommandAuthorized: true,
CommandSource: "text",
});
const cfg = {
commands: {
bash: true,
},
} as OpenClawConfig;
expect(shouldBypassAcpDispatchForCommand(ctx, cfg)).toBe(true);
});
});

View File

@@ -31,16 +31,16 @@ export function shouldBypassAcpDispatchForCommand(
if (!candidate) {
return false;
}
const normalized = candidate.trim();
const allowTextCommands = shouldHandleTextCommands({
cfg,
surface: ctx.Surface ?? ctx.Provider ?? "",
commandSource: ctx.CommandSource,
});
if (maybeResolveTextAlias(candidate, cfg) != null) {
if (!normalized.startsWith("/") && maybeResolveTextAlias(candidate, cfg) != null) {
return allowTextCommands;
}
const normalized = candidate.trim();
if (!normalized.startsWith("!")) {
return false;
}

View File

@@ -55,6 +55,25 @@ describe("tryDispatchAcpReplyHook", () => {
vi.clearAllMocks();
});
it("skips ACP runtime lookup for plain-text deny turns", async () => {
const result = await tryDispatchAcpReplyHook(
{
...event,
sendPolicy: "deny",
ctx: buildTestCtx({
SessionKey: "agent:test:session",
BodyForCommands: "write a test",
BodyForAgent: "write a test",
}),
},
ctx,
);
expect(result).toBeUndefined();
expect(bypassMock).not.toHaveBeenCalled();
expect(dispatchMock).not.toHaveBeenCalled();
});
it("skips ACP dispatch when send policy denies delivery and no bypass applies", async () => {
bypassMock.mockResolvedValue(false);

View File

@@ -42,9 +42,22 @@ function loadDispatchAcpRuntime() {
}
function hasExplicitCommandCandidate(ctx: PluginHookReplyDispatchEvent["ctx"]): boolean {
return [ctx.CommandBody, ctx.BodyForCommands].some(
(value) => typeof value === "string" && value.trim().length > 0,
);
const commandBody = ctx.CommandBody;
if (typeof commandBody === "string" && commandBody.trim().length > 0) {
return true;
}
const bodyForCommands = ctx.BodyForCommands;
if (typeof bodyForCommands !== "string") {
return false;
}
const normalized = bodyForCommands.trim();
if (!normalized) {
return false;
}
return normalized.startsWith("!") || normalized.startsWith("/");
}
export async function tryDispatchAcpReplyHook(