fix(google-meet): guard linux chrome realtime tool actions

This commit is contained in:
Peter Steinberger
2026-05-02 08:07:05 +01:00
parent 74a55d7b21
commit afd0a7b403
6 changed files with 109 additions and 2 deletions

View File

@@ -62,6 +62,7 @@ function setup(
unknown
>,
);
googleMeetPluginTesting.setPlatformForTests(() => options?.registerPlatform ?? "darwin");
return harness;
}
@@ -106,6 +107,7 @@ describe("google-meet create flow", () => {
afterEach(() => {
vi.unstubAllGlobals();
googleMeetPluginTesting.setCallGatewayFromCliForTests();
googleMeetPluginTesting.setPlatformForTests();
});
it("CLI create can configure API-created space access", async () => {

View File

@@ -86,6 +86,7 @@ function setup(
unknown
>,
);
googleMeetPluginTesting.setPlatformForTests(() => options?.registerPlatform ?? "darwin");
return harness;
}
@@ -303,6 +304,7 @@ describe("google-meet plugin", () => {
vi.unstubAllGlobals();
chromeTransportTesting.setDepsForTest(null);
googleMeetPluginTesting.setCallGatewayFromCliForTests();
googleMeetPluginTesting.setPlatformForTests();
});
it("defaults to chrome realtime with safe read-only tools", () => {
@@ -507,6 +509,42 @@ describe("google-meet plugin", () => {
);
});
it("keeps the agent tool visible on non-macOS hosts but blocks local Chrome realtime joins", async () => {
const { cliRegistrations, methods, tools } = setup(undefined, { registerPlatform: "linux" });
const tool = tools[0] as {
execute: (id: string, params: unknown) => Promise<{ isError?: boolean; content: unknown }>;
};
expect(tools).toHaveLength(1);
expect(cliRegistrations).toHaveLength(1);
expect(methods.has("googlemeet.setup")).toBe(true);
expect(
googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({
config: resolveGoogleMeetConfig({}),
raw: { action: "join" },
platform: "linux",
}),
).toBe(true);
const blocked = await tool.execute("id", { action: "join" });
expect(JSON.stringify(blocked)).toContain("local Chrome realtime audio is macOS-only");
expect(
googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({
config: resolveGoogleMeetConfig({}),
raw: { action: "join", mode: "transcribe" },
platform: "linux",
}),
).toBe(false);
expect(
googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({
config: resolveGoogleMeetConfig({}),
raw: { action: "join", transport: "chrome-node" },
platform: "linux",
}),
).toBe(false);
});
it("returns structured gateway errors for missing session ids", async () => {
const { methods } = setup();
for (const method of ["googlemeet.leave", "googlemeet.speak"]) {

View File

@@ -345,12 +345,17 @@ function shouldJoinCreatedMeet(raw: Record<string, unknown>): boolean {
const googleMeetToolDeps = {
callGatewayFromCli,
platform: () => process.platform,
};
export const __testing = {
setCallGatewayFromCliForTests(next?: typeof callGatewayFromCli): void {
googleMeetToolDeps.callGatewayFromCli = next ?? callGatewayFromCli;
},
setPlatformForTests(next?: () => NodeJS.Platform): void {
googleMeetToolDeps.platform = next ?? (() => process.platform);
},
isGoogleMeetAgentToolActionUnsupportedOnHost,
};
type GoogleMeetGatewayToolAction =
@@ -382,6 +387,43 @@ function googleMeetGatewayMethodForToolAction(action: GoogleMeetGatewayToolActio
}
}
function isGoogleMeetAgentToolActionUnsupportedOnHost(params: {
config: GoogleMeetConfig;
raw: Record<string, unknown>;
platform?: NodeJS.Platform;
}): boolean {
const platform = params.platform ?? googleMeetToolDeps.platform();
if (platform === "darwin") {
return false;
}
const action = params.raw.action;
if (
action !== "join" &&
action !== "test_speech" &&
!(action === "create" && shouldJoinCreatedMeet(params.raw))
) {
return false;
}
const transport = normalizeTransport(params.raw.transport) ?? params.config.defaultTransport;
const mode =
action === "test_speech"
? "realtime"
: (normalizeMode(params.raw.mode) ?? params.config.defaultMode);
return transport === "chrome" && mode === "realtime";
}
function assertGoogleMeetAgentToolActionSupported(params: {
config: GoogleMeetConfig;
raw: Record<string, unknown>;
}): void {
if (!isGoogleMeetAgentToolActionUnsupportedOnHost(params)) {
return;
}
throw new Error(
"Google Meet local Chrome realtime audio is macOS-only. On this host, use mode: transcribe, transport: twilio, or transport: chrome-node backed by a macOS node.",
);
}
function resolveGoogleMeetToolGatewayTimeoutMs(config: GoogleMeetConfig): number {
return Math.max(
60_000,
@@ -944,11 +986,12 @@ export default definePluginEntry({
name: "google_meet",
label: "Google Meet",
description:
"Join and track Google Meet sessions through Chrome or Twilio. Call setup_status before join/create/test_listen/test_speech; if it reports a Chrome node offline or local audio missing, surface that blocker instead of retrying or switching transports. Offline nodes are diagnostics only, not usable candidates. If a Meet tab is already open after a timeout, call recover_current_tab before retrying join to report login, permission, or admission blockers without opening another tab.",
"Join and track Google Meet sessions through Chrome or Twilio. Call setup_status before join/create/test_listen/test_speech; if it reports a Chrome node offline or local audio missing, surface that blocker instead of retrying or switching transports. Offline nodes are diagnostics only, not usable candidates. If local Chrome realtime audio is unsupported on this OS, use mode=transcribe, transport=twilio, or a macOS chrome-node for realtime Chrome. If a Meet tab is already open after a timeout, call recover_current_tab before retrying join to report login, permission, or admission blockers without opening another tab.",
parameters: GoogleMeetToolSchema,
async execute(_toolCallId, params) {
const raw = asParamRecord(params);
try {
assertGoogleMeetAgentToolActionSupported({ config, raw });
switch (raw.action) {
case "join": {
return json(await callGoogleMeetGatewayFromTool({ config, action: "join", raw }));

View File

@@ -60,6 +60,7 @@ export function setupGoogleMeetPlugin(
argv: string[],
options?: { timeoutMs?: number },
) => Promise<CommandResult>;
registerPlatform?: NodeJS.Platform;
} = {},
) {
const methods = new Map<string, unknown>();
@@ -157,7 +158,16 @@ export function setupGoogleMeetPlugin(
registerCli: (_registrar: unknown, opts: unknown) => cliRegistrations.push(opts),
registerNodeHostCommand: (command: unknown) => nodeHostCommands.push(command),
});
plugin.register(api);
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
configurable: true,
value: options.registerPlatform ?? "darwin",
});
try {
plugin.register(api);
} finally {
Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
}
return {
cliRegistrations,
methods,