mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:31:00 +00:00
Add Google Meet space access controls
This commit is contained in:
committed by
Peter Steinberger
parent
53c4217110
commit
f2c1a56bbd
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Plugins/beta: prepare diagnostics OpenTelemetry, Discord, Diffs, Lobster, Memory LanceDB, Microsoft Teams, QQ Bot, Voice Call, and WhatsApp for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
|
- Plugins/beta: prepare diagnostics OpenTelemetry, Discord, Diffs, Lobster, Memory LanceDB, Microsoft Teams, QQ Bot, Voice Call, and WhatsApp for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
|
||||||
- Plugins/beta: prepare Brave, Codex, Feishu, Synology Chat, Tlon, and Twitch for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
|
- Plugins/beta: prepare Brave, Codex, Feishu, Synology Chat, Tlon, and Twitch for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
|
||||||
- Providers/xAI: add Grok 4.3 to the bundled catalog and make it the default xAI chat model.
|
- Providers/xAI: add Grok 4.3 to the bundled catalog and make it the default xAI chat model.
|
||||||
|
- Google Meet: let API-created rooms set `accessType` and `entryPointAccess`, and add `googlemeet end-active-conference` for closing managed spaces after a call. (#74824) Thanks @BsnizND.
|
||||||
- Plugins/ClawHub: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verifying the ClawPack response header and downloaded bytes before installing. Thanks @vincentkoc.
|
- Plugins/ClawHub: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verifying the ClawPack response header and downloaded bytes before installing. Thanks @vincentkoc.
|
||||||
- Plugins/ClawHub: persist ClawPack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc.
|
- Plugins/ClawHub: persist ClawPack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc.
|
||||||
- Plugins/ClawHub: allow official bundled-plugin cutovers to prefer ClawHub installs with npm fallback only when the ClawHub package or version is absent. Thanks @vincentkoc.
|
- Plugins/ClawHub: allow official bundled-plugin cutovers to prefer ClawHub installs with npm fallback only when the ClawHub package or version is absent. Thanks @vincentkoc.
|
||||||
|
|||||||
@@ -125,6 +125,24 @@ Create a new meeting and join it:
|
|||||||
openclaw googlemeet create --transport chrome-node --mode realtime
|
openclaw googlemeet create --transport chrome-node --mode realtime
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For API-created rooms, use Google Meet `SpaceConfig.accessType` when you want
|
||||||
|
the room's no-knock policy to be explicit instead of inherited from the Google
|
||||||
|
account defaults:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw googlemeet create --access-type OPEN --transport chrome-node --mode realtime
|
||||||
|
```
|
||||||
|
|
||||||
|
`OPEN` lets anyone with the Meet URL join without knocking. `TRUSTED` lets the
|
||||||
|
host organization's trusted users, invited external users, and dial-in users
|
||||||
|
join without knocking. `RESTRICTED` limits no-knock entry to invitees. These
|
||||||
|
settings only apply to the official Google Meet API creation path, so OAuth
|
||||||
|
credentials must be configured.
|
||||||
|
|
||||||
|
If you authenticated Google Meet before this option was available, rerun
|
||||||
|
`openclaw googlemeet auth login --json` after adding the
|
||||||
|
`meetings.space.settings` scope to your Google OAuth consent screen.
|
||||||
|
|
||||||
Create only the URL without joining:
|
Create only the URL without joining:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -504,6 +522,7 @@ In Google Cloud Console:
|
|||||||
4. Add the scopes OpenClaw requests:
|
4. Add the scopes OpenClaw requests:
|
||||||
- `https://www.googleapis.com/auth/meetings.space.created`
|
- `https://www.googleapis.com/auth/meetings.space.created`
|
||||||
- `https://www.googleapis.com/auth/meetings.space.readonly`
|
- `https://www.googleapis.com/auth/meetings.space.readonly`
|
||||||
|
- `https://www.googleapis.com/auth/meetings.space.settings`
|
||||||
- `https://www.googleapis.com/auth/meetings.conference.media.readonly`
|
- `https://www.googleapis.com/auth/meetings.conference.media.readonly`
|
||||||
5. Create an OAuth client ID.
|
5. Create an OAuth client ID.
|
||||||
- Application type: **Web application**.
|
- Application type: **Web application**.
|
||||||
@@ -517,6 +536,8 @@ In Google Cloud Console:
|
|||||||
|
|
||||||
`meetings.space.created` is required by Google Meet `spaces.create`.
|
`meetings.space.created` is required by Google Meet `spaces.create`.
|
||||||
`meetings.space.readonly` lets OpenClaw resolve Meet URLs/codes to spaces.
|
`meetings.space.readonly` lets OpenClaw resolve Meet URLs/codes to spaces.
|
||||||
|
`meetings.space.settings` lets OpenClaw pass `SpaceConfig` settings such as
|
||||||
|
`accessType` during API room creation.
|
||||||
`meetings.conference.media.readonly` is for Meet Media API preflight and media
|
`meetings.conference.media.readonly` is for Meet Media API preflight and media
|
||||||
work; Google may require Developer Preview enrollment for actual Media API use.
|
work; Google may require Developer Preview enrollment for actual Media API use.
|
||||||
If you only need browser-based Chrome joins, skip OAuth entirely.
|
If you only need browser-based Chrome joins, skip OAuth entirely.
|
||||||
@@ -708,6 +729,21 @@ openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 --jso
|
|||||||
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
|
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
End an active conference for an API-created space when you want to close the
|
||||||
|
room after the call:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw googlemeet end-active-conference https://meet.google.com/abc-defg-hij
|
||||||
|
```
|
||||||
|
|
||||||
|
This calls Google Meet `spaces.endActiveConference` and requires OAuth with the
|
||||||
|
`meetings.space.created` scope for a space the authorized account can manage.
|
||||||
|
OpenClaw accepts a Meet URL, meeting code, or `spaces/{id}` input and resolves it
|
||||||
|
to the API space resource before ending the active conference.
|
||||||
|
It is separate from `googlemeet leave`: `leave` stops OpenClaw's local/session
|
||||||
|
participation, while `end-active-conference` asks Google Meet to end the active
|
||||||
|
conference for the space.
|
||||||
|
|
||||||
Write a readable report:
|
Write a readable report:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -764,6 +800,26 @@ Agents can also create the same bundle through the `google_meet` tool:
|
|||||||
|
|
||||||
Set `"dryRun": true` to return only the export manifest and skip file writes.
|
Set `"dryRun": true` to return only the export manifest and skip file writes.
|
||||||
|
|
||||||
|
Agents can also create an API-backed room with an explicit access policy:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "create",
|
||||||
|
"transport": "chrome-node",
|
||||||
|
"mode": "realtime",
|
||||||
|
"accessType": "OPEN"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And they can end the active conference for a known room:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "end_active_conference",
|
||||||
|
"meeting": "https://meet.google.com/abc-defg-hij"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Run the guarded live smoke against a real retained meeting:
|
Run the guarded live smoke against a real retained meeting:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1502,6 +1558,8 @@ argument list, and do not point it at scripts from untrusted locations.
|
|||||||
`googlemeet speak` triggers the active realtime audio bridge for a Chrome
|
`googlemeet speak` triggers the active realtime audio bridge for a Chrome
|
||||||
session. `googlemeet leave` stops that bridge. For Twilio sessions delegated
|
session. `googlemeet leave` stops that bridge. For Twilio sessions delegated
|
||||||
through the Voice Call plugin, `leave` also hangs up the underlying voice call.
|
through the Voice Call plugin, `leave` also hangs up the underlying voice call.
|
||||||
|
Use `googlemeet end-active-conference` when you also want to close the active
|
||||||
|
Google Meet conference for an API-managed space.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ describe("google-meet create flow", () => {
|
|||||||
googleMeetPluginTesting.setCallGatewayFromCliForTests();
|
googleMeetPluginTesting.setCallGatewayFromCliForTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("CLI create prints the new meeting URL", async () => {
|
it("CLI create can configure API-created space access", async () => {
|
||||||
const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
|
const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
|
||||||
const url = input instanceof Request ? input.url : input.toString();
|
const url = input instanceof Request ? input.url : input.toString();
|
||||||
if (url.includes("oauth2.googleapis.com")) {
|
if (url.includes("oauth2.googleapis.com")) {
|
||||||
@@ -142,9 +142,27 @@ describe("google-meet create flow", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await program.parseAsync(["googlemeet", "create", "--no-join"], { from: "user" });
|
await program.parseAsync(
|
||||||
|
[
|
||||||
|
"googlemeet",
|
||||||
|
"create",
|
||||||
|
"--no-join",
|
||||||
|
"--access-type",
|
||||||
|
"OPEN",
|
||||||
|
"--entry-point-access",
|
||||||
|
"ALL",
|
||||||
|
],
|
||||||
|
{ from: "user" },
|
||||||
|
);
|
||||||
expect(stdout.output()).toContain("meeting uri: https://meet.google.com/new-abcd-xyz");
|
expect(stdout.output()).toContain("meeting uri: https://meet.google.com/new-abcd-xyz");
|
||||||
expect(stdout.output()).toContain("space: spaces/new-space");
|
expect(stdout.output()).toContain("space: spaces/new-space");
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
"https://meet.googleapis.com/v2/spaces",
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ config: { accessType: "OPEN", entryPointAccess: "ALL" } }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
stdout.restore();
|
stdout.restore();
|
||||||
}
|
}
|
||||||
@@ -220,6 +238,27 @@ describe("google-meet create flow", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects access policy flags when tool create would use browser fallback", async () => {
|
||||||
|
const { methods } = setup(
|
||||||
|
{
|
||||||
|
defaultTransport: "chrome-node",
|
||||||
|
chromeNode: { node: "parallels-macos" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodesInvokeHandler: async () => {
|
||||||
|
throw new Error("browser fallback should not run");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.create", {
|
||||||
|
join: false,
|
||||||
|
accessType: "OPEN",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("access policy options require OAuth/API room creation");
|
||||||
|
});
|
||||||
|
|
||||||
it("reports structured manual action when browser creation needs Google login", async () => {
|
it("reports structured manual action when browser creation needs Google login", async () => {
|
||||||
const { methods } = setup(
|
const { methods } = setup(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -557,6 +557,7 @@ describe("google-meet plugin", () => {
|
|||||||
"export",
|
"export",
|
||||||
"recover_current_tab",
|
"recover_current_tab",
|
||||||
"leave",
|
"leave",
|
||||||
|
"end_active_conference",
|
||||||
"speak",
|
"speak",
|
||||||
"test_speech",
|
"test_speech",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "./src/config.js";
|
} from "./src/config.js";
|
||||||
import {
|
import {
|
||||||
buildGoogleMeetPreflightReport,
|
buildGoogleMeetPreflightReport,
|
||||||
|
endGoogleMeetActiveConference,
|
||||||
fetchGoogleMeetArtifacts,
|
fetchGoogleMeetArtifacts,
|
||||||
fetchGoogleMeetAttendance,
|
fetchGoogleMeetAttendance,
|
||||||
fetchLatestGoogleMeetConferenceRecord,
|
fetchLatestGoogleMeetConferenceRecord,
|
||||||
@@ -201,6 +202,7 @@ const GoogleMeetToolSchema = Type.Object({
|
|||||||
"export",
|
"export",
|
||||||
"recover_current_tab",
|
"recover_current_tab",
|
||||||
"leave",
|
"leave",
|
||||||
|
"end_active_conference",
|
||||||
"speak",
|
"speak",
|
||||||
"test_speech",
|
"test_speech",
|
||||||
],
|
],
|
||||||
@@ -212,6 +214,19 @@ const GoogleMeetToolSchema = Type.Object({
|
|||||||
description: "For action=create, set false to create the URL without joining.",
|
description: "For action=create, set false to create the URL without joining.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
accessType: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
enum: ["OPEN", "TRUSTED", "RESTRICTED"],
|
||||||
|
description:
|
||||||
|
"For action=create with Google Meet OAuth, configure who can join without knocking.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
entryPointAccess: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
enum: ["ALL", "CREATOR_APP_ONLY"],
|
||||||
|
description: "For action=create with Google Meet OAuth, configure allowed join entry points.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
url: Type.Optional(Type.String({ description: "Explicit https://meet.google.com/... URL" })),
|
url: Type.Optional(Type.String({ description: "Explicit https://meet.google.com/... URL" })),
|
||||||
transport: Type.Optional(
|
transport: Type.Optional(
|
||||||
Type.String({ enum: ["chrome", "chrome-node", "twilio"], description: "Join transport" }),
|
Type.String({ enum: ["chrome", "chrome-node", "twilio"], description: "Join transport" }),
|
||||||
@@ -343,6 +358,7 @@ type GoogleMeetGatewayToolAction =
|
|||||||
| "recover_current_tab"
|
| "recover_current_tab"
|
||||||
| "setup_status"
|
| "setup_status"
|
||||||
| "leave"
|
| "leave"
|
||||||
|
| "end_active_conference"
|
||||||
| "speak"
|
| "speak"
|
||||||
| "test_speech";
|
| "test_speech";
|
||||||
|
|
||||||
@@ -354,6 +370,8 @@ function googleMeetGatewayMethodForToolAction(action: GoogleMeetGatewayToolActio
|
|||||||
return "googlemeet.setup";
|
return "googlemeet.setup";
|
||||||
case "test_speech":
|
case "test_speech":
|
||||||
return "googlemeet.testSpeech";
|
return "googlemeet.testSpeech";
|
||||||
|
case "end_active_conference":
|
||||||
|
return "googlemeet.endActiveConference";
|
||||||
default:
|
default:
|
||||||
return `googlemeet.${action}`;
|
return `googlemeet.${action}`;
|
||||||
}
|
}
|
||||||
@@ -842,6 +860,25 @@ export default definePluginEntry({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
api.registerGatewayMethod(
|
||||||
|
"googlemeet.endActiveConference",
|
||||||
|
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||||
|
try {
|
||||||
|
const raw = asParamRecord(params);
|
||||||
|
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
await endGoogleMeetActiveConference({
|
||||||
|
accessToken: token.accessToken,
|
||||||
|
meeting: resolveMeetingInput(config, raw.meeting),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
sendError(respond, err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
api.registerGatewayMethod(
|
api.registerGatewayMethod(
|
||||||
"googlemeet.speak",
|
"googlemeet.speak",
|
||||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||||
@@ -999,6 +1036,15 @@ export default definePluginEntry({
|
|||||||
}
|
}
|
||||||
return json(await callGoogleMeetGatewayFromTool({ config, action: "leave", raw }));
|
return json(await callGoogleMeetGatewayFromTool({ config, action: "leave", raw }));
|
||||||
}
|
}
|
||||||
|
case "end_active_conference": {
|
||||||
|
return json(
|
||||||
|
await callGoogleMeetGatewayFromTool({
|
||||||
|
config,
|
||||||
|
action: "end_active_conference",
|
||||||
|
raw,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
case "speak": {
|
case "speak": {
|
||||||
const sessionId = normalizeOptionalString(raw.sessionId);
|
const sessionId = normalizeOptionalString(raw.sessionId);
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
|
|||||||
@@ -324,6 +324,64 @@ describe("google-meet CLI", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ends an active conference for a Meet space", async () => {
|
||||||
|
const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
|
||||||
|
const url = requestUrl(input);
|
||||||
|
if (url.pathname === "/v2/spaces/abc-defg-hij") {
|
||||||
|
return jsonResponse({
|
||||||
|
name: "spaces/space-resource-123",
|
||||||
|
meetingCode: "abc-defg-hij",
|
||||||
|
meetingUri: "https://meet.google.com/abc-defg-hij",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url.pathname === "/v2/spaces/space-resource-123:endActiveConference") {
|
||||||
|
return jsonResponse({});
|
||||||
|
}
|
||||||
|
return new Response("not found", { status: 404 });
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const stdout = captureStdout();
|
||||||
|
try {
|
||||||
|
await setupCli({}).parseAsync(
|
||||||
|
[
|
||||||
|
"googlemeet",
|
||||||
|
"end-active-conference",
|
||||||
|
"https://meet.google.com/abc-defg-hij",
|
||||||
|
"--access-token",
|
||||||
|
"token",
|
||||||
|
"--expires-at",
|
||||||
|
String(Date.now() + 120_000),
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
{ from: "user" },
|
||||||
|
);
|
||||||
|
expect(JSON.parse(stdout.output())).toMatchObject({
|
||||||
|
space: "spaces/space-resource-123",
|
||||||
|
ended: true,
|
||||||
|
tokenSource: "cached-access-token",
|
||||||
|
});
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
"https://meet.googleapis.com/v2/spaces/space-resource-123:endActiveConference",
|
||||||
|
expect.objectContaining({ method: "POST", body: "{}" }),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
stdout.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects access policy flags when create would use browser fallback", async () => {
|
||||||
|
await expect(
|
||||||
|
setupCli({
|
||||||
|
runtime: {
|
||||||
|
createViaBrowser: vi.fn(async () => {
|
||||||
|
throw new Error("browser fallback should not run");
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}).parseAsync(["googlemeet", "create", "--access-type", "OPEN"], { from: "user" }),
|
||||||
|
).rejects.toThrow("access policy options require OAuth/API room creation");
|
||||||
|
});
|
||||||
|
|
||||||
it("prints the latest conference record", async () => {
|
it("prints the latest conference record", async () => {
|
||||||
stubMeetArtifactsApi();
|
stubMeetArtifactsApi();
|
||||||
const stdout = captureStdout();
|
const stdout = captureStdout();
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import {
|
|||||||
type GoogleMeetCalendarLookupResult,
|
type GoogleMeetCalendarLookupResult,
|
||||||
} from "./calendar.js";
|
} from "./calendar.js";
|
||||||
import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
|
import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
|
||||||
|
import { hasCreateSpaceConfigInput, resolveCreateSpaceConfig } from "./create.js";
|
||||||
import {
|
import {
|
||||||
buildGoogleMeetPreflightReport,
|
buildGoogleMeetPreflightReport,
|
||||||
createGoogleMeetSpace,
|
createGoogleMeetSpace,
|
||||||
|
endGoogleMeetActiveConference,
|
||||||
fetchGoogleMeetArtifacts,
|
fetchGoogleMeetArtifacts,
|
||||||
fetchGoogleMeetAttendance,
|
fetchGoogleMeetAttendance,
|
||||||
fetchLatestGoogleMeetConferenceRecord,
|
fetchLatestGoogleMeetConferenceRecord,
|
||||||
@@ -159,6 +161,8 @@ type CreateOptions = {
|
|||||||
clientId?: string;
|
clientId?: string;
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
|
accessType?: string;
|
||||||
|
entryPointAccess?: string;
|
||||||
join?: boolean;
|
join?: boolean;
|
||||||
transport?: GoogleMeetTransport;
|
transport?: GoogleMeetTransport;
|
||||||
mode?: GoogleMeetMode;
|
mode?: GoogleMeetMode;
|
||||||
@@ -1367,6 +1371,14 @@ export function registerGoogleMeetCli(params: {
|
|||||||
.option("--client-id <id>", "OAuth client id override")
|
.option("--client-id <id>", "OAuth client id override")
|
||||||
.option("--client-secret <secret>", "OAuth client secret override")
|
.option("--client-secret <secret>", "OAuth client secret override")
|
||||||
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
||||||
|
.option(
|
||||||
|
"--access-type <type>",
|
||||||
|
"Google Meet SpaceConfig accessType for API create: OPEN, TRUSTED, or RESTRICTED",
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--entry-point-access <type>",
|
||||||
|
"Google Meet SpaceConfig entryPointAccess for API create: ALL or CREATOR_APP_ONLY",
|
||||||
|
)
|
||||||
.option("--no-join", "Only create the meeting URL; do not join it")
|
.option("--no-join", "Only create the meeting URL; do not join it")
|
||||||
.option("--transport <transport>", "Join transport: chrome, chrome-node, or twilio")
|
.option("--transport <transport>", "Join transport: chrome, chrome-node, or twilio")
|
||||||
.option(
|
.option(
|
||||||
@@ -1380,6 +1392,11 @@ export function registerGoogleMeetCli(params: {
|
|||||||
.option("--json", "Print JSON output", false)
|
.option("--json", "Print JSON output", false)
|
||||||
.action(async (options: CreateOptions) => {
|
.action(async (options: CreateOptions) => {
|
||||||
if (!hasCreateOAuth(params.config, options)) {
|
if (!hasCreateOAuth(params.config, options)) {
|
||||||
|
if (hasCreateSpaceConfigInput(options as Record<string, unknown>)) {
|
||||||
|
throw new Error(
|
||||||
|
"Google Meet access policy options require OAuth/API room creation. Configure Google Meet OAuth or remove --access-type/--entry-point-access.",
|
||||||
|
);
|
||||||
|
}
|
||||||
const rt = await params.ensureRuntime();
|
const rt = await params.ensureRuntime();
|
||||||
const result = await rt.createViaBrowser();
|
const result = await rt.createViaBrowser();
|
||||||
const join =
|
const join =
|
||||||
@@ -1423,7 +1440,10 @@ export function registerGoogleMeetCli(params: {
|
|||||||
const token = await resolveGoogleMeetAccessToken(
|
const token = await resolveGoogleMeetAccessToken(
|
||||||
resolveCreateTokenOptions(params.config, options),
|
resolveCreateTokenOptions(params.config, options),
|
||||||
);
|
);
|
||||||
const result = await createGoogleMeetSpace({ accessToken: token.accessToken });
|
const result = await createGoogleMeetSpace({
|
||||||
|
accessToken: token.accessToken,
|
||||||
|
config: resolveCreateSpaceConfig(options as Record<string, unknown>),
|
||||||
|
});
|
||||||
const join =
|
const join =
|
||||||
options.join !== false
|
options.join !== false
|
||||||
? await (
|
? await (
|
||||||
@@ -1463,6 +1483,39 @@ export function registerGoogleMeetCli(params: {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
root
|
||||||
|
.command("end-active-conference")
|
||||||
|
.description("End the active conference for a Google Meet space")
|
||||||
|
.argument("[meeting]", "Meet URL, meeting code, or spaces/{id}")
|
||||||
|
.option("--access-token <token>", "Access token override")
|
||||||
|
.option("--refresh-token <token>", "Refresh token override")
|
||||||
|
.option("--client-id <id>", "OAuth client id override")
|
||||||
|
.option("--client-secret <secret>", "OAuth client secret override")
|
||||||
|
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
||||||
|
.option("--json", "Print JSON output", false)
|
||||||
|
.action(async (meeting: string | undefined, options: ResolveSpaceOptions & JsonOptions) => {
|
||||||
|
const token = await resolveGoogleMeetAccessToken(
|
||||||
|
resolveOAuthTokenOptions(params.config, options),
|
||||||
|
);
|
||||||
|
const result = await endGoogleMeetActiveConference({
|
||||||
|
accessToken: token.accessToken,
|
||||||
|
meeting: resolveMeetingInput(params.config, meeting ?? options.meeting),
|
||||||
|
});
|
||||||
|
if (options.json) {
|
||||||
|
writeStdoutJson({
|
||||||
|
...result,
|
||||||
|
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeStdoutLine("space: %s", result.space);
|
||||||
|
writeStdoutLine("ended: yes");
|
||||||
|
writeStdoutLine(
|
||||||
|
"token source: %s",
|
||||||
|
token.refreshed ? "refresh-token" : "cached-access-token",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
root
|
root
|
||||||
.command("join")
|
.command("join")
|
||||||
.argument("[url]", "Explicit https://meet.google.com/... URL")
|
.argument("[url]", "Explicit https://meet.google.com/... URL")
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||||
import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
|
import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
|
||||||
import { createGoogleMeetSpace } from "./meet.js";
|
import {
|
||||||
|
createGoogleMeetSpace,
|
||||||
|
type GoogleMeetAccessType,
|
||||||
|
type GoogleMeetEntryPointAccess,
|
||||||
|
type GoogleMeetSpaceConfig,
|
||||||
|
} from "./meet.js";
|
||||||
import { resolveGoogleMeetAccessToken } from "./oauth.js";
|
import { resolveGoogleMeetAccessToken } from "./oauth.js";
|
||||||
import type { GoogleMeetRuntime } from "./runtime.js";
|
import type { GoogleMeetRuntime } from "./runtime.js";
|
||||||
import { createMeetWithBrowserProxyOnNode } from "./transports/chrome-create.js";
|
import { createMeetWithBrowserProxyOnNode } from "./transports/chrome-create.js";
|
||||||
@@ -14,6 +19,47 @@ function normalizeMode(value: unknown): GoogleMeetMode | undefined {
|
|||||||
return value === "realtime" || value === "transcribe" ? value : undefined;
|
return value === "realtime" || value === "transcribe" ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeGoogleMeetAccessType(value: unknown): GoogleMeetAccessType | undefined {
|
||||||
|
const normalized = normalizeOptionalString(value)?.toUpperCase().replaceAll("-", "_");
|
||||||
|
return normalized === "OPEN" || normalized === "TRUSTED" || normalized === "RESTRICTED"
|
||||||
|
? normalized
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeGoogleMeetEntryPointAccess(
|
||||||
|
value: unknown,
|
||||||
|
): GoogleMeetEntryPointAccess | undefined {
|
||||||
|
const normalized = normalizeOptionalString(value)?.toUpperCase().replaceAll("-", "_");
|
||||||
|
return normalized === "ALL" || normalized === "CREATOR_APP_ONLY" ? normalized : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCreateSpaceConfig(
|
||||||
|
raw: Record<string, unknown>,
|
||||||
|
): GoogleMeetSpaceConfig | undefined {
|
||||||
|
const rawAccessType = normalizeOptionalString(raw.accessType);
|
||||||
|
const rawEntryPointAccess = normalizeOptionalString(raw.entryPointAccess);
|
||||||
|
const accessType = normalizeGoogleMeetAccessType(raw.accessType);
|
||||||
|
const entryPointAccess = normalizeGoogleMeetEntryPointAccess(raw.entryPointAccess);
|
||||||
|
if (rawAccessType !== undefined && !accessType) {
|
||||||
|
throw new Error("Invalid Google Meet accessType. Expected OPEN, TRUSTED, or RESTRICTED.");
|
||||||
|
}
|
||||||
|
if (rawEntryPointAccess !== undefined && !entryPointAccess) {
|
||||||
|
throw new Error("Invalid Google Meet entryPointAccess. Expected ALL or CREATOR_APP_ONLY.");
|
||||||
|
}
|
||||||
|
const config = {
|
||||||
|
...(accessType ? { accessType } : {}),
|
||||||
|
...(entryPointAccess ? { entryPointAccess } : {}),
|
||||||
|
};
|
||||||
|
return Object.keys(config).length > 0 ? config : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCreateSpaceConfigInput(raw: Record<string, unknown>): boolean {
|
||||||
|
return (
|
||||||
|
normalizeOptionalString(raw.accessType) !== undefined ||
|
||||||
|
normalizeOptionalString(raw.entryPointAccess) !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function createSpaceFromParams(config: GoogleMeetConfig, raw: Record<string, unknown>) {
|
async function createSpaceFromParams(config: GoogleMeetConfig, raw: Record<string, unknown>) {
|
||||||
const token = await resolveGoogleMeetAccessToken({
|
const token = await resolveGoogleMeetAccessToken({
|
||||||
clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
|
clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
|
||||||
@@ -22,7 +68,10 @@ async function createSpaceFromParams(config: GoogleMeetConfig, raw: Record<strin
|
|||||||
accessToken: normalizeOptionalString(raw.accessToken) ?? config.oauth.accessToken,
|
accessToken: normalizeOptionalString(raw.accessToken) ?? config.oauth.accessToken,
|
||||||
expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : config.oauth.expiresAt,
|
expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : config.oauth.expiresAt,
|
||||||
});
|
});
|
||||||
const result = await createGoogleMeetSpace({ accessToken: token.accessToken });
|
const result = await createGoogleMeetSpace({
|
||||||
|
accessToken: token.accessToken,
|
||||||
|
config: resolveCreateSpaceConfig(raw),
|
||||||
|
});
|
||||||
return { source: "api" as const, token, ...result };
|
return { source: "api" as const, token, ...result };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +102,11 @@ export async function createMeetFromParams(params: {
|
|||||||
"URL-only creation was requested. Call google_meet with action=join and url=meetingUri to enter the meeting.",
|
"URL-only creation was requested. Call google_meet with action=join and url=meetingUri to enter the meeting.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (hasCreateSpaceConfigInput(params.raw)) {
|
||||||
|
throw new Error(
|
||||||
|
"Google Meet access policy options require OAuth/API room creation. Configure Google Meet OAuth or remove accessType/entryPointAccess.",
|
||||||
|
);
|
||||||
|
}
|
||||||
const browser = await createMeetWithBrowserProxyOnNode({
|
const browser = await createMeetWithBrowserProxyOnNode({
|
||||||
runtime: params.runtime,
|
runtime: params.runtime,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ const GOOGLE_MEET_API_HOST = "meet.googleapis.com";
|
|||||||
const GOOGLE_MEET_MEDIA_SCOPE =
|
const GOOGLE_MEET_MEDIA_SCOPE =
|
||||||
"https://www.googleapis.com/auth/meetings.conference.media.readonly";
|
"https://www.googleapis.com/auth/meetings.conference.media.readonly";
|
||||||
const GOOGLE_MEET_SPACE_SCOPE = "https://www.googleapis.com/auth/meetings.space.readonly";
|
const GOOGLE_MEET_SPACE_SCOPE = "https://www.googleapis.com/auth/meetings.space.readonly";
|
||||||
|
const GOOGLE_MEET_SPACE_CREATED_SCOPE = "https://www.googleapis.com/auth/meetings.space.created";
|
||||||
|
const GOOGLE_MEET_SPACE_SETTINGS_SCOPE = "https://www.googleapis.com/auth/meetings.space.settings";
|
||||||
|
|
||||||
type GoogleMeetSpace = {
|
export type GoogleMeetAccessType = "OPEN" | "TRUSTED" | "RESTRICTED";
|
||||||
|
export type GoogleMeetEntryPointAccess = "ALL" | "CREATOR_APP_ONLY";
|
||||||
|
|
||||||
|
export type GoogleMeetSpaceConfig = {
|
||||||
|
accessType?: GoogleMeetAccessType;
|
||||||
|
entryPointAccess?: GoogleMeetEntryPointAccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GoogleMeetSpace = {
|
||||||
name: string;
|
name: string;
|
||||||
meetingCode?: string;
|
meetingCode?: string;
|
||||||
meetingUri?: string;
|
meetingUri?: string;
|
||||||
activeConference?: Record<string, unknown>;
|
activeConference?: Record<string, unknown>;
|
||||||
config?: Record<string, unknown>;
|
config?: GoogleMeetSpaceConfig & Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GoogleMeetPreflightReport = {
|
export type GoogleMeetPreflightReport = {
|
||||||
input: string;
|
input: string;
|
||||||
resolvedSpaceName: string;
|
resolvedSpaceName: string;
|
||||||
meetingCode?: string;
|
meetingCode?: string;
|
||||||
@@ -29,12 +39,17 @@ type GoogleMeetPreflightReport = {
|
|||||||
blockers: string[];
|
blockers: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GoogleMeetCreateSpaceResult = {
|
export type GoogleMeetCreateSpaceResult = {
|
||||||
space: GoogleMeetSpace;
|
space: GoogleMeetSpace;
|
||||||
meetingUri: string;
|
meetingUri: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GoogleMeetConferenceRecord = {
|
export type GoogleMeetEndActiveConferenceResult = {
|
||||||
|
space: string;
|
||||||
|
ended: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GoogleMeetConferenceRecord = {
|
||||||
name: string;
|
name: string;
|
||||||
space?: string;
|
space?: string;
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
@@ -353,7 +368,12 @@ export async function fetchGoogleMeetSpace(params: {
|
|||||||
|
|
||||||
export async function createGoogleMeetSpace(params: {
|
export async function createGoogleMeetSpace(params: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
config?: GoogleMeetSpaceConfig;
|
||||||
}): Promise<GoogleMeetCreateSpaceResult> {
|
}): Promise<GoogleMeetCreateSpaceResult> {
|
||||||
|
const body =
|
||||||
|
params.config && Object.keys(params.config).length > 0
|
||||||
|
? JSON.stringify({ config: params.config })
|
||||||
|
: "{}";
|
||||||
const { response, release } = await fetchWithSsrFGuard({
|
const { response, release } = await fetchWithSsrFGuard({
|
||||||
url: `${GOOGLE_MEET_API_BASE_URL}/spaces`,
|
url: `${GOOGLE_MEET_API_BASE_URL}/spaces`,
|
||||||
init: {
|
init: {
|
||||||
@@ -363,7 +383,7 @@ export async function createGoogleMeetSpace(params: {
|
|||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: "{}",
|
body,
|
||||||
},
|
},
|
||||||
policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
|
policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
|
||||||
auditContext: "google-meet.spaces.create",
|
auditContext: "google-meet.spaces.create",
|
||||||
@@ -375,7 +395,10 @@ export async function createGoogleMeetSpace(params: {
|
|||||||
response,
|
response,
|
||||||
detail,
|
detail,
|
||||||
prefix: "Google Meet spaces.create",
|
prefix: "Google Meet spaces.create",
|
||||||
scopes: ["https://www.googleapis.com/auth/meetings.space.created"],
|
scopes:
|
||||||
|
params.config && Object.keys(params.config).length > 0
|
||||||
|
? [GOOGLE_MEET_SPACE_CREATED_SCOPE, GOOGLE_MEET_SPACE_SETTINGS_SCOPE]
|
||||||
|
: [GOOGLE_MEET_SPACE_CREATED_SCOPE],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const payload = (await response.json()) as GoogleMeetSpace;
|
const payload = (await response.json()) as GoogleMeetSpace;
|
||||||
@@ -392,7 +415,46 @@ export async function createGoogleMeetSpace(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGoogleMeetConferenceRecord(params: {
|
export async function endGoogleMeetActiveConference(params: {
|
||||||
|
accessToken: string;
|
||||||
|
meeting: string;
|
||||||
|
}): Promise<GoogleMeetEndActiveConferenceResult> {
|
||||||
|
const resolved = await fetchGoogleMeetSpace({
|
||||||
|
accessToken: params.accessToken,
|
||||||
|
meeting: params.meeting,
|
||||||
|
});
|
||||||
|
const space = resolved.name;
|
||||||
|
const { response, release } = await fetchWithSsrFGuard({
|
||||||
|
url: `${GOOGLE_MEET_API_BASE_URL}/${encodeSpaceNameForPath(space)}:endActiveConference`,
|
||||||
|
init: {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${params.accessToken}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: "{}",
|
||||||
|
},
|
||||||
|
policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
|
||||||
|
auditContext: "google-meet.spaces.endActiveConference",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (!response.ok) {
|
||||||
|
const detail = await response.text();
|
||||||
|
throw await googleApiError({
|
||||||
|
response,
|
||||||
|
detail,
|
||||||
|
prefix: "Google Meet spaces.endActiveConference",
|
||||||
|
scopes: [GOOGLE_MEET_SPACE_CREATED_SCOPE],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { space, ended: true };
|
||||||
|
} finally {
|
||||||
|
await release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGoogleMeetConferenceRecord(params: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
conferenceRecord: string;
|
conferenceRecord: string;
|
||||||
}): Promise<GoogleMeetConferenceRecord> {
|
}): Promise<GoogleMeetConferenceRecord> {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const GOOGLE_MEET_TOKEN_HOST = "oauth2.googleapis.com";
|
|||||||
export const GOOGLE_MEET_SCOPES = [
|
export const GOOGLE_MEET_SCOPES = [
|
||||||
"https://www.googleapis.com/auth/meetings.space.created",
|
"https://www.googleapis.com/auth/meetings.space.created",
|
||||||
"https://www.googleapis.com/auth/meetings.space.readonly",
|
"https://www.googleapis.com/auth/meetings.space.readonly",
|
||||||
|
"https://www.googleapis.com/auth/meetings.space.settings",
|
||||||
"https://www.googleapis.com/auth/meetings.conference.media.readonly",
|
"https://www.googleapis.com/auth/meetings.conference.media.readonly",
|
||||||
"https://www.googleapis.com/auth/calendar.events.readonly",
|
"https://www.googleapis.com/auth/calendar.events.readonly",
|
||||||
"https://www.googleapis.com/auth/drive.meet.readonly",
|
"https://www.googleapis.com/auth/drive.meet.readonly",
|
||||||
|
|||||||
Reference in New Issue
Block a user