fix(feishu): suppress stale missing-scope grant notices (openclaw#31870) thanks @liuxiaopai-ai

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check (fails on unrelated baseline lint in src/browser/chrome.ts)

Co-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Mark L
2026-03-03 07:34:11 +08:00
committed by GitHub
parent f22fc17c78
commit 55f04636f3
3 changed files with 73 additions and 0 deletions

View File

@@ -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:<chat_id>` instead of `user:<sender_open_id>` 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.

View File

@@ -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);

View File

@@ -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 };
}