fix(security): enforce auth for abort triggers and models

This commit is contained in:
Agent
2026-03-01 21:17:29 +00:00
parent c89836a251
commit 3a93a7bb1e
4 changed files with 71 additions and 8 deletions

View File

@@ -19,6 +19,7 @@ import {
type ProviderInfo,
} from "../../telegram/model-buttons.js";
import type { ReplyPayload } from "../types.js";
import { rejectUnauthorizedCommand } from "./command-gates.js";
import type { CommandHandler } from "./commands-types.js";
const PAGE_SIZE_DEFAULT = 20;
@@ -363,6 +364,14 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
if (!allowTextCommands) {
return null;
}
const commandBodyNormalized = params.command.commandBodyNormalized.trim();
if (!commandBodyNormalized.startsWith("/models")) {
return null;
}
const unauthorized = rejectUnauthorizedCommand(params, "/models");
if (unauthorized) {
return unauthorized;
}
const modelsAgentId =
params.agentId ??
@@ -374,7 +383,7 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
const reply = await resolveModelsCommandReply({
cfg: params.cfg,
commandBodyNormalized: params.command.commandBodyNormalized,
commandBodyNormalized,
surface: params.ctx.Surface,
currentModel: params.model ? `${params.provider}/${params.model}` : undefined,
agentDir: modelsAgentDir,

View File

@@ -14,6 +14,7 @@ import {
setAbortMemory,
stopSubagentsForRequester,
} from "./abort.js";
import { rejectUnauthorizedCommand } from "./command-gates.js";
import { persistAbortTargetEntry } from "./commands-session-store.js";
import type { CommandHandler } from "./commands-types.js";
import { clearSessionQueues } from "./queue.js";
@@ -92,11 +93,9 @@ export const handleStopCommand: CommandHandler = async (params, allowTextCommand
if (params.command.commandBodyNormalized !== "/stop") {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /stop from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
);
return { shouldContinue: false };
const unauthorizedStop = rejectUnauthorizedCommand(params, "/stop");
if (unauthorizedStop) {
return unauthorizedStop;
}
const abortTarget = resolveAbortTarget({
ctx: params.ctx,
@@ -151,6 +150,10 @@ export const handleAbortTrigger: CommandHandler = async (params, allowTextComman
if (!isAbortTrigger(params.command.rawBodyNormalized)) {
return null;
}
const unauthorizedAbortTrigger = rejectUnauthorizedCommand(params, "abort trigger");
if (unauthorizedAbortTrigger) {
return unauthorizedAbortTrigger;
}
const abortTarget = resolveAbortTarget({
ctx: params.ctx,
sessionKey: params.sessionKey,

View File

@@ -2,14 +2,14 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { compactEmbeddedPiSession } from "../../agents/pi-embedded.js";
import { abortEmbeddedPiRun, compactEmbeddedPiSession } from "../../agents/pi-embedded.js";
import {
addSubagentRunForTests,
listSubagentRunsForRequester,
resetSubagentRegistryForTests,
} from "../../agents/subagent-registry.js";
import type { OpenClawConfig } from "../../config/config.js";
import { updateSessionStore } from "../../config/sessions.js";
import { updateSessionStore, type SessionEntry } from "../../config/sessions.js";
import * as internalHooks from "../../hooks/internal-hooks.js";
import { clearPluginCommands, registerPluginCommand } from "../../plugins/commands.js";
import { typedCases } from "../../test-utils/typed-cases.js";
@@ -431,6 +431,43 @@ describe("/compact command", () => {
});
});
describe("abort trigger command", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("rejects unauthorized natural-language abort triggers", async () => {
const cfg = {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("stop", cfg);
const sessionEntry: SessionEntry = {
sessionId: "session-1",
updatedAt: Date.now(),
abortedLastRun: false,
};
const sessionStore: Record<string, SessionEntry> = {
[params.sessionKey]: sessionEntry,
};
const result = await handleCommands({
...params,
sessionEntry,
sessionStore,
command: {
...params.command,
isAuthorizedSender: false,
senderId: "unauthorized",
},
});
expect(result).toEqual({ shouldContinue: false });
expect(sessionStore[params.sessionKey]?.abortedLastRun).toBe(false);
expect(vi.mocked(abortEmbeddedPiRun)).not.toHaveBeenCalled();
});
});
describe("buildCommandsPaginationKeyboard", () => {
it("adds agent id to callback data when provided", () => {
const keyboard = buildCommandsPaginationKeyboard(2, 3, "agent-main");
@@ -739,6 +776,19 @@ describe("/models command", () => {
expect(result.reply?.text).toContain("Use: /models <provider>");
});
it("rejects unauthorized /models commands", async () => {
const params = buildPolicyParams("/models", cfg, { Provider: "discord", Surface: "discord" });
const result = await handleCommands({
...params,
command: {
...params.command,
isAuthorizedSender: false,
senderId: "unauthorized",
},
});
expect(result).toEqual({ shouldContinue: false });
});
it("lists providers on telegram (buttons)", async () => {
const params = buildPolicyParams("/models", cfg, { Provider: "telegram", Surface: "telegram" });
const result = await handleCommands(params);