diff --git a/CHANGELOG.md b/CHANGELOG.md index 0949ab940b2..a0c90c43b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ Docs: https://docs.openclaw.ai ### Fixes - Security/Feishu: enforce ID-only allowlist matching for DM/group sender authorization, normalize Feishu ID prefixes during checks, and ignore mutable display names so display-name collisions cannot satisfy allowlist entries. This ships in the next npm release. Thanks @jiseoung for reporting. +- Feishu/Commands: in group chats, command authorization now falls back to top-level `channels.feishu.allowFrom` when per-group `allowFrom` is not set, so `/command` no longer gets blocked by an unintended empty allowlist. (#23756) +- Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip `openclaw: workspace:*` from plugin `devDependencies` during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603) - Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks. - Providers/OpenRouter: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed. - Installer/Smoke tests: remove legacy `OPENCLAW_USE_GUM` overrides from docker install-smoke runs so tests exercise installer auto TTY detection behavior directly. diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 0daebe19d04..8f2c306b9c8 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -264,4 +264,52 @@ describe("handleFeishuMessage command authorization", () => { }), ); }); + + it("falls back to top-level allowFrom for group command authorization", async () => { + mockShouldComputeCommandAuthorized.mockReturnValue(true); + mockResolveCommandAuthorizedFromAuthorizers.mockReturnValue(true); + + const cfg: ClawdbotConfig = { + commands: { useAccessGroups: true }, + channels: { + feishu: { + allowFrom: ["ou-admin"], + groups: { + "oc-group": { + requireMention: false, + }, + }, + }, + }, + } as ClawdbotConfig; + + const event: FeishuMessageEvent = { + sender: { + sender_id: { + open_id: "ou-admin", + }, + }, + message: { + message_id: "msg-group-command-fallback", + chat_id: "oc-group", + chat_type: "group", + message_type: "text", + content: JSON.stringify({ text: "/status" }), + }, + }; + + await dispatchMessage({ cfg, event }); + + expect(mockResolveCommandAuthorizedFromAuthorizers).toHaveBeenCalledWith({ + useAccessGroups: true, + authorizers: [{ configured: true, allowed: true }], + }); + expect(mockFinalizeInboundContext).toHaveBeenCalledWith( + expect.objectContaining({ + ChatType: "group", + CommandAuthorized: true, + SenderId: "ou-admin", + }), + ); + }); }); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index ba48abb60cb..1bddac1fe42 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -693,7 +693,9 @@ export async function handleFeishuMessage(params: { return; } - const commandAllowFrom = isGroup ? (groupConfig?.allowFrom ?? []) : effectiveDmAllowFrom; + const commandAllowFrom = isGroup + ? (groupConfig?.allowFrom ?? configAllowFrom) + : effectiveDmAllowFrom; const senderAllowedForCommands = resolveFeishuAllowlistMatch({ allowFrom: commandAllowFrom, senderId: ctx.senderOpenId, diff --git a/package.json b/package.json index bf133a9a266..decade023cd 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "@grammyjs/runner": "^2.0.3", "@grammyjs/transformer-throttler": "^1.2.1", "@homebridge/ciao": "^1.3.5", + "@larksuiteoapi/node-sdk": "^1.59.0", "@line/bot-sdk": "^10.6.0", "@lydell/node-pty": "1.2.0-beta.3", "@mariozechner/pi-agent-core": "0.54.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e9de9be20c..20b006556bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@homebridge/ciao': specifier: ^1.3.5 version: 1.3.5 + '@larksuiteoapi/node-sdk': + specifier: ^1.59.0 + version: 1.59.0 '@line/bot-sdk': specifier: ^10.6.0 version: 10.6.0 diff --git a/scripts/sync-plugin-versions.ts b/scripts/sync-plugin-versions.ts index 865b9b7d4cf..651d44f1944 100644 --- a/scripts/sync-plugin-versions.ts +++ b/scripts/sync-plugin-versions.ts @@ -4,25 +4,9 @@ import { join, resolve } from "node:path"; type PackageJson = { name?: string; version?: string; + devDependencies?: Record; }; -const rootPackagePath = resolve("package.json"); -const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson; -const targetVersion = rootPackage.version; - -if (!targetVersion) { - throw new Error("Root package.json missing version."); -} - -const extensionsDir = resolve("extensions"); -const dirs = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) => - entry.isDirectory(), -); - -const updated: string[] = []; -const changelogged: string[] = []; -const skipped: string[] = []; - function ensureChangelogEntry(changelogPath: string, version: string): boolean { if (!existsSync(changelogPath)) { return false; @@ -42,35 +26,83 @@ function ensureChangelogEntry(changelogPath: string, version: string): boolean { return true; } -for (const dir of dirs) { - const packagePath = join(extensionsDir, dir.name, "package.json"); - let pkg: PackageJson; - try { - pkg = JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson; - } catch { - continue; +function stripWorkspaceOpenclawDevDependency(pkg: PackageJson): boolean { + const devDeps = pkg.devDependencies; + if (!devDeps || devDeps.openclaw !== "workspace:*") { + return false; } - - if (!pkg.name) { - skipped.push(dir.name); - continue; + delete devDeps.openclaw; + if (Object.keys(devDeps).length === 0) { + delete pkg.devDependencies; } - - const changelogPath = join(extensionsDir, dir.name, "CHANGELOG.md"); - if (ensureChangelogEntry(changelogPath, targetVersion)) { - changelogged.push(pkg.name); - } - - if (pkg.version === targetVersion) { - skipped.push(pkg.name); - continue; - } - - pkg.version = targetVersion; - writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`); - updated.push(pkg.name); + return true; } -console.log( - `Synced plugin versions to ${targetVersion}. Updated: ${updated.length}. Changelogged: ${changelogged.length}. Skipped: ${skipped.length}.`, -); +export function syncPluginVersions(rootDir = resolve(".")) { + const rootPackagePath = join(rootDir, "package.json"); + const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson; + const targetVersion = rootPackage.version; + if (!targetVersion) { + throw new Error("Root package.json missing version."); + } + + const extensionsDir = join(rootDir, "extensions"); + const dirs = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) => + entry.isDirectory(), + ); + + const updated: string[] = []; + const changelogged: string[] = []; + const skipped: string[] = []; + const strippedWorkspaceDevDeps: string[] = []; + + for (const dir of dirs) { + const packagePath = join(extensionsDir, dir.name, "package.json"); + let pkg: PackageJson; + try { + pkg = JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson; + } catch { + continue; + } + + if (!pkg.name) { + skipped.push(dir.name); + continue; + } + + const changelogPath = join(extensionsDir, dir.name, "CHANGELOG.md"); + if (ensureChangelogEntry(changelogPath, targetVersion)) { + changelogged.push(pkg.name); + } + + const removedWorkspaceDevDependency = stripWorkspaceOpenclawDevDependency(pkg); + if (removedWorkspaceDevDependency) { + strippedWorkspaceDevDeps.push(pkg.name); + } + + const versionChanged = pkg.version !== targetVersion; + if (!versionChanged && !removedWorkspaceDevDependency) { + skipped.push(pkg.name); + continue; + } + + pkg.version = targetVersion; + writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`); + updated.push(pkg.name); + } + + return { + targetVersion, + updated, + changelogged, + skipped, + strippedWorkspaceDevDeps, + }; +} + +if (import.meta.main) { + const summary = syncPluginVersions(); + console.log( + `Synced plugin versions to ${summary.targetVersion}. Updated: ${summary.updated.length}. Changelogged: ${summary.changelogged.length}. Stripped workspace devDeps: ${summary.strippedWorkspaceDevDeps.length}. Skipped: ${summary.skipped.length}.`, + ); +}