diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index c63bc3050b3e..4c46e3953a04 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -afd47fa416fe8fc5a2364078727d811a55842280d7cdf37625480de4f51ff22a plugin-sdk-api-baseline.json -79d5b76cb0ec23311fbdd58043a15bca3353afcc5e9da91348ec5c8124d65e39 plugin-sdk-api-baseline.jsonl +1818f0bf7e1f19917074efbeb8d6627e799f0909d019681cd8e184244de0641f plugin-sdk-api-baseline.json +49dcff59d33606b9675962183b3e5bc10c09141ca9c75db609401efffcacf291 plugin-sdk-api-baseline.jsonl diff --git a/docs/channels/qqbot.md b/docs/channels/qqbot.md index d914cd0f642e..7ee286ab21c2 100644 --- a/docs/channels/qqbot.md +++ b/docs/channels/qqbot.md @@ -267,6 +267,11 @@ Append `?` to any command for usage help (for example `/bot-upgrade ?`). Admin commands (`/bot-me`, `/bot-upgrade`, `/bot-logs`, `/bot-clear-storage`, `/bot-streaming`, `/bot-approve`) are direct-message-only and require the sender's openid in an explicit non-wildcard `allowFrom` list. A wildcard `allowFrom: ["*"]` permits chat but does not grant admin command access. Group messages match against `groupAllowFrom` first and fall back to `allowFrom`. Running an admin command in a group returns a hint rather than silently dropping. +When QQ Bot exec approvals use the default same-chat fallback, native approval +button clicks follow the same explicit non-wildcard command allowlist. To grant +approval-only access without broader command access, configure +`channels.qqbot.execApprovals.approvers`. + ## Engine architecture QQ Bot ships as a self-contained engine inside the plugin: diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md index acba96b48382..1d1e917e8b5e 100644 --- a/docs/plugins/sdk-channel-plugins.md +++ b/docs/plugins/sdk-channel-plugins.md @@ -152,6 +152,8 @@ Most channel plugins do not need approval-specific code. - Use `approvalCapability.render` only when a channel truly needs custom approval payloads instead of the shared renderer. - Use `approvalCapability.describeExecApprovalSetup` when the channel wants the disabled-path reply to explain the exact config knobs needed to enable native exec approvals. The hook receives `{ channel, channelLabel, accountId }`; named-account channels should render account-scoped paths such as `channels..accounts..execApprovals.*` instead of top-level defaults. - If a channel can infer stable owner-like DM identities from existing config, use `createResolvedApproverActionAuthAdapter` from `openclaw/plugin-sdk/approval-runtime` to restrict same-chat `/approve` without adding approval-specific core logic. +- If custom approval auth intentionally allows only same-chat fallback, return `markImplicitSameChatApprovalAuthorization({ authorized: true })` from `openclaw/plugin-sdk/approval-auth-runtime`; otherwise core treats the result as explicit approver authorization. +- If a channel-owned native callback resolves approvals directly, use `isImplicitSameChatApprovalAuthorization(...)` before resolving so implicit fallback still goes through the channel's normal actor authorization. - If a channel needs native approval delivery, keep channel code focused on target normalization plus transport/presentation facts. Use `createChannelExecApprovalProfile`, `createChannelNativeOriginTargetResolver`, `createChannelApproverDmTargetResolver`, and `createApproverRestrictedNativeApprovalCapability` from `openclaw/plugin-sdk/approval-runtime`. Put the channel-specific facts behind `approvalCapability.nativeRuntime`, ideally via `createChannelApprovalNativeRuntimeAdapter(...)` or `createLazyChannelApprovalNativeRuntimeAdapter(...)`, so core can assemble the handler and own request filtering, routing, dedupe, expiry, gateway subscription, and routed-elsewhere notices. `nativeRuntime` is split into a few smaller seams: - `createChannelNativeOriginTargetResolver` uses the shared channel-route matcher by default for `{ to, accountId, threadId }` targets. Pass `targetsMatch` only when a channel has provider-specific equivalence rules, such as Slack timestamp prefix matching. - Pass `normalizeTargetForMatch` to `createChannelNativeOriginTargetResolver` when the channel needs to canonicalize provider ids before the default route matcher or a custom `targetsMatch` callback runs, while preserving the original target for delivery. Use `normalizeTarget` only when the resolved delivery target itself should be canonicalized. diff --git a/extensions/qqbot/src/bridge/sdk-adapter.ts b/extensions/qqbot/src/bridge/sdk-adapter.ts index de545034508c..ee29f11b091b 100644 --- a/extensions/qqbot/src/bridge/sdk-adapter.ts +++ b/extensions/qqbot/src/bridge/sdk-adapter.ts @@ -1,3 +1,4 @@ +import { parseAccessGroupAllowFromEntry } from "openclaw/plugin-sdk/access-groups"; import { createChannelIngressResolver, defineStableChannelIngressIdentity, @@ -133,7 +134,7 @@ async function resolveQQBotSlashCommandAuthorized(params: { (params.isGroup && params.groupAllowFrom && params.groupAllowFrom.length > 0 ? params.groupAllowFrom : params.allowFrom); - const explicitAllowFrom = normalizeQQBotAllowFrom(rawAllowFrom).filter((entry) => entry !== "*"); + const explicitAllowFrom = normalizeQQBotCommandAllowFrom(rawAllowFrom); if (explicitAllowFrom.length === 0) { return false; } @@ -162,3 +163,24 @@ async function resolveQQBotSlashCommandAuthorized(params: { }); return resolved.commandAccess.authorized; } + +function normalizeQQBotCommandAllowFrom( + rawAllowFrom: Array | null | undefined, +): string[] { + const entries: string[] = []; + for (const rawEntry of rawAllowFrom ?? []) { + const entry = String(rawEntry).trim(); + if (!entry) { + continue; + } + if (parseAccessGroupAllowFromEntry(entry)) { + entries.push(entry); + continue; + } + const normalized = normalizeQQBotSenderId(entry); + if (normalized && normalized !== "*") { + entries.push(normalized); + } + } + return entries; +} diff --git a/extensions/qqbot/src/command-auth.test.ts b/extensions/qqbot/src/command-auth.test.ts index e74f2f97d8a0..ee62bb138b92 100644 --- a/extensions/qqbot/src/command-auth.test.ts +++ b/extensions/qqbot/src/command-auth.test.ts @@ -46,6 +46,21 @@ describe("qqbot: prefix normalization for inbound commandAuthorized", () => { return result.commandAccess.authorized; } + async function resolveSlashCommandAuthorized( + rawAllowFrom: string[], + senderId: string, + cfg: Record = {}, + ): Promise { + return await access.resolveSlashCommandAuthorization({ + cfg, + accountId: "default", + conversationId: senderId, + isGroup: false, + senderId, + allowFrom: rawAllowFrom, + }); + } + it("authorizes when allowFrom uses qqbot: prefix and senderId is the bare id", async () => { await expect(resolveInboundCommandAuthorized(["qqbot:USER123"], "USER123")).resolves.toBe(true); }); @@ -66,6 +81,21 @@ describe("qqbot: prefix normalization for inbound commandAuthorized", () => { await expect(resolveInboundCommandAuthorized(["*"], "ANYONE")).resolves.toBe(true); }); + it("authorizes slash commands from access group allowFrom entries", async () => { + await expect( + resolveSlashCommandAuthorized(["accessGroup:operators"], "USER123", { + accessGroups: { + operators: { + type: "message.senders", + members: { + qqbot: ["USER123"], + }, + }, + }, + }), + ).resolves.toBe(true); + }); + it("denies group command auth in an open group without explicit allowlists", async () => { await expect(resolveInboundCommandAuthorized([], "ANYONE", { isGroup: true })).resolves.toBe( false, diff --git a/extensions/qqbot/src/engine/gateway/gateway.ts b/extensions/qqbot/src/engine/gateway/gateway.ts index fa8b0908609b..778a825fb154 100644 --- a/extensions/qqbot/src/engine/gateway/gateway.ts +++ b/extensions/qqbot/src/engine/gateway/gateway.ts @@ -183,6 +183,7 @@ export async function startGateway(ctx: CoreGatewayContext): Promise { const handleInteraction = createInteractionHandler(account, ctx.runtime, log, { getActiveCfg: () => activeCfgProvider.getActiveCfg(), + resolveCommandAuthorized: (params) => adapters.access.resolveSlashCommandAuthorization(params), }); const connection = new GatewayConnection({ diff --git a/extensions/qqbot/src/engine/gateway/interaction-handler.test.ts b/extensions/qqbot/src/engine/gateway/interaction-handler.test.ts index d2601628a1df..b31bcbbe93bb 100644 --- a/extensions/qqbot/src/engine/gateway/interaction-handler.test.ts +++ b/extensions/qqbot/src/engine/gateway/interaction-handler.test.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createSdkAccessAdapter } from "../../bridge/sdk-adapter.js"; import { registerPlatformAdapter, type PlatformAdapter } from "../adapter/index.js"; import type { InteractionEvent } from "../types.js"; import { createInteractionHandler } from "./interaction-handler.js"; @@ -17,13 +18,17 @@ vi.mock("../messaging/sender.js", () => ({ const resolveApprovalMock = vi.fn(async () => true); -const account: GatewayAccount = { - accountId: "default", - appId: "app", - clientSecret: "secret", - markdownSupport: false, - config: {}, -}; +function makeAccount(config: GatewayAccount["config"] = {}): GatewayAccount { + return { + accountId: "default", + appId: "app", + clientSecret: "secret", + markdownSupport: false, + config, + }; +} + +const account = makeAccount(); const runtime = {} as GatewayPluginRuntime; @@ -42,12 +47,13 @@ function makeRestrictedCfg(approvers: string[]): OpenClawConfig { } as OpenClawConfig; } -function makeUnrestrictedCfg(): OpenClawConfig { +function makeCommandAuthorizedFallbackCfg(): OpenClawConfig { return { channels: { qqbot: { appId: "app", clientSecret: "secret", + allowFrom: ["ATTACKER_OPENID"], }, }, } as OpenClawConfig; @@ -172,9 +178,9 @@ describe("createInteractionHandler approval buttons", () => { ); }); - it("allows approval button clicks when exec approvals are not configured", async () => { + it("resolves fallback approval buttons from explicit command-authorized senders", async () => { const handler = createInteractionHandler(account, runtime, undefined, { - getActiveCfg: () => makeUnrestrictedCfg(), + getActiveCfg: () => makeCommandAuthorizedFallbackCfg(), }); handler(makeApprovalEvent()); @@ -184,6 +190,152 @@ describe("createInteractionHandler approval buttons", () => { ); }); + it("delegates fallback approval button auth to the gateway command resolver", async () => { + const access = createSdkAccessAdapter(); + const handler = createInteractionHandler(account, runtime, undefined, { + getActiveCfg: () => + ({ + accessGroups: { + operators: { + type: "message.senders", + members: { + qqbot: ["ATTACKER_OPENID"], + }, + }, + }, + channels: { + qqbot: { + appId: "app", + clientSecret: "secret", + allowFrom: ["accessGroup:operators"], + }, + }, + }) as OpenClawConfig, + resolveCommandAuthorized: (params) => access.resolveSlashCommandAuthorization(params), + }); + + handler(makeApprovalEvent()); + + await vi.waitFor(() => + expect(resolveApprovalMock).toHaveBeenCalledWith("exec:abc12345", "allow-once"), + ); + }); + + it("uses merged account config for fallback button command auth", async () => { + const handler = createInteractionHandler(account, runtime, undefined, { + getActiveCfg: () => + ({ + channels: { + qqbot: { + appId: "app", + clientSecret: "secret", + accounts: { + default: { + allowFrom: ["ATTACKER_OPENID"], + }, + }, + }, + }, + }) as OpenClawConfig, + }); + + handler(makeApprovalEvent()); + + await vi.waitFor(() => + expect(resolveApprovalMock).toHaveBeenCalledWith("exec:abc12345", "allow-once"), + ); + }); + + it("rejects fallback approval buttons from senders without explicit command auth", async () => { + const handler = createInteractionHandler(account, runtime, undefined, { + getActiveCfg: () => + ({ + channels: { + qqbot: { + appId: "app", + clientSecret: "secret", + allowFrom: ["OWNER_OPENID"], + }, + }, + }) as OpenClawConfig, + }); + + handler(makeApprovalEvent()); + + await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled()); + + expect(acknowledgeInteractionMock).toHaveBeenCalledWith( + { appId: "app", clientSecret: "secret" }, + "interaction-1", + 0, + { content: "You are not authorized to approve this request." }, + ); + expect(resolveApprovalMock).not.toHaveBeenCalled(); + }); + + it.each([ + [ + "no allowlist", + { + channels: { + qqbot: { + appId: "app", + clientSecret: "secret", + }, + }, + }, + ], + [ + "wildcard allowlist", + { + channels: { + qqbot: { + appId: "app", + clientSecret: "secret", + allowFrom: ["*"], + }, + }, + }, + ], + ] satisfies Array<[string, OpenClawConfig]>)( + "rejects fallback approval buttons when %s does not grant command auth", + async (_name, cfg) => { + const handler = createInteractionHandler(account, runtime, undefined, { + getActiveCfg: () => cfg, + }); + + handler(makeApprovalEvent()); + + await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled()); + + expect(acknowledgeInteractionMock).toHaveBeenCalledWith( + { appId: "app", clientSecret: "secret" }, + "interaction-1", + 0, + { content: "You are not authorized to approve this request." }, + ); + expect(resolveApprovalMock).not.toHaveBeenCalled(); + }, + ); + + it("rejects fallback approval buttons without a trusted actor id", async () => { + const handler = createInteractionHandler(account, runtime, undefined, { + getActiveCfg: () => makeCommandAuthorizedFallbackCfg(), + }); + + handler(makeApprovalEvent({ group_member_openid: undefined, user_openid: undefined })); + + await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled()); + + expect(acknowledgeInteractionMock).toHaveBeenCalledWith( + { appId: "app", clientSecret: "secret" }, + "interaction-1", + 0, + { content: "You are not authorized to approve this request." }, + ); + expect(resolveApprovalMock).not.toHaveBeenCalled(); + }); + it("rejects approval button clicks when active config cannot be loaded", async () => { const handler = createInteractionHandler(account, runtime, undefined, { getActiveCfg: () => { diff --git a/extensions/qqbot/src/engine/gateway/interaction-handler.ts b/extensions/qqbot/src/engine/gateway/interaction-handler.ts index 0cd3255e0ef2..af63d00cd37c 100644 --- a/extensions/qqbot/src/engine/gateway/interaction-handler.ts +++ b/extensions/qqbot/src/engine/gateway/interaction-handler.ts @@ -11,12 +11,17 @@ * branches fall through to a bare ACK (backward-compatible). */ +import { isImplicitSameChatApprovalAuthorization } from "openclaw/plugin-sdk/approval-auth-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime"; import { authorizeQQBotApprovalAction } from "../../exec-approvals.js"; import { resolveQQBotEffectivePolicies } from "../access/resolve-policy.js"; import { getPlatformAdapter } from "../adapter/index.js"; import { parseApprovalButtonData } from "../approval/index.js"; +import { + resolveQQBotCommandsAllowFrom, + resolveSlashCommandAuth, +} from "../commands/slash-command-auth.js"; import { getPluginVersion, getFrameworkVersion } from "../commands/slash-commands-impl.js"; import { resolveGroupConfig, resolveMentionPatterns } from "../config/group.js"; import { resolveAccountBase } from "../config/resolve.js"; @@ -26,6 +31,17 @@ import type { InteractionEvent, QQBotAccountConfigView } from "../types.js"; import { InteractionType } from "./constants.js"; import type { GatewayAccount, GatewayPluginRuntime, EngineLogger } from "./types.js"; +type QQBotCommandAuthorizationResolver = (params: { + cfg: OpenClawConfig; + accountId: string; + isGroup: boolean; + senderId: string; + conversationId: string; + allowFrom?: Array; + groupAllowFrom?: Array; + commandsAllowFrom?: Array; +}) => boolean | Promise; + // ============ claw_cfg snapshot ============ /** @@ -154,7 +170,10 @@ export function createInteractionHandler( account: GatewayAccount, runtime: GatewayPluginRuntime, log?: EngineLogger, - options?: { getActiveCfg?: () => OpenClawConfig }, + options?: { + getActiveCfg?: () => OpenClawConfig; + resolveCommandAuthorized?: QQBotCommandAuthorizationResolver; + }, ): (event: InteractionEvent) => void { return (event) => { const creds = accountToCreds(account); @@ -187,12 +206,13 @@ export function createInteractionHandler( } void handleApprovalButtonInteraction({ - accountId: account.accountId, + account, creds, event, getActiveCfg: options?.getActiveCfg ?? runtime.config?.current, log, parsed, + resolveCommandAuthorized: options?.resolveCommandAuthorized, }); }; } @@ -200,12 +220,13 @@ export function createInteractionHandler( // ============ Helpers ============ async function handleApprovalButtonInteraction(params: { - accountId: string; + account: GatewayAccount; creds: { appId: string; clientSecret: string }; event: InteractionEvent; getActiveCfg?: () => OpenClawConfig | Record; log?: EngineLogger; parsed: { approvalId: string; decision: "allow-once" | "allow-always" | "deny" }; + resolveCommandAuthorized?: QQBotCommandAuthorizationResolver; }): Promise { if (!params.getActiveCfg) { await acknowledgeApprovalInteraction(params.creds, params.event, params.log, { @@ -230,11 +251,12 @@ async function handleApprovalButtonInteraction(params: { return; } - const authorization = authorizeApprovalButtonActor({ + const authorization = await authorizeApprovalButtonActor({ cfg, - accountId: params.accountId, + account: params.account, event: params.event, approvalKind: resolveApprovalKind(params.parsed.approvalId), + resolveCommandAuthorized: params.resolveCommandAuthorized, }); if (!authorization.authorized) { await acknowledgeApprovalInteraction(params.creds, params.event, params.log, { @@ -283,38 +305,102 @@ async function acknowledgeApprovalInteraction( } } -function authorizeApprovalButtonActor(params: { +async function authorizeApprovalButtonActor(params: { cfg: OpenClawConfig; - accountId: string; + account: GatewayAccount; event: InteractionEvent; approvalKind: "exec" | "plugin"; -}): { authorized: boolean; reason?: string } { + resolveCommandAuthorized?: QQBotCommandAuthorizationResolver; +}): Promise<{ authorized: boolean; reason?: string }> { const senderIds = resolveApprovalActorSenderIds(params.event); if (senderIds.length === 0) { - return authorizeQQBotApprovalAction({ + const result = authorizeQQBotApprovalAction({ cfg: params.cfg, - accountId: params.accountId, + accountId: params.account.accountId, senderId: null, approvalKind: params.approvalKind, }); + return result.authorized && isImplicitSameChatApprovalAuthorization(result) + ? { authorized: false, reason: "You are not authorized to approve this request." } + : result; } let denial: { authorized: boolean; reason?: string } | undefined; for (const senderId of senderIds) { const result = authorizeQQBotApprovalAction({ cfg: params.cfg, - accountId: params.accountId, + accountId: params.account.accountId, senderId, approvalKind: params.approvalKind, }); if (result.authorized) { - return result; + if ( + !isImplicitSameChatApprovalAuthorization(result) || + (await isImplicitApprovalButtonActorAuthorized({ + cfg: params.cfg, + account: params.account, + event: params.event, + senderId, + resolveCommandAuthorized: params.resolveCommandAuthorized, + })) + ) { + return result; + } + denial ??= { + authorized: false, + reason: "You are not authorized to approve this request.", + }; + continue; } denial ??= result; } return denial ?? { authorized: false, reason: "You are not authorized to approve this request." }; } +async function isImplicitApprovalButtonActorAuthorized(params: { + cfg: OpenClawConfig; + account: GatewayAccount; + event: InteractionEvent; + senderId: string; + resolveCommandAuthorized?: QQBotCommandAuthorizationResolver; +}): Promise { + const accountConfig = resolveApprovalButtonAccountConfig(params.cfg, params.account.accountId); + const authInput = { + cfg: params.cfg, + accountId: params.account.accountId, + senderId: params.senderId, + isGroup: Boolean(params.event.group_openid), + conversationId: params.event.group_openid ?? params.event.user_openid ?? params.senderId, + allowFrom: accountConfig.allowFrom, + groupAllowFrom: accountConfig.groupAllowFrom, + commandsAllowFrom: resolveQQBotCommandsAllowFrom(params.cfg), + }; + return params.resolveCommandAuthorized + ? await params.resolveCommandAuthorized(authInput) + : resolveSlashCommandAuth(authInput); +} + +function resolveApprovalButtonAccountConfig( + cfg: OpenClawConfig, + accountId: string, +): QQBotAccountConfigView { + const qqbot = readRecord(readRecord(cfg.channels)?.qqbot); + const accounts = readRecord(qqbot?.accounts); + if (accountId === "default") { + return { + ...qqbot, + ...readRecord(accounts?.default), + } as QQBotAccountConfigView; + } + return (readRecord(accounts?.[accountId]) ?? {}) as QQBotAccountConfigView; +} + +function readRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : undefined; +} + function resolveApprovalActorSenderIds(event: InteractionEvent): string[] { const ids = [event.group_member_openid, event.user_openid].flatMap((value) => { const normalized = typeof value === "string" ? value.trim() : ""; diff --git a/extensions/qqbot/src/exec-approvals.test.ts b/extensions/qqbot/src/exec-approvals.test.ts new file mode 100644 index 000000000000..009ebafd3975 --- /dev/null +++ b/extensions/qqbot/src/exec-approvals.test.ts @@ -0,0 +1,68 @@ +import { isImplicitSameChatApprovalAuthorization } from "openclaw/plugin-sdk/approval-auth-runtime"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerPlatformAdapter, type PlatformAdapter } from "./engine/adapter/index.js"; +import { authorizeQQBotApprovalAction } from "./exec-approvals.js"; + +describe("authorizeQQBotApprovalAction", () => { + beforeEach(() => { + registerPlatformAdapter({ + validateRemoteUrl: vi.fn(async () => undefined), + resolveSecret: vi.fn(async (value: unknown) => + typeof value === "string" ? value : undefined, + ), + downloadFile: vi.fn(async () => "/tmp/file"), + fetchMedia: vi.fn(async () => { + throw new Error("unused"); + }), + getTempDir: () => "/tmp", + hasConfiguredSecret: (value: unknown) => typeof value === "string" && value.length > 0, + normalizeSecretInputString: (value: unknown) => + typeof value === "string" ? value : undefined, + resolveSecretInputString: ({ value }: { value: unknown }) => + typeof value === "string" ? value : undefined, + } as PlatformAdapter); + }); + + it("marks unconfigured exec approval fallback authorization as implicit", () => { + const result = authorizeQQBotApprovalAction({ + cfg: { + channels: { + qqbot: { + appId: "app", + clientSecret: "secret", + }, + }, + } as OpenClawConfig, + accountId: "default", + senderId: "ATTACKER_OPENID", + approvalKind: "exec", + }); + + expect(result).toEqual({ authorized: true }); + expect(isImplicitSameChatApprovalAuthorization(result)).toBe(true); + }); + + it("keeps configured approver authorization explicit", () => { + const result = authorizeQQBotApprovalAction({ + cfg: { + channels: { + qqbot: { + appId: "app", + clientSecret: "secret", + execApprovals: { + enabled: true, + approvers: ["OWNER_OPENID"], + }, + }, + }, + } as OpenClawConfig, + accountId: "default", + senderId: "OWNER_OPENID", + approvalKind: "exec", + }); + + expect(result).toEqual({ authorized: true }); + expect(isImplicitSameChatApprovalAuthorization(result)).toBe(false); + }); +}); diff --git a/extensions/qqbot/src/exec-approvals.ts b/extensions/qqbot/src/exec-approvals.ts index 96a7f1d71ad8..d03a06b98a0a 100644 --- a/extensions/qqbot/src/exec-approvals.ts +++ b/extensions/qqbot/src/exec-approvals.ts @@ -1,4 +1,7 @@ -import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime"; +import { + markImplicitSameChatApprovalAuthorization, + resolveApprovalApprovers, +} from "openclaw/plugin-sdk/approval-auth-runtime"; import { createChannelExecApprovalProfile, isChannelExecApprovalClientEnabledFromConfig, @@ -224,7 +227,7 @@ export function authorizeQQBotApprovalAction(params: { approvalKind: "exec" | "plugin"; }): { authorized: boolean; reason?: string } { if (resolveQQBotExecApprovalConfig(params) === undefined) { - return { authorized: true }; + return markImplicitSameChatApprovalAuthorization({ authorized: true }); } const authorized = diff --git a/src/acp/control-plane/manager.test.ts b/src/acp/control-plane/manager.test.ts index f2f6e1c55c24..a8cfcada7c16 100644 --- a/src/acp/control-plane/manager.test.ts +++ b/src/acp/control-plane/manager.test.ts @@ -277,8 +277,8 @@ function extractStateUpsertPersistenceOptions(): Array<{ if (next?.state && payload.skipMaintenance && payload.takeCacheOwnership) { options.push({ state: next.state, - ...(payload.skipMaintenance ? { skipMaintenance: true } : {}), - ...(payload.takeCacheOwnership ? { takeCacheOwnership: true } : {}), + skipMaintenance: true, + takeCacheOwnership: true, }); } } diff --git a/src/plugin-sdk/approval-auth-helpers.ts b/src/plugin-sdk/approval-auth-helpers.ts index a15f8ed48c63..6d1a522d3644 100644 --- a/src/plugin-sdk/approval-auth-helpers.ts +++ b/src/plugin-sdk/approval-auth-helpers.ts @@ -10,7 +10,7 @@ const IMPLICIT_SAME_CHAT_APPROVAL_AUTHORIZATION = Symbol( "openclaw.implicitSameChatApprovalAuthorization", ); -function markImplicitSameChatApprovalAuthorization( +export function markImplicitSameChatApprovalAuthorization( result: ApprovalAuthorizationResult, ): ApprovalAuthorizationResult { // Keep this non-enumerable to avoid changing auth payload shape. diff --git a/src/plugin-sdk/approval-auth-runtime.ts b/src/plugin-sdk/approval-auth-runtime.ts index 552a38a48a38..e77951600471 100644 --- a/src/plugin-sdk/approval-auth-runtime.ts +++ b/src/plugin-sdk/approval-auth-runtime.ts @@ -1,2 +1,6 @@ export { resolveApprovalApprovers } from "./approval-approvers.js"; -export { createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers.js"; +export { + createResolvedApproverActionAuthAdapter, + isImplicitSameChatApprovalAuthorization, + markImplicitSameChatApprovalAuthorization, +} from "./approval-auth-helpers.js"; diff --git a/src/plugins/contracts/plugin-sdk-subpaths.test.ts b/src/plugins/contracts/plugin-sdk-subpaths.test.ts index a8b445d8f3c4..45864db1ac30 100644 --- a/src/plugins/contracts/plugin-sdk-subpaths.test.ts +++ b/src/plugins/contracts/plugin-sdk-subpaths.test.ts @@ -572,6 +572,8 @@ describe("plugin-sdk subpath exports", () => { ]); expectSourceMentions("approval-auth-runtime", [ "createResolvedApproverActionAuthAdapter", + "isImplicitSameChatApprovalAuthorization", + "markImplicitSameChatApprovalAuthorization", "resolveApprovalApprovers", ]); expectSourceMentions("reply-chunking", [