diff --git a/src/auto-reply/reply/commands-approve.ts b/src/auto-reply/reply/commands-approve.ts index 72bb6f5765c..9aa9cb5f620 100644 --- a/src/auto-reply/reply/commands-approve.ts +++ b/src/auto-reply/reply/commands-approve.ts @@ -10,7 +10,7 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message- import { requireGatewayClientScopeForInternalChannel } from "./command-gates.js"; import type { CommandHandler } from "./commands-types.js"; -const COMMAND_REGEX = /^\/approve(?:\s|$)/i; +const COMMAND_REGEX = /^\/?approve(?:\s|$)/i; const FOREIGN_COMMAND_MENTION_REGEX = /^\/approve@([^\s]+)(?:\s|$)/i; const DECISION_ALIASES: Record = { diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index 3d35a26a034..24c888a3b2a 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -799,6 +799,30 @@ describe("/approve command", () => { ); }); + it("accepts bare approve text for Slack-style manual approvals", async () => { + const cfg = { + commands: { text: true }, + channels: { slack: { allowFrom: ["*"] } }, + } as OpenClawConfig; + const params = buildParams("approve abc allow-once", cfg, { + Provider: "slack", + Surface: "slack", + SenderId: "U123", + }); + + callGatewayMock.mockResolvedValue({ ok: true }); + + const result = await handleCommands(params); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Approval allow-once submitted"); + expect(callGatewayMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: "exec.approval.resolve", + params: { id: "abc", decision: "allow-once" }, + }), + ); + }); + it("accepts Telegram command mentions for /approve", async () => { const cfg = createTelegramApproveCfg(); const params = buildParams("/approve@bot abc12345 allow-once", cfg, { @@ -2633,8 +2657,7 @@ describe("handleCommands subagents", () => { }); createTaskRecord({ runtime: "subagent", - ownerKey: "agent:main:main", - scopeKind: "session", + requesterSessionKey: "agent:main:main", childSessionKey: "agent:main:subagent:abc", runId: "run-1", task: "do thing", diff --git a/src/infra/exec-approval-reply.test.ts b/src/infra/exec-approval-reply.test.ts index 8d37961924d..26e61ac7f76 100644 --- a/src/infra/exec-approval-reply.test.ts +++ b/src/infra/exec-approval-reply.test.ts @@ -234,6 +234,10 @@ describe("exec approval reply helpers", () => { approvalId: "req-1", decision: "deny", }); + expect(parseExecApprovalCommandText("approve req-1 allow-once")).toEqual({ + approvalId: "req-1", + decision: "allow-once", + }); expect(parseExecApprovalCommandText("/approve@clover req-1 allow-once")).toEqual({ approvalId: "req-1", decision: "allow-once", diff --git a/src/infra/exec-approval-reply.ts b/src/infra/exec-approval-reply.ts index c502541f4c1..3fe7f6300a6 100644 --- a/src/infra/exec-approval-reply.ts +++ b/src/infra/exec-approval-reply.ts @@ -140,7 +140,7 @@ export function parseExecApprovalCommandText( ): { approvalId: string; decision: ExecApprovalReplyDecision } | null { const trimmed = raw.trim(); const match = trimmed.match( - /^\/approve(?:@[^\s]+)?\s+([A-Za-z0-9][A-Za-z0-9._:-]*)\s+(allow-once|allow-always|always|deny)\b/i, + /^\/?approve(?:@[^\s]+)?\s+([A-Za-z0-9][A-Za-z0-9._:-]*)\s+(allow-once|allow-always|always|deny)\b/i, ); if (!match) { return null;