refactor(meetings): share plugin entry registration (#113007)

This commit is contained in:
Peter Steinberger
2026-07-23 07:18:38 -04:00
committed by GitHub
parent 852548ce05
commit d124fb235f
4 changed files with 542 additions and 784 deletions

View File

@@ -1,32 +1,19 @@
import {
readNonNegativeIntegerParam,
readPositiveIntegerParam,
} from "openclaw/plugin-sdk/channel-actions";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
callGatewayFromCli,
ErrorCodes,
errorShape,
type GatewayRequestHandlerOptions,
} from "openclaw/plugin-sdk/gateway-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { jsonResult as json } from "openclaw/plugin-sdk/tool-results";
import { MeetingPlatformAdapter } from "openclaw/plugin-sdk/meeting-runtime";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { Type } from "typebox";
import {
resolveTeamsMeetingsConfig,
resolveTeamsMeetingsGatewayOperationTimeoutMs,
type TeamsMeetingsConfig,
type TeamsMeetingsMode,
type TeamsMeetingsTransport,
} from "./src/config.js";
import { handleTeamsMeetingsNodeHostCommand } from "./src/node-host.js";
import { createTeamsMeetingsNodeInvokePolicy } from "./src/node-invoke-policy.js";
import { TeamsMeetingsRuntime } from "./src/runtime.js";
import { TEAMS_MEETINGS_NODE_COMMAND } from "./src/transports/teams-meetings-platform-constants.js";
import { normalizeTeamsMeetingUrl } from "./src/transports/teams-meetings-urls.js";
import type { TeamsMeetingsJoinRequest } from "./src/transports/types.js";
const loadTeamsMeetingsCli = createLazyRuntimeModule(() => import("./src/cli.js"));
@@ -71,384 +58,72 @@ const TeamsMeetingsToolSchema = Type.Object({
message: Type.Optional(Type.String({ description: "Instructions to speak" })),
});
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
class TeamsMeetingsInvalidRequestError extends Error {}
function invalidRequest(message: string): TeamsMeetingsInvalidRequestError {
return new TeamsMeetingsInvalidRequestError(message);
}
function normalizeTransport(value: unknown): TeamsMeetingsTransport | undefined {
if (value === undefined) {
return undefined;
}
if (value === "chrome" || value === "chrome-node") {
return value;
}
throw invalidRequest("transport must be chrome or chrome-node");
}
function normalizeMode(value: unknown): TeamsMeetingsMode | undefined {
if (value === undefined) {
return undefined;
}
if (value === "agent" || value === "bidi" || value === "transcribe") {
return value;
}
throw invalidRequest("mode must be agent, bidi, or transcribe");
}
function requireString(value: unknown, name: string): string {
const normalized = normalizeOptionalString(value);
if (!normalized) {
throw invalidRequest(`${name} required`);
}
return normalized;
}
function readSinceIndex(raw: Record<string, unknown>): number | undefined {
try {
return readNonNegativeIntegerParam(raw, "sinceIndex");
} catch (error) {
throw invalidRequest(formatErrorMessage(error));
}
}
function keepTrustedToolAgentId(
raw: Record<string, unknown>,
client: GatewayRequestHandlerOptions["client"],
): Record<string, unknown> {
const { agentId: rawAgentId, ...rest } = raw;
if (client?.internal?.pluginRuntimeOwnerId !== "teams-meetings") {
return rest;
}
const agentId = normalizeOptionalString(rawAgentId);
return agentId ? { ...rest, agentId } : rest;
}
function trustedToolAgentId(
raw: Record<string, unknown>,
client: GatewayRequestHandlerOptions["client"],
): string | undefined {
return normalizeOptionalString(keepTrustedToolAgentId(raw, client).agentId);
}
function joinRequest(raw: Record<string, unknown>, options?: { allowTimeout?: boolean }) {
if (!options?.allowTimeout && raw.timeoutMs !== undefined) {
throw invalidRequest("timeoutMs is supported only by testSpeech or testListen");
}
let url: string;
let timeoutMs: number | undefined;
try {
url = normalizeTeamsMeetingUrl(requireString(raw.url, "url"));
timeoutMs = readPositiveIntegerParam(raw, "timeoutMs");
} catch (error) {
if (error instanceof TeamsMeetingsInvalidRequestError) {
throw error;
}
throw invalidRequest(formatErrorMessage(error));
}
return {
url,
transport: normalizeTransport(raw.transport),
mode: normalizeMode(raw.mode),
message: normalizeOptionalString(raw.message),
requesterSessionKey: normalizeOptionalString(raw.requesterSessionKey),
agentId: normalizeOptionalString(raw.agentId),
timeoutMs,
};
}
type ToolAction = "join" | "leave" | "status" | "transcript" | "speak";
function gatewayMethod(action: ToolAction): string {
return `teamsmeetings.${action}`;
}
function readErrorDetails(error: unknown): unknown {
return error && typeof error === "object" && "details" in error
? (error as { details?: unknown }).details
: undefined;
}
async function callGatewayFromTool(params: {
action: ToolAction;
config: TeamsMeetingsConfig;
raw: Record<string, unknown>;
runtime?: OpenClawPluginApi["runtime"];
}) {
try {
if (params.runtime) {
return await params.runtime.gateway.request(gatewayMethod(params.action), params.raw, {
timeoutMs: resolveTeamsMeetingsGatewayOperationTimeoutMs(params.config),
scopes: ["operator.admin"],
});
}
return await callGatewayFromCli(
gatewayMethod(params.action),
{
json: true,
timeout: String(resolveTeamsMeetingsGatewayOperationTimeoutMs(params.config)),
},
params.raw,
{ progress: false, scopes: ["operator.admin"] },
);
} catch (error) {
const details = readErrorDetails(error);
if (details && typeof details === "object") {
return details;
}
throw error;
}
}
export default definePluginEntry({
id: "teams-meetings",
name: "Microsoft Teams meetings",
description: "Join Microsoft Teams meetings as a Chrome browser guest",
configSchema: teamsMeetingsConfigSchema,
register(api: OpenClawPluginApi) {
const config = teamsMeetingsConfigSchema.parse(api.pluginConfig);
let runtime: TeamsMeetingsRuntime | undefined;
const ensureRuntime = async () => {
if (!config.enabled) {
throw new Error("Microsoft Teams meetings plugin disabled in plugin config");
export default definePluginEntry(
MeetingPlatformAdapter.createPluginEntry<
TeamsMeetingsConfig,
TeamsMeetingsJoinRequest,
TeamsMeetingsRuntime
>({
id: "teams-meetings",
name: "Microsoft Teams meetings",
description: "Join Microsoft Teams meetings as a Chrome browser guest",
configSchema: teamsMeetingsConfigSchema,
disabledMessage: "Microsoft Teams meetings plugin disabled in plugin config",
gatewayMethodPrefix: "teamsmeetings",
invalidRequest: (message) => new TeamsMeetingsInvalidRequestError(message),
isInvalidRequest: (error) => error instanceof TeamsMeetingsInvalidRequestError,
normalizeUrl: normalizeTeamsMeetingUrl,
resolveGatewayTimeoutMs: resolveTeamsMeetingsGatewayOperationTimeoutMs,
normalizeRequesterSessionKey: (value) =>
typeof value === "string" && value.trim() ? value.trim() : undefined,
normalizeToolAgentId: (agentId) => (agentId ? normalizeAgentId(agentId) : undefined),
resolveToolRuntime: async (api, agentId) => {
const trustedRouting = Boolean(agentId && agentId !== "main");
const useRuntime = trustedRouting ? await api.runtime.gateway.isAvailable() : false;
if (trustedRouting && !useRuntime) {
throw new Error(
"Per-agent Microsoft Teams meeting routing requires a Gateway-hosted agent run.",
);
}
runtime ??= new TeamsMeetingsRuntime({
return useRuntime ? api.runtime : undefined;
},
unknownActionMessage: "unknown teams_meetings action",
toolName: "teams_meetings",
toolLabel: "Microsoft Teams meetings",
toolDescription:
"Join and manage Microsoft Teams meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
toolParameters: TeamsMeetingsToolSchema,
createRuntime: ({ api, config }) =>
new TeamsMeetingsRuntime({
config,
fullConfig: api.config,
runtime: api.runtime,
logger: api.logger,
});
return runtime;
};
const sendError = (
respond: GatewayRequestHandlerOptions["respond"],
error: unknown,
code: Parameters<typeof errorShape>[0] = ErrorCodes.UNAVAILABLE,
) => {
const payload = { error: formatErrorMessage(error) };
respond(false, payload, errorShape(code, payload.error, { details: payload }));
};
const sendRequestError = (respond: GatewayRequestHandlerOptions["respond"], error: unknown) =>
sendError(
respond,
error,
error instanceof TeamsMeetingsInvalidRequestError
? ErrorCodes.INVALID_REQUEST
: ErrorCodes.UNAVAILABLE,
);
api.registerGatewayMethod(
"teamsmeetings.join",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = keepTrustedToolAgentId(asRecord(params), client);
respond(true, await (await ensureRuntime()).join(joinRequest(raw)));
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"teamsmeetings.leave",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const agentId = trustedToolAgentId(raw, client);
const sessionId = requireString(raw.sessionId, "sessionId");
const rt = await ensureRuntime();
respond(
true,
agentId && !rt.ownsSession(agentId, sessionId)
? { found: false }
: await rt.leave(sessionId),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"teamsmeetings.status",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const agentId = trustedToolAgentId(raw, client);
const rt = await ensureRuntime();
respond(
true,
agentId
? await rt.statusForAgent(agentId, normalizeOptionalString(raw.sessionId))
: await rt.status(normalizeOptionalString(raw.sessionId)),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"teamsmeetings.transcript",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const sessionId = requireString(raw.sessionId, "sessionId");
const sinceIndex = readSinceIndex(raw);
const agentId = trustedToolAgentId(raw, client);
const rt = await ensureRuntime();
respond(
true,
agentId && !rt.ownsSession(agentId, sessionId)
? { found: false }
: await rt.transcript(sessionId, sinceIndex === undefined ? {} : { sinceIndex }),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"teamsmeetings.speak",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const sessionId = requireString(raw.sessionId, "sessionId");
const agentId = trustedToolAgentId(raw, client);
const rt = await ensureRuntime();
respond(
true,
agentId && !rt.ownsSession(agentId, sessionId)
? { found: false, spoken: false }
: await rt.speak(sessionId, normalizeOptionalString(raw.message)),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"teamsmeetings.setup",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
respond(
true,
await (
await ensureRuntime()
).setupStatus({
mode: normalizeMode(params?.mode),
transport: normalizeTransport(params?.transport),
}),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
for (const [method, run] of [
[
"teamsmeetings.testSpeech",
(rt: TeamsMeetingsRuntime, raw: Record<string, unknown>) =>
rt.testSpeech(joinRequest(raw, { allowTimeout: true })),
],
[
"teamsmeetings.testListen",
(rt: TeamsMeetingsRuntime, raw: Record<string, unknown>) =>
rt.testListen(joinRequest(raw, { allowTimeout: true })),
],
] as const) {
api.registerGatewayMethod(
method,
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = keepTrustedToolAgentId(asRecord(params), client);
respond(true, await run(await ensureRuntime(), raw));
} catch (error) {
sendRequestError(respond, error);
}
},
);
}
api.registerTool(
(toolContext) => ({
name: "teams_meetings",
label: "Microsoft Teams meetings",
description:
"Join and manage Microsoft Teams meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
parameters: TeamsMeetingsToolSchema,
async execute(_toolCallId, params) {
const raw = asRecord(params);
const action = raw.action as ToolAction;
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
const contextAgentId =
toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : undefined;
try {
if (!(["join", "leave", "status", "transcript", "speak"] as const).includes(action)) {
throw new Error("unknown teams_meetings action");
}
const trustedRouting = Boolean(agentId && agentId !== "main");
const useRuntime = trustedRouting ? await api.runtime.gateway.isAvailable() : false;
if (trustedRouting && !useRuntime) {
throw new Error(
"Per-agent Microsoft Teams meeting routing requires a Gateway-hosted agent run.",
);
}
return json(
await callGatewayFromTool({
action,
config,
raw: {
...raw,
...(requesterSessionKey ? { requesterSessionKey } : {}),
...(useRuntime ? { agentId } : {}),
},
runtime: useRuntime ? api.runtime : undefined,
}),
);
} catch (error) {
return json({ error: formatErrorMessage(error) });
}
},
}),
{ name: "teams_meetings" },
);
api.registerNodeHostCommand({
command: TEAMS_MEETINGS_NODE_COMMAND,
cap: "teams-meetings",
dangerous: true,
handle: handleTeamsMeetingsNodeHostCommand,
});
api.registerNodeInvokePolicy(createTeamsMeetingsNodeInvokePolicy(config));
api.registerCli(
async ({ program }) => {
const cli = await loadTeamsMeetingsCli();
cli.registerTeamsMeetingsCli({ program, config });
},
{
commands: ["teamsmeetings"],
descriptors: [
{
name: "teamsmeetings",
description: "Join and manage Microsoft Teams meeting guests",
hasSubcommands: true,
},
],
},
);
},
});
nodeCommand: TEAMS_MEETINGS_NODE_COMMAND,
cap: "teams-meetings",
nodeHandler: handleTeamsMeetingsNodeHostCommand,
createNodePolicy: createTeamsMeetingsNodeInvokePolicy,
registerNodeWhen: () => true,
registerCli: (api, config) => {
api.registerCli(
async ({ program }) => {
const cli = await loadTeamsMeetingsCli();
cli.registerTeamsMeetingsCli({ program, config });
},
{
commands: ["teamsmeetings"],
descriptors: [
{
name: "teamsmeetings",
description: "Join and manage Microsoft Teams meeting guests",
hasSubcommands: true,
},
],
},
);
},
}),
);

View File

@@ -1,34 +1,18 @@
import {
readNonNegativeIntegerParam,
readPositiveIntegerParam,
} from "openclaw/plugin-sdk/channel-actions";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
callGatewayFromCli,
ErrorCodes,
errorShape,
type GatewayRequestHandlerOptions,
} from "openclaw/plugin-sdk/gateway-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { jsonResult as json } from "openclaw/plugin-sdk/tool-results";
import { MeetingPlatformAdapter } from "openclaw/plugin-sdk/meeting-runtime";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { Type } from "typebox";
import {
resolveZoomMeetingsConfig,
resolveZoomMeetingsGatewayOperationTimeoutMs,
type ZoomMeetingsConfig,
type ZoomMeetingsMode,
type ZoomMeetingsTransport,
} from "./src/config.js";
import {
ZoomMeetingsInvalidRequestError,
zoomMeetingsInvalidRequest as invalidRequest,
} from "./src/errors.js";
import { ZoomMeetingsInvalidRequestError, zoomMeetingsInvalidRequest } from "./src/errors.js";
import { handleZoomMeetingsNodeHostCommand } from "./src/node-host.js";
import { createZoomMeetingsNodeInvokePolicy } from "./src/node-invoke-policy.js";
import { ZoomMeetingsRuntime } from "./src/runtime.js";
import type { ZoomMeetingsJoinRequest } from "./src/transports/types.js";
import { ZOOM_MEETINGS_NODE_COMMAND } from "./src/transports/zoom-meetings-platform-constants.js";
import { normalizeZoomMeetingUrl } from "./src/transports/zoom-meetings-urls.js";
@@ -75,382 +59,66 @@ const ZoomMeetingsToolSchema = Type.Object({
message: Type.Optional(Type.String({ description: "Instructions to speak" })),
});
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function normalizeTransport(value: unknown): ZoomMeetingsTransport | undefined {
if (value === undefined) {
return undefined;
}
if (value === "chrome" || value === "chrome-node") {
return value;
}
throw invalidRequest("transport must be chrome or chrome-node");
}
function normalizeMode(value: unknown): ZoomMeetingsMode | undefined {
if (value === undefined) {
return undefined;
}
if (value === "agent" || value === "bidi" || value === "transcribe") {
return value;
}
throw invalidRequest("mode must be agent, bidi, or transcribe");
}
function requireString(value: unknown, name: string): string {
const normalized = normalizeOptionalString(value);
if (!normalized) {
throw invalidRequest(`${name} required`);
}
return normalized;
}
function readSinceIndex(raw: Record<string, unknown>): number | undefined {
try {
return readNonNegativeIntegerParam(raw, "sinceIndex");
} catch (error) {
throw invalidRequest(formatErrorMessage(error));
}
}
function keepTrustedToolContext(
raw: Record<string, unknown>,
client: GatewayRequestHandlerOptions["client"],
): Record<string, unknown> {
const { agentId: rawAgentId, requesterSessionKey: rawRequesterSessionKey, ...rest } = raw;
if (client?.internal?.pluginRuntimeOwnerId !== "zoom-meetings") {
return rest;
}
const agentId = normalizeOptionalString(rawAgentId);
const requesterSessionKey = normalizeOptionalString(rawRequesterSessionKey);
return {
...rest,
...(agentId ? { agentId } : {}),
...(requesterSessionKey ? { requesterSessionKey } : {}),
};
}
function trustedToolAgentId(
raw: Record<string, unknown>,
client: GatewayRequestHandlerOptions["client"],
): string | undefined {
return normalizeOptionalString(keepTrustedToolContext(raw, client).agentId);
}
function joinRequest(raw: Record<string, unknown>, options?: { allowTimeout?: boolean }) {
if (!options?.allowTimeout && raw.timeoutMs !== undefined) {
throw invalidRequest("timeoutMs is supported only by testSpeech or testListen");
}
let url: string;
let timeoutMs: number | undefined;
try {
url = normalizeZoomMeetingUrl(requireString(raw.url, "url"));
timeoutMs = readPositiveIntegerParam(raw, "timeoutMs");
} catch (error) {
if (error instanceof ZoomMeetingsInvalidRequestError) {
throw error;
}
throw invalidRequest(formatErrorMessage(error));
}
return {
url,
transport: normalizeTransport(raw.transport),
mode: normalizeMode(raw.mode),
message: normalizeOptionalString(raw.message),
requesterSessionKey: normalizeOptionalString(raw.requesterSessionKey),
agentId: normalizeOptionalString(raw.agentId),
timeoutMs,
};
}
type ToolAction = "join" | "leave" | "status" | "transcript" | "speak";
function gatewayMethod(action: ToolAction): string {
return `zoommeetings.${action}`;
}
function readErrorDetails(error: unknown): unknown {
return error && typeof error === "object" && "details" in error
? (error as { details?: unknown }).details
: undefined;
}
async function callGatewayFromTool(params: {
action: ToolAction;
config: ZoomMeetingsConfig;
raw: Record<string, unknown>;
runtime?: OpenClawPluginApi["runtime"];
}) {
try {
if (params.runtime) {
return await params.runtime.gateway.request(gatewayMethod(params.action), params.raw, {
timeoutMs: resolveZoomMeetingsGatewayOperationTimeoutMs(params.config),
scopes: ["operator.admin"],
});
}
return await callGatewayFromCli(
gatewayMethod(params.action),
{
json: true,
timeout: String(resolveZoomMeetingsGatewayOperationTimeoutMs(params.config)),
},
params.raw,
{ progress: false, scopes: ["operator.admin"] },
);
} catch (error) {
const details = readErrorDetails(error);
if (details && typeof details === "object") {
return details;
}
throw error;
}
}
export default definePluginEntry({
id: "zoom-meetings",
name: "Zoom meetings",
description: "Join Zoom meetings as a Chrome browser guest",
configSchema: zoomMeetingsConfigSchema,
register(api: OpenClawPluginApi) {
const config = zoomMeetingsConfigSchema.parse(api.pluginConfig);
let runtime: ZoomMeetingsRuntime | undefined;
const ensureRuntime = async () => {
if (!config.enabled) {
throw new Error("Zoom meetings plugin disabled in plugin config");
export default definePluginEntry(
MeetingPlatformAdapter.createPluginEntry<
ZoomMeetingsConfig,
ZoomMeetingsJoinRequest,
ZoomMeetingsRuntime
>({
id: "zoom-meetings",
name: "Zoom meetings",
description: "Join Zoom meetings as a Chrome browser guest",
configSchema: zoomMeetingsConfigSchema,
disabledMessage: "Zoom meetings plugin disabled in plugin config",
gatewayMethodPrefix: "zoommeetings",
invalidRequest: zoomMeetingsInvalidRequest,
isInvalidRequest: (error) => error instanceof ZoomMeetingsInvalidRequestError,
normalizeUrl: normalizeZoomMeetingUrl,
resolveGatewayTimeoutMs: resolveZoomMeetingsGatewayOperationTimeoutMs,
normalizeRequesterSessionKey: (value, trustedOwner) =>
trustedOwner && typeof value === "string" && value.trim() ? value.trim() : undefined,
normalizeToolAgentId: (agentId) => normalizeAgentId(agentId),
resolveToolRuntime: async (api) => {
if (!(await api.runtime.gateway.isAvailable())) {
throw new Error("Zoom meeting tools require a Gateway-hosted agent run.");
}
runtime ??= new ZoomMeetingsRuntime({
return api.runtime;
},
unknownActionMessage: "unknown zoom_meetings action",
toolName: "zoom_meetings",
toolLabel: "Zoom meetings",
toolDescription:
"Join and manage Zoom meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
toolParameters: ZoomMeetingsToolSchema,
createRuntime: ({ api, config }) =>
new ZoomMeetingsRuntime({
config,
fullConfig: api.config,
runtime: api.runtime,
logger: api.logger,
});
return runtime;
};
const sendError = (
respond: GatewayRequestHandlerOptions["respond"],
error: unknown,
code: Parameters<typeof errorShape>[0] = ErrorCodes.UNAVAILABLE,
) => {
const payload = { error: formatErrorMessage(error) };
respond(false, payload, errorShape(code, payload.error, { details: payload }));
};
const sendRequestError = (respond: GatewayRequestHandlerOptions["respond"], error: unknown) =>
sendError(
respond,
error,
error instanceof ZoomMeetingsInvalidRequestError
? ErrorCodes.INVALID_REQUEST
: ErrorCodes.UNAVAILABLE,
);
api.registerGatewayMethod(
"zoommeetings.join",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = keepTrustedToolContext(asRecord(params), client);
respond(true, await (await ensureRuntime()).join(joinRequest(raw)));
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"zoommeetings.leave",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const agentId = trustedToolAgentId(raw, client);
const sessionId = requireString(raw.sessionId, "sessionId");
const rt = await ensureRuntime();
respond(
true,
agentId && !rt.ownsSession(agentId, sessionId)
? { found: false }
: await rt.leave(sessionId),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"zoommeetings.status",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const agentId = trustedToolAgentId(raw, client);
const rt = await ensureRuntime();
respond(
true,
agentId
? await rt.statusForAgent(agentId, normalizeOptionalString(raw.sessionId))
: await rt.status(normalizeOptionalString(raw.sessionId)),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"zoommeetings.transcript",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const sessionId = requireString(raw.sessionId, "sessionId");
const sinceIndex = readSinceIndex(raw);
const agentId = trustedToolAgentId(raw, client);
const rt = await ensureRuntime();
respond(
true,
agentId && !rt.ownsSession(agentId, sessionId)
? { found: false }
: await rt.transcript(sessionId, sinceIndex === undefined ? {} : { sinceIndex }),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"zoommeetings.speak",
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = asRecord(params);
const sessionId = requireString(raw.sessionId, "sessionId");
const agentId = trustedToolAgentId(raw, client);
const rt = await ensureRuntime();
respond(
true,
agentId && !rt.ownsSession(agentId, sessionId)
? { found: false, spoken: false }
: await rt.speak(sessionId, normalizeOptionalString(raw.message)),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
api.registerGatewayMethod(
"zoommeetings.setup",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
respond(
true,
await (
await ensureRuntime()
).setupStatus({
mode: normalizeMode(params?.mode),
transport: normalizeTransport(params?.transport),
}),
);
} catch (error) {
sendRequestError(respond, error);
}
},
);
for (const [method, run] of [
[
"zoommeetings.testSpeech",
(rt: ZoomMeetingsRuntime, raw: Record<string, unknown>) =>
rt.testSpeech(joinRequest(raw, { allowTimeout: true })),
],
[
"zoommeetings.testListen",
(rt: ZoomMeetingsRuntime, raw: Record<string, unknown>) =>
rt.testListen(joinRequest(raw, { allowTimeout: true })),
],
] as const) {
api.registerGatewayMethod(
method,
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const raw = keepTrustedToolContext(asRecord(params), client);
respond(true, await run(await ensureRuntime(), raw));
} catch (error) {
sendRequestError(respond, error);
}
},
);
}
api.registerTool(
(toolContext) => ({
name: "zoom_meetings",
label: "Zoom meetings",
description:
"Join and manage Zoom meeting browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.",
parameters: ZoomMeetingsToolSchema,
async execute(_toolCallId, params) {
const raw = asRecord(params);
const action = raw.action as ToolAction;
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
const contextAgentId =
toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
const agentId = normalizeAgentId(contextAgentId);
try {
if (!(["join", "leave", "status", "transcript", "speak"] as const).includes(action)) {
throw new Error("unknown zoom_meetings action");
}
const useRuntime = await api.runtime.gateway.isAvailable();
if (!useRuntime) {
throw new Error("Zoom meeting tools require a Gateway-hosted agent run.");
}
return json(
await callGatewayFromTool({
action,
config,
raw: {
...raw,
...(requesterSessionKey ? { requesterSessionKey } : {}),
agentId,
},
runtime: api.runtime,
}),
);
} catch (error) {
return json({ error: formatErrorMessage(error) });
}
},
}),
{ name: "zoom_meetings" },
);
if (config.enabled) {
api.registerNodeHostCommand({
command: ZOOM_MEETINGS_NODE_COMMAND,
cap: "zoom-meetings",
dangerous: true,
handle: handleZoomMeetingsNodeHostCommand,
});
api.registerNodeInvokePolicy(createZoomMeetingsNodeInvokePolicy(config));
}
api.registerCli(
async ({ program }) => {
const cli = await loadZoomMeetingsCli();
cli.registerZoomMeetingsCli({ program, config });
},
{
commands: ["zoommeetings"],
descriptors: [
{
name: "zoommeetings",
description: "Join and manage Zoom meeting guests",
hasSubcommands: true,
},
],
},
);
},
});
nodeCommand: ZOOM_MEETINGS_NODE_COMMAND,
cap: "zoom-meetings",
nodeHandler: handleZoomMeetingsNodeHostCommand,
createNodePolicy: createZoomMeetingsNodeInvokePolicy,
registerNodeWhen: (config) => config.enabled,
registerCli: (api, config) => {
api.registerCli(
async ({ program }) => {
const cli = await loadZoomMeetingsCli();
cli.registerZoomMeetingsCli({ program, config });
},
{
commands: ["zoommeetings"],
descriptors: [
{
name: "zoommeetings",
description: "Join and manage Zoom meeting guests",
hasSubcommands: true,
},
],
},
);
},
}),
);