fix(qqbot): gate fallback approval buttons (#87154)

QQBot fallback approval buttons now reuse the same slash-command authorization path as real commands, including access groups and default-account config merging.

Verification:
- node scripts/test-extension.mjs qqbot
- node --max-old-space-size=8192 --import tsx scripts/generate-plugin-sdk-api-baseline.ts --check && git diff --check
- pnpm lint --threads=8
- node scripts/run-vitest.mjs src/agents/agent-command.live-model-switch.test.ts
- GitHub PR checks for 7cc0f15031: passed

Thanks @eleqtrizit.

Co-authored-by: Agustin Rivera <agustin@rivera-web.com>
This commit is contained in:
Agustin Rivera
2026-05-27 00:44:55 -07:00
committed by GitHub
parent 7615c3137d
commit 08a73dbe4b
14 changed files with 406 additions and 31 deletions

View File

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

View File

@@ -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:

View File

@@ -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.<channel>.accounts.<id>.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.

View File

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

View File

@@ -46,6 +46,21 @@ describe("qqbot: prefix normalization for inbound commandAuthorized", () => {
return result.commandAccess.authorized;
}
async function resolveSlashCommandAuthorized(
rawAllowFrom: string[],
senderId: string,
cfg: Record<string, unknown> = {},
): Promise<boolean> {
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,

View File

@@ -183,6 +183,7 @@ export async function startGateway(ctx: CoreGatewayContext): Promise<void> {
const handleInteraction = createInteractionHandler(account, ctx.runtime, log, {
getActiveCfg: () => activeCfgProvider.getActiveCfg(),
resolveCommandAuthorized: (params) => adapters.access.resolveSlashCommandAuthorization(params),
});
const connection = new GatewayConnection({

View File

@@ -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: () => {

View File

@@ -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<string | number>;
groupAllowFrom?: Array<string | number>;
commandsAllowFrom?: Array<string | number>;
}) => boolean | Promise<boolean>;
// ============ 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<string, unknown>;
log?: EngineLogger;
parsed: { approvalId: string; decision: "allow-once" | "allow-always" | "deny" };
resolveCommandAuthorized?: QQBotCommandAuthorizationResolver;
}): Promise<void> {
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<boolean> {
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<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function resolveApprovalActorSenderIds(event: InteractionEvent): string[] {
const ids = [event.group_member_openid, event.user_openid].flatMap((value) => {
const normalized = typeof value === "string" ? value.trim() : "";

View File

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

View File

@@ -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 =

View File

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

View File

@@ -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.

View File

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

View File

@@ -572,6 +572,8 @@ describe("plugin-sdk subpath exports", () => {
]);
expectSourceMentions("approval-auth-runtime", [
"createResolvedApproverActionAuthAdapter",
"isImplicitSameChatApprovalAuthorization",
"markImplicitSameChatApprovalAuthorization",
"resolveApprovalApprovers",
]);
expectSourceMentions("reply-chunking", [