fix: clarify google meet twilio dial plan

This commit is contained in:
Peter Steinberger
2026-05-02 10:21:02 +01:00
parent 800a33bbfe
commit 4f6a4317de
6 changed files with 151 additions and 6 deletions

View File

@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
- Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma.
- Gateway/pricing: abort in-flight model pricing catalog fetches when Gateway shutdown stops the refresh loop, and avoid post-stop cache writes or refresh timers. Fixes #72208. Thanks @rzcq.
- Codex/app-server: make startup retry cleanup ownership-aware so concurrent Codex lanes cannot close another lane's freshly restarted shared app-server client. Thanks @vincentkoc.
- Google Meet/Twilio: report missing dial-in details during setup and explain that Twilio cannot join Meet URLs without a phone dial plan. Thanks @vincentkoc.
- Control UI/Talk: allow the OpenAI Realtime WebRTC offer endpoint through the Control UI CSP, configure browser sessions with explicit VAD/transcription input settings, and surface OpenAI realtime error/lifecycle events instead of leaving Talk stuck as live with no diagnostic. Fixes #73427.
- Plugins: clarify config-selected duplicate plugin override diagnostics and document manifest schema updates for bundled-plugin forks. Fixes #8582. Thanks @sachah.
- CLI backends/Claude: make live-session JSONL turn caps bounded and configurable via `reliability.outputLimits`, raising the default guard for tool-heavy Claude CLI turns while preserving memory limits. Fixes #75838. Thanks @hcordoba840.

View File

@@ -22,7 +22,8 @@ Google Meet participant support for OpenClaw — the plugin is explicit by desig
- There is no automatic consent announcement.
- The default Chrome audio backend is `BlackHole 2ch`.
- Chrome can run locally or on a paired node host.
- Twilio accepts a dial-in number plus optional PIN or DTMF sequence.
- Twilio accepts a dial-in number plus optional PIN or DTMF sequence; it
cannot dial a Meet URL directly.
- The CLI command is `googlemeet`; `meet` is reserved for broader agent
teleconference workflows.

View File

@@ -1068,6 +1068,21 @@ describe("google-meet plugin", () => {
});
});
it("explains that Twilio joins need dial-in details", async () => {
const { tools } = setup({ defaultTransport: "twilio" });
const tool = tools[0] as {
execute: (id: string, params: unknown) => Promise<{ details: { error?: string } }>;
};
const result = await tool.execute("id", {
action: "join",
url: "https://meet.google.com/abc-defg-hij",
});
expect(result.details.error).toContain("Twilio transport requires a Meet dial-in phone number");
expect(result.details.error).toContain("Google Meet URLs do not include dial-in details");
});
it("hangs up delegated Twilio calls on leave", async () => {
const { tools } = setup({ defaultTransport: "twilio" });
const tool = tools[0] as {
@@ -1619,6 +1634,98 @@ describe("google-meet plugin", () => {
);
});
it("reports missing Twilio dial plan for explicit Twilio setup", async () => {
vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123");
vi.stubEnv("TWILIO_AUTH_TOKEN", "secret");
vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234");
const { tools } = setup(
{ defaultTransport: "chrome" },
{
fullConfig: {
plugins: {
allow: ["google-meet", "voice-call"],
entries: {
"voice-call": {
enabled: true,
config: {
provider: "twilio",
publicUrl: "https://voice.example.com/voice/webhook",
},
},
},
},
},
},
);
const tool = tools[0] as {
execute: (
id: string,
params: unknown,
) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>;
};
const result = await tool.execute("id", { action: "setup_status", transport: "twilio" });
expect(result.details.ok).toBe(false);
expect(result.details.checks).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "twilio-dial-plan",
ok: false,
message: expect.stringContaining("dial-in phone number"),
}),
]),
);
});
it("accepts request-provided Twilio dial-in details during setup", async () => {
vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123");
vi.stubEnv("TWILIO_AUTH_TOKEN", "secret");
vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234");
const { tools } = setup(
{ defaultTransport: "chrome" },
{
fullConfig: {
plugins: {
allow: ["google-meet", "voice-call"],
entries: {
"voice-call": {
enabled: true,
config: {
provider: "twilio",
publicUrl: "https://voice.example.com/voice/webhook",
},
},
},
},
},
},
);
const tool = tools[0] as {
execute: (
id: string,
params: unknown,
) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>;
};
const result = await tool.execute("id", {
action: "setup_status",
transport: "twilio",
dialInNumber: "+15551234567",
});
expect(result.details.ok).toBe(true);
expect(result.details.checks).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "twilio-dial-plan",
ok: true,
message: expect.stringContaining("request includes"),
}),
]),
);
});
it.each([
"http://127.0.0.1:3334/voice/webhook",
"http://[::1]:3334/voice/webhook",

View File

@@ -239,8 +239,15 @@ const GoogleMeetToolSchema = Type.Object({
"Join mode. realtime starts live listen/talk-back through the realtime voice model; transcribe joins without the realtime talk-back bridge.",
}),
),
dialInNumber: Type.Optional(Type.String({ description: "Meet dial-in number for Twilio" })),
pin: Type.Optional(Type.String({ description: "Meet phone PIN for Twilio" })),
dialInNumber: Type.Optional(
Type.String({
description:
"Meet dial-in phone number for Twilio. Required for Twilio unless twilio.defaultDialInNumber is configured; Meet URLs cannot be dialed directly.",
}),
),
pin: Type.Optional(
Type.String({ description: "Meet phone PIN for Twilio; # is appended if omitted" }),
),
dtmfSequence: Type.Optional(Type.String({ description: "Explicit DTMF sequence for Twilio" })),
sessionId: Type.Optional(Type.String({ description: "Meet session ID" })),
message: Type.Optional(Type.String({ description: "Realtime instructions to speak now" })),
@@ -776,6 +783,7 @@ export default definePluginEntry({
await rt.setupStatus({
transport: normalizeTransport(params?.transport),
mode: normalizeMode(params?.mode),
dialInNumber: normalizeOptionalString(params?.dialInNumber),
}),
);
} catch (err) {
@@ -986,7 +994,7 @@ 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 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.",
"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, local audio missing, or missing Twilio dial plan, surface that blocker instead of retrying or switching transports. Twilio cannot dial a Meet URL directly: provide dialInNumber plus optional pin/dtmfSequence, or configure twilio.defaultDialInNumber. 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);

View File

@@ -226,9 +226,17 @@ export class GoogleMeetRuntime {
return session ? { found: true, session } : { found: false };
}
async setupStatus(options: { transport?: GoogleMeetTransport; mode?: GoogleMeetMode } = {}) {
async setupStatus(
options: {
transport?: GoogleMeetTransport;
mode?: GoogleMeetMode;
dialInNumber?: string;
} = {},
) {
const transport = resolveTransport(options.transport, this.params.config);
const mode = resolveMode(options.mode, this.params.config);
const twilioDialInNumber =
transport === "twilio" ? normalizeDialInNumber(options.dialInNumber) : undefined;
const shouldCheckChromeNode =
transport === "chrome-node" ||
(!options.transport && Boolean(this.params.config.chromeNode.node));
@@ -236,6 +244,7 @@ export class GoogleMeetRuntime {
fullConfig: this.params.fullConfig,
mode,
transport,
twilioDialInNumber,
});
if (shouldCheckChromeNode) {
try {
@@ -440,7 +449,9 @@ export class GoogleMeetRuntime {
request.dialInNumber ?? this.params.config.twilio.defaultDialInNumber,
);
if (!dialInNumber) {
throw new Error("dialInNumber required for twilio transport");
throw new Error(
"Twilio transport requires a Meet dial-in phone number. Google Meet URLs do not include dial-in details; pass dialInNumber with optional pin/dtmfSequence, configure twilio.defaultDialInNumber, or use chrome/chrome-node transport.",
);
}
const dtmfSequence = buildMeetDtmfSequence({
pin: request.pin ?? this.params.config.twilio.defaultPin,

View File

@@ -87,6 +87,7 @@ export function getGoogleMeetSetupStatus(
fullConfig?: unknown;
mode?: GoogleMeetMode;
transport?: GoogleMeetTransport;
twilioDialInNumber?: string;
},
): {
ok: boolean;
@@ -99,6 +100,7 @@ export function getGoogleMeetSetupStatus(
fullConfig?: unknown;
mode?: GoogleMeetMode;
transport?: GoogleMeetTransport;
twilioDialInNumber?: string;
},
) {
const checks: SetupCheck[] = [];
@@ -193,6 +195,21 @@ export function getGoogleMeetSetupStatus(
});
}
if (transport === "twilio") {
const hasRequestDialPlan = Boolean(options?.twilioDialInNumber);
const hasDefaultDialPlan = Boolean(config.twilio.defaultDialInNumber);
const hasDialPlan = hasRequestDialPlan || hasDefaultDialPlan;
checks.push({
id: "twilio-dial-plan",
ok: hasDialPlan,
message: hasRequestDialPlan
? "Twilio request includes a Meet dial-in number"
: hasDefaultDialPlan
? "Twilio default Meet dial-in number is configured"
: "Twilio joins require a Meet dial-in phone number; pass dialInNumber with optional pin/dtmfSequence or configure twilio.defaultDialInNumber",
});
}
const shouldCheckTwilioDelegation =
config.voiceCall.enabled &&
(transport === "twilio" ||