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

@@ -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" ||