diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3cb8b0eaf..19815ff1121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - Feishu/topic session routing: use `thread_id` as topic session scope fallback when `root_id` is absent, keep first-turn topic keys stable across thread creation, and force thread replies when inbound events already carry topic/thread context. (#29788) Thanks @songyaolun. - Feishu/DM pairing reply target: send pairing challenge replies to `chat:` instead of `user:` so Lark/Feishu private chats with user-id-only sender payloads receive pairing messages reliably. (#31403) Thanks @stakeswky. - Feishu/Lark private DM routing: treat inbound `chat_type: "private"` as direct-message context for pairing/mention-forward/reaction synthetic handling so Lark private chats behave like Feishu p2p DMs. (#31400) Thanks @stakeswky. +- Feishu/Sender lookup permissions: suppress user-facing grant prompts for stale non-existent scope errors (`contact:contact.base:readonly`) during best-effort sender-name resolution so inbound messages continue without repeated false permission notices. (#31761) - Sandbox/workspace mount permissions: make primary `/workspace` bind mounts read-only whenever `workspaceAccess` is not `rw` (including `none`) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang. - Security audit/skills workspace hardening: add `skills.workspace.symlink_escape` warning in `openclaw security audit` when workspace `skills/**/SKILL.md` resolves outside the workspace root (for example symlink-chain drift), plus docs coverage in the security glossary. - Signal/message actions: allow `react` to fall back to `toolContext.currentMessageId` when `messageId` is omitted, matching Telegram behavior and unblocking agent-initiated reactions on inbound turns. (#32217) Thanks @dunamismax. diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 3247dea9b65..3be5dbf8c02 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -1073,6 +1073,67 @@ describe("handleFeishuMessage command authorization", () => { ); }); + it("ignores stale non-existent contact scope permission errors", async () => { + mockShouldComputeCommandAuthorized.mockReturnValue(false); + mockCreateFeishuClient.mockReturnValue({ + contact: { + user: { + get: vi.fn().mockRejectedValue({ + response: { + data: { + code: 99991672, + msg: "permission denied: contact:contact.base:readonly https://open.feishu.cn/app/cli_scope_bug", + }, + }, + }), + }, + }, + }); + + const cfg: ClawdbotConfig = { + channels: { + feishu: { + appId: "cli_scope_bug", + appSecret: "sec_scope_bug", + groups: { + "oc-group": { + requireMention: false, + }, + }, + }, + }, + } as ClawdbotConfig; + + const event: FeishuMessageEvent = { + sender: { + sender_id: { + open_id: "ou-perm-scope", + }, + }, + message: { + message_id: "msg-perm-scope-1", + chat_id: "oc-group", + chat_type: "group", + message_type: "text", + content: JSON.stringify({ text: "hello group" }), + }, + }; + + await dispatchMessage({ cfg, event }); + + expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1); + expect(mockFinalizeInboundContext).toHaveBeenCalledWith( + expect.objectContaining({ + BodyForAgent: expect.not.stringContaining("Permission grant URL"), + }), + ); + expect(mockFinalizeInboundContext).toHaveBeenCalledWith( + expect.objectContaining({ + BodyForAgent: expect.stringContaining("ou-perm-scope: hello group"), + }), + ); + }); + it("routes group sessions by sender when groupSessionScope=group_sender", async () => { mockShouldComputeCommandAuthorized.mockReturnValue(false); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 9bfac4d4d18..3c8da4bc103 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -44,6 +44,13 @@ type PermissionError = { grantUrl?: string; }; +const IGNORED_PERMISSION_SCOPE_TOKENS = ["contact:contact.base:readonly"]; + +function shouldSuppressPermissionErrorNotice(permissionError: PermissionError): boolean { + const message = permissionError.message.toLowerCase(); + return IGNORED_PERMISSION_SCOPE_TOKENS.some((token) => message.includes(token)); +} + function extractPermissionError(err: unknown): PermissionError | null { if (!err || typeof err !== "object") return null; @@ -140,6 +147,10 @@ async function resolveFeishuSenderName(params: { // Check if this is a permission error const permErr = extractPermissionError(err); if (permErr) { + if (shouldSuppressPermissionErrorNotice(permErr)) { + log(`feishu: ignoring stale permission scope error: ${permErr.message}`); + return {}; + } log(`feishu: permission error resolving sender name: code=${permErr.code}`); return { permissionError: permErr }; }