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

@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
- Infer/media: report missing image-understanding and audio-transcription provider configuration for `image describe`, `image describe-many`, and `audio transcribe` instead of blaming the input path when no provider is available. Fixes #73569 and supersedes #73593, #74288, and #74495. Thanks @bittoby, @tmimmanuel, @Linux2010, and @vyctorbrzezowski.
- Docs/health: clarify that session listing surfaces stored conversation rows rather than Discord/channel socket liveness, and point connectivity checks at channel status and health probes. Fixes #70420. Thanks @ashersoutherncities-art and @martingarramon.
- WhatsApp/Cron: keep DM pairing-store approvals out of implicit cron and heartbeat recipient fallback, so scheduled automation only uses explicit targets, active configured recipients, or configured `allowFrom` entries. Fixes #62339. Thanks @kelvinisly-collab.
- Google Meet: keep the agent-facing `google_meet` tool visible on non-macOS hosts but block local Chrome realtime actions with guidance, so Linux agents can still use transcribe, Twilio, chrome-node, and artifact flows without choosing the macOS-only BlackHole path. Refs #75950. Thanks @actual-software-inc.
- Active Memory: use the configured recall timeout as the blocking prompt-build hook budget by default and move cold-start setup grace behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently extends 15000 ms configs to 45000 ms on the main lane. Fixes #75843. Thanks @vishutdhar.
- Plugins/web-provider: reuse the active gateway plugin registry for runtime web provider resolution after deriving the same candidate plugin ids as the loader path, avoiding a redundant `loadOpenClawPlugins` call on every request while preserving origin and scope filters. Fixes #75513. Thanks @jochen.
- Crestodian/CLI: exit non-zero when interactive Crestodian is invoked without a TTY, so scripts and CI no longer treat the setup error as success. Fixes #73646 and supersedes #73928 and #74059. Thanks @bittoby, @luyao618, and @Linux2010.

View File

@@ -119,6 +119,13 @@ Or let an agent join through the `google_meet` tool:
}
```
The agent-facing `google_meet` tool stays available on non-macOS hosts for
artifact, calendar, setup, transcribe, Twilio, and `chrome-node` flows. Local
Chrome realtime actions are blocked there because the bundled realtime Chrome
audio path currently depends on macOS `BlackHole 2ch`. On Linux, use
`mode: "transcribe"`, Twilio dial-in, or a macOS `chrome-node` host for realtime
Chrome participation.
Create a new meeting and join it:
```bash
@@ -1267,6 +1274,12 @@ If you just edited `plugins.entries.google-meet`, restart or reload the Gateway.
The running agent only sees plugin tools registered by the current Gateway
process.
On non-macOS Gateway hosts, the agent-facing `google_meet` tool stays visible,
but local Chrome realtime actions are blocked before they hit the audio bridge.
Local Chrome realtime audio currently depends on macOS `BlackHole 2ch`, so
Linux agents should use `mode: "transcribe"`, Twilio dial-in, or a macOS
`chrome-node` host instead of the default local Chrome realtime path.
### No connected Google Meet-capable node
On the node host, run:

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,