diff --git a/extensions/voice-call/index.test.ts b/extensions/voice-call/index.test.ts index c6086c077f0..c9a717cebaa 100644 --- a/extensions/voice-call/index.test.ts +++ b/extensions/voice-call/index.test.ts @@ -29,6 +29,7 @@ const callGatewayFromCliMock = vi.fn(); type Registered = { methods: Map; + methodScopes: Map; tools: unknown[]; service?: Parameters[0]; }; @@ -108,6 +109,7 @@ function createServiceContext(): Parameters[" function setup(config: Record): Registered { const methods = new Map(); + const methodScopes = new Map(); const tools: unknown[] = []; let service: Registered["service"]; const api = createTestPluginApi({ @@ -120,7 +122,10 @@ function setup(config: Record): Registered { pluginConfig: config, runtime: { tts: { textToSpeechTelephony: vi.fn() } } as unknown as OpenClawPluginApi["runtime"], logger: noopLogger, - registerGatewayMethod: (method: string, handler: unknown) => methods.set(method, handler), + registerGatewayMethod: (method: string, handler: unknown, opts?: { scope?: string }) => { + methods.set(method, handler); + methodScopes.set(method, opts?.scope); + }, registerTool: (tool: unknown) => tools.push(tool), registerCli: () => {}, registerService: (registeredService) => { @@ -129,7 +134,7 @@ function setup(config: Record): Registered { resolvePath: (p: string) => p, }); plugin.register(api); - return { methods, tools, service }; + return { methods, methodScopes, tools, service }; } function envRef(id: string) { @@ -363,6 +368,24 @@ describe("voice-call plugin", () => { expect(payload.callId).toBe("call-1"); }); + it("registers voice call gateway methods with least-privilege scopes", () => { + const { methodScopes } = setup({ provider: "mock" }); + + for (const method of [ + "voicecall.initiate", + "voicecall.start", + "voicecall.continue", + "voicecall.continue.start", + "voicecall.speak", + "voicecall.dtmf", + "voicecall.end", + ]) { + expect(methodScopes.get(method)).toBe("operator.write"); + } + expect(methodScopes.get("voicecall.continue.result")).toBe("operator.read"); + expect(methodScopes.get("voicecall.status")).toBe("operator.read"); + }); + it("preserves mode on legacy voicecall.start", async () => { const { methods } = setup({ provider: "mock" }); const handler = methods.get("voicecall.start") as diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index b82e500f953..abf14ce541c 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -22,6 +22,9 @@ import { import type { CoreConfig } from "./src/core-bridge.js"; import { createVoiceCallContinueOperationStore } from "./src/gateway-continue-operation.js"; +const VOICE_CALL_WRITE_METHOD_SCOPE = { scope: "operator.write" as const }; +const VOICE_CALL_READ_METHOD_SCOPE = { scope: "operator.read" as const }; + const voiceCallConfigSchema = { parse(value: unknown): VoiceCallConfig { const normalized = normalizeVoiceCallLegacyConfigInput(value); @@ -415,6 +418,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_WRITE_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -432,6 +436,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_WRITE_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -452,6 +457,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_WRITE_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -473,6 +479,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_READ_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -508,6 +515,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_WRITE_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -531,6 +539,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_WRITE_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -553,6 +562,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_WRITE_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -576,6 +586,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_READ_METHOD_SCOPE, ); api.registerGatewayMethod( @@ -604,6 +615,7 @@ export default definePluginEntry({ sendError(respond, err); } }, + VOICE_CALL_WRITE_METHOD_SCOPE, ); api.registerTool({