diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fefed4bedd..24b895dc6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Security: require operator.approvals for gateway /approve commands. (#1) Thanks @mitsuhiko, @yueyueL. - Security: Matrix allowlists now require full MXIDs; ambiguous name resolution no longer grants access. Thanks @MegaManSec. - Security: enforce access-group gating for Slack slash commands when channel type lookup fails. - Security: require validated shared-secret auth before skipping device identity on gateway connect. diff --git a/src/auto-reply/reply/commands-approve.test.ts b/src/auto-reply/reply/commands-approve.test.ts index 6add5a19cea..5d860ee12d9 100644 --- a/src/auto-reply/reply/commands-approve.test.ts +++ b/src/auto-reply/reply/commands-approve.test.ts @@ -98,4 +98,52 @@ describe("/approve command", () => { expect(result.reply?.text).toContain("requires operator.approvals"); expect(mockCallGateway).not.toHaveBeenCalled(); }); + + it("allows gateway clients with approvals scope", async () => { + const cfg = { + commands: { text: true }, + } as OpenClawConfig; + const params = buildParams("/approve abc allow-once", cfg, { + Provider: "webchat", + Surface: "webchat", + GatewayClientScopes: ["operator.approvals"], + }); + + const mockCallGateway = vi.mocked(callGateway); + mockCallGateway.mockResolvedValueOnce({ ok: true }); + + const result = await handleCommands(params); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Exec approval allow-once submitted"); + expect(mockCallGateway).toHaveBeenCalledWith( + expect.objectContaining({ + method: "exec.approval.resolve", + params: { id: "abc", decision: "allow-once" }, + }), + ); + }); + + it("allows gateway clients with admin scope", async () => { + const cfg = { + commands: { text: true }, + } as OpenClawConfig; + const params = buildParams("/approve abc allow-once", cfg, { + Provider: "webchat", + Surface: "webchat", + GatewayClientScopes: ["operator.admin"], + }); + + const mockCallGateway = vi.mocked(callGateway); + mockCallGateway.mockResolvedValueOnce({ ok: true }); + + const result = await handleCommands(params); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Exec approval allow-once submitted"); + expect(mockCallGateway).toHaveBeenCalledWith( + expect.objectContaining({ + method: "exec.approval.resolve", + params: { id: "abc", decision: "allow-once" }, + }), + ); + }); });