diff --git a/extensions/talk-voice/index.test.ts b/extensions/talk-voice/index.test.ts index 630d420e88d..934b922ab58 100644 --- a/extensions/talk-voice/index.test.ts +++ b/extensions/talk-voice/index.test.ts @@ -233,6 +233,14 @@ describe("talk-voice plugin", () => { expect(runtime.config.writeConfigFile).not.toHaveBeenCalled(); }); + it("rejects /voice set from non-webchat gateway callers missing operator.admin", async () => { + const { runtime, run } = createElevenlabsVoiceSetHarness("telegram", ["operator.write"]); + const result = await run(); + + expect(result.text).toContain("requires operator.admin"); + expect(runtime.config.writeConfigFile).not.toHaveBeenCalled(); + }); + it("allows /voice set from gateway client with operator.admin scope", async () => { const { runtime, run } = createElevenlabsVoiceSetHarness("webchat", ["operator.admin"]); const result = await run(); diff --git a/extensions/talk-voice/index.ts b/extensions/talk-voice/index.ts index fcc9e8cd04b..c7666dbbc8c 100644 --- a/extensions/talk-voice/index.ts +++ b/extensions/talk-voice/index.ts @@ -99,6 +99,15 @@ function asProviderBaseUrl(value: unknown): string | undefined { return trimmed || undefined; } +const TALK_ADMIN_SCOPE = "operator.admin"; + +function requiresAdminToSetVoice(channel: string, gatewayClientScopes?: readonly string[]): boolean { + if (Array.isArray(gatewayClientScopes)) { + return !gatewayClientScopes.includes(TALK_ADMIN_SCOPE); + } + return channel === "webchat"; +} + export default definePluginEntry({ id: "talk-voice", name: "Talk Voice", @@ -164,10 +173,9 @@ export default definePluginEntry({ } if (action === "set") { - // Internal gateway callers already expose operator scopes and should - // match the admin-only config.patch RPC. External channels rely on - // the plugin command's authorized-sender gate instead. - if (ctx.channel === "webchat" && !ctx.gatewayClientScopes?.includes("operator.admin")) { + // Gateway callers can override messageChannel, so scope presence is + // the reliable signal for internal admin-only mutations. + if (requiresAdminToSetVoice(ctx.channel, ctx.gatewayClientScopes)) { return { text: `⚠️ ${commandLabel} set requires operator.admin.` }; }